Before reading this article, please go through the following articles:
- Object Relational Mapping (ORM) Using NHibernate - Part 1 of 8
- Object Relational Mapping (ORM) Using NHibernate - Part 2 of 8
- Object Relational Mapping (ORM) Using NHibernate - Part 3 of 8
- Object Relational Mapping (ORM) using NHibernate - Part 4 of 8
- Object Relational Mapping (ORM) Using NHibernate - Part 5-A of 8
- Object Relational Mapping (ORM) Using NHibernate: Part 5-B of 8
Coding Inheritance Of Entity Classes
Parts 1-5 of the article series detailed various types of associations representing the "has a" relationship between entity classes. Inheritance represents the "is a" relationship among entity classes. We are all familiar with this and also know if used correctly inheritance can yield the benefit of polymorphic behavior and that an interface-based inheritance, if used correctly in clients, can increase flexibility, but if used incorrectly, inheritance reduces flexibility. There are four 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 hierarchy). 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 comparative study between NHibernate and Language Integrated Query (the most popular and extensively used library) on using them, if it's 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 an abstract class) is mapped to a table. Every property of an entity class is mapped to its own table only. The "is a" relationship between the entity classes is mapped to their respective tables using the foreign key constraint i.e. the pimarykey of the superclass is posted as a foreign key to all the respective child classes. Objects of a particular class in the hierarchy are fetched using the "JOIN" statements (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 calls using base classes (the foreign key posted from a base class table to a 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.
E-Commerce Sample Scenario
Before looking at the sample for an inheritance, the OrderProcessing of the e-commerce example needs to be completed so that an order is made available to the e-commerce site for Payment and then Shipping (our sample class for inheritance). So first we will work on order processing and do the Shipping inheritance sample here in the next section of this same article.
Order Processing
The product description matching customers' buying needs are selected by the customer for buying and are added to the shopping cart of the customer. After the selections are over, the customer makes an Order out of the selections in the shopping cart and submits it to the e-commerce 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 the amount for shipping the items in an order. The customer will make a payment and buy. The ECommece Site will link the items to the paid order matching the product descriptions in the shopping cart of the customer and all these items will be shipped afterward.
In the last part of the article series (Part 5 B), we completed the scenario of a customer selecting items for a shopping cart. Items are now in the 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 shopping cart are submitted as an order to the e-commerce site and how it is processed further.
The construction code of PaymentApprovedOrder in Article Part 4 - Figure 5 and the client code so far, show how order processing is done currently for testing purposes. Now here we will refine this order processing. As a first step towards it, we will remove the order processing code from the PaymentApprovedOrder constructor and move it to the 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 the Shopping Cart. Now we are adding OrderProcessing methods to it. A look at the methods in it with the client code should give an 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 an inventory item. This is done as explained before to avoid the problem of an item being in a cart until it's 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 the shipping department needs concrete Item instances of inventory and not product descriptions.
An Order in the ECommerce system contains an OrderItems collection for items in the inventory that is ordered and the NotAvailableItemDescriptions property in it is a collection of ProductDescriptions to represent the product descriptions in the Order for which an item is not presently available in the Inventory. The ShoppingCart instance with a collection of selections has to be converted to an Order instance with the collection of items first. So when the customer wants to make an order for his shopping cart, we process the ShoppingCart from the customer containing ShoppingCartSelections (each selection has a product description that the customer wants to buy) and query the ITEM table and obtain an Item in inventory matching that selection's ProductDescription and add it to the OrderItems collection, if the match is found for a particular ProductDescription. If the match is not found in Inventory to get an Item instance for a particular ProductDescription, we add the ProductDescription itself to the NotAvailableProductDescription collection of Orders. The Total Value of all items including not available product descriptions is calculated along with shipping charges and provided to the customer. Once the Customer makes a Payment, a PaymentApprovedOrder is created by the ECommerceSellerSystem class along with items for a particular PaymentApprovedOrder marked so in the inventory ITEM table. Items in an Order which is not found in the inventory may still exist as a 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 the 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 following code.
The code for the ECommerceSellerSystem class is:
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 an 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
{
// else if it is null item is unavailable
// add selection to not available list in orders collection
order.NotAvailableItemDescriptions.Add(selection.CurrentProduct);
}
}
}
}
catch (Exception ex)
{
// Handle exceptions
}
return order;
}
public void DestructShoppingCartBindingForItemsInOrder(Order order)
{
//************************************************************************
// The IsCartedState of Item exists only when an order is made. At the exit
// of making an order, the IsCarted state should be destroyed, immaterial
// of whether the order was made or not. An item will never be allowed to be carted
// permanently. A item can exist in inventory or in order but not in the cart. So,
// whenever an Order object is destroyed, call this method for that Order. It destroys the item bindings to the cart.
// If an Order goes out of scope, before it goes out
// of scope, this method is called (example by using an unload event handler, etc.).
// So an item will be paid or in inventory only. When an Order is made, an item can be carted, but
// by the end of the order lifetime, the item will cease to exist in the 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)
{
// Handle exceptions
}
}
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);
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)
{
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 an item should be sent - i.e., today
// which is the same day as the order day
shipping = new FastShipping(DateTime.Today);
}
shipping.CalculateShippingCharges(order_amount);
// Most important thing is Shipping is an abstract superclass -
// 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 NHibernate helper 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)
{
// Handle exceptions
}
return item;
}
public void MarkItemAsOrdered(Order order)
{
// Isolation Control Required
try
{
foreach (Item item in order.OrderItems)
{
item.IsOrdered = true;
}
}
catch (Exception ex)
{
// Handle exceptions
}
}
}
The mapping code for PaymentApprovedOrder is:
<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" />
<idbag name="PaidOrderItems" table="PAYMENTAPPROVEDORDER_ITEMS" cascade="save-update">
<collection-id column="JoinTableRowId" type="long">
<generator class="identity" />
</collection-id>
<key column="PAYMENTAPPROVEDORDERID" />
<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" />
<many-to-many class="ProductDescription" column="PRODUCTDESCRIPTIONID" />
</idbag>
</class>
The code for the Item class is:
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 the Item class is:
<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" />
<many-to-one name="ItemDescription" class="ProductDescription" column="PRODUCTDESCRIPTIONID" not-null="true" lazy="false" />
<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 the ProductDescription class is:
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:
<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 an important point to note here in the ECommerceSellerSystem class and the Item class. The ITEM table and Item class have a flag called IsShopCarted. This flag is set briefly when an order is processed and an Item instance is queried and obtained for a product description to show that the particular item is now attached to a shopping cart. The moment the Order is paid or Order Instance is going to be destroyed or the Order is going to go out of scope etc. this flag must be reset to false (usually in an event handler that is triggered by the cause for the destruction of the Order instance) wherever such a thing might happen. So the lifetime of this flag to be in true is restricted to the time an 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 the Order later). When both the IsShopCarted flag and IsOrdered flag are set to False then an Item is available in the inventory for the order. If the IsShopCarted flag is set, an item is not available and is in a shopping cart that is currently undergoing Order processing but the flag will be reset to true or false immaterial of whether OrderProcessing is a success or failure. The IsOrdered flag is set to True when an item is purchased.
The Item object is simply in various states: an Item is in Inventory, the Item is in a Cart, the Item is a PaidOrder or the Item is in Shipping. Just the flags IsShopCarted and IsOrdered will do for this sample. We will handle the state of Item IsInShipping in 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:
// Create repositories for different entities
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 product descriptions
ProductDescription description1 = new ProductDescription { ManufacturerName = "samsung", Price = 60000, ProductName = "mobile" };
description1.ProductUserReviews.Add(new ProductReview { UserEmailId = "[email protected]", UserComment = "GOOD PRODUCT.MUST BUY", ProductRating = 5.0 });
// ... Add more product reviews to description1 ...
ProductDescription description2 = new ProductDescription { ManufacturerName = "nokia", Price = 70000, ProductName = "mobile" };
description2.ProductUserReviews.Add(new ProductReview { UserEmailId = "[email protected]", UserComment = "GOOD PRODUCT.MUST BUY", ProductRating = 5.0 });
// ... Add more product reviews to description2 ...
// Add product descriptions to the repository
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 };
// ... Initialize more items ...
// Add all items to the repository
foreach (var item in items)
{
items_repo.addItem(item);
}
// Create customers and add them to the repository
Customer customer1 = new Customer { CustomerName = "AliceWonder" };
Customer customer2 = new Customer { CustomerName = "JimTreasure" };
Customer customer3 = new Customer { CustomerName = "OliverTwist" };
// ... Set email identities for customers ...
customer_repo.addItem(customer1);
customer_repo.addItem(customer2);
customer_repo.addItem(customer3);
// Create shopping carts for customers
ShoppingCart cart1 = new ShoppingCart(customer1);
ShoppingCart cart2 = new ShoppingCart(customer2);
cart_repo.addItem(cart1);
cart_repo.addItem(cart2);
// Add selections to the shopping carts
ECommerceSellerSystem system = new ECommerceSellerSystem();
ShoppingCartSelection selection1 = system.AddSelectionToCustomerCart(description1, cart1, 23);
cart_selection_repo.addItem(selection1);
ShoppingCartSelection selection2 = system.AddSelectionToCustomerCart(description2, cart2, 2);
cart_selection_repo.addItem(selection2);
// Make orders and process payments for customer 1
ECommerceSellerSystem seller_system1 = new ECommerceSellerSystem();
Order order1 = seller_system1.MakeOrder(cart1, true, false);
order1 = seller_system1.ProcessAmountForOrder(order1);
Payment payment1 = new Payment { PaymentAmount = (order1.OrderCost + order1.ShippingCharges) };
seller_system1.MakeOrderPayment(order1, payment1);
// Make orders and process payments for customer 2
ECommerceSellerSystem seller_system2 = new ECommerceSellerSystem();
Order order2 = seller_system2.MakeOrder(cart2, false, true);
order2 = seller_system2.ProcessAmountForOrder(order2);
Payment payment2 = new Payment { PaymentAmount = (order2.OrderCost + order2.ShippingCharges) };
seller_system2.MakeOrderPayment(order2, payment2);
The customer's shopping cart selection manages the shopping cart, does order processing, and creates a payment-approved order and next will be doing shipping processing also. To accomplish this it uses a set of domain classes that has no static variable except for one or two which can be removed. The preceding lines would get the wheels turning for astute WCF developers for the possibility of opening this class as a per-call instanced service. What would be even better would be proper interface factoring so that each of the functions of order processing, shipping, and shop selection handling are factored into various interfaces and made available with a service orientation. That was the essence of the ideas right from the start and could be accomplished with effort and improvements. Next, we will see the inheritance example with Shipping having created an Order. The MakeOrderPayment method in the class.
MAPPING INHERITANCE EXAMPLE
The entire Shipping scenario in the 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 the next paragraph. We provide flexibility that a PaidOrder (instance of PaymentApprovedOrder) can be shipped in many parts, in other words in many shipping. 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 of the items ordered by the customer, which should be shipped in that particular instance of order shipment. Remember the association class Ticket with its passenger's collection in article 5A; the same method is used here but Items to be shipped are populated differently which is to be shown in the next article also an order shipment may have only part of the ordered items (the remaining items may be sent in different shipping and hence put under a 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:
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);
}
public class OrderShipment
{
public OrderShipment()
{
// SHIPMENT ITEMS - DON'T 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:
<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: a. FastShipping and b. ParcelService. FastShipping is a specialized form of Shipping that offers same-day shipping and faster delivery with extra charges. ParcelService just transfers an item with gift wrapping if required. FastShipping is not offered with gift wraps because our efficient e-commerce site jumps into action when an order is to be fast-shipped and does not have time for gift wraps.
Have a look at Figure 1 below. It shows the shipping's abstract superclass and 2 child classes and their mapping code. Read the note in the figure that explains it.
Figure 1
To know the power of inheritance mapping using NHibernate we will have a look at the methods "MakeOrderPayment" & "ProvideShippingInformation" in the ECommerceSellerSystem class. What is of interest here is the way the Shipping instance is created and persisted in the database. The Shipping instance is created in the ProvideShippingInformation method and consumed in the MakeOrderPayment method and persisted in the database. Have a look at the code below.
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 an order is made. At the exit
// of making an 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 the item should be sent - i.e today
// which is the same day as the order day
shipping = new FastShipping(DateTime.Today);
}
shipping.CalculateShippingCharges(order_amount);
// Most Important thing is Shipping is an abstract superclass -
// 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 the Order passed as parameters and stores it to a base class reference of Shipping. This same base class reference is then used to calculate the shipping charges of an 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 the caller with "MakeOrderPayment" as the base class reference containing the concrete child instance itself. What is absolute of interest here is that in the caller, this abstract base class reference i.e. Shipping and 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 as shown below (note that they do not have a property for the identity value; more on this later):
public class FastShipping : Shipping
{
public FastShipping()
{
ShipmentsInThisShipping = new Iesi.Collections.Generic.HashedSet<OrderShipment>();
}
public FastShipping(string docket, DateTime delivery_date)
{
TrackingNumber = docket;
ShipmentsInThisShipping = new Iesi.Collections.Generic.HashedSet<OrderShipment>();
DeliveryDate = delivery_date;
}
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 = (0.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 = (0.02 * total_amount) + STANDARDRATE;
}
}
So what are the rows formed by NHibernate in the shipping tables while an order is processed for the test code given earlier? Refer to Figure 2. Please note that this is the snapshot of the shipping tables in the database when a customer leaves an order on the site. A customer can only give a preference for fast shipping or parcel service with gift wrapping. Hence a row is created in the superclass table SHIPPING and its primary key is posted as a foreign key in a row of the FASTSHIPPING table if the customer wants the FastShipping Service or it is posted in a row of PARCELSERVICE table if he wants ParcelService. The charges are also filled in along with information for the subclass like the gift wrap for the parcel service subclass and start delivery by the date for fast shipping (useful to send notifications to the shipping department). A customer cannot name the courier by which shipping will be made (column SHIPPING NAME in the SHIPPING TABLE) or other Shipping details. These are entered by the shipping department of the site. Hence unless they start shipping and enter 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.
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 the mapping file using <key column=".."> tag. See the Figure 2 arrows and mapping codes to understand. Also, look at the C# code for the 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 & PARCELSERVICEID are foreign keys posted automatically by NHibernate from the base class using the information (<key column="...">) given in the mapping file while declaring the subclasses using the <joined-subclass> tag. NHibernate uses the foreign key from the base class to the child class to capture the "is a" inheritance relationship in this Table Per Subclass method.
<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 the 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 the Item in the organization (i.e. from inventory to shipments). Enjoy NHibernate.
References
To gain a more comprehensive understanding of the subject, please read the next part,