掌握 ASP.NET 之路:自定義實體類簡介

掌握 ASP.NET 之路:自定義實體類簡介
發佈日期 : 5/24/2005  | 更新日期 : 5/24/2005
Karl Seguin
Microsoft Corporation
摘要:有些狀況下,非類型化的  DataSet 可能並不是數據操做的最佳解決方案。本指南的目的就是探討  DataSet 的一種替代解決方案,即:自定義實體與集合。(本文包含一些指向英文站點的連接。)
本頁內容

引言

ADODB.RecordSet 和經常被遺忘的  MoveNext 的時代已通過去,取而代之的是 Microsoft ADO.NET 強大而又靈活的功能。咱們的新武器就是  System.Data 名稱空間,它的特色是具備速度極快的 DataReader 和功能豐富的  DataSet,並且打包在一個面向對象的強大模型中。可以使用這樣的工具一點都不奇怪。任何 3 層體系結構都依靠可靠的數據訪問層 (DAL) 將數據層與業務層完美地鏈接起來。高質量的 DAL 有助於改善代碼的從新使用,它是得到高性能的關鍵,並且是徹底透明的。
隨着工具的改進,咱們的開發模式也發生了變化。告別  MoveNext 並不僅是讓咱們擺脫了繁瑣的語法,它還讓咱們認識了斷開鏈接的數據,這種數據對咱們開發應用程序的方式產生了深入的影響。
由於咱們已經熟悉了  DataReader(其行爲與  RecordSet 很是相似),因此沒花多長時間就進一步開發出  DataAdapterDataSetDataTable 和  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 中的  UserIdFirstName 和  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. 轉換可能因爲如下緣由而失敗:
    • 值可能爲空。
    • 開發人員可能對基礎數據類型判斷有誤(仍是這個問題,即開發人員須要很是熟悉數據庫架構)。
    • 若是您使用序號值,誰知道位置 X 處其實是一個什麼樣的列。
  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 對象,而房地產公司則可能具備  HouseRoom 和  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();
 }
}
但在處理併發時就不那麼直觀了,也就是說,當兩個用戶試圖同時更新相同的數據時會出現什麼狀況呢?默認的行爲(若是您沒有執行任何操做)是最後提交數據的人將覆蓋之前全部的工做。這可能不是理想的狀況,由於一個用戶的工做將在未得到任何提示的狀況下被覆蓋。要徹底避免全部衝突,一種方法就是使用消極的併發技術;但此方法須要具備某種鎖定機制,這可能很難經過可縮放的方式實現。替代方法就是使用積極的併發技術。讓第一個提交的用戶控制並通知後面的用戶是一般採起的更溫和、更用戶友好的方法。這能夠經過某種行版本控制(例如時間戳)來實現。
參考資料:
性能
與合理的靈活性和功能問題相對的是,咱們常常擔憂細小的性能差別。儘管性能的確很重要,但提供適用於一切狀況而不是最簡單狀況的通用原則一般很難。例如,將自定義集合與  DataSet 相比,哪一個更快?使用自定義集合,您能夠大量使用  DataReader,這是從數據庫中提取數據的較快方式。但答案實際上取決於您使用它們的方式以及處理的數據類型,因此通常性的說明沒有任何用。更重要的一點是要認識到,無論您能節省多少處理時間,與維護性方面的差別相比均可能微不足道。
固然,並非說您不可能找到一個既具備高性能又可維護的解決方案。雖然我強調說答案實際上取決於您的使用方式,但的確有一些模式能夠幫助您最大程度地提升性能。可是,首先要知道的是自定義實體與集合緩存以及  DataSet,而且可以利用相同的機制(相似於  HttpCache)。 DataSet 的優點之一是它可以編寫  Select 語句,以便只獲取所需的信息。使用自定義實體時,您經常感到不得不填充整個實體以及子實體。例如,若是要經過  DataSet 顯示一個  Organization 列表,您能夠只提取  OganizationIdName 和  Address 並將其綁定到重複器。使用自定義實體時,我總以爲還須要獲取全部其餘的  Organization信息,若是該組織經過了 ISO 認證,則多是一個位標記,即全部員工、其餘聯繫信息等的集合。可能其餘人沒有碰到這個大難題,但幸運的是,若是咱們願意,咱們能夠對自定義實體進行很好的控制。最經常使用的方法是使用一種延遲加載模式,它只在首次須要時獲取信息(能夠很好地封裝在屬性中)。這種對各個屬性的控制提供了經過其餘方式沒法輕易得到的巨大靈活性(請想象一下在  DataColumn 級別執行相似操做的狀況)。
參考資料:
排序與篩選
雖然  DataView 對排序和篩選的內置支持須要您瞭解有關 SQL 和基礎數據結構的知識,但它提供的方便確實是自定義集合所不具有的。咱們仍然能夠排序和篩選,但首先須要編寫功能。由於技術不必定是最早進的,因此代碼的完整描述不屬於本節要討論的範圍。大多數技術都很類似,例如使用篩選器類篩選集合以及使用比較器類進行排序,我認爲不存在固定的模式。可是,的確存在一些參考資料:
代碼生成
解決概念上的障礙後,自定義實體與集合的主要缺點就是靈活性、抽象和維護性差所致使的代碼數量的增長。實際上,您可能會認爲我所說的維護成本和錯誤的下降這一切都抵不上代碼的增長。雖然這一觀點是成立的(一樣,由於任何解決方案都不是天衣無縫的),但能夠經過設計模式和框架(例如 CSLA.NET)大大緩解此問題。代碼生成工具與模式和框架徹底不一樣,這些工具能夠大大下降您實際須要編寫的代碼數量。本指南最初打算專門闢出一節詳細介紹代碼生成工具,特別是流行的免費 CodeSmith;但現有的許多參考資料均可能超出了我本身對該產品的認識。
在繼續以前,我認識到代碼生成聽起來像天方夜譚同樣。但通過正確的使用和理解後,它的確是您工具包中不可缺乏的一個強大的武器,即便您沒有處理自定義實體也是如此。雖然代碼生成的確不只僅適用於自定義實體,但不少都是專爲自定義實體而設計的。緣由很簡單:自定義實體須要大量重複代碼。
簡言之,代碼生成是如何工做的?構想聽起來好像高不可攀甚至反而會下降效率,但您基本上經過編寫代碼(模板)來生成代碼。例如,CodeSmith 附帶了許多強大的類,使您能夠鏈接到數據庫並獲取全部屬性:表、列(類型、大小等)和關係。得到這些信息後,咱們前面討論的大部分工做均可以自動完成。例如,開發人員能夠選擇一個表,而後使用正確的模板自動建立自定義實體(帶有正確的字段、屬性和構造函數),並得到映射函數、自定義集合以及基本的選擇、插入、更新和刪除功能。甚至還能夠更進一步,實現排序、篩選以及咱們提到的其餘高級功能。
CodeSmith 還附帶了許多現成的模板,能夠做爲很好的學習資料。最後,CodeSmith 還爲實現 CSLA.NET 框架提供了許多模板。我最初只花了幾個小時來學習基本概念、熟悉 CodeSmith 的功能,但它爲我節省的時間已經多得沒法計算了。另外,若是全部的開發人員都使用相同的模板,代碼的高度一致性將使您可以輕鬆地繼續其餘人的工做。
參考資料:
O/R  映射器
即便由於對 O/R 映射器知之甚少使我不敢隨便對它們發表議論,但它們自身的潛在價值使其不容忽視。代碼生成器生成基於模板的代碼,供您複製並粘貼到您本身的源代碼中,而 O/R 映射器則在運行時經過某種配置機制動態生成代碼。例如,在 XML 文件中,您能夠指定某個表的列 X 映射到某個實體的屬性 Y。您仍然須要建立自定義實體,可是集合、映射和其餘數據訪問函數(包括存儲過程)都是動態建立的。從理論上講,O/R 映射器幾乎能夠徹底解決自定義實體存在的問題。隨着關係世界和對象世界的差別愈來愈明顯以及映射過程愈來愈複雜,O/R 映射器的價值就變得愈加不可限量了。O/R 映射器的兩個缺點聽說就是不夠安全和性能較差(至少在 .NET 環境中是這樣)。根據我所閱讀的資料,我確信它們並非不夠安全,雖然在有些狀況下性能較差,但在另一些狀況下卻表現突出。O/R 映射器並不適合全部狀況,但若是您要處理複雜的系統,則應嘗試一下它們的功能。
參考資料:
.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 類型的對象了,這爲咱們提供了編譯時檢查和優化的全部優勢。
參考資料:
能夠爲空的類型
能夠爲空的類型實際上就是因爲其餘緣由而非上述緣由而使用的泛型。處理數據庫時面臨的挑戰之一就是正確一致地處理支持  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;
}
參考資料:
迭代程序
咱們前面討論的  UserCollection 示例只展現了自定義集合中可能須要的基本功能。有一個操做沒法經過所提供的實現來完成,即經過一個  foreach 循環在集合中循環。要完成此操做,您的自定義集合必須具備實現  IEnumerable 接口的枚舉數支持類。這是一個很是直觀且重複性較強的過程,但卻引入了更多的代碼。C# 2.0 引入了新的  yield 關鍵字來爲您處理此接口的實現細節。Visual Basic .NET 中當前沒有與新的 yield 關鍵字等效的關鍵字。
參考資料:

小結

請勿輕率地作出向自定義實體與集合轉換的決定。這裏有許多須要考慮的因素。例如,您對 OO 概念的熟悉程度、可用來熟悉新方法的時間以及您打算部署它的環境。雖然整體上它們有很大的優勢,但並不必定適合您的特定狀況。即便適合您的狀況,它們的缺點也可能會打消您使用它們的念頭。還要記住有許多可替代的解決方案。Jimmy Nilsson 在他的  Choosing Data Containers for .NET 中概述了其中的某些替代方案,此專欄系列包括 5 部分( 12345)。
自定義實體使您得到了面向對象的編程的豐富功能,並幫助您構建了可靠、可維護的 N 層體系結構的框架。本指南的目的之一是讓您從構成系統的業務實體,而不是通常的  DataSet 和  DataTable 的角度來考慮您的系統。咱們還討論了一些關鍵的問題,無論您選擇的途徑(即設計模式)、對象世界與關係世界的差別( 瞭解詳��信息)以及 N 層體系結構是什麼,您都應注意這些問題。請記住,您以前花費的時間會在系統的整個生命週期內爲您帶來更多的回報。
相關書籍
相關文章
相關標籤/搜索