首个基于NHibernate的应用程序
Your first NHibernate based application
英文原文地址:http://www.nhforge.org/wikis/howtonh/your-first-nhibernate-based-application.aspx
翻译原文地址:http://www.cnblogs.com/13yan/p/5671072.html
定义领域模型
让我们开始通过定义一个非常简单的领域模型。目前它是由一个称为产品的实体。该产品具有 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来完成这一任务。
领域模型中实体的一个实例对应数据库表中的行。所以我们必须在数据库中定义实体和相应的表之间的映射。此映射可以是另外定义一个映射文件 (一个xml 文档) 或装饰的实体和属性,可以完成此映射。随后,我将开始映射文件的定义。
译者的话:装饰的实体是什么?目前我们知道除了xml文件映射,还有Fluent NHibernate的Mapping和特性(attribute,类似Java中注解@),装饰可能是指他们的统称吧。
定义映射
创建一个文件夹 Mappings 到 FirstSample 项目中。并在该文件夹中添加一个新的 xml 文档并命名为 Product.hbm.xml。请注意"hbm"是文件名称的一部分。这是一项约定,这个约定用于NHibernate 自动识别这个文件为一个映射文件。右键此 xml 文件点击属性,在生成操作一项定义"嵌入的资源"。
在 Windows 资源管理器中找到 nhibernate mapping.xsd,它在 NHibernate 的 src 文件夹中,并将其复制到您的 SharedLibs 文件夹中。编辑 xml 映射文件时,在VS菜单中的XML-架构中导入此xsd文档。VS 然后将智能感知和验证。
回到在 VS 将架构添加到 Product.hbm.xml 文件
让我们从现在开始。每个映射文件都须定义一个 <hibernate-mapping> 根节点。
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="FirstSolution"
namespace="FirstSolution.Domain">
<!-- more mapping info here -->
</hibernate-mapping>
在映射文件引用领域模型类时你一定要提供的类的完全限定的名称(如 FirstSample.Domain.Product , FirstSample)。若要使 xml 不那么繁琐,你可以定义程序集名称和领域模型类的命名空间,到根节点的两个属性:assembly 和namespace。它是类似于使用 C# 中的声明。
现在,我们必须先为产品实体定义一个主键。技术上我们可以拿产品的名称属性作为主键,因为此属性必须定义,并且必须是唯一的。但通常会使用代理键代替它成为主键。因此我们将添加一个名为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; }
}
}
完整的映射文件
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="FirstSolution"
namespace="FirstSolution.Domain">
<class name="Product">
<id name="Id">
<generator class="guid" />
</id>
<property name="Name" />
<property name="Category" />
<property name="Discontinued" />
</class>
</hibernate-mapping>
NHibernate 不会以我们的方式,比如,它定义了很多合理的默认值。所以,如果您不显式地提供属性的列名,它将按属性名去对应列名。或 NHibernate从类的定义中,可以自动推断的表名或列名。因此我的 xml 映射文件不会被堆满冗余信息。有关于映射文件更详细的解释请参阅在线文档。你可以在这里找到它。
你解决方案资源管理器现在应该看起来像这样 (Domain.cd 包含我们简单的领域模型类图)
译者的话:
配置 NHibernate
我们现在必须告诉 NHibernate 我们想要使用哪个数据库产品,并提供详细的链接信息,以连接字符串的形式。NHibernate 支持许多数据库产品 !
向 FirstSolution 项目中添加一个新的 xml 文件,并命名为 hibernate.cfg.xml。将其属性"复制到输出目录"设置为"始终复制"。由于我们引用了SQL Server Compact Edition数据库在first sample项目中,所以输入以下信息到xml 文件中。
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<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.xml 文件来调用。这正是我们为什么要在这个文件中这么设置的原因。
在第三行的代码,我们告诉 NHibernate 它可以发现并包含Product类的程序集的映射信息。它将在嵌入的资源中只找到一个(Product.hbm.xml)这样的文件。
第四行代码使用NHibernate中 SchemaExport 的工具类,为我们在自动生成数据库中的架构。
注︰ 我们先不用去理解此测试方法中NHibernate 如何工作 , 但应当关注是否正确地安装。
如果你有安装的 TestDriven.Net 你可以现在只是右键点击里面的测试方法并选择"运行 Test(s)"来执行测试。
译者的话:VS2012以上版本的单元测试可以不用TestDriven.Net和NUnit,微软有自带的。
如果每一件事是好的你应该看到下面的结果,在输出窗口
译者的话:
如果你有安装 ReSharper 你可以开始测试通过单击黄色绿色圆圈的左边框,选择运行。
其结果是,如下所示
译者的话:原文没图,我们还是用VS自带的吧,如下图
在出现问题时
如果你测试失败,请仔细检查你的目标目录,在其中找到下列文件 (即︰ m:devprojectsFirstSolutionsrcFirstSolution.Testsindebug)
仔细检查NHibernate 配置文件 (hibernate.cfg.xml) 中或在映射文件 (Product.hbm.xml)中是否有错别字,最后检查映射文件 (Product.hbm.xml)是否设置为"嵌入的资源"的"生成操作"。如果测试成功,才继续。
我们第一次的 CRUD 操作
现在很明显我们的系统已是准备好开始了。我们成功地实现了我们的领域模型,定义映射文件和配置 NHibernate。最后我们使用 NHibernate 从我们的领域模型 (和我们映射文件) 自动生成数据库架构。
在 DDD (参考Eric Evans的《领域驱动设计》) 的精神,我们为所有的 crud 操作(创建、 读取、 更新和删除)定义了Repository。Repository接口是领域模型不实现的一部分!执行是特定的基础设施。我们要保持我们的领域模型和持久化无关 (PI)。
译者的话:这一段我不知道该如何去翻译它,但我可以解释它的意思。它的大致意思是根据DDD的思想,领域模型Domain里面不应该有和持久化有关的东西,比如我们的Product中不该包含数据库CRUD操作,而这些CRUD的基础操作该在仓储Repository接口中实现。
到我们的 FirstSolution 项目的Domain文件夹中添加一个新的接口。把它叫做 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);
}
译者的话:NHibernate3.0中,只有3个参数。new SchemaExport(cfg).Execute(false,true,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 方法。让我们实现它。但是,等一等,我们必须首先定义一个小的Helper类提供我们NHibernate session对象上的需求。
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,此类只创建session factory第一次。
现在我们可以定义 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);
}
}
第一次运行时此代码将失败,因为Update方法尚未在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 类似 (但不是等于)。
译者的话:他指的第一种方法HQL是这个样子的。
在上面的示例中我介绍了常用的技术使用 NHibernate 时。它被称为fluent接口。作为结果的代码是简练也更易于理解。你可以看到一个 HQL 查询是一个字符串,它可以具有嵌入 (命名) 参数。参数使用前缀 ':'。NHibernate 定义很多的helper方法 (如示例中使用 SetString),将各种类型的值分配给这些参数。最后通过使用 UniqueResult 我告诉 NHibernate 希望只有一条记录返回。如果多个然后引发异常,HQL 查询将返回一条记录。要获取更多的信息 HQL 请阅读在线文档。
第二个版本使用criteria query来搜索请求的Product。
public Product GetByName(string name)
{
using (ISession session = NHibernateHelper.OpenSession())
{
Product product = session
.CreateCriteria(typeof(Product))
.Add(Restrictions.Eq("Name", name))
.UniqueResult<Product>();
return product;
}
}
NHibernate 的许多用户认为这种做法是更多面向的对象。在另一方面编写的criteria语法复杂查询可以迅速成为难以理解。
实现的最后一个方法是 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())
{
var products = session
.CreateCriteria(typeof(Product))
.Add(Restrictions.Eq("Category", category))
.List<Product>();
return products;
}
}
摘要
在这篇文章中我已经给你如何实现基本示例领域模型,定义映射到数据库以及如何配置 NHibernate 能够持久化领域对象在数据库中。我给你展示了如何通常编写和测试您的领域对象的 CRUD 方法。我拿MS SQL Compact Edition 作为示例数据库,但可以使用任何其他受支持的数据库 (你只需要相应地更改 hibernate.cfg.xml 文件)。我们没有依赖于外部框架或工具以外的数据库和 NHibernate 本身 (.NET 当然从来没有计算在内)。
译者的话:终于翻译完了,这篇快速开始非常适合初学者,因为提供的例子是可以被实现的,而且可以同时入门DDD和TDD,看得出作者非常用心。而我也在其中加入了批注和补充了显示不了的图片。