Click here to Skip to main content
15,867,756 members
Articles / NHibernate

Object Relational Mapping (ORM) using NHibernate - Part 6 of 8 - Coding Inheritance Of Entity Classes

Rate me:
Please Sign up or sign in to vote.
4.92/5 (4 votes)
22 Nov 2012CPOL14 min read 23.5K   15  
A full series of 8 part articles to show One-To-One, Many-To-One, Many-To-Many associations mapping using NHibernate, using collections with NHibernate, inheritance relationships using NHibernate, lazy initializations/fetches using NHibernate.

Introduction

Parts 1-5 of the article series detailed on different types of association which represents the "has a" relationship between entity classes. Inheritance represents the "is a" relationship between entity classes. We are all familiar with this and also know if used correctly Inheritance can yield the benefit of polymorphic behaviour and that an interface based inheritance if used correctly in clients can increase flexibility, but if used wrongly, inheritance reduces flexibility. There are four different ways of mapping Inheritance hierarchies in Nhibernate. We will discuss the one way which gives maximum support for inheritance and polymorphism (the main benefit of capturing an inheritance heirarchy). This is called Table Per Subclass. Also note that there may be a few places where we would have used NHibernate queries in the samples for this article. It is self explanatory. We will be writing a 3 part article series on NHibernate queries later and perhaps subsequent to that, another article series that does a comparitive study between Nhibernate and LanguageIntegratedQuery (most popular and extensively used library) on using them, if its known that there might be sufficient interests in such a comparison study.

Background

In Table Per Subclass inheritance mapping, each and every entity class of the hierarchy (including abstract class) is mapped to a table. Every property of a entity class is mapped to its own table only. The "is a" relationship between the entity classes is mapped to their respective tables by using the foreignkey constraint i,e The pimarykey of the superclass is posted as a foreignkey to all the respective child classes. Objects of a particular class in the hierarchy is fetched by using the "JOIN" statments (nhibernate generates sql case statements for the hierarchy). The advantages of this scheme are many: Normalized schema (unlike the other simpler schemes like Table Per Class hierarchy which uses one table for the entire hierarchy resulting in denormalization), allows polymorphic call using base class (the foreignkey posted from baseclass table to child class table is used), etc. The main disadvantage is performance loss by joins for very large hierarchies. But the full hierarchy (with polymorphic support) can be captured in database tables using this technique in Nhibernate. Hence i prefer this over other techniques used to capture inheritance in Nhibernate.

Using the code

E-COMMERCE SAMPLE SCENARIO

Before looking at the sample for inheritance, the OrderProcessing of the ecommerce example needs to be completed so that a order is made available to ecommerce site for Payment and then Shipping (our sample class for inheritance). So first we will work on orderprocessing and do the Shipping inheritance sample here in the next section of this same article.

ORDER PROCESSING

The product description matching customer's buying needs are selected by the customer for buying and are added to the shoppingcart of the customer. After the selections are over, the customer makes an Order out of the selections in the shoppingcart and submits it to the ecommerce site along with details like fast shipping or slow parcel service and whether gift packing is required. The ecommerce site will in turn process the Order and provide the total amount to be paid including amount for shipping the items in Order. The customer will make a payment and buy. Ecommerce Site will link the items to the paid order matching the product descriptions in the shopping cart of customer and all these items will be shipped afterwards. In the last part of the article series (PART 5 B) we completed the scenario of Customer selecting items for shoppingcart. Items are now in ShoppingCart as a collection of ShoppingCartSelection instances. Each instance of ShoppingCartSelection contains a ProductDescription which is of interest to the Customer and a reference to the parent ShoppingCart that this ShoppingCartSelection belongs to. Now in this section we will see how the selections in a shoppingcart is submitted as order to the ecommerce site and how it is processed further.
The constructior code of PaymentApprovedOrder in Article Part 4 - Figure 5 and the client code so far, shows how order processing is done currently for testing purpose. Now here we will refine this order processing. As a first step towards it, we will remove the order processing code from PaymentApprovedOrder constructor and move it to ECommerceSellerSystem class. The constructor for PaymentApprovedOrder will now contain only code for initializing associations and properties.

The ECommerceSellerSystem class currently has a method for adding ShopSelections to Shopping Cart. Now we are adding OrderProcessing methods to it. A look at the methods in it with the client code should give the idea of what we are doing and the control flow. Please refer to the ECommerceSellerSystem class code given below. The main idea is that the ShoppingCart instance has a collection of ShoppingCartSelections each of which contains a particular ProductDescription instance that the Customer wants to buy. Note that the cart selection instance has a product description and not inventory item. This is done as told before to avoid the problem of a item being in a cart until its flushed or expired and not being available to customers ready to purchase it. Also this captures the e-commerce domain correctly. But other departments like shipping department needs concrete Item instances of inventory and not ProductDescriptions.

