NHibernate – Cross session operations

time to read 5 min | 816 words

This started out as a support question, but it is an interesting enough (and general enough) that I think it is important to make sure that it is recorded.

When working with detached entities (from another session), sometimes, at seemingly random places, NHibernate will throw a NonUniqueObjectException. Where it actually happen and the exact cause depend on several variables, but the root problem is simple: working with detached entities safely requires that you be aware of possible identity map violations.

Let us look at some code and then I can explain:

using (var session = sessionFactory.OpenSession())
using (var tx = session.BeginTransaction())
{
// get the entity
post = session.Get<Post>(postId);
// force the association to be eagerly loaded
System.Console.WriteLine(post.User.Username);
tx.Commit();
}

using (var session = sessionFactory.OpenSession())
using (var tx = session.BeginTransaction())
{
// get the user for the entity
var anotherUser = session.Get<User>(post.User.Id);

// will return false
ReferenceEquals(anotherUser, post.User);

// opps, user instance with the same id but with different
// reference was detected
session.SaveOrUpdate(post);

tx.Commit();
}

The reason that the problem seems obscure at first is that there are quite a few variables that are going to affect how this will behave. In order to reproduce the issue you need:

  • An association that is marked with a cascade option, such as “save-update”, “all” or “all-delete-orphan”
  • The entity this association points needs to be loaded in the second session.

Depending on what you are doing (saving an entity vs. updating it) and what the options are for the id generation, you may get the error on the SaveOrUpdate or on the Commit.

The actual details are pretty unimportant, but understanding what is going on is. The issue is that NHibernate has been asked to perform something that violate one of its core assumptions, break the identity map.

Because of the cascade options set on Post.User, we are asking NHibernate to also save the User instance associated with the post. The problem is that when NHibernate is encountering that, it is going to see an entity with an id that is already on the session but as a different reference. That violates the identity map rules and force NHibernate to throw an exception.

The root cause is, as I mentioned, trying to work with a detached entity as if it was a regular entity. NHibernate provides a different API for working with detached entities safely, precisely because of those sort of reasons.

The appropriate way of handling such an issue is to use the Merge method, which will take a detached object graph and merge it into the session, properly resolving such conflict. Note, however, that Merge will return a different entity instance than the one that you passed.

Let us look at the code:

using (var session = sessionFactory.OpenSession())
using (var tx = session.BeginTransaction())
{
// get the user for the entity
var anotherUser = session.Get<User>(post.User.Id);

// will return false
ReferenceEquals(anotherUser, post.User);

// will merge the detached entity into the session
// creating NEW entity instance or re-using the one
// that is already in the session
var mergedPost = (Post)session.Merge(post);

// will return false
ReferenceEquals(mergedPost, post);

// will return true
ReferenceEquals(anotherUser, mergedPost.User);

tx.Commit();
}

As you can see, this is a fairly small change, and NHibernate now takes care of wiring up everything correctly even in the face of conflicting changes.

You can play around with the code here:

http://github.com/ayende/nhibernate-blog-model/tree/non-unique-object-execption

http://github.com/ayende/nhibernate-blog-model/tree/non-unique-object-execption-resolution