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

iDotNetSpace發表於2010-03-04

摘要:有些情況下,非型別化的 DataSet 可能並非資料操作的最佳解決方案。本指南的目的就是探討 DataSet 的一種替代解決方案,即:自定義實體與集合。(本文包含一些指向英文站點的連結。)

本頁內容 

 

  1. 引言 
  2. DataSet 存在的問題 
  3. 自定義實體類 
  4. 物件關係對映 
  5.  自定義集合 
  6.  管理關係 
  7.  高階內容 
  8.  小結 

 

引言 

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 函式:

//C#

public DataSetGetAllUsers() {

SqlConnectionconnection = new SqlConnection(CONNECTION_STRING);

SqlCommand command =new SqlCommand("GetUsers", connection);

command.CommandType =CommandType.StoredProcedure;

SqlDataAdapter da =new SqlDataAdapter(command);

try {

DataSet ds = newDataSet();

da.Fill(ds);

return ds;

}finally {

connection.Dispose();

command.Dispose();

da.Dispose();

 }           

}

然後我們有一個頁面,它使用重複器顯示所有使用者:


public sub page_load

users.DataSource =GetAllUsers()

users.DataBind()

end sub

正如我們所看到的那樣,我們的 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) 等工具對您的開發人員並沒有太大的幫助。前面我們說過需要深入瞭解構架的知識,就是指這個意思。我們再來看一個非常常見的示例:

//C#

int userId =Convert.ToInt32(ds.Tables[0].Rows[0]("UserId"));

這段程式碼顯示了從 DataSet 中檢索值的可能方法——可能您的程式碼中到處都需要檢索值(如果不進行轉換,而您使用的又是 Visual Basic .NET,您可能會使用 Option Strict Off這樣的程式碼,而這會給您帶來更大的麻煩。)

不幸的是,這些程式碼中的每一行都可能會產生大量的執行時錯誤:

  1. 轉換可能由於以下原因而失敗:
  • 值可能為空。
  • 開發人員可能對基礎資料型別判斷有誤(還是這個問題,即開發人員需要非常熟悉資料庫架構)。
  • 如果您使用序號值,誰知道位置 X 處實際上是一個什麼樣的列。
  1. ds.Tables(0) 可能返回一個空引用(如果 DAL 方法或儲存過程中有任何部分失敗)。
  2. “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 類可能如下所示:

//C#

