Tutorial :What is the proper way to re-attach detached objects in Hibernate?



Question:

I have a situation in which I need to re-attach detached objects to a hibernate session, although an object of the same identity MAY already exist in the session, which will cause errors.

Right now, I can do one of two things.

  1. getHibernateTemplate().update( obj ) This works if and only if an object doesn't already exist in the hibernate session. Exceptions are thrown stating an object with the given identifier already exists in the session when I need it later.

  2. getHibernateTemplate().merge( obj ) This works if and only if an object exists in the hibernate session. Exceptions are thrown when I need the object to be in a session later if I use this.

Given these two scenarios, how can I generically attach sessions to objects? I don't want to use exceptions to control the flow of this problem's solution, as there must be a more elegant solution...


Solution:1

So it seems that there is no way to reattach a stale detached entity in JPA.

merge() will push the stale state to the DB, and overwrite any intervening updates.

refresh() cannot be called on a detached entity.

lock() cannot be called on a detached entity, and even if it could, and it did reattach the entity, calling 'lock' with argument 'LockMode.NONE' implying that you are locking, but not locking, is the most counterintuitive piece of API design I've ever seen.

So you are stuck. There's an detach() method, but no attach() or reattach(). An obvious step in the object lifecycle is not available to you.

Judging by the number of similar questions about JPA, it seems that even if JPA does claim to have a coherent model, it most certainly does not match the mental model of most programmers, who have been cursed to waste many hours trying understand how to get JPA to do the simplest things, and end up with cache management code all over their applications.

It seems the only way to do it is discard your stale detached entity and do a find query with the same id, that will hit the L2 or the DB.

Mik


Solution:2

Undiplomatic answer: You're probably looking for an extended persistence context. This is one of the main reasons behind the Seam Framework... If you're struggling to use Hibernate in Spring in particular, check out this piece of Seam's docs.

Diplomatic answer: This is described in the Hibernate docs. If you need more clarification, have a look at Section 9.3.2 of Java Persistence with Hibernate called "Working with Detached Objects." I'd strongly recommend you get this book if you're doing anything more than CRUD with Hibernate.


Solution:3

All of these answers miss an important distinction. update() is used to (re)attach your object graph to a Session. The objects you pass it are the ones that are made managed.

merge() is actually not a (re)attachment API. Notice merge() has a return value? That's because it returns you the managed graph, which may not be the graph you passed it. merge() is a JPA API and its behavior is governed by the JPA spec. If the object you pass in to merge() is already managed (already associated with the Session) then that's the graph Hibernate works with; the object passed in is the same object returned from merge(). If, however, the object you pass into merge() is detached, Hibernate creates a new object graph that is managed and it copies the state from your detached graph onto the new managed graph. Again, this is all dictated and governed by the JPA spec.

In terms of a generic strategy for "make sure this entity is managed, or make it managed", it kind of depends on if you want to account for not-yet-inserted data as well. Assuming you do, use something like

if ( session.contains( myEntity ) ) {      // nothing to do... myEntity is already associated with the session  }  else {      session.saveOrUpdate( myEntity );  }  

Notice I used saveOrUpdate() rather than update(). If you do not want not-yet-inserted data handled here, use update() instead...


Solution:4

If you are sure that your entity has not been modified (or if you agree any modification will be lost), then you may reattach it to the session with lock.

session.lock(entity, LockMode.NONE);  

It will lock nothing, but it will get the entity from the session cache or (if not found there) read it from the DB.

It's very useful to prevent LazyInitException when you are navigating relations from an "old" (from the HttpSession for example) entities. You first "re-attach" the entity.

Using get may also work, except when you get inheritance mapped (which will already throw an exception on the getId()).

entity = session.get(entity.getClass(), entity.getId());  


Solution:5

I went back to the JavaDoc for org.hibernate.Session and found the following:

Transient instances may be made persistent by calling save(), persist() or saveOrUpdate(). Persistent instances may be made transient by calling delete(). Any instance returned by a get() or load() method is persistent. Detached instances may be made persistent by calling update(), saveOrUpdate(), lock() or replicate(). The state of a transient or detached instance may also be made persistent as a new persistent instance by calling merge().

Thus update(), saveOrUpdate(), lock(), replicate() and merge() are the candidate options.

update(): Will throw an exception if there is a persistent instance with the same identifier.

saveOrUpdate(): Either save or update

lock(): Deprecated

replicate(): Persist the state of the given detached instance, reusing the current identifier value.

merge(): Returns a persistent object with the same identifier. The given instance does not become associated with the session.

Hence, lock() should not be used straightway and based on the functional requirement one or more of them can be chosen.


Solution:6

I did it that way in C# with NHibernate, but it should work the same way in Java:

public virtual void Attach()  {      if (!HibernateSessionManager.Instance.GetSession().Contains(this))      {          ISession session = HibernateSessionManager.Instance.GetSession();          using (ITransaction t = session.BeginTransaction())          {              session.Lock(this, NHibernate.LockMode.None);              t.Commit();          }      }  }  

First Lock was called on every object because Contains was always false. The problem is that NHibernate compares objects by database id and type. Contains uses the equals method, which compares by reference if it's not overwritten. With that equals method it works without any Exceptions:

public override bool Equals(object obj)  {      if (this == obj) {           return true;      }       if (GetType() != obj.GetType()) {          return false;      }      if (Id != ((BaseObject)obj).Id)      {          return false;      }      return true;  }  


Solution:7

Session.contains(Object obj) checks the reference and will not detect a different instance that represents the same row and is already attached to it.

Here my generic solution for Entities with an identifier property.

public static void update(final Session session, final Object entity)  {      // if the given instance is in session, nothing to do      if (session.contains(entity))          return;        // check if there is already a different attached instance representing the same row      final ClassMetadata classMetadata = session.getSessionFactory().getClassMetadata(entity.getClass());      final Serializable identifier = classMetadata.getIdentifier(entity, (SessionImplementor) session);        final Object sessionEntity = session.load(entity.getClass(), identifier);      // override changes, last call to update wins      if (sessionEntity != null)          session.evict(sessionEntity);      session.update(entity);  }  

This is one of the few aspects of .Net EntityFramework I like, the different attach options regarding changed entities and their properties.


Solution:8

I came up with a solution to "refresh" an object from the persistence store that will account for other objects which may already be attached to the session:

public void refreshDetached(T entity, Long id)  {      // Check for any OTHER instances already attached to the session since      // refresh will not work if there are any.      T attached = (T) session.load(getPersistentClass(), id);      if (attached != entity)      {          session.evict(attached);          session.lock(entity, LockMode.NONE);      }      session.refresh(entity);  }  


Solution:9

Sorry, cannot seem to add comments (yet?).

Using Hibernate 3.5.0-Final

Whereas the Session#lock method this deprecated, the javadoc does suggest using Session#buildLockRequest(LockOptions)#lock(entity)and if you make sure your associations have cascade=lock, the lazy-loading isn't an issue either.

So, my attach method looks a bit like

MyEntity attach(MyEntity entity) {      if(getSession().contains(entity)) return entity;      getSession().buildLockRequest(LockOptions.NONE).lock(entity);      return entity;  

Initial tests suggest it works a treat.


Solution:10

try getHibernateTemplate().replicate(entity,ReplicationMode.LATEST_VERSION)


Solution:11

In the original post, there are two methods, update(obj) and merge(obj) that are mentioned to work, but in opposite circumstances. If this is really true, then why not test to see if the object is already in the session first, and then call update(obj) if it is, otherwise call merge(obj).

The test for existence in the session is session.contains(obj). Therefore, I would think the following pseudo-code would work:

if (session.contains(obj))  {      session.update(obj);  }  else   {      session.merge(obj);  }  


Solution:12

Perhaps it behaves slightly different on Eclipselink. To re-attach detached objects without getting stale data, I usually do:

Object obj = em.find(obj.getClass(), id);  

and as an optional a second step (to get caches invalidated):

em.refresh(obj)  


Solution:13

to reattach this object, you must use merge();

this methode accept in parameter your entity detached and return an entity will be attached and reloaded from Database.

Example :      Lot objAttach = em.merge(oldObjDetached);      objAttach.setEtat(...);      em.persist(objAttach);  


Solution:14

calling first merge() (to update persistent instance), then lock(LockMode.NONE) (to attach the current instance, not the one returned by merge()) seems to work for some use cases.


Solution:15

try getHibernateTemplate().saveOrUpdate()  

Note:If u also have question or solution just comment us below or mail us on toontricks1994@gmail.com
Previous
Next Post »