65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 NHibernate 进行对象关系映射 (ORM) - 第 6 部分,共 8 部分 - 对实体类进行继承编码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (4投票s)

2012年11月1日

CPOL

14分钟阅读

viewsIcon

24100

一个完整的 8 部分系列文章,演示使用 NHibernate 进行一对一、多对一、多对多关联映射,使用 NHibernate 处理集合,使用 NHibernate 进行继承关系映射,以及使用 NHibernate 进行延迟初始化/获取。

引言

文章系列的前 5 部分详细介绍了表示实体类之间“拥有”关系的各种关联类型。继承表示实体类之间的“是”关系。我们都熟悉这一点,并且知道如果使用得当,继承可以带来多态行为的好处,并且基于接口的继承如果被客户端正确使用,可以提高灵活性,但如果使用不当,继承会降低灵活性。Nhibernate 中有四种不同的继承层级映射方式。我们将讨论一种能够最大程度支持继承和多态性(这是捕获继承层级的主要好处)的方式。这种方式称为“每个子类一张表”。另外请注意,在本文的示例中,我们可能会在少数地方使用 NHibernate 查询。这是不言而喻的。我们稍后将撰写一篇关于 NHibernate 查询的 3 部分系列文章,也许在此之后,还将有一篇系列文章对 NHibernate 和 LanguageIntegratedQuery(最流行和使用最广泛的库)在使用它们方面进行比较研究,前提是对此类比较研究有足够大的兴趣。

背景

在“每个子类一张表”的继承映射中,层级中的每个实体类(包括抽象类)都映射到一个表。实体类的每个属性都只映射到其自身的表。实体类之间的“是”关系通过外键约束映射到它们各自的表,即父类的 primarykey 作为外键指向所有相应的子类。通过使用“JOIN”语句(NHibernate 会为层级生成 SQL CASE 语句)来获取层级中特定类的对象。这种方案有很多优点:规范化模式(不像“每个类一张表”这样简单的方案,它为整个层级使用一个表,导致反规范化),允许使用基类进行多态调用(利用从父类表指向子类表的指向外键),等等。主要缺点是对于非常大的层级,通过 JOIN 会导致性能损失。但是,通过这种 NHibernate 技术可以在数据库表中捕获(支持多态性)完整的层级。因此,我更倾向于此而不是 NHibernate 中用于捕获继承的其他技术。

使用代码

电子商务示例场景

在看继承示例之前,需要完成电子商务示例中的订单处理,以便订单可以提供给电子商务网站进行支付,然后进行发货(我们继承的示例类)。因此,我们首先将处理订单处理,并在本文下一节中进行发货继承示例。

订单处理

客户根据购买需求选择产品描述,并将它们添加到客户的购物车中。选择完成后,客户根据购物车中的选择创建一个订单,并将其提交给电子商务网站,附带详细信息,例如快速发货还是慢速邮寄,以及是否需要礼品包装。电子商务网站将处理订单,并提供包括订单中商品发货费用在内的总金额。客户将进行支付购买。电子商务网站会将商品链接到已付款的订单,匹配客户购物车中的产品描述,然后所有这些商品将发货。在文章系列的最后一部分(第 5B 部分)中,我们完成了客户选择购物车商品的场景。商品现在作为 ShoppingCartSelection 实例的集合在购物车中。每个 ShoppingCartSelection 实例都包含客户感兴趣的 ProductDescription 和一个指向其所属 ShoppingCart 的父级引用的。现在,在本节中,我们将看到购物车中的选择如何被提交为电子商务网站的订单以及如何进一步处理。
第 4 部分图 5 中的 PaymentApprovedOrder 的构造函数代码以及到目前为止的客户端代码,显示了为测试目的订单处理是如何进行的。现在我们将对此订单处理进行改进。作为第一步,我们将从 PaymentApprovedOrder 构造函数中删除订单处理代码,并将其移至 ECommerceSellerSystem 类。PaymentApprovedOrder 的构造函数现在将只包含初始化关联和属性的代码。

ECommerceSellerSystem 类目前有一个为购物车添加 ShopSelections 的方法。现在我们将为其添加 OrderProcessing 方法。查看其中方法以及客户端代码应该能让您了解我们在做什么以及控制流。请参阅下面的 ECommerceSellerSystem 类代码。主要思想是,ShoppingCart 实例有一个 ShoppingCartSelections 的集合,其中每个都包含客户想要购买的 ProductDescription 实例。请注意,购物车选择实例拥有产品描述而不是库存项。这样做是为了避免商品在购物车中直到刷新或过期都无法提供给准备购买它们的客户的问题。这也正确地捕获了电子商务域。但是,其他部门(如发货部门)需要具体的库存项实例,而不是产品描述。

