Ayende @ Rahien

Refunds available at head office

NHibernate – Cross session operations

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

Comments

Steve Bohlen
11/08/2009 06:15 PM by
Steve Bohlen

This is a great post about a thorny recurring issue that can (sometimes) be hard to explain to people; now I have an easy reference article to point people to when they run up against this!

Scott White
11/08/2009 07:36 PM by
Scott White

I use Google Reader extensively. I found your site no problem. I wouldn't worry about it.

RichB
11/08/2009 09:24 PM by
RichB

NHibernateUtil.Initialize() is a much better way to eager initialize

D Kulkarni
11/08/2009 09:38 PM by
D Kulkarni

Any chance of getting a rev of LINQ for nhibernate for the 2.1.1 GA? I am getting a bunch of assembly version mismatch errors.

Thanks.

Dinesh

smurf
11/09/2009 01:12 AM by
smurf

is this (merging a detatched entity into a session) the only intended aspect of merge? or are there others as well?

Alex
11/09/2009 01:41 PM by
Alex

What is the downside of using session.Lock(item, LockMode.None) for associating item with session?

I ask because I have the folowing scenario:

I have to delete a few trees of items,when each item may belong to more then one tree. Merging each item into session means I will have to traverse all the trees and replace all instances of old item with new item. Will the session.Lock have unintended side effects?

Ben
11/09/2009 05:46 PM by
Ben

Thanks Oren. You made my day, I owe you a beer! :)

alexz
11/15/2009 11:35 AM by
alexz

hi, I want to know how to implement session per view in multilayer project.

could you show us same sample?

Ayende Rahien
11/15/2009 01:01 PM by
Ayende Rahien

alexz,

Search the blog, and read about rhino commons.

Comments have been closed on this topic.