天天看點

Nhibernate 同 asp.net ,泛型,單元測試的最佳實踐(同文章裡的内容)

介紹

        NHibernate,和其他ORM(對象關系映射)工具一樣,可以減輕數千行代碼,和存儲過程的維護,是以可以讓開發者更多的關注項目的核心:領域模型和業務邏輯。即使你使用CodeSimth 或者LLBLGen Pro工具自動生成你的ADO.Net 資料通路層,但是NHibernate降低了業務模型和關系模型的耦合。你的資料庫應該執行定義和支援你的領域模型的細節,而不做其他事前。 由于論壇上充滿了相關要點的激烈讨論,這篇文章并不是說使用ORM比ADO.NET更好,或者NHibernate比其他ORM工具更好,僅僅是在描述如何使用定制好的設計模式把NHibernate融入到ASP.NET中。

       這篇文章假設讀者對C#,NHibernate有很好的了解,有開發資料通路對象模式的經驗,對泛型有基本的了解。如果你僅僅想獲得NHibernate的知識,我給你推薦兩篇很好的入門文章在 TheServerSide.net  Part 1 and Part 2. 。為了擴張對DAO(Date Access Object)設計模式的了解,可以去 to J2EE's BluePrints catalog. 

       在ASP.NET中建立堅固的資料綜合性的例子,我們的主要完成以下目标:

l         陳述層和業務邏輯層應該不必了解如何和資料庫通信是有好處的。你能夠修改你的需求隻需對這些層作最小的改動

l         業務邏輯應該很容易測試,不依賴一個活動的資料庫。

l         NHibernate特性,如lazy-loading,應該能夠能夠在整個頁面生命周期都可用。

l         .NET 2.0 的泛型應該對減小代碼重複起杠杆作用。

一個簡單的應用程式包括NHibernate,ASP.NET,.NET Generics的綜合執行個體,同時彙集了上面的目标。下面描述的是如何在程式理應用前面提及到的目标。但是在進入詳細之前,我們先總體了解一下,并且下載下傳例子安裝并運作。

一個簡單的應用程式

 整個簡單的應用程式,冒着成為死屍的風險,利用SQL Server 2000 的資料 Northwind 去 檢視并且修改Customers表。示範 Lazy-loading的使用,同時也陳列了每個Customer 的Orders。所有這些你隻需要安裝.NET 2.0 Framework,SQL Server 2000自帶了資料庫Northwind.

擷取程式安裝并運作:

1.        在你選擇的檔案下解壓執行個體程式。

2.        打開NHibernateSample.Web/web.config 和 NHibernateSample.Tests/App.config ,然後修改 the database connection strings  連接配接SQL Server 2000 中的 Northwind 資料庫

3.        在IIS中建立一個新的虛拟目錄。别名為NHibernateSample,并且整個目錄應該指向NHibernateSample.Web 檔案夾,在你解壓之後會有此檔案夾。

4.        打開浏覽器輸入http://localhost/NHibernateSample/ViewCustomers.aspx,回車,程式就可以運作了。

現在程式連同文章都在你面前了,我們來看看我們的設計目标是怎樣達到的。

NHibernateSample.Web

 正如我們所期待的那樣,NHibernateSample.Web項目包括了應用程式的配置和所有web頁面。

在這個例子當中,Code-behind 頁面扮演控制器,同業務和資料通路層通信,是以這不是最好的MVC(Model-View-Controller,模型—視圖—控制器)分離實踐,但是它很簡單,能容易示範。(我在以後将要徹底的讨論MVC和ASP.NET)

這裡我們仔細的看一些有趣的片斷。

Open Session in View

如果你想了解NHibernate's lazy-loading(你最明确的意向),那麼請看Open-Session-in-View 模式。("Session"在此上下文中是 NHibernate session...而不是 ASP.NET 中的"

Session

" 對象.)本質上說,這個模式建議每次HTTP請求打開一個NHibernate session。盡管session在理論上會被清除在ASP.NET 頁面生命周期之外。但是它在可用的執行方法中是多變的。我獲得這種方法通過建立一個專門的HTTPModule去控制這個模式的細節。在一旁集中的管理session,這個方法提供了額外的優點:我們可以執行Open-Session-in-View模式而不必在Code-behind 頁中放置任何的session管理代碼。

下面看看它是怎麼樣實作的,看看App_Code目錄下的

