NHibernate 简单化






4.92/5 (200投票s)
一个简单、直接的教程,旨在尽快让你掌握NHibernate的基础知识。

引言
这篇文章源于我尝试入门 NHibernate 时遇到的沮丧。我发现所有入门资料要么非常含糊,要么就详细到让我还没入门就感到不知所措。我当时想找的是一个简单、直接的教程,能让我尽快掌握 NHibernate 的基础知识。我没找到。希望这篇文章能为其他人满足这些需求。
这篇文章可能会比较长,但我还是鼓励你把它读完。 NHibernate 是一个复杂的软件,学习曲线很陡峭。这篇文章将把学习曲线从几天或几周缩短到几个小时。
问题
NHibernate 解决了这样一个众所周知的问题:对象持久化代码的开发非常令人头疼。各种文章估计,在 N 层应用程序中,从四分之一到三分之一的代码都用于“持久化层”—将业务对象数据从数据库读取出来,再写回去。这些代码是重复的、耗时的,并且写起来很乏味。
解决这个问题有各种方案。代码生成器可以在几秒钟内生成数据访问代码。但如果业务模型发生变化,代码就需要重新生成。“对象关系管理器”(ORM)如 NHibernate 采取了不同的方法。它们透明地管理数据访问,暴露一个相对简单的 API,可以用一两行代码加载或保存整个对象图。
介绍 NHibernate
Hibernate 是一个框架形式的持久化引擎。它从数据库加载业务对象,并将这些对象的更改保存回数据库。如前所述,它可以用短短一两行代码加载或保存整个对象图。
NHibernate 使用映射文件来指导它从数据库到业务对象再到数据库的转换。作为替代,你可以在类和属性上使用特性(attributes),而不是映射文件。为了尽可能保持简单,本文将使用映射文件,而不是特性。此外,映射文件使得业务逻辑和持久化代码之间的分离更清晰。
因此,只需要在应用程序中添加几行代码,并为每个持久化类创建一个简单的映射文件,NHibernate 就会处理所有数据库操作。使用 NHibernate 可以节省大量的开发时间,这真是太棒了。
请注意,NHibernate 并非 .NET universe 中唯一的 ORM 框架。市面上有几十种商业和开源产品提供类似的服务。NHibernate 是其中最受欢迎的之一,可能因为它源自 Java universe 中流行的 ORM 框架 Hibernate。此外,微软已经承诺为 ADO.NET 提供一个“Entity Framework”,以提供 ORM 服务。然而,该产品已经延迟,可能需要一段时间才能发布。
安装 NHibernate
使用 NHibernate 的第一步是下载 NHibernate 和 Log4Net。Log4Net 是一个开源的日志记录应用程序,NHibernate 可以使用它来记录错误和警告。NHibernate 包含了最新的 Log4Net 二进制文件,或者你可以下载完整的 Log4Net 安装包。下载地址如下:
严格来说,Log4Net 不是使用 NHibernate 所必需的,但它的自动日志记录在调试时会非常有用。
入门
在本文中,我将使用一个非常简单的演示应用程序(演示应用),它除了演示 NHibernate 的数据访问之外,不做任何实际工作。它是一个控制台应用程序,通过消除 UI 代码来简化操作。该应用程序创建一些业务对象,使用 NHibernate 持久化它们,然后将它们从数据库读回。
要在你的机器上运行演示应用,你需要做几件事:
- 替换对 NHibernate 和 Log4Net 的引用
- 附加 NhibernateSimpleDemo 数据库
- 修改连接字符串
演示应用包含对 NHibernate 和 Log4Net 的引用。只要 NHibernate 和 Log4Net 安装在默认位置,这些引用在你的 PC 上应该都是有效的。如果引用无效,你需要将它们替换为你 PC 上安装的 NHibernate (NHibernate.dll) 和 Log4Net (log4net.dll) 的引用。这些 DLL 文件可以在你开发 PC 的 NHibernate 安装文件夹中找到。
演示应用配置为使用 SQL Server Express 2005。数据库文件(NhibernateSimpleDemo.mdf 和 NhibernateSimpleDemo.ldf)随演示应用一起打包。你需要将数据库附加到你机器上的 SQL Server。
最后,App.config 文件中的连接字符串假设你正在运行 SQL Server Express 2005 的命名实例,并且该实例名为 'SQLEXPRESS'。如果你的 PC 运行的是 SQL Server 2005 的不同配置,你需要修改 App.config 文件中的连接字符串。请注意,该数据库不适用于旧版本的 SQL Server。
业务模型
有两种方法可以使用 NHibernate 开发应用程序。第一种是“以数据为中心”的方法,它从数据模型开始,然后从数据库创建业务对象。第二种是“以对象为中心”的方法,它从业务模型开始,然后创建一个数据库来持久化模型。演示应用采用了以对象为中心的方法。
以下是演示应用的业务模型:

该模型代表了一个订单系统的骨架。模型并不完整—它只有足够的类来演示 NHibernate 的对象持久化。每个类中的细节也很少。并且很明显,模型的设计并不代表最佳实践。但它足以展示 NHibernate 的工作方式。
本文将使用该模型来演示 NHibernate 对象持久化的几个方面:
- 持久化简单属性;
- 持久化“组件”(没有相应数据库表的类);
- 持久化一对多关联;以及
- 持久化多对一关联;以及
- 持久化多对多关联
本文不涉及更高级的主题,例如继承。网络上有大量关于 NHibernate 的技术信息。本文旨在帮助你快速上手。
模型由五个类组成,其中四个是持久化的。非持久化的 OrderSystem 类充当对象模型的根。我们在初始化应用程序时实例化一个 OrderSystem 对象。然后我们将其他对象加载到 OrderSystem 中。
OrderSystem.Customers 属性保存着一个卖家的 Customer 列表。Customer 可以通过其 CustomerID 访问。每个 Customer 对象保存着一个客户的 ID、姓名和地址,以及该客户下的订单列表。地址被封装在一个单独的 Address 类中。
Order 类包含一个订单的订单 ID、日期、下订单的客户的引用,以及订单中产品的一个集合。Product 类只包含一个产品的 ID 和名称—记住,我们只是想展示 NHibernate 的工作方式。Product 对象在应用程序初始化时创建,并加载到 OrderSystem.Catalog 属性中。当创建一个 Order 对象时,Product 对象引用会从 OrderSystem.Catalog 属性复制并添加到 Order.OrderItems 属性中。
NHibernate 最强大的特性之一是它不需要业务类实现特殊的接口。事实上,业务对象通常不知道用于加载和保存它们的对象持久化机制。NHibernate 使用的映射数据包含在单独的 XML 文件中。
这种方法松耦合了业务类和数据访问类,从而使得业务层更灵活、更易于维护。NHibernate 唯一的要求是集合必须用接口而不是具体类型进行类型化。这是一种所有 OO 编程都推荐的做法,它不会以任何方式将业务类绑定到 NHibernate。
数据库
这是演示应用用来持久化模型的数据库:

