Wednesday, 19 August 2015

Ugh... "A different object with the same identifier value was already associated with the session"

nested exception is org.hibernate.NonUniqueObjectException: A different object with the same identifier value was already associated with the session If you see the above message, you're probably calling saveOrUpdate() (or something similar) somewhere in your code.
The root of the problem is usually somewhere before whatever line (of your code) the stacktrace reports, so you'll have to find and fix it.

In this post I'll talk about a little bit how hibernate deals with objects, what can make this problem appear and how you can list what hibernate currently has in it's session.

The problem and how to spot it

Often the problem is solved with a merge(), but you should know why it solves the problem (so that you know what you are doing). I'll try to provide a good summary to help you understand what is going on, but do look at Working with Objects in Hibernate Docs for more in-depth knowledge.

So what is merge() and why is it "solving" the problem?

First of all, I'd like to say that if you didn't have a merge() in that area of your code, then you probably don't want to put one there. I say "probably" because you're likely seeing this issue now whereas before you didn't. You know your code and you should be the judge of what should and shouldn't be done. However, keep in mind that if you found a solution but you don't know the problem, then you won't be sure if you are solving it without causing side-effects. It's a bit like 42.

Like I said before, you're seeing this NonUniqueObjectException because you are probably trying to call saveOrUpdate() on a detached object while you have a version of it attached to the hibernate session. There are a number of ways this can happen:
  • Fetched an object, changed it, saved it, changed it again and saved it a second time. (When a hibernate session is closed, the objects are detached from it but they remain in the application's memory, meaning you can still change them).
  • Created a new object newX that is equal to one that exists in the database (oldX), fetch oldX, then save the newX.
Modifying the detached object then calling saveOrUpdate() will confuse hibernate as it was not keeping track of the changes. Instead of assuming you want the detached object and (possibly) saving bad data, hibernate throws NonUniqueObjectException and lets you fix the problem.

Check if you have any of the scenarios above (double fetching, double edit+save or something else which can create two versions of the same persistent object).

How does merge() work?

You can use it to attach an object to the current hibernate session without any consideration for what is already there. If you have two instances of Object A with the same identifier*, then (a copy of) the object that you pass in to merge() will be re-attached to the session, saved and returned back to you. The previous object in the session will be discarded.
* meaning they are "the same" as far your persistent layer (aka database) is concerned


Excuse my terrible drawing skills.

Above is a very rough picture of what happens when you call merge() when you have a version of Object A in the session (blue) and another version of the same object in memory (green). After calling merge, it will put a copy of the green Object A into the session and return it back to you (yellow).

In practical terms, this means that merge() will make sure the current state of the object that you passed into it will be persisted.

How is it different from saveOrUpdate()?

Before you say "So merge() is what I wanted to use all along!", you should understand what is the difference between these two. The hibernate documentation kindly provides this information (transcribed below):
saveOrUpdate() does the following:
  • if the object is already persistent in this session, do nothing
  • if another object associated with the session has the same identifier, throw an exception
  • if the object has no identifier property, save() it
  • if the object's identifier has the value assigned to a newly instantiated object, save() it
  • if the object is versioned by a or , and the version property value is the same value assigned to a newly instantiated object, save() it
  • otherwise update() the object

and merge() is very different:
  • if there is a persistent instance with the same identifier currently associated with the session, copy the state of the given object onto the persistent instance
  • if there is no persistent instance currently associated with the session, try to load it from the database, or create a new persistent instance
  • the persistent instance is returned
  • the given instance does not become associated with the session, it remains detached

How do I know if the object in the session is different?

Regardless of which scenario you're experiencing, you can compare what you have currently in the session and what you have in memory.
It's somewhat easy to list all the objects inside a hibernate session and the code snippet below can be helpful when you're debugging your application. static class HibernateSessionUtils { public static Map listAllObjects(Session session){ SessionImplementor sessionImplementor = (SessionImplementor) session; StatefulPersistenceContext context = (StatefulPersistenceContext) sessionImplementor.getPersistenceContext(); return context.getEntitiesByKey(); } } then just use it like so Map hibernateObjectsMap = HibernateSessionUtils.listAllObjects(session)
Hopefully by comparing the two objects (the one in the session and the one you have), you'll find out what the problem is.

How do I fix it?

Finally, the part that matters.
After knowing what you're experiencing, you should know how to avoid it (and if you can avoid it). So first you should try to prevent double-fetching or double-(edit+save)
If you cannot avoid the scenarios above, then it could be safe to use merge() or you may just want to evict() one of the objects.


Now that you know a little bit more about this subject and you still think merge() is the way to go, then go ahead and use it: Object attachedObject = session.merge(detachedObject);
Happy coding!

No comments:

Post a Comment