Sunday, November 13, 2011

NHibernate bi-directional - use it carefully (Part II)

In the previous post I've showed you one reason to be very careful when using bi-directional associations in NHibernate.

In this post I'll show you another reason. Again, for the example, I will use Customer with many Orders.

We will create a one-to-many association from Customer to Orders:
<set name="Orders" lazy="true" cascade="all-delete-orphan">
      <key column="CUSTOMER_ID" />
      <one-to-many class="Order" not-found="ignore"/>
</set>

and we will create a many-to-one from Order to Customer:
<many-to-one name="Customer" class="Customer" column="CUSTOMER_ID"/>
Now consider the following scenario:
someCustomer.Orders.Add(someNewOrder);

...query the db using HQL

...

someNewOrder.OrderDate = DateTime.Now;

....

CustomerRepository.SaveOrUpdate(someCustomer);
transaction.Commit();

If you'll take a look at table 'ORDERS' right after the commit, you'll find out that the CUSTOMER_ID field in the new record created for someNewOrder is... NULL.

And why is that?
let's see what NHibernate is doing a step by step:
1 - Right before querying the DB by using HQL, NHibernate is flushing all the changes to the DB, and that includes creating a new record for someNewOrder At this stage the record in the DB is perfectly well.

2 - By changing someNewOrder.OrderDate NHIbernate considers someNewOrder as 'dirty' and it will flushed it to the DB later on when the transaction will be commited.

3 - Finally, when the transaction is commited, again NHibernate is flushing all the changes but this time it's generating an 'UPDATE' statement for someNewOrder since it's no longer a new object - it has a record in the DB.

And here is the problem - as we saw in the last step, an 'UPDATE' statement for someNewOrder
is executed. But since someNewOrder.Customer is null (nowhere in the code we've ever changed it) - the 'UPDATE' statement looks something like this:
"UPDATE ORDERS SET ...CUSTOMER_ID=NULL... WHERE...".

OK, now you have a few options to solve it:
1 - do not use HQL - use session.Get() instead (only if you can...). This doesn't enforce NHibernate to flush like HQL does.

2 - You can make sure that someNewOrder.Customer actually does hold the Customer before committing.

3 - You can go to Order.hbm.xml and add update="false" to the many-to-one association. like this:
<many-to-one name="Customer" class="Customer" column="CUSTOMER_ID 
update="false"/>
this tells NHibernate not to include the Customer when generating an 'UPDATE' statement for an Order.
But this also means that you can not update the Order's Customer like this:
someOrder.Customer = someCustomer;
OrderRepository.SaveOrUpdate(someOrder);
it won't work if you chose option No. 3...

No comments:

Post a Comment