ECommerce 系统中的 Order 包含 OrderItems 集合(用于已订购的库存商品)和 NotAvailableItemDescriptions 属性(这是一个 ProductDescriptions 的集合,用于表示订单中暂时库存中不可用的产品描述)。ShoppingCart 实例及其选择集合必须首先转换为 Order 实例及其商品集合。因此,当客户想要为一个购物车下订单时,我们处理来自客户的包含 ShoppingCartSelections 的购物车(每个选择都有客户想要购买的产品描述),然后查询 ITEM 表,获取库存中与该选择的 ProductDescription 匹配的 Item,并将其添加到 OrderItems 集合中(如果找到匹配项)。如果库存中找不到 Item 实例来匹配某个 ProductDescription,我们将该 ProductDescription 添加到 Order 的 NotAvailableProductDescription 集合中。所有商品(包括不可用的产品描述)的总价值与运费一起计算并提供给客户。一旦客户付款,ECommerceSellerSystem 类将创建一个 PaymentApprovedOrder,并为特定的 PaymentApprovedOrder 标记库存 ITEM 表中的商品。库存中找不到的订单中的商品可能仍以 NotAvailableProuctDescription 集合的形式存在,并与相应的 PaymentApprovedOrderId 一起添加到单独的表中(称为 NOTAVAILABLEPAIDORDER_PRODUCTDESCRIPTIONS)。稍后将由电子商务系统处理,例如购买所需商品并将其添加到库存中,然后将其添加到相应的 PaymentApprovedOrder 中。因此,一个具有订单所有商品的 PaymentApprovedOrder 仅从购物车中存储的 ProductDescription 实例创建。请参阅下面的代码。
ECommerceSellerSystem 类的代码如下:

public class ECommerceSellerSystem
{
    public ECommerceSellerSystem()
    {
    }
    public ShoppingCartSelection AddSelectionToCustomerCart(ProductDescription product_description,ShoppingCart cart,int quantity)
    {
        return new ShoppingCartSelection(product_description, cart,quantity);              
    }
    public Order MakeOrder(ShoppingCart cart,bool is_fast,bool is_gift_wrap)
    {
        //***************************************
        //ISOLATION & SYNCHRO REQUIRED HERE LATER
        //***************************************
        //ProductDescriptions in Cart must be
        //Changed to Items in Inventory here
        //and a order must be created
        //Item availability in inventory must
        //be handled
        //Use PRODUCTDESCRIPTIONID which is a
        //FOREIGN KEY IN INVENTORY ITEM to pull 
        //items
        Order order = new Order();
        order.OrderedByCustomer = cart.CartOfCustomer;
        order.IsGiftWrapped = is_gift_wrap;
        order.IsFastShipping = is_fast;
        IRepository<Item> item_repo = new DBRepository<Item>();
        try
        {
            foreach (ShoppingCartSelection selection in cart.CartSelections)
            {
                for (int count = 0; count < selection.Quantity; count++)
                {
                    Item item = NewItemMatchingProductDescription(selection.CurrentProduct);
                    if (item != null)
                    {
                        item.IsShopCarted = true;
                        item_repo.updateItem(item);
                        order.OrderItems.Add(item);
                    }
                    //else if it is null item is unavailable
                    //add selection to not available list in orders collection
                    else
                        order.NotAvailableItemDescriptions.Add(selection.CurrentProduct);
                }
            }
        }
        catch (Exception ex)
        {
        }
        return order;
    }
    public void DestructShoppingCartBindingForItemsInOrder(Order order)
    {
        //************************************************************************
        //The IsCartedState of Item exists only when a order is made. At the exit
        //of making an order, the IsCarted state should be destroyed immaterial
        //of whether order was made or not. A item will never be allowed to be carted
        //permanently. A item can exist in inventory or in order but not in cart.SO,
        // Whenever a Order object is destroyed
        // call this method for that Order.It destroys the item bindings to cart.
        //If a Order goes out of scope, before it goes out
        // of scope, this method is called (example by using unload event handler etc). 
        //So a item will be paid or in inventory only. When a Order is made a item can be carted, but
        //by the end of order lifetime item will cease to 
        // exist in cart because we set IsShopCarted to false.
        //***************************************************************************
        try
        {
            IRepository<Item> item_repo = new DBRepository<Item>();
            foreach (Item item in order.OrderItems)
            {
                item.IsShopCarted = false;
                item_repo.updateItem(item);
            }
        }
        catch (Exception ex)
        {
        }
    }
    public Order ProcessAmountForOrder(Order order)
    {
        double total_order_amount = 0;
        //Calculate Total Payment
        foreach (Item item in order.OrderItems)
        {
            total_order_amount += item.ItemDescription.Price;
        }
        foreach (ProductDescription desc in order.NotAvailableItemDescriptions)
        {
            total_order_amount += desc.Price;
        }
        order.OrderCost = total_order_amount;
        //Calculate Shipping cost
        Shipping shipping = ProvideShippingInformation(total_order_amount, 
                               order.IsFastShipping, order.IsGiftWrapped);
        if (shipping != null)
        {
            order.ShippingCharges = shipping.ShippingCharges;
        }
        return order;
    }
    
