編寫可移植資料訪問層發布日期: 8/25/2004 | 更新日期: 8/25/2004
Silvano Coriani
Microsoft Corporation
適用於:
Microsoft Visual Studio .NET 2003
Microsoft .NET Framework 1.1
ADO.NET
各種 RDBMS
摘要:瞭解如何編寫透明地使用不同資料來源(從 Microsoft Access 到 SQL Server 以及 Oracle RDBMS)的智能應用程式。
本頁內容
|
引言 |
|
使用通用資料存取方法 |
|
使用基本介面 |
|
編寫專門的資料訪問層 |
|
從其他層使用資料訪問類 |
|
一些可能的改進 |
|
結論 |
引言
在負責諮詢工作的過去 6 年中,我曾多次聽說關於資料訪問和操作方面的問題,它時刻困擾著使用者:“如何編寫應用程式,以便只需對其進行很少的改動或不進行改動即可使用資料庫伺服器 x、y 和 z?”由於知道資料訪問層仍然是現代應用程式的最關鍵區段,並且通常是經驗不足的開發人員的頭號敵人,因此我的第一反應始終是:根本辦不到!
面對著人們惶惶不安的面孔以及“使用 Microsoft 在 ADO 中提供的通用資料存取方法如何?”這樣的問題,我決定針對此問題提供更詳細的說明以及建議的解決方案。
問題在於,如果應用程式是較小的原型,或者如果並發使用者較少並且資料訪問邏輯比較簡單,那麼即使您選擇下面這些最簡單的方法,也不會遇到任何問題:使用 RAD 工具(如 Data Environment in Microsoft Visual Basic 6.0),或某些“一攬子”解決方案(如 ActiveX Data Control 和其他第三方組件),這些解決方案通常會隱藏應用程式與特定資料來源之間進行的複雜互動。然而,當使用者數量增加使得必須解決並行作業問題時,由於頻繁使用動態記錄集、伺服器端游標以及不必要的鎖定策略,導致出現許多效能問題。為達到使用者目標而必須對系統所做的設計和代碼更改將花費您大量的時間,因為您從開始時就沒有考慮過這一問題。
返回頁首
使用通用資料存取方法
在將 ADO 可靠地併入 MDAC(Microsoft Data Access Components 2.1 版)後,Microsoft 掀起了通用資料訪問的使用高潮。其主導思想是向開發人員展示,通過使用簡單的物件模型(“串連”、“命令”和“記錄集”),可以編寫出能夠與各種不同的資料來源(無論是關係資料來源還是非關係資料來源)連線應用程式程式。文檔(以及當時的大多數文章和樣本)中通常未曾提及的是,即使使用相同的資料訪問技術,各種資料來源的可程式化性和特徵也千差萬別。
其結果是,在需要從多個資料來源擷取資料的應用程式中,最簡單的方法是使用所有資料來源所提供的功能的“共同點”,但因此會失去使用資料來源特定選項的好處,即為訪問和操作各種 RDBMS 中的資訊提供最佳方法。
我對該方法始終存在的懷疑是,經過與我的客戶進行更詳細的分析後,我們通常一致認為與應用程式中處理顯示和商務邏輯的其他部分相比,與資料來源進行互動的只是應用程式很小的一部分。通過進行精心的模組化設計,可以將 RDBMS 特定代碼隔離在一些容易互換的模組中,從而避免對資料訪問使用“通用”方法。然而,我們可以使用非常特定的資料存取碼(根據資料來源的不同,使用預存程序、命令批處理和其他特性),而不觸及其他大多數應用程式代碼。這總是提醒大家:正確的設計是編寫可移植的有效代碼的關鍵。
ADO.NET 將一些重要的變化引入到資料訪問編碼領域,如專用 .NET 資料提供者這樣的概念。使用特定的提供者,可以繞過為數眾多但有時沒必要的一系列軟體介面和服務(它們是 OLE DB 和 ODBC 層在資料存取碼與資料庫伺服器之間插入的內容),從而以最佳方式串連到資料來源。但每個資料來源仍然存在不同的特徵和特性(具有不同的 SQL Dialect),且編寫高效的應用程式仍然必須使用這些特定特徵而不是“共同點”。從可移植性觀點看來,託管和非託管的資料訪問技術仍然非常類似。
除“利用資料來源的唯一特徵”外,編寫良好資料訪問層所必需的其他規則對每個資料來源通常都是相同的:
| • |
在可能的情況下使用串連池機制。 |
| • |
節約使用資料庫伺服器的有限資源。 |
| • |
注意網路的往返。 |
| • |
在適當的情況下,增強執行計畫的重複使用率並避免重複編譯。 |
| • |
使用適當的鎖定模型管理並發性。 |
從我使用模組化設計方法的個人經驗來看,整個應用程式中專用於處理特定資料來源的代碼量不會超過總量的 10%。顯而易見,這比僅僅更改設定檔中的連接字串更複雜,但我認為,這樣做會獲得效能收益,因此這是一個可接受的折衷辦法。
返回頁首
使用基本介面
此處的目標是使用抽象,並將特定於特殊資料來源的代碼封裝在類層中,從而使應用程式的其他部分獨立於後端資料庫伺服器或免受其影響。
.NET Framework 的物件導向這一特性將在該過程中為我們提供協助,使我們能夠選擇要使用的抽象層級。選項之一是使用每個 .NET 資料提供者都必須實現的基本介面(IDbConnection、IDbCommand、IDataReader 等)。另一個選項是建立一組類(資料訪問層),用於管理應用程式的所有資料訪問邏輯(例如,使用 CRUD 範例)。為檢查這兩種可能性,我們首先從基於 Northwind 資料庫的訂單輸入應用程式樣本入手,然後插入和檢索不同資料來源中的資訊。
資料提供者基本介面標識應用程式與資料來源進行互動通常所需的典型行為:
| • |
定義連接字串。 |
| • |
開啟和關閉與資料來源的物理串連。 |
| • |
定義命令和相關參數。 |
| • |
執行可以建立的不同種類的命令。
| • |
返回一組資料。 |
| • |
返回標量值。 |
| • |
對資料執行操作但不返回任何內容。 |
|
| • |
對返回的資料集提供只向前型訪問和唯讀型訪問。 |
| • |
定義使資料集與資料來源(資料配接器)的內容保持同步所需的一組操作。 |
但事實上,如果將檢索、插入、更新和刪除不同資料來源(使用不同的資料提供者)中的資訊所需的各種操作封裝在資料訪問層中,並且只公開基本介面的成員,則可以實現第一級抽象-至少從資料提供者的角度來看是這樣。讓我們看一看以下示範該設計思想的代碼:
using System;using System.Data;using System.Data.Common;using System.Data.SqlClient;using System.Data.OleDb;using System.Data.OracleClient;namespace DAL{ public enum DatabaseType { Access, SQLServer, Oracle // 任何其他資料來源類型 } public enum ParameterType { Integer, Char, VarChar // 定義公用參數類型集 } public class DataFactory { private DataFactory(){} public static IDbConnection CreateConnection (string ConnectionString, DatabaseType dbtype) { IDbConnection cnn; switch(dbtype) { case DatabaseType.Access: cnn = new OleDbConnection (ConnectionString); break; case DatabaseType.SQLServer: cnn = new SqlConnection (ConnectionString); break; case DatabaseType.Oracle: cnn = new OracleConnection (ConnectionString); break; default: cnn = new SqlConnection (ConnectionString); break; } return cnn; } public static IDbCommand CreateCommand (string CommandText, DatabaseType dbtype, IDbConnection cnn) { IDbCommand cmd; switch(dbtype) { case DatabaseType.Access: cmd = new OleDbCommand (CommandText, (OleDbConnection)cnn); break; case DatabaseType.SQLServer: cmd = new SqlCommand (CommandText, (SqlConnection)cnn); break; case DatabaseType.Oracle: cmd = new OracleCommand (CommandText, (OracleConnection)cnn); break; default: cmd = new SqlCommand (CommandText, (SqlConnection)cnn); break; } return cmd; } public static DbDataAdapter CreateAdapter (IDbCommand cmd, DatabaseType dbtype) { DbDataAdapter da; switch(dbtype) { case DatabaseType.Access: da = new OleDbDataAdapter ((OleDbCommand)cmd); break; case DatabaseType.SQLServer: da = new SqlDataAdapter ((SqlCommand)cmd); break; case DatabaseType.Oracle: da = new OracleDataAdapter ((OracleCommand)cmd); break; default: da = new SqlDataAdapter ((SqlCommand)cmd); break; } return da; } }}
該類的作用是嚮應用程式的較進階別隱藏與建立特定類型(來自特定的資料提供者)的執行個體有關的細節,應用程式現在可以使用通過基本介面公開的一般行為與資料來源進行互動。
讓我們瞭解一下如何從應用程式的其他部分使用該類:
using System;using System.Data;using System.Data.Common;using System.Configuration; namespace DAL{ public class CustomersData { public DataTable GetCustomers() { string ConnectionString = ConfigurationSettings.AppSettings ["ConnectionString"]; DatabaseType dbtype = (DatabaseType)Enum.Parse (typeof(DatabaseType), ConfigurationSettings.AppSettings ["DatabaseType"]); IDbConnection cnn = DataFactory.CreateConnection (ConnectionString,dbtype); string cmdString = "SELECT CustomerID" + ",CompanyName,ContactName FROM Customers"; IDbCommand cmd = DataFactory.CreateCommand( cmdString, dbtype,cnn); DbDataAdapter da = DataFactory.CreateAdapter(cmd,dbtype); DataTable dt = new DataTable("Customers"); da.Fill(dt); return dt; } public CustomersDS GetCustomerOrders(string CustomerID) { // 待定 return null; } public CustomersList GetCustomersByCountry (string CountryCode) { // 待定 return null; } public bool InsertCustomer() { // 待定 return false; } }}
在 CustomerData 類的 GetCustomers() 方法中,我們可以看到通過讀取設定檔中的資訊。可以使用 DataFactory 類通過特定連接字串建立 XxxConnection 執行個體,並編寫與基本資料來源沒有特定依賴性的其餘代碼部分。
與資料層互動的一個業務層類樣本看起來可能類似下面這樣:
using System;using System.Data; using DAL;namespace BLL{ public class Customers { public DataTable GetAllCustomers() { CustomersData cd = new CustomersData(); DataTable dt = cd.GetCustomers(); return dt; } public DataSet GetCustomerOrders() { // 待定 return null; } }}
這樣看來,此方法出現什麼問題了?此處的問題是,只有一個重要細節將代碼綁定到特定資料來源:命令字串的 SQL 文法!實際上,如果以這種方式編寫應用程式,則使其具有可移植性的唯一辦法是採用可以由任何資料來源解釋的基本 SQL 文法,但這樣可能會失去從特定資料來源的特定功能獲得好處的機會。如果應用程式只對資料進行很簡單和很標準的操作,並且如果您不希望使用特定資料來源中的進階功能(如 XML 支援),這可能是個小問題。但通常此方法將導致效能降低,因為您無法使用每個資料來源的最佳特性。
返回頁首
編寫專門的資料訪問層
因此,只使用基本介面不足以通過不同資料來源提供可接受層級的抽象。這種情況下,一個好的解決方案是提高此抽象的層級,即建立一組類(如 Customer、Order 等)來封裝特定資料提供者的使用,並通過與特定資料來源、類型化的“資料集”、對象集合等無關的資料結構與應用程式的其他層級交換資訊。
可以在特定程式集內部建立此層的專用類(為每個受支援的資料來源分別建立一個專用類),並可以在需要的情況下按照設定檔中的說明從應用程式載入它們。這樣,如果您希望嚮應用程式中添加全新的資料來源,唯一要做的事情是針對一組通用介面組中定義的“合約”實現一組新類。
讓我們看一個實際例子:如果希望將 Microsoft SQL Server 和 Microsoft Access 作為資料來源為其提供支援,則應該在 Microsoft Visual Studio .NET 中建立兩個不同項目,每個資料來源分別建立一個。
為 SQL Server 建立的項目將類似於如下所示:
using System;using System.Data;using System.Data.Common;using System.Data.SqlClient; using System.Configuration; using Common;namespace DAL{ public class CustomersData : IDbCustomers { public DataTable GetCustomers() { string ConnectionString = ConfigurationSettings.AppSettings ["ConnectionString"]; using (SqlConnection cnn = new SqlConnection (ConnectionString)) { string cmdString = "SELECT CustomerID," + "CompanyName,ContactName " + "FROM Customers"; SqlCommand cmd = new SqlCommand (cmdString, cnn); SqlDataAdapter da = new SqlDataAdapter(cmd); DataTable dt = new DataTable("Customers"); da.Fill(dt); return dt; } } public DataTable GetCustomerOrders(string CustomerID) { // 待定 return null; } public DataTable GetCustomersByCountry (string CountryCode) { // 待定 return null; } public bool InsertCustomer() { // 待定 return false; } }}
從 Microsoft Access 進行資料檢索的代碼類似於如下所示:
using System;using System.Data;using System.Data.Common;using System.Data.OleDb; using System.Configuration; using Common;namespace DAL{ public class CustomersData : IDbCustomers { public DataTable GetCustomers() { string ConnectionString = ConfigurationSettings.AppSettings ["ConnectionString"]; using (OleDbConnection cnn = new OleDbConnection (ConnectionString)) { string cmdString = "SELECT CustomerID," + "CompanyName,ContactName " + "FROM Customers"; OleDbCommand cmd = new OleDbCommand (cmdString, cnn); OleDbDataAdapter da = new OleDbDataAdapter(cmd); DataTable dt = new DataTable("Customers"); da.Fill(dt); return dt; } } public DataTable GetCustomerOrders(string CustomerID) { // 待定 return null; } public DataTable GetCustomersByCountry (string CountryCode) { // 待定 return null; } public bool InsertCustomer() { // 待定 return false; } }}
CustomersData 類實現 IdbCustomers 介面。需要支援新資料來源時,只能建立一個實現該介面的新類。
此類型的介面可以類似於如下所示:
using System;using System.Data; namespace Common{ public interface IDbCustomers { DataTable GetCustomers(); DataTable GetCustomerOrders(string CustomerID); DataTable GetCustomersByCountry(string CountryCode); bool InsertCustomer(); }}
我們可以建立專用程式集或共用組件來封裝這些資料訪問類,在第一種情況下,程式集載入程式將搜尋我們在 AppBase 檔案夾的設定檔內指定的程式集,或者使用典型探測規則在子目錄內進行搜尋。如果我們必須與其他應用程式共用這些類,則可以將這些程式集置於全域組件快取中。
返回頁首
從其他層使用資料訪問類
這兩個幾乎相同的 CustomersData 類包含在應用程式其餘部分將使用的兩個不同程式集內。通過下面的設定檔,我們現在可以指定要載入的程式集以及面向的資料來源。
可能的設定檔樣本將類似於如下所示:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="ConnectionString" value="Server=(local);Database=Northwind; User ID=UserDemo;Pwd=UserDemo" /> <add key="DALAssembly" value="DALAccess, version=1.0.0.0, PublicKeyToken=F5CD5666253D6082" /><!-- <add key="ConnectionString" value="Provider=Microsoft.Jet.OLEDB.4.0; Data Source=../../../Northwind.mdb" />--> </appSettings></configuration>
我們必須在此檔案內指定兩條資訊。第一條資訊是規範的連接字串(用於為更改提供機會),如伺服器名稱或其他一些用於串連的參數。第二條資訊是程式集的完全限定名,應用程式的上一層將動態載入此程式集以尋找與特定資料來源一起使用的類:
讓我們再來看一下這部分代碼:
using System;using System.Data; using System.Configuration;using System.Reflection;using Common;namespace BLL{ public class Customers { public DataTable GetAllCustomers() { string AssemblyName = ConfigurationSettings.AppSettings ["DALAssembly"]; string TypeName = "DAL.CustomersData"; IDbCustomers cd = // (IDbCustomers)= Assembly.Load(AssemblyName). CreateInstance(mytype); DataTable dt = cd.GetCustomers(); return dt; } public DataSet GetCustomerOrders() { // 待定 return null; } }}
您可以看到,程式集使用從設定檔中讀取的名稱進行載入,並建立和使用 CustomersData 類的執行個體。
返回頁首
一些可能的改進
要瞭解我所建議的方法的樣本,請查看 NET Pet Shop v3.0 應用程式範例。建議您下載此樣本並深入瞭解它,不僅是為瞭解決可移植性問題,同時也是為瞭解決其他相關問題(如緩衝和效能最佳化)。
在為可移植應用程式設計資料訪問層的過程中,一個需要注意的重要問題是如何與其他層進行資訊通訊。在本文的樣本中,我只使用了一個普通的 DataTable 執行個體;在生產環境中,您可能希望根據必須表示的資料類型(您必須處理分層結構等)考慮不同的解決方案。在這裡,我不希望從頭開始,建議您查閱 Designing Data Tier Components and Passing Data Through Tiers 指南,它詳細描述了不同情況以及所建議的解決方案的優點。
如我簡介中所述,在設計階段,應該考慮您的目標資料來源所公開的特定特性以及總體資料訪問。這應該涵蓋預存程序、XML 序列化等事項。關於 Microsoft SQL Server 2000,您可以在下面的網址中找到有關如何最佳化使用這些特性的介紹:.NET Data Access Architecture Guide。強烈建議您閱讀一下該指南。
我總是收到許多關於 Data Access Application Block 以及它如何與參數關聯(如本文所述)的請求。這些 .NET 類充當 SQL Server .NET 資料提供者之上的抽象層,並使您能夠編寫更多優秀代碼與資料庫伺服器進行互動。下面是一段示範可行操作的代碼:
DataSet ds = SqlHelper.ExecuteDataset( connectionString, CommandType.StoredProcedure, "getProductsByCategory", new SqlParameter("@CategoryID", categoryID));
此方法還有一個外延,您可以在 GotDotNet 上的開放原始碼 Data Access Block 3.0 (Abstract Factory Implementation) 樣本中找到。此版本實現相同的抽象原廠模式,並使您能夠根據可用的 .NET 資料提供者使用不同資料來源。
返回頁首
結論
您現在應能夠根據選擇的特定資料來源構建不需要修改的商務邏輯類,並可以利用給定資料來源的唯一特性獲得更好的效果。這是有代價的:我們必須實現多組類,以便封裝特定資料來源的低層級操作,以及可以為每個特定資料來源(預存程序、函數等)構建的所有可程式化對象。如果希望獲得高效能和高可移植性,就必須付出這樣的代價。根據我的實際經驗,這是完全值得的!