設計.NET應用程式資料訪問層五大原則

來源:互聯網
上載者:User
程式|訪問|設計|資料

  摘要:大多數使用.NET架構組件工作的開發人員的一個核心工作是實現資料訪問功能,他們建立的資料訪問層(data access layer)是應用程式的精華部分。本文概述了使用Visual Studio .NET和.NET架構組件建立資料訪問層需要考慮的五個想法。這些技巧包括通過使用基類(base class)利用面相對象技術和.NET架構組件基礎結構,使類容易繼承,在決定顯示方法和外部介面前仔細地檢驗需求。

  如果你正在建立以資料為中心(data-centric)的.NET架構組件應用程式,你最終必須建立資料訪問層。也許你知道在.NET架構組件中建立自己的代碼有很多好處。因為它支援實現和介面(interface)繼承,你的代碼更容易重複使用,特別是被使用不同的架構組件相容(Framework-compliant)語言的開發人員使用。本文我將概述為基於.NET架構組件的應用程式建立資料訪問層的五條規則。

  開始前,我必須提醒你建立的任何基於本文討論的規則的資料訪問層必須與傳統Windows平台上開發人員喜歡的多層或者n層應用程式相容。在這種結構中,表現層包含Web表單、Windows表單、調用與資料訪問層的工作相應的事務層的XML服務代碼。該層由多個資料訪問類(data access classe)組成。換句話說,在交易處理協調不是必要的情況下,表現層將直接調用資料訪問層。這種結構是傳統的模型-視列表-控製程序(Model-View-Controller,MVC)模式的變體,在多種情況下被Visual Studio .NET和它暴露的控制項採用。

  規則1:使用物件導向特性

  最基本的物件導向事務是建立一個使用實現繼承的抽象類別。這個基類可以包括你的所有資料訪問類通過繼承能夠使用的服務。如果那些服務足夠了,它們就能通過在整個組織的基類分布實現重複使用。例如最簡單的情況是基類能夠為衍生類處理串連的建立過程,如列表1所示。

Imports System.Data.SqlClient

Namespace ACME.Data
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 IDisposable.Dispose
_connection.Dispose()
End Sub

