編寫可移植數(shù)據(jù)訪問層
2024-07-21 02:23:02
供稿:網(wǎng)友
摘要:了解如何編寫透明地使用不同數(shù)據(jù)源(從 microsoft access 到 sql server 以及 oracle rdbms)的智能應(yīng)用程序。
本頁內(nèi)容
引言
使用通用數(shù)據(jù)訪問方法
使用基本接口
編寫專門的數(shù)據(jù)訪問層
從其他層使用數(shù)據(jù)訪問類
一些可能的改進(jìn)
結(jié)論
引言
在負(fù)責(zé)咨詢工作的過去 6 年中,我曾多次聽說關(guān)于數(shù)據(jù)訪問和操作方面的問題,它時(shí)刻困擾著用戶:“如何編寫應(yīng)用程序,以便只需對(duì)其進(jìn)行很少的改動(dòng)或不進(jìn)行改動(dòng)即可使用數(shù)據(jù)庫服務(wù)器 x、y 和 z?”由于知道數(shù)據(jù)訪問層仍然是現(xiàn)代應(yīng)用程序的最關(guān)鍵部分,并且通常是經(jīng)驗(yàn)不足的開發(fā)人員的頭號(hào)敵人,因此我的第一反應(yīng)始終是:根本辦不到!
面對(duì)著人們惶惶不安的面孔以及“使用 microsoft 在 ado 中提供的通用數(shù)據(jù)訪問方法如何?”這樣的問題,我決定針對(duì)此問題提供更詳細(xì)的說明以及建議的解決方案。
問題在于,如果應(yīng)用程序是較小的原型,或者如果并發(fā)用戶較少并且數(shù)據(jù)訪問邏輯比較簡(jiǎn)單,那么即使您選擇下面這些最簡(jiǎn)單的方法,也不會(huì)遇到任何問題:使用 rad 工具(如 data environment in microsoft® visual basic® 6.0),或某些“一攬子”解決方案(如 activex® data control 和其他第三方組件),這些解決方案通常會(huì)隱藏應(yīng)用程序與特定數(shù)據(jù)源之間進(jìn)行的復(fù)雜交互。然而,當(dāng)用戶數(shù)量增加使得必須解決并發(fā)操作問題時(shí),由于頻繁使用動(dòng)態(tài)記錄集、服務(wù)器端光標(biāo)以及不必要的鎖定策略,導(dǎo)致出現(xiàn)許多性能問題。為達(dá)到用戶目標(biāo)而必須對(duì)系統(tǒng)所做的設(shè)計(jì)和代碼更改將花費(fèi)您大量的時(shí)間,因?yàn)槟鷱拈_始時(shí)就沒有考慮過這一問題。
使用通用數(shù)據(jù)訪問方法
在將 ado 可靠地并入 mdac(microsoft data access components 2.1 版)后,microsoft 掀起了通用數(shù)據(jù)訪問的使用高潮。其主導(dǎo)思想是向開發(fā)人員展示,通過使用簡(jiǎn)單的對(duì)象模型(“連接”、“命令”和“記錄集”),可以編寫出能夠與各種不同的數(shù)據(jù)源(無論是關(guān)系數(shù)據(jù)源還是非關(guān)系數(shù)據(jù)源)連接的應(yīng)用程序。文檔(以及當(dāng)時(shí)的大多數(shù)文章和示例)中通常未曾提及的是,即使使用相同的數(shù)據(jù)訪問技術(shù),各種數(shù)據(jù)源的可編程性和特征也千差萬別。
其結(jié)果是,在需要從多個(gè)數(shù)據(jù)源獲取數(shù)據(jù)的應(yīng)用程序中,最簡(jiǎn)單的方法是使用所有數(shù)據(jù)源所提供的功能的“共同點(diǎn)”,但因此會(huì)失去使用數(shù)據(jù)源特定選項(xiàng)的好處,即為訪問和操作各種 rdbms 中的信息提供最佳方法。
我對(duì)該方法始終存在的懷疑是,經(jīng)過與我的客戶進(jìn)行更詳細(xì)的分析后,我們通常一致認(rèn)為與應(yīng)用程序中處理顯示和業(yè)務(wù)邏輯的其他部分相比,與數(shù)據(jù)源進(jìn)行交互的只是應(yīng)用程序很小的一部分。通過進(jìn)行精心的模塊化設(shè)計(jì),可以將 rdbms 特定代碼隔離在一些容易互換的模塊中,從而避免對(duì)數(shù)據(jù)訪問使用“通用”方法。然而,我們可以使用非常特定的數(shù)據(jù)訪問代碼(根據(jù)數(shù)據(jù)源的不同,使用存儲(chǔ)過程、命令批處理和其他特性),而不觸及其他大多數(shù)應(yīng)用程序代碼。這總是提醒大家:正確的設(shè)計(jì)是編寫可移植的有效代碼的關(guān)鍵。
ado.net 將一些重要的變化引入到數(shù)據(jù)訪問編碼領(lǐng)域,如專用 .net 數(shù)據(jù)提供程序這樣的概念。使用特定的提供程序,可以繞過為數(shù)眾多但有時(shí)沒必要的一系列軟件接口和服務(wù)(它們是 ole db 和 odbc 層在數(shù)據(jù)訪問代碼與數(shù)據(jù)庫服務(wù)器之間插入的內(nèi)容),從而以最佳方式連接到數(shù)據(jù)源。但每個(gè)數(shù)據(jù)源仍然存在不同的特征和特性(具有不同的 sql dialect),且編寫高效的應(yīng)用程序仍然必須使用這些特定特征而不是“共同點(diǎn)”。從可移植性觀點(diǎn)看來,托管和非托管的數(shù)據(jù)訪問技術(shù)仍然非常類似。
除“利用數(shù)據(jù)源的唯一特征”外,編寫良好數(shù)據(jù)訪問層所必需的其他規(guī)則對(duì)每個(gè)數(shù)據(jù)源通常都是相同的:
• 在可能的情況下使用連接池機(jī)制。
• 節(jié)約使用數(shù)據(jù)庫服務(wù)器的有限資源。
• 注意網(wǎng)絡(luò)的往返。
• 在適當(dāng)?shù)那闆r下,增強(qiáng)執(zhí)行計(jì)劃的重復(fù)使用率并避免重復(fù)編譯。
• 使用適當(dāng)?shù)逆i定模型管理并發(fā)性。
從我使用模塊化設(shè)計(jì)方法的個(gè)人經(jīng)驗(yàn)來看,整個(gè)應(yīng)用程序中專用于處理特定數(shù)據(jù)源的代碼量不會(huì)超過總量的 10%。顯而易見,這比僅僅更改配置文件中的連接字符串更復(fù)雜,但我認(rèn)為,這樣做會(huì)獲得性能收益,因此這是一個(gè)可接受的折衷辦法。
使用基本接口
此處的目標(biāo)是使用抽象,并將特定于特殊數(shù)據(jù)源的代碼封裝在類層中,從而使應(yīng)用程序的其他部分獨(dú)立于后端數(shù)據(jù)庫服務(wù)器或免受其影響。
.net framework 的面向?qū)ο筮@一特性將在該過程中為我們提供幫助,使我們能夠選擇要使用的抽象級(jí)別。選項(xiàng)之一是使用每個(gè) .net 數(shù)據(jù)提供程序都必須實(shí)現(xiàn)的基本接口(idbconnection、idbcommand、idatareader 等)。另一個(gè)選項(xiàng)是創(chuàng)建一組類(數(shù)據(jù)訪問層),用于管理應(yīng)用程序的所有數(shù)據(jù)訪問邏輯(例如,使用 crud 范例)。為檢查這兩種可能性,我們首先從基于 northwind 數(shù)據(jù)庫的訂單輸入應(yīng)用程序示例入手,然后插入和檢索不同數(shù)據(jù)源中的信息。
數(shù)據(jù)提供程序基本接口標(biāo)識(shí)應(yīng)用程序與數(shù)據(jù)源進(jìn)行交互通常所需的典型行為:
• 定義連接字符串。
• 打開和關(guān)閉與數(shù)據(jù)源的物理連接。
• 定義命令和相關(guān)參數(shù)。
• 執(zhí)行可以創(chuàng)建的不同種類的命令。
• 返回一組數(shù)據(jù)。
• 返回標(biāo)量值。
• 對(duì)數(shù)據(jù)執(zhí)行操作但不返回任何內(nèi)容。
• 對(duì)返回的數(shù)據(jù)集提供只向前型訪問和只讀型訪問。
• 定義使數(shù)據(jù)集與數(shù)據(jù)源(數(shù)據(jù)適配器)的內(nèi)容保持同步所需的一組操作。
但事實(shí)上,如果將檢索、插入、更新和刪除不同數(shù)據(jù)源(使用不同的數(shù)據(jù)提供程序)中的信息所需的各種操作封裝在數(shù)據(jù)訪問層中,并且只公開基本接口的成員,則可以實(shí)現(xiàn)第一級(jí)抽象-至少從數(shù)據(jù)提供程序的角度來看是這樣。讓我們看一看以下演示該設(shè)計(jì)思想的代碼:
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
// 任何其他數(shù)據(jù)源類型
}
public enum parametertype
{
integer,
char,
varchar
// 定義公用參數(shù)類型集
}
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;
}
}
}
該類的作用是向應(yīng)用程序的較高級(jí)別隱藏與創(chuàng)建特定類型(來自特定的數(shù)據(jù)提供程序)的實(shí)例有關(guān)的細(xì)節(jié),應(yīng)用程序現(xiàn)在可以使用通過基本接口公開的一般行為與數(shù)據(jù)源進(jìn)行交互。
讓我們了解一下如何從應(yīng)用程序的其他部分使用該類:
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 類通過特定連接字符串創(chuàng)建 xxxconnection 實(shí)例,并編寫與基本數(shù)據(jù)源沒有特定依賴性的其余代碼部分。
與數(shù)據(jù)層交互的一個(gè)業(yè)務(wù)層類示例看起來可能類似下面這樣:
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;
}
}
}
這樣看來,此方法出現(xiàn)什么問題了?此處的問題是,只有一個(gè)重要細(xì)節(jié)將代碼綁定到特定數(shù)據(jù)源:命令字符串的 sql 語法!實(shí)際上,如果以這種方式編寫應(yīng)用程序,則使其具有可移植性的唯一辦法是采用可以由任何數(shù)據(jù)源解釋的基本 sql 語法,但這樣可能會(huì)失去從特定數(shù)據(jù)源的特定功能獲得好處的機(jī)會(huì)。如果應(yīng)用程序只對(duì)數(shù)據(jù)進(jìn)行很簡(jiǎn)單和很標(biāo)準(zhǔn)的操作,并且如果您不希望使用特定數(shù)據(jù)源中的高級(jí)功能(如 xml 支持),這可能是個(gè)小問題。但通常此方法將導(dǎo)致性能降低,因?yàn)槟鸁o法使用每個(gè)數(shù)據(jù)源的最佳特性。
編寫專門的數(shù)據(jù)訪問層
因此,只使用基本接口不足以通過不同數(shù)據(jù)源提供可接受級(jí)別的抽象。這種情況下,一個(gè)好的解決方案是提高此抽象的級(jí)別,即創(chuàng)建一組類(如 customer、order 等)來封裝特定數(shù)據(jù)提供程序的使用,并通過與特定數(shù)據(jù)源、類型化的“數(shù)據(jù)集”、對(duì)象集合等無關(guān)的數(shù)據(jù)結(jié)構(gòu)與應(yīng)用程序的其他級(jí)別交換信息。
可以在特定程序集內(nèi)部創(chuàng)建此層的專用類(為每個(gè)受支持的數(shù)據(jù)源分別創(chuàng)建一個(gè)專用類),并可以在需要的情況下按照配置文件中的說明從應(yīng)用程序加載它們。這樣,如果您希望向應(yīng)用程序中添加全新的數(shù)據(jù)源,唯一要做的事情是針對(duì)一組通用接口組中定義的“合同”實(shí)現(xiàn)一組新類。
讓我們看一個(gè)實(shí)際例子:如果希望將 microsoft® sql server™ 和 microsoft® access 作為數(shù)據(jù)源為其提供支持,則應(yīng)該在 microsoft® visual studio® .net 中創(chuàng)建兩個(gè)不同項(xiàng)目,每個(gè)數(shù)據(jù)源分別創(chuàng)建一個(gè)。
為 sql server 創(chuàng)建的項(xiàng)目將類似于如下所示:
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 進(jìn)行數(shù)據(jù)檢索的代碼類似于如下所示:
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 類實(shí)現(xiàn) idbcustomers 接口。需要支持新數(shù)據(jù)源時(shí),只能創(chuàng)建一個(gè)實(shí)現(xiàn)該接口的新類。
此類型的接口可以類似于如下所示:
using system;
using system.data;
namespace common
{
public interface idbcustomers
{
datatable getcustomers();
datatable getcustomerorders(string customerid);
datatable getcustomersbycountry(string countrycode);
bool insertcustomer();
}
}
我們可以創(chuàng)建專用程序集或共享程序集來封裝這些數(shù)據(jù)訪問類,在第一種情況下,程序集加載程序?qū)⑺阉魑覀冊(cè)?appbase 文件夾的配置文件內(nèi)指定的程序集,或者使用典型探測(cè)規(guī)則在子目錄內(nèi)進(jìn)行搜索。如果我們必須與其他應(yīng)用程序共享這些類,則可以將這些程序集置于全局程序集緩存中。
從其他層使用數(shù)據(jù)訪問類
這兩個(gè)幾乎相同的 customersdata 類包含在應(yīng)用程序其余部分將使用的兩個(gè)不同程序集內(nèi)。通過下面的配置文件,我們現(xiàn)在可以指定要加載的程序集以及面向的數(shù)據(jù)源。
可能的配置文件示例將類似于如下所示:
<?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>
我們必須在此文件內(nèi)指定兩條信息。第一條信息是規(guī)范的連接字符串(用于為更改提供機(jī)會(huì)),如服務(wù)器名稱或其他一些用于連接的參數(shù)。第二條信息是程序集的完全限定名,應(yīng)用程序的上一層將動(dòng)態(tài)加載此程序集以查找與特定數(shù)據(jù)源一起使用的類:
讓我們?cè)賮砜匆幌逻@部分代碼:
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;
}
}
}
您可以看到,程序集使用從配置文件中讀取的名稱進(jìn)行加載,并創(chuàng)建和使用 customersdata 類的實(shí)例。
一些可能的改進(jìn)
要了解我所建議的方法的示例,請(qǐng)查看 net pet shop v3.0 示例應(yīng)用程序。建議您下載此示例并深入了解它,不僅是為了解決可移植性問題,同時(shí)也是為了解決其他相關(guān)問題(如緩存和性能優(yōu)化)。
在為可移植應(yīng)用程序設(shè)計(jì)數(shù)據(jù)訪問層的過程中,一個(gè)需要注意的重要問題是如何與其他層進(jìn)行信息通信。在本文的示例中,我只使用了一個(gè)普通的 datatable 實(shí)例;在生產(chǎn)環(huán)境中,您可能希望根據(jù)必須表示的數(shù)據(jù)類型(您必須處理分層結(jié)構(gòu)等)考慮不同的解決方案。在這里,我不希望從頭開始,建議您查閱 designing data tier components and passing data through tiers 指南,它詳細(xì)描述了不同情況以及所建議的解決方案的優(yōu)點(diǎn)。
如我簡(jiǎn)介中所述,在設(shè)計(jì)階段,應(yīng)該考慮您的目標(biāo)數(shù)據(jù)源所公開的特定特性以及總體數(shù)據(jù)訪問。這應(yīng)該涵蓋存儲(chǔ)過程、xml 序列化等事項(xiàng)。關(guān)于 microsoft® sql server™ 2000,您可以在下面的網(wǎng)址中找到有關(guān)如何優(yōu)化使用這些特性的介紹:.net data access architecture guide。強(qiáng)烈建議您閱讀一下該指南。
我總是收到許多關(guān)于 data access application block 以及它如何與參數(shù)關(guān)聯(lián)(如本文所述)的請(qǐng)求。這些 .net 類充當(dāng) sql server .net 數(shù)據(jù)提供程序之上的抽象層,并使您能夠編寫更多優(yōu)秀代碼與數(shù)據(jù)庫服務(wù)器進(jìn)行交互。下面是一段演示可行操作的代碼:
dataset ds = sqlhelper.executedataset(
connectionstring,
commandtype.storedprocedure,
"getproductsbycategory",
new sqlparameter("@categoryid", categoryid));
此方法還有一個(gè)外延,您可以在 gotdotnet 上的開放源代碼 data access block 3.0 (abstract factory implementation) 示例中找到。此版本實(shí)現(xiàn)相同的抽象工廠模式,并使您能夠根據(jù)可用的 .net 數(shù)據(jù)提供程序使用不同數(shù)據(jù)源。
結(jié)論
您現(xiàn)在應(yīng)能夠根據(jù)選擇的特定數(shù)據(jù)源構(gòu)建不需要修改的業(yè)務(wù)邏輯類,并可以利用給定數(shù)據(jù)源的唯一特性獲得更好的效果。這是有代價(jià)的:我們必須實(shí)現(xiàn)多組類,以便封裝特定數(shù)據(jù)源的低級(jí)別操作,以及可以為每個(gè)特定數(shù)據(jù)源(存儲(chǔ)過程、函數(shù)等)構(gòu)建的所有可編程對(duì)象。如果希望獲得高性能和高可移植性,就必須付出這樣的代價(jià)。根據(jù)我的實(shí)際經(jīng)驗(yàn),這是完全值得的!