How to Re-Attach Detached Objects in Hibernate

How do I persist a detached object?

JPA does not allow you to reattach detached objects.

The JPA specification defines the merge() operation. The operation seems to be useful to implement the described use case.

Please refer to the specification:

3.2.7.1 Merging Detached Entity State


The merge operation allows for the propagation of state from detached entities onto persistent entities managed by the entity manager. The semantics of the merge operation applied to an entity X are as follows:

  • If X is a detached entity, the state of X is copied onto a pre-existing managed entity instance X' of the same identity or a new managed copy X' of X is created.
  • If X is a new entity instance, a new managed entity instance X' is created and the state of X is copied into the new managed entity instance X'.
  • If X is a removed entity instance, an IllegalArgumentException will be thrown by the merge operation (or the transaction commit will fail).
  • If X is a managed entity, it is ignored by the merge operation, however, the merge operation is cascaded to entities referenced by relationships from X if these relationships have been annotated with the cascade element value cascade=MERGE or cascade=ALL annotation.
  • For all entities Y referenced by relationships from X having the cascade element value cascade=MERGE or cascade=ALL, Y is merged recursively as Y'. For all such Y referenced by X, X' is set to reference Y'. (Note that if X is managed then X is the same object as X'.)
  • If X is an entity merged to X', with a reference to another entity Y, where cascade=MERGE or cascade=ALL is not specified, then navigation of the same association from X' yields a reference to a managed object Y' with the same persistent identity as Y.

The persistence provider must not merge fields marked LAZY that have not been fetched: it must ignore such fields when merging.

Any Version columns used by the entity must be checked by the persistence runtime implementation during the merge operation and/or at flush or commit time. In the absence of Version columns there is no additional version checking done by the persistence provider runtime during the merge operation.

— JSR 338: JavaTM Persistence API, Version 2.1, Final Release.

Reattaching a detached instance: hibernate lock()

//Changes made before the call to lock() aren’t propagated to the database
item.setInitialPrice(new BigDecimal(179));

This is the expected behavior, since the item object is detached, and detached objects are not subject to automatic dirty checking, so no change is propagated to the database in this particular state.

Once you reattach it:

sessionTwo.buildLockRequest(LockOptions.NONE).lock(item);

the dirty checking will be applied at flush time. When you reattach an entity, Hibernate issues an SQL SELECT to fetch the latest entity state, which will be used at flush time to compare it against the in-memory object data.

That's why after lock (even for LockOptions.NONE), you see the changes propagated.

If you comment the lock method, then no change will be propagated, since the second session
is unaware of the detached item.

Hibernate: how to reattach an externalized entity that has an @ElementCollection

Picking up where the posters left off from the links in the question, here's the approximate/relevant code for how I managed to reattach. Below that is an outline of what's going on.

@Repository
public abstract class DaoHibernate<T> implements Dao<T> {

@Override
public T reattach(T entity) {
if (getCurrentSession().contains(entity)) {
return entity;
}
if (entity instanceof User) {
return (T) reattachedUser((User) entity);
}
if (entity instanceof Content) {
Content content = (Content) entity;
User user = content.getUser();
if (!currentSession().contains(user)) {
content.setUser(reattachedUser(user));
}
content.setAttributes(persistentAttributesMap(content.getId(), content.getAttributes(), Content.class);
getCurrentSession().lock(content, LockMode.NONE);
return entity;
}
throw new UnsupportedOperationException("reattach is not supported for entity: " + entity.getClass().getName());
}

private User reattachedUser(User user) {
user.setAttributes(persistentAttributesMap(user.getId(), user.getAttributes(), User.class));
getCurrentSession().lock(user, LockMode.NONE);
return user;
}

@SuppressWarnings ("unchecked")
private Map<String, String> persistentAttributesMap(long id, Map<String, String> attributes, Class clazz) {
SessionFactory sessionFactory = getSessionFactory();
Session currentSession = sessionFactory.getCurrentSession();
String role = clazz.getName() + ".attributes";
CollectionPersister collectionPersister = ((SessionFactoryImplementor) sessionFactory).getCollectionPersister(role);
MapType mapType = (MapType) collectionPersister.getCollectionType();
PersistentMap persistentMap = (PersistentMap) mapType.wrap((SessionImplementor) currentSession, attributes);
persistentMap.setOwner(id);
persistentMap.setSnapshot(id, role, ImmutableMap.copyOf(attributes));
persistentMap.setCurrentSession(null);
return persistentMap;
}

...
}

Walk through

As you can see, we have to ensure we never try to reattach an entity that is already in the current session, or else hibernate will throw an exception. That's why we have to do getCurrentSession().contains(entity) in reattach(). Care must be taken here using contains(), because hibernate will not use entity.hachCode() to lookup the entity, but rather System.identityHashCode(entity), which ensures not only that it is an equivalent instance, but the exact same instance that may already be in the session. In other words, you will have to manage reusing instances appropriately.

As long as associated entities are marked with Cascade.ALL, hibernate should do the right thing. That is, unless you have a hibernate managed collection like our @ElementCollection map of attributes. In this case, we have to manually create a PersistentCollection (PersistentMap, to be precise) and set the right properties on it, as in persistentAttributesMap, or else hibernate will throw an exception. In short, on the PersistentMap, we have to:

  • Set the owner and snapshot key as the id of the owning entity
  • Set the snapshot role as the fully qualified entity.property name, as hibernate sees it
  • Set the snapshot Serializable argument as an immutable copy of the existing collection
  • Set the session to null so hibernate won't think we're trying to attach it to the existing session twice

To complete the reattachment, call session.lock(entity, LockMode.NONE). At this point, as far as I can tell from my testing, hibernate respects this entity and persists all changes correctly when you call saveOrUpdate().

Caveats

I realize this is not a generic solution for all cases. This was just a quick solution to my specific problem that others can hopefully utilize and improve upon. Software is iterative.

Reattaching and manipulating a detached instance in Hibernate (JPA)

I think you can just use the detached Object o and after just to use the session.update(o).

Try to watch this video tutorial, maybe it will be useful!

Ciao!

re-attach a detached entity with no SQL query executed

Hibernate.update method doesn't access database to retrieve any entity information if this is you concern. It adds the entity to the persistent context (re-attach), then HH will execute update-queries to save modification of this tracked entity when you perform flush or the underlying transaction is committed.

But to use it, the entity must had been retrieved in a previous session. You can not pass new object/entity instance to it.



Related Topics



Leave a reply



Submit