NHibernateSessionModule

類。

是web.config中HTTPModule的中包含如下片斷。

<httpModules>      
 <add name="NHibernateSessionModule"       
       type="NHibernateSample.Web.NHibernateSessionModule" />      
</httpModules>      

HTTPModule包含在例子程式中開始一個事物在web請求開始的時候。并且送出/關閉它在請求結束的時候。聯合它隻需要很短的時間,因為NHibernate不會打開資料庫連接配接直到需要的時候。我們會留意到,你可以控制這個政策同其他政策曝露通過session管理。你想打開一個session的其它政策不和事物或者用Nhibernate注冊一個

IInterceptor

接口。(用

IInterceptor

c可以很好的控制權限,請檢視Hibernate in Action,8.3.2節-稽核日志的更多細節)

NHibernate 在 web.config中的設定

有兩種使NHibernate最優化的關鍵字設定: hibernate.connection.isolation 和 hibernate.default_schema. NHibernate 的預設設定是IsolationLevel.Unspecified 作為他的資料庫隔離級别.換句話說, NHibernate 分支它等于ADO.NET标示決定使用哪種隔離級别通過預設設定。. 如果你正在使用 Serializable 級别 , 這是相當嚴格的隔離級别會阻止大多數引用程式的設定。 一個更我合理的設定是以ReadCommitted 開始. 用這種設定,“讀事務”阻止其他事務通路某行。 但是,未别授權的“寫事務”會阻止其他事務通路這一行。其他預設選項包括 (注意它們都是受版本變化影響的):

  • SQL Server 2000 - Read Committed
  • SQL Server 2005 - Read Committed
  • Firebird - Read Committed
  • MySQL's InnoDB - Repeatable Read
  • PostgreSQL - Read Committed
  • Oracle - Read Committed

其它可選的設定也不應被忽視, hibernate.default_schema, 很容易被忽視,但它對查詢的執行有重大的意義。通過預設設定,資料表在在準備好的NHibernate查詢的内部。像-那些是标準的但是不是不是充分查詢;例如 Customers 和 Northwind.dbo.Customers. 問題的饑餓症在 sp_execsql,存儲過程用來執行NHibernate 查詢, 效率是不高不除非使用表的全稱。盡管這隻是很小的句法不同,但是在一些海量的訂單中會使速度變得的很慢。在資料頁中, 明确的指定hibernate.default_schema.的值可以使性能提高33%之多.下面是在 web.config:中設定這些設定的一個例子。

<add key="hibernate.connection.isolation" value="ReadCommitted" />

<add key="hibernate.default_schema" value="Northwind.dbo" />

為了減少從NHibernateSample.Web 項目中通路資料的執行細節,NHibernate session管理已經完全被分離到了NHibernateSample.Data項目。為了獲悉植入的HBM映射檔案的所在,定義了名為HBM_ASSEMBLY web.config to設定。Nhibernate将顧這個集合通過調用目标映射取得HBM檔案。(NHibernate will review this assembly for embedded HBM files to load object mappings)(這不是直接使用Nhibinate設定,但是通過自定義類我們很快就可以回顧了)

簡單目錄和更新組成

這個web項目包含兩個頁面:ViewCustomers.aspx 和 EditCustomer.aspx.(我将給出三種方法去描述它們是他們做了什麼)重要的是注意code-behind頁和DAO工廠回話來通路資料庫;代碼沒有受具體的資料通路對象的執行的束縛。這使它很容易在DAO執行和單元測試之間交換而不依賴某個活動資料庫。在任何地方,都可以很容易的從資料庫中取得所有的Customers。如下:

IDaoFactory daoFactory = new NHibernateDaoFactory();      
ICustomerDao customerDao = daoFactory.GetCustomerDao();      
IList<Customer> allCustomers = customerDao.GetAll();      

在上面的例子中,得到一個具體的

NHibernateDaoFactory

參考就是通過關鍵字new。