    public void MakeOrderPayment(Order order, Payment pay)
    {
        NHibernateHelper nh = NHibernateHelper.Create();
        using (NHibernate.ITransaction transaction = nh.Session.BeginTransaction())
        {
            IRepository<PaymentApprovedOrder> pay_order_repo = 
                     new DBRepository<PaymentApprovedOrder>();
            IRepository<Shipping> shipping_repo = new DBRepository<Shipping>();

            MarkItemAsOrdered(order);
            PaymentApprovedOrder paid_order = new PaymentApprovedOrder(order, pay);
            pay_order_repo.addItem(paid_order);
            //Item will have only two states - Is Ordered or Inventory item.
            //The IsCartedState of Item exists only when a order is made. At the exit
            //of making a order, the IsCarted state should be destroyed,
            DestructShoppingCartBindingForItemsInOrder(order);
            Shipping shipping = ProvideShippingInformation(order.OrderCost, order.IsFastShipping, order.IsGiftWrapped);
            shipping.ShippingCharges = order.ShippingCharges;
            shipping_repo.addItem(shipping);
            transaction.Commit();
        }
    }
    public Shipping ProvideShippingInformation(double order_amount,
                    bool is_fast_shipping,bool is_gift_wrapped)
    {
        //Important thing to note here is PaymentApprovedOrder is already saved in db
        //no need to process a transaction to envelope both

        Shipping shipping = null;
        if (!is_fast_shipping)
        {
            shipping = new ParcelService(is_gift_wrapped);
        }
        else
        {
            //Fast Shipping - same day delivery - no time for gift wraps
            //So only the latest date by which item should be sent - i.e today
            //which is same day as order day
            shipping = new FastShipping(DateTime.Today);
        }

        shipping.CalculateShippingCharges(order_amount);
        //MostImportant thing is Shipping is abstract super class - 
        //having two concrete child classes
        //We did not tell NHibernate which child class we are dealing with

        return shipping;
    }

    public Item NewItemMatchingProductDescription(ProductDescription product_description)
    {
        Item item = null;
        try
        {
            //create nhibernatehelper for query
            NHibernateHelper nh = NHibernateHelper.Create();
            string query_string = "from Item item1 where item1.ItemDescription = :description" +
            " and item1.IsOrdered = :ordered_truth_value and item1.IsShopCarted = :karted_truth_value";
            IList<Item> items_list = nh.Session.CreateQuery(query_string).
            SetParameter<ProductDescription>("description", product_description).
            SetParameter<bool>("ordered_truth_value", false).
            SetParameter<bool>("karted_truth_value",false).List<Item>();
            if (items_list != null && items_list.Count > 0)
            {
                item = items_list[0];
            }
        }
        catch (Exception ex)
        {
        }
        return item;
    }
    public void MarkItemAsOrdered(Order order)
    {
        //Isolation Control Required
        try
        {          
            foreach (Item item in order.OrderItems)
            {
                item.IsOrdered = true;
            }         
        }
        catch (Exception ex)
        {
        }
    }
}

PaymentApprocedOrder 的代码如下:

public class PaymentApprovedOrder
{
    public PaymentApprovedOrder()
    {
        //NOTE: THIS LIST IS MAPPED TO IDBAG. SO NO ORDERING.
        PaidOrderItems = new List<Item>();
    }
    public PaymentApprovedOrder(Order order,Payment pay)
    {
        //NOTE: THIS LIST IS MAPPED TO IDBAG. SO NO ORDERING.
        PaidOrderItems = new List<Item>();
        CurrentOrder = order;

        // SET ONE END OF ASSOCIATION
        OrderPayment = pay;

        // SET OTHER END OF ASSOCIATION
        pay.PaidOrder = this;
        // Set the Customer BIDEIRECTIONAL ASSOCIATION
        order.OrderedByCustomer.AddPaidOrder(this);
        
        foreach (Item item in order.OrderItems)
        {
            //SET THE ORDER ITEMS 
            // THE ORDER USED IN PAYMENTAPPROVEDORDER HERE
            //CONTAINS ORDERITEMS
            AddPaidItem(item);
        }
 
        this.NotAvailableItemDescriptions = order.NotAvailableItemDescriptions;
    }
 
    public virtual long PaymentApprovedOrderId { get; set; }

    public virtual Order CurrentOrder { get; set; }

    // NOTE: BIDIRECTIONAL ASSOCIATION WITH "PAYMENT"

    public virtual Payment OrderPayment { get; set; }

    //NOTE: BIDIRECTIONAL ONE-TO-MANY ASSOCIATION WITH ITEM
    //NOTE: THIS LIST IS MAPPED TO IDBAG. SO NO ORDERING.

    public virtual IList<Item> PaidOrderItems { get; set; }
 
    //NOTE: BIDIRECTIONAL MANY-TO-ONE ASSOCIATION WITH CUSTOMER
    public virtual Customer PaidByCustomer { get; set; }
    public virtual Iesi.Collections.Generic.ISet<OrderShipment> ShipmentsOfThisPaidOrder { get; set; }
    public virtual System.Collections.Generic.IList<ProductDescription> NotAvailableItemDescriptions { get; set; }
    public virtual void AddPaidItem(Item item)
    {
        //SET THE REFERENCE FOR PAYMENTAPPROVEDORDER
        //IN ITEM OBJECT TO THIS (THE PAYMENT APPROVED
        //ORDER INSTANCE TO WHICH THE item IS ADDED")
        // THIS IS FIRST END OF THE
        // ONE-TO-MANY ASSOCIATION - THE "ONE" END
        item.PaidOrder = this;
        //ADD "item" TO THE SET PaidOrderItems
        //OTHER END OF ASSOCIATION - THE "MANY" END
        PaidOrderItems.Add(item);
    }
}