public class User {

#region "Fieldsand Properties"

private int userId;

private stringuserName;

private stringpassword;

public int UserId {

get { return userId;}

set { userId = value;}

  }

public stringUserName {

get { returnuserName; }

set { userName =value; }

 }

public stringPassword {

get { returnpassword; }

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 的資料檢視

從這個關係架構對映到自定義實體是一個非常簡單的事情:

//C#

public UserGetUser(int userId) {

SqlConnectionconnection = 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 = newUser();

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 可以將這些問題分散到整個程式碼中。

改進

上文的程式碼對顯示對映的基本概念很有用,但可以在兩個關鍵的方面進行改進。首先,我們需要提取並將程式碼填充到其自己的函式中,因為程式碼有可能會被重新使用:

//C#

public UserPopulateUser(IDataRecord dr) {

User user = newUser();

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 類的自定義集合的不同部分:

//C#

public classUserCollection :CollectionBase {

public User this[intindex] {

get {return(User)List[index];}

set {List[index] =value;}

 }

public int Add(Uservalue) {

return(List.Add(value));

 }

public intIndexOf(User value) {

return(List.IndexOf(value));

 }

public voidInsert(int index, User value) {

List.Insert(index,value);

 }

public voidRemove(User value) {

List.Remove(value);

 }

public boolContains(User value) {

return(List.Contains(value));

 }

}

通過實現 CollectionBase 可以完成更多工,但上面的程式碼代表了自定義集合所需的核心功能。觀察一下 Add 函式,可以看出我們只是簡單地將對 List.Add(它是一個Arraylist)的呼叫封裝到僅允許 User 物件的函式中。

對映自定義集合

將我們的關係資料對映到自定義集合的過程與我們對自定義實體執行的過程非常相似。我們不再建立一個實體並將其返回,而是將該實體新增到集合中並迴圈到下一個:

//C#

public UserCollectionGetAllUsers() {

SqlConnectionconnection = 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 的使用者:

//C#

public UserFindUserById(int userId) {

foreach (User user inList) {

if (user.UserId ==userId){

return user;

  }

 }

return null;

}

另一個示例可能是返回基於特定標準(例如部分使用者名稱)的使用者子集:

//C#

public UserCollectionFindMatchingUsers(string search) {

if (search == null){

throw newArgumentNullException("search cannot be null");

 }

UserCollectionmatchingUsers = new UserCollection();

foreach (User user inList) {

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 那樣使用:

//C#

UserCollection users= DAL.GetAllUsers();

repeater.DataSource =users;

repeater.DataBind();

 

<!-- HTML --&gt


您可以不使用列名稱作為 DataBinder.Eval 的第二個引數,而指定您希望顯示的屬性名稱,在本例中為 UserName

對於在許多資料繫結控制元件提供的 OnItemDataBound 或 OnItemCreated 中執行處理的人來說,您可能會將 e.Item.DataItem 強制轉換成 DataRowView。當繫結到自定義集合時,e.Item.DataItem 則被強制轉換成自定義實體,在我們的示例中為User 類:

//C#

protected voidr_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

//C#

public class User {

private Role role;

public Role Role {

get {return role;}

set {role = value;}

 }

}

或者一個 Role 集合:

//C#

public class User {

privateRoleCollection roles;

public RoleCollectionRoles {

get {

if (roles == null){

roles = newRoleCollection();

   }

return roles;

  }

 }

}

在這兩個示例中,我們有一個虛構的 Role 類或 RoleCollection 類,它們就是類似於User 和 UserCollection 類的其他自定義實體或集合類。

對映關係

真正的問題在於如何對映關係。讓我們看一個簡單的示例,我們希望根據 userId 及其角色來檢索一個使用者。首先,我們看一看關係模型:

 

 3  User  Role 之間的關係

這裡,我們看到了一個 User 表和一個 Role 表,我們可以將這兩個表都以直觀的方式對映到自定義實體。我們還有一個 UserRoleJoin 表,它代表了 User 與 Role 之間的多對多關係。

然後,我們使用儲存過程來獲取兩個單獨的結果:第一個代表 User,第二個代表該使用者的 Role

CREATE PROCEDUREGetUserById(

@UserId INT

)AS

SELECT UserId,UserName, [Password]

FROM Users

WHERE UserId =@UserID

SELECT R.RoleId,R.[Name], R.Code

FROM Roles R INNERJOIN

UserRoleJoin URJ ONR.RoleId = URJ.RoleId

WHERE  URJ.UserId = @UserId

最後,我們從關係模型對映到物件模型:

//C#

public UserGetUserById(int userId) {

SqlConnectionconnection = 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 還提供了一個聯機模式目錄,它對於已經熟悉概念但需要一個便利參考的人士很有用。

併發

前面的示例介紹的都是從資料庫中提取資料並根據這些資料建立物件。總體而言,更新、刪除和插入資料等操作是很直觀的。我們的業務層負責建立物件、將物件傳遞給資料訪問層,然後讓資料訪問層處理物件世界與關係世界之間的對映。例如:

//C#

public voidUpdateUser(User user) {

SqlConnectionconnection = 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 Framework2.0 的功能

即將面世的 .NET Framework 2.0 版將改變我們在本指南中討論的一些實施細節。這些改變將減少支援自定義實體所需的程式碼數量,並有助於處理對映問題。

泛型

議論頗多的泛型之所以存在,主要原因之一就是為了向開發人員提供現成的強型別的集合。我們避開 Arraylist 等現有集合是因為它們屬於弱型別。泛型提供了與當前集合同樣的方便性,而且它們屬於強型別。這是通過在宣告時指定型別來實現的。例如,我們可以替換 UserCollection 而不需要增加程式碼,然後只需建立一個 List 泛型的新例項並指定我們的 User 類即可:

//C#

IListusers = new IList();

宣告後,我們的 user 集合就只能處理 User 型別的物件了,這為我們提供了編譯時檢查和優化的所有優點。

參考資料:

可以為空的型別

可以為空的型別實際上就是由於其他原因而非上述原因而使用的泛型。處理資料庫時面臨的挑戰之一就是正確一致地處理支援 NULL 的列。在處理字串和其他類(稱為引用型別)時,您只需為程式碼中的某個變數指定 nothing/null

//C#

if(dr["UserName"] == DBNull.Value){

user.UserName = null;

}

也可以什麼都不做(預設情況下,引用型別為 nothing/null)。這對值型別(例如整數布林值小數等)並不完全一樣。您當然也可以為這些值指定 nothing/null,但這樣將會指定一個預設值。如果您只宣告整數,或者為其指定 nothing/null,變數的值實際上將為 0。這使其很難對映回資料庫:值究竟為 0 還是 null?可以為空的型別允許值型別具有具體的值或者為空,從而解決了這個問題。例如,如果我們要在 userId 列中支援 null 值(並不是很符合實際情況),我們會首先將 userId 欄位和對應的屬性宣告為可以為空的型別:

//C#

privateNullable userId;

publicNullable UserId {

get { return userId;}

set { userId = value;}

}

然後利用 HasValue 屬性判斷是否指定了 nothing/null

//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 DataContainers for .NET 中概述了其中的某些替代方案,此專欄系列包括 5 部分(12345)。

自定義實體使您獲得了物件導向的程式設計的豐富功能,並幫助您構建了可靠、可維護的 N 層體系結構的框架。本指南的目的之一是讓您從構成系統的業務實體,而不是一般的DataSet 和 DataTable 的角度來考慮您的系統。我們還討論了一些關鍵的問題,不管您選擇的途徑(即設計模式)、物件世界與關係世界的差異(瞭解詳細資訊)以及 N 層體系結構是什麼,您都應注意這些問題。請記住,您之前花費的時間會在系統的整個生命週期內為您帶來更多的回報。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-628591/,如需轉載,請註明出處,否則將追究法律責任。

相關文章