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





5.00/5 (6投票s)
本系列共八篇文章,旨在展示使用 NHibernate 进行一对一、多对一、多对多关联映射,使用 NHibernate 的集合,使用 NHibernate 的继承关系,使用 NHibernate 的延迟初始化/获取。
文章系列
引言
这是本系列文章的第五部分,将分为两个部分:5A 和 5B。第 5A 部分将侧重于多对多实体关联,并重点介绍一个常用的示例,但模型和代码略有不同。第 5B 部分将基于本系列所有文章考虑的电子商务场景中的多对多实体关联示例。
在所有实体关联中,最有趣的是多对多实体关联。然而,它却被大多数人所回避。NHibernate 中的多对多实体关联可以完全按照在 OOAD 中捕获的方式来实现。NHibernate 如何用于实现多对多关联的持久化是我们的兴趣点。
背景
NHibernate 处理多对多实体关联的方式可能倾向于遵循 OOAD 的常规路线,如前所述。使用关联类将多对多关联拆分成两个一对多关联是 OOAD 处理对象中多对多关联的方式(如图 1 所示)。因此,遵循这一点,我们的多对多实体关联就变成了两个一对多实体关联的映射,这一点我们已经在本文的第 3 部分中进行了研究。如果针对领域场景进行了正确的建模,多对多实体关联将会在关联类上清晰地封装和捕获链接所需的所有信息,而 OOAD 处理多对多关联方式最独特之处在于,这个关联类不是为了拆分多对多关联而专门创建的人工类,而是通常作为领域本身的一部分自然存在。第 5A 部分提供的示例将再次证实这一点,并展示 NHibernate 如何用于实现其持久化。
使用代码
多对多实体关联示例
多对多关联的常见使用场景是,某种类型的系统可能提供多种服务,每种服务都有许多用户,而用户可以选择使用系统中的多种服务中的特定类型服务。需要捕获用户到服务的分配。此场景的例子不胜枚举,如乘客与火车(或其他交通服务)、电影观众与影院、酒店房间与入住者等。
让我们以火车为例,它很好地捕捉了多对多关联的场景。一位乘客可以乘坐多趟火车。一趟火车可以载有多名乘客。因此,在火车票务系统中,火车和乘客都是实体类,它们之间的关联是多对多的。关联类是“车票”,它打破了这种多对多关联,并将特定乘客的实例与特定火车的实例绑定。现在,一趟火车有许多车票(一对多),一位乘客可以有多张车票用于多趟火车(一对多)。每个开发者都知道这个场景,因为它是最常用的场景。我们讨论的主题是揭示如何使用 NHibernate 来映射多对多实体关联。
我们的示例场景是一个控制台应用程序,用于持久化在线火车票务系统中多对多关联。我们将稍微不同地建模场景,因为在常规实践中,乘客不一定必须提前预订自己的车票,而且提前预订的火车票在预订单上可以列出多名乘客的姓名(家庭一同预订一张票,包含所有家庭成员姓名)。
因此,在我们的在线票务系统中,注册用户可以为多趟火车预订车票。一趟火车会被多名注册用户预订车票。因此,希望预订多趟火车车票的注册用户与被多名注册用户预订车票的火车之间的关联是多对多的实体关联。用户提交一个包含出行乘客列表的表单,以及他需要预订车票的火车。关联类是在线 Ticket
,它将预订车票的特定用户以及预订车票的特定火车与注册用户在预订表中列出的该车票的乘客列表关联起来。车票由 TicketCounter
类签发,它是签发车票的预订系统。我们将忽略火车的日期和时间,仅通过名称来区分火车,因为我们的兴趣仅限于多对多实体关联以及使用 NHibernate 进行持久化。映射注册用户和火车之间的多对多实体类关联的优势在于,用户实例将拥有该特定注册用户预订的车票集合,而火车将拥有为该特定火车预订的车票集合,并且车票将包含乘客列表和其他与乘客相关的信息,所有这些都由 NHibernate 持久化。
现在参考图 2。它显示了 Train
和 User
(注册用户)类的映射。Train
和 User
(注册用户)之间的多对多实体关联通过关联类 Ticket
分割为两个一对多关联,这由橙色箭头表示。另外请注意,正如预期的那样,现在 Train
和 User
之间不再互相引用,尽管它们在域中存在多对多关联。它们只与关联类 Ticket
存在一对多的引用,这在 C# 代码中由 ISet<Ticket>
集合表示,并在 User
和 Train
中映射到 <set>
集合,由蓝色和紫色箭头表示。
下面的 C# 代码片段显示了 Ticket.cs 类。请注意,车票类使用复合键。我们一直倡导使用生成器生成的代理键,而不是复合键。但多对多关联是这一规则的例外,因为复合键在此处非常适合。我们已单独定义了此复合键类。在可下载的项目中,您将在 Ticket.cs C# 文件中看到这个 CompositeKey
类。在 Ticket
的构造函数中,请注意这个复合键被设置为购买车票的 User 和购买车票的 Train 的主键值。它正确地捕获了车票的领域场景。然后,必须在构造函数中为两个双向一对多关联设置关联。每张车票都将包含乘客列表,其中包含注册用户在预订表中填写的乘客信息。
public class Ticket
{
public Ticket()
{
// CompositeID is the composite key class defined separately
CompositeId = new CompositeKey();
// PASSENGERS LIST PER TICKET
Passengers = new List<Person>();
}
public Ticket(User user, Train train, IList<Person> passengers)
{
// CompositeID is the composite key from Train and Passenger
CompositeId = new CompositeKey();
//SET ASSOCIATION END
ReservedByUser = user;
ReservedForTrain = train;
//SET COMPOSITEE KEY
CompositeId.trainKey = train.TrainId;
CompositeId.userKey = user.UserId;
//OTHER ENDS OF ASSOCIATION
Iesi.Collections.Generic.ISet<Ticket> userList = user.UserBookings;
Iesi.Collections.Generic.ISet<Ticket> trainList = train.TrainBookings;
//SET OTHER ASSOCIATION END
userList.Add(this);
// CAN COMBINE PREVIOUS lines as user.UserBookings.Add(this)
//SET OTHER ASSOCIATION END
trainList.Add(this);
TicketCounter++;
TicketNumber = TicketCounter.ToString();
//ADD PASSENGERS TO TICKET
Passengers = passengers;
}
public virtual CompositeKey CompositeId { get; set; }
public virtual string TicketNumber { get; set; }
public virtual User ReservedByUser { get; set; }
public virtual Train ReservedForTrain { get; set; }
protected virtual int TicketCounter { get; set; }
public virtual IList<Person> Passengers { get; set; }
}
下面的映射代码片段 Ticket.hbm 如下所示。这里没有意外。映射代码中唯一新的标签是为 Ticket
类定义的复合键使用的 <composite-id>
标签。但是,在关联类的映射文件中,最重要的是与 Train
类和 User
类的两个 <many-to-one>
标签。请记住,我们采用了 OOAD 方法来处理多对多关联,并根据此方法使用 NHibernate。根据此方法,Train
和 User
之间的 <many-to-many>
关联必须拆分成与 Ticket
关联类的两个 <many-to-one>
关联。下面的 Ticket.hbm 映射文件中的两个 <many-to-one>
映射清楚地显示了 NHibernate 中如何实现这一点。
<class name="Ticket" table="TICKET" >
<composite-id name="CompositeId" class="CompositeKey">
<key-property name="userKey" access="field"
column="USERID" type="long"/>
<key-property name="trainKey" access="field"
column="TRAINID" type="long"/>
</composite-id>
<property name="TicketNumber" type="string"
column="TICKETNUMBER" not- null="true" />
<many-to-one class="User" name="ReservedByUser"
not-null="true" unique="true"
insert="false" update="false">
<column name="USERID"></column>
</many-to-one>
<many-to-one class="Train" name="ReservedForTrain"
not-null="true" unique="true"
insert="false" update="false">
<column name="TRAINID"></column>
</many-to-one>
<list table="TICKET_PASSENGERS"
name="Passengers" cascade="save-update">
<key not-null="true">
<column name="USERID"></column>
<column name="TRAINID"></column>
</key>
<list-index column="PASSENGER_LIST_POSITION"></list-index>
<one-to-many class="Person"/>
</list>
</class>
请注意,在上面代码片段和图 2 中显示的一对多关联中,我们将集合关联的两端都设置为逆向(使用 insert=false
和 update=false
,inverse=true
)。阅读过本系列文章的人会知道,只需要将一端设置为逆向。为什么这里两端都设置为逆向?因为我们在代码中使用事务(IssueTicket
方法)来确保对 Ticket
、User
和 Train
表的插入是原子性的。更重要的是,两张表的主键,即 User 和 Train,已被设为关联类中的复合键,从而代表了关联并形成了一个复合键。因此,无需在关联的两端自动生成额外的 SQL 语句。所以它们两端都被设置为逆向。此解决方案的结构在 Microsoft Visual Studio 2012 中可以更清楚地理解,当您查看为 Ticket - 关联类形成的表的结构时。它将非常清晰地显示由两个主键组成的复合键,这两个主键也是两个关联的一部分。使用 Visual Studio 2012 中提供的下载代码进行观察。此解决方案很好。还有一种方法是使用“join”表进行多对多关联,但我认为此解决方案更合适且面向对象。
本文提供的可下载解决方案(MS Visual Studio 2012 试用版)还包含客户端代码。它们是用于演示此处介绍的多对多实体关联的基于控制台的项目。运行代码的更改是向名为 RailwayReservationSystem 的项目添加一个基于服务的数据库。其步骤对所有人都很熟悉,但我会简要描述以完成文章:右键单击 RailwayReservationSystem 项目。在出现的弹出菜单中,选择“添加”,然后选择“新建项”。将打开添加新项的向导。在此,选择“服务基数据库”并单击“确定”按钮。稍后会弹出一个对话框,您可以取消。现在将一个新的数据库添加到您的项目空间。选择它,您将在解决方案资源管理器中的属性窗口中看到其属性。只需复制其完整路径,并在 RailwayReservationSystem 项目的 .config 配置文件以及客户端项目的 app.config 文件中使用它。运行客户端项目时,我们可以清楚地看到添加到 VS2012 中的数据库中各自表中的车票、用户和火车已持久化。请勿使用提供的示例下载项目来试验数据库获取。获取将在第 8 篇文章中介绍。现在,请享受使用 NHibernate 试验对象关联持久化的乐趣。使用 VS2012 中的 SQL 菜单以及通过此菜单中的编辑器编写 SQL 查询来检查 Train
、User
、Ticket
到数据库的持久化。
结论
能够按照我们在 OOAD 中的建模方式来编码 NHibernate 多对多实体关联是一个巨大的优势。原因在文章本身已经提到。OOAD 中用于拆分多对多关联的关联类不是仅用于此目的的人工类。它就像我们在示例中看到的,作为每个领域的一部分而存在,并精确地捕获所需的信息。租赁代理系统中文房屋和租户之间的租赁关系、房地产系统中文地主和租户之间的租赁协议、各种领域中的各种车票、服务提供商系统中文消费者和服务提供商之间的订阅计划、酒店系统中文酒店房间和入住者之间的房间预订/入住情况、医院系统中文医生和患者之间的咨询记录,都是这些领域中上述多对多关联的关联类的示例。因此,让 NHibernate 将这种多对多实体关联与该领域相应的关联类进行映射是非常有用的。下一篇文章 5B 将讨论我们示例场景中的多对多实体关联。享受 NHibernate 带来的便利。