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






4.93/5 (6投票s)
本系列共 8 篇文章,旨在展示使用 NHibernate 进行一对一、多对一、多对多的关联映射,以及使用 NHibernate 的集合、NHibernate 的继承关系以及 NHibernate 的延迟初始化/获取。
文章系列
引言
这是关于多对多实体关联文章的第二部分。这里的想法是在贯穿本系列文章的示例研究中,将多对多实体关联应用于电子商务场景。
背景
《第 5A 部分》中提到的背景在此也适用。从中汲取灵感,值得记住的是:NHibernate 中的多对多实体关联可以完全按照 OOAD 中多对多关联的分离方式来表示(使用关联类,并将其分解为两个一对多关联)。
使用代码
电子商务示例场景
给出多对多实体关联示例的场景是:
当客户访问电子商务网站时,他会打开网站上的各种产品描述。如果某个产品描述符合他的购买兴趣,他可能会选择将其添加到购物车中,以便稍后订购。
从这个示例场景来看,明显的实体类是 ProductDescription
、ShoppingCart
和 Customer
。根据 OOAD,可以得出结论,上述场景在 ProductDescription
和 Customer
之间存在多对多关联,并使用 ShoppingCart
类作为关联类,其中包含一个购物车项的集合(与前一篇 5A 文章中的 Ticket 关联类及其 Passenger 集合类似)。这是常规做法,但有一种更好的建模方式。
回顾领域,购物车和产品描述通过 ShoppingCartSelection
链接关联,该链接表示客户添加到特定购物车的某个项目。因此,一个 ShoppingCart
可能包含许多选中的 ProductDescription
,而一个 ProductDescription
也可能被选中放入多个 ShoppingCart
中(每个购物车可能属于不同的客户)。因此,ProductDescription
和 ShoppingCart
之间的关联可以建模为多对多关联,关联类是 ShoppingCartSelection
。这个关联类 ShoppingCartSelection
将特定的 ShoppingCart
与特定的 ProductDescription
相关联,即它将 ShoppingCart
和 ProductDescription
之间的多对多关联分解为两个以自身为中间的一对多关联。在确定 ShoppingCartSelection
作为关联类之前,主要关注点是它是基于 ShoppingCart
的实体类还是值类型?订单将基于 ShoppingCartSelection
,当生成订单时,购物车将不复存在,但其 ShoppingCartSelection
必须在订单中继续存在,以便进行后续处理,如支付和配送。因此,ShoppingCartSelection
仅是实体类,可以最终确定为 ShoppingCart
和 ProductDescription
之间多对多关联的关联类。
要完成此场景,接下来需要捕获的是,购物车始终与客户以一对一或多对一关联(根据我们的需要)关联。如果我们允许客户拥有多个购物车,那么在购物车和客户之间就是多对一关联;否则就是一对一关联。我们在此使用客户和购物车之间的一对一关联。我们可以使用可选的一对一关联来正确地捕获客户可能没有关联购物车(如果他们不购买任何东西)的场景。有关使用可选一对多关联及其优势,请参阅本系列文章的第 4 部分。使用可选一对一关联的主要优势将是避免外键列中的空值。这是否意味着我们必须在这里使用可选一对一关联来避免外键列中的空值?不一定。根据本系列文章的第 1 部分,我们知道对象之间的一对一关联是通过表之间的外键来实现的。因此,只需将 CUSTOMER 表的主键放在 SHOPPINGCART 表中。在 Customer
和 ShoppingCart
之间的一对一关联中,Customer 表的主键 CUSTOMERID 将成为 SHOPPINGCART 表中的外键列。对于没有购物车的客户,不会出现空值,因为 SHOPPINGCART 表中不会添加这样的行。如果我们反过来做,即使用 SHOPPINGCART 表的主键 SHOPPINGCARTID 将外键放在 CUSTOMER 表中,那么对于没有购物车的客户, CUSTOMER 表中放置的外键列 SHOPPINGCARTID 自然会有一个空值来体现这种关联。这些都是精细的细节,在不付出额外努力的情况下可以提高质量。可以通过使用属性 ref
来处理双向一对一关联(有关更多信息,请参阅本系列文章的第 1 部分)。如果我们仅为了严格遵守领域而将这种一对一关联捕获为可选的一对一关联,那么我们将需要处理另一个连接表。拥有比类少的表,即细粒度的方法更好。但对于可选的一对多关系,没有这样的解决方法,它应该按照其本身进行编码。
ProductDescription
和 ShoppingCart
之间的多对多关联如图 1 所示。查看图 1,主要看到的是,尽管 ProductDescription
和 ShoppingCart
之间存在多对多关联,但它们之间没有互相引用。遵循 OOAD 的原则,关联类 ShoppingCartSelection
将此多对多关联分解为两个以自身为中间的一对多关联,以及这两个类。在图 1 中,橙色箭头清晰地显示了这一点。接下来要观察的是,两个一对多关联的集合端都是 **inverse**(反向),正如紫色下划线清晰显示的那样。如果查看关联的另一端,即通过在关联类 ShoppingCartSelection
的映射文件中使用 many-to-one 标签形成(如下面的代码所示)的非集合端,您会注意到它也被编码为通过使用 insert=false
和 update=false
属性来获得反向效果。可以立即推断出,我们正在限制 NHibernate 为关联两端自动生成插入和更新的 SQL 语句。那么,这个双向关联是如何创建并填充到数据库中的?解释在本篇文章的下一段中给出。目前,请观察下面 ShoppingCartSelection
关联类的代码片段以及图 1 中两端的反向设置。
关联类 ShoppingCartSelection
的代码如下所示
public class ShoppingCartSelection
{
public ShoppingCartSelection()
{
ShoppingCartSelectionId = new CompositeKey();
}
public ShoppingCartSelection(ProductDescription product, ShoppingCart cart,int quantity)
{
ShoppingCartSelectionId = new CompositeKey();
//Set the composite keys
ShoppingCartSelectionId.class1Key = product.ProductDescriptionId;
ShoppingCartSelectionId.class2Key = cart.ShoppingCartId;
//Set the associations
product.CartSelectionsWithThisProduct.Add(this);
cart.CartSelections.Add(this);
CurrentProduct = product;
ParentCart = cart;
//SetProperty
Quantity = quantity;
}
public virtual CompositeKey ShoppingCartSelectionId { get; set; }
public virtual ProductDescription CurrentProduct { get; set;
public virtual ShoppingCart ParentCart { get; set; }
public virtual int Quantity { get; set; }
}
映射文件 ShoppingCartSelection.hbm 的代码如下所示<class name="ShoppingCartSelection" table="SHOPPINGCARTSELECTION" >
<composite-id name="ShoppingCartSelectionId" class="CompositeKey">
<key-property column="PRODUCTDESCRIPTIONID" name ="class1Key"
access="field" type="long"></key-property>
<key-property column="SHOPPINGCARTID" name ="class2Key"
access="field" type="long"></key-property>
</composite-id>
<property name="Quantity" column="QUANTITY" type="int" />
<many-to-one name ="CurrentProduct" column ="PRODUCTDESCRIPTIONID"
class="ProductDescription" insert="false" update="false"/>
<many-to-one name="ParentCart" column="SHOPPINGCARTID"
class="ShoppingCart" insert="false" update="false"/>
</class>
由关联类形成的两个一对多双向实体关联的两端都被标记为反向端。从本系列文章之前的文章中,我们知道当一端被标记为反向时,NHibernate 不会生成自动插入和更新语句。那么为什么将两端都映射为反向呢?如前所述,我们使用 NHibernate 来捕捉多对多关联,就像我们在 OOAD 中思考的那样。我们使用关联类。重要的是,使用的关联类不使用代理键。它使用从关联两端引入的两个外键的复合键。此链接在关联类的构造函数中建立(如上面代码中 ShoppingCartSelection
的构造函数所示)。由于两个关联都通过关联类的构造函数中的复合键进行捕获,因此一对多关联与两个多端类之间的插入和更新语句的需要是什么?因此,关联的两端都标记为反向。请参见下面的图 2,其中我显示了为 SHOPPINGCARTSELECTION 形成的表的结构。请注意,两个外键列共同构成了该表的复合键,并在 ShoppingCartSelection
类的构造函数中设置了该键,此链接必须仅通过代码管理,如上所示。由于两个一对多关联已经作为复合键捕获,因此我们将链接的两端都设置为反向,并避免 NHibernate 生成自动 SQL 语句。图 2
这一切都很好,但这种方法的优势在哪里?优势在于 Customer
对象可以访问 ShoppingKart
对象,该对象又包含其 ShoppingCartSelection
实例的集合,这些实例包含已购买的 ProductDescription
的信息,现在可以设置购买优先级(根据用户预算),通过添加或删除项目来从购物车选择中生成订单。最大的业务价值在于关联的另一端。ProductDescription
实例拥有其 ShoppingKartSelection
实例的集合,这些实例表示将此特定 ProductDescription
实例选中的所有 ShoppingKartSelection
实例。每个 ShoppingKartSelection
实例都与一个 ShoppingKart
实例关联,该实例与一个选中了该商品并将其放入 ShoppingKart
的 Customer
实例相关联。如果 ProductDescription
是 HighValue
(高价值),产生高利润,电子商务网站可以利用这种关联找到哪些客户选中了高价值的 ProductDescription
并将其放入购物车但未下单,并不断提醒他购买(整个订单提醒场景可以轻松自动化)。所有这些实例及其关联都可以使用 NHibernate **无需查询,仅通过对象**即可从数据库持久化和获取。我们开发人员知道将所有内容作为对象访问的优势。如果正确使用 OOAD 并充分分析业务用例,就能产生纯粹的价值。下面给出了具有与 ShoppingCart
双向一对一关联的 Customer
类的代码
public class Customer
{
public Customer()
{
CustomerPaidOrders = new List<PaymentApprovedOrder>();
}
public virtual long CustomerId { get; set; }
public virtual string CustomerName { get; set; }
public virtual Email EmailIdentity { get; set; }
public virtual IList<PaymentApprovedOrder> CustomerPaidOrders { get; set; }
public virtual ShoppingCart CustomerCart { get; set; }
public virtual void AddPaidOrder(PaymentApprovedOrder order)
{
//SET ONE end of the association
order.PaidByCustomer = this;
//SET MANY end of the association
CustomerPaidOrders.Add(order);
}
}
Customer
类的映射文件如下所示<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>
<list name ="CustomerPaidOrders" cascade="save-update">
<key column ="CUSTOMERID" not-null ="true"></key>
<list-index column ="PAIDORDER_LIST_POSITION"></list-index>
<one-to-many class ="PaymentApprovedOrder"/>
</list>
<one-to-one class ="ShoppingCart" name ="CustomerCart" property-ref="CartOfCustomer"/>
</class>
用于测试此功能的客户端代码如下(其他类所需的附加信息可以从第 1 至第 4 部分的文章中获取)IRepository<ProductDescription>product_repo = new DBRepository<ProductDescription>();
IRepository<ShoppingCartSelection> cart_selection_repo = new DBRepository<ShoppingCartSelection>();
IRepository<ShoppingCart> cart_repo = new DBRepository<ShoppingCart>();
IRepository<Customer> customer_repo = new DBRepository<Customer>();
Customer customer1 = new Customer { CustomerName = "AliceWonder"};
Customer customer2 = new Customer { CustomerName="JimTreasure" };
Customer customer3 = new Customer { CustomerName = "OliverTwist" };
customer1.EmailIdentity = new Email { EmailAddress =
"<a href="mailto:alice@wonderland.com">alice@wonderland.com</a>" };
customer2.EmailIdentity = new Email { EmailAddress=
"<a href="mailto:jim@treasureisland.com">jim@treasureisland.com</a>" };
customer3.EmailIdentity = new Email { EmailAddress =
"<a href="mailto:olivertwist@london.com">olivertwist@london.com</a>" };
//customer 1 add to repository
customer_repo.addItem(customer1);
//Extra customers added to repository - these have no carts
customer_repo.addItem(customer2);
customer_repo.addItem(customer3);
ProductDescription description1 =
new ProductDescription{ ManufacturerName="samsung",Price=60000,ProductName="mobile" };
description1.ProductUserReviews.Add(new ProductReview { UserEmailId="<a href="mailto:a1@a.com",
UserComment="GOOD">a1@a.com",
UserComment="GOOD</a> PRODUCT.MUST BUY",ProductRating=5.0 });
description1.ProductUserReviews.Add(new ProductReview { UserEmailId =
"<a href="mailto:b1@b.com">b1@b.com</a>",
UserComment = "Dull PRODUCT.Dont BUY", ProductRating=0 });
description1.ProductUserReviews.Add(
new ProductReview {UserEmailId="<a href="mailto:c1@c.com",
UserComment="OK">c1@c.com",
UserComment="OK</a> PRODUCT.Can Buy",ProductRating=3.0 });
ProductDescription description2 = new ProductDescription { ManufacturerName =
"nokia", Price = 60000, ProductName = "mobile" };
description1.ProductUserReviews.Add(new ProductReview { UserEmailId =
"<a href="mailto:a2@a.com">a2@a.com</a>",
UserComment = "GOOD PRODUCT.MUST BUY", ProductRating = 5.0 });
description1.ProductUserReviews.Add(new ProductReview { UserEmailId =
"<a href="mailto:b2@b.com">b2@b.com</a>",
UserComment = "Dull PRODUCT.Dont BUY", ProductRating = 0 });
description1.ProductUserReviews.Add(new ProductReview { UserEmailId =
"<a href="mailto:c2@c.com">c2@c.com</a>",
UserComment = "OK PRODUCT.Can Buy", ProductRating = 3.0 });
product_repo.addItem(description1);
product_repo.addItem(description2);
//Customer is buying. So fist step is shopping cart created
ShoppingCart cart = new ShoppingCart(customer1);
cart_repo.addItem(cart);
ECommerceSellerSystem system = new ECommerceSellerSystem();
// customer selects a product description to buy
ShoppingCartSelection selection1 = system.AddSelectionToCustomerCart(description1, cart,1);
//system adds the selection to repository (adds to selection - viewable from cart&descriptions)
cart_selection_repo.addItem(selection1);
//customer selects to buy
ShoppingCartSelection selection2 = system.AddSelectionToCustomerCart(description2, cart,2);
//system adds selection to repository (adds to selection - viewable from cart&descriptions)
cart_selection_repo.addItem(selection2);
运行此客户端代码在 ShoppingCartSelection
中产生的結果如图 3 所示
结论
因此,这总结了 NHibernate 中多对多实体关联的编码。下一篇文章将介绍 Nhibernate 中的继承持久化。数据库处理继承的方式曾让许多花费数小时来捕捉多态设计和广泛继承层次结构的人们感到心碎,结果却发现数据库将设计简化为行、列和键。但数据库是纯粹的业务价值,在所有企业平台中都是绝对必需的。因此,NHibernate 对编码继承持久化提供了非常强大的支持。在 NHibernate 中编码继承的一种方法,我们将比其他可能的方法更常用。我们将在下一篇文章中介绍这一点。尽情享受 NHibernate。