PaymentApprovedOrder 的映射代码如下:

<class name="PaymentApprovedOrder" table="PAYMENTAPPROVEDORDER">
    <id name ="PaymentApprovedOrderId" type ="long" 
       column ="PAYMENTAPPROVEDORDERID" generator="native"/>
    
    <many-to-one name ="OrderPayment" lazy="false" 
       class ="Payment" column="PAYMENTID" 
       unique="true" not-null="true" cascade="save-update"/>
    <many-to-one name ="PaidByCustomer" class ="Customer" 
       column ="CUSTOMERID" not-null="true" insert="false" 
       update="false" cascade ="save-update"></many-to-one>
 
 
    <idbag name="PaidOrderItems" table="PAYMENTAPPROVEDORDER_ITEMS" cascade="save-update">
      <collection-id column="JoinTableRowId" type="long">
        <generator class="identity" />
      </collection-id>
      <key column="PAYMENTAPPROVEDORDERID"></key> 
      <many-to-many class="Item" column="ITEMID" unique="true"/>
    </idbag>
 
    <idbag name="NotAvailableItemDescriptions" table="NOTAVAILABLEPAIDORDER_PRODUCTDESCRIPTIONS" cascade="save-update">
      <collection-id column="NOTAVAILABLEPRODUCTSDESCRIPTIONID" type="long">
        <generator class="identity" />
      </collection-id>
      <key column ="PAYMENTAPPROVEDORDERID"></key>
      <many-to-many class ="ProductDescription" column="PRODUCTDESCRIPTIONID"/>
    </idbag>
    
</class>

Item 类的代码如下:

public class Item
{
    public Item() 
    { 
        IsOrdered = false;
        IsShopCarted = false;
        PaidOrder = null;
    }
    public virtual long ItemId { get; set; }
    public virtual bool IsOrdered { get; set; }
    public virtual string InventorySerialCode { get; set; }
    public virtual PaymentApprovedOrder PaidOrder { get; set; }
    public virtual ProductDescription ItemDescription { get; set; }
    public virtual bool IsShopCarted { get; set; }
}

Item 类的映射代码如下:

<class name ="Item" table ="ITEM">
    <id name ="ItemId" column ="ITEMID" type ="long" generator="native" />
    <property name="IsOrdered" type="bool" column="ISORDERED"/>
    <property name ="IsShopCarted" type ="bool" column ="ISSHOPCARTED" />
    <property name ="InventorySerialCode" type="string" column ="INVENTORYSERIALCODE"></property>
    <many-to-one name="ItemDescription" class="ProductDescription" 
       column="PRODUCTDESCRIPTIONID" not-null="true" lazy="false"></many-to-one>
    <join optional="true" inverse="true" table ="PAYMENTAPPROVEDORDER_ITEMS">
      <key column ="ITEMID" unique="true" not-null="true"/>
      <many-to-one  class ="PaymentApprovedOrder" name ="PaidOrder" column ="PAYMENTAPPROVEDORDERID"/>   
    </join>
</class>

ProductDescription 类的代码如下:

public class ProductDescription
{
    public ProductDescription()
    {
        ProductUserReviews = new List<ProductReview>();
        CartSelectionsWithThisProduct = new HashedSet<ShoppingCartSelection>();        
    }
    public virtual long ProductDescriptionId { get; set; }
    public virtual string ProductName { get; set; }
    public virtual string ManufacturerName { get; set; }
    public virtual double Price { get; set; }
    public virtual IList<ProductReview> ProductUserReviews{ get; set; }
    public virtual Iesi.Collections.Generic.ISet<ShoppingCartSelection> CartSelectionsWithThisProduct { get; set; }      
}

ProductDescription 的映射代码如下:

<class name="ProductDescription" table ="PRODUCTDESCRIPTION" >
    
    <id name ="ProductDescriptionId" type ="long" 
       column ="PRODUCTDESCRIPTIONID" generator="native"></id>
    
    <property name ="ProductName" type ="string" column ="PRODUCTNAME" />
    <property name ="ManufacturerName" type ="string" column ="MANUFACTURERNAME" />
    <property name ="Price" type ="double" column ="PRICE" />
    
    <list table="PRODUCTREVIEWS" name="ProductUserReviews">
      
      <key column="PRODUCTDESCRIPTIONID" not-null="true"></key>
      
      <list-index column="REVIEWLIST_POSITON"></list-index>
      
      <composite-element class="ProductReview">
        <property name="UserComment" type="string" column="USERCOMMENT"/>
        <property name="UserEmailId" type="string" column="USEREMAILID"/>
        <property name="ProductRating" type="double" column="USERRATING"/>
      </composite-element>      
    </list>
 
 
    <set name="CartSelectionsWithThisProduct" inverse="true">
      <key column ="PRODUCTDESCRIPTIONID"></key>
      <one-to-many class ="ShoppingCartSelection"/>      
    </set>
 
