使用 NHibernate 进行对象关系映射 (ORM) - 第 4 部分,共 8 部分:编码可选的一对多实体关联





5.00/5 (10投票s)
包含 8 部分的完整系列文章,展示使用 NHibernate 进行一对一、多对一、多对多关联映射,使用 NHibernate 处理集合,使用 NHibernate 处理继承关系,使用 NHibernate 处理延迟初始化/获取。
引言
在前一篇文章(本系列文章的第 3 部分)中已解释,可选的一对多关联允许关联的基数中出现值“0”(零)。文章还通过一个示例展示了,如果不正确映射,可选的一对多关联会导致可空的外键列,因此需要以不同的方式处理以避免空值,这对于数据库存储数据的质量始终是首选。所示的示例是 PaymentApprovedOrder 和 Inventory 中的 Items 之间的一个可选的一对多关联,它被简单地映射为 One-To-Many,从而导致 ForeignKeyValue 中出现空值。图 1 显示了 Item 表,其中由于将 PaymentApprovedOrder 和 Item 之间的可选的一对多关联简单地映射为 One-To-Many,导致外键 PAYMENTAPPROVEDORDERID 出现空值。
图 1 - PaymentApprovedOrder 和 Item(Inventory 中的 Items)的一对多关联结果 - 请注意外键列中的空值
本文旨在通过正确地将 PaymentApprovedOrder 和 Item 之间的关联映射为可选的一对多,来消除数据库中引入的空值。
背景
在 NHibernate 中,通过在“一对”端表和“多”端表之间引入一个 JOIN TABLE 来正确映射可选的一对多关联。连接表中的一行将同时具有“一对”端表和“多”端表的主键作为其外键。它通过将外键移动到连接表来避免先前在“多”端表中出现的外键空值。那么外键列的空值现在会出现在连接表中吗?绝对不会。在 NHibernate 中,您通过声明连接表中的一行是 OPTIONAL(简单来说就是该行对于任何外键空值都缺席)来定义连接表。也就是说,如果连接表中存在一行,则两个外键都是必需的且存在的。抽象地阅读可能不会有启发。阅读完示例后,请重新阅读本段,以欣赏 NHibernate 连接表如何用于解决双向可选一对多关联映射中的空值问题。抽象概念最重要,因此在完成示例后再次阅读本段,才能真正体会 NHibernate 的妙处。
使用代码
继续电子商务示例
PaymentApprovedOrder 和 Item 之间的关联必须映射为可选的双向一对多关联。我们在上一篇文章中已将其映射为简单的一对多关联。现在我们将改进它。*务必注意,映射具有双向关联的集合的最佳且推荐的方法是使用 <set> 和 <idbag> 等集合。对于双向关联中的集合,最好避免使用像 <list> 这样的有序集合。对于可选的一对多关联,最佳的集合是 <idbag>。我们将看到为什么 <idbag> 比 <set> 更受青睐。* <idbag> 在 C# 代码中将使用“IList<>”声明,但应该记住 <idbag> 不存储排序信息或索引信息。
首先,让我们考虑关联的 PaymentApprovedOrder 端。PaymentApprovedOrder 的一个实例将包含一个或多个 Item 实例。PaymentApprovedOrder.cs 中的 C# 代码已更改,以便集合的声明为 IList<Item> 以在映射文件中映射到 <idbag>。但是,要了解将 PaymentApprovedOrder 类从简单的一对多映射到可选一对多关联的更改,请参阅图 2。
图 2
在图 2(下半部分 - 可选一对多映射)中,除了将集合映射从 <set> 更改为 <idbag> 之外,最显著的变化在于 <idbag> 集合的映射方式。在此,为实体关联命名了集合表。请记住,在文章系列的第 2 部分和第 3 部分中,集合表仅为值类型命名,而对于实体关联,NHibernate 会从关联中推断出集合表。此外,在 <idbag> 集合中没有 <element> 标签,但它有一个指定为 <many-to-many> 的关联,表示这是一个带有集合表名的实体关联。由 <idbag table="..."> 集合命名的表称为 JOIN TABLE,并在下一段中非常清楚地进行了说明。必须使用 <many-to-many> 关联映射而不是 <many-to-one> 映射,因为 JOIN TABLE 与 <many-to-one> 映射无法按要求工作。通过使用 unique="true" 属性,将 <many-to-many> 映射约束为像 <many-to-one> 关联一样工作,例如 <many-to-one unique="true">。有关这如何工作的解释,请参阅本系列文章的第 1 部分,其中使用 unique="true" 属性将 <many-to-one> 约束为表现得像 <one-to-one>。最值得注意的重要属性是 <idbag> 使用 <collection-id> 标签为集合定义了一个单独的代理键。<idbag> 中的此主键列使其非常适合用于可选的一对多关联集合,我们将在回答为什么 <idbag> 比 <set> 集合更受青睐来映射双向可选一对多关联时详细讨论这一点。从该关联的这一端看,集合表的其中一个外键使用 <key> 标签命名,此处为 "PAYMENTAPPROVEDORDERID"。这是定义了带有 <idbag> 和 jointable 的集合的关联的一端,我们将在下一部分详细介绍关联的另一端。
让我们考虑关联的另一端,即 Item 类。请参阅图 3。映射文件 Item.hbm 中发生了一个重大变化。以前,我们直接将 Item 和 PaymentApprovedOrder 之间的关联映射为简单的 Many-To-One 关联。我们知道在数据库中要实现这种关联链接,“ONE”端表的主键“PAYMENTAPPROVEDORDERID”(来自“PAYMENTAPPROVEDORDER”表)被发布为“MANY”端表即 ITEM 表的外键(我们已经在本文系列的第 3 部分 - 背景部分进行了说明)。要确认这一点,请参阅图 1,它显示了 ITEM 表中的列,并且发现它有一个 PAYMENT APPROVEDORDERID 的列,这是从 PAYMENT APPROVEDORDER 表发布的主键。现在的问题是,我们的 ITEM 表代表库存中的商品,这些商品可能根本没有被订购过。因此,这个 PAYMENT APPROVEDORDERID 将会有空键。从这个观察结果可以清楚地看出,我们需要做什么来消除空值?我们需要将这个外键列 PAYMENT APPROVEDORDERID 移出 ITEM 表,同时保持 PAYMENT APPROVEDORDER 表和 ITEM 表之间的链接。这可以通过在 PAYMENT APPROVEDORDER 表和 ITEM 表之间引入一个名为 PAYMENT APPROVEDORDER_ITEMS 的 JOIN TABLE 来实现,并且 PAYMENT APPROVEDORDER 表和 ITEM 表之间的可选一对多关联被映射到这个表。另外请注意,前面段落中 <idbag> 集合映射中命名的集合表就是这个 PAYMENT APPROVEDORDER_ITEMS 连接表。连接表中的列将是发布的外键,来自与一对多关联链接的表的主键。因此,在我们的例子中,连接表将包含设置为 PAYMENT APPROVEDORDERID(PAYMENT APPROVEDORDER 表的主键)和 ITEMID(ITEM 表的主键)的外键。这个连接表中的一行将表示为特定 paymentapprovedorder 购买的商品。由于导致空值的外键列已从 ITEM 表移至连接表,因此 ITEM 表中不会有任何空值。最有趣的是,JOIN TABLE 本身不会有任何空值,只有当一个订单购买了一个商品时,才会有对应的行,即当一个已付款订单有一个已购买的商品时。让我们看看如何在 Item.hbm 映射文件中实现这一点。
参考图 3,查看如何使用 <join table="..."> 标签(在图 3 的椭圆形中显示)在 Item.hbm 映射文件中映射此 JOIN TABLE,以及 ITEM 和 PAYMENT APPROVEDORDER 之间的 many-to-one 关联如何被推送到 join table 中(由橙色箭头显示)。
图 3
查看图 3。浅青色椭圆形显示连接表映射。一如既往,由于关联是双向的,一端必须设为 inverse,此处选择了 join 端。Join 表将有两个外键列 - ITEMID 和 PAYMENT APPROVEDORDERID。ITEMID 使用 <key..> 标签设置为其中一个外键。只需跟随橙色箭头,即可查看 many-to-one 映射如何从 ITEM 表推送到 JOIN 表。此处在 <many-to-one> 映射中命名的列成为另一个外键(集合端也命名为外键列)。
因此,现在在 ITEM 表中产生所有空值的 PAYMENT APPROVEDORDERID 外键列已被从 ITEM 表推送到 PAYMENT APPROVEDORDER_ITEMS 连接表,以及为特定 PAYMENT APPROVEDORDER 购买的商品的 ITEMID。那么 jointable 如何避免空值呢?在图 3 中,join table 的映射(用浅青色椭圆形显示)显示了一个 optional="true" 属性,用于 join table,例如 <join optional="true" table="...">。attribute optional="true" 意味着只有当列非空时,才会向此 join table 添加一行。如果添加到 jointable 的行的列存在空值,那么在设置 <join option="true"...> 时,就不会添加该带有空值的行。因此,JOIN TABLE 保持无空值。更准确地说,连接表如下映射了领域场景:只有当一个商品是已付款订单中的一个已购买商品时,才会向连接表添加一行。否则,该商品仅存在于库存中,而不在连接表中,连接表中只应包含已付款和已购买的商品。这在图 4 中可以非常清楚地看到,它显示了 ITEM 表和连接表 PAYMENT APPROVEDORDER_ITEMS 表,对于与图 1 中使用的相同测试数据没有任何空值(使用了与本文系列第 3 部分的 <set> 示例相同的测试数据,该示例在图 1 中产生了空值。请注意,第 3 部分的 <list> 示例使用了不同的测试数据。测试数据如下。请与图 1 进行比较)。
图 4 - 显示了 PaymentApprovedOrder 和 Item(Inventory 中的 Items)的可选一对多关联结果。请将此图与充满空值的图 1 进行比较
请参阅图 4 的下半部分,它显示了 ITEM 表。第二列 ISORDERED 表示当一个商品被订购并付款时设置的布尔值。这些商品将在发货后从 ITEM 表中移除,我们稍后会看到。对于讨论的主题,最有趣的是,对于库存中已付款和已订购的商品,如图 4 下半部分 ITEM 表中 ISORDERED 列的值 1 所示,JOIN TABLE(如图 4 上半部分所示)中存在一行,指示已订购的 ITEM 和商品的相应 PAYMENT APPROVEDORDER。另请注意,与图 1 不同,两个表中都没有空值。
PaymentApprovedOrder.cs 文件显示在图 5 中。C# 代码中的 bag 集合声明由橙色箭头显示。
图 5
下面的客户端测试代码与之前在本文系列第 3 部分的简单一对多关联(使用 <set> 示例)中显示的相同。唯一的变化是上一篇文章中添加的客户和订单之间的关联。但测试数据是相同的。两种情况下产生的结果已在前面显示,并有助于说明正确映射可选一对多如何避免空列。请注意,为了充分展示级联操作,我们使用客户存储库将所有持久化实例保存到数据库,这将级联到 PaymentApprovedOrder、Payment、Item 等。由于我们使用正确设置了级联的双向关联来显示父子关系,因此也可能存在其他方法。
//TEST CLIENT CODE
IRepository<Item> items_repo = new DBRepository<Item>();
IRepository<PaymentApprovedOrder> paid_orders_repo = new DBRepository<PaymentApprovedOrder>();
IRepository<Customer> customer_repo = new DBRepository<Customer>();
//CREATE 7 NEW ITEMS
Item[] items = new Item[7];
items[0] = new Item { InventorySerialCode = "00A0110" };
items[1] = new Item { InventorySerialCode = "01A0101" };
items[2] = new Item { InventorySerialCode = "02A10101" };
items[3] = new Item { InventorySerialCode = "03A01010" };
items[4] = new Item { InventorySerialCode = "04A101010" };
items[5] = new Item { InventorySerialCode = "05A010101" };
items[6] = new Item { InventorySerialCode = "06A0100100" };
//ADD LAST FIVE ITEMS TO REPSITORY
//ITEMS ADDED HAVE SERIAL CODE 03--- to 07---
//ALL THESE ITEMS WILL HAVE NULL FOR PAYMENTAPPROVEDORDER REFERENCE
//BECAUSE THEY EXIST BEFORE IN INVENTORY AND NOT BOUGHT
for (int counter = 3; counter < items.Length; counter++)
{
items_repo.addItem(items[counter]);
}
// CREATE AN ORDER
Order order = new Order();
//ADD FIRST THREE ITEMS TO ORDER
//IN ACTUAL SCENARIOS ALL ITEMS WILL
//BE EXISTING IN REPOSITORY (DB)
//EVEN BEFORE A ORDER IS PLACED
//BUT JUST TO SHOW CASCADE ATTRIBUTE IN
//ACTION, THEY HAVE NOT BEEN ADDED BEFORE.
order.OrderItems.Add(items[0]);
order.OrderItems.Add(items[1]);
order.OrderItems.Add(items[2]);
//ADD ONE MORE ITEM TO ORDER THAT WAS ALREADY SAVED IN REPOSITORY
//THIS IS THE CORRECT WAY. ITEMS EXIST IN REPOSITORY EVEN
//BEFORE A ORDER IS CREATED.
order.OrderItems.Add(items[3]);
//// Add Customer for the Order
Email mail1 = new Email { EmailAddress =
"<a href="mailto:alice@wonderland.com">alice@wonderland.com" };
Customer customer = new Customer { CustomerName = "AliceWonder", EmailIdentity = mail1 };
order.OrderedByCustomer = customer;
//CREATE A PAYMENT
Payment payment = new Payment { PaymentAmount = 1000 };
//CREATE A PAYMENT APPROVED ORDER
PaymentApprovedOrder paid_order = new PaymentApprovedOrder(order, payment);
// SAVE PAYMENTAPPROVEDORDER etc TO DB using CUSTOMER
//ALL ITEMS NOT IN DB WILL BE SAVED TO DB
//BECAUSE CASCADE IS SET .
customer_repo.addItem(customer);
最后,为什么 <idbag> 比 <set> 集合更受青睐来映射双向可选一对多关联?请参考图 2。图中椭圆形显示 <idbag> 的集合映射将拥有一个代理键作为集合表的主键。这就是为什么它比 <set> 集合更受青睐来映射可选一对多关联。<set> 映射没有这个代理键,而且为了遵守 set 的定义,即其中的所有项都是唯一的,<set> 将使用表中的列创建一个复合主键,在我们的连接表中,这两个外键列,即 ITEMID 和 PAYMENT APPROVEDORDERID。因此,这两个列都必须映射为非空。但对于库存中未购买的商品,订单的引用将为空。当这些商品保存到数据库时(例如,在向库存添加新商品时),将引发一个属性异常,指出正在保存的实例引用的一个非空属性,因为对订单的引用为空。因此,为了避免这种情况,直接的 <bag> 集合更适合可选的一对多关联。因此,双向可选一对多关联使用 <idbag> 正确映射。
关注点
结论
必须注意的是,尽管 <idbag> 集合在 C# 代码中被映射为 list,但 bag 的位置信息不会被捕获(对于实体双向关联通常不会有太大影响)。但是 <idbag> 非常适合正确映射可选一对多双向关联。本系列的下一篇文章将讨论多对多关联。