请注意,数据库和对象模型并不完全匹配。对象模型有一个 Address 类,在数据库中没有对应的表,而数据库有一个 OrderItems 表,没有对应的类。这种不匹配是故意的。NHibernate 的一个方面是,我们想展示类和数据库表之间不必存在一对一的对应关系。
以下是不匹配的原因:
- Address 类不代表业务模型中的实体。相反,它代表实体中持有的一个值,在本例中是 Customer.Address 属性。我们将地址封装在一个单独的类中,以便演示 NHibernate 所称的“组件映射”。
- OrderItems 表是 Order 和 Product 之间多对多关系的链接表。因此,它不代表业务模型中的实体。
Customers 表包含通常的客户信息的骨架,包括客户的地址。最佳实践会建议将地址放在一个单独的表中,这与我们在这里的做法相反。我们将地址信息包含在 Customers 表中,以便演示如何持久化 NHibernate 所称的“组件”—没有自己表的类。我们将在下面更详细地讨论组件。
Orders 表包含最基本的信息;只有 ID(订单号)、日期和下订单的客户的 CustomerID。Order 和 Customer 之间的数据关系是通过 Orders.CustomerID 列到 Customers.ID 列的外键来维护的。
Order 条目需要多对多关系(每个 Order 可以包含多个条目,每个 Product 可以出现在多个 Order 中),所以我们使用 OrderItems 表作为中间表。它只是将订单号与产品 ID 关联起来。
同样,数据库并非旨在作为最佳实践,甚至不是真实世界的示例设计。它只包含足够的信息来展示 NHibernate 的工作方式。
映射业务模型
许多 NHibernate 的介绍从配置代码开始,但我们将从不同的地方开始:映射类。映射是 NHibernate 所做工作的核心,它也是初学者遇到的最大障碍。在讨论了映射之后,我们将转向配置和使用 NHibernate 所需的代码。
映射仅仅是指定数据库中的哪些表对应于业务模型中的哪些类。请注意,我们将把一个特定类映射到的表称为该类的“映射表”。
我们在上面提到,NHibernate 不需要要映射的类实现任何特殊的接口或其他代码。但是,它确实要求类属性声明为 virtual,以便它能够按需创建代理。NHibernate 文档讨论了这一要求。目前,只需注意演示应用中所有业务模型类中的所有属性都声明为 virtual。
映射可以通过单独的 XML 文件或类、属性和成员变量上的特性来完成。如果使用文件进行映射,它们可以以几种方式合并到项目中。为了保持简单,本文将展示一种映射方式:我们将映射到编译为程序集资源的 XML 文件。
你可以在一个映射文件中映射任意数量的类,但通常的做法是为每个类创建一个单独的映射文件。这样做可以使映射文件保持简短易读。
为了开始检查映射,让我们看看映射文件 Customer.hbm.xml。hbm.xml 扩展名是 NHibernate 映射文件的标准扩展名。我们将文件放在 Model 文件夹中,但也可以将其放在项目中的任何位置。重要的是将文件的 BuildAction 属性设置为 Embedded Resource。此设置将导致映射文件被编译到程序集中,这样它就不会丢失或与应用程序分离。
任何映射文件的起始标签都是标准的:
<?xml version="1.0"?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="NHibernateSimpleDemo"
assembly="NHibernateSimpleDemo">
第一个标签是 XML 声明,第二个标签定义了 XML 命名空间。你也可以在这里包含 XSD 信息。第二个标签还包含定义命名空间和程序集名称的属性,这些名称将用于映射引用。这使我们不必在映射标签中包含完全限定的类名。
<class> 标签
下一个标签标识了我们在此文件中映射的类:
<!-- Mappings for class 'Customer' -->
<class name="Customer" table="Customers" lazy="false">
<class> 标签的属性指定了正在映射的类及其在数据库中的映射表。
- name 属性指定了正在映射的类。
- table 属性指定了该类的映射表。
- lazy 属性告诉 NHibernate 不要对此类使用“延迟加载”。
“延迟加载”是指 NHibernate 在应用程序需要访问数据之前,不会从数据库加载对象。这种方法有助于减少业务模型的内存占用,并可以提高性能。为了保持简单,我们在此应用程序中不使用延迟加载。但是,一旦你开始使用 NHibernate,就应该尽快学习其细节。
请注意,class 标签有许多可选属性,已在 NHibernate 帮助文档中进行了记录。为了保持简单,我们在此不做赘述。
<id> 标签
一旦我们识别了正在识别的类及其映射表,我们就需要指定类的标识属性及其在映射表中对应的标识列。请注意,在设置数据库时,我们将 CustomerID 字段指定为数据库的主键。在列的 IdentitySpecification 属性中,我们指定该列是标识列,它应该以 1 开始并以相同的值递增。

所以,我们需要做的是:
- 在 Customer 类中指定标识属性;
- 在 Customers 表中指定记录标识列;以及
- 告诉 NHibernate 让 SQL Server 设置 Customers 表中 CustomerID 列的值。
这是我们这样做的:
<!-- Identity mapping -->
<id name="ID">
<column name=" CustomerID " />
<generator class="native" />
</id>
标识规范是通过属性和嵌套标签的组合来设置的。
- <id> 标签的 name 属性指定了 Customer 类中的标识属性。在本例中,它是 ID 属性。
- <column> 标签的 name 属性指定了 Customers 表中的记录标识列。在本例中,它是 CustomerID 列。
- <generator> 标签的 class 属性指定了记录标识值将由 SQL Server 本地生成。
简单属性
一旦我们映射了类的标识属性,我们就可以开始映射其他属性了。Customer 类有一个简单属性 Name。我们希望将其映射到 Customers 表的 Name 列。由于属性和列名相同,我们的映射非常简单:
<!-- Simple mappings -->
<property name="Name" />
请注意,我们可以将 Name 属性映射到具有不同名称的列(例如,CustomerName)。在这种情况下,与 <id> 标签一样,我们只需在 <property> 标签中包含一个属性,指定目标列的名称。
property 标签包含许多可选属性,可用于指定属性类型、属性长度、是否允许 null 等。但是,NHibernate 可以使用 .NET 反射推断出这些信息,因此我们在演示应用中省略了这些标签。
“组件”映射
NHibernate 使用“组件”一词来指代没有相应数据库表的类。这遵循了“实体类”和“值类”之间经常进行的区分。
- 实体类是代表业务模型中实体的类。在我们的模型中,实体类是 Customer、Order 和 OrderItem。这些类代表业务对象。
- Address 类不代表业务对象—它提供了一种封装 Customer 对象值的方式。在 NHibernate 的术语中,它是 Customer 对象的“组件”。
请注意,NHibernate 对“组件”的使用与 .NET 中该术语的使用完全无关。组件只是一个类,它充当实体类的类值,并且没有自己的表。
组件类(值类)不映射在自己的文件中。相反,它映射在其父类的文件中,在本例中是 Customer 类。
<!-- Component mapping: Address-->
<component name="Address">
<property name="StreetAddress" />
<property name="City" />
<property name="State" />
<property name="Zip" />
</component>
<component> 标签是一个复合标签。它以 <property> 标签开始,其中有一个属性指定了要映射的属性的名称。NHibernate 将使用 .NET 反射来确定属性类型(即组件类)。你可以在可选的“class”属性中指定类的名称。
<component> 标签是一个复合标签—它包含 <property> 标签来映射组件类的每个属性。因此,Customer.hmb.xml 文件映射了两个类—Customer 实体类和 Address 值类。
关联(Associations)概述
面向对象的(OO)设计建立在业务模型中的类以各种方式相互关联的概念之上。
- 一对一(One-to-one):一个对象只关联另一个对象。例如,一个 Husband 对象关联一个 Wife 对象。
- 一对多(One-to-many):这种类型的关联通常被称为“包含”。例如,一个 Customer 对象可能包含对所有代表该 Customer 所下订单的 Order 对象的引用的集合。
- 多对一(Many-to-one):多个对象可以引用一个单一对象。例如,许多代表特定 Customer 所下订单的 Order 对象可以持有对单个 Customer 对象的引用。
- 多对多(Many-to-many):许多对象可以引用许多其他对象。例如,一个 Order 对象可以包含对代表订单中产品的 Product 对象的引用的集合,并且一个 Product 对象可以包含在许多不同的 Order 对象中。
关联可以是单向的或双向的。在本文中,我们将双向关联视为两个单向关联,它们方向相反。因此,我们将讨论从“拥有类”(拥有关联的类)到“目标类”的关联。现在,让我们来看看 Customer 类中的关联,以及它们如何在 Customer.hbm.xml 文件中映射。
集合映射:一对多
检查 Customer 类,你会看到一个 Orders 属性,其中包含 Customer 所下订单的列表。关于 Orders 属性需要注意的第一件事是,它不是 .NET List<T> 集合的类型。相反,它被类型化为 IList<T> 接口。
// Property variable
private IList<Order> p_Orders = new List<Order>();
// Orders property
public IList<Order> Orders
{
get { return p_Orders; }
set { p_Orders = value; }
}
该属性“声明”为 IList<Orders> 类型,而属性变量“实例化”为 List<Order>。
这是因为 NHibernate 要求集合使用接口而不是实现进行类型化。如前所述,使用接口而不是具体类进行类型化被认为是良好的编程实践,它不会以任何方式将业务模型绑定到 NHibernate。使用接口进行类型化可以为 NHibernate 加载集合提供灵活性,并提高其效率。
NHibernate 提供了几种不同的标签可用于映射集合。由于这个集合是一个 IList<T>,我们将使用一个 <bag> 标签来映射关联。
<!-- One-to-many mapping: Orders -->
<bag name="Orders" cascade="all-delete-orphan" lazy="false">
<key column="CustomerID" />
<one-to-many class="Order" />
</bag>
该标签包含一个 name 属性,指定要映射的属性。它还包含一个 cascade 属性,指定要应用于此关联的级联样式。“级联”仅仅意味着 NHibernate 将加载、保存和删除所有子对象。all-delete-orphan 值表示 NHibernate 应该级联所有保存和删除,并且它应该删除因删除而剩余的任何孤立项。
请注意,在任何关联映射中都必须指定级联样式,否则 NHibernate 将不会级联保存和删除。作为替代,<class> 标签可以使用 default-cascade 属性为整个类指定默认级联样式。但是,此属性仅提供保存和更新的级联,而不提供删除的级联。因此,最好在每个关联映射中使用 cascade 属性。
另请注意,我们为该关联关闭了延迟加载。如前所述,延迟加载在处理大型、多层集合时非常有用。为保持简单,它在演示应用中已关闭。但我们绝对建议在生产应用中使用它。
<bag> 标签包含另外两个标签:
- <key> 标签的 column 属性指定了目标类中用于作为当前类映射表和目标类映射表之间外键的列。
- <one-to-many> 标签指定了正在映射的类与 class 属性中命名的类之间存在一对多关系。在本例中,就是 Order 类—一个 Customer 可以包含多个 Orders。请注意,class 属性是必需的。
请注意,似乎有什么东西丢失了:我们指定了目标类,但没有指定它的映射表!<key> 标签指定了一个列,但没有指定一个表。那么 NHibernate 如何知道要使用哪个表呢?答案是,由于我们指定了目标类,NHibernate 可以在目标类的映射文件中查找其映射表。因此,我们不需要在此处指定目标类的映射表。
这样,我们就完成了 Customer 类的映射。我们可以关闭 Customer 映射文件,然后继续处理 Order 类。
集合映射:多对一
打开 Order.hbm.xml 文件。到目前为止,其内容应该让你觉得很熟悉。有常规的 <class> 和 <property> 标签,以及一个 <set> 标签用于订单与其条目之间的一对多关联。但 Order 类中还有另一个关联,即与 Customer 类的关联,这样每个订单都知道下订单的 Customer。
乍一看,这可能看起来像是一对一关联—一个订单对一个客户。但那是不正确的,因为许多 Order 对象可以持有对单个 Customer 对象的引用。即使单个 Order 对象与单个 Customer 对象关联,在类级别,这种关联也是多对一的。
<many-to-one> 标签映射了这种关联。它是一个简单的标签,因为它不包含其他标签。它之所以如此简单,是因为尽管关联是多对一的,但“多”个对象是逐个关联的。它几乎和 <property> 映射一样简单。
<!-- Many-to-one mapping: Customer -->
<many-to-one name="Customer"
class="Customer"
column="CustomerID"
cascade="all" />
属性本身很简单:
- name 属性指定了拥有类中要映射的属性的名称。在本例中,它是 Order 类中的 Customer 属性。
- class 属性指定了目标类。在本例中,目标是 Customer 类。
- column 属性指定了拥有类映射表中用作指向目标类的外键的列。在本例中,它是 Orders 表的 CustomerID 列,因为我们正在映射 Order 类。
- cascade 属性指定了此关联的级联样式。
class 属性是可选的;NHibernate 可以通过 .NET 反射确定类。如果映射表中列的名称与要映射的类属性的名称相同,则可以省略 column 属性。但是,由于这种情况很少发生,所以通常会包含该属性。
集合映射:多对多
Order.hbm.xml 文件中最后一个值得关注的映射是 OrderItems 属性的映射。请注意,OrderItems 属性的类型是 IList<Product>。OrderItems 属性包含 Product 对象引用的集合。
起初,这种关联看起来是一对多关联,因为一个 Order 与多个 Product 相关联。但这种关联意味着每个 Product 只能与一个 Order 相关联。显然,任何 Product 都可以出现在多个 Orders 中,所以我们这里的情况是多对多关联。
在数据库中,我们使用 OrderItems 表作为 Orders 表和 Products 表之间的链接表。OrderItems 表包含涉及的订单 ID 和涉及的产品 ID。这种方法是处理多对多关联的常规手段。
请注意,我们正在处理一个单向多对多关联。它从 Order 类指向 Product 类。双向关联更复杂,本文不在此涵盖。
那么,我们如何映射多对多关联呢?与映射一对多关联的方式非常相似。我们使用一个 <bag> 标签,它包含一个 <many-to-many> 标签。
<!-- Many-to-many mapping: OrderItems -->
<bag name="Orders" table="OrderItems" cascade="none" lazy="false">
<key column ="OrderID" />
<many-to-many class="Product" column="ProductID" />
</bag>
<bag> 标签按以下方式指定关联:
- <bag> 标签的 name 属性指定了 Customer 类中要映射的属性。在本例中,它是 OrderItems 属性。
- <bag> 标签的 table 属性指定了链接表。在本例中,它是 OrderItems 表。
- <bag> 标签的 cascade 属性指定了要应用于此关联的级联样式。
- <bag> 标签的 lazy 属性关闭了此关联的延迟加载。
- <key> 标签的 column 属性指定了链接表中用于将链接表与当前类映射表关联的外键列。在本例中,它是 OrderItems 表的 OrderID 列,将其链接回 Orders 表。
- <many-to-many> 标签的 class 属性指定了多对多关联的目标类。在本例中,它是 Product 类。
- <many-to-many> 标签的 column 属性指定了链接表中用于将链接表与目标类映射表关联的外键列。在本例中,它是链接表中的 ProductID 列,它将链接表与数据库的 Products 表关联起来。
与一对多关联一样,我们不需要指定目标类的映射表。由于我们已经指定了目标类,NHibernate 可以在目标类映射文件中查找其映射表。
请注意,<bag> 标签的 cascade 属性设置为 none。这是因为我们不希望在删除订单时从目录中删除产品。以这种方式使用 cascade 属性可以让我们对类持久化中的级联进行细粒度控制。通过将 Customer 映射文件中的一对多关系设置为 all-delete-orphan,我们确保持久化操作(保存、更新和删除)会从 Customers 级联到它们的 Orders。通过将 Orders 映射文件中的多对多关系中的 cascade 属性设置为 none,我们在该点停止了级联。这可以防止在删除订单时从目录中删除产品。
Order 类中唯一的其他映射是 Date 属性的简单属性映射。我们在此不花时间讨论这一点,您可以关闭 Order 映射文件。
我们将不检查 Product.hbm.xml 映射文件,因为我们已经涵盖了其中包含的所有内容。读者可以打开它并识别文件中的所有项目,这是一个很好的练习。
调试映射文档
映射文档的调试大多数都在运行时进行。当应用程序配置 NHibernate 时,它会尝试编译它能找到的映射文档。如果 NHibernate 遇到问题,它会抛出 NHibernate.MappingException 类型的异常。你可以处理这些异常,也可以让它们停止执行。在后一种情况下,异常和堆栈跟踪可以从 Log4Net 日志中读取。最常见的异常看起来是这样的:
Could not compile the mapping document:
NHibernateSimpleDemo.Model.Order.hbm.xml --->
NHibernate.PropertyNotFoundException: Could not find a getter for
property 'OrderItems' in class 'NHibernateSimpleDemo.Order'
调试遵循常规模式—修复错误,重新编译,然后重新执行。如果你的应用程序能够完成 NHibernate 的配置,你就知道它在映射文档方面没有问题。我们下面会讨论配置。
请注意,如果 NHibernate 抱怨你的某个类未被映射,即使你已经为该类创建了映射文件,也请检查映射文件中的 <class> 声明,确保你输入的类名及其映射表是正确的。如果这些都正确,请验证你是否已将文件的 Build Action 属性设置为 Embedded Resource。
集成 NHibernate
没有一种“正确”的方法可以将 NHibernate 集成到你的应用程序中。作者的个人偏好是遵循通用的三层架构,并将 NHibernate 配置和处理代码放在数据层。演示应用有一个 Persistence 文件夹,其中包含一个 PersistenceManager 类。
PersistenceManager 包含用于持久化业务模型中每个实体的通用方法。虽然单个类对于演示应用来说已足够,但这对于生产应用程序来说可能不是最佳实践。在实际应用中,你可能希望将这些方法拆分到多个持久化类中。
PersistenceManager 类配置 NHibernate 并持有一个 SessionFactory 对象的全局引用。SessionFactory 创建 Session 对象。Session 是 NHibernate 的基本工作单元。Session 代表你的应用程序和 NHibernate 之间的一次对话。
你可以将它们视为比事务高一个层级。一个 Session 通常包含一个事务,但也可以包含多个。基本上,你打开一个 NHibernate Session,执行一个或多个事务,关闭 Session,然后将其释放。
Session 由 SessionFactory 对象创建。SessionFactory 资源密集且初始化成本相对较高。另一方面,Session 使用有限的资源,初始化成本很小。因此,一般的方法是在应用程序初始化时创建一个全局的 SessionFactory,然后使用该 SessionFactory 按需创建 Session 对象。
演示项目在应用程序初始化时初始化一个 PersistenceManager 对象。PersistenceManager 配置一个 SessionFactory,该 SessionFactory 成为 PersistenceManager 的成员变量。应用程序可以调用 PersistenceManager.SessionFactory 来按需创建 Session。
配置 NHibernate – 配置数据
配置 NHibernate 有两个要素:
- 配置数据
- 配置代码
配置数据可以放在应用程序根目录下的单独配置文件中,或者放在应用程序的 App.config 文件中。为了保持简单,演示应用将数据放在 App.config 文件中。这意味着少了一个可能丢失或与应用程序分离的文件。
在本文开头,我们建议将 Log4Net 与 NHibernate 一起下载。Log4Net 有自己的配置数据,我们也将将其放在 App.config 文件夹中。Log4Net 配置数据非常简单,因此我们在此不做讨论。请注意,NHibernate 和 Log4Net 在 App.config 的 <configSections> 部分都需要标签。
NHibernate 的配置数据相当清晰。这些数据由 NHibernate 使用来创建 SessionFactory。以下是根据数据设置的 SessionFactory 属性:
- Connection provider:NHibernate 应该使用的 IConnectionProvider。演示应用使用默认提供程序。
- Dialect:要使用的数据库方言。演示应用指定 SQL Server 2005 方言。
- Connection driver:要使用的 ADO.NET 驱动程序。演示应用指定 SQL Server 客户端驱动程序。
- Connection string:用于连接数据库的连接字符串。连接字符串是标准的 ADO.NET 连接字符串;你无需修改有效的连接字符串即可将其与 NHibernate 一起使用。
请注意,演示应用中的连接字符串对于作者的开发环境是有效的。你需要更改连接字符串以匹配你的数据库设置。
配置 NHibernate – 配置 Log4Net
你可能还记得,我们在设置演示应用项目时导入了对 Log4Net 的引用。配置 NHibernate 的第一步是配置 Log4Net。请注意,如果你使用 Log4Net(其使用是可选的),Log4Net 必须在 NHibernate 之前配置,因为 NHibernate 在初始化时会期望看到 Log4Net。
Log4Net 配置很简单。首先,确保 App.config 文件中启用了 Log4Net:
<!-- Note: Logger level can be ALL/DEBUG/INFO/WARN/ERROR/FATAL/OFF -->
<!-- Specify the logging level for NHibernates -->
<logger name="NHibernate">
<level value="DEBUG" />
</logger>
接下来,在你的代码中添加以下属性。演示应用将其添加在 PersistenceManager 类的命名空间声明上方:
[assembly: log4net.Config.XmlConfigurator(Watch=true)]
namespace NHibernateSimpleDemo
{
public class PersistenceManager : IDisposable
{
…
}
配置 Log4Net 的最后一步是调用其 Configure() 方法。演示应用将该调用封装在一个 ConfigureLog4Net() 方法中,该方法从 PersistenceManager 构造函数中调用。
private void ConfigureLog4Net()
{
log4net.Config.XmlConfigurator.Configure();
}
配置 NHibernate – 配置代码
演示应用在初始化 PersistenceManager 时配置 NHibernate。PersistenceManager 在一个 private 方法中进行配置,该方法从 PersistenceManager 构造函数中调用。配置代码很简单:
private void ConfigureNHibernate()
{
// Initialize
Configuration cfg = new Configuration();
cfg.Configure();
// Add class mappings to configuration object
Assembly thisAssembly = typeof(Customer).Assembly;
cfg.AddAssembly(thisAssembly);
// Create session factory from configuration object
m_SessionFactory = cfg.BuildSessionFactory();
}
首先,我们创建一个 NHibernate Configuration 对象。然后我们将映射文件中的类映射传递给它。请注意,AddAssembly() 方法要求所有映射文件都嵌入到项目程序集中。为此,请将每个映射文件的 BuildAction 属性设置为 Embedded Resource。
一旦我们将映射文件传递给 Configuration 对象,我们就只需要告诉它为我们创建一个 SessionFactory。NHibernate 将查找并读取它所需的配置数据;我们不需要指定数据是在 App.config 中还是在一个单独的 XML 文件中找到,我们也不需要显式加载数据。
BuildSessionFactory() 返回一个 SessionFactory 对象,演示应用将其传递给 Persistence Manager 的 SessionFactory 成员变量。之后,应用程序可以随时通过调用全局 SessionFactory 对象上的该属性来调用 SessionFactory,以按需创建 Session。
请注意,如果我们使用的是多个类;例如,每个实体都有一个持久化类,我们将不得不将 SessionFactory 对象的引用传递给这些类中的每一个,以便它们可以按需创建 Session。由于演示应用的所有持久化方法都封装在一个类(PersistenceManager)中,它们可以简单地将 SessionFactory 调用为成员变量。
使用 NHibernate 持久化类
NHibernate 最强大的特性之一是它能够自动级联加载、保存和删除。例如,当我们保存一个 Customer 对象时,NHibernate 将自动保存自上次保存以来已更改的客户的 Order 对象。换句话说,当我们保存一个对象时,我们保存了它的整个对象图。该功能极大地简化了我们的持久化代码。
事实上,通过使用 .NET 2.0 泛型,演示应用能够完全摒弃常规的持久化类。它的持久化代码减少到几个通用方法,我们可以直接将它们合并到 PersistenceManager 中!显然,演示应用不是一个完整的、实际的应用程序,良好的设计可能需要对持久化层进行更细粒度的方法。但演示应用有效地说明了 NHibernate 如何极大地简化持久化代码。
PersistenceManager 类包含实现所有基本 CRUD(创建、检索、更新和删除)操作的方法:
- Save():此方法将新对象或现有对象保存到数据库。
- RetrieveAll():此方法从数据库检索给定类型的所有对象。
- RetrieveEquals():此方法检索给定类型中某个属性等于指定值的所有对象。该方法使用 NHibernate 的 QueryByCriteria 功能,该功能可以实现在各种检索方法中,以检索“类似”字符串或指定范围内的值。
- Delete():此方法有两个重载。第一个删除传递给它的单个对象。第二个删除传递给它的对象列表。
大多数方法遵循通用模式:
- 它们将一个新的 NHibernate session 对象封装在 using 语句中。using 语句确保 Session 在方法完成后被正确关闭和释放,即使抛出了异常。RetrieveAll<T>() 方法是一个例外,我们将在下面讨论。
- Save() 和 Delete() 方法进一步将 NHibernate 事务封装在 using 语句中,原因相同。
- 该方法调用 NHibernate session 对象的通用方法,以执行需要完成的工作。
请注意,PersistenceManager 包含一个 Close() 方法,并且它实现了 IDisposable 接口。这意味着应用程序在完成对 PersistenceManager 的使用后,必须关闭并释放 PersistenceManager,演示应用就是这样做的。
PersistenceManager 中的 CRUD 方法并非旨在代表对象持久化的完整实现。CRUD 方法旨在展示 NHibernate 中持久化工作的基础。NHibernate 文档包含有关 session 对象提供的 CRUD 方法的完整信息,以及如何在应用程序中实现这些方法。
收益
此时,你可能会问自己,就像我一样,NHibernate 是否有一个如此复杂的设置,以至于手动编写 CRUD 代码可能同样容易。我个人发现 NHibernate 的学习曲线相当陡峭且进展缓慢。
好了,这就是一切回报的地方。如果你仔细想想我们到目前为止所做的事情,实际上不过是创建了一些简短的映射文件,并在我们的应用程序中添加了少量代码。一旦你学会了系统,它实际上并不算太糟糕。你从中获得的回报是巨大的。
花点时间想一想,需要多少手动编写的代码才能加载 Customers 集合,以及每个 Customer 的 Order 对象,以及每个 Order 的 Product 对象。以下是使用 NHibernate 完成此操作所需的代码:
IList<T> itemList = session.CreateCriteria(typeof(T)).List<T>();
没错—一行代码,加载整个对象图。以下是使用 NHibernate 将相同的对象图写回数据库所需的代码:
foreach (Customer Customer in OrderSystem.Customers)
{
using (ISession session = m_SessionFactory.OpenSession())
{
using (session.BeginTransaction())
{
session.SaveOrUpdate(item);
session.Transaction.Commit();
}
}
}
实际的保存操作需要两行代码,假设你想要事务支持!因此,一旦设置完成,NHibernate 就能非常快速地处理 CRUD 操作。现在让我们转向演示应用如何实现这些操作。
运行演示应用
演示应用是一个控制台应用程序,所以它实际上没有用户界面。相反,Program 类承担了 UI 的角色。Main() 方法与 Controller 类进行通信,Controller 类根据 Program 类传递给 Controller 的请求来操作模型。此外,Program 类订阅 OrderSystem.Populate 事件,该事件在 OrderSystem 被重建或加载时触发。
这种方法是“模型-视图-控制器”(MVC)架构的实现,你可以在另一篇文章中了解更多关于它的信息。如果你不熟悉 MVC 架构,你可能会发现从 Program.Main() 方法的顶部开始单步调试演示应用有助于理解控制流程。
Program.Main() 方法只是向 Controller 发送请求,这些请求提供了演示应用所执行操作的广泛概述。请注意,应用程序在每个主要步骤之后都会暂停,以便你检查控制台,然后再继续。
应用程序的第一个任务是实例化一个 Controller 对象,这是它直接通信的唯一对象。请注意,在 Controller 初始化时,它会创建另外两个对象:
- 一个 OrderSystem 对象,包含演示应用的业务模型;以及
- 一个 PersistenceManager,包含应用程序的所有持久化层逻辑。
如前所述,PersistenceManager 是一个非常轻量级的类,因为它将大部分工作委托给 NHibernate。
这些对象仅对 Controller 可见;Program 类不知道 PersistenceManager,对 OrderSystem 的了解也非常有限。这使得我们的 UI 与应用程序的其余部分松散耦合。因此,重新设计演示应用为 Windows Forms 应用程序应该非常容易。我们只需要设计一个将相同的请求传递给现有 Controller 的 GUI,并处理 OrderSystem.Populated 事件。
接下来,应用程序清除数据库中的所有数据。为了充分演示 NHibernate 的工作原理,我们将每次运行应用程序时都从一个干净的开始。ClearDatabase() 方法说明了一个重要的观点——我们可以将 NHibernate 调用与 ADO.NET 调用混合使用。在这种情况下,我们需要进行几个简单的 ADO.NET 调用来清除数据库。因此,ClearDatabase() 方法借用了 NHibernate 的连接并创建了一个 ADO.NET 命令对象来完成工作。
保存业务模型
清除数据库后,应用程序会在内存中构建业务模型。OrderSystem.Populate() 方法执行此工作,并且 OrderSystem 在完成后会触发一个 Populated 事件。此事件通知应用程序的其余部分,业务模型已被重建或从数据库加载。Program 类订阅此事件,并在每次重新加载或重建业务模型时使用它来打印客户和订单列表。
模型构建完成后,应用程序会保存它。而这正是 NHibernate 真正闪光的地方。PersistenceManager 中的 Save<T>() 方法展示了使用 NHibernate 持久化代码可以多么简单。我们甚至不需要考虑数据库。Controller 只需告诉 PersistenceManager 保存我们的对象:
private static void SaveBusinessObjects
(OrderSystem OrderSystem, PersistenceManager persistenceManager)
{
// Save Products
foreach (Product product in OrderSystem.Catalog)
{
persistenceManager.Save<Product>(product);
}
// Save Customers (also saves Orders)
foreach (Customer Customer in OrderSystem.Customers)
{
persistenceManager.Save<Customer>(Customer);
}
}
请注意,SaveBusinessObjects() 方法保存 Products 和 Customers,但不保存 Orders。由于我们已为 Customer.Orders 属性启用了级联,因此在保存 Customers 时会自动保存 Customers 的 orders。因此,无需在 OrderSystem.Orders 列表中保存 Order 对象。
另请注意,NHibernate 会负责在数据库中创建 OrderItems 表的记录,即使我们没有任何代码指示它这样做。这是因为 Order 映射文件将 OrderItems 表指定为 Order.OrderItems 属性中包含的多对多关联的链接表。这是 NHibernate 简化持久化代码的另一个例子。
删除业务对象
保存业务模型后,应用程序会将其从 RAM 中清除。我们这样做是为了设置下一个演示,但这同时也说明了一个重要观点:从 RAM 中删除一个业务对象并不会将其数据从数据库中删除。正如我们下面将看到的,我们必须明确指示 NHibernate 从数据库中删除一个对象。如果我们只是从 RAM 中删除业务对象,NHibernate 可以在任何时候重新加载该业务对象。这是我们下一个演示的主题。
使用 NHibernate 加载对象
业务模型从 RAM 中删除后,应用程序会将其从数据库中重新加载。此步骤展示了如何加载持久化对象。应用程序使用 Controller.LoadBusinessObjects() 方法来加载对象。此方法与其他 Program 类中的方法一样简单。它只是调用 PersistenceManager.RetrieveAll<T>() 方法,该方法将其大部分工作委托给 NHibernate。
演示应用中的 RetrieveAll<T>() 方法实际上是该方法的第二个版本。原始版本非常简单。它创建一个 NHibernate session 对象,并将其封装在 using 语句中。然后,它使用 NHibernate 的“Query By Criteria”功能从数据库中获取特定类型的全部对象。
public IList<T> RetrieveAll<T>()
{
using (ISession session = m_SessionFactory.OpenSession())
{
// Retrieve all objects of the type passed in
ICriteria targetObjects = m_Session.CreateCriteria(typeof(T));
IList<T> itemList = targetObjects.List<T>();
// Set return value
return itemList;
}
}
获取对象的类型由 T 类型参数指定。该方法创建了一个指定此类型的 ICriteria 对象,并调用 Criteria 的 List<T>() 方法返回符合条件的对象的列表。
这导致了一个细微的问题,很容易被忽略。演示应用加载 Order 对象两次—一次是显式的,一次是隐式的:
- 当它加载 OrderSystem.Orders 列表时,它显式加载 Order 对象。
- 当它加载 OrderSystem.Customers 列表时,它隐式加载 Order 对象。Customer 映射文件中 Orders 属性的 <bag> 标签中的 cascade 属性会导致在加载 Custoomer 对象时自动加载该客户的所有 orders。
换句话说,级联也适用于加载,而不仅仅是保存。如果我们没有单独的 Orders 集合,我们可以省略显式加载。然而,OrderSystem.Orders 集合方便使用,因为它允许我们查看订单而无需知道下订单的客户。因此,我们将保留显式加载。
但是,两次加载 Order 对象会导致 NHibernate 创建两个不同的 Order 对象“代表同一个订单”的风险,而我们想要的是两个指向“同一个对象”的引用。

这个问题通常被称为“对象身份”。如果我们最终得到两个不同的对象,那么对 Customers 列表中的订单所做的更改将不会反映在 Orders 列表中的同一个订单上!显然,这个问题非常重要。
规则是:NHibernate 将仅在同一 Session 中加载引用时才保证对象身份。如果你再看一遍 RetrieveAll<T>() 方法的旧版本,你会发现它未能通过该测试,因为为每种加载的类型都创建了不同的 Session。简而言之,RetrieveAll<T>() 方法的旧版本会产生重复的 Order 对象,而不是指向同一个 Order 对象的重复引用。
这是我们如何修复该方法。首先,我们将 session 变量提升为成员变量,而不是局部变量。这个更改使得 Session 的生命周期可以超越单个类型的加载。接下来,我们向方法添加了一个新参数,SessionAction。这个参数指定了方法相对于 session 成员变量应该采取什么操作:
- Begin:方法应开始一个新的 Session。
- Continue:方法应继续一个现有的 Session。
- End:方法应继续一个现有的 Session,并在完成后将其关闭。
- BeginAndEnd:方法应开始一个新的 Session,并在完成后将其关闭。
修订后的方法可以在同一个 Session 中加载任意数量的不同类型的对象,这保证了加载多次的对象的身份。
public IList<T> RetrieveAll<T>(SessionAction sessionAction)
{
// Open a new session if specified
if ((sessionAction == SessionAction.Begin) || (sessionAction ==
SessionAction.BeginAndEnd))
{
m_Session = m_SessionFactory.OpenSession();
}
// Retrieve all objects of the type passed in
ICriteria targetObjects = m_Session.CreateCriteria(typeof(T));
IList<T> itemList = targetObjects.List<T>();
// Close the session if specified
if ((sessionAction == SessionAction.End) || (sessionAction ==
SessionAction.BeginAndEnd))
{
m_Session.Close();
m_Session.Dispose();
}
// Set return value
return itemList;
}
方法的工作原理如下:
- 首先,如果需要,会开始一个新的 Session。
- 然后,类型像以前一样加载,使用查询标准。
- 最后,根据需要关闭新的 Session。
为保持简单,代码省略了 try-catch 块,如果你没有将代码封装在 using 语句中,则应该使用该块。相反,如果方法被告知结束一个 Session,该方法只需关闭并释放 Session。
正如我们上面提到的,应用程序只在“最顶层”的持久化对象上调用 RetrieveAll<T>() 方法;也就是说,Products、Customers 和 Orders。我们不必显式检索 OrderItems,也不必将 Orders 加载到 Customers 中,或将 OrderItems 加载到 Orders 中。NHibernate 会自动处理加载对象图中任何子对象的加载。
请注意,NHibernate 提供了多种查询数据库以加载业务对象的特性:
- 查询标准(Query by criteria):通过创建 Criteria 对象并设置其属性来查询数据库。
- 查询示例(Query by example):通过创建要检索类型的示例对象并设置其属性来指定选择条件来查询数据库。
- Hibernate 查询语言(Hibernate Query Language):一种类似 SQL 的查询语言。
- SQL 语句:如果其他数据库查询方法不适合你的需求,你可以向 NHibernate 提交 SQL 语句。
SQL 语句应该只作为最后的手段使用,如果其他任何数据库查询方法都无效。NHibernate 文档详细讨论了所有这些选项。
验证对象身份
演示应用重新加载业务模型后,它会演示对象身份已得到保留。实际上,有两个对象被加载了两次:
- 正如我们上面讨论的,Order 对象被显式加载到 OrderSystem.Orders 属性中,并隐式加载到 OrderSystem.Customers[i].Orders 属性中。
- 此外,Customer 对象被显式加载到 OrderSystemCustomers 属性中,并隐式加载到 OrderSystem.Orders[i].Customer 属性中。
业务模型设置为第一个 Customer(Able, Inc.)下第一个 Order。这个事实允许我们测试:
- Customers 列表中的第一个 Customer,以及 Orders 列表中的第一个 Order 中的 Customer,是同一个对象。
- Orders 列表中的第一个 Order,以及 Customers 列表中第一个 Customer 的第一个 Order,是同一个对象。
我们使用 object.ReferenceEquals() 方法来验证对象身份,该方法测试两个引用是否指向同一个对象。
// Compare Customer #1 to the Order #1 Customer--should be equal
Customer CustomerA = OrderSystem.Customers[0];
Customer CustomerB = OrderSystem.Orders[0].Customer;
bool sameObject = object.ReferenceEquals(CustomerA, CustomerB);
…
// Compare Order #1 to the Customer #1 order--should be equal
Order orderA = OrderSystem.Orders[0];
Order orderB = OrderSystem.Customers[0].Orders[0];
sameObject = object.ReferenceEquals(CustomerA, CustomerB);
演示应用显示其比较结果,然后暂停等待用户输入,然后再继续。
从数据库中移除对象
如上所述,从对象模型中删除一个对象并不会将其从数据库中删除。演示应用通过清除对象模型,然后从数据库重新加载它来演示这一点。
要从数据库中删除一个对象,我们必须明确告知 NHibernate 去执行此操作。演示应用的 PersistenceManager 包含一个 Delete<T>() 方法,该方法执行此任务。
public void Delete<T>(IList<T> itemsToDelete)
{
using (ISession session = m_SessionFactory.OpenSession())
{
foreach (T item in itemsToDelete)
{
using (session.BeginTransaction())
{
session.Delete(item);
session.Transaction.Commit();
}
}
}
}
该方法遵循与 Save<T>() 方法相同的通用模式。两个 using 语句分别用于封装 NHibernate Session 和事务。繁重的工作是通过简单地调用 session 的 Delete() 方法来完成的。调用 session.Delete() 将删除一个对象(假设级联已在映射文件中设置)以及该对象的所有子对象。
演示应用通过从数据库中删除第一个 Customer,Able, Inc. 来展示这一点。然后它像以前一样清除对象模型,并从数据库重新加载对象模型。这次,只有两个 Customers,只有两个 Orders。NHibernate 不仅从数据库中删除了 Able, Inc. Customer 记录,而且还删除了 Able, Inc. 的 Orders,以及那些订单中的 OrderItems。
让焦点回到它应该在的地方
我们已经完成了对NHibernate
基本 CRUD 操作的介绍。我希望您能记住以下一点:NHibernate
将大大简化您应用程序的持久化层。一旦您为类创建了映射文件,您几乎就可以忘记持久化了。而且,您不必花费数天甚至数周的时间编写笨拙的持久化层来实现这些好处。
演示应用程序的持久化层说明了使用NHibernate
时该层可以多么轻量。我们只有几个方法,其中大部分是通用的,减少了对重载或为每个业务模型类创建持久化类的需求。方法本身很简单,设计或编码所需的时间不多。您需要花费的用于持久化对象的精力大大减少了。对于一款免费软件来说,这可不算差!
但还有另一个好处,而且可能最重要。看看 VS 2005 的解决方案资源管理器中的演示项目,您会发现应用程序的大部分工作都在模型中完成。Controller
仅负责管理工作,并将任务委派给业务模型和持久化管理器,而持久化管理器基本上只是NHibernate
的一个薄包装器。因此,您可以自由地将几乎所有注意力都集中在业务模型上。通过将您从编写持久化代码的繁重工作中解脱出来,NHibernate
使您能够将焦点放在您的业务模型上,那里才是它应该在的地方。
下一步该做什么
我们对NHibernate
的介绍真的只是冰山一角。即便如此,您也应该掌握足够的内容来开始工作了。在您自己的几个演示项目中尝试使用它,以了解它的工作原理。届时,您应该可以深入阅读该框架的文档了。
NHibernate
附带两个文档文件。第一个是对框架的总体解释,第二个是NHibernate
API 的参考。此外,Manning Publications 出版了NHibernate in Action (ISBN: 1-932394-92-3),该书对框架进行了广泛而详细的解释。它现在已提供电子书版本,并定于 2007 年 12 月发行纸质版。
当您进一步深入了解NHibernate
时,请务必学习延迟加载及其工作原理。正如我们在上面讨论过的,延迟加载对于高效使用NHibernate
至关重要,值得花时间学习。
结论
希望您喜欢这个NHibernate
的介绍,并希望它能降低掌握该框架的学习曲线。请在 Code Project 上发布您的评论和问题,我会尽力回答。如果您发现任何错误,请发布评论,以便我在修订版文章中进行更正。