Friday, 4 September 2009

NHibernate Bidirectional Cascade – when is it appropriate?

I recently found a situation where I needed to use NHibernate’s cascade facility from both ends of a relationship. Normally, I’d use it only in one direction, but in this scenario the only practical solution was to use it in both directions. I’m unsure whether this is a good thing to do, so in this post I’ll:

  • talk about why I normally use it in one direction
  • talk about the situation that led to the bidirectional cascade
  • ask whether this was the right solution.

One-directional cascades

Normally, the cascade is tied to the concept of ownership in the model. We cascade changes from the parent (e.g. an Order) to its logical children (e.g. OrderLines). This fits in with the Domain-Driven Design concept of an aggregate root. If we have the appropriate cascades set up from the aggregate root to the contained entities, then we can follow a simple workflow:

  1. load up an aggregate root from the repository
  2. modify, add and remove entities within the aggregate
  3. let NHibernate automatically persist the changes to the database when the session is flushed

This makes for very clean domain layer code which remains persistent ignorant. The domain code that is performing these modifications doesn’t have to explicitly save or delete any of the entities within the aggregate.

I suppose this model does not say that we can’t have cascade running from children to parents. But I always felt that the natural order of things was to have cascades running in one direction.

Why we needed bidirectional cascades

transient

In this scenario, the Customer is an aggregate root containing the ShoppingOrder and OrderLine objects. I’ve set up cascades from the Customer down to the OrderLine class, which allows the following code:

using (new SessionScope())
{
    var customer = ActiveRecordMediator<Customer>.FindByPrimaryKey(customerId);
    var shoppingOrder = customer.AddOrder(some, parameters);
    var newOrderLine = shoppingOrder.AddLine(some, parameters);
}

which executes SQL like this:

SELECT ... FROM Customer WHERE Id = ...
INSERT INTO ShoppingOrder ...
INSERT INTO OrderLine ...

Perfect. Note that I’m using Castle.ActiveRecord to configure and access NHibernate, with auto-flushing sessions. As far as I know, this scenario would translate directly to an equivalent plain NHibernate set up.

The problem comes with the OrderLineState. In our application, we have a separate part of the system which tracks the state of orders. There are two implications of this being a separate concern:

  • The OrderLineState is not considered part of the Customer aggregate. There’s no relation from the OrderLine to the OrderLineState.
  • The piece of code that creates the initial state object for an OrderLine is logically a long way from the piece of code that creates the OrderLine.

This means that when we create the OrderLineState, we need to explicitly tell NHibernate about the new entity:

using (new SessionScope())
{
    var customer = ActiveRecordMediator<Customer>.FindByPrimaryKey(customerId);
    var shoppingOrder = customer.AddOrder(some, parameters);
    var newOrderLine = shoppingOrder.AddLine(some, parameters);

    // In some other part of the system, perhaps responding to a domain event
    var state = new OrderLineState(newOrderLine);
    ActiveRecordMediator<OrderLineState>.Save(state);
}

But this code throws an exception from the call to Save(state):

NHibernate.PropertyValueException: not-null property references a null or transient valueNHOddities.OrderLine.Parent

This makes sense. We’ve asked NHibernate to save the OrderLineState object. This has a non-null, cascading reference to the OrderLine, so it tries to save the OrderLine. Because the relationship OrderLine.Parent (the Order) does not have cascading configured, NHibernate won’t try to save the Order yet. So the Order referenced by OrderLine.Parent is a transient object, and we get the corresponding exception.

There are a variety of ways to fix this. All of them work, in that they allow the code above to run and make the correct INSERT statements in the database.

  1. Remove the NHibernate NotNull constraint on the OrderLine.Parent property. This lets NHibernate save the OrderLine before it’s saved the order. I don’t like this for two reasons. Firstly, the NotNull constraint reflects a genuine constraint: an OrderLine must always belong to an order. Secondly, that NotNull constraint is probably reflected on the database column definition, so we’d have to change the database schema, removing a meaningful and useful constraint. In general, I don’t like having to change what is a genuine reflection of the domain just to make things convenient for the framework I’m using.
  2. Explicitly flush the session after adding the Order and OrderLine to the Customer aggregate, and before saving the new OrderLineState. I don’t like this one either. It requires my domain code to explicitly deal with session management. We usually consider this to be an infrastructural concern. We have a web app, so we handle session and transaction management at the request level.
  3. Add a cascade on the BelongsTo relation OrderLine.Parent.

Introducing this bidirectional cascade seems like the best (or at least, least-worst) solution. But it doesn’t quite seem right.

Is there anything wrong with a bidirectional cascade?

As I discussed earlier, it feels like cascades should run from parents to children, not the other way, and not in both directions. This isn’t written down as a rule or guidance anywhere that I can find in the NHibernate documentation, but it seems to be the standard approach. Indeed when I tweeted about this, I got a reply from none other than one of the authors of NHibernate In Action himself:

@ascordellis Cascade on both sides feels heavy, are you sure that both sides need to be the big daddy of the relationship?

For now, I’m leaving it in. For our problem, it’s the least intrusive solution. But I’d love to hear your opinion. Is there a better way to solve our problem? Should NHibernate just deal with it for us?

3 comments:

  1. I like this post. I'm a lazy programmer and I've just gone bi-directional and created unit tests to ensure the persistence was working as I'd expected.

    ReplyDelete
  2. Thanks for the post. Personally, I think bidirectional cascading should definitely be handled both efficiently and safely in any ORM. Otherwise, you can't avoid the need for different mappings for different use cases.

    I'm actually having difficulties with bidirectional cascading in NH. The cascade is causing the save/update to come back around to the originating entity thus causing an 'unsaved transient entity' exception. If you have encountered this in your work, I'd love to hear how you fixed it.

    ReplyDelete
  3. @Brad, We haven't had that problem in our codebase - presumably it occurs if you have a loop structure in your entity relationships?

    Nice point re not wanting different mappings for different use cases.

    ReplyDelete