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
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!
I use Google Reader extensively. I found your site no problem. I wouldn't worry about it.
NHibernateUtil.Initialize() is a much better way to eager initialize
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
is this (merging a detatched entity into a session) the only intended aspect of merge? or are there others as well?
check the docs :-)
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?
Alex,
You have to give more details about this if you want me to answer that.
And no, Lock wouldn't be useful for you, I think.
Please use the NHibernate mailing list for followups
http://groups.google.com/group/nhusers
Alternatively, you may choose our commercial support option:
http://nhprof.com/commercialsupport
Thanks Oren. You made my day, I owe you a beer! :)
hi, I want to know how to implement session per view in multilayer project.
could you show us same sample?
alexz,
Search the blog, and read about rhino commons.
Comment preview