Wednesday, October 8, 2014

Inverse + Cascade variations on NHibernate

Keep forgetting how the persistence steps should be like based on the variations of Insert=false/true + Cascade=none/all?


Let's keep a matrix of those variations and the effect on the persistence code



Given this table:

create table [Order]
(
    OrderId             int identity(1,1)     not null,
    OrderDescription    nvarchar(100)         not null,
    
    constraint pk_Order primary key (OrderId)
);

create table LineItem
(
    Order_OrderId   int                 not null,
    
    LineItemId      int identity(1,1)   not null,
    
    ProductId       int                 not null,
    Quantity        int                 not null,
    
    
    constraint fk_LineItem_Order    foreign key (Order_OrderId) references [Order] (OrderId),

    constraint pk_LineItem          primary key (LineItemId),
    
    constraint fk_LineItem_Product  foreign key (ProductId)     references Product (ProductId)
);


And this domain model and mapping:
public class Order
{
    public virtual int                  OrderId          { get; set; }
    
    public virtual string               OrderDescription { get; set; }
    public virtual IList<LineItem>      LineItems        { get; set; }
}


public class LineItem
{
    public virtual Order    Order       { get; set; }
    
    public virtual int      LineItemId  { get; set; }
    
    public virtual Product  Product     { get; set; }
    public virtual int      Quantity    { get; set; }
}


public class OrderMapping : ClassMapping<Order>
{
    public OrderMapping()
    {
        Id(x => x.OrderId);
        Property(x => x.OrderDescription);
        Bag (
            property => property.LineItems,
            collectionMapping => collectionMapping.Key(keyMapping => keyMapping.Column("Order_OrderId")),
            mapping => mapping.OneToMany()
            );
    }
}



And this common code:
var order = 
    new Order
    { 
        OrderDescription = "Cat",
        LineItems = 
            new[] 
            { 
                new LineItem { Product = session.Load<Product>(1), Quantity = 2 }, 
                new LineItem { Product = session.Load<Product>(3), Quantity = 4 }
            }
    };


Here are the variations:

Inverse

false
using (var session = sessionFactory.OpenSession())
using (var transaction = session.BeginTransaction())
{
    /* common code here */ 

    foreach(var li in order.LineItems)
    {
        // li.Order = order; // optional  
        session.Save(li); 
    }

    session.Save(order);
    transaction.Commit();
}
using (var session = sessionFactory.OpenSession())
using (var transaction = session.BeginTransaction())
{ 
    /* common code here */

    session.Save(order);
    transaction.Commit();
}
true
using (var session = sessionFactory.OpenSession())
using (var transaction = session.BeginTransaction())
{
    /* common code here */

    foreach(var li in order.LineItems)
    {
        li.Order = order;   
        session.Save(li); 
    }

    session.Save(order);
    transaction.Commit();
}
using (var session = sessionFactory.OpenSession())
using (var transaction = session.BeginTransaction())
{
    /* common code here */

    foreach(var li in order.LineItems)
        li.Order = order;
  
    session.Save(order);
    transaction.Commit();
}
Cascade
none
all



Inverse=false + Cascade=none:
  • Pros:
    • Leaving LineItem.Order as null will also work. LineItem.Order_Order_Id column will still be assigned of value from its parent. In fact, it's best not to assign LineItem.Order to its parent; when it's assigned of value, I noticed three steps are being performed on LineItem: INSERT+UPDATE+UPDATE. Odd
    • However if LineItem.Order would be left null, it's better to remove that property. Promotes unidirectional navigation. Pure DDD.
  • Cons:
    • Have to call session.Save on each child
    • Three steps happen when persisting LineItem, INSERT+UPDATE+UPDATE:
      • INSERT happens on LineItem where all its columns are assigned of values, except for LineItem.Order_OrderId column. LineItem.Order_OrderId column is left as null, even if we assign a value on its corresponding LineItem.Order property
      • UPDATE, albeit just a repeat of the same process of INSERT above. As if INSERTing it won't save the entity enough ^_^
      • UPDATE again, this time, LineItem.Order_Order column is assigned of value of parent entity's id
      • In order to prevent the three steps above, albeit will just be lessened to two only(INSERT+UPDATE), re-arrange the persistence order, just move the persistence of LineItem collection after of parent, to wit:
      • session.Save(order);  
        foreach(var li in order.LineItems)
        {
            // optional. it's better to remove this assignment
            // NHibernate still thinks the first (INSERT) on child entity LineItem has its parent id column(LineItem.Order_OrderId) set to null(when it fact it already has a non-null value during INSERT), 
            // consequently will still do UPDATE regardless of LineItem.Order_OrderId already have a value or not
            li.Order = order; 
        
            session.Save(li); 
        }
        transaction.Commit();
        
    • It's better not to re-arrange the persistence, strongly-advised to just remove the LineItem.Order property, it's more DDD. And three steps won't be performed, just two


Inverse=false + Cascade=all:
  • Pros:
    • Leaving null on LineItem.Order property will also work, LineItem.Order_OrderId column will still be assigned of value of parent entity Order.OrderId value, albeit on second step, during UPDATE, see the Cons below
    • No need to perform session.Save on each item of collection
    • Can be pure DDD by removing the LineItem.Order property
  • Cons: Two steps happen to persist LineItem
    • INSERT, all the columns of the child are assigned of value except for its parent id column, LineItem.Order_OrderId column is null initially
    • UPDATE, the parent id column(LineItem.Order_OrderId column) of the child is assigned of value of its parent entity's id

Inverse=true + Cascade=none:
  • Pros: Nothing
  • Cons:
    • Setting the parent property (LineItem.Order) of each item in collection's is a must, and so is the saving of LineItem itself
    • It's also a two-step process to persist LineItem, uses INSERT+UPDATE. Can make it one step by placing the persistence code of LineItem after of its parent entity(Order), to wit:
    • session.Save(order);  
      foreach(var li in order.LineItems)
      {
          li.Order = order; 
          session.Save(li); 
      }
      transaction.Commit();
      


Inverse=true + Cascade=all:
  • Pros:
    • No duplicate steps(INSERT+UPDATE) when persisting LineItem, every columns are assign of value during INSERT, including LineItem.Order_OrderId. Efficient
    • No need to perform session.Save on each item on collection
  • Cons: Can't be pure DDD, can't remove the child's parent property (LineItem.Order property)



The best configuration is Inverse=true+Cascade=all. Especially if we can elegantly hide the parent property of child item it's very DDD. DDD promotes the use of unidirectional mapping, the navigation of Value Objects like LineItem should come from root only(Order)


Next best is Inverse=false+Cascade=all, that is if it's acceptable to have a two-step operation(INSERT+UPDATE) when a child is persisted. With this configuration, you can remove the LineItem.Order parent reference property and NHibernate will still be able to assign LineItem.Order_OrderId column to its parent Order.OrderId value. This can enforce navigation of Value Objects like LineItem from root entity only (Order), no need to use tricks like using EditorBrowsable attribute to hide the navigation property from the child, as it's removed





Happy Coding!

No comments:

Post a Comment