Nhibernate 同 asp.net ,泛型,单元测试的最佳实践(同文章里的内容)

来源:互联网 发布:郑州淘宝开店培训 编辑:程序博客网 时间:2024/04/29 03:57

介绍

        NHibernate,和其他ORM(对象关系映射)工具一样,可以减轻数千行代码,和存储过程的维护,因此可以让开发者更多的关注项目的核心:领域模型和业务逻辑。即使你使用CodeSimth 或者LLBLGen Pro工具自动生成你的ADO.Net 数据访问层,但是NHibernate降低了业务模型和关系模型的耦合。你的数据库应该执行定义和支持你的领域模型的细节,而不做其他事前。 由于论坛上充满了相关要点的激烈讨论,这篇文章并不是说使用ORMADO.NET更好,或者NHibernate比其他ORM工具更好,仅仅是在描述如何使用定制好的设计模式把NHibernate融入到ASP.NET中。

       这篇文章假设读者对C#NHibernate有很好的理解,有开发数据访问对象模式的经验,对泛型有基本的了解。如果你仅仅想获得NHibernate的知识,我给你推荐两篇很好的入门文章在 TheServerSide.net  Part 1 and Part 2. 。为了扩张对DAODate Access Object)设计模式的了解,可以去 to J2EE's BluePrints catalog. 

       ASP.NET中建立坚固的数据综合性的例子,我们的主要完成以下目标:

l         陈述层和业务逻辑层应该不必了解如何和数据库通信是有好处的。你能够修改你的需求只需对这些层作最小的改动

l         业务逻辑应该很容易测试,不依赖一个活动的数据库。

l         NHibernate特性,如lazy-loading,应该能够能够在整个页面生命周期都可用。

l         .NET 2.0 的泛型应该对减小代码重复起杠杆作用。

 

一个简单的应用程序包括NHibernateASP.NET,.NET Generics的综合实例,同时汇集了上面的目标。下面描述的是如何在程序理应用前面提及到的目标。但是在进入详细之前,我们先总体了解一下,并且下载例子安装并运行。

 

一个简单的应用程序

 整个简单的应用程序,冒着成为死尸的风险,利用SQL Server 2000 的数据 Northwind 查看并且修改Customers表。演示 Lazy-loading的使用,同时也陈列了每个Customer Orders。所有这些你只需要安装.NET 2.0 FrameworkSQL 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 页面扮演控制器,同业务和数据访问层通信,因此这不是最好的MVCModel-View-Controller,模型视图控制器)分离实践,但是它很简单,能容易演示。(我在以后将要彻底的讨论MVCASP.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.configHTTPModule的中包含如下片断。

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

HTTPModule包含在例子程序中开始一个事物在web请求开始的时候。并且提交/关闭它在请求结束的时候。联合它只需要很短的时间,因为NHibernate不会打开数据库连接直到需要的时候。我们会留意到,你可以控制这个策略同其他策略曝露通过session管理。你想打开一个session的其它策略不和事物或者用Nhibernate注册一个 IInterceptor 接口。(用IInterceptorc可以很好的控制权限,请查看Hibernate in Action8.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能很好的帮助我)这是很大的提高,你能够单元测试用“mockDAO类,例如:下面代码,在NHibernateSample.Tests.Data.CustomerDaoTestsr中,重新得到一个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,和创建返回“mockDAO对象作单元测试的具体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的管理。

DAOFactoryGenericDAO

我已经在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. 利用CallContextWeb程序,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-PresenterFPage Controller, Front Controller, 模式 或者类似的其它的MVC.但是在此说明,那是完全不同的讨论。

我希望这篇文章能在ASP.NET, NHibernate, Generics之间起到杠杆的作用,我当前用这种方法在我的项目中取得了很大的成功,很希望听到你的经历,同时让我知道你的建议和问题。

总结

以下是对本文的简单总结:

  • 业务对象应该通过接口同数据访问对象通信,总是依赖于抽象对象。
  • 在客户端,业务逻辑层实现具体的数据访问对象现接口,
  • 通过抽象工厂暴露实践访问对象的接口有助于测试和降低耦合。
  • 在陈述层和业务逻辑层之外保存NHibernate session管理的细节。
  • 用单元测试分类很容易避免同数据库有连接的测试。
  •   web.config 中设置hibernate.default_schema可以显著的提高性能!

 

原文地址:http://www.codeproject.com/aspnet/NHibernateBestPractices.asp,请在这里下载代码  <over>

 
原创粉丝点击