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:
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)
{
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
order.NotAvailableItemDescriptions.Add(selection.CurrentProduct);
}
}
}
catch (Exception ex)
{
}
return order;
}
public void DestructShoppingCartBindingForItemsInOrder(Order order)
{
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;
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;
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
{
shipping = new FastShipping(DateTime.Today);
}
shipping.CalculateShippingCharges(order_amount);
return shipping;
}
public Item NewItemMatchingProductDescription(ProductDescription product_description)
{
Item item = null;
try
{
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)
{
try
{
foreach (Item item in order.OrderItems)
{
item.IsOrdered = true;
}
}
catch (Exception ex)
{
}
}
}
The Code for PaymentApprocedOrder
is:
public class PaymentApprovedOrder
{
public PaymentApprovedOrder()
{
PaidOrderItems = new List<Item>();
}
public PaymentApprovedOrder(Order order,Payment pay)
{
PaidOrderItems = new List<Item>();
CurrentOrder = order;
OrderPayment = pay;
pay.PaidOrder = this;
order.OrderedByCustomer.AddPaidOrder(this);
foreach (Item item in order.OrderItems)
{
AddPaidItem(item);
}
this.NotAvailableItemDescriptions = order.NotAvailableItemDescriptions;
}
public virtual long PaymentApprovedOrderId { get; set; }
public virtual Order CurrentOrder { get; set; }
public virtual Payment OrderPayment { get; set; }
public virtual IList<Item> PaidOrderItems { get; set; }
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)
{
item.PaidOrder = this;
PaidOrderItems.Add(item);
}
}
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"></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:
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:
<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:
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 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:
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>();
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);
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 };
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_repo.addItem(customer1);
customer_repo.addItem(customer2);
customer_repo.addItem(customer3);
ShoppingCart cart1 = new ShoppingCart(customer1);
cart_repo.addItem(cart1);
ShoppingCart cart2 = new ShoppingCart(customer2);
cart_repo.addItem(cart2);
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);
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);
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);
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:
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:
public class OrderShipment
{
public OrderShipment()
{
ShipmentItems = new List<ShipmentItem>();
}
public OrderShipment(PaymentApprovedOrder paid_order, Shipping shipping)
{
OrderShipmentId = new CompositeKey();
OrderShipmentId.class1Key = paid_order.PaymentApprovedOrderId;
OrderShipmentId.class2Key = shipping.ShippingId;
ShipmentItems = new List<ShipmentItem>();
paid_order.ShipmentsOfThisPaidOrder.Add(this);
shipping.ShipmentsInThisShipping.Add(this);
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:
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.
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.
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
{
shipping = new FastShipping(DateTime.Today);
}
shipping.CalculateShippingCharges(order_amount);
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):
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.
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.
<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.