</class>

ECommerceSellerSystem 类和 Item 类中有一个重要的注意事项。ITEM 表和 Item 类有一个名为 IsShopCarted 的标志。当处理订单并查询到 Item 实例以表示特定项已附加到购物车时,此标志会短暂设置为 true。一旦订单付款或 Order 实例即将销毁或 Order 实例即将超出范围等,此标志必须重置为 false(通常在由 Order 实例销毁的原因触发的事件处理程序中)。因此,此标志为 true 的生命周期仅限于处理订单的时间。可以通过方法“DestructShoppingCartBindingsForItemInOrder”(稍后也可以为 Order 中的每个 Item 执行类似的方法)重置 Order 的此标志。当 IsShopCarted 标志和 IsOrdered 标志都设置为 False 时,Item 就可用于库存订购。如果 IsShopCarted 标志设置为 true,则 Item 不可用,并且位于正在处理订单的 ShoppingKart 中,但该标志的重置为 true 或 false 与 OrderProcessing 的成功或失败无关。当 Item 被购买时,IsOrdered 标志设置为 True。Item 对象只是处于不同的状态:Item 在库存中、Item 在购物车中、Item 已付款订单中、Item 在发货中。仅 IsShopCartedIsOrdered 标志就足以满足此示例。我们将在下一篇文章中以其他方式处理 Item IsInShipping 的状态,而无需设置任何标志。但在此之前,我们需要看看系统是如何处理发货的。下面的代码显示了客户端测试代码。

IRepository<Item> items_repo = new DBRepository<Item>();
IRepository<ProductDescription> product_repo = new DBRepository<ProductDescription>();
IRepository<Customer> customer_repo = new DBRepository<Customer>();
IRepository<ShoppingCartSelection> cart_selection_repo = new DBRepository<ShoppingCartSelection>();
IRepository<ShoppingCart> cart_repo = new DBRepository<ShoppingCart>();


//Create 2 product descriptions

ProductDescription description1 = new ProductDescription { ManufacturerName = "samsung", Price = 60000, ProductName = "mobile" };
description1.ProductUserReviews.Add(new ProductReview { UserEmailId = 
  "<a href="mailto:a1@a.com">a1@a.com</a>", 
  UserComment = "GOOD 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">c1@c.com</a>", 
  UserComment = "OK PRODUCT.Can Buy", ProductRating = 3.0 });

ProductDescription description2 = new ProductDescription { ManufacturerName = 
  "nokia", Price = 70000, 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);  

//CREATE 7 NEW ITEMS

Item[] items = new Item[7];
items[0] = new Item { InventorySerialCode = "00A0110",ItemDescription = description1 };
items[1] = new Item { InventorySerialCode = "01A0101", ItemDescription = description1 };
items[2] = new Item { InventorySerialCode = "02A10101", ItemDescription = description1 };
items[3] = new Item { InventorySerialCode = "03A01010", ItemDescription = description1 };
items[4] = new Item { InventorySerialCode = "04A101010", ItemDescription = description1 };
items[5] = new Item { InventorySerialCode = "05A010101", ItemDescription = description1 };
items[6] = new Item { InventorySerialCode = "06A0100100", ItemDescription = description1 };

//ADD ALL ITEMS TO REPSITORY
//ITEMS ADDED HAVE SERIAL CODE 00--- to 06
//ALL THESE ITEMS WILL HAVE NULL FOR PAYMENTAPPROVEDORDER REFERENCE
//BECAUSE THEY EXIST BEFORE IN INVENTORY AND NOT BOUGHT
for (int counter = 0; counter < items.Length; counter++)
{
    items_repo.addItem(items[counter]);
}
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 added to repository
customer_repo.addItem(customer1);

//Extra customers added to repository - these have no carts 
customer_repo.addItem(customer2);

customer_repo.addItem(customer3);

//Customer is buying. So fist step is shopping cart created
//if none exists before.
ShoppingCart cart1 = new ShoppingCart(customer1);
cart_repo.addItem(cart1);
ShoppingCart cart2 = new ShoppingCart(customer2);
cart_repo.addItem(cart2);
ECommerceSellerSystem system = new ECommerceSellerSystem();
// customer selects a product description to buy
ShoppingCartSelection selection1 = system.AddSelectionToCustomerCart(description1, cart1, 23);
//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, cart2, 2);
//system adds selection to repository (adds to selection - viewable from cart&descriptions)
cart_selection_repo.addItem(selection2);

ECommerceSellerSystem seller_system1 = new ECommerceSellerSystem();
// Customer makes a order out of his cart - Selects items he needs to buy
//Fast Shipping cannot be gift wrapped is a constraint to be handled in Presentationlayer
Order order1 = seller_system1.MakeOrder(cart1,true,false);
//Customer wants to process the order to know the payment
order1 = seller_system1.ProcessAmountForOrder(order1);
//Customer Makes a payment and ORDERS the items
Payment payment1 = new Payment { PaymentAmount=(order1.OrderCost+order1.ShippingCharges) };
seller_system1.MakeOrderPayment(order1, payment1);
//The previous order is added to customer list of paid orders