A Order in the ECommerce system contains OrderItems collection for items in inventory that is ordered and NotAvailableItemDescriptions property in it is a collection of ProductDescriptions to represent the product descriptions in Order for which a item is not presently available in Inventory. The ShoppingCart instance with collection of selections has to be converted to Order instance with collection of items first. So when customer wants to make a order for his shoppingcart, we process the ShoppingCart from customer containing ShoppingCartSelections (each selection has a product description that cutomer wants to buy) and query the ITEM table and obtain a Item in inventory matching that selection's ProductDescription and add it to OrderItems collection, if the match is found for a particular ProductDescription. If the match is not found in Inventory to get a Item instance for a particular ProductDescription, we add the ProductDescription itself to NotAvailableProductDescription collection of Order. Total Value of all items including not available product descriptions are calculated along with shipping charges and provided to customer. Once Customer makes a Payment, a PaymentApprovedOrder is created by ECommerceSellerSystem class along with items for a particular PaymentApprovedOrder marked so in inventory ITEM table. Items in a Order which is not found in inventory may still exist as NotAvailableProuctDescription collection and are added to a separate table (called NOTAVAILABLEPAIDORDER_PRODUCTDESCRIPTIONS) with the corresponding PaymentApprovedOrderId. They will be later processed by the e-commerce system like purchasing the required item and adding it to inventory and then adding it to the corresponding PaymentApprovedOrder. Thus a PaymentApprovedOrder with all items for the order is created only from ProductDescription instances stored in a cart. Refer to the code below.
The code for ECommerceSellerSystem class is:

C#
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)
        {
        }
    }
}

The Code for PaymentApprocedOrder is:

C#
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);
    }
}

The mapping code for PaymentApprovedOrder is:

XML
<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>

The code for Item class is:

C#
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; }
}

The mapping code for Item class is:

XML
<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>

The code for ProductDescription class is:

C#
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; }      
}

The mapping code for ProductDescription is:

XML
<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>

There is a important point to note here in ECommerceSellerSystem class and Item class. The ITEM table and Item class has a flag called IsShopCarted. This flag is set briefly when a order is processed and Item instance is queried and obtained for a ProductDescription to show that the particular item is now attached to a ShoppingCart. The moment the Order is paid or Order Instance is going to be destroyed or Order is going to go out of scope etc this flag must be reset to false (mostly inside a event handler that is triggered by the cause for the destruction of Order instance) wherever such a thing might happen. So the lifetime of this flag to be in true is restricted to the time a Order is processed. This flag can be reset for an Order by using the method - "DestructShoppingCartBindingsForItemInOrder" (even a similar method can be done for each Item in Order later). When both IsShopCarted flag and IsOrdered flag is set to False then an Item is available in inventory for order. If IsShopCarted flag is set, an item is not available and is in a ShoppingKart which is currently undergoing Order processing but the flag will be reset to true or false immaterial of OrderProcessing is success or failure. Is Ordered flag is set to True when item is bought. The Item object is simply in different states: Item is in Inventory, Item is in Cart, Item is in PaidOrder, Item is in Shipping. Just the flags IsShopCarted and IsOrdered will do for this sample. We will handle the state of Item IsInShipping a different way in the next article without any flag setting. But before that we need to look at how shipping is handled by the system. The client test code is shown below:

C#
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);

Before wrapping up this discussion, there is an interesting observation. The class ECommerceSellerSystem functions almost like a facade (its methods can be factored further to make it a proper facade) and it is not a Singleton class. It processes the customer's shoping cart selection, manages shopping cart, does order processing, and creates payment approved order and next will be doing shipping processing also. To accomplish this it uses a set of domain classes which has no static variable except for one or two which can be removed. The above lines would get the wheels turning for astute WCF developers for the possibility of opening this class has a percall instanced service. Better still employ proper interface factoring so that each of the function of order processing, shipping, shop selection handling are factored into different interfaces and be made available with service orientation. That was the drift of the ideas right from start and could be achieved with efforts and improvements. Next we will see the inheritance example with Shipping having created a Order. The MakeOrderPayment method in the class.

Mapping inheritance example

The entire Shipping scenario in ECommerce system primarily consists of a Shipping class (abstract class) that has all the information for shipping the product (shipping firm name etc). It has two concrete child classes which we discuss in next paragraph. We provide flexibility that a PaidOrder (instance of PaymentApprovedOrder) can be shipped in many parts i,e in many shippings. Also a Shipping instance can have many PaymentApprovedOrders in it if it is made by the same customer. So a many-to-many bidirectional association exists between Shipping and PaymentApprovedOrder and the association class is OrderShipment. Each OrderShipment will have a collection of Items. This collection is part ofthe items ordered by customer, which should be shipped in that particular instance of OrderShipment. Remember the association class Ticket with its passengers collection in article 5A- same method is used here but Items to be shipped are populated differently which is to be shown in next article and also a ordershipment may have only part of the ordered items (remaining items may be sent in a different shipping and hence put under different order shipment). So for now we capture Shipping and the association class OrderShipment between Shipping and PaymentApprovedOrder. The Shipping class code is given by:

