在負責咨詢工作的過去 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 數據提供程序使用不同數據源。
結論
您現在應能夠根據選擇的特定數據源構建不需要修改的業務邏輯類,並可以利用給定數據源的唯一特性獲得更好的效果。這是有代價的:我們必須實現多組類,以便封裝特定數據源的低級別操作,以及可以為每個特定數據源(存儲過程、函數等)構建的所有可編程對象。如果希望獲得高性能和高可移植性,就必須付出這樣的代價。