ECommerceSellerSystem seller_system2 = new ECommerceSellerSystem();
// Customer makes a order out of his cart - Selects items he needs to buy
//Fast Shipping cannot be gift wrapped is a constraint to be handled in Presentationlayer
Order order2 = seller_system2.MakeOrder(cart2,false,true);
//Customer wants to process the order to know the payment
order2 = seller_system2.ProcessAmountForOrder(order2);
//Customer Makes a payment and ORDERS the items
Payment payment2 = new Payment { PaymentAmount = (order2.OrderCost+order2.ShippingCharges) };
seller_system2.MakeOrderPayment(order2, payment2);

在结束本次讨论之前,有一个有趣的观察。ECommerceSellerSystem 类几乎像一个外观(它的方法可以进一步分解以使其成为一个合适的外观),并且它不是一个单例类。它处理客户的购物车选择,管理购物车,进行订单处理,创建已付款订单,接下来还将进行发货处理。为了实现这一点,它使用了一组领域类,这些类除了少数可以移除的静态变量外,没有其他静态变量。上面的几行话可能会让敏锐的 WCF 开发人员想到将此类作为每个调用实例化的服务打开的可能性。更好的方法是采用适当的接口分解,以便订单处理、发货、购物车选择处理的每个功能都被分解到不同的接口中,并以面向服务的方式提供。这从一开始就是我们想法的重点,并且可以通过努力和改进来实现。接下来,我们将看到在 Shipping 已创建 Order 的继承示例。类中的 MakeOrderPayment 方法。

映射继承示例

电子商务系统中整个发货场景主要包含一个 Shipping 类(抽象类),该类包含发货所需的所有信息(发货公司名称等)。它有两个具体的子类,我们将在下一段中讨论。我们提供了灵活性,即一个 PaidOrder(PaymentApprovedOrder 的实例)可以分多次发货,即多次发货。同样,一个 Shipping 实例可以有多个 PaymentApprovedOrders,如果它们是由同一客户创建的。因此,Shipping 和 PaymentApprovedOrder 之间存在一个多对多双向关联,关联类是 OrderShipment。每个 OrderShipment 将有一个 Items 集合。此集合是客户订购的商品的一部分,应在该 OrderShipment 的特定实例中发货。请记住第 5A 部分中的关联类 Ticket 及其乘客集合——这里使用相同的方法,但要发货的商品填充方式不同,将在下一篇文章中展示,并且一个 OrderShipment 可能只包含部分订购的商品(其余商品可能在不同的发货中发送,因此归入不同的 OrderShipment)。因此,目前我们捕获 Shipping 以及 Shipping 和 PaymentApprovedOrder 之间的关联类 OrderShipment。Shipping 类代码如下:

public abstract class Shipping
{
    public virtual long ShippingId { get; set; }
    public virtual string ShippingName { get; set; }
    public virtual DateTime? ShippingStartDate { get; set; }
    public virtual ISet<OrderShipment> ShipmentsInThisShipping { get; set; }
    public virtual double ShippingCharges { get; set; }
    public abstract void CalculateShippingCharges(double total_amount);
}

OrderShipment 类代码如下:

public class OrderShipment
{
    public OrderShipment()
    {
        //SHIPMENT ITEMS - DONT SET HERE - Just allocate memory only
        ShipmentItems = new List<ShipmentItem>();
    }
    public OrderShipment(PaymentApprovedOrder paid_order, Shipping shipping)
    {
            OrderShipmentId = new CompositeKey();
            //Set the Composite Key
            OrderShipmentId.class1Key = paid_order.PaymentApprovedOrderId;
            OrderShipmentId.class2Key = shipping.ShippingId;
            //set shipments
            ShipmentItems = new List<ShipmentItem>();
            //Set Both sides of the bidirectional association
            paid_order.ShipmentsOfThisPaidOrder.Add(this);
            shipping.ShipmentsInThisShipping.Add(this);
            //other ends
            CurrentPaidOrder = paid_order;
            CurrentShipping = shipping;
    }
    public virtual CompositeKey OrderShipmentId { get; set; }
    public virtual PaymentApprovedOrder CurrentPaidOrder { get; set; }
    public virtual Shipping CurrentShipping { get; set; }
    public virtual IList<ShipmentItem> ShipmentItems { get; set; }
}

Order Shipment 的映射如下:

<class name="OrderShipment" table="ORDERSHIPMENT" >
    
    <composite-id name ="OrderShipmentId" class ="CompositeKey">
      <key-property name ="class1Key" column ="PAYMENTAPPROVEDORDERID" 
         type ="long" access="field"></key-property>
      <key-property name ="class2Key" column ="SHIPPINGID" 
         type ="long" access="field"></key-property>
    </composite-id>
    
    <many-to-one class="PaymentApprovedOrder" name="CurrentPaidOrder" 
      column="PAYMENTAPPROVEDORDERID" insert="false" update="false"></many-to-one>
    
    <many-to-one class="Shipping" name="CurrentShipping" 
      column="SHIPPINGID" insert="false" update="false"></many-to-one>
    
    <list name ="ShipmentItems" table ="SHIPMENTITEMS" cascade="save-update">
      <key not-null="true">
        <column name ="PAYMENTAPPROVEDORDERID" />
        <column name ="SHIPPINGID" />
      </key>
      <list-index column="POSITION" />
      <one-to-many class ="ShipmentItem"/>
    </list>
</class>

Shipping 抽象类有两个具体的子类:FastShippingParcelServiceFastShipping 是 Shipping 的一种特殊形式,提供当日发货和更快的交付,需要额外费用。ParcelService 仅传输商品,如果需要则提供礼品包装。FastShipping 不提供礼品包装,因为我们的高效电子商务网站在订单需要快速发货时会立即行动,没有时间进行礼品包装。请看下面的图 1。它显示了 Shipping 的抽象父类和 2 个子类及其映射代码。阅读图中解释的注释。

图 1

为了了解 NHibernate 继承映射的强大功能,我们将看一下 ECommerceSellerSystem 类中的 MakeOrderPaymentProvideShippingInformation 方法。有趣之处在于 Shipping 实例的创建和持久化到数据库的方式。Shipping 实例在 ProvideShippingInformation 方法中创建,在 MakeOrderPayment 方法中使用,并持久化到数据库。请看下面的代码。

public void MakeOrderPayment(Order order, Payment pay)
{
    NHibernateHelper nh = NHibernateHelper.Create();
    using (NHibernate.ITransaction transaction = nh.Session.BeginTransaction())
    {
        IRepository<PaymentApprovedOrder> pay_order_repo = new DBRepository<PaymentApprovedOrder>();
        IRepository<Shipping> shipping_repo = new DBRepository<Shipping>();

        MarkItemAsOrdered(order);
        PaymentApprovedOrder paid_order = new PaymentApprovedOrder(order, pay);
        pay_order_repo.addItem(paid_order);
        //Item will have only two states - Is Ordered or Inventory item.
        //The IsCartedState of Item exists only when a order is made. At the exit
        //of making a order, the IsCarted state should be destroyed,
        DestructShoppingCartBindingForItemsInOrder(order);
        Shipping shipping = ProvideShippingInformation(order.OrderCost, order.IsFastShipping, order.IsGiftWrapped);
        shipping.ShippingCharges = order.ShippingCharges;
        shipping_repo.addItem(shipping);
        transaction.Commit();
    }
}

public Shipping ProvideShippingInformation(double order_amount,bool is_fast_shipping,bool is_gift_wrapped)
{
    //Important thing to note here is PaymentApprovedOrder is already saved in db
    //no need to process a transaction to envelope both
    Shipping shipping = null;
    if (!is_fast_shipping)
    {
        shipping = new ParcelService(is_gift_wrapped);
    }
    else
    {
        //Fast Shipping - same day delivery - no time for gift wraps
        //So only the latest date by which item should be sent - i.e today
        //which is same day as order day
        shipping = new FastShipping(DateTime.Today);
    }
    shipping.CalculateShippingCharges(order_amount);
    //MostImportant thing is Shipping is abstract super class - 
    //having two concrete child classes
    //We did not tell NHibernate which child class we are dealing with
    return shipping;
}

请查看 ProvideShippingnformation 方法。它根据 Order 参数中提供的信息创建 FastShippingParcelService 的具体实例,并将其存储在 Shipping 的基类引用中。此相同的基类引用随后用于通过对 CalculateShippingCharges 方法的多态调用来计算 Order 的发货费用,该方法在 Shipping 的两个具体类,即 FastShippingParcelService 中被重写(每个类都有不同的发货费用计算方式——快速发货费用更高)。然后将其作为包含具体子类实例的基类引用返回给调用者 - "MakeOrderPayment"。最令人感兴趣的是,在调用者中,这个存储具体子类实例(FastShippingParcelService)的抽象基类引用(Shipping)与 NHibernate 一起用于持久化。您无需告诉 NHibernate 要存储哪个具体实例等。这一切都由 NHibernate 处理(尽管如果您稍后查看图 2 中表的结构,可以清楚地理解 NHibernate 持久化的内部工作原理)。抽象 Shipping 类的具体子类,即 FastShippingParcelService,如下所示(请注意,它们没有身份值属性——稍后会详细介绍)。

