摘要大多數使用NET框架組件工作的開發人員的一個核心工作是實現數據訪問功能他們建立的數據訪問層(data access layer)是應用程序的精華部分本文概述了使用Visual Studio NET和NET框架組件建立數據訪問層需要考慮的五個想法這些技巧包括通過使用基類(base class)利用面相對象技術和NET框架組件基礎結構使類容易繼承在決定顯示方法和外部界面前仔細地檢驗需求
如果你正在建立以數據為中心(datacentric)的NET框架組件應用程序你最終必須建立數據訪問層也許你知道在NET框架組件中建立自己的代碼有很多好處因為它支持實現和接口(interface)繼承你的代碼更容易重復使用特別是被使用不同的框架組件兼容(Frameworkcompliant)語言的開發人員使用本文我將概述為基於NET框架組件的應用程序建立數據訪問層的五條規則
開始前我必須提醒你建立的任何基於本文討論的規則的數據訪問層必須與傳統Windows平台上開發人員喜歡的多層或者n層應用程序兼容在這種結構中表現層包含Web窗體Windows窗體調用與數據訪問層的工作相應的事務層的XML服務代碼該層由多個數據訪問類(data access classe)組成換句話說在事務處理協調不是必要的情況下表現層將直接調用數據訪問層這種結構是傳統的模型視列表控制程序(ModelViewControllerMVC)模式的變體在多種情況下被Visual Studio NET和它暴露的控件采用
規則使用面向對象特性
最基本的面向對象事務是建立一個使用實現繼承的抽象類這個基類可以包括你的所有數據訪問類通過繼承能夠使用的服務如果那些服務足夠了它們就能通過在整個組織的基類分布實現重復使用例如最簡單的情況是基類能夠為衍生類處理連接的建立過程如列表所示
Imports SystemDataSqlClient
Namespace ACMEData
Public MustInherit Class DALBase : Implements IDisposable
Private _connection As SqlConnection
Protected Sub New(ByVal connect As String)
_connection = New SqlConnection(connect)
End Sub
Protected ReadOnly Property Connection() As SqlConnection
Get
Return _connection
End Get
End Property
Public Sub Dispose() Implements IDisposableDispose
_connectionDispose()
End Sub
End Class
End Namespace
列表簡單基類
在列表中可以看到對DALBase類作了MustInherit標記(C#中的抽象)以確保它在繼承關系中使用接著該類在公共構造函數中包括了一個實例化的私有SqlConnection對象它接收連接字符串作為一個參數當來自IDisposable接口的Dispose方法確保連接對象已經被配置了的時候受保護的(protected)Connection屬性允許衍生類訪問該連接對象
即使在下面簡化的例子中你也能開始看到抽象基類的用處
Public Class WebData : Inherits DALBase
Public Sub New()
MyBaseNew(ConfigurationSettingsAppSettings(ConnectString))
End Sub
Public Function GetOrders() As DataSet
Dim da As New SqlDataAdapter(usp_GetOrders MeConnection)
daSelectCommandCommandType = CommandTypeStoredProcedure
Dim ds As New DataSet()
daFill(ds)
Return ds
End Function
End Class
在這種情況下WebData類繼承自DALBase結果就是不必擔心實例化SqlConnection對象而是通過MyBase關鍵字(或者C#中的基關鍵字)簡單地把連接字符串傳遞給基類WebData類的GetOrders方法能使用MeConnection(在C#中是thisConnection)訪問受保護的屬性雖然這個例子相對簡單但是你將在規則和中看到基類也提供了其它的服務
當數據訪問層必須在COM+環境中運行時抽象的基類很有用在這種情況下因為允許組件使用COM+的必要代碼復雜得多所以更好的方式是建立一個如列表所示的服務組件(serviced component)基類
Transaction(TransactionOptionSupported) _
EventTrackingEnabled(True)> _
Public MustInherit Class DALServicedBase : Inherits ServicedComponent
Private _connection As SqlConnection
Protected Overrides Sub Construct(ByVal s As String)
_connection = New SqlConnection(s)
End Sub
Protected ReadOnly Property Connection() As SqlConnection
Get
Return _connection
End Get
End Property
End Class
列表服務組件基類
在這段代碼中DALServicedBase類包含的基本功能與列表中的相同但是加上了從SystemEnterpriseServices名字空間的ServicedComponent的繼承並且包括了一些屬性指明組件支持對象構造事務和靜態跟蹤接著該基類仔細地捕捉組件服務管理器(Component Services Manager)中的構造字符串並且再次建立和暴露SqlConnection對象我們要注意的是當一個類繼承自DALServicedBase時它也繼承了屬性的設置換句話說一個衍生類的事務選項也設置為Supported如果衍生類想重載這種行為它能在類的層次重新定義該屬性
此外衍生類在適當情況下應該有利於自身重載和共享方法使用重載的方法(一個方法有多個調用信號)在本質上有兩種情況首先它們在一個方法需要接受多種類型的參數時使用框架組件中的典型例子是SystemConvert類的方法例如ToString方法包含個接受一個參數的重載方法每個重載方法的類型不同其次重載的方法用於暴露參數數量不斷增長的信號而不是不同類型的必要參數在數據訪問層中這類重載變得效率很高因為它能用於為數據檢索和修改暴露交替的信號例如GetOrders方法可以重載這樣一個信號不接受參數並返回所有訂單但是附加的信號接受參數以表明調用程序希望檢索特定的顧客訂單代碼如下
Public Overloads Function GetOrders() As DataSet
Public Overloads Function GetOrders(ByVal customerId As Integer) As DataSet
這種情況下的一個好的實現技巧是抽象GetOrders方法的功能到一個能被每個重載信號調用的私有的或者受保護的方法中
共享方法(C#中的靜態方法)也能用於暴露數據訪問類的所有實例能夠訪問的字段屬性和方法盡管共享成員不能與使用組件服務(Component Services)的類一起使用但是對於在數據訪問類的共享構造函數中檢索並被所有實例讀取的只讀數據是有用的使用共享成員讀/寫數據時要小心因為為了訪問該共享數據執行的多個線程可能會競爭
規則堅持設計指導
隨Visual Studio NET一起發布的在線文檔中有一個叫類庫開發人員的設計指導(Design Guidelines for Class Library Developers)的主題它覆蓋了類屬性和方法的名字轉換是重載的成員構造函數和事件的補充模式你必須遵循名字轉換的主要原因之一是NET框架組件提供的跨語言(crosslanguage)繼承如果你在Visual Basic NET中建立一個數據訪問層基類你想確保使用NET框架組件兼容的其它語言的開發人員能繼承它並容易理解它怎樣工作通過堅持我概述的指導方針你的名字轉換和構造就不會是語言特定的(language specific)例如你可能注意到在本文例子的代碼中第一個詞小寫並加上intercaps是用於方法的參數的每個詞大寫是用於方法的基類使用Base標志來標識它是一個抽象類
可以推測NET框架組件設計指導都是普通設計模式像Gang of Four (AddisonWesley )寫的Design Patterns記載的一樣例如NET框架組件使用了Observer模式的一個變體叫做Event模式在類中暴露事件時你必須遵循它
規則利用基礎結構(Infrastructure)
NET框架組件包括一些類和構造它們能輔助處理通常的與基礎結構相關的事務例如裝置和異常處理通過基類把這些概念與繼承組合起來將非常強大例如你能考慮一下SystemDiagnostics名字空間中暴露的跟蹤功能除了提供Trace和Debug類外該名字空間還包括衍生自Switch和TraceListener的類Switch類的BooleanSwitch和TraceSwitch能被配置用於打開和關閉應用程序和配置文件在TraceSwitch中可以暴露多層次跟蹤TraceListener類的TextWriterTraceListener和EventLogTraceListener分別將Trace和Debug方法的輸入定位到文本文件和事件日志
這樣作的結果是給基類添加了跟蹤功能使衍生類記錄消息日志更簡單接著應用程序能使用配置文件控制是否允許跟蹤你能包括一個BooleanSwitch類型的私有變量並在構造函數中實例化它來給列表中的DALBase添加這個功能
Public Sub New(ByVal connect As String)
_connection = New SqlConnection(connect)
_dalSwitch = New BooleanSwitch(DAL Data Access Code)
End Sub
傳遞給BooleanSwitch的參數包括名字和描述接著你能添加一個受保護的屬性打開和關閉開關也能添加一個屬性使用Trace對象的WriteLineIf方法格式化並寫入跟蹤消息
Protected Property TracingEnabled() As Boolean
Get
Return _dalSwitchEnabled
End Get
Set(ByVal Value As Boolean)
_dalSwitchEnabled = Value
End Set
End Property
Protected Sub WriteTrace(ByVal message As String)
TraceWriteLineIf(MeTracingEnabled Now & : & message)
End Sub
通過這種途徑衍生類自己並不知道開關(switch)和監聽(listener)類當數據訪問類產生一個有意義的信號時能夠簡單地調用WriteTrace方法
type=SystemDiagnosticsTextWriterTraceListener
initializeData=DALLogtxt />
列表跟蹤的配置文件
為了建立一個監聽器並打開它需要使用應用程序配置文件列表顯示了一個簡單的配置文件它能夠打開剛才顯示的數據訪問類開關並通過myListener調用TextWriterTraceListener把輸出定位到文件DALLogtxt中當然你能通過從TraceListener類衍生程序化地建立監聽器並把該監聽器直接包含在數據訪問類中
Public Class DALException : Inherits ApplicationException
Public Sub New()
MyBaseNew()
End Sub
Public Sub New(ByVal message As String)
MyBaseNew(message)
End Sub
Public Sub New(ByVal message As String ByVal innerException As
Exception)
MyBaseNew(message innerException)
End Sub
在這兒添加自定義成員
Public ConnectString As String
End Class
列表自定義異常類
你從中收益的第二個基礎結構是結構化異常處理(SEH)在最基本的層次數據訪問類能夠暴露它的衍生自SystemApplicationException 的Exception(異常)對象並能進一步暴露自定義成員例如列表中顯示的DALException對象能用於包裝數據訪問類中的代碼產生的異常接著基類能暴露一個受保護的方法包裝該異常組裝自定義成員並把它發回給調用程序如下所示
Protected Sub ThrowDALException(ByVal message As String _
ByVal innerException As Exception)
Dim newMine As New DALException(message innerException)
newMineConnectString = MeConnectionConnectionString
MeWriteTrace(message & { & innerExceptionMessage & })
Throw newMine
End Sub
使用這種方法衍生類能簡單地調用受保護的方法傳遞進去一個特定的數據異常(典型的有SqlException或者 OleDbException)該異常被截取並添加了從屬於特定數據域的消息基類在DALException中包裝該異常並把它發回到調用程序這就允許調用程序用一個Catch語句輕易地捕捉所有來自數據訪問類的異常
作為選擇之一你可以看一看MSDN上發布的Exception Management Application Block Overview該框架組件通過一系列對象結合了異常和應用程序日志記錄實際上通過從NET 框架組件提供的BaseApplicationException類衍生的自定義異常類能夠簡單地插入該框架組件
規則仔細選擇外部界面
在你設計數據訪問類的方法時需要考慮它們怎樣接受和返回數據對大多數開發人員來說主要有三個選擇直接使用ADONET對象使用XML使用自定義類
如果直接暴露ADONET對象你能使用一到兩個編程模型第一個包括數據集和數據表對象它們對不連接數據訪問很有用有很多關於數據集和與它關聯的數據表的文章但是當你必須使用從下層數據存儲斷開的數據時它才最有用處換句話說數據集能在應用程序各層之間傳遞即使那些層在物理上是分布式的當業務和數據服務層放置在同一群服務器上並且與表現服務分開時也能使用此外數據集對象是通過基於XML的Web服務返回數據的理想方法因為它們是可串行化的因此能在SOAP回應消息中返回
這與使用實現IDataReader接口的類(例如SqlDataReader 和OleDbDataReader)訪問數據不同數據閱讀器(data reader)用只向前的只讀的方式訪問數據兩者之間最大的不同是數據集和數據表對象能在應用程序域之間傳遞通過傳遞值(by value)實現然而數據閱讀器能在各處傳遞但是一般通過引用(by reference)實現在列表中Read和GetValues在服務器過程中執行並且它們的返回值復制到客戶端
該圖顯示了數據閱讀器怎樣存活在應用程序域中它在那兒它被建立並且對它的所有訪問結果都在客戶端和服務器應用程序域之間的循環之中這意味著當數據訪問方法在相同的應用程序域運行時應該返回數據閱讀器作為調用者
使用數據閱讀器時有兩個問題需要考慮首先當你從數據訪問類的一個方法返回數據閱讀器時你必須考慮與數據閱讀器關聯的連接對象的生存期默認情況是當調用程序通過數據閱讀器重復時連接仍然是忙的不幸的是當調用程序結束後連接仍然打開因此它不返回到連接池(如果允許連接池)但是當通過傳遞CommandBehaviorCloseConnection 枚舉給command對象的ExecuteReader方法連接的Close方法被調用時你能命令數據閱讀器關閉它的連接
其次為了把表現層從特定的框架組件數據提供程序(例如SqlClient或者OleDb)中分離出來調用代碼應該使用IDataReader接口(例如SqlDataReader)而不是具體類型來引用返回值通過這種方法如果應用程序後端從Oracle移植到 SQL Server或者數據訪問類的一個方法的返回類型改變了表現層也不需要更改
如果你希望數據訪問類返回XML你可以從SystemXml名字空間中的XmlDocument和XmlReader中選擇一個它與數據集和IDataReader類似換句話說當數據從數據源斷開時你的方法應該返回一個XmlDocument(或者XmlDataDocument)然而XmlReader可用於訪問XML數據的流
最後你也能決定與公共屬性一起返回自定義類這些類可以使用Serialization(串行化)屬性標記這樣它們就能跨越應用程序域復制另外如果你從方法中返回多個對象就需要強化類型(strongly typed)的集合類
Imports SystemXmlSerialization
_
Public Class Book : Implements IComparable
Public ProductID As Integer
Public ISBN As String
Public Title As String
Public Author As String
Public UnitCost As Decimal
Public Description As String
Public PubDate As Date
Public Function CompareTo(ByVal o As Object) As Integer _
Implements IComparableCompareTo
Dim b As Book = CType(o Book)
Return MeTitleCompareTo(bTitle)
End Function
End Class
Public NotInheritable Class BookCollection : Inherits ArrayList
Default Public Shadows Property Item(ByVal productId As Integer) _
As Book
Get
Return Me(IndexOf(productId))
End Get
Set(ByVal Value As Book)
Me(IndexOf(productId)) = Value
End Set
End Property
Public Overloads Function Contains(ByVal productId As Integer) As _
Boolean
Return ( <> IndexOf(productId))
End Function
Public Overloads Function IndexOf(ByVal productId As Integer) As _
Integer
Dim index As Integer =
Dim item As Book
For Each item In Me
If itemProductID = productId Then
Return index
End If
index = index +
Next
Return
End Function
Public Overloads Sub RemoveAt(ByVal productId As Integer)
RemoveAt(IndexOf(productId))
End Sub
Public Shadows Function Add(ByVal value As Book) As Integer
Return MyBaseAdd(value)
End Function
End Class
列表使用自定義類
上列表(列表)包含了一個簡單的Book類和與它關聯的集合類的例子你能注意到Book類用Serializable做了標記使它跨越應用程序域能使用by value語法該類實現了IComparable接口因此當它包含在一個集合類中的時候默認情況下它將按Title排序BookCollection類從SystemCollections名字空間的ArrayList衍生並且為了將該集合限制到Book對象而隱藏了Item屬性和ADD方法
通過使用自定義類你完全地控制了數據的表現開發人員的效率並且沒有依賴ADONET的調用但是這種途徑需要更多的代碼因為NET框架組件沒有包含任何與對象相關的技術映射在這種情況下你應該在數據訪問類中建立一個數據讀取器並使用它來組合自定義類
規則抽象NET框架組件數據提供程序
最後一條規則說明了為什麼和怎樣抽象數據訪問類內部使用的NET框架組件數據提供程序(data provider)先前我說過ADONET編程模型暴露了特定的NET框架組件數據提供程序包括SqlClientOleDb和其它MSDN Online Web站點上可用的但是這種設計的結果是提高性能為數據提供程序暴露特定數據源功能的能力它強迫你決定使用那種數據提供程序編碼換句話說開發人員典型地會選擇使用SqlClient或OleDb接著在各自的名字空間直接對它們的類進行編程
如果你想改變NET框架組件數據提供程序你必須重新編寫數據訪問方法為了避免這種情況發生你可以使用Abstract Factory設計模式使用這種模式你能建立一個簡單的類它暴露方法來建立主要的NET框架組件數據提供程序對象(commandconnectiondata adapter和parameter)而那些對象基於傳遞給構造函數的NET框架組件數據提供程序的信息列表中的代碼就是這樣一個簡單的類
public enum ProviderType :int {SqlClient = OLEDB = }
public class ProviderFactory {
public ProviderFactory(ProviderType provider) {
_pType = provider;
_initClass();
}
public ProviderFactory() {
_initClass();
}
private ProviderType _pType = ProviderTypeSqlClient;
private bool _pTypeSet = false;
private Type[] _conType _comType _parmType _daType;
private void _initClass() {
_conType = new Type[];
_comType = new Type[];
_parmType = new Type[];
_daType = new Type[];
// 為提供程序初始化類型
_conType[(int)ProviderTypeSqlClient] = typeof(SqlConnection);
_conType[(int)ProviderTypeOLEDB] = typeof(OleDbConnection);
_comType[(int)ProviderTypeSqlClient] = typeof(SqlCommand);
_comType[(int)ProviderTypeOLEDB] = typeof(OleDbCommand);
_parmType[(int)ProviderTypeSqlClient] = typeof(SqlParameter);
_parmType[(int)ProviderTypeOLEDB] = typeof(OleDbParameter);
_daType[(int)ProviderTypeSqlClient] = typeof(SqlDataAdapter);
_daType[(int)ProviderTypeOLEDB] = typeof(OleDbDataAdapter);
}
public ProviderType Provider {
get {
return _pType;
}
set {
if (_pTypeSet) {
throw new ReadOnlyException(Provider already set to
+ _pTypeToString());
}
else {
_pType = value;
_pTypeSet = true;
}
}
}
public IDataAdapter CreateDataAdapter(string commandTextIDbConnection
connection) {
IDataAdapter d;
IDbDataAdapter da;
d = (IDataAdapter)ActivatorCreateInstance(_daType[(int)_pType]
false);
da = (IDbDataAdapter)d;
daSelectCommand = thisCreateCommand(commandText connection);
return d; }
public IDataParameter CreateParameter(string paramName DbType
paramType) {
IDataParameter p;
p = (IDataParameter)ActivatorCreateInstance(_parmType[(int)_pType]
false);
pParameterName = paramName;
pDbType = paramType;
return p;
}
public IDataParameter CreateParameter(string paramName DbType
paramType Object value) {
IDataParameter p;
p = (IDataParameter)ActivatorCreateInstance(_parmType[(int)_pType]
false);
pParameterName = paramName;
pDbType = paramType;
pValue = value;
return p;
}
public IDbConnection CreateConnection(string connect) {
IDbConnection c;
c = (IDbConnection)ActivatorCreateInstance(_conType[(int)_pType]
false);
cConnectionString = connect;
return c;
}
public IDbCommand CreateCommand(string cmdText IDbConnection
connection) {
IDbCommand c;
c = (IDbCommand)ActivatorCreateInstance(_comType[(int)_pType]
false);
cCommandText = cmdText;
cConnection = connection;
return c;
}
}
列表 ProviderFactory
為了使用該類數據訪問類的代碼必須對多個NET框架組件數據提供程序實現的接口(包括IDbCommandIDbConnectionIDataAdapter和IDataParameter)進行編程例如為了使用一個參數化存儲過程的返回值來填充數據集必須在數據訪問類的某個方法中有下面的代碼
Dim _pf As New ProviderFactory(ProviderTypeSqlClient)
Dim cn As IDbConnection = _pfCreateConnection(_connect)
Dim da As IDataAdapter = _pfCreateDataAdapter(usp_GetBook cn)
Dim db As IDbDataAdapter = CType(da IDbDataAdapter)
dbSelectCommandCommandType = CommandTypeStoredProcedure
dbSelectCommandParametersAdd(_pfCreateParameter(@productIdDbTypeInt id))
Dim ds As New DataSet(Books)
daFill(ds)
典型的情況是你在類的層次聲明ProviderFactory變量並在數據訪問類的構造函數中實例化它另外它的構造函數與從配置文件中讀取的提供程序一起組裝而不應該是硬代碼你可以想象ProviderFactory是數據訪問類的一個重大的補充並且能被包括進部件分發給其它的開發人員
結論
在Web服務時代將建立越來越多的應用程序操作來自獨立的應用程序層的數據如果你遵循一些基本規則並形成習慣編寫數據訪問代碼將更快更容易並且更能重新使用把你的錯誤保存到服務器允許你保持數據獨立
From:http://tw.wingwit.com/Article/program/net/201311/11869.html