1. 程式人生 > >剖析 .Net 下的資料訪問層技術

剖析 .Net 下的資料訪問層技術

自從 .NET 真正走入開發人員那天起,“效率”兩個字就一直成為眾多程式設計師津津樂道的話題。無論是從開發模式(Cross Language)、系統框架(.NET Framework),還是各種使用方便的工具(VS.NET),無一不體現出了它的勝人一籌。 同時,在另一方面,.NET 是否可以真正勝任企業級應用(Enterprise Application)開發的重任,卻依然爭論不斷,褒貶不一。 通常來說,對於一個企業級應用,需要考慮的方面很多,如安全、效能、伸縮性、易用性等。在本文中,作者更願意與大家一起探討 .NET 下資料訪問層的相關技術,這可能是在多層架構(n-Tier Architecture
)誕生之日起就受到廣泛關注的敏感話題,而對於大部分開發人員來說,這也可能是專案中最讓人沮喪的部分,甚或引起爭議最多的部分。 在以下論述中,為統一起見,作者暫時將資料訪問層簡稱為DALData Access Layer)。 l分析問題 簡單統計分析後,就不難發現,DAL之所以讓人畏懼,並非出於技術本身的問題,甚至恰恰相反,很多開發人員認為這是最沒有技術含量的部分之一(就作者經歷的大小專案來看,該層所佔的開發時間一般較短,也是很多開發人員不願意承擔的“苦差”),只是架構需要或者某些思想作怪(如:為DAL而DAL)才加入了這所謂的第四層(傳統三層架構並沒有提出DAL思想)。 DAL的提出,確實對傳統的架構模式提出了巨大挑戰,加入的目的肯定也是希望借其進一步提高生產效率,在這種模式下,理想情況是:大部分開發人員從此擺脫DBA之苦,甚或徹底斷絕與資料庫的直接關係,SQL之痛將離我們而去,整個OO世界從此清靜。 不過,理想歸理想,能否成為現實則需通過專案檢驗。 接下來,作者試圖分析比較流行且較有代表性的幾種解決方案,看看能否從中得出一些有價值的結論,併為我們今後在設計與實現DAL時提供一些借鑑。 uADO.NET
首先,提到.NET下的DAL,立刻映入眼簾的就是ADO.NET。 沒錯,幾乎所有的DAL解決方案(請允許作者使用Solution而非 Framework)都必須從它發展而來,沒得選擇,這也是具有.NET特色的 實現方式(相比較J2EE)。 排除商業因素及CLR本身的需要,ADO.NET真正帶給我們的東西 不多,值得一提的也就DataSet(就作者經歷的專案來說,使用更多的是 DataTable和DataView)。從微軟早期的記憶體資料庫(Memory Database) 鮮有人問津到今天的DataSet大行其道,這其中的曲折實非片言隻語所 能道盡,總之,有一點可以肯定,正是有了DataSet這種選擇,.NET下 的DAL才能象今天這般百花齊放,大家的思路才能更趨開闊。 Duwamish
這方面有很多好的Sample,最經典的莫過於微軟大力推薦的企業級開發套餐:Duwamish。 對於希望學習.NET下DAL設計的朋友,這是一個不錯的起點,這 方面的完整剖析,大家可以參考CSDN開發高手,2003.11,本文不 再贅述。 作者自己參與的一個專案中就使用了Duwamish方案,當時限於工 期,感覺這是一個很好的參考,沒做深入分析就開始設計了。現在回想 起來,發現還是有很多不足之處。 舉個簡單的例子,Duwamish方案中並沒有考慮Cache Management, 而這對於企業級應用來說,某些時候就是一個不得不考慮的問題;另一 方面,雖然Duwamish中告別了SQL語句(全部採用儲存過程實現), 但資料庫痕跡依舊十分明顯,比如:某些欄位名稱的定義,關聯表名稱 的定義等等。 還有一個十分頭疼的問題是在開發過程中體現出來的。一開始,那 些比較簡單的資料表還比較容易實現,到了一些包含相互關係的資料表 時,我們的DAL工程師就感到了壓力,到後來,幾乎又做了一遍DBA 在資料庫建模時早已做過的工作,只不過,這次將資料庫指令碼換作了 C# 實現(或者說:將資料庫結構換成了表面上具有OO特色DataSet) 而已。 可能,Duwamish的實現比較經典,但在實際應用中,有時並不意 味著Best Practice。就拿我們的專案來說,雖然成功交付,但無論從模 型複用角度,還是開發效率來說,都不能算很成功。套用一句流行語: 其實我們可以做得更好! PetShop ADO.NET上另一個值得參考的DAL實現就是鼎鼎大名的PetShop。 當然了,與Duwamish相似,名氣大未必真的實用。PetShop雖然 彌補了Duwamish在某些方面的不足,例如:通過Factory支援多種數 據庫儲存,引入了Cache機制,提供了更為便利的SQL Helper,但也同 時帶來了另一些問題。其中,最麻煩的就是SQL語句的引入,而且還 是針對不同資料庫儲存的不同SQL語句(主要是SQL Server與Oracle 的引數表示方式不同)。 另一方面,PetShop雖然沒有使用DataSet而代之以更為簡潔的普通 實體物件(Model),但它還是將DataReader的結果轉換到了包含實體對 象的列表集合中供離線使用,從這個意義上說,可謂換湯不換藥。甚至, 在某些場合,例如:需要進行資料過濾,或者在主從資料間導航,反而 更為不便(此時,簡單的Collection或者List是無法滿足需求的,DBA 與DAL開發人員只能再提供其它的方法來達到目的)。 從上述兩個例子中,我們可以看出,即使在微軟的開發團隊中,也 沒有能夠在DAL這個問題上達成一致。這方面的更詳細資訊,有興趣 的朋友可以參考如下文章: 實戰 上面剖析的兩個解決方案,讓我們看到了它們各自的優勢與不足,而企業級應用的複雜環境也不太可能要求一個放之四海而皆準的框架就能解決所有難題,因此,只能根據具體情況具體分析。 作者曾經參與一個(.NET)大型外包專案的開發工作,有幸一睹其DAL的設計思想,深感震撼,在此與各位朋友一起共同探討。 以SQL Server所帶Northwind資料庫為例,如下就是一段基於該DAL的呼叫程式碼(作者做了一些名稱上的調整): // 根據EmployeeID返回其Title boEmp = new EmployeeDAL(); boEmp.Keys[“Emp_ID”] = 1; // 注意:實際欄位名為:EmployeeID boEmp.Select(); string strTitle = boEmp[“Emp_Title”]; // 注意:實際欄位名為:Title …… // 根據 City 返回所有符合條件的 Employee boEmp = new EmployeeDAL(); boEmp.Keys[“Emp_City”] = “Seattle”; boEmp.Select(); // 注意:該方法與上面的呼叫完全相同 DataTable dtEmp = boEmp.Table; 如果不考慮物件建立(可以採用Object Pooling或者Cached Object)以及呼叫後的處理,實際的程式碼只有兩行! 更讓人吃驚的是,上述EmployeeDAL類沒有任何真正意義上的實現程式碼,僅僅是聲明瞭類名,然後從一個通用基類繼承而已!! 最優雅的地方還不在於此,實際上,就算在那個基類中,也根本看不到SqlConnection或者OracleAdapter之類的幫派之爭。 相信大家也猜出來了,沒錯,它是借鑑了PetShop的實現,採用了Factory模式來保證DAL可以適用於不同的資料庫儲存。不過,這種實現與PetShop還是有很大的區別:至少,它沒有產生不同的SQL語句,更沒有出現不同的引數呼叫方式(SQL Server中一般使用“@”符號,Oracle中一般使用“:”符號),所有幫派一視同仁! 這其中,當然得益於Factory的實現技巧,但更重要的因素還在於設計方式的精妙。其實,在.NET Framework中,已經提供了這種設計方式的基石,說白了,就是System.Data中的那些Interface(如:IDBConnection,IdataAdapter等)。 在這樣的設計基礎上,我們針對每一個DAL類,就不再需要為不同的資料庫儲存提供不同的資料存取實現了。例如:在PetShop中,針對訂單資料需要實現Order類,很自然的,系統為SQL Server與Oracle分別實現了Order類並使用不同Provider(SqlClient,OracleClient)提供的方法進行操作。而在實際呼叫時,PetShop通過Factory模式動態建立真正的Order類並激活相應的方法,一個面向不同資料庫儲存的方案就躍然紙上。 其實,PetShop這種方案已經比較靈活了,如果更能省去“撰寫不同Order類”之苦,那就真的送佛送到天了J。而所有這些功能,在作者所參與的這個專案中,已經完全搞定了! 至於上面的“EmployeeDAL(當然,包括其它所有DAL類)沒有任何真正實現程式碼”,只不過玩了一個小小的配置技巧而已:將不同的DAL類與相關的Stored Procedure(請注意:不是Table或View)按照Namespace分別儲存到XML檔案中。 可能大家已經看出來了,理論上,甚至只需要一個DAL類就可以完成上述所有的工作!但在實際操作中,不同的DAL類可能還是有一些資料處理上的細微差別(比如:資料校驗,格式轉換等)。 總的來說,在這樣一個大專案中,不可能要求所有開發人員(除了DBA,DAL Framework Developer)都去了解ADO.NET的方方面面,雖然作者對此頗有研究,但在這個專案中,卻從頭至尾只用到了兩個類:DataTable,DataView(甚至連Transaction都無需瞭解)!   其它       結束ADO.NET剖析前,不得不提提DataReader與DataSet間的兄弟 之爭。       就作者所看過的資料,幾乎所有的都建議實際情況具體分析,剩下 很少很少的則全憑個人習慣決定。       在學習ADO.NET時,作者也是抱著這樣的想法,並反覆牢記資料 上總結的那些條款(就像當年學習GOF 23條時那樣,幾乎可以倒背如 流了J),想到終有一日也可在ADO.NET下大展神威了。       可惜現實不隨人願,連續做了幾個專案,無論規模大小,竟然全部 採用了DataSet解決方案!       此時,再回頭看看學習ADO.NET時開啟最為頻繁的PetShop專案, 兩相一比較,這才看出些許端倪。       簡單的說,PetShop採用瞭如下這種“曲線救國”的方式來實現資料 交換:       DataReader獲取資料 => 建立資料實體類 => 根據欄位型別填充 據實體類 => 將資料實體新增到列表類中(僅針對返回超過一條資料的 場合)       (補充:採用資料實體類或者集合類可以比較方便的實現Cache Manament, 而普通的DataReader由於其資料讀取方式限制,無法滿足這種需求)       這個過程與DataAdapter.Fill() 所所產生的效果大同小異,只不過, 在Fill() 中DataAdpater自動建立DataReader去獲取資料,之後建立 DataTable(相當於資料實體類),並根據欄位型別填充DataTable,當然 ,如果可能返回多條記錄,DataTable完全可以處理,就沒必要去實現列 表操作了。       可能讀者馬上產生了疑問:既然如此,PetShop中為何還需要資料實 體類呢?       這其中還是有一些差別的。       首先,資料實體類是輕量級的structure,一般僅包含資料欄位,沒有 什麼操作方法,這比DataTable或者DataRow還是有一些效能上的優勢 (在資料量不大時可以忽略不計);另一方面,資料實體類的操作相對 簡單,不需要開發人員具備任何ADO.NET知識(其實就DataTable來 說,這也不算什麼問題),點點屬性就可以了。       不過,根據作者的實踐來看,這兩方面似乎還不足以使人轉而使用 DataReader方案,理由列舉如下: (1)對於資料量較大的場合,可以採用分批讀取的方式,這有點類似DataGrid的資料分頁效果; (2)對於簡單的資料,實體類還能應付,一旦涉及關聯資料,就只能另外撰寫方法了。而所有這些,在DataSet中是非常容易處理的(對於企業級應用,大部分情況都需要處理比較複雜的資料); (3)DataTable“天生”就支援資料集合操作,這樣的特性比“集合+實體”的混合模式(PetShop)更容易控制,也更自然; (4)實體類在宣告時需要確定所有資料型別,當進行資料填充時,就需要DataReader再次關注實體所對應的資料型別,不能有絲毫差錯!在這方面,DataTable就顯得非常方便,操作時只需要一次型別關注即可; (5)DataSet解決方案可以非常方便的支援序列化操作(如:Remoting,WebServices),同時,與XML的關係更是親密無比,這對於和其它系統的互動來說也是至關重要的。 分析過一些技術和方案,相信讀者朋友已有一些體會。值此收官之際,如果非要在這裡提供一個“綜上所述”,那作者的建議就非常明確: 在企業級應用開發中,儘可能的採用DataSet(DataTable / DataView)+ Cache Management解決方案! 其它開發中,只在如下4種情況才考慮使用DataReader(就作者經驗來說,大部分使用DataReader都屬第種情況): (1)對資源要求比較苛刻的場合,這裡的資源主要指記憶體和資料庫連線; (2)希望在讀取資料庫返回結果集時作自定義處理,例如:在讀取一條記錄後立刻終止處理,或者在讀取時作計算操作。 (提示:這種情況類似於XML中的SAX(Simple API for XML)技術,無需一次性讀入所有XML資料即可進行操作;相反的,DOM(Document Object Model)則要求必須裝載所有XML資料後才能開始操作(MSXML4.0已開始允許只讀取XML文件部分資料即可開始操作,這是後話)!) (3)只希望得到返回記錄數或者返回記錄的部分欄位,如: string GetNameByID(int nID) //根據員工ID返回員工姓名,這裡只需要 // 讀取姓名欄位; (提示:這種情況一般可以通過執行特定的查詢或儲存過程直接解決) (4)出於某些方面的考慮(例如:n-Tier系統中嚴格區分各Layer間的職責),無法(或者禁止)通過資料庫本身進行查詢過濾,這時就只有使用DataReader在讀取時進行過濾操作! (提示:雖然DataView也能達到這種目的,但它的過濾前提是必須讀取到所有返回資料,所以效能上不如DataReader!) uO/R Mapping O/R Mapping的全稱是:Object Relational Mapping,主要目的是在傳統RDBMS與OO Language之間建對映關係,從而使開發人員徹底脫離資料持久這片剪不斷理還亂的苦海。 關於O/R Mapping或者近來比較熱門的O/X Mapping(大家可以參考“程式設計師,2004.01P86”),可能需要專門的文章進行詳細論述,本文的目的主要是對現有方案的優缺點進行簡單剖析以及提供一些實踐中的參考資訊。 相比較J2EE平臺,.NET下的O/R Mapping可謂沒什麼歷史,至今還尚未有經過考驗的成熟的可用方案。但是,隨著各大廠商的重視以及開源專案的如火如荼,.NET O/R Mapping的步伐也開始慢慢跟上,使這塊本屬於J2EE的領地加入了新的競爭對手(會不會使更多的開發人員投入.NET陣營?J),也讓眾多疲於在SQL Clause或ADO.NET中來回奔命的DAL開發人員看到了“光明之路”。 接下來,就讓我們一起看看在這片比ADO.NET更廣闊的土地上有些什麼值得探討的Solution。 ØOpen Source 開源方面一直與.NET保持一定距離,O/R Mapping更是寥寥無幾,但就作者的下載試用和原始碼分析來看,個人以為如下的兩個解決方案還是有一定參考價值的:OPFOJB。 有關這兩個開源專案的簡介,大家可以參考“程式設計師,2004.01P13 OPF的全稱是:Object Persistent Framework。 OJB的全稱是:ObJect Relational Bridge。 在實現手法上,這兩個方案的思路完全不同,具有各自的代表性。 OPF走的路線有點類似於Typed DataSet或Borland ECO(請參考下面的介紹),實現比較簡單,提供更多的原始碼級控制;而OJB的實現則類似於Microsoft ObjectSpaces(請參考下面的介紹),採用了配置檔案的方式,相對就比較複雜了。 這兩個方案的基本框架如下所示: OPF: 從圖中不難看出: (1)Persistent類扮演了DataSet的角色,除了常規的物件資料操作外,還可以設定不同物件間的關係(如主從關係,集合關係等,這一點在Borland ECO所生成的程式碼中也可略見一二),這也是上文所說“提供更多原始碼級控制”的原因所在; (2)PersistentSqlDataManager則扮演了DataAdapter的角色,通過預先設定的Commands來執行真正的資料庫操作;在實際撰寫的employee data manager中,開發人員確實需要提供基本的SQL語句,就像在SqlCommond中設定的那樣(Borland ECO則更進一步,以OCL代替了SQL); (3)ObjectBroker的作用非常重要,它是物件與資料間的橋樑,RegisterPersistent方法建立了這種虛擬(Object)與現實(RDBMS)間的關係; (4)在employee business object的宣告中,物件屬性與資料庫欄位的對應關係是通過.NET Attribute機制體現的,所以修改起來還是比較方便的,雖然相比配置檔案的方式顯得不夠靈活(請參考OJB的介紹),比如:需要重新編譯,開發人員不得不關注資料庫欄位等。         OJB: 從圖中不難看出: (1)該方案的實現比較複雜,但使用者需要實際撰寫的程式碼變少了(只需要編寫employee business object),這其中的關鍵就在於引入了配置檔案;同時,由於配置檔案的引入,我們在hello world application中也不需要呼叫類似OPF解決方案(請參考上文的OPF類圖)中的RegisterObject方法,所有這一切(甚至包括資料庫連線資訊),系統都已瞭如指掌! (2)該方案中,SQL命令通過Criteria類被徹底替代,而QueryFacade則充當了Adapter的功能,通過PersistenceBroker這一真正的Command與資料庫進行通訊; (3)無論是repository.xml配置檔案,還是Criteria、QueryFacade類,我們都可以在ObjectSpaces(請參考下面的介紹)中找到類似的實現(難道是巧合?),同時,作者個人以為,這種方式也更符合O/R Mapping的精神,減輕了開發人員的負擔! (4)OJB還有一個非常cool的工具“repositorygen.exe”,可以用來生成repository.xml配置檔案(同樣的,原始碼無償奉上J),這一點,甚至連ObjectSpaces都沒能做到(想想那麼多欄位、屬性、關聯、對映,簡直可以讓人發瘋J)! ØMicrosoft ObjectSpaces 這是一個在幾年前就讓眾多.NET guy伸長脖子激動不已的技術。就作者來說,那個時候,只要一提起這個話題,一般都是在J2EE guy的嘲笑聲中悻悻而歸,恨不能自己也搞個ENB(相對EJB)或者NCMP(相對CMP)什麼的。 終於,我們可以在.NET Framework 1.2(可在VS.NET 2004Whidbey或Yukon中找到,目前都是Beta版本)中一睹其“芳容”了J。 首先,讓我們看看用ObjectSpaces寫出的程式碼是什麼樣子(依然使用上面的employee例子): // 初始化ObjectSpace SqlConnection conn = new SqlConnection("Data Source=localhost; Integrated Security=SSPI; Database=Northwind"); ObjectSpace os = new ObjectSpace("map.xml", conn); // 根據EmployeeID返回其Title Employee oEmp = (Employee)os.GetObject ( new ObjectQuery(typeof(Employee), "ID = 1")); // 注意:實際欄位名為:EmployeeID string strTitle = oEmp.Title; …… // 根據 City 返回所有符合條件的 Employee ObjectSet oSet = os.GetObjectSet( new ObjectQuery(typeof(Employee), "City = '”Seattle'")); // 注意:返回的不是DataTable,而是物件集合 foreach (Employee oEmp in oSet) { …… // 注意:在這裡可以對oEmp做任何操作 針對上面第二段程式碼,還有一種解決方案,就是以ObjectReader替代ObjectSet,這其中所包含的差異,類似於ADO.NET 1.0(包含ObjectSpacesd的ADO.NET又稱為ADO.NET 2.0)中的DataSet / DataTable與DataReader間的不同(不得不佩服Microsoft在前後一致性上表現出的老謀深算J)。 仔細分析上面的程式碼,就可以發現它和前面討論的OJB有驚人的相似點(OJB中作者只畫了基本類圖,但足可看出這種思想上的接近)! 例如:ObjectSpace類基本上提供了OJB中的QueryFacade功能;ObjectQuery類基本上提供了OJB中的Criteria功能;同時,兩種解決方案又不約而同的使用了配置檔案來儲存O/R Mapping資訊;而應用程式一般也就通過這2個類進行資料操作,非常方便。稍微有些區別的可能是在資料返回格式上(這一點,ObjectSpaces考慮得更細緻,可以參考上面的程式碼),但這已經對實際的程式碼實現影響不大了。 如果將ObjectSpaces下的呼叫程式碼與前面給出的那段在ADO.NET下撰寫的程式碼作個比較,不難看出,ObjectSpaces給出的程式碼更易閱讀和理解,就算不熟悉ADO.NET整體架構的開發人員,也可輕鬆上手(唯一涉及RDBMS的程式碼只有建立資料庫連線時需要)。對於已經熟悉ADO.NET或曾接觸過O/R Mapping(如:J2EE下的Hibernate)的朋友來說,真可謂小菜一碟! 從.NET Framework 1.2文件中可以知道,ObjectSpaces總共提供了3個名稱空間,整體結構非常清晰: System.Data.ObjectSpaces System.Data.ObjectSpaces.Query System.Data.ObjectSpaces.Schema ObjectSpaces、Query已在上面的程式碼中見識過,從名字中可以猜出,它們主要負責向外提供基本訪問介面(如查詢、增 / 刪 / 改等)和解析各種查詢條件(如物件過濾等),Schema名稱空間則主要用來操作O/R Mapping配置檔案,併為其它兩個名稱空間中的類提供服務。 在ObjectSpaces中,O/R Mapping配置檔案主要指map.xml,這個檔案的名字是可以隨意更換的,比較類似OJB中的repository.xml。另外兩個分別描述資料庫結構和物件結構的配置檔案也非常重要:RSD.xml(Relational Schema Definition),OSD.xml(Object Schema Definition)。可以將它們理解為Typed DataSet中的XSD檔案,沒有它們,所有的資料 / 物件Mapping和Validation都將是“非法”的J! 本文中,作者不準備對ObjectSpaces來個深度探索,也不會提供什麼Sample說明其優越性,這方面,.NET Framework SDK早已為大家提供了豐富套餐。 作者只是希望,如果從DAL的角度來分析,ObjectSpaces技術能為我們帶來什麼,是否意味著從此告別DataReader / DataSet,抑或為開發人員帶來了新的煩惱? 好處不多說,僅舉數例即可明瞭: (1)ObjectSpaces全部採用物件方式訪問資料,大大緩解了很多開發人員的SQL(或者說RDBMS)恐懼症; (2)對於比較簡單的資料庫結構變化,只需要修改配置檔案即可,無需重新編譯程式碼(較之OPF中將對映關係以.NET Attribute方式封裝於程式碼中,顯得更加靈活、方便); (3)對於比較複雜的資料庫結構變化,由於只涉及物件操作,所以修改的工作也要比以前簡單許多; (4)採用了O/R Mapping配置檔案後,資料庫設計與DAL開發可以分別進行,相互的影響也降到了最低點; 不足則是我們更須關注的話題: (1)目前版本不支援中文(永遠的話題J)查詢,不爽! (2)當前版本僅支援SQL Server 2000以上版本的資料庫系統,弱(這是個很耐人尋味的限制,有興趣的讀者不妨想想到底是什麼原因)! (1、2引自.NET Framework SDK Document,就這兩點已排除了很多躍躍欲試的朋友。而作者參與的.NET專案雖不受1影響,但由於經常使用Oracle,就不得不暫時忍痛割愛了J) (3)效能問題。雖然ObjectSpaces也提供了類似DataReader的功能(ObjectReader),但畢竟需要進行一次資料強型別填充,無論如何會有損失,如果返回資料量變大,將是一個不得不考慮的問題; (4)還是效能問題map.xml是個好東東,但如何優化對它的訪問以及進行正確的Validation(基於RSD.xml、OSD.xml)畢竟需要時間,甚至在某些時候(資料庫結構比較複雜),這會造成比第3點更為嚴重的後果; 說了些不足,其實也無須過於擔心,畢竟,沒有十全十美的解決方案,怎麼取捨就看你自己的決定了。 本章最後,作者給出了一個自己的總結,可供您參考一二。在所有的分析完畢之後,作者也試圖結合自己的實踐提供“我的方案(撰寫中)”,希望能給各位讀者帶來幫助。