I consider Hibernate a great and useful tool. But it has some mean Exceptions lurking in the darker corners. Today I'd like to explore the ones from the title in a little more detail, including different approaches on how to avoid them.
Let's start with the well known LazyInitializationException. When you search for this one in the Internet you end up with scenarios like this one:
You load A, close the session you used and try to access a property of A which gets loaded lazily resulting in a LazyInitializationException. That one is well described, including strategies to avoid it, which basically consist of not closing the session to early.
But there are more hideous scenarios. Assume the classes Mom, Dad and Kid are mapped as one would expect, with references from Mom and Dad to Kid. Now consider the following piece of code:
Session session = openSession();
session.beginTransaction();
Mom mom = (Mom) session.load(Mom.class, momId);
Kid momsKid = mom.getKid();
// dad comes by
Dad dad = (Dad) session.load(Dad.class, dadId);
Kid dadsKid = dad.getKid();
// and leaves
session.evict(dad);
// alas he has taken kido with him
momsKid.getName();
Depending on your exact Mapping you might get a LazyInitializationException in the last line, although the session is still open. The problem here is that when dad got evicted the kid got evicted as well. And since Mom has a reference to the exact same object, that object isn't attached to any session anymore. In order to see this effect you'll have to use mappings like this:
@ManyToOne(fetch = LAZY, cascade = CascadeType.ALL)
@Cascade(org.hibernate.annotations.CascadeType.ALL)
private Kid kid;
The NonUniqueObjectException gets caused by what could be considered the reverse process. Examine the following piece of code:
// Dad left for another woman, I mean Session
Dad dad = loadDad();
Session session = openSession();
session.beginTransaction();
Mom mom = (Mom) session.load(Mom.class, momId);
System.out.println(mom.getKid().getName());
// Now dad wants to move back in
session.saveOrUpdate(dad);
The first method call loads a Dad object including Kid from a different session. Then from the 'main' session Mom gets loaded including kid. When we attach the Dad object to the session it tries to attach its version of the Kid as well, which collides with the Kid already present in the session.
So how can we avoid these kind of problem? I see several options:
- Don't use Cascade. Of course this means much more effort on your side in many cases, since you have to carefully track what you need to attach or evict from a session.
- Don't use evict and don't reattach existing instances to sessions. I guess this should work in many cases. But I have seen cases where this approach would have caused a large overhead, because reloading complex object graphs.
- Carefully design your cascading boundaries. A look at the Aggregate Root from DDD might help here. An Aggregate Root is a single class which works as a gate keeper (did anybody say facade) to a cluster of classes related to the Aggregate Root. When all the Hibernate related actions go against that Aggregate Root, and cascade from there to the content and only the content of the Aggregate Root you should be safe. All entities outside an Aggregate Root have to use the Aggregate Root or Repositories to gain access. This needs some bookkeeping. But when you do it properly it might actually improve the over all design of your code. Please note that all this is a thought experiment on my side, and I can't promise that it actually works as intended, but I think it should.
If you want the complete source code for the example, you can download this source.zip file.
Talks
Wan't to meet me in person to tell me how stupid I am? You can find me at the following events: