在 Enterprise Library 套件中,有一個功能區塊叫做 Data Access Application Block(DAAB),可協助開發人員寫出更彈性、一致的程式碼,並減少一些重複瑣碎的工作。本文將以範例說明 DAAB 的一些基本用法,以及在不使用 Entity Framework 或 NHibernate 等物件關聯對應框架的情形下,如何以傳統的 ADO.NET 資料存取技術搭配 DAAB 來實作簡易的資料存取物件(Data Access Objects)。
開發環境
在整理這篇筆記時,我是用 Visual Studio 2012 來寫範例程式。看起來,Enterprise Library 5.0 的組態設定工具目前尚未支援 Visual Studio 2012,不過沒關係,除了有些組態需要自己編輯,其他用法都和使用 Visual Studio 2010 時沒有兩樣。
這樣只會加入兩個組件:
接著,在應用程式組態檔中加入下列連線字串:
注意其中的 providerName 不可省略,原因稍後會解釋。
若無其他需求,組態檔裡面只要有連線字串就夠了,無須添加其他額外的組態設定。
查詢資料的範例程式碼
基本上就跟傳統 ADO.NET 寫法類似,也就是三步驟:
底下是一個使用 IDataReader 來查詢資料的範例:
這段程式碼會從 Northwind 資料庫中取出所有的客戶資料,並在螢幕上輸出每一名客戶的 CompanyName。
其中比較值得注意的是 DatabaseFactory.CreateDatabase("Northwind") 這行程式碼,傳入的字串參數就是組態檔中的連線字串名稱。如果呼叫此方法時不傳入任何參數,就表示要使用預設的資料庫連線字串。此時,應用程式組態檔中除了先前的連線字串,還必須加入額外區段:
建立好 Database 物件之後,再呼叫其 GetSqlStringCommand 方法來建立一個可下 SQL 命令的物件。如果要呼叫預儲程序,就用 GetStoredProcCommand。然後,仍然是利用 Database 的 ExecuteReader 方法來取得 IDataReader 物件,以便讀取資料。若要直接取得整個結果集,可用 ExecuteDataSet 方法。
呼叫預儲程序
底下的程式片段示範如何呼叫預儲程序,並取得該預儲程序之傳回值。
注意在呼叫 GetStoredProcCommand 方法時,傳入了預儲程序所需要的兩個輸入參數,但沒有輸出參數,因為輸出參數會幫你自動加進去,不用自行建立。這個自動建立的輸出參數名稱叫做 "@RETURN_VALUE",我們可以透過此參數來取得預儲程序的回傳值。
接著進一步說明 DatabaseFactory.CreateDatabase() 方法在背後做了些什麼,以及另一種建立 Database 物件的寫法(而且是官方建議的寫法)。
建立 Database 物件
DatabaseFactory.CreateDatabase() 方法內部會再去呼叫另一個 InnerCreateDatabase 方法來建立 Database 物件,其原始程式碼如下:
其中 EnterpriseLibraryContainer 提供了一個預設的 Unity 容器物件(可參考之前寫的 Unity 系列文章),並且讓我們可以透過其 Current 屬性來取得該容器。此 Current 屬性的型別是 IServiceLocator,由此亦可看出這裡是採用 service locator 的方式來動態決定欲建立之 Database 物件的實際型別。
當然,我們的應用程式也可以用 service locator 的方式來建立 Database 物件。只要將前面的範例程式稍微修改一下就行了:
現在我們知道有兩種方式可以建立 Database 物件:
按照 MSDN 網站上的說明:
建議採用的是 service locator 的寫法。這種寫法比較彈性,因為如果有一天我們不想要使用預設容器,只要把新的容器物件指定給 EnterpriseLibraryContainer.Current,就可以把它替換掉了。
其餘讀取資料的程式碼很簡單,一看就知道是建立在 ADO.NET 基礎之上,就不多解釋了.....等一下!怎麼沒有看到開啟和關閉資料庫連線的操作呢?
原因在於,Database 物件大部分的方法都會自動開啟和關閉資料庫連線,所以我們通常不用特別寫程式碼來開啟和關閉連線。比較特別的是 ExecuteReader 方法--它會傳回一個實作 IDataReader 介面的物件,讓我們透過此物件來進行後續的讀取資料的動作。由於還需要讀取資料,Database 物件當然不能立刻關閉連線,而且它也不知道用戶端何時會處理完。於是這關閉連線的工作便由底層的 IDataReader 實作類別來負責:當 IDataReader 物件關閉的時候,就會一併關閉資料庫連線。因此,我們必須確保在讀取完資料之後,呼叫 IDataReader 的 Close 方法,或者使用 C# 的 using 敘述來確保物件在摧毀時會去執行其 Dispose 方法( Dispose 會呼叫 Close)。
Database 物件的實際型別
透過前述方法所建立的 Database 物件,這個 Database 型別是個基礎型別,而其物件的真正型別則是由連線字串中的 providerName 參數來決定。所以前面有提到,組態檔中的連線字串一定得要指定 providerName 參數。底下列出幾種參數值所對應的實際物件型別:
SqlDatabase、GenericDatabase、和 OracleDatabase 都是繼承自基礎類別 Database。雖然我們也可以在程式中直接使用特定的子類別,但如果沒有特殊原因(例如需要使用資料庫提供的某些特殊資料型別),還是用 Database 這個基礎型別就好。
開發環境
- Enterprise Library version 5.0
- Visual Studio 2012
在整理這篇筆記時,我是用 Visual Studio 2012 來寫範例程式。看起來,Enterprise Library 5.0 的組態設定工具目前尚未支援 Visual Studio 2012,不過沒關係,除了有些組態需要自己編輯,其他用法都和使用 Visual Studio 2010 時沒有兩樣。
取得套件
透過 NuGet 來取得套件會比較方便。首先,建立一個 Console 應用程式專案,然後在 Solution Explorer 中的專案名稱上點右鍵,選 Manage NuGet Packages,再依下圖操作:
不透過 NuGet 也可以,但加入組件參考的手續比較麻煩。參考下圖:
這樣只會加入兩個組件:
如果要用到 Microsoft.Practice.ServiceLocation 和 Unity 等組件,就還得另外勾選,不像 NuGet 只要勾選兩項,該有的就都有了。
注意:欲使用 Data Access Application Block,應用程式專案的 target framework 不可以是 .NET Framework 4.0 Client Profile,而必須是 .NET Framework 4.0,否則編譯時會出現奇妙的錯誤訊息:
The type or namespace name 'Data' does not exist in the namespace 'Microsoft.Practices.EnterpriseLibrary' (are you missing an assembly reference?)
注意:欲使用 Data Access Application Block,應用程式專案的 target framework 不可以是 .NET Framework 4.0 Client Profile,而必須是 .NET Framework 4.0,否則編譯時會出現奇妙的錯誤訊息:
The type or namespace name 'Data' does not exist in the namespace 'Microsoft.Practices.EnterpriseLibrary' (are you missing an assembly reference?)
設定連線字串
接著,在應用程式組態檔中加入下列連線字串:
<connectionStrings> <add name="Northwind" connectionString="Server=localhost;Database=Northwind;uid=sa;pwd=" providerName="System.Data.SqlClient" /> </connectionStrings>
注意其中的 providerName 不可省略,原因稍後會解釋。
若無其他需求,組態檔裡面只要有連線字串就夠了,無須添加其他額外的組態設定。
查詢資料的範例程式碼
基本上就跟傳統 ADO.NET 寫法類似,也就是三步驟:
- 建立資料庫(連線)物件。
- 建立 command 物件。
- 執行 command。若是屬於會傳回結果集的命令,則利用傳回之 IDataReader 物件或 DataSet 物件來取得資料。
底下是一個使用 IDataReader 來查詢資料的範例:
using System.Data; using System.Data.Common; using Microsoft.Practices.EnterpriseLibrary.Data; .... static void DemoSqlStringCommand() { Database db = DatabaseFactory.CreateDatabase("Northwind"); DbCommand cmd = db.GetSqlStringCommand("select * from Customers"); using (IDataReader rdr = db.ExecuteReader(cmd)) { while (rdr.Read()) { Console.WriteLine(rdr["CompanyName"]); } } }
這段程式碼會從 Northwind 資料庫中取出所有的客戶資料,並在螢幕上輸出每一名客戶的 CompanyName。
其中比較值得注意的是 DatabaseFactory.CreateDatabase("Northwind") 這行程式碼,傳入的字串參數就是組態檔中的連線字串名稱。如果呼叫此方法時不傳入任何參數,就表示要使用預設的資料庫連線字串。此時,應用程式組態檔中除了先前的連線字串,還必須加入額外區段:
<configSections> <section name="dataConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data, Version=5.0..0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="true" /> </configSections> <dataConfiguration defaultDatabase="Northwind" />
建立好 Database 物件之後,再呼叫其 GetSqlStringCommand 方法來建立一個可下 SQL 命令的物件。如果要呼叫預儲程序,就用 GetStoredProcCommand。然後,仍然是利用 Database 的 ExecuteReader 方法來取得 IDataReader 物件,以便讀取資料。若要直接取得整個結果集,可用 ExecuteDataSet 方法。
呼叫預儲程序
底下的程式片段示範如何呼叫預儲程序,並取得該預儲程序之傳回值。
static void Main(string[] args) { string param1 = "A001"; string param2 = "Y"; Database db = DatabaseFactory.CreateDatabase("Northwind"); var cmd = db.GetStoredProcCommand("YourStoredProcName", param1, param2); db.ExecuteNonQuery(cmd); int retVal = (int)cmd.Parameters["@RETURN_VALUE"].Value; Console.WriteLine(retVal); }
注意在呼叫 GetStoredProcCommand 方法時,傳入了預儲程序所需要的兩個輸入參數,但沒有輸出參數,因為輸出參數會幫你自動加進去,不用自行建立。這個自動建立的輸出參數名稱叫做 "@RETURN_VALUE",我們可以透過此參數來取得預儲程序的回傳值。
接著進一步說明 DatabaseFactory.CreateDatabase() 方法在背後做了些什麼,以及另一種建立 Database 物件的寫法(而且是官方建議的寫法)。
建立 Database 物件
DatabaseFactory.CreateDatabase() 方法內部會再去呼叫另一個 InnerCreateDatabase 方法來建立 Database 物件,其原始程式碼如下:
private static Database InnerCreateDatabase(string name) { try { return EnterpriseLibraryContainer.Current.GetInstance<database>(name); } catch (ActivationException configurationException) { TryLogConfigurationError(configurationException, name); throw; } }
其中 EnterpriseLibraryContainer 提供了一個預設的 Unity 容器物件(可參考之前寫的 Unity 系列文章),並且讓我們可以透過其 Current 屬性來取得該容器。此 Current 屬性的型別是 IServiceLocator,由此亦可看出這裡是採用 service locator 的方式來動態決定欲建立之 Database 物件的實際型別。
當然,我們的應用程式也可以用 service locator 的方式來建立 Database 物件。只要將前面的範例程式稍微修改一下就行了:
.... using Microsoft.Practices.EnterpriseLibrary.Common.Configuration; .... static void DemoSqlStringCommand() { Database db = EnterpriseLibraryContainer.Current.GetInstance<Database>("Northwind"); .... }
現在我們知道有兩種方式可以建立 Database 物件:
- 使用物件工廠的靜態方法,也就是 DatabaseFactory.CreateBase()。
- 使用 service locator,也就是 EnterpriseLibraryContainer.Current.GetInstance<Database>()。
按照 MSDN 網站上的說明:
The legacy static facades and factories that were the default approach in versions of Enterprise Library prior to version 5.0 are still available, and continue to be supported for the purpose of backwards compatibility. However, new code should use either the service locator approach or the techniques for accessing the container directly, as described in previous sections of this topic.
建議採用的是 service locator 的寫法。這種寫法比較彈性,因為如果有一天我們不想要使用預設容器,只要把新的容器物件指定給 EnterpriseLibraryContainer.Current,就可以把它替換掉了。
其餘讀取資料的程式碼很簡單,一看就知道是建立在 ADO.NET 基礎之上,就不多解釋了.....等一下!怎麼沒有看到開啟和關閉資料庫連線的操作呢?
原因在於,Database 物件大部分的方法都會自動開啟和關閉資料庫連線,所以我們通常不用特別寫程式碼來開啟和關閉連線。比較特別的是 ExecuteReader 方法--它會傳回一個實作 IDataReader 介面的物件,讓我們透過此物件來進行後續的讀取資料的動作。由於還需要讀取資料,Database 物件當然不能立刻關閉連線,而且它也不知道用戶端何時會處理完。於是這關閉連線的工作便由底層的 IDataReader 實作類別來負責:當 IDataReader 物件關閉的時候,就會一併關閉資料庫連線。因此,我們必須確保在讀取完資料之後,呼叫 IDataReader 的 Close 方法,或者使用 C# 的 using 敘述來確保物件在摧毀時會去執行其 Dispose 方法( Dispose 會呼叫 Close)。
Database 物件的實際型別
透過前述方法所建立的 Database 物件,這個 Database 型別是個基礎型別,而其物件的真正型別則是由連線字串中的 providerName 參數來決定。所以前面有提到,組態檔中的連線字串一定得要指定 providerName 參數。底下列出幾種參數值所對應的實際物件型別:
- System.Data.SqlClient:SqlDatabase (內部其實是用 System.Data.SqlClient 中的類別)
- System.Data.OleDb:GenericDatabase
- System.Data.OracleClient:OracleDatabase
SqlDatabase、GenericDatabase、和 OracleDatabase 都是繼承自基礎類別 Database。雖然我們也可以在程式中直接使用特定的子類別,但如果沒有特殊原因(例如需要使用資料庫提供的某些特殊資料型別),還是用 Database 這個基礎型別就好。
管理交易
Database 物件會直接使用 ADO.NET 的 TransactionScope 類別來管理交易,所以交易處理的程式寫法並沒有什麼特別之處。只要知道這點就夠了:在 TransactionScope 物件管轄範圍內的程式碼區塊中的所有資料異動都會包在同一個交易裡。
範例:
接著要看的是 DAAB 對於我們在設計資料存取層的時候有哪些幫助。
資料存取層
在設計資料存取物件(Data Access Object;DAO)時,一個常見的作法是讓我們的 DAO 類別的建構子傳入一個 Connection 物件。使用 DAAB 時則改為傳入 Database 物件。底下是一個簡單的範例:
咦?不是 DAO 嗎?為什麼類別名稱叫 CustomerRepository?
之所以如此命名,是因為我的腦袋已經被 DAO 和 Repository 搞亂了希望偏向 Repository 模式來設計。其實我覺得 DAO 和 Repository 在觀念上蠻接近的,只是名稱不同而已。簡單地說,DAO 在實作時比較偏向資料存取層的觀點;Repository 則比較偏向領域模型,亦即純粹從領域物件的角度去看事情,避免有關聯式資料庫的影子(至少外在看不太出來)。
用戶端程式碼看起來會像這樣:
資料欄位與物件屬性對應
我在定義 Customer 類別時,是把屬性名稱都取得跟資料庫中對應的欄位名稱完全相同。像這種簡單的情形,用 DAAB 提供的預設 row mapper 就可以輕鬆解決。先前的 GetByID 方法可以改成這樣:
對照原本要寫一堆把欄位值塞給物件屬性的程式碼,現在只要兩行就寫完,真是輕鬆多了。這裡是使用 DAAB 的 MapBuilder 類別,它有個 BuildAllProperties 方法會根據外界傳入的型別來建立物件屬性與資料欄位之間的預設對應。
若要傳回物件集合,例如所有的客戶資料,我們可以再為 CustomerRepository 添加一個 GetAll 方法:
這裡同樣也是只用到預設對應。
小結
DAAB 還有些用法,這裡沒有提到,例如非同步存取、呼叫預儲程序時如何設定參數等等。在討論資料存取物件時所使用的範例是比較單純的情況,實際運用時,領域物件的屬性與資料欄位之間的對應往往會複雜一些,此時可能就得撰寫額外的程式碼來處理物件與底層資料庫之間的欄位對應和轉換。這部分可以參考 MSDN 網站上的文件:Building Output Mappers。
延伸閱讀
Database 物件會直接使用 ADO.NET 的 TransactionScope 類別來管理交易,所以交易處理的程式寫法並沒有什麼特別之處。只要知道這點就夠了:在 TransactionScope 物件管轄範圍內的程式碼區塊中的所有資料異動都會包在同一個交易裡。
範例:
using System.Transactions; .... static void DemoTransaction() { Database db = EnterpriseLibraryContainer.Current.GetInstance<database>("Northwind"); try { using (TransactionScope scope = new TransactionScope()) { DbCommand cmd1 = db.GetSqlStringCommand("update Customers set City='台北' where CustomerID='ALFKI'"); DbCommand cmd2 = db.GetSqlStringCommand("update Customers set City='高雄' where CustomerID='ANATR'"); int affectedRows = db.ExecuteNonQuery(cmd1); affectedRows += db.ExecuteNonQuery(cmd2); if (affectedRows > 0) { throw new TransactionAbortedException("程式故意放棄交易"); } scope.Complete(); Console.WriteLine("交易成功!"); } } catch (TransactionAbortedException ex) { Console.WriteLine("交易失敗: {0}", ex.Message); } catch (ApplicationException ex) { Console.WriteLine("應用程式錯誤: {0}", ex.Message); } }
接著要看的是 DAAB 對於我們在設計資料存取層的時候有哪些幫助。
資料存取層
在設計資料存取物件(Data Access Object;DAO)時,一個常見的作法是讓我們的 DAO 類別的建構子傳入一個 Connection 物件。使用 DAAB 時則改為傳入 Database 物件。底下是一個簡單的範例:
public class CustomerRepository { private Database db; public CustomerRepository(Database aDatabase) { db = aDatabase; } public Customer GetByID(string id) { string sql = String.Format("select * from Customers where CustomerID='{0}'", id); using (IDataReader reader = db.ExecuteReader(CommandType.Text, sql)) { if (reader.Read()) { Customer customer = new Customer(); // 把 reader 物件中的欄位值塞給 customer 物件的對應屬性 customer.CustomerID = reader["CustomerID"].ToString(); customer.CompanyName = reader["CompanyID"].ToString(); customer.Address = reader["Address"].ToString(); // Yada yada....(反正就是一堆把資料欄位值塞給物件屬性的程式碼) return customer; } return null; } } }
咦?不是 DAO 嗎?為什麼類別名稱叫 CustomerRepository?
之所以如此命名,是因為我
用戶端程式碼看起來會像這樣:
Database db = EnterpriseLibraryContainer.Current.GetInstance<Database>(); CustomerRepository customerRepository = new CustomerRepository(db); Customer customer = customerRepository.GetByID("ALFKI"); Console.WriteLine(customer.CompanyName);
CustomerRepository 的 GetByID 方法會傳回一個 Customer 型別的物件。Customer 是個單純的 Data Transfer Object,只有一堆屬性而已:
public class Customer { public string CustomerID { get; set; } public string CompanyName { get; set; } public string Address { get; set; } public string City { get; set; } public string Region { get; set; } }
在 GetByID 方法中,取得 IDataReader 物件之後,接著就是建立一個 Customer 物件,然後把 IDataReader 物件中的各欄位值逐一塞給 Customer 物件的對應屬性。這個部分主要是在處理底層資料欄位與物件屬性之間的對應,其實挺瑣碎的。接著看看 DAAB 可以在這個瑣碎的地方幫我們什麼。
資料欄位與物件屬性對應
我在定義 Customer 類別時,是把屬性名稱都取得跟資料庫中對應的欄位名稱完全相同。像這種簡單的情形,用 DAAB 提供的預設 row mapper 就可以輕鬆解決。先前的 GetByID 方法可以改成這樣:
public Customer GetByID(string id) { string sql = String.Format("select * from Customers where CustomerID='{0}'", id); using (IDataReader reader = db.ExecuteReader(CommandType.Text, sql)) { if (reader.Read()) { // 把 reader 物件中的欄位值塞給 cust 物件的對應屬性 IRowMapper<Customer> mapper = MapBuilder<Customer>.BuildAllProperties(); Customer customer = mapper.MapRow(reader); return customer; } return null; } }
對照原本要寫一堆把欄位值塞給物件屬性的程式碼,現在只要兩行就寫完,真是輕鬆多了。這裡是使用 DAAB 的 MapBuilder 類別,它有個 BuildAllProperties 方法會根據外界傳入的型別來建立物件屬性與資料欄位之間的預設對應。
若要傳回物件集合,例如所有的客戶資料,我們可以再為 CustomerRepository 添加一個 GetAll 方法:
public List<Customer> GetAll() { string sql = "select * from Customers"; IEnumerable<Customer> customers = db.ExecuteSqlStringAccessor<Customer>(sql); return customers.ToList<Customer>(); }
這裡同樣也是只用到預設對應。
小結
DAAB 還有些用法,這裡沒有提到,例如非同步存取、呼叫預儲程序時如何設定參數等等。在討論資料存取物件時所使用的範例是比較單純的情況,實際運用時,領域物件的屬性與資料欄位之間的對應往往會複雜一些,此時可能就得撰寫額外的程式碼來處理物件與底層資料庫之間的欄位對應和轉換。這部分可以參考 MSDN 網站上的文件:Building Output Mappers。
延伸閱讀
- Using Microsoft.Practices.EnterpriseLibrary.Data IResultSetMapper<T>
- Extending Enterprise Library 5 Data Access Part 1: Out-of-the-box Features
- Extending Enterprise Library 5 Data Access Part 2: Extensions
沒有留言: