使用 NHibernate 进行对象关系映射 (ORM) - 第 1 部分(共 8 部分):一对一实体关联编码






4.96/5 (27投票s)
包含 8 部分的完整系列文章,展示使用 NHibernate 进行一对一、多对一、多对多关联映射,使用 NHibernate 处理集合,使用 NHibernate 处理继承关系,使用 NHibernate 处理延迟初始化/获取。
引言
在使用 C# 进行快速应用程序开发时,对象关系映射的默认选择是语言集成查询(Language Integrated Query),另一个主要选项是 NHibernate。在某些场景下,NHibernate 是一个有用的补充。SPRING.NET 和 CASTLE WINDSOR 这两个最古老的依赖注入(DI)容器都对 NHibernate 提供了广泛的支持。当正确使用 NHibernate 进行持久化时,关注点分离可以做到零泄漏。如果编码正确,所有 POCO(PLAIN OLD CLR OBJECTS,纯粹的 CLR 对象)都能保证不受持久化和事务问题的影响。这为领域类的重用、单元测试和测试驱动开发带来了巨大优势。对于具有战略思维的资源管理者和偏爱 POCO 的架构师来说,NHibernate 提供了他们非常了解且无需我介绍的其他独特优势。话虽如此,除非有战略或系统要求需要使用 NHibernate,否则在 C# 中应选择语言集成查询。但如果确实需要使用 NHibernate,而你又是 ORM 和 NHibernate 的新手,那么这个由 8 部分组成的文章系列可能对你有所帮助。
背景
在开始阅读这个关于 NHibernate 的系列文章之前,请先阅读 NHibernate 页面上的这篇教程
你可以在这里下载 NHibernate
http://nhforge.org/wikis/howtonh/your-first-nhibernate-based-application.aspx
上述教程是官方提供且最好的入门指南,用于在 VISUAL STUDIO 中设置 NHibernate 并快速上手。要阅读我的系列文章,你必须先按照上述链接中的指南,在 VISUAL STUDIO 中设置好用于 NHibernate 的项目。上述链接文章中的 Visual Studio 设置与第 7 篇中提供的示例项目之间有一个变化:我们这里使用的是 SQLSERVER 数据库,而不是 SQL SERVER Compact Edition,并且在添加数据库时选择了“基于服务的数据库”选项。
+ 此处所有示例均使用 MICROSOFT VISUAL STUDIO 2012 PROFESSIONAL 试用版。
可下载的代码示例
第 7 篇文章包含一个可下载的基于控制台的电子商务应用场景项目(使用简单的 POCO,以便于理解关联的概念)。所有文章的示例都基于这个电子商务场景。在第 1 至 5 篇文章中,我们将逐步演化各种对象关联,以构建第 7 篇文章中的完整示例。由于它是基于控制台的,所以只包含 POCO。在第 1 至 5 篇文章中,我将第 7 篇文章示例项目中用于建立关联的 NHibernate 和 C# 代码连同解释和图示嵌入到了文章本身,以展示如何在 NHibernate 中映射各种关联。请通读第 1 至 5 篇文章,因为它们在文章的图示和解释中嵌入了代码示例。
使用代码及 NHibernate 进行一对一映射
希望你已经阅读了“先决条件”部分中 NHibernate 官方博客上发布的文章并进行了尝试,因为那是 NHibernate 的最佳入门介绍。
开发/使用 ORM 软件的根本出发点是确定对象的标识。对象标识对于包括 NHibernate 在内的所有 ORM 系统都至关重要,因为当一个 CLR 加载的对象被修改时,ORM 软件必须明确无误地知道这些更改应该持久化到数据库表的哪一行。这是通过为对象上的每个操作单元(对象创建和保存=insert 语句,对象修改=update 语句,对象删除=delete 语句)自动生成 DML SQL 语句来实现的。通俗地说,这意味着 ORM 软件应该明确知道该对象映射到数据库表中的哪一行(虽然这种说法更适用于使用行网关模式的对象关系映射,但我们在此不作深入讨论)。因此,每个对象都会被分配一个独立的标识,这个标识作为其属性存储,并在 NHibernate 映射文件中使用 `
请参考下面的示例图一,它展示了一个名为 Customer 的 C# 类,该类有一个名为 CustomerId、类型为 long 的属性。这个属性通过 NHibernate 映射文件 Customer.hbm 映射到一个名为 CUSTOMER 的数据库表,其中 C# 类的 CustomerId 属性成为了该表的主键。
绿色箭头显示了 C# 类到 .HBM 映射文件的映射过程,以及 C# 类中的标识符属性是如何在 .HBM 映射文件中使用 `
橙色箭头表示表是如何形成的,其主键的设置与 NHibernate 映射文件中的 `
INDICATED BY THE <ID> TAG OF NHIBERATE MAPPING FILE. HERE IN THIS EXAMPLE IT IS "CUSTOMERID".
N
图一
如前所述,确定对象标识并将其映射为数据库表的主键是任何 ORM 的第一步。那么现在的问题是:“我们是否必须默认给所有类添加 Id 属性?”要知道答案,必须了解 NHibernate 中实体(Entity)和值类型(Valuetypes)的区别。
实体和值类型
实体和值类型之间的区别通过一个例子可以很好地理解。考虑我们在这里所有文章中使用的电子商务网站的例子。自然,每个电子商务网站都会存储购买者的信息,因此会有一个 Customer 类,其中可能包含一个 EMailAddress 字段。因此,Customer 类需要在其行为中包含对有效电子邮件地址输入的约束检查,以及发送自动邮件的附加方法(电子商务网站总是会向客户发送大量此类邮件)。由于 EMailAddress 字段带来的所有这些行为上的增加,Customer 对象的内聚性降低了。因此,EMailAddress 字段应该被做成一个单独的类,并将所有这些行为移至 EMail 类中。但是我们不能将 EMail 表示为一个独立的数据库表,因为 Customer 和 Email 之间存在一对一的关联,而一对一的关联明确地向建模者暗示,这两个表可能需要合并成一个更合适的实体,并为该实体创建一个数据库表。此外,我们的 EMail 类包含了很多行为(检查电子邮件地址格式、发送电子邮件,甚至可能使用电子邮件地址作为登录 ID 的功能),但仍然只有一个字段——emailaddress,这进一步证实了这个一对一的关联需要统一到一个实体中。同时,虽然 EMail 类提高了对象模型的内聚性,但我们发现一个 EMail 对象不能独立于 Customer 对象存在,并且总是与一个 Customer 对象相关联,没有共享引用,因为一个电子邮件既不能在两个客户之间共享,如果客户不存在,客户的电子邮件也没有任何用处。像 Customer 这样具有独立生命周期的对象被称为实体(Entity),而像 EMail 这样不能独立存在且没有共享引用(严格来说只有一对一关联)的依赖对象在 NHibernate 中被称为值类型(Valuetypes)。
实体对象有对应的数据库表,因此需要在类中定义标识符属性,并在映射时有相应的 id 属性。
值类型没有单独的表,它们被放置在与它们紧密相关(一对一关系)的实体所在的同一个表中,就像 Email 值类型对应 Customer 实体一样。因此,值类型不需要单独的标识符字段,因为它们通过与之关联的实体来识别,并且没有独立的数据库表。
由于值类型(例如 EMail)的生命周期完全依赖于其相关的实体(例如 Customer)且没有共享引用,在 UML 模型中,实体和值类型之间的关联是组合(COMPOSITION),而不是聚合(AGGREGATION)。这种关联在模型上的差异可能不会改变 Java 和 C# 的代码,但在 C++ 中,这种差异会对代码产生巨大影响,因为它意味着值类型的对象必须通过值实例化,而不是使用指针或引用。
最后,在 NHibernate 中,虽然所有实体类都有一个单独的 .hbm 映射文件,但值类型没有单独的映射文件或数据库表。它们通过 `
识别实体和值类型
要识别一个实体,看它是否满足以下清单:
- 这个类的对象是否具有独立的生命周期?对于实体来说,是的。
- 这个类的对象的引用是否被共享?对于实体来说,可能是的。
- 这个类的对象是否需要一个独立的数据库表和标识符?是的。
要识别一个值类型,对于清单中的所有问题,答案都是一个明确的“否”。
实体和值类型示例
在我们的电子商务场景示例应用程序中,有一个 Customer C# 类(带有 id 属性)和一个 Email C# 类(没有 id 属性),它们都映射到同一个名为“CUSTOMER”的表中,映射文件为 Customer.hbm.xml。我们像往常一样编写 Customer 和 Email 类的 C# 代码,并通过在 Visual Studio 中添加一个 Customer.hbm.xml 文件来为 Customer 类创建映射文件。 Customer.hbm 映射如下所示:
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly ="EShopSystem" namespace ="EShopSystem.Domain" >
<class name="Customer" table="CUSTOMER" >
<id name ="CustomerId" column ="CUSTOMERID" type ="long" generator="native" />
<property name="CustomerName" column="CUSTOMERNAME" type="string" />
<component class="Email" name="EmailIdentity">
<property name="EmailAddress" column="EMAILADDRESS" type="string" />
</component>
</class>
</hibernate-mapping>
请参考下面的图二。在这张图中,我展示了上述映射文件如何对应 C# 类。我将 Customer 和 Email 类的 C# 代码并排放在图的顶行,用一条黑线隔开。在 Customer 和 Email 类的 C# 代码下方,我放置了 Customer.hbm 映射文件的代码。我用箭头展示了 C# 代码类与其对应的 .hbm 映射文件之间的映射关系。不喜欢这种表示风格的人可以直接下载本系列第 7 篇文章中的示例项目,并参考 C# 代码和映射文件。但这种方法对于理解 NHibernate 映射来说要简单得多。
现在来解释一下图二中的简单代码。首先,在继续之前,请检查图二中的 C# 代码,并注意 Customer 实体类有一个名为 CustomerId、类型为 long 的属性,用于存储对象标识符,而这个属性在值类型 Email 中是不存在的。原因如前所述,实体 Customer 需要这个标识符,因为它有自己的数据库表——CUSTOMER TABLE,表中的每一行都对应一个 Customer 类型的 C# 对象,而每一行的主键就存储在相应 C# 对象的标识符属性中。像 Email 这样的值类型没有自己的数据库表,因此没有这个标识符。现在我们继续解释下面的图二。
Customer 类作为一个实体,必须有一个表——CUSTOMER,以及一个对应的 Customer.hbm 映射文件。请跟随绿色箭头查看 Customer C# 类到其在 Customer.hbm 映射文件中对应的 `
如前所述,Email 类是一个值类型。因此它不能有自己的表或映射文件。所以 Email 是在 Customer 类的 .hbm 映射文件中通过使用 `
customer 和 email 之间的关联在 C# 中通过常规方式捕获,即在 Customer 中存储一个名为 EmailIdentity 的 EMail 对象引用。这个属性在 Customer C# 类中名为 EmailIdentity。请跟随图二中的紫色箭头,查看 Customer 类上的 EmailIdentity 属性与其在映射文件中对应标签的映射关系。
在 Customer 类的映射文件中为 Email 类进行这种 `
图二
因此,可以使用的类比表多,这使得将丰富的领域模型映射到较少的表成为可能,方法是使用前面给出的清单,恰当地将类映射为实体和值类型。
一对一实体关联
之前发现 Customer 和 EMail 之间存在一对一关联,但当我们更深入地分析这个一对一关联时,发现 EMail 是一个值类型,应与 Customer 放在同一个表中,因此它不是一个映射到不同表的一对一实体关联。现在的问题是如何映射实体类型的一对一关联。
对象之间的关联是通过存储引用来创建的。它们可以是单向的或双向的。例如,要在对象 B 和对象 A 之间创建关联,我们将 B 的引用存储在 A 中,如下图三的第 1 部分所示。但是数据库没有引用的概念。因此,在数据库中,要建立从表 A 到表 B 的链接,需要将表 B 的主键作为外键放在表 A 中。现在它们之间就建立了可导航性。见图三的第 2 部分。所有 ORM 软件都通过在一个表中使用另一个表的外键来将一对一关联映射到数据库表。虽然这对所有人来说都很容易推断,但我们不要忘记,这个细节必须在参与关联的实体的映射文件中指定,以便 NHibernate 进行管理。必须为关联中涉及的每个类提供两个独立的映射文件。
图三
在 NHibernate 中编写一对一映射
考虑一个电子商务应用的场景。
“客户提交订单并付款。电子商务应用处理订单,一旦付款被批准,就将该订单添加到已批准付款的订单中。”
在这个场景中,PaymentApprovedOrder 和 Payment 之间的关联是一对一的实体关联。我们不深入探讨 PaymentApprovedOrder 和 Payment 类的字段和行为细节,而是关注这两个实体之间的一对一关联,这与此处的讨论更相关。
在深入探讨关联细节之前,值得注意的是要理解为什么 Payment 类必须是实体而不是值类型,因为它看起来生命周期与 Order 类绑定,没有 Order 实例,Payment 就无法存在。虽然没有订单就无法存在支付是事实,但 Payment 实例的生命周期比 Order 实例更长,因为订单在发货后即告完成,但 Payment 对象可能会在 Order 和 Shipment 实例之后继续存在,尤其是在其完成需要进一步处理时间的情况下。一个订单的支付甚至可以延续数年(如果分期付款)。如果将 Payment 设为值类型,那么它的生命周期将随着 Order 实例的结束而结束,无法独立于 Order 实例持久存在,这是一个问题。因此,必须将 Payment 设为实体,以赋予其独立的生命周期。如果仍有疑问,请参考实体和值类型的核对清单。NHibernate 的核心在于基于强大的面向对象和关系分析做出合乎逻辑的设计决策。
下面的图四展示了如何映射 Payment 和 PaymentApprovedOrder 之间的关联。对于 Payment 和 PaymentApprovedOrder 类,我们只添加最少的属性,因为我们在此感兴趣的主题是它们之间的双向一对一关联以及如何在 NHibernate 中进行映射。
在下面的图四中,首先编写了 PaymentApprovedOrder 和 Payment 的 C# 代码。在 C# 代码下方,编写了 .HBM 映射文件。首先,为了在 C# 代码中表示 PaymentApprovedOrder 和 Payment 之间的关联,像往常一样存储了引用。请看图中 PaymentApprovedOrder 和 Payment 的 C# 代码。这些引用必须在 .HBM 文件中进行映射,以在 NHibernate 中映射该关联。现在我们继续解释下面的图四,看看这个关联是如何映射的。
我们先来看看 PaymentApprovedOrder 是如何映射的。PaymentApprovedOrder 类存储了一个名为 OrderPayment 的 Payment 类实例的引用。请看图四中的橙色箭头,了解存储的 Payment 引用是如何从 C# 类映射到其 .HBM 文件的。
这个关联的映射如下:
在包含 PaymentApprovedOrder C# 类的 PaymentApprovedOrder.cs 文件中:
public virtual Payment OrderPayment { get; set; }
在 PaymentApprovedOrder.hbm 文件中:
<many-to-one name ="OrderPayment" lazy="false" class ="Payment" column="PAYMENTID" unique="true" not-null="true" cascade="save-update"/>
这里值得注意的是,我们说 PaymentApprovedOrder 与 Payment 类存在一对一关联。为什么它被标记为 `
通过这里的 `
这种用法似乎明显违反了 PAYMENTAPPROVEDORDER 和 PAYMENT 之间所需的一对一关联。
正如我们已经解释过的,实例之间的关联在数据库中通过外键关系来表示。因此,为了将多个 PaymentApprovedOrder 实例与一个 Payment 实例之间的这种 `
这不是我们想要的。我们希望 PAYMENTAPPROVEDORDER 表中代表 PaymentApprovedOrder 类不同对象的每一行,都对应一个来自 PAYMENT 表的外键,该外键恰好代表一个 Payment 对象实例,并且每个来自 PAYMENT 表的外键在 PAYMENTAPPROVEDORDER 表中只出现一次(因为这是一对一关系)。
如果你看一下这个 `
现在所有读者心中的问题会是,为什么不直接使用 `
另一个问题是,我们是不是通过使用 `
我们需要理解 NHibernate 是如何看待映射文件并解释我们定义的 `
那么,如果我们想让它成为一个双向链接,该怎么做呢?
我们还需要映射关联的另一端。为此,我们使用 Payment.hbm 文件。
现在让我们看看映射是如何完成的。请参考图四。 请看绿色箭头,了解存储的 PaymentApprovedOrder 引用是如何从 C# 代码映射到其映射文件的。
下面的代码片段来自 Payment.cs 和 Payment.hbm,用于展示双向链接:
在包含 Payment 类的 Payment.CS 文件中,
public virtual PaymentApprovedOrder PaidOrder { get; set; }
在 Payment.hbm 文件中,
<one-to-one name ="PaidOrder" lazy="false" class ="PaymentApprovedOrder" property-ref="OrderPayment"/>
在 Payment 的映射文件中,我们看到关联的另一端被定义成了我们希望看到的样子,一个 `
图四
现在我们已经完成了一对一关联。类似地,在这个由 8 部分组成的文章系列中,我们将完成 `
“lazy”属性
为清晰起见,我已从图四中移除了“lazy=false”属性,但你会在映射文件的代码片段中看到它。这个“lazy”属性是什么?简而言之,一个面向对象的系统由一个通过关联相互连接的对象网络组成。所有这些对象都被持久化到数据库中。因此,当我们需要从数据库中获取一个对象的信息时,我们可能会引发一场对象雪崩,因为我们需要的对象会有关联或集合,而这些关联或集合又可能有其他关联,为了完全获取一个对象,所有这些都必须从数据库中获取。更糟糕的是,我们最终可能根本不会使用我们获取的对象上的任何关联或集合。为了避免这种雪崩,NHibernate 默认使用“lazy=true”,以便在以后需要时延迟获取对象的所有关联和集合,当前只立即获取正在使用的对象。在这种情况下,只返回对象本身,其集合和多重关联则用代理或占位符表示。在我们的案例中,我们确信不会引起从数据库中获取数据的雪崩,所以我们设置 lazy=false。请注意,lazy=false 很方便,但在真实场景中,最佳选择始终是 lazy=true。
但在这里的示例应用程序中设置 "lazy=true" 会导致异常。为什么?我来告诉你原因。请参考图五中的代码片段。我们有一个通用的便利类,名为 `DBRepository<...>
,在其中我们打开一个到 NHibernate 的 Session 对象,执行一些数据库操作,并在最后关闭 Session 对象。例如,当我们要通过标识符检索一个对象时,我们使用 `DBRepository<...>.getItemById(...)
。如果 lazy=true,对象在被检索时,其关联和集合会使用代理或占位符,并在调用 `DBRepository<...>.getItemById(..)
时返回给调用方法。但如果你查看 `DBRepository<....>.getItembyID(...)
,我们在方法调用结束退出前关闭了 Session 对象。因此,当使用 `DBRepository<...>.getItembyID(...)
从数据库检索到的对象被使用其某个关联或集合时,由于延迟初始化功能是开启的(因为设置了 lazy=true 属性),它只会有其关联和集合的代理,因此需要 Session 对象去数据库中获取信息。不幸的是,Session 对象已经关闭,因此会抛出异常。所以,如果必须设置 lazy=true,那么这个示例必须为生产环境重新架构。在这个八部分的系列文章中,我也会展示如何做到这一点。但对于一对一关联,最简单的解决方案是设置 lazy=false,就像我在这里的代码片段中展示的那样。我们稍后会探讨更复杂的解决方案。
T IRepository<T>.getItemById(long item_id)
{
// THIS METHOD IS IN DBREPOSITORY<T> class
T temp;
using (ISession session = NHibernateHelper.Create().Session)
{
temp = session.Get<T>(item_id);
}
return temp; //NOTE : AT THIS POINT WHEN METHOD RETURNS SESSION IS DELETED.
图五
这个 8 部分系列文章的第 7 篇中的 Microsoft Visual Studio 2012 示例项目在一个可下载的项目中展示了这些关联映射。你可能需要也可能不需要更改连接字符串(ConnectionString)。代码中的客户端项目包含了查看每个关联和映射所需的示例代码。所有示例都使用 POCO,因此它们相当容易理解。第 1-5 篇文章将嵌入第 7 篇文章的完整示例下载。
结论
在本系列的后续文章中,我们将探讨使用 NHibernate 映射多对一、多对多关联,使用 NHibernate 进行继承映射,在生产场景中有效使用 NHibernate,以及使用 NHibernate 处理集合将是下一步。尽情享受 NHibernate 吧。