Posted on 2006-07-27 09:13 天使の淚 閱讀(13) 評論(0) 編輯 收藏 引用 收藏至365Key 所屬分類: asp.net
這篇文章真是不錯,詳細易懂。
Karl Seguin
Microsoft Corporation
摘要:有些情況下,非類型化的 DataSet 可能并非資料操作的最佳解決方案。本指南的目的就是探讨 DataSet 的一種替代解決方案,即:自定義實體與集合。(本文包含一些指向英文站點的連結。)
本頁内容
| 引言 |
| DataSet 存在的問題 |
| 自定義實體類 |
| 對象關系映射 |
| 自定義集合 |
| 管理關系 |
| 進階内容 |
| 小結 |
引言
ADODB.RecordSet 和常常被遺忘的 MoveNext 的時代已經過去,取而代之的是 Microsoft ADO.NET 強大而又靈活的功能。我們的新武器就是 System.Data 名稱空間,它的特點是具有速度極快的 DataReader 和功能豐富的 DataSet,而且打包在一個面向對象的強大模型中。能夠使用這樣的工具一點都不奇怪。任何 3 層體系結構都依靠可靠的資料通路層 (DAL) 将資料層與業務層完美地連接配接起來。高品質的 DAL 有助于改善代碼的重新使用,它是獲得高性能的關鍵,而且是完全透明的。
随着工具的改進,我們的開發模式也發生了變化。告别 MoveNext 并不隻是讓我們擺脫了繁瑣的文法,它還讓我們認識了斷開連接配接的資料,這種資料對我們開發應用程式的方式産生了深刻的影響。
因為我們已經熟悉了 DataReader(其行為與 RecordSet 非常類似),是以沒花多長時間就進一步開發出 DataAdapter、DataSet、DataTable 和 DataView。正是在開發這些新對象的過程中不斷得到磨煉的技能改變了我們的開發方式。斷開連接配接的資料使我們可以利用新的緩存技術,進而大大提高了應用程式的性能。這些類的功能使我們能夠編寫出更智能、更強大的函數,同時還能減少(有時候甚至是大大減少)常見活動所需的代碼數量。
有些情況下非常适合使用 DataSet,例如在設計原型、開發小型系統和支援實用程式時。但是,在企業系統中使用 DataSet 可能并不是最佳的解決方案,因為對企業系統來說,易于維護要比投入市場的時間更重要。本指南的目的就是探讨一種适合處理此類工作的 DataSet 的替代解決方案,即:自定義實體與集合。盡管還存在其他替代解決方案,但它們都無法提供相同的功能或無法獲得更多的支援。我們的首要任務是了解 DataSet 的缺點,以便了解我們要解決的問題。
記住,每種解決方案都有優缺點,是以 DataSet 的缺點可能比自定義實體的缺點(我們也将進行讨論)更容易讓您接受。您和您的團隊必須自己決定哪個解決方案更适合您的項目。記住要考慮解決方案的總成本,包括要求改變的實質所在以及生産後所需的時間比實際開發代碼的時間更長的可能性。最後請注意,我所說的 DataSet 并不是類型化的 DataSet,但它确實可以彌補非類型化的 DataSet 的一些缺點。
傳回頁首
DataSet 存在的問題
缺少抽象
尋找替代解決方案的第一個也是最明顯的原因就是 DataSet 無法從資料庫結構中提取代碼。DataAdapter 可以很好地使您的代碼獨立于基礎資料庫供應商(Microsoft、Oracle、IBM 等),但不能抽象出資料庫的核心元件:表、列和關系。這些核心資料庫元件也是 DataSet 的核心元件。DataSet 和資料庫不僅共享通用元件,不幸的是,它們還共享架構。假定有下面這樣一個 Select 語句:
SELECT UserId, FirstName, LastName
FROM Users
我們知道這些值可以從 DataSet 中的 UserId、FirstName 和 LastName 這些 DataColumn 中獲得。
為什麼會這麼複雜?讓我們看一個基本的日常示例。首先我們有一個簡單的 DAL 函數:
'Visual Basic .NET
Public Function GetAllUsers() As DataSet
Dim connection As New SqlConnection(CONNECTION_STRING)
Dim command As SqlCommand = New SqlCommand("GetUsers", connection)
command.CommandType = CommandType.StoredProcedure
Dim da As SqlDataAdapter = New SqlDataAdapter(command)
Try
Dim ds As DataSet = New DataSet
da.Fill(ds)
Return ds
Finally
connection.Dispose()
command.Dispose()
da.Dispose()
End Try
End Function
//C#
public DataSet GetAllUsers() {
SqlConnection connection = new SqlConnection(CONNECTION_STRING);
SqlCommand command = new SqlCommand("GetUsers", connection);
command.CommandType = CommandType.StoredProcedure;
SqlDataAdapter da = new SqlDataAdapter(command);
try {
DataSet ds = new DataSet();
da.Fill(ds);
return ds;
}finally {
connection.Dispose();
command.Dispose();
da.Dispose();
}
}
然後我們有一個頁面,它使用重複器顯示所有使用者:
<HTML>
<body>
<form id="Form1" method="post" runat="server">
<asp:Repeater ID="users" Runat="server">
<ItemTemplate>
<%# DataBinder.Eval(Container.DataItem, "FirstName") %>
<br />
</ItemTemplate>
</asp:Repeater>
</form>
</body>
</HTML>
<script runat="server">
public sub page_load
users.DataSource = GetAllUsers()
users.DataBind()
end sub
</script>
正如我們所看到的那樣,我們的 ASPX 頁面利用 DAL 函數 GetAllUsers 作為重複器的 DataSource。如果由于某種原因(為了性能而降級、為清楚起見而進行了标準化、要求發生了變化)導緻資料庫架構發生變化,變化就會一直影響 ASPX,即影響使用“FirstName”列名的 Databinder.Eval 行。這将立刻在您腦海中産生一個危險信号:資料庫架構的變化會一直影響到 ASPX 代碼嗎?聽起來不太像 N 層,對嗎?
如果我們所要做的隻是對列進行簡單的重命名,那麼更改本例中的代碼并不複雜。但是,如果在許多地方都使用了 GetAllUsers,更糟糕的是,如果将其作為為無數使用者提供服務的 Web 服務,那又會怎麼樣呢?怎樣才能輕松或安全地傳播更改?對于這個基本示例而言,存儲過程本身作為抽象層可能已經足夠;但是依賴存儲過程獲得除最基本的保護以外的功能則可能會在以後造成更大的問題。可以将此視為一種寫死;實質上,使用 DataSet 時,您可能需要在資料庫架構(不管使用列名稱還是序号位置)和應用層/業務層之間建立一個嚴格的連接配接。但願以前的經驗(或邏輯)已經讓您了解到寫死對維護工作以及将來的開發産生的影響。
DataSet 無法提供适當抽象的另一個原因是它要求開發人員必須了解基礎架構。我們所說的不是基礎知識,而是關于列名稱、類型和關系的所有知識。去掉這個要求不僅使您的代碼不像我們看到的那樣容易中斷,還使代碼更易于編寫和維護。簡單地說:
Convert.ToInt32(ds.Tables[0].Rows[i]["userId"]);
不僅難于閱讀,而且需要非常熟悉列名稱及其類型。理想情況下,您的業務層不需要知道有關基礎資料庫、資料庫架構或 SQL 的任何内容。如果您像上述代碼字元串中那樣使用 DataSet(使用 CodeBehind 并不會有任何改善),您的業務層可能會很薄。
弱類型
DataSet 屬于弱類型,是以容易出錯,還可能會影響您的開發工作。這意味着無論何時從 DataSet 中檢索值,值都以 System.Object 的形式傳回,您需要對這種值進行轉換。您面臨轉換可能會失敗的風險。不幸的是,失敗不是在編譯時發生,而是在運作時發生。另外,在處理弱類型的對象時,Microsoft Visual Studio.NET (VS.NET) 等工具對您的開發人員并沒有太大的幫助。前面我們說過需要深入了解構架的知識,就是指這個意思。我們再來看一個非常常見的示例:
'Visual Basic.NET
Dim userId As Integer =
? Convert.ToInt32(ds.Tables(0).Rows(0)("UserId"))
Dim userId As Integer = CInt(ds.Tables(0).Rows(0)("UserId"))
Dim userId As Integer = CInt(ds.Tables(0).Rows(0)(0))
//C#
int userId = Convert.ToInt32(ds.Tables[0].Rows[0]("UserId"));
這段代碼顯示了從 DataSet 中檢索值的可能方法——可能您的代碼中到處都需要檢索值(如果不進行轉換,而您使用的又是 Visual Basic .NET,您可能會使用 Option Strict Off 這樣的代碼,而這會給您帶來更大的麻煩。)
不幸的是,這些代碼中的每一行都可能會産生大量的運作時錯誤:
1. | 轉換可能由于以下原因而失敗:
| ||||||
2. | ds.Tables(0) 可能傳回一個空引用(如果 DAL 方法或存儲過程中有任何部分失敗)。 | ||||||
3. | “UserId”可能由于以下原因而是一個無效的列名稱:
|
我們可以修改代碼并以更安全的方式編寫,即為 null/nothing 添加檢查,為轉換添加 try/catch,但這些對開發人員都沒有幫助。
更糟糕的是,正如我們前面所說,這不是抽象的。這意味着,每次要從 DataSet 中檢索 userId 時,您都将面臨上面提到的風險,或者需要對相同的保護性步驟進行重新程式設計(當然,實用程式功能可能會有助于降低風險)。弱類型對象将錯誤從設計時或編譯時(這時總能夠自動檢測并輕松修複錯誤)轉移到運作時(這時的錯誤可能會出現在生産過程中,而且更難查明)。
非面向對象
您不能僅僅因為 DataSet 是對象,而 C# 和 Visual Basic .NET 是面向對象 (OO) 的語言就能以面向對象的方式使用 DataSet。OO 程式設計的“hello world”是一個典型的 Person 類,該類又是 Employee 的子類。但 DataSet 并沒有使此類繼承或其他大多數 OO 技術成為可能(或者至少使它們變得自然/直覺)。Scott Hanselman 是類實體的堅決支援者,他做出了最好的解釋:
“DataSet 是一個對象,對嗎?但它并不是域對象,它不是一個‘蘋果’或‘桔子’,而是一個‘DataSet’類型的對象。DataSet 是一隻碗(它知道支援資料存儲)。DataSet 是一個知道如何儲存行和列的對象,它非常了解資料庫。但是,我不希望傳回碗,我希望傳回域對象,例如‘蘋果’。”1
DataSet 使資料之間保持一種關系,使它們更強大并且能夠在關系資料庫中友善地使用。不幸的是,這意味着您将失去 OO 的所有優點。
因為 DataSet 不能作為域對象,是以無法向它們添加功能。通常情況下,對象具有字段、屬性和方法,它們的行為針對的是類的執行個體。例如,您可能會将 Promote 或 CalcuateOvertimePay 函數與 User 對象相關聯,該對象可以通過 someUser.Promote() 或 someUser.CalculateOverTimePay() 安全地調用。因為無法向 DataSet 添加方法,是以您需要使用實用程式功能來處理弱類型對象,并且在整個代碼中包含寫死值的更多執行個體。您一般會以過程代碼結束,在過程代碼中,您要麼不斷地從 DataSet 中擷取資料,要麼以繁瑣的方式将它們存儲在本地變量中并向其他位置傳遞。兩種方法都有缺點,而且都沒有任何優點。
與 DataSet 相反的情況
如果您認為資料通路層應傳回 DataSet,您可能會漏掉一些重要的優點。其中一個原因是您可能正在使用一個較薄或不存在的業務層,除了其他問題外,它還限制了您進行抽象的能力。另外,因為您使用的是一般的預編譯解決方案,是以很難利用 OO 技術。最後,Visual Studio.NET 等工具使開發人員無法輕松地利用弱類型對象(例如 DataSet),是以降低了效率并且增加了出錯的可能性。
所有這些因素都以不同的方式對代碼的可維護性産生了直接的影響。缺乏抽象使功能改善和錯誤修複變得更複雜、更危險。您無法充分利用 OO 提供的代碼重新使用或可讀性方面的改進。當然還有一點,無論您的開發人員處理的是業務邏輯還是表示邏輯,他們都必須非常了解您的基礎資料結構。
傳回頁首
自定義實體類
與 DataSet 有關的大多數問題都可以利用 OO 程式設計的豐富功能在定義明确的業務層中解決。實際上,我們希望獲得按照關系組織的資料(資料庫),并将資料作為對象(代碼)使用。這個概念就是,不是獲得儲存汽車資訊的 DataTable,而是獲得汽車對象(稱為自定義實體或域對象)。
在了解自定義實體之前,讓我們首先看一看我們将要面臨的挑戰。最明顯的挑戰就是所需代碼的數量。我們不是簡單地擷取資料并自動填充 DataSet,而是擷取資料并手動将資料映射到自定義實體(必須先建立好)。由于這是一項重複性的任務,我們可以使用代碼生成工具或 O/R 映射器(後文有詳細的介紹)來減輕工作量。更大的問題是将資料從關系世界映射到對象世界的具體過程。對于簡單的系統,映射通常是直接的,但是随着複雜性的增加,這兩個世界之間的差異就會産生問題。例如,繼承在對象世界中是獲得代碼重新使用以及可維護性的重要技術。不幸的是,繼承對關系資料庫來說卻是一個陌生的概念。另外一個例子就是處理關系的方式不同:對象世界依靠維護單個對象的引用,而關系世界則是利用外鍵。
因為代碼的數量以及關系資料和對象之間的差異不斷增加,看起來這個方法并不太适合更複雜的系統,但事實正好相反。通過将各種問題隔離到一個層中,即映射過程(同樣可以自動化),複雜的系統也可以從此方法獲益。另外,此方法已經很常用,這意味着可以通過幾種已有的設計模式徹底解決增加的複雜性。前面讨論的 DataSet 的缺點在複雜系統中将成倍擴大,最後您會得出這樣一個系統,它欠缺靈活應變能力的缺點恰好超出其建構的難度。
什麼是自定義實體?
自定義實體是代表業務域的對象,是以,它們是業務層的基礎。如果您有一個使用者身份驗證元件(本指南通篇都使用該示例進行講解),您就可能具有 User 和 Role 對象。電子商務系統可能具有 Supplier 和 Merchandise 對象,而房地産公司則可能具有 House、Room 和 Address 對象。在您的代碼中,自定義實體隻是一些類(實體和“類”之間具有非常密切的關系,就像在 OO 程式設計中使用的那樣)。一個典型的 User 類可能如下所示:
'Visual Basic .NET
Public Class User
#Region "Fields and Properties"
Private _userId As Integer
Private _userName As String
Private _password As String
Public Property UserId() As Integer
Get
Return _userId
End Get
Set(ByVal Value As Integer)
_userId = Value
End Set
End Property
Public Property UserName() As String
Get
Return _userName
End Get
Set(ByVal Value As String)
_userName = Value
End Set
End Property
Public Property Password() As String
Get
Return _password
End Get
Set(ByVal Value As String)
_password = Value
End Set
End Property
#End Region
#Region "Constructors"
Public Sub New()
End Sub
Public Sub New(id As Integer, name As String, password As String)
Me.UserId = id
Me.UserName = name
Me.Password = password
End Sub
#End Region
End Class
//C#
public class User {
#region "Fields and Properties"
private int userId;
private string userName;
private string password;
public int UserId {
get { return userId; }
set { userId = value; }
}
public string UserName {
get { return userName; }
set { userName = value; }
}
public string Password {
get { return password; }
set { password = value; }
}
#endregion
#region "Constructors"
public User() {}
public User(int id, string name, string password) {
this.UserId = id;
this.UserName = name;
this.Password = password;
}
#endregion
}
為什麼能夠從它們獲益?
使用自定義實體獲得的主要好處來自這樣一個簡單的事實,即它們是完全受您控制的對象。具體而言,它們允許您:
• | 利用繼承和封裝等 OO 技術。 |
• | 添加自定義行為。 |
例如,我們的 User 類可以通過為其添加 UpdatePassword 函數而受益(我們可能會使用外部/實用程式函數對資料集執行此類操作,但會影響可讀性/維護性)。另外,它們屬于強類型,這表示我們可以獲得 IntelliSense 支援:
圖 1:User 類的 IntelliSense
最後,因為自定義實體為強類型,是以不太需要進行容易出錯的強制轉換:
Dim userId As Integer = user.UserId
'與
Dim userId As Integer =
? Convert.ToInt32(ds.Tables("users").Rows(0)("UserId"))
傳回頁首
對象關系映射
正如前文所讨論的那樣,此方法的主要挑戰之一就是處理關系資料和對象之間的差異。因為我們的資料始終存儲在關系資料庫中,是以我們隻能在這兩個世界之間架起一座橋梁。對于上文的 User 示例,我們可能希望在資料庫中建立一個如下所示的使用者表:
圖 2:User 的資料視圖
從這個關系架構映射到自定義實體是一個非常簡單的事情:
'Visual Basic .NET
Public Function GetUser(ByVal userId As Integer) As User
Dim connection As New SqlConnection(CONNECTION_STRING)
Dim command As New SqlCommand("GetUserById", connection)
command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId
Dim dr As SqlDataReader = Nothing
Try
connection.Open()
dr = command.ExecuteReader(CommandBehavior.SingleRow)
If dr.Read Then
Dim user As New User
user.UserId = Convert.ToInt32(dr("UserId"))
user.UserName = Convert.ToString(dr("UserName"))
user.Password = Convert.ToString(dr("Password"))
Return user
End If
Return Nothing
Finally
If Not dr is Nothing AndAlso Not dr.IsClosed Then
dr.Close()
End If
connection.Dispose()
command.Dispose()
End Try
End Function
//C#
public User GetUser(int userId) {
SqlConnection connection = new SqlConnection(CONNECTION_STRING);
SqlCommand command = new SqlCommand("GetUserById", connection);
command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId;
SqlDataReader dr = null;
try{
connection.Open();
dr = command.ExecuteReader(CommandBehavior.SingleRow);
if (dr.Read()){
User user = new User();
user.UserId = Convert.ToInt32(dr["UserId"]);
user.UserName = Convert.ToString(dr["UserName"]);
user.Password = Convert.ToString(dr["Password"]);
return user;
}
return null;
}finally{
if (dr != null && !dr.IsClosed){
dr.Close();
}
connection.Dispose();
command.Dispose();
}
}
我們仍然按照通常的方式設定連接配接和指令對象,但接着建立了 User 類的一個新執行個體并從 DataReader 中填充該執行個體。您仍然可以在此函數中使用 DataSet 并将其映射到您的自定義實體,但 DataSet 相對于 DataReader 的主要好處是前者提供了資料的斷開連接配接的視圖。在本例中,User 執行個體提供了斷開連接配接的視圖,使我們可以利用 DataReader 的速度。
等一下!您并沒有解決任何問題!
細心的讀者可能注意到我前面提到 DataSet 的問題之一是它們并非強類型,這導緻效率降低并增加了出現運作時錯誤的可能性。它們還需要開發人員深入了解基礎資料結構。看一看上文的代碼,您可能會注意到這些問題依然存在。但請注意,我們已經将這些問題封裝到一個非常孤立的代碼區域内;這表示您的類實體的使用者(Web 界面、Web 服務使用者、Windows 表單)仍然完全沒有意識到這些問題。相反,使用 DataSet 可以将這些問題分散到整個代碼中。
改進
上文的代碼對顯示映射的基本概念很有用,但可以在兩個關鍵的方面進行改進。首先,我們需要提取并将代碼填充到其自己的函數中,因為代碼有可能會被重新使用:
'Visual Basic .NET
Public Function PopulateUser(ByVal dr As IDataRecord) As User
Dim user As New User
user.UserId = Convert.ToInt32(dr("UserId"))
'檢查 NULL 的示例
If Not dr("UserName") Is DBNull.Value Then
user.UserName = Convert.ToString(dr("UserName"))
End If
user.Password = Convert.ToString(dr("Password"))
Return user
End Function
//C#
public User PopulateUser(IDataRecord dr) {
User user = new User();
user.UserId = Convert.ToInt32(dr["UserId"]);
//檢查 NULL 的示例
if (dr["UserName"] != DBNull.Value){
user.UserName = Convert.ToString(dr["UserName"]);
}
user.Password = Convert.ToString(dr["Password"]);
return user;
}
第二個需要注意的事項是,我們不對映射函數使用 SqlDataReader,而是使用 IDataRecord。這是所有 DataReader 實作的接口。使用 IDataRecord 使我們的映射過程獨立于供應商。也就是說,我們可以使用上一個函數從 Access 資料庫中映射 User,即使它使用 OleDbDataReader 也可以。如果您将這個特定的方法與 Provider Model Design Pattern(連結 1、連結 2)結合使用,您的代碼就可以輕松地用于不同的資料庫提供程式。
最後,以上代碼說明了封裝的強大功能。處理 DataSet 中的 NULL 并非最簡單的事,因為每次提取值時都需要檢查它是否為 NULL。使用上述填充方法,我們在一個地方就輕松地解決了此問題,使我們的客戶無需處理它。
映射到何處?
關于此類資料通路和映射函數的歸屬問題存在一些争論,即究竟是作為獨立類的一部分,還是作為适當自定義實體的一部分。将所有使用者相關的任務(擷取資料、更新和映射)都作為 User 自定義實體的一部分當然很不錯。這在資料庫架構與自定義實體很相似時會很有用(比如在本例中)。随着系統複雜性的增加,這兩個世界的差異開始顯現出來,将資料層和業務層明确分離對簡化維護有很大的幫助(我喜歡将其稱為資料通路層)。将通路和映射代碼放在其自己的層 (DAL) 上有一個副作用,即它為確定資料層與業務層的明确分離提供了一個嚴格的原則:
“永遠不要從 System.Data 傳回類或從 DAL 傳回子命名空間”
傳回頁首
自定義集合
到目前為止,我們隻了解了如何處理單個實體,但您經常需要處理多個對象。一個簡單的解決方案是将多個值存儲在一個一般的集合(例如 Arraylist)中。這并非最理想的解決方案,因為它又産生了與 DataSet 有關的一些問題,即:
• | 它們不是強類型,并且 |
• | 無法添加自定義行為。 |
最能滿足我們需求的解決方案是建立我們自己的自定義集合。幸虧 Microsoft .NET Framework 提供了一個專門為了此目的而繼承的類:CollectionBase。CollectionBase 的工作原理是,将所有類型的對象都存儲在專有 Arraylist 中,但是通過隻接受特定類型(例如 User 對象)的方法來提供對這些專有集合的通路。也就是說,将弱類型代碼封裝在強類型的 API 中。
雖然自定義集合可能看起來有很多代碼,但大多數都可以由代碼生成功能或通過剪切和粘貼友善地完成,并且通常隻需要一次搜尋和替換即可。讓我們看一看構成 User 類的自定義集合的不同部分:
'Visual Basic .NET
Public Class UserCollection
Inherits CollectionBase
Default Public Property Item(ByVal index As Integer) As User
Get
Return CType(List(index), User)
End Get
Set
List(index) = value
End Set
End Property
Public Function Add(ByVal value As User) As Integer
Return (List.Add(value))
End Function
Public Function IndexOf(ByVal value As User) As Integer
Return (List.IndexOf(value))
End Function
Public Sub Insert(ByVal index As Integer, ByVal value As User)
List.Insert(index, value)
End Sub
Public Sub Remove(ByVal value As User)
List.Remove(value)
End Sub
Public Function Contains(ByVal value As User) As Boolean
Return (List.Contains(value))
End Function
End Class
//C#
public class UserCollection :CollectionBase {
public User this[int index] {
get {return (User)List[index];}
set {List[index] = value;}
}
public int Add(User value) {
return (List.Add(value));
}
public int IndexOf(User value) {
return (List.IndexOf(value));
}
public void Insert(int index, User value) {
List.Insert(index, value);
}
public void Remove(User value) {
List.Remove(value);
}
public bool Contains(User value) {
return (List.Contains(value));
}
}
通過實作 CollectionBase 可以完成更多任務,但上面的代碼代表了自定義集合所需的核心功能。觀察一下 Add 函數,可以看出我們隻是簡單地将對 List.Add(它是一個 Arraylist)的調用封裝到僅允許 User 對象的函數中。
映射自定義集合
将我們的關系資料映射到自定義集合的過程與我們對自定義實體執行的過程非常相似。我們不再建立一個實體并将其傳回,而是将該實體添加到集合中并循環到下一個:
'Visual Basic .NET
Public Function GetAllUsers() As UserCollection
Dim connection As New SqlConnection(CONNECTION_STRING)
Dim command As New SqlCommand("GetAllUsers", connection)
Dim dr As SqlDataReader = Nothing
Try
connection.Open()
dr = command.ExecuteReader(CommandBehavior.SingleResult)
Dim users As New UserCollection
While dr.Read()
users.Add(PopulateUser(dr))
End While
Return users
Finally
If Not dr Is Nothing AndAlso Not dr.IsClosed Then
dr.Close()
End If
connection.Dispose()
command.Dispose()
End Try
End Function
//C#
public UserCollection GetAllUsers() {
SqlConnection connection = new SqlConnection(CONNECTION_STRING);
SqlCommand command =new SqlCommand("GetAllUsers", connection);
SqlDataReader dr = null;
try{
connection.Open();
dr = command.ExecuteReader(CommandBehavior.SingleResult);
UserCollection users = new UserCollection();
while (dr.Read()){
users.Add(PopulateUser(dr));
}
return users;
}finally{
if (dr != null && !dr.IsClosed){
dr.Close();
}
connection.Dispose();
command.Dispose();
}
}
我們從資料庫中獲得資料、建立自定義集合,然後通過在結果中循環來建立每個 User 對象并将其添加到集合中。同樣要注意 PopulateUser 映射函數是如何重新使用的。
添加自定義行為
在讨論自定義實體時,我們隻是泛泛地提到可以将自定義行為添加到類中。您向實體中添加的功能類型很大程度上取決于您要實作的業務邏輯的類型,但您可能希望在自定義集合中實作某些常見的功能。一個示例就是傳回一個基于某個鍵的實體,例如基于 userId 的使用者:
'Visual Basic .NET
Public Function FindUserById(ByVal userId As Integer) As User
For Each user As User In List
If user.UserId = userId Then
Return user
End If
Next
Return Nothing
End Function
//C#
public User FindUserById(int userId) {
foreach (User user in List) {
if (user.UserId == userId){
return user;
}
}
return null;
}
另一個示例可能是傳回基于特定标準(例如部分使用者名)的使用者子集:
'Visual Basic .NET
Public Function FindMatchingUsers(ByVal search As String) As UserCollection
If search Is Nothing Then
Throw New ArgumentNullException("search cannot be null")
End If
Dim matchingUsers As New UserCollection
For Each user As User In List
Dim userName As String = user.UserName
If Not userName Is Nothing And userName.StartsWith(search) Then
matchingUsers.Add(user)
End If
Next
Return matchingUsers
End Function
//C#
public UserCollection FindMatchingUsers(string search) {
if (search == null){
throw new ArgumentNullException("search cannot be null");
}
UserCollection matchingUsers = new UserCollection();
foreach (User user in List) {
string userName = user.UserName;
if (userName != null && userName.StartsWith(search)){
matchingUsers.Add(user);
}
}
return matchingUsers;
}
可以通過 DataTable.Select 以相同的方式使用 DataSets。需要說明的重要一點是,盡管建立自己的功能使您可以完全控制您的代碼,但 Select 方法為完成同樣的操作提供了一個非常友善且不需要編寫代碼的方法。但另一方面,Select 需要開發人員了解基礎資料庫,而且它不是強類型。
綁定自定義集合
我們看到的第一個示例是将 DataSet 綁定到 ASP.NET 控件。考慮到它很普通,您會高興地發現自定義集合綁定同樣很簡單(這是因為 CollectionBase 實作了用于綁定的 Ilist)。自定義集合可以作為任何控件的 DataSource,而 DataBinder.Eval 隻能像您使用 DataSet 那樣使用:
'Visual Basic .NET
Dim users as UserCollection = DAL.GetallUsers()
repeater.DataSource = users
repeater.DataBind()
//C#
UserCollection users = DAL.GetAllUsers();
repeater.DataSource = users;
repeater.DataBind();
<!-- HTML -->
<asp:Repeater onItemDataBound="r_IDB" ID="repeater" Runat="server">
<ItemTemplate>
<asp:Label ID="userName" Runat="server">
<%# DataBinder.Eval(Container.DataItem, "UserName") %><br />
</asp:Label>
</ItemTemplate>
</asp:Repeater>
您可以不使用列名稱作為 DataBinder.Eval 的第二個參數,而指定您希望顯示的屬性名稱,在本例中為 UserName。
對于在許多資料綁定控件提供的 OnItemDataBound 或 OnItemCreated 中執行處理的人來說,您可能會将 e.Item.DataItem 強制轉換成 DataRowView。當綁定到自定義集合時,e.Item.DataItem 則被強制轉換成自定義實體,在我們的示例中為 User 類:
'Visual Basic .NET
Protected Sub r_ItemDataBound (s As Object, e As RepeaterItemEventArgs)
Dim type As ListItemType = e.Item.ItemType
If type = ListItemType.AlternatingItem OrElse
? type = ListItemType.Item Then
Dim u As Label = CType(e.Item.FindControl("userName"), Label)
Dim currentUser As User = CType(e.Item.DataItem, User)
If Not PasswordUtility.PasswordIsSecure(currentUser.Password) Then
ul.ForeColor = Drawing.Color.Red
End If
End If
End Sub
//C#
protected void r_ItemDataBound(object sender, RepeaterItemEventArgs e) {
ListItemType type = e.Item.ItemType;
if (type == ListItemType.AlternatingItem ||
? type == ListItemType.Item){
Label ul = (Label)e.Item.FindControl("userName");
User currentUser = (User)e.Item.DataItem;
if (!PasswordUtility.PasswordIsSecure(currentUser.Password)){
ul.ForeColor = Color.Red;
}
}
}
傳回頁首
管理關系
即使在最簡單的系統中,實體之間也存在關系。對于關系資料庫,可以通過外鍵維護關系;而使用對象時,關系隻是對另一個對象的引用。例如,根據我們前面的示例,User 對象完全可以具有一個 Role:
'Visual Basic .NET
Public Class User
Private _role As Role
Public Property Role() As Role
Get
Return _role
End Get
Set(ByVal Value As Role)
_role = Value
End Set
End Property
End Class
//C#
public class User {
private Role role;
public Role Role {
get {return role;}
set {role = value;}
}
}
或者一個 Role 集合:
'Visual Basic .NET
Public Class User
Private _roles As RoleCollection
Public ReadOnly Property Roles() As RoleCollection
Get
If _roles Is Nothing Then
_roles = New RoleCollection
End If
Return _roles
End Get
End Property
End Class
//C#
public class User {
private RoleCollection roles;
public RoleCollection Roles {
get {
if (roles == null){
roles = new RoleCollection();
}
return roles;
}
}
}
在這兩個示例中,我們有一個虛構的 Role 類或 RoleCollection 類,它們就是類似于 User 和 UserCollection 類的其他自定義實體或集合類。
映射關系
真正的問題在于如何映射關系。讓我們看一個簡單的示例,我們希望根據 userId 及其角色來檢索一個使用者。首先,我們看一看關系模型:
圖 3:User 與 Role 之間的關系
這裡,我們看到了一個 User 表和一個 Role 表,我們可以将這兩個表都以直覺的方式映射到自定義實體。我們還有一個 UserRoleJoin 表,它代表了 User 與 Role 之間的多對多關系。
然後,我們使用存儲過程來擷取兩個單獨的結果:第一個代表 User,第二個代表該使用者的 Role:
CREATE PROCEDURE GetUserById(
@UserId INT
)AS
SELECT UserId, UserName, [Password]
FROM Users
WHERE UserId = @UserID
SELECT R.RoleId, R.[Name], R.Code
FROM Roles R INNER JOIN
UserRoleJoin URJ ON R.RoleId = URJ.RoleId
WHERE URJ.UserId = @UserId
最後,我們從關系模型映射到對象模型:
'Visual Basic .NET
Public Function GetUserById(ByVal userId As Integer) As User
Dim connection As New SqlConnection(CONNECTION_STRING)
Dim command As New SqlCommand("GetUserById", connection)
command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId
Dim dr As SqlDataReader = Nothing
Try
connection.Open()
dr = command.ExecuteReader()
Dim user As User = Nothing
If dr.Read() Then
user = PopulateUser(dr)
dr.NextResult()
While dr.Read()
user.Roles.Add(PopulateRole(dr))
End While
End If
Return user
Finally
If Not dr Is Nothing AndAlso Not dr.IsClosed Then
dr.Close()
End If
connection.Dispose()
command.Dispose()
End Try
End Function
//C#
public User GetUserById(int userId) {
SqlConnection connection = new SqlConnection(CONNECTION_STRING);
SqlCommand command = new SqlCommand("GetUserById", connection);
command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId;
SqlDataReader dr = null;
try{
connection.Open();
dr = command.ExecuteReader();
User user = null;
if (dr.Read()){
user = PopulateUser(dr);
dr.NextResult();
while(dr.Read()){
user.Roles.Add(PopulateRole(dr));
}
}
return user;
}finally{
if (dr != null && !dr.IsClosed){
dr.Close();
}
connection.Dispose();
command.Dispose();
}
}
User 執行個體即被建立和填充;我們轉移到下一個結果/選擇并進行循環,填充 Role 并将它們添加到 User 類的 RolesCollection 屬性中。
傳回頁首
進階内容
本指南的目的是介紹自定義實體與集合的概念及使用。使用自定義實體是業界廣泛采用的做法,是以,也就産生了同樣多的模式以處理各種情況。設計模式具有優勢的原因有很多。首先,在處理具體的情況時,您可能不是第一次碰到某個給定的問題。設計模式使您可以重新使用給定問題的已經過嘗試和測試的解決方案(雖然設計模式并不意味着全盤照抄,但它們幾乎總是能夠為解決方案提供一個可靠的基礎)。相應地,這使您對系統随着複雜性增加而進行縮放的能力充滿了信心,不僅因為它是一個廣泛使用的方法,還因為它具有詳盡的記錄。設計模式還為您提供了一個通用的詞彙表,使知識的傳播和傳授更容易實作。
不能說設計模式隻适用于自定義實體,實際上許多設計模式都并非如此。但是,如果您找機會試一下,您可能會驚喜地發現許多記載詳盡的模式确實适用于自定義實體和映射過程。
最後這一部分專門介紹大型或較複雜的系統可能會碰到的一些進階情況。因為大多數主題都可能值得您單獨學習,是以我會盡量為您提供一些入門資料。
Martin Fowler 的 Patterns of Enterprise Application Architecture 就是一個很好的入門材料,它不僅可以作為常見設計模式的優秀參考(具有詳細的解釋和大量的示例代碼),而且它的前 100 頁确實可以讓您透徹地了解整個概念。另外,Fowler 還提供了一個聯機模式目錄,它對于已經熟悉概念但需要一個便利參考的人士很有用。
并發
前面的示例介紹的都是從資料庫中提取資料并根據這些資料建立對象。總體而言,更新、删除和插入資料等操作是很直覺的。我們的業務層負責建立對象、将對象傳遞給資料通路層,然後讓資料通路層處理對象世界與關系世界之間的映射。例如:
'Visual Basic .NET
Public sub UpdateUser(ByVal user As User)
Dim connection As New SqlConnection(CONNECTION_STRING)
Dim command As New SqlCommand("UpdateUser", connection)
' 可以借助可重新使用的函數對此進行反向映射
command.Parameters.Add("@UserId", SqlDbType.Int)
command.Parameters(0).Value = user.UserId
command.Parameters.Add("@Password", SqlDbType.VarChar, 64)
command.Parameters(1).Value = user.Password
command.Parameters.Add("@UserName", SqlDbType.VarChar, 128)
command.Parameters(2).Value = user.UserName
Try
connection.Open()
command.ExecuteNonQuery()
Finally
connection.Dispose()
command.Dispose()
End Try
End Sub
//C#
public void UpdateUser(User user) {
SqlConnection connection = new SqlConnection(CONNECTION_STRING);
SqlCommand command = new SqlCommand("UpdateUser", connection);
// 可以借助可重新使用的函數對此進行反向映射
command.Parameters.Add("@UserId", SqlDbType.Int);
command.Parameters[0].Value = user.UserId;
command.Parameters.Add("@Password", SqlDbType.VarChar, 64);
command.Parameters[1].Value = user.Password;
command.Parameters.Add("@UserName", SqlDbType.VarChar, 128);
command.Parameters[2].Value = user.UserName;
try{
connection.Open();
command.ExecuteNonQuery();
}finally{
connection.Dispose();
command.Dispose();
}
}
但在處理并發時就不那麼直覺了,也就是說,當兩個使用者試圖同時更新相同的資料時會出現什麼情況呢?預設的行為(如果您沒有執行任何操作)是最後送出資料的人将覆寫以前所有的工作。這可能不是理想的情況,因為一個使用者的工作将在未獲得任何提示的情況下被覆寫。要完全避免所有沖突,一種方法就是使用消極的并發技術;但此方法需要具有某種鎖定機制,這可能很難通過可縮放的方式實作。替代方法就是使用積極的并發技術。讓第一個送出的使用者控制并通知後面的使用者是通常采取的更溫和、更使用者友好的方法。這可以通過某種行版本控制(例如時間戳)來實作。
參考資料:
• | Introduction to Data Concurrency in ADO.NET |
• | CSLA.NET's concurrency techniques |
• | Unit of Work design pattern |
• | Optimistic offline lock design pattern |
• | Pessimistic offline lock design pattern |
性能
與合理的靈活性和功能問題相對的是,我們經常擔心細小的性能差異。盡管性能的确很重要,但提供适用于一切情況而不是最簡單情況的通用原則通常很難。例如,将自定義集合與 DataSet 相比,哪個更快?使用自定義集合,您可以大量使用 DataReader,這是從資料庫中提取資料的較快方式。但答案實際上取決于您使用它們的方式以及處理的資料類型,是以一般性的說明沒有任何用。更重要的一點是要認識到,不管您能節省多少處理時間,與維護性方面的差異相比都可能微不足道。
當然,并不是說您不可能找到一個既具有高性能又可維護的解決方案。雖然我強調說答案實際上取決于您的使用方式,但的确有一些模式可以幫助您最大程度地提高性能。但是,首先要知道的是自定義實體與集合緩存以及 DataSet,并且能夠利用相同的機制(類似于 HttpCache)。DataSet 的優勢之一是它能夠編寫 Select 語句,以便隻擷取所需的資訊。使用自定義實體時,您常常感到不得不填充整個實體以及子實體。例如,如果要通過 DataSet 顯示一個 Organization 清單,您可以隻提取 OganizationId、Name 和 Address 并将其綁定到重複器。使用自定義實體時,我總覺得還需要擷取所有其他的 Organization 資訊,如果該組織通過了 ISO 認證,則可能是一個位标記,即所有員工、其他聯系資訊等的集合。可能其他人沒有碰到這個大難題,但幸運的是,如果我們願意,我們可以對自定義實體進行很好的控制。最常用的方法是使用一種延遲加載模式,它隻在首次需要時擷取資訊(可以很好地封裝在屬性中)。這種對各個屬性的控制提供了通過其他方式無法輕易獲得的巨大靈活性(請想象一下在 DataColumn 級别執行類似操作的情況)。
參考資料:
• | Lazy Load 設計模式 |
• | CSLA.NET lazy load |
排序與篩選
雖然 DataView 對排序和篩選的内置支援需要您了解有關 SQL 和基礎資料結構的知識,但它提供的友善确實是自定義集合所不具備的。我們仍然可以排序和篩選,但首先需要編寫功能。因為技術不一定是最先進的,是以代碼的完整描述不屬于本節要讨論的範圍。大多數技術都很相似,例如使用篩選器類篩選集合以及使用比較器類進行排序,我認為不存在固定的模式。但是,的确存在一些參考資料:
• | Generic sort function |
• | Sorting & Filtering Custom Collections 教程 |
代碼生成
解決概念上的障礙後,自定義實體與集合的主要缺點就是靈活性、抽象和維護性差所導緻的代碼數量的增加。實際上,您可能會認為我所說的維護成本和錯誤的降低這一切都抵不上代碼的增加。雖然這一觀點是成立的(同樣,因為任何解決方案都不是完美無缺的),但可以通過設計模式和架構(例如 CSLA.NET)大大緩解此問題。代碼生成工具與模式和架構完全不同,這些工具可以大大降低您實際需要編寫的代碼數量。本指南最初打算專門辟出一節詳細介紹代碼生成工具,特别是流行的免費 CodeSmith;但現有的許多參考資料都可能超出了我自己對該産品的認識。
在繼續之前,我認識到代碼生成聽起來像天方夜譚一樣。但經過正确的使用和了解後,它的确是您工具包中不可缺少的一個強大的武器,即使您沒有處理自定義實體也是如此。雖然代碼生成的确不僅僅适用于自定義實體,但很多都是專為自定義實體而設計的。原因很簡單:自定義實體需要大量重複代碼。
簡言之,代碼生成是如何工作的?構想聽起來好像遙不可及甚至反而會降低效率,但您基本上通過編寫代碼(模闆)來生成代碼。例如,CodeSmith 附帶了許多強大的類,使您可以連接配接到資料庫并擷取所有屬性:表、列(類型、大小等)和關系。獲得這些資訊後,我們前面讨論的大部分工作都可以自動完成。例如,開發人員可以選擇一個表,然後使用正确的模闆自動建立自定義實體(帶有正确的字段、屬性和構造函數),并獲得映射函數、自定義集合以及基本的選擇、插入、更新和删除功能。甚至還可以更進一步,實作排序、篩選以及我們提到的其他進階功能。
CodeSmith 還附帶了許多現成的模闆,可以作為很好的學習資料。最後,CodeSmith 還為實作 CSLA.NET 架構提供了許多模闆。我最初隻花了幾個小時來學習基本概念、熟悉 CodeSmith 的功能,但它為我節省的時間已經多得無法計算了。另外,如果所有的開發人員都使用相同的模闆,代碼的高度一緻性将使您能夠輕松地繼續其他人的工作。
參考資料:
• | Code Generation with CodeSmith |
• | CodeSmith 首頁 |
O/R 映射器
即使因為對 O/R 映射器知之甚少使我不敢随便對它們發表議論,但它們自身的潛在價值使其不容忽視。代碼生成器生成基于模闆的代碼,供您複制并粘貼到您自己的源代碼中,而 O/R 映射器則在運作時通過某種配置機制動态生成代碼。例如,在 XML 檔案中,您可以指定某個表的列 X 映射到某個實體的屬性 Y。您仍然需要建立自定義實體,但是集合、映射和其他資料通路函數(包括存儲過程)都是動态建立的。從理論上講,O/R 映射器幾乎可以完全解決自定義實體存在的問題。随着關系世界和對象世界的差異越來越明顯以及映射過程越來越複雜,O/R 映射器的價值就變得越發不可限量了。O/R 映射器的兩個缺點據說就是不夠安全和性能較差(至少在 .NET 環境中是這樣)。根據我所閱讀的資料,我确信它們并不是不夠安全,雖然在有些情況下性能較差,但在另外一些情況下卻表現突出。O/R 映射器并不适合所有情況,但如果您要處理複雜的系統,則應嘗試一下它們的功能。
參考資料:
• | Mapper 設計模式 |
• | Data Mapper 設計模式 |
• | Wilson ORMapper |
• | Frans Bouma 關于 O/R 映射的文章 |
• | LLBGenPro |
• | NHibernate |
.NET Framework 2.0 的功能
即将面世的 .NET Framework 2.0 版将改變我們在本指南中讨論的一些實施細節。這些改變将減少支援自定義實體所需的代碼數量,并有助于處理映射問題。
泛型
議論頗多的泛型之是以存在,主要原因之一就是為了向開發人員提供現成的強類型的集合。我們避開 Arraylist 等現有集合是因為它們屬于弱類型。泛型提供了與目前集合同樣的友善性,而且它們屬于強類型。這是通過在聲明時指定類型來實作的。例如,我們可以替換 UserCollection 而不需要增加代碼,然後隻需建立一個 List<T> 泛型的新執行個體并指定我們的 User 類即可:
'Visual Basic .NET
Dim users as new IList(of User)
//C#
IList<User> users = new IList<user>();
聲明後,我們的 user 集合就隻能處理 User 類型的對象了,這為我們提供了編譯時檢查和優化的所有優點。
參考資料:
• | Introducing .NET Generics |
• | An Introduction to C# Generics |
可以為空的類型
可以為空的類型實際上就是由于其他原因而非上述原因而使用的泛型。處理資料庫時面臨的挑戰之一就是正确一緻地處理支援 NULL 的列。在處理字元串和其他類(稱為引用類型)時,您隻需為代碼中的某個變量指定 nothing/null:
'Visual Basic .NET
if dr("UserName") Is DBNull.Value Then
user.UserName = nothing
End If
//C#
if (dr["UserName"] == DBNull.Value){
user.UserName = null;
}
也可以什麼都不做(預設情況下,引用類型為 nothing/null)。這對值類型(例如整數、布爾值、小數等)并不完全一樣。您當然也可以為這些值指定 nothing/null,但這樣将會指定一個預設值。如果您隻聲明整數,或者為其指定 nothing/null,變量的值實際上将為 0。這使其很難映射回資料庫:值究竟為 0 還是 null?可以為空的類型允許值類型具有具體的值或者為空,進而解決了這個問題。例如,如果我們要在 userId 列中支援 null 值(并不是很符合實際情況),我們會首先将 userId 字段和對應的屬性聲明為可以為空的類型:
'Visual Basic .NET
Private _userId As Nullable(Of Integer)
Public Property UserId() As Nullable(Of Integer)
Get
Return _userId
End Get
Set(ByVal value As Nullable(Of Integer))
_userId = value
End Set
End Property
//C#
private Nullable<int> userId;
public Nullable<int> UserId {
get { return userId; }
set { userId = value; }
}
然後利用 HasValue 屬性判斷是否指定了 nothing/null:
'Visual Basic .NET
If UserId.HasValue Then
Return UserId.Value
Else
Return DBNull.Value
End If
//C#
if (UserId.HasValue) {
return UserId.Value;
} else {
return DBNull.Value;
}
參考資料:
• | Nullable types in C# |
• | Nullable types in VB.NET |
疊代程式
我們前面讨論的 UserCollection 示例隻展示了自定義集合中可能需要的基本功能。有一個操作無法通過所提供的實作來完成,即通過一個 foreach 循環在集合中循環。要完成此操作,您的自定義集合必須具有實作 IEnumerable 接口的枚舉數支援類。這是一個非常直覺且重複性較強的過程,但卻引入了更多的代碼。C# 2.0 引入了新的 yield 關鍵字來為您處理此接口的實作細節。Visual Basic .NET 中目前沒有與新的 yield 關鍵字等效的關鍵字。
參考資料:
• | What's new In C# 2.0 - Iterators |
• | C# Iterators |
傳回頁首
小結
請勿輕率地做出向自定義實體與集合轉換的決定。這裡有許多需要考慮的因素。例如,您對 OO 概念的熟悉程度、可用來熟悉新方法的時間以及您打算部署它的環境。雖然總體上它們有很大的優點,但并不一定适合您的特定情況。即使适合您的情況,它們的缺點也可能會打消您使用它們的念頭。還要記住有許多可替代的解決方案。Jimmy Nilsson 在他的 Choosing Data Containers for .NET 中概述了其中的某些替代方案,此專欄系列包括 5 部分(1、2、3、4、5)。
自定義實體使您獲得了面向對象的程式設計的豐富功能,并幫助您建構了可靠、可維護的 N 層體系結構的架構。本指南的目的之一是讓您從構成系統的業務實體,而不是一般的 DataSet 和 DataTable 的角度來考慮您的系統。我們還讨論了一些關鍵的問題,不管您選擇的途徑(即設計模式)、對象世界與關系世界的差異(了解詳細資訊)以及 N 層體系結構是什麼,您都應注意這些問題。請記住,您之前花費的時間會在系統的整個生命周期内為您帶來更多的回報。
相關書籍
• | Microsoft ASP.NET Coding Strategies with the Microsoft ASP.NET Team |
• | Expert C# Business Objects |
• | Expert One-on-One Visual Basic .NET Business Objects |
1http://www.hanselman.com/blog/PermaLink.aspx?guid=d88f7539-10d8-4697-8c6e-1badb08bb3f5
© 2005 Microsoft Corporation 版權所有。保留所有權利。使用規定。