代码改变世界

Welcome to NHibernate

2009-12-05 16:28  jiva  阅读(591)  评论(0编辑  收藏  举报

Welcome to NHibernate

Wiki extracted from the original blog post of Gabriel Schenker

假如你正在阅读这篇文章,我们假定你已经下载了NHibernate(downloaded NHibernate), 并且准备开始使用。

这个教程主要包含以下内容:

Ø 安装NHibernate

Ø 定义一个简单的业务对象类(Business Object Class)

Ø 创建NHiberante Mapping (从数据库中)加载和保存业务对象(Business Object)

Ø 配置NHibernate以便连接到数据库并进行操作(talk to your local database)

Ø 自动生成数据库(注: 数据库是不能自动生成的,从下文内容看应该是自动生成Database Schema)

Ø 使用Repository pattern 编写简单的CRUD操作。

Ø 编写单元测试保证代码运行正确。

最后的Solution 差不多应该是这个样子:

clip_image002

要事第一(First things first)

首先你下载的Zip文件开始。

Install Nhibernate

假如你已经下载了NHibernate binaries Zip File,你所需要做的事情就是将这个文件解压到合适的目录(somewhere sensible)。 就个人来说,我一般创建一个叫做SharedLibs的目录,然后将NHibernate解压到该处(c:\Code\SharedLibs\NHibernate )(注: 好习惯),当然你可以放到任意目录处,只是请记住这个目录将是你添加对NHibernate 和NUnit DLLs References的地方。

Yeah! NHibernate 已经安装完成.(注:2.0已经没有1.2的msi文件了。呵呵)。然后我们将使用VS创建一个Project。 VS的版本是 VS2008, .net Framework 3.5。

Create your Project

开始构造应用程序和业务对象时(our application and business objects), 我们需要创建一个空的Project。打开VS并且创建一个Class Library project。然后让我们开始做一些有意思的事情:创建一个业务对象(Business Object)。

Defining the Business Objects

我们首先定义个非常简单的业务背景(domain)。目前这个Domain只包含一个叫做Product的实体(Entity)。Product有三个属性: Name, Category, Discontinued。

clip_image004

在你的Solution的FirstSample Project中添加一个名称为Domain的文件夹, 在该目录下添加一个Product.cs Class。 代码很简单,并且使用自动属性特性(C# 3.0的一个特性)。

代码如下

namespace FirstSolution.Domain
{
    
public class Product
    {
        
public string Name { getset; }
     
public string Category { getset; }
    
public bool Discontinued { getset; }
    }

 

 

我们希望能够将该业务实体(Entity)的实例(Instance)持久化(persist)到一个(关系)数据库中,

我们选择NHibernate 来完成这个任务。一个对象实例(instance of an entity)对应数据库表中的一行(a row in a table in the database)。这种对应关系可以同Mapping File(XML)来实现,也可以通过给实体类(entity)添加属性(attibute)来实现。我们使用Mapping File。

Define the Mapping

在FirstSample Project中创建一个Mappings文件夹。添加一个XML文件,命名为Product.hbm.xml。注意文件名称中的hbm部分,这是NHibernate自动识别这个文件为一个映射文件(mapping file)的约定。将该文件定义为嵌入资源(Embedded Resource)。

在NHibernate中找到NHibernate-mapping.xsd文件(一般在NHibernate-2.1.0.GA-bin\Required_Bins目录下),当处理mapping Files的时候我们可以使用这个XML作为XML Schema Definition File. 当编辑Mapping File的时候,VS会提供智能提示和校验。

在VS中给Product.hbm.xml文件添加Schema, 如下图所示:

clip_image006

现在开始: 每个Mapping File都需要定义一个根节点<hibernate-mapping>

代码
1 <?xml version="1.0" encoding="utf-8" ?>
2 <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
3 assembly="FirstSolution"
4 namespace="FirstSolution.Domain">
5 <!-- more mapping info here -->
6 </hibernate-mapping

 

在Mapping File中,当引用一个业务实体类(Domain Class)时,你经常需要提供完整的限定类名(qualified name of the class), 例如(FirstSample.Domain.Product, FirstSample)。你可以通过定义Assembly name来使得XML不那么冗长。方法是在根节点中为属性(attributes) assembly 和namespace来指定为业务实体类(domain class)的assemble name 和namespace。这个和在C#中使用using 差不多。

首先我们需要为Product Entity 定义一个主键(primary key)(注:这个说法是不大准确的,业务实体没有primary key 的概念,这个应该是为了one entity mapping one row)。由于Products的属性Name必须定义并且唯一,所以我们可以采用这个属性来作为主键,但是比较常用的做法是使用一个代理键(surrogate key)。因此我们添加了一个属性ID,我们使用GUID作为ID字段的类型,当然我们也可以使用Int 或者long类型。(注: 由于本人使用时sql server 2000, 所以使用了Int)作为主键.

 1 using System;
 2 namespace FirstSolution.Domain
 3 {
 4   public class Product
 5   {
 6     public Guid Id { getset; } 
 7 
 8     //在不支持GUID的数据库上建议使用Int 
 9 
10    //public int Id { get; set; } 
11 
12     public string Name { getset; }
13     public string Category { getset; }
14     public bool Discontinued { getset; }
15 }
16 }

 

完整的映射文件(mapping file)如下

 1 <?xml version="1.0" encoding="utf-8" ?>
 2 <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
 3 assembly="FirstSolution"
 4 namespace="FirstSolution.Domain">
 5 <class name="Product">
 6    <id name="Id">
 7      <generator class="guid" />
 8    </id>
 9    <property name="Name" />
10    <property name="Category" />
11    <property name="Discontinued" />
12 </class>
13 </hibernate-mapping>

 

(注: 蛮简单的,Mapping File其实也没有那么烦,或许就是NHibernate之所以不是Hibernate)

为了应用方便,简单(doesn't get in our way), NHibernate定义了很多合理的默认值(reasonable defaults),. 所以你不需要明确的为某个属性(property)指定列名(column), NHibernate自动会根据属性(property)来命名column,或者Nhibernate能够自动根据类的的定义推断出来表名或者列的类型。所以上面的XML看起来没有大量的信息,比较清爽。可以通过Online doc 找到关于Mapping 的更多的解释.点这里.

目前为止, Solution Explorer应该差不多这个样子。(注:Design是类图设计,自己添加)

clip_image002[1]

Configure NHibernate

我们需要告诉NHibernate我们使用哪个数据库产品,还有一些连接到该数据库的详细信息。NHibernate支持很多中数据库。

首先添加一个XML文件到FirstSolution Project, 命名为hibernate.cfg.xml. 设置该文件的属性"Copy to Output"为"Copy always"。 (注:作者使用SQL Server CE, 我使用SQL Server2000, 所以都按照SQL Server2000的走)。

(注:这个文件的内容可以NHibernate-2.1.0.GA-bin\Configuration_Templates,直接Copy过来,然后根据需要裁剪一下。另外这个XML也有相应的XML Schema, 是NHibernate-2.1.0.GA-bin\Required_Bins\nhibernate-configuration.xsd)。

(以下是作者使用的SQL Server CE 的配置文件。可以参考修改成不同的数据库。比如我的是SQL Server2000)

 1 <?xml version="1.0" encoding="utf-8" ?>
 2 <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
 3 <session-factory>
 4   <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
 5   <property name="dialect">NHibernate.Dialect.MsSqlCeDialect</property>
 6   <property name="connection.driver_class">NHibernate.Driver.SqlServerCeDriver</property>
 7   <property name="connection.connection_string">Data Source=FirstSample.sdf</property>
 8   <property name="show_sql">true</property>
 9 </session-factory>
10 </hibernate-configuration>

 

注意上面属性的 show_SQL 是设置为True的,这样可以看到NHibernate生成的SQL 语句。

在SQL Server中添加数据库。名字要和你在上面的配置文件中的database name 一致。

Test the Setup

构建测试。

(注:略掉作者的SQL CE的相关配置)

在Test Project中添加对FirstSolution的引用。同时添加 NHibernate.dll, nunit.framework.dll。设置这些文件的属性"copy local "to True. 在Test Project中添加一份hibernate.cfg.xml的引用。

添加一个叫做GenerateSchema_Fixture的Class到Test Project。 Test Project目前看起来应该是这个样子。

clip_image008

(注:略掉作者的SQL CE的相关配置)

Now add the following code to the GenerateSchema_Fixture file

 1 using FirstSolution.Domain; 
 2 
 3 using NHibernate.Cfg; 
 4 
 5 using NHibernate.Tool.hbm2ddl; 
 6 
 7 using NUnit.Framework; 
 8 
 9 namespace FirstSolution.Tests 
10 
11 
12 
13    [TestFixture] 
14 
15    public class GenerateSchema_Fixture 
16 
17   { 
18 
19       [Test] 
20 
21       public void Can_generate_schema() 
22 
23       { 
24 
25         var cfg = new Configuration(); 
26 
27         cfg.Configure(); 
28 
29         cfg.AddAssembly(typeof (Product).Assembly); 
30 
31         new SchemaExport(cfg).Execute(falsetruefalsefalse); 
32 
33       } 
34 
35    } 
36 
37 }

 

第一行测试代码创建一个NHibernate Configuration class的实例,这个可以用来对NHibernate进行配置。第二行告诉NHibernate配置自己。由于这里我们没有提供额外的配置信息,Nhibernate会根据约定自动查找配置信息,NHibernate 会自动在输出目录中查找叫做Hibernate.cfg.xml的文件。这正是我们对这个文件配置所需要的。

第三行代码告诉NHibernate 它可以在包含类Product的Assembly中找到Mapping 信息,目前他会找到作为嵌入资源的Product.hbm.xml。

第四行代码使用NHibernate 的SchemeExport Helper Class自动生成database schema. SchemaExport将会在数据库中创建Product table。(注: 作者的代码有问题,自己根据VS提示修改吧。)

Note: with this test method we do NOT want to find out whether NHibernate does its job correctly (you can be sure it does) but rater whether we have setup our system correctly.

如果你已经安装了TestDriven.net, 你可以在Test Method中右键点击 Run Test来启动测试。
(注: TestDriven不支持VS Express版本)

clip_image010

如果没有出现什么问题的话可以在输出窗口中看到如下信息.(注: 如果开了show_SQL, 还可以看到生成的SQL)。

clip_image012

(注: 略掉作者使用ReSharper 的部分)

In case of Problems

请注意检查在NHibernate configuration file (hibernate.cfg.xml) or in the mapping file (Product.hbm.xml). 中没有出现拼写错误。同时确保mapping file (Product.hbm.xml) 设置为 "Embedded Resource".

Our first CRUD operations

准备工作已经完成。 我们已经成功个的实现了我们业务实体类(Domain), 定义了Mapping Files, 并且配置好了NHibernate。而且我们使用NHibernate为我们的业务(Domain)自动生成了数据库Schema。

本着DDD的思想(in the spirit of DDD)(Domain Driven Design by Eric Evans), 我们定义执行所有CRUD(create, read , update, delete)操作的的Repository。 Repository的接口是领域(domain)的一部分,但是对于这个Repository接口的实现却不是领域(domain)的内容。实现是和基础架构相关的。我们希望保持我们的领域(domain)是持久无关的(persistence ignorant )

在FirstSolution Project 中添加一个名称为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); 



 

在FirstSolutionTest中添加一个名称为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. 这是一个expensive process, 不应该多次执行。这也是为什么把创建session factory的操作放到整个测试过程中只执行一次的方法中。(注: 该方法有TestFixtureSetUp特性)。

为了避免测试方法相互影响,我们在执行每个测试方法之前都重新创建数据库Schema。代码如下:

[SetUp]
public void SetupContext()
{
new SchemaExport(_configuration).Execute(falsetruefalsefalse);
}

 

(注: 该方法有Setup特性)

现在我们开始实现往数据库添加一个Product 实例的测试方法。首先在FirstSolution Project中添加一个叫做Repositories的文件夹,添加叫做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);
}

 