public class FastShipping:Shipping
{
    public FastShipping()
    {
ShipmentsInThisShipping = new Iesi.Collections.Generic.HashedSet<OrderShipment>();
    }
    public FastShipping(string docket, DateTime delivety_date)
    {
ShipmentsInThisShipping = new Iesi.Collections.Generic.HashedSet<OrderShipment>();
        TrackingNumber = docket;
        DeliveryDate = DeliveryDate;
    }
    public FastShipping(DateTime start_latest_by)
    {
ShipmentsInThisShipping = new Iesi.Collections.Generic.HashedSet<OrderShipment>();
        StartDeliveryLatestBy = start_latest_by;
    }
    public virtual int STANDARDRATE { get; set; } 
    public virtual string TrackingNumber { get; set; }
    public virtual DateTime? DeliveryDate { get; set; }
    public virtual DateTime? StartDeliveryLatestBy { get; set; }
    public override void CalculateShippingCharges(double total_amount)
    {
        ShippingCharges =((.1) * total_amount) + STANDARDRATE;    
    }
}

public class ParcelService:Shipping
{
    public ParcelService()
    {
ShipmentsInThisShipping = new Iesi.Collections.Generic.HashedSet<OrderShipment>();
    }
    public ParcelService(bool is_gift_wrapped)
    {
ShipmentsInThisShipping = new Iesi.Collections.Generic.HashedSet<OrderShipment>();
        IsGiftWrapped = is_gift_wrapped;
    }
    public virtual int STANDARDRATE { get; set; }
    public virtual string ParcelRegistrationNumber { get; set; }
    public virtual bool IsGiftWrapped { get; set; }
    public override void CalculateShippingCharges(double total_amount)
    {
        ShippingCharges = ((.02) * total_amount) + STANDARDRATE;            
    }
}

那么,当处理前面测试代码的订单时,NHibernate 在发货表中会形成哪些行?请参考图 2。请注意,这是客户在网站下单时发货表的快照。客户只能选择快速发货或带礼品包装的邮寄服务。因此,如果客户想要 FastShipping 服务,则会在超类表 SHIPPING 中创建一个行,其主键会作为外键插入到 FASTSHIPPING 表的行中;如果客户想要 ParcelService 服务,则会插入到 PARCELSERVICE 表的行中。除了子类信息(如 ParcelService 的礼品包装和 FastShipping 的 startdeliveryby 日期)外,还会填充费用(用于向发货部门发送通知)。客户无法指定发货的快递公司(SHIPPING TABLE 中的 SHIPPINGNAME 列)或其他发货详情。这些信息由网站发货部门输入。因此,除非他们开始发货并输入发货信息,否则所有这些值都将为 null,如图 2 所示。为避免 null,您可以使用默认值,例如“待填写”。请参考图 2。

图 2

Shipping 及其子类的映射文件如下所示。在此映射文件中最重要的注意事项是,对于子类,即 FastShippingParcelService,主键值不生成(即 FASTSHIPPINGID 和 PARCELSERVICEID)。相反,它是一个从 SHIPPING TABLE(基类)指向的外键值,并在映射文件中使用 <key column=".."> 标签声明。请参阅图 2 中的箭头和映射代码进行理解。还要查看 FastShipping 子类和 ParcelService 子类的 C# 代码。它们没有 FastShippingIdParcelServiceId 属性,不像我们为所有实体类那样。这是因为 FASTSHIPPINGID 和 PARCELSERVICEID 列都是 NHibernate 自动从基类通过映射文件中给出的信息(<key column="...">)生成的,而在使用 <joined-subclass> 标签声明子类时。NHibernate 使用从基类到子类的外键来捕获“每个子类一张表”方法中的“是”继承关系。

<class name="Shipping" table="SHIPPING">
    <id name ="ShippingId" column ="SHIPPINGID" generator ="native" />
    
    <property name="ShippingName" column="SHIPPINGNAME" type="string" />
    
    <property name="ShippingStartDate" column="SHIPPINGSTARTDATE" type="DateTime" />
 
 
    <property name="ShippingCharges" column="SHIPPINGCHARGES" type="double" />
 
    <set name="ShipmentsInThisShipping" table="ORDERSHIPMENT">
      <key column ="SHIPPINGID" />
      <one-to-many class ="OrderShipment"/>
    </set>

    <joined-subclass name="FastShipping" table="FASTSHIPPING">
      <key column="FASTSHIPPINGID"/>
      <property name ="TrackingNumber" type ="string" column ="TRACKINGNUMBER" />
      <property name ="StartDeliveryLatestBy" type ="DateTime" column ="STARTDELIVERYBY" />
      <property name ="DeliveryDate" type ="DateTime" column ="DELIVERYDATE" />
    </joined-subclass>
 
    <joined-subclass name="ParcelService" table="PARCELSERVICE">
      <key column="PARCELSERVICEID"/>
      <property name ="ParcelRegistrationNumber" type ="string" column ="PARCELREGISTRATIONNUMBER" />
      <property name ="IsGiftWrapped" type ="bool" column ="ISGIFTWRAPPED" />
    </joined-subclass>
</class>

结论

完整的继承层级映射和多态支持是 NHibernate 的“每个子类一张表”方法的优势。对于非常大的层级,SQL 语句中的 JOIN 可能会导致性能损失。在下一篇文章(第 7 部分)中,我们将通过允许网站输入发货信息,并以一种不同的、有用的方式(捕获 Item 在组织中的移动,即从库存到发货)向 OrderShipment 添加 Item 集合来完成发货处理。享受 NHibernate。

© . All rights reserved.