你的位置:首页 > ASP.net教程

[ASP.net教程]【翻译】首个基于NHibernate的应用程序


首个基于NHibernate的应用程序

 Your first NHibernate based application

原文地址:http://www.nhforge.org/wikis/howtonh/your-first-nhibernate-based-application.aspx

 

本文涉及到的DEMO下载

 

定义领域模型

 

让我们开始通过定义一个非常简单的领域模型。目前它是由一个称为产品的实体。该产品具有 3 个属性:名称、 类别和中止。

 

添加一个文件夹 Domain 到您的解决方案的 FirstSample 项目。到此文件夹中添加一个新类 Product.cs。该代码是非常简单,使用自动属性 (C# 3.0新的特征)

 

namespace FirstSolution.Domain

{

 public class Product

 {

  public string Name { get; set; }

  public string Category { get; set; }

  public bool Discontinued { get; set; }

 }

}

 

现在我们想要能够持久化相关数据库中此实体的实例。我们选择了 NHibernate来完成这一任务。

领域模型中实体的一个实例对应数据库表中的行。所以我们必须在数据库中定义实体和相应的表之间的映射。此映射可以是另外定义一个映射文件 (一个

 

定义映射

 

创建一个文件夹 Mappings 到 FirstSample 项目中。并在该文件夹中添加一个新的 Product.hbm.。请注意"hbm"是文件名称的一部分。这是一项约定,这个约定用于NHibernate 自动识别这个文件为一个映射文件。右键此 文件点击属性,在生成操作一项定义"嵌入的资源"

 

在 Windows 资源管理器中找到 nhibernate mapping.xsd,它在 NHibernate 的 src 文件夹中,并将其复制到您的 SharedLibs 文件夹中。在VS菜单中的

 

回到在 VS 将架构添加到 Product.hbm. 文件

 

让我们从现在开始。每个映射文件都须定义一个 <hibernate-mapping> 根节点。

 

<?

<hibernate-mapping

       assembly="FirstSolution"

       namespace="FirstSolution.Domain">

 

  <!-- more mapping info here -->

 

</hibernate-mapping>

 

在映射文件引用领域模型类时你一定要提供的类的完全限定的名称(如 FirstSample.Domain.Product , FirstSample)。若要使

 

现在,我们必须先为产品实体定义一个主键。技术上我们可以拿产品的名称属性作为主键,因为此属性必须定义,并且必须是唯一的。但通常会使用代理键代替它成为主键。因此我们将添加一个名为Id的属性到我们的实体。我们使用 Guid 作为 Id 的类型,但也可以是 int 或 long。

 

using System;

 

namespace FirstSolution.Domain

{

 public class Product

 {

  public Guid Id { get; set; }

  public string Name { get; set; }

  public string Category { get; set; }

  public bool Discontinued { get; set; }

 }

}

 

完整的映射文件

 

<?

<hibernate-mapping

       assembly="FirstSolution"

       namespace="FirstSolution.Domain">

 

  <class name="Product">

 <id name="Id">

   <generator />

 </id>

 <property name="Name" />

 <property name="Category" />

 <property name="Discontinued" />

  </class>

 

</hibernate-mapping>

 

 

NHibernate 不会以我们的方式,比如,它定义了很多合理的默认值。所以,如果您不显式地提供属性的列名,它将按属性名去对应列名。或 NHibernate从类的定义中,可以自动推断的表名或列名。因此我的

 

你解决方案资源管理器现在应该看起来像这样 (Domain.cd 包含我们简单的领域模型类图)

 

配置 NHibernate

 

我们现在必须告诉 NHibernate 我们想要使用哪个数据库产品,并提供它在详细的链接信息,以连接字符串的形式。NHibernate 支持许多数据库产品 !

 

向 FirstSolution 项目中添加一个新的 hibernate.cfg.。将其属性"复制到输出目录"设置为"始终复制"。由于我们引用了SQL Server Compact Edition数据库在first sample项目中,所以输入以下信息到

 

<?

<hibernate-configuration

  <session-factory>

 <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>

 <property name="dialect">NHibernate.Dialect.MsSqlCeDialect</property>

 <property name="connection.driver_class">NHibernate.Driver.SqlServerCeDriver</property>

 <property name="connection.connection_string">Data Source=FirstSample.sdf</property>

 

 <property name="show_sql">true</property>

  </session-factory>

</hibernate-configuration>

 

使用此配置文件,我们告诉 NHibernate 我们想要使用 MS SQL Server Compact Edition作为我们的目标数据库和数据库的名称是 FirstSample.sdf (= 连接字符串)。我们同时也定义了希望看到NHibernate生成并发送到数据库的 SQL语句 (在开发过程中调试时强烈推荐启用此定义)。仔细检查你的代码中有没有错别字 !

 

添加一个叫FirstSample.sdf的空的数据库,到 FirstSample 项目 (选择本地数据库作为模板)

 

单击添加并忽略数据集创建向导 (就是点击取消)。

 

译者的话:我们不一定安装过MS SQL Server Compact Edition数据库,我将在Demo中把它替换成SQLite和相应的配置,这样我们就不需要为了这个快速入门而去专门找一个数据库了。

 

测试设置

 

是时候来测试我们的安装了。首先验证您的 SharedLibs 文件夹中有以下文件

 

您可以找到Microsoft SQL Server Compact Edition在你的程序文件夹中目录最后 8 个文件。

 

注︰ System.Data.SqlServerCe.dll 位于子文件夹中的桌面。

 

所有其他文件可以在NHibernate 文件夹中找到。

 

在您的测试项目中添加对 FirstSample 项目的引用。另外测试项目引用 NHibernate.dll、 nunit.framework.dll 和 Systm.Data.SqlServerCe.dll (记得要引用位于 SharedLibs 文件夹中的文件 !)。要注意为设置属性"复制本地"为 true 为 System.Data.SqlServerCe.dll, 因为在默认情况下它设置为 false !

 

译者的话:现在VS2012以上都有自带的单元测试项目,也非常好用。所以无需引用nunit.framework.dll,同样System.Data.SqlServerCe.dll也可以替换成System.Data.Sqlite.dll。

 

在测试项目中添加一个类,命名为 GenerateSchema_Fixture。

 

现在将下面的代码添加到 GenerateSchema_Fixture 文件

 

using FirstSolution.Domain;
using NHibernate.Cfg;
using NHibernate.Tool.hbm2ddl;
using NUnit.Framework;
 
namespace FirstSolution.Tests
{
 [TestFixture]
 public class GenerateSchema_Fixture
 {
  [Test]
  public void Can_generate_schema()
  {
   var cfg = new Configuration();
   cfg.Configure();
   cfg.AddAssembly(typeof (Product).Assembly);
   
   new SchemaExport(cfg).Execute(false, true, false, false);
  }
 }
}

 

测试方法的第一行创建 NHibernate 配置类的一个新实例。此类用于配置 NHibernate。在第二行,我们告诉 NHibernate 配置本身。NHibernate 将留心配置信息,因为我们没在测试方法中提供任何信息。所以 NHibernate 将搜索输出目录中的 hibernate.cfg. 文件来调用。这正是我们为什么要在这个文件中这么设置的原因。

 

在第三行的代码,我们告诉 NHibernate 它可以发现并包含Product类的程序集的映射信息。它将在嵌入的资源中只找到一个(Product.hbm.这样的文件。

 

第四行代码使用NHibernate中 SchemaExport 的工具类,为我们在自动生成数据库中的架构。

 

注︰ 我们先不用去理解此测试方法中NHibernate 如何工作 , 但应当关注是否正确地安装。

 

如果你有安装的 TestDriven.Net 你可以现在只是右键点击里面的测试方法并选择"运行 Test(s)"来执行测试。

 

译者的话:VS2012以上版本的单元测试可以不用它,微软有自带的。

 

如果每一件事是好的你应该看到下面的结果,在输出窗口

 

如果你有安装 ReSharper 你可以开始测试通过单击黄色绿色圆圈的左边框,选择运行。

 

译者的话:ReSharper同样也不用去理它,这个东西提示实在太多,现在版本的VS智能提醒够用了。

 

其结果是,如下所示

 

在出现问题时

 

如果你测试失败,请双重检查你的目标目录,在其中找到下列文件 (即︰ m:dev\projects\FirstSolution\src\FirstSolution.Tests\bin\debug)

 

仔细检查NHibernate 配置文件 (hibernate.cfg. 中或在映射文件 (Product.hbm.中是否有错别字,最后检查映射文件 (Product.hbm.是否设置为"嵌入的资源"的"生成操作"。如果测试成功,才继续。

 

我们第一次的 CRUD 操作

 

现在很明显我们的系统已是准备好开始了。我们成功地实现了我们的领域模型,定义映射文件和配置 NHibernate。最后我们使用 NHibernate 从我们的领域模型 (和我们映射文件) 自动生成数据库架构。

 

在 DDD (参考Eric Evans的《领域驱动设计》) 的精神,我们为所有的 crud 操作(创建、 读取、 更新和删除)定义了Repository。Repository接口是领域模型不实现的一部分!执行是特定的基础设施。我们要保持我们的领域模型和持久化无关 (PI)。

 

译者的话:这一段我不知道该如何去翻译它,但我可以解释它的意思。它的大致意思是根据DDD的思想,领域模型Domain里面不应该有和持久化有关的东西,比如我们的Product中不该包含数据库CRUD操作,而这些CRUD的基础操作该在仓储Repository接口中实现。

 

到我们的 FirstSolution 项目的域文件夹中添加一个新的界面。把它叫做 IProductRepository。让我们定义以下接口

 

using System;
using System.Collections.Generic;
 
namespace FirstSolution.Domain
{
 public interface IProductRepository
 {
  void Add(Product product);
  void Update(Product product);
  void Remove(Product product);
  Product GetById(Guid productId);
  Product GetByName(string name);
  ICollection<Product> GetByCategory(string category);
 }
}

 

添加一个类 ProductRepository_Fixture 到测试项目下,并添加下面的代码

 

[TestFixture]
 public class ProductRepository_Fixture
 {
  private ISessionFactory _sessionFactory;
  private Configuration _configuration;
 
  [TestFixtureSetUp]
  public void TestFixtureSetUp()
  {
   _configuration = new Configuration();
   _configuration.Configure();
   _configuration.AddAssembly(typeof (Product).Assembly);
   _sessionFactory = _configuration.BuildSessionFactory();
  }
 }

 

在 TestFixtureSetUp 方法的第四行,我们创建一个session factory。这是一个开销很大的过程,因此应该只有一次执行。这就是为什么把它放到这种测试期间只执行一次的方法的原因。

 

要保持我们测试方法无副作用,每个测试方法执行之前,我们重新创建我们的数据库架构。因此我们添加下面的方法

 

[SetUp]

  public void SetupContext()

  {

   new SchemaExport(_configuration).Execute(false, true, false, false);

  }

 

现在我们可以实现向数据库中添加一个新的Product实例的测试方法。添加一个新的文件夹名为Repositories到 FirstSolution 项目。到此文件夹中添加一个类 ProductRepository。使 ProductRepository 从 IProductRepository 接口继承。

 

using System;
using System.Collections.Generic;
using FirstSolution.Domain;
 
namespace FirstSolution.Repositories
{
 public class ProductRepository : IProductRepository
 {
  public void Add(Product product)
  {
   throw new NotImplementedException();
  }
 
  public void Update(Product product)
  {
   throw new NotImplementedException();
  }
 
  public void Remove(Product product)
  {
   throw new NotImplementedException();
  }
 
  public Product GetById(Guid productId)
  {
   throw new NotImplementedException();
  }
 
  public Product GetByName(string name)
  {
   throw new NotImplementedException();
  }
 
  public ICollection<Product> GetByCategory(string category)
  {
   throw new NotImplementedException();
  }
 }
}

 

操作数据

 

现在回到ProductRepository_Fixture测试类和实现第一个测试方法

  [Test]

  public void Can_add_new_product()

  {

   var product = new Product {Name = "Apple", Category = "Fruits"};

   IProductRepository repository = new ProductRepository();

   repository.Add(product);

  }

 

首次运行的测试方法将失败,因为我们的仓储类未实现 Add 方法。让我们做它。但是,等一等,我们必须首先定义一个小的帮助器类提供我们会话对象上的需求。

 

using FirstSolution.Domain;
using NHibernate;
using NHibernate.Cfg;
 
namespace FirstSolution.Repositories
{
 public class NHibernateHelper
 {
  private static ISessionFactory _sessionFactory;
 
  private static ISessionFactory SessionFactory
  {
   get
   {
    if(_sessionFactory == null)
    {
     var configuration = new Configuration();
     configuration.Configure();
     configuration.AddAssembly(typeof(Product).Assembly);
     _sessionFactory = configuration.BuildSessionFactory();
    }
    return _sessionFactory;
   }
  }
 
  public static ISession OpenSession()
  {
   return SessionFactory.OpenSession();
  }
 }
}

 

运行期间,此类只创建session factory第一次,不管客户端何时需要一个新的session。

 

现在我们可以定义 Add 方法在 ProductRepository 中,如下所示

 

public void Add(Product product)

  {

   using (ISession session = NHibernateHelper.OpenSession())

    using (ITransaction transaction = session.BeginTransaction())

    {

     session.Save(product);

     transaction.Commit();

    }

  }

 

第二次运行的测试方法会再次失败并显示以下消息

 

这是因为 NHibernate 是默认情况下配置为使用延迟加载的所有实体。这是推荐的方法,我强烈建议不要更改,为了最大的灵活性。

 

我们怎样才能解决这个问题?很容易,让领域模型中所有属性 (方法) 加上Virtual关键字即可。让我们为我们的Product类加上这个。

public class Product

 {

  public virtual Guid Id { get; set; }

  public virtual string Name { get; set; }

  public virtual string Category { get; set; }

  public virtual bool Discontinued { get; set; }

 }

 

现在再次运行测试。它应该会成功,我们得到以下输出

 

请注意NHibernate输出的 sql 语句。

 

现在我们觉得,我们成功地向一个新的Product插入数据库。但让我们测试它是否真的是这样。让我们来扩展我们的测试方法

 

[Test]
  public void Can_add_new_product()
  {
   var product = new Product {Name = "Apple", Category = "Fruits"};
   IProductRepository repository = new ProductRepository();
   repository.Add(product);
 
   // use session to try to load the product
   using(ISession session = _sessionFactory.OpenSession())
   {
    var fromDb = session.Get<Product>(product.Id);
    // Test that the product was successfully inserted
    Assert.IsNotNull(fromDb);
    Assert.AreNotSame(product, fromDb);
    Assert.AreEqual(product.Name, fromDb.Name);
    Assert.AreEqual(product.Category, fromDb.Category);
   }
  }

 

再次运行测试。希望它会成功......

 

现在我们准备也实现repository中的其他方法。为了测试这我们宁愿要一个repository  (即数据库表) 已经包含了一些产品。没有什么比这更简单。只是添加 CreateInitialData 方法,如下所示添加到测试类

 

private readonly Product[] _products = new[]
     {
      new Product {Name = "Melon", Category = "Fruits"},
      new Product {Name = "Pear", Category = "Fruits"},
      new Product {Name = "Milk", Category = "Beverages"},
      new Product {Name = "Coca Cola", Category = "Beverages"},
      new Product {Name = "Pepsi Cola", Category = "Beverages"},
     };
 
  private void CreateInitialData()
  {
   
   using(ISession session = _sessionFactory.OpenSession())
    using(ITransaction transaction = session.BeginTransaction())
    {
     foreach (var product in _products)
      session.Save(product);
     transaction.Commit();
    }
  }

 

(在创建架构调用后) 从 SetupContext 方法调用此方法。现在每次数据库架构创建数据库后填充一些产品。

让我们测试用下面的代码库的更新方法

 

[Test]
  public void Can_update_existing_product()
  {
   var product = _products[0];
   product.Name = "Yellow Pear";
   IProductRepository repository = new ProductRepository();
   repository.Update(product);
 
   // use session to try to load the product
   using (ISession session = _sessionFactory.OpenSession())
   {
    var fromDb = session.Get<Product>(product.Id);
    Assert.AreEqual(product.Name, fromDb.Name);
   }
  }

 

第一次运行时此代码将失败,因为更新方法尚未在Repository中实现。注︰ 这是预期的行为,因为在 TDD 第一次运行测试时它应该总是失败 !

 

译者的话:这篇快速开始的入门教程水非常深,又是DDD,又是TDD,吓死人了,没接触过的人可以忽略。同时也可见NHibernate更多是面向一些资深的面向对象程序员,可悲的是很多程序员未入门时就接触到了它。叹息!

 

类似于 Add 方法我们实现Repository中的 Update 方法。唯一的区别是我们调用NHibernate session对象的update 方法而不是Save方法。

 

  public void Update(Product product)

  {

   using (ISession session = NHibernateHelper.OpenSession())

   using (ITransaction transaction = session.BeginTransaction())

   {

    session.Update(product);

    transaction.Commit();

   }

  }

 

再次运行测试希望它成功。

 

Delete 方法是直截了当。测试是否真的已删除记录时,我们只是断言由会话的 get 方法返回的值是等于 null。这里是测试方法

 

    [Test]

  public void Can_remove_existing_product()

  {

   var product = _products[0];

   IProductRepository repository = new ProductRepository();

   repository.Remove(product);

 

   using (ISession session = _sessionFactory.OpenSession())

   {

    var fromDb = session.Get<Product>(product.Id);

    Assert.IsNull(fromDb);

   }

  }

 

Repository中删除方法的实现

 

  public void Remove(Product product)

  {

   using (ISession session = NHibernateHelper.OpenSession())

    using (ITransaction transaction = session.BeginTransaction())

    {

     session.Delete(product);

     transaction.Commit();

    }

  }

 

查询数据库

 

我们仍然必须执行查询的数据库对象的三个方法。我们先从最容易的一个,GetById。我们首先编写测试

 

[Test]

  public void Can_get_existing_product_by_id()

  {

   IProductRepository repository = new ProductRepository();

   var fromDb = repository.GetById(_products[1].Id);

   Assert.IsNotNull(fromDb);

   Assert.AreNotSame(_products[1], fromDb);

   Assert.AreEqual(_products[1].Name, fromDb.Name);

  }

 

然后完成测试的代码

 

  public Product GetById(Guid productId)

  {

   using (ISession session = NHibernateHelper.OpenSession())

    return session.Get<Product>(productId);

  }

 

现在,那很简单。为以下两种方法,我们使用session对象的新方法。让我们开始用 GetByName 方法。像往常一样我们先写测试

 

    [Test]

  public void Can_get_existing_product_by_name()

  {

   IProductRepository repository = new ProductRepository();

   var fromDb = repository.GetByName(_products[1].Name);

 

   Assert.IsNotNull(fromDb);

   Assert.AreNotSame(_products[1], fromDb);

   Assert.AreEqual(_products[1].Id, fromDb.Id);

  }

 

GetByName 方法的实现可以通过使用两个不同的方法。第一使用 HQL (Hibernate Query Language) 和第二个 HCQ (Hibernate Criteria Query)。让我们开始使用 HQL。HQL 是面向对象的查询语言 SQL 类似 (但不是等于)。




在上面的示例中我介绍了常用的技术使用 NHibernate 时。它被称为fluent接口。作为结果的代码是简练也更易于理解。你可以看到一个 HQL 查询是一个字符串,它可以具有嵌入 (命名) 参数。参数使用前缀 ':'。NHibernate 定义很多的helper方法 (如示例中使用 SetString),将各种类型的值分配给这些参数。最后通过使用 UniqueResult 我告诉 NHibernate 希望只有一条记录返回。如果多个然后引发异常,HQL 查询将返回一条记录。要获取更多的信息 HQL 请阅读在线文档。

 

第二个版本使用criteria query来搜索请求的Product。

 

  public Product GetByName(string name)

  {

   using (ISession session = NHibernateHelper.OpenSession())

   {

    Productproduct = session

    .CreateCriteria(typeof(Product))

    .Add(Restrictions.Eq("Name", name))

    .UniqueResult<Product>();

    return product;

   }

  }

 

NHibernate 的许多用户认为这种做法是更多面向的对象。在另一方面编写的标准语法复杂查询可以迅速成为难以理解。

 

实现的最后一个方法是 GetByCategory。此方法返回Product的列表。测试可以实现,如下所示

 

[Test]

  public void Can_get_existing_products_by_category()

  {

  IProductRepository repository = new ProductRepository();

   var fromDb =repository.GetByCategory("Fruits");

 

  Assert.AreEqual(2, fromDb.Count);

   Assert.IsTrue(IsInCollection(_products[0],fromDb));

  Assert.IsTrue(IsInCollection(_products[1], fromDb));

  }

 

  private bool IsInCollection(Product product, ICollection<Product> fromDb)

  {

   foreach (var item in fromDb)

    if (product.Id == item.Id)

     return true;

   return false;

  }

 

方法本身可能包含下面的代码

 

  public ICollection<Product> GetByCategory(string category)

  {

   using (ISession session = NHibernateHelper.OpenSession())

   {

    varproducts = session

    .CreateCriteria(typeof(Product))

     .Add(Restrictions.Eq("Category", category))

    .List<Product>();

    return products;

   }

  }

 

摘要

 

在这篇文章中我已经给你如何实现基本示例领域模型,定义映射到数据库以及如何配置 NHibernate 能够持久化领域对象在数据库中。我给你展示了如何通常编写和测试您的领域对象的 CRUD 方法。我拿MS SQL Compact Edition 作为示例数据库,但可以使用任何其他受支持的数据库 (你只需要相应地更改 hibernate.cfg.文件)。我们没有依赖于外部框架或工具以外的数据库和 NHibernate 本身 (.NET 当然从来没有计算在内)。

 

译者的话:终于翻译完了,这篇快速开始非常适合初学者,因为提供的例子是可以被实现的,而且可以同时入门DDD和TDD,看得出作者非常用心。而我也在其中加入了批注,以适应我们当下的情况。