由于没有实现Repository 类中的Add 方法,这个测试将会运行失败。让我们实现Add方法。等待,让我们定义一个可以随时给我们提供session object 的helper class。

代码
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 (注:昂贵的东西一只足以)。

现在我们定义ProductRepository 中的Add方法如下:

代码
public void Add(Product product)
{
using (ISession session = NHibernateHelper.OpenSession())
using (ITransaction transaction = session.BeginTransaction())
{
session.Save(product);
transaction.Commit();

 


}

第二次运行测试方法将会返回一个错误信息如下所示:

clip_image014

这是因为NHibernate默认配置为lazy load的缘故。这是推荐的设置,为了最大程度的灵活性,建议不要修改这个配置。

根据提示解决这个问题,将Product Entity的所有属性设置为virtual 即可。如下。

代码
public class Product
{
public virtual Guid Id { getset; }
public virtual string Name { getset; }
public virtual string Category { getset; }
public virtual bool Discontinued { getset; }

 

再次运行测试。这次应该能够顺利通过,并能够得到如下输出。

clip_image016

注意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的其他方法。为了测试我们将会创建一个包含多个Product的repository(也就是数据库表). 很简单。 只需要在Test class中添加方法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中调用(创建完Schema的时候)即可。现在每次当创建完数据库的时候都会使用这个方法进行数据库的数据初始化。

让我们使用如下代码测试Repository的Update方法

代码
[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方法还没有实现呢。注意:在TDD中当你第一次运行测试一只会失败,这是期望的行为。(注:太死板了吧)

在Repository中实现Update方法和Add方法差不多。唯一的区别是我们会调用NHibernate的Session Object的Update方法而不是Save方法。

代码
public void Update(Product product)
{
using (ISession session = NHibernateHelper.OpenSession())
using (ITransaction transaction = session.BeginTransaction())
{
session.Update(product);
transaction.Commit();
}

 

运行测试。结果如下:

clip_image018

测试Delete 方法也很简单。我们只需要将已经删除掉的记录从数据库中重新获取,看看是不是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中的Remove方法实现如下: 

public void Remove(Product product)
{
using (ISession session = NHibernateHelper.OpenSession())
using (ITransaction transaction = session.BeginTransaction())
{
session.Delete(product);
transaction.Commit();
}

 

Querying the database

我们还需要实现三个对数据库进行查询的方法。先从比较简单的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);


Repository中的相应方法实现如下: 

public Product GetById(Guid productId)
{
using (ISession session = NHibernateHelper.OpenSession())
return session.Get<Product>(productId);

 

很简单吧。接下来的两个方法我们将使用Session的一个新的Object。 先从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的面向对象的查询语言。

介绍fluent interfaces 。略。

第二个版本使用Criteria query 去查询特定的Product. 需要引用NHibernate.Criterion。

代码
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的一个List。测试代码如下:

代码
[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方法。我是用了SQL CE, 当然其他支持的数据库也可以使用。整个Project处理数据库和NHibernate外没有依赖任何其他的外部框架或者工具。

注:

一直想学习NHibernate,在网上看到了几篇简单的入门教程,跟着这些教程做的时候,不知道是他们使用较早版本的Nhibernate还是其他的原因。感觉一个入门下来就想让人往回跑了。

一个典型的NHibernate的过程大体如下:

Ø 配置hibernate.cfg.xml连接到数据库中

Ø 定义相应的XX.hbm.xml映射将领域类映射到数据库中

Ø 获取session 执行CRUD操作。

其实和一个常规的DataSet操作没有太多的区别么,只不过是多了ORMaping的步骤而已。

整个过程下来,感觉NHibernate没有网上流传的那种配置的地域般的可怕(或许在公司的XXX下使用XML编程了半年的缘故??),还是比较简单的,另外在其他的关于NHibernate的教程中,感觉作者对SQL都十分的厌烦,这里没有体现。我觉得其实NHibernate最大的价值不是说不用写什么SQL了,而是Test 。

也许是篇Wiki, 感觉有很多历史。多人的思路。

其他:

1. 整个文章开始之前来个概括, 给人很直接的感觉。

2. 作者有着良好的个人习惯,比如将代码放到某处。

3. 整个过程概述: 先是做啥,然后做啥。。。。

问题:

1. 如何使FirstSolution 中的Configure File和Test同步的问题

2. FirstSolution和Test中是否需要使用相同的方法创建Session

3. 比如对Delete方法的测试。是否可以使用ProductRepository先Remove然后在Get来进行测试呢?使用一个没有经过测试的方法去支持另外一个方法?