C#
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);
}

The OrderShipment class code is given by:

C#
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; }
}

The mapping for Order Shipment is:

XML
<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>

The Shipping abstract class has two concrete child classes: FastShipping and ParcelService. FastShipping is a specialized form of Shipping that offers same day shipping and faster delivery with extra charges. ParcelService just transfers item with gift wrapping if required. FastShipping is not offered with gift wrap because our efficient e-commerce site jumps to action when an order is to be fast shipped and does not have time for gift wraps. Have a look at Figure1 below. It shows the shipping's abstract super class and 2 child classes and their mapping code. Read the note in the figure that explains it.

Image 1

Figure 1

To know the power of inheritance mapping using NHibernate we will have a look at the methods MakeOrderPayment and ProvideShippingInformation in ECommerceSellerSystem class. What is of interest here is the way Shipping instance is created and persisted to the database. The Shipping instance is created in ProvideShippingInformation method and consumed in MakeOrderPayment method and persisted to db. Have a look at the code below.

C#
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;
}

Have a look at the method ProvideShippingnformation. It creates a concrete instance of FastShipping or ParcelService according to the info provided in Order passed as parameters and stores it to a base class reference of Shipping. This same baseclass reference is then used to calculate shipping charges of a Order by using the polymorphic call to the method CalculateShippingCharges which is overridden in both concrete classes of Shipping, i.e., FastShipping and ParcelService (each has a different way of calculating shipping charges- fast shipping is higher). It is then returned to caller - "MakeOrderPayment" as the base class reference containing concrete child instance itself. What is absolutely of interest here is that in the caller, this abstract baseclass reference i.e Shipping, storing the concrete child class instance (FastShipping or ParcelService) is used with NHibernate for persistence. You do not tell NHibernate what concrete instance is to be stored etc. It is all handled by Nhibernate (though the internals of how nHibernate persists are clearly understandable if you see the structure of tables in Figure 2 later). The concrete child classes of the abstract Shipping class i.e., FastShipping and ParcelService is as shown below (Note that they do not have property for identity value - more on this later):

C#
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;            
    }
}

So what are the rows formed by Nhibernate in the shipping tables while a order is processed for the testcode given earlier? Refer to figure 2. Please note that this is the snapshot of the shipping tables in the database when customer leaves an order to the site. A customer can only give a preference for fast shipping or parcel service with gift wrapped. Hence a row is created in the superclass table SHIPPING and its primarykey is posted as a foreignkey in a row of table FASTSHIPPING if the customer wants FastShipping Service or it is posted in a row of PARCELSERVICE table if he wants ParcelService. The charges is also filled in along with information for subclass like the giftwrap for parcelservice subclass and startdeliveryby date for fast shipping (useful to send notifications to shipping department). Customer cannot name the courier by which shipping will be made (column SHIPPINGNAME in SHIPPING TABLE)or other Shipping details. These are input by shipping department of site. Hence unless they start shipping and input information for shipping, all these values will be null as shown in figure 2. To avoid nulls, you can use default values like "To Be Filled". Refer to figure 2.

Image 2

Figure 2

The mapping file for Shipping and its subclasses is shown below. The most important point to note in this mapping file is that for the subclasses, i.e., FastShipping and ParcelService, the primary key value is not generated (i.e FASTSHIPPINGID and PARCELSERVICEID). Instead it is a foreign key value posted from the SHIPPING TABLE (base class) and declared in mapping file using <key column=".."> tag. See Figure 2 arrows and mapping code to understand. Also look at the c# code for FastShipping subclass and ParcelService subclass. They do not have a property for FastShippingId and ParcelServiceId as we do for all entity classes. This is because both columns FASTSHIPPINGID and PARCELSERVICEID are foreign keys posted automatically by NHibernate from the baseclass using the information (<key column="...">) given in mapping file while declaring the subclasses using <joined-subclass> tag. Nhibernate uses the foreignkey from base class to child class to capture the "is a" inheritance relationship in this Table Per Subclass method.

XML
<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>

Conclusion

Full Inheritance hierarchy mapping and polymorphic support are the strengths of Nhibernate's Table Per Subclass method. For very large hierarchies, the joins in SQL statements may result in a performance loss. In next article (part 7) we will finish the shipping processing by allowing the shipping information to be input by the site and also shipping of the items by adding the item collection to OrderShipment in a different and useful way that captures the movement of Item in the organization (i.e., from inventory to shipments). Enjoy Nhibernate.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
India India
Software Developer developing in c#.net.

Comments and Discussions

 
-- There are no messages in this forum --