為了生成代碼,這個引用應該被“注入”到名為控制反轉(IoC),或者叫“依賴倒置”的技術運作時中,Martin Fowler 已經寫了一篇關于這種模式的介紹( a great introduction to this pattern),它的想法就是減少代碼間的耦合。用IoC能夠移開很多具體對象連同伴随這些固定的依賴的直接事例化用(With IoC, it's possible to remove many direct instantiations of concrete objects along with the inflexibility that comes along with these dependencies)。IoC的衆多優點包括:靈活的架構,強調面向接口編碼,極易單元測試。缺點是要在程式中又要添加複雜的一層。這裡有兩個很好的适合IoC實踐的工具:

  • Spring .NET: 這個架構供給 IoC 通過 XML 配置檔案. Spring .NET 也包含很多強大的子產品, 如果你不僅僅需要IoC,還需要其它的功能時,它是非常有魅力的。它是我當然我了使用IOC而使用的架構, 但是這裡它不是唯一的選擇。
  • Castle Windsor: Castle Windsor 容器提供了很好的 IoC支援通過配置和強類型聲明。 它的一些優點是使表産生更少的XML和捕獲更多的編譯錯誤。和Spring.net,它也提供了許多額外的開發效用。

當它用newyw關鍵字在你的code-behind,或者自定控件,或者業務對象中建立

NHiberanteDaoFactory

時,不應該建立它的直接依賴。而是他們的DAO依賴應該提供一個共有的方法或者經由構造函數。(IoC能很好的幫助我)這是很大的提高,你能夠單元測試用“mock”DAO類,例如:下面代碼,在

NHibernateSample.Tests.Data.CustomerDaoTests

r中,重新得到一個custome并且給出了它的DAO依賴:

IDaoFactory daoFactory = new NHibernateDaoFactory();      
ICustomerDao customerDao = daoFactory.GetCustomerDao();      
Customer customer = customerDao.GetById("CHOPS", false);      
// Give the customer its DAO dependency via a public setter      
customer.OrderDao = daoFactory.GetOrderDao();      

用這項技術,業務層決不需要直接依賴資料層,隻是依賴資料層裡的定義的接口,同樣的我們接下來将看NHibernateSample.Core 項目。

NHibernateSample.Core

 NHibernateSample.Core項目包含了域模型和NHibernate HBM檔案。這個項目也包括了資料存取對象接口,它在

NHibernateSample.Core.Data

命名空間。(可以論證,HBM檔案屬于NHibernateSample.Data 項目,但是為了把HBM檔案和域對象放在一起友善維護的價值遠大于打破封裝的價值。)

資料依賴倒置

你會注意到NHibernateSample.Core 項目并不包含執行資料通路對象的細節,緊緊是描述它需要的接口。具體執行這些接口的DAO類在NHibernateSample.Data項目中。這種技術被Martin Fowler叫做分離接口(Separated Interface),或是在Robert Martin的在靈活軟體開發(Agile Software Development)中叫做 “依賴倒置”。考慮到NHibernateSample.Core a作為“上一級别層”,NHibernateSample.Data 作為“下一級别層”,Martin曾描述到“每一個上一級别層為下一級别層聲明一個它需要的抽象接口。然後在下一級别層中實作這些抽象接口….是以,上層不依賴下層,相反,下層依賴上層的抽象接口服務”。依賴倒置是打破域和資料層之間的雙向依賴的完美技術。

在行為中看它的應用,在

NHibernateSample.Core.DataInterface

在描述了資料接口。

NHibernateSample.Core.DataInterfaces

.

IGenericDao

是的泛型的接口,它提供了一個典型的資料通路的功能。編譯

IDaoFactory

接口允許你為産品代碼建立具體的DAOfactory,和建立傳回“mock”DAO對象作單元測試的具體DAOfactory。(這在abstract factory pattern抽象工廠模式中描述到)這意味着每次可以測試一個單獨的功能。

Generics with NHibernate

到目前為止,C#2.0為帶給表最大的益處是包含了泛型。有了泛型,更多的代碼重用能被高效的實作當仍然需要加強強類型的 “契約”(while still enforcing strongly typed coding "contracts")。雖然優點很大,但NHibernate還沒及時的更新這一語言的新特性。(盡管我知道他們忙于這事)同時,一種解決方案在這裡(here)被發現,uick, take a guess - NHibernate.Generic, NHibernate.Generics提供了NHibernate普遍綁定的

IList

ISet

泛集合型的封裝。我沒有花太多的時間描述這種方法,因為Oren Eini's websitew作了徹底的工作,但是在代碼中有個特别有趣的注意事項。在

NHibernateSample.Core.Domain

對象的構造函數中

Customer

Order

被"wire up"編碼為了處理相關的對象。這個集合封裝器接受了兩個匿名的操作添加/删除的方法由于關系的結束。是以如果你從父類中移除了子類,父類自動從子類中擷取,并且替代以前的(。到NHibernateSample.Tests.CustomerTests 看它工作)

public Customer() {      
    // Implement parent/child relationship add/remove scaffolding      
    _orders = new EntityList<Order>(      
        delegate(Order order) { order.ParentCustomer = this; },      
        delegate(Order order) { order.ParentCustomer = null; }      
        );      
}      

在上面的例子中,注意這些私有成員形成的關系必須用如下格式:

_camelCase

. 有幾個其他的關于NHiberante.Generics站點的告誡 ,他們花很少的價格對Nhibernate泛型的支援。但在工作中確定密切注視NHibernate's website as的對支援泛型的更新.

NHibernateSample.Data

NHibernateSample.Data 項目包含了對資料庫通信的執行細節和對NHibernate sessions的管理。

DAOFactory和GenericDAO

我已經在

NHibernateDaoFactory

GenericNHibernateDAO

,類中分别實作了IDAOfactory 和 IgenericDAO接口,他們Jave版的C#入口。(described in detail at NHibernate's website)我強烈推薦仔細的回顧一下這篇文章。最重要的事是注意它僅僅用了幾行代碼就建立了完整而成熟的DAO對象供我們使用。

  1. NHibernateSample.Data.NHibernateDaoFactory

    . 中添加新的一行執行和取得DAO方法
  2. NHibernateSample.Core.DataInterfaces.IDaoFactory

    . 添加新的一行接口和重獲方法

檢視已經存在于項目中的

ICustomerDao

例子,大概隻有5行代碼…不算過分。。

處理 NHibernate Session

最後,隻剩下NHibernate sessions是如何管理的問題了?這個問題的詳細答案在

NHibernateSessionManager

類中。在這個類中,獲得一個session的基本流程如下:

  1. 用戶端代碼調用

    NHibernateSessionManager.Instance.GetSession()

    .
  2. 如果沒有示例,單獨的對象構造session工廠,從web.config.中的

    HBM_ASSEMBLY

    調用HBM映射檔案。(

    NHibernateSessionManager

    is是單獨的因為建立session 工廠的代價是很昂貴的。)
  3.  GetSession

    看是否有session 已經幫定在

    System.Runtime.Remoting.Messaging.CallContext[

    "THREAD_SESSION"

    ]

    上。
  4. 如果沒有找到打開的NHibernate session , 那麼就打開一個新的(并綁定到可選的

    IInterceptor

    上) 并 放到

    CallContext[

    "THREAD_SESSION"

    ]

    中。
  5. GetSession

    傳回 session并給

    CallContext[

    "THREAD_SESSION"

    ]

    指派。

這個流傳也是

NHibernateSessionManager

的操作台, 接着在Hibernate in Action, chapter 8 - Writing Hibernate Applications. 中有更進一步的描述。

 HTTPModule 記述NHibernateSample.Web 工程在請求一個web頁面時候打開一個事物,在請求結束時送出/關閉事物。下面是修改過的HttpModule示例 使

IInterceptor

綁定session的時候也包含一個事物:

public void Init(HttpApplication context) {      
    context.BeginRequest +=       
          new EventHandler(InitNHibernateSession);      
    ...      
}      
private void InitNHibernateSession(object sender, EventArgs e) {      
    IInterceptor myNHibernateInterceptor = ...      
    // Bind the interceptor to the session.      
    // Using open-session-in-view, an interceptor       
    // cannot be bound to an already opened session,      
    // so this must be our very first step.      
    NHibernateSessionManager.Instance.RegisterInterceptor(myNHibernateInterceptor);      
    // Encapsulate the already opened session within a transaction      
    NHibernateSessionManager.Instance.BeginTransaction();      
}      

NHibernateSample.Tests

我想你一定能夠猜出這個項目的作用。

單元測試性能

牢靠的單元測試是很有必要的,如果一組測試花太多的時間來運作,開發者就停止運作他們。我們想運作所有的單元測試!事實上,如果測試花的時間超過了0.1秒,這個測試已經很慢了。 現在,如果你以前使用過單元測試,你該知道任何單元測試需要通路一個活動資料庫所花的時間比運作時間要多。 用NUnit,你可以把測試分類,使它一次運作不同的測試組變得很容易,這樣就可以把需要很多時間的連接配接資料庫的測試排斥在外。這是個小例子:

 [TestFixture]      
[Category("NHibernate Tests")]      
public class SomeTests      
{      
    [Test]      
    public void TestSomethingThatDependsOnDb() { ... }      
}      

用NHibernate測試

在這篇文章的早些版本中,

ISession

被存儲并且擷取通過

HttpContext.Current.Items.

使用這種方式的問題是當你運作單元測試的時候強迫你模HTTPcontext。也防止了架構被windows程式幹擾。推薦

ISession

存儲獲得通過

System.Runtime.Remoting.Messaging.CallContext.

利用

CallContext

為Web程式,windows程式,單元測試提供恰當的

ISession

存儲,同樣的,看

NHibernateSample.Tests.Data.CustomerDaoTests

單元測試現在是"HTTP agnostic",另外,你會注意到這是做單元測試,除非你想把資料送出到資料庫,否則復原事物是個不錯的選擇。

用NHibernate "Mocked"測試

除非你明确測試DAO類,你常常不想運作依賴某個資料庫的測試。他們是緩慢且不穩定的;也就是當資料改變時,測試就終止了。當測試業務邏輯子產品時,單元測試不應該被打破如果資料改變了。但是最大的障礙是業務對象可能依賴DAOs。使用我們已經放在某個地方的 抽象工廠模式,我們能夠把mock DAOs注入到業務邏輯對象中,是以模拟對資料庫的通信。NHibernateSample.Tests.Domain.CustomerTests.中有個例子。下面的小段代碼建立了mock DAO,通過公有的setter把它賦給Customer對象。因為setter隻實作了

IOrderDao

接口。是以mock對象很容易代替活動資料庫的行為。

Customer customer = new Customer("Acme Anvils");      
customer.ID = "ACME";      
customer.OrderDao = new MockOrderDao();      

不幸的是,你正在維護的遺留的代碼并沒有“針對接口編碼”。常常都是直接依賴具體對象,它很難用mock對象代替DAO的模拟。遇到這些情況,你的選擇是在測試内部重構這些代碼,或者使用對象模拟工具如TypeMock.它甚至能夠模拟密封或者單個的類-放棄它很不容易;雖然它很強大,但最好把它遺忘除非絕對的需要。過早的使用TypeMock使你遠離面向接口程式設計。關于處理遺留代碼的更多的課程是重構代碼使其更為靈活。Michael Feather的 Working Effectively with Legacy Code 中有很多重構遺留代碼到測試中的好主意。

實踐

這個程式為建立可更新的企業級的web程式提供了一個健壯的資料層。(大多數技術也适合windows程式)但是在你自己的環境中使用之前,我的建議是:

  • NHibernateSample.Core.DataInterfaces.IGenericDAO

    ,

    NHibernateSample.Data.GenericNHibernateDAO

    ,

    NHibernateSample.Data.NHibernateSessionManager

    i放入一個可以重複使用的類庫中,因為他們在程式中傳遞中在不需要任何的修改。
  • 為注入DAO依賴選擇一個IoC服務到你的頁面管理器中。
  • 最後,我将忽視怎麼樣建立code-behind邏輯- 很快可以建立code-behind頁面;我考慮使用  Model-View-PresenterF,Page Controller, Front Controller, 模式 或者類似的其它的MVC.但是在此說明,那是完全不同的讨論。

我希望這篇文章能在ASP.NET, NHibernate, 和 Generics之間起到杠杆的作用,我目前用這種方法在我的項目中取得了很大的成功,很希望聽到你的經曆,同時讓我知道你的建議和問題。

總結

以下是對本文的簡單總結:

  • 業務對象應該通過接口同資料通路對象通信,總是依賴于抽象對象。
  • 在用戶端,業務邏輯層實作具體的資料通路對象現接口,
  • 通過抽象工廠暴露實踐通路對象的接口有助于測試和降低耦合。
  • 在陳述層和業務邏輯層之外儲存NHibernate session管理的細節。
  • 用單元測試分類很容易避免同資料庫有連接配接的測試。
  • 在  web.config 中設定

    hibernate.default_schema

    可以顯著的提高性能!

原文位址:http://www.codeproject.com/aspnet/NHibernateBestPractices.asp,請在這裡下載下傳代碼  <over>