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();
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