End Class
End Namespace

  列表1.簡單基類

  在列表中可以看到,對DALBase類作了MustInherit標記(C#中的抽象),以確保它在繼承關係中使用。接著該類在公用建構函式中包括了一個執行個體化的私人SqlConnection對象,它接收連接字串作為一個參數。當來自IDisposable介面的Dispose方法確保連線物件已經被配置了的時候,受保護的(protected)Connection屬性允許衍生類訪問該連線物件。

  即使在下面簡化的例子中你也能開始看到抽象基類的用處:

Public Class WebData : Inherits DALBase
Public Sub New()
MyBase.New(ConfigurationSettings.AppSettings("ConnectString"))
End Sub

Public Function GetOrders() As DataSet
Dim da As New SqlDataAdapter("usp_GetOrders", Me.Connection)
da.SelectCommand.CommandType = CommandType.StoredProcedure
Dim ds As New DataSet()

da.Fill(ds)
Return ds
End Function
End Class

  在這種情況下,WebData類繼承自DALBase,結果就是不必擔心執行個體化SqlConnection對象,而是通過MyBase關鍵字(或者C#中的基關鍵字)簡單地把連接字串傳遞給基類。WebData類的GetOrders方法能使用Me.Connection(在C#中是this.Connection)訪問受保護的屬性。雖然這個例子相對簡單,但是你將在規則2和3中看到基類也提供了其它的服務。

  當資料訪問層必須在COM+環境中運行時抽象的基類很有用。在這種情況下,因為允許組件使用COM+的必要代碼複雜得多,所以更好的方式是建立一個如列表2所示的服務元件(serviced component)基類。

Transaction(TransactionOption.Supported), _
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

  列表2.服務元件基類

  在這段代碼中,DALServicedBase類包含的準系統與列表1中的相同,但是加上了從System.EnterpriseServices名字空間的ServicedComponent的繼承,並且包括了一些屬性,指明組件支援物件建構、事務和靜態跟蹤。接著該基類仔細地捕捉元件服務管理器(Component Services Manager)中的構造字串並且再次建立和暴露SqlConnection對象。我們要注意的是當一個類繼承自DALServicedBase時,它也繼承了屬性的設定。換句話說,一個衍生類的事務選項也設定為Supported。如果衍生類想重載這種行為,它能在類的層次重新定義該屬性。

  此外,衍生類在適當情況下應該有利於自身重載和共用方法。使用重載的方法(一個方法有多個調用訊號)在本質上有兩種情況。首先,它們在一個方法需要接受多種類型的參數時使用。架構組件中的典型例子是System.Convert類的方法。例如ToString方法包含18個接受一個參數的重載方法,每個重載方法的類型不同。其次,重載的方法用於暴露參數數量不斷增長的訊號,而不是不同類型的必要參數。在資料訪問層中這類重載變得效率很高,因為它能用於為資料檢索和修改暴露交替的訊號。例如GetOrders方法可以重載,這樣一個訊號不接受參數並返回所有訂單,但是附加的訊號接受參數以表明調用程式希望檢索特定的顧客訂單,代碼如下:

Public Overloads Function GetOrders() As DataSet
Public Overloads Function GetOrders(ByVal customerId As Integer) As DataSet

  這種情況下的一個好的實現技巧是抽象GetOrders方法的功能到一個能被每個重載訊號調用的私人的或者受保護的方法中。

  共用方法(C#中的靜態方法)也能用於暴露資料訪問類的所有執行個體能夠訪問的欄位、屬性和方法。儘管共用成員不能與使用元件服務(Component Services)的類一起使用,但是對於在資料訪問類的共用建構函式中檢索並被所有執行個體讀取的唯讀資料是有用的。使用共用成員讀/寫資料時要小心,因為為了訪問該共用資料,執行的多個線程可能會競爭。

  規則2:堅持設計指導

  隨Visual Studio .NET一起發布的線上文檔中有一個叫"類庫開發人員的設計指導(Design Guidelines for Class Library Developers)"的主題,它覆蓋了類、屬性和方法的名字轉換,是重載的成員、建構函式和事件的補充模式。你必須遵循名字轉換的主要原因之一是.NET架構組件提供的跨語言(cross-language)繼承。如果你在Visual Basic .NET中建立一個資料訪問層基類,你想確保使用.NET架構組件相容的其它語言的開發人員能繼承它並容易理解它怎樣工作。通過堅持我概述的指導方針,你的名字轉換和構造就不會是語言特定的(language specific)。例如,你可能注意到在本文例子的代碼中第一個詞小寫,並加上intercaps是用於方法的參數的,每個詞大寫是用於方法的,基類使用Base標誌來標識它是一個抽象類別。

  可以推測.NET架構組件設計指導都是普通設計模式,像Gang of Four (Addison-Wesley, 1995)寫的Design Patterns記載的一樣。例如.NET架構組件使用了Observer模式的一個變體,叫做Event模式,在類中暴露事件時你必須遵循它。

  規則3:利用基礎結構(Infrastructure)

  .NET架構組件包括一些類和構造,它們能輔助處理通常的與基礎結構相關的事務,例如裝置和異常處理。通過基類把這些概念與繼承組合起來將非常強大。例如,你能考慮一下System.Diagnostics名字空間中暴露的跟蹤功能。除了提供Trace和Debug類外,該名字空間還包括衍生自Switch和TraceListener的類。Switch類的BooleanSwitch和TraceSwitch能被配置用於開啟和關閉應用程式和設定檔,在TraceSwitch中可以暴露多層次跟蹤。TraceListener類的TextWriterTraceListener和EventLogTraceListener分別將Trace和Debug方法的輸入定位到文字檔和事件記錄。

  這樣作的結果是給基類添加了跟蹤功能,使衍生類記錄訊息日誌更簡單。接著應用程式能使用設定檔控制是否允許跟蹤。你能包括一個BooleanSwitch類型的私人變數並在建構函式中執行個體化它來給列表1中的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 _dalSwitch.Enabled
End Get
Set(ByVal Value As Boolean)
_dalSwitch.Enabled = Value
End Set
End Property

Protected Sub WriteTrace(ByVal message As String)
Trace.WriteLineIf(Me.TracingEnabled, Now & ": " & message)
End Sub

  通過這種途徑,衍生類自己並不知道開關(switch)和監聽(listener)類,當資料訪問類產生一個有意義的訊號時能夠簡單地調用WriteTrace方法。

type="System.Diagnostics.TextWriterTraceListener"
initializeData="DALLog.txt" />

  列表3.跟蹤的設定檔

  為了建立一個監聽器並開啟它,需要使用應用程式設定檔。列表3顯示了一個簡單的設定檔,它能夠開啟剛才顯示的資料訪問類開關,並通過myListener調用TextWriterTraceListener把輸出定位到檔案DALLog.txt中。當然,你能通過從TraceListener類衍生程式化地建立監聽器並把該監聽器直接包含在資料訪問類中。

Public Class DALException : Inherits ApplicationException
Public Sub New()
MyBase.New()
End Sub

Public Sub New(ByVal message As String)
MyBase.New(message)
End Sub

Public Sub New(ByVal message As String, ByVal innerException As
Exception)
MyBase.New(message, innerException)
End Sub
'在這兒添加自訂成員
Public ConnectString As String
End Class

  列表4.自訂異常類

  你從中收益的第二個基礎結構是結構化異常處理(SEH)。在最基本的層次,資料訪問類能夠暴露它的衍生自System.ApplicationException 的Exception(異常)對象並能進一步暴露自訂成員。例如,列表4中顯示的DALException對象能用於封裝資料訪問類中的代碼產生的異常。接著基類能暴露一個受保護的方法封裝該異常,組裝自訂成員,並把它發回給調用程式,如下所示:

Protected Sub ThrowDALException(ByVal message As String, _
ByVal innerException As Exception)
Dim newMine As New DALException(message, innerException)

newMine.ConnectString = Me.Connection.ConnectionString
Me.WriteTrace(message & "{" & innerException.Message & "}")
Throw newMine
End Sub

  使用這種方法,衍生類能簡單地調用受保護的方法,傳遞進去一個特定的資料異常(典型的有SqlException或者 OleDbException),該異常被截取並添加了從屬於特定資料域的訊息。基類在DALException中封裝該異常並把它發回到調用程式。這就允許調用程式用一個Catch語句輕易地捕捉所有來自資料訪問類的異常。

  作為選擇之一,你可以看一看MSDN上發布的"Exception Management Application Block Overview"。該架構組件通過一系列對象結合了異常和應用程式記錄檔記錄。實際上,通過從.NET 架構組件提供的BaseApplicationException類衍生的自訂異常類能夠簡單地插入該架構組件。

  規則4:仔細選擇外部介面

  在你設計資料訪問類的方法時,需要考慮它們怎樣接受和返回資料。對大多數開發人員來說,主要有三個選擇:直接使用ADO.NET對象、使用XML、使用自訂類。

  如果直接暴露ADO.NET對象,你能使用一到兩個編程模型。第一個包括資料集和資料表對象,它們對不串連資料訪問很有用。有很多關於資料集和與它關聯的資料表的文章,但是當你必須使用從下層資料存放區斷開的資料時它才最有用處。換句話說,資料集能在應用程式各層之間傳遞,即使那些層在物理上是分布式的,當業務和資料服務層放置在同一群伺服器上並且與表現服務分開時也能使用。此外,資料集對象是通過基於XML的Web服務返回資料的理想方法,因為它們是可序列化的,因此能在SOAP回應訊息中返回。

  這與使用實現IDataReader介面的類(例如SqlDataReader 和OleDbDataReader)訪問資料不同。資料閱讀器(data reader)用只向前的,唯讀方式訪問資料。兩者之間最大的不同是資料集和資料表對象能在應用程式定義域之間傳遞,通過傳遞值(by value)實現,然而資料閱讀器能在各處傳遞,但是一般通過引用(by reference)實現。在列表5中,Read和GetValues在伺服器過程中執行並且它們的傳回值複製到用戶端。

  該圖顯示了資料閱讀器怎樣存活在應用程式定義域中,它在那兒它被建立,並且對它的所有訪問結果都在用戶端和伺服器應用程式域之間的迴圈之中。這意味著當資料存取方法在相同的應用程式定義域運行時,應該返回資料閱讀器作為調用者。

  使用資料閱讀器時有兩個問題需要考慮。首先,當你從資料訪問類的一個方法返回資料閱讀器時,你必須考慮與資料閱讀器關聯的連線物件的生存期。預設情況是當調用程式通過資料閱讀器重複時串連仍然是忙的,不幸的是當調用程式結束後,串連仍然開啟,因此它不返回到串連池(如果允許串連池)。但是,當通過傳遞CommandBehavior.CloseConnection 枚舉給command對象的ExecuteReader方法,串連的Close方法被調用時,你能命令資料閱讀器關閉它的串連。

  其次,為了把表現層從特定的架構組件資料提供者(例如SqlClient或者OleDb)中分離出來,調用代碼應該使用IDataReader介面(例如SqlDataReader)而不是具體類型來引用傳回值。通過這種方法,如果應用程式後端從Oracle移植到 SQL Server,或者資料訪問類的一個方法的傳回型別改變了,表現層也不需要更改。

  如果你希望資料訪問類返回XML,你可以從System.Xml名字空間中的XmlDocument和XmlReader中選擇一個,它與資料集和IDataReader類似。換句話說,當資料從資料來源斷開時你的方法應該返回一個XmlDocument(或者XmlDataDocument),然而XmlReader可用於訪問XML資料的流。

  最後,你也能決定與公用屬性一起返回自訂類。這些類可以使用Serialization(序列化)屬性標記,這樣它們就能跨越應用程式定義域複製。另外,如果你從方法中返回多個對象,就需要強化類型(strongly typed)的集合類。

Imports System.Xml.Serialization

_
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 IComparable.CompareTo
Dim b As Book = CType(o, Book)
Return Me.Title.CompareTo(b.Title)
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 (-1 <> IndexOf(productId))
End Function

Public Overloads Function IndexOf(ByVal productId As Integer) As _
Integer
Dim index As Integer = 0
Dim item As Book

For Each item In Me
If item.ProductID = productId Then
Return index
End If
index = index + 1
Next
Return -1
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 MyBase.Add(value)
End Function
End Class

  列表6.使用自訂類

  上列表(列表6)包含了一個簡單的Book類和與它關聯的集合類的例子。你能注意到Book類用Serializable做了標記,使它跨越應用程式定義域能使用"by value"文法。該類實現了IComparable介面,因此當它包含在一個集合類中的時候,預設情況下它將按Title排序。BookCollection類從System.Collections名字空間的ArrayList衍生,並且為了將該集合限制到Book對象而隱藏了Item屬性和ADD方法。

  通過使用自訂類你完全地控制了資料的表現、開發人員的效率並且沒有依賴ADO.NET的調用。但是這種途徑需要更多的代碼,因為.NET架構組件沒有包含任何與對象相關的技術映射。在這種情況下,你應該在資料訪問類中建立一個資料讀取器並使用它來組合自訂類。

  規則5:抽象.NET架構組件資料提供者

  最後一條規則說明了為什麼和怎樣抽象資料訪問類內部使用的.NET架構組件資料提供者(data provider)。先前我說過ADO.NET編程模型暴露了特定的.NET架構組件資料提供者,包括SqlClient、OleDb和其它MSDN Online Web網站上可用的。但是這種設計的結果是提高效能,為資料提供者暴露特定資料來源功能的能力,它強迫你決定使用那種資料提供者編碼。換句話說,開發人員典型地會選擇使用SqlClient或OleDb,接著在各自的名字空間直接對它們的類進行編程。

  如果你想改變.NET架構組件資料提供者,你必須重新編寫資料存取方法。為了避免這種情況發生,你可以使用Abstract Factory設計模式。使用這種模式,你能建立一個簡單的類,它暴露方法來建立主要的.NET架構組件資料提供者對象(command、connection、data adapter和parameter),而那些對象基於傳遞給建構函式的.NET架構組件資料提供者的資訊。列表7中的代碼就是這樣一個簡單的類。

public enum ProviderType :int {SqlClient = 0, OLEDB = 1}

public class ProviderFactory {
public ProviderFactory(ProviderType provider) {
_pType = provider;
_initClass();
}

public ProviderFactory() {
_initClass();
}

private ProviderType _pType = ProviderType.SqlClient;
private bool _pTypeSet = false;
private Type[] _conType, _comType, _parmType, _daType;


private void _initClass() {
_conType = new Type[2];
_comType = new Type[2];
_parmType = new Type[2];
_daType = new Type[2];

// 為提供者初始化類型
_conType[(int)ProviderType.SqlClient] = typeof(SqlConnection);
_conType[(int)ProviderType.OLEDB] = typeof(OleDbConnection);
_comType[(int)ProviderType.SqlClient] = typeof(SqlCommand);
_comType[(int)ProviderType.OLEDB] = typeof(OleDbCommand);
_parmType[(int)ProviderType.SqlClient] = typeof(SqlParameter);
_parmType[(int)ProviderType.OLEDB] = typeof(OleDbParameter);
_daType[(int)ProviderType.SqlClient] = typeof(SqlDataAdapter);
_daType[(int)ProviderType.OLEDB] = typeof(OleDbDataAdapter);
}

public ProviderType Provider {
get {
return _pType;
}
set {
if (_pTypeSet) {
throw new ReadOnlyException("Provider already set to "
+ _pType.ToString());
}
else {
_pType = value;
_pTypeSet = true;
}
}
}
public IDataAdapter CreateDataAdapter(string commandText,IDbConnection
connection) {
IDataAdapter d;
IDbDataAdapter da;

d = (IDataAdapter)Activator.CreateInstance(_daType[(int)_pType],
false);
da = (IDbDataAdapter)d;
da.SelectCommand = this.CreateCommand(commandText, connection);
return d; }

public IDataParameter CreateParameter(string paramName, DbType
paramType) {
IDataParameter p;
p = (IDataParameter)Activator.CreateInstance(_parmType[(int)_pType],
false);
p.ParameterName = paramName;
p.DbType = paramType;
return p;
}

public IDataParameter CreateParameter(string paramName, DbType
paramType, Object value) {
IDataParameter p;
p = (IDataParameter)Activator.CreateInstance(_parmType[(int)_pType],
false);
p.ParameterName = paramName;
p.DbType = paramType;
p.Value = value;
return p;
}

public IDbConnection CreateConnection(string connect) {
IDbConnection c;
c = (IDbConnection)Activator.CreateInstance(_conType[(int)_pType],
false);
c.ConnectionString = connect;
return c;
}

public IDbCommand CreateCommand(string cmdText, IDbConnection
connection) {
IDbCommand c;
c = (IDbCommand)Activator.CreateInstance(_comType[(int)_pType],
false);
c.CommandText = cmdText;
c.Connection = connection;
return c;
}
}

  列表7. ProviderFactory

  為了使用該類,資料訪問類的代碼必須對多個.NET架構組件資料提供者實現的介面(包括IDbCommand、IDbConnection、IDataAdapter和IDataParameter)進行編程。例如,為了使用一個參數化預存程序的傳回值來填充資料集,必須在資料訪問類的某個方法中有下面的代碼:

Dim _pf As New ProviderFactory(ProviderType.SqlClient)
Dim cn As IDbConnection = _pf.CreateConnection(_connect)
Dim da As IDataAdapter = _pf.CreateDataAdapter("usp_GetBook", cn)

Dim db As IDbDataAdapter = CType(da, IDbDataAdapter)
db.SelectCommand.CommandType = CommandType.StoredProcedure
db.SelectCommand.Parameters.Add(_pf.CreateParameter("@productId",DbType.Int32, id))

Dim ds As New DataSet("Books")
da.Fill(ds)

  典型的情況是你在類的層次聲明ProviderFactory變數並在資料訪問類的建構函式中執行個體化它。另外,它的建構函式與從設定檔中讀取的提供者一起組裝,而不應該是硬代碼。你可以想象,ProviderFactory是資料訪問類的一個重大的補充,並且能被包括進組件,分發給其它的開發人員。

  結論

  在Web服務時代將建立越來越多的應用程式操作來自獨立的應用程式層的資料。如果你遵循一些基本規則並形成習慣,編寫資料存取碼將更快、更容易,並且更能重新使用,把你的錯誤儲存到伺服器,允許你保持資料獨立。



相關文章

Beyond APAC's No.1 Cloud

19.6% IaaS Market Share in Asia Pacific - Gartner IT Service report, 2018

Learn more >

Apsara Conference 2019

The Rise of Data Intelligence, September 25th - 27th, Hangzhou, China

Learn more >

Alibaba Cloud Free Trial

Learn and experience the power of Alibaba Cloud with a free trial worth $300-1200 USD

Learn more >

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。