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

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


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


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...


Entity states

JPA defines the following entity states:

New (Transient)

A newly created object that hasn’t ever been associated with a Hibernate Session (a.k.a Persistence Context) and is not mapped to any database table row is considered to be in the New (Transient) state.

To become persisted we need to either explicitly call the EntityManager#persist method or make use of the transitive persistence mechanism.

Persistent (Managed)

A persistent entity has been associated with a database table row and it’s being managed by the currently running Persistence Context. Any change made to such an entity is going to be detected and propagated to the database (during the Session flush-time).

With Hibernate, we no longer have to execute INSERT/UPDATE/DELETE statements. Hibernate employs a transactional write-behind working style and changes are synchronized at the very last responsible moment, during the current Session flush-time.

Detached

Once the currently running Persistence Context is closed all the previously managed entities become detached. Successive changes will no longer be tracked and no automatic database synchronization is going to happen.

Entity state transitions

You can change the entity state using various methods defined by the EntityManager interface.

To understand the JPA entity state transitions better, consider the following diagram:

JPA entity state transitions

When using JPA, to reassociate a detached entity to an active EntityManager, you can use the merge operation.

When using the native Hibernate API, apart from merge, you can reattach a detached entity to an active Hibernate Sessionusing the update methods, as demonstrated by the following diagram:

Hibernate entity state transitions

Merging a detached entity

The merge is going to copy the detached entity state (source) to a managed entity instance (destination).

Consider we have persisted the following Book entity, and now the entity is detached as the EntityManager that was used to persist the entity got closed:

Book _book = doInJPA(entityManager -> {    Book book = new Book()    .setIsbn("978-9730228236")    .setTitle("High-Performance Java Persistence")    .setAuthor("Vlad Mihalcea");     entityManager.persist(book);     return book;});

While the entity is in the detached state, we modify it as follows:

_book.setTitle(    "High-Performance Java Persistence, 2nd edition");

Now, we want to propagate the changes to the database, so we can call the merge method:

doInJPA(entityManager -> {    Book book = entityManager.merge(_book);     LOGGER.info("Merging the Book entity");     assertFalse(book == _book);});

And Hibernate is going to execute the following SQL statements:

SELECT    b.id,    b.author AS author2_0_,    b.isbn AS isbn3_0_,    b.title AS title4_0_FROM    book bWHERE    b.id = 1 -- Merging the Book entity UPDATE    bookSET    author = 'Vlad Mihalcea',    isbn = '978-9730228236',    title = 'High-Performance Java Persistence, 2nd edition'WHERE    id = 1

If the merging entity has no equivalent in the current EntityManager, a fresh entity snapshot will be fetched from the database.

Once there is a managed entity, JPA copies the state of the detached entity onto the one that is currently managed, and during the Persistence Context flush, an UPDATE will be generated if the dirty checking mechanism finds that the managed entity has changed.

So, when using merge, the detached object instance will continue to remain detached even after the merge operation.

Reattaching a detached entity

Hibernate, but not JPA supports reattaching through the update method.

A Hibernate Session can only associate one entity object for a given database row. This is because the Persistence Context acts as an in-memory cache (first level cache) and only one value (entity) is associated with a given key (entity type and database identifier).

An entity can be reattached only if there is no other JVM object (matching the same database row) already associated with the current Hibernate Session.

Considering we have persisted the Book entity and that we modified it when the Book entity was in the detached state:

Book _book = doInJPA(entityManager -> {    Book book = new Book()    .setIsbn("978-9730228236")    .setTitle("High-Performance Java Persistence")    .setAuthor("Vlad Mihalcea");     entityManager.persist(book);     return book;});      _book.setTitle(    "High-Performance Java Persistence, 2nd edition");

We can reattach the detached entity like this:

doInJPA(entityManager -> {    Session session = entityManager.unwrap(Session.class);     session.update(_book);     LOGGER.info("Updating the Book entity");});

And Hibernate will execute the following SQL statement:

-- Updating the Book entity UPDATE    bookSET    author = 'Vlad Mihalcea',    isbn = '978-9730228236',    title = 'High-Performance Java Persistence, 2nd edition'WHERE    id = 1

The update method requires you to unwrap the EntityManager to a Hibernate Session.

Unlike merge, the provided detached entity is going to be reassociated with the current Persistence Context and an UPDATE is scheduled during flush whether the entity has modified or not.

To prevent this, you can use the @SelectBeforeUpdate Hibernate annotation which will trigger a SELECT statement that fetched loaded state which is then used by the dirty checking mechanism.

@Entity(name = "Book")@Table(name = "book")@SelectBeforeUpdatepublic class Book {     //Code omitted for brevity}

Beware of the NonUniqueObjectException

One problem that can occur with update is if the Persistence Context already contains an entity reference with the same id and of the same type as in the following example:

Book _book = doInJPA(entityManager -> {    Book book = new Book()    .setIsbn("978-9730228236")    .setTitle("High-Performance Java Persistence")    .setAuthor("Vlad Mihalcea");     Session session = entityManager.unwrap(Session.class);    session.saveOrUpdate(book);     return book;}); _book.setTitle(    "High-Performance Java Persistence, 2nd edition"); try {    doInJPA(entityManager -> {        Book book = entityManager.find(            Book.class,            _book.getId()        );         Session session = entityManager.unwrap(Session.class);        session.saveOrUpdate(_book);    });} catch (NonUniqueObjectException e) {    LOGGER.error(        "The Persistence Context cannot hold " +        "two representations of the same entity",        e    );}

Now, when executing the test case above, Hibernate is going to throw a NonUniqueObjectException because the second EntityManager already contains a Book entity with the same identifier as the one we pass to update, and the Persistence Context cannot hold two representations of the same entity.

org.hibernate.NonUniqueObjectException:    A different object with the same identifier value was already associated with the session : [com.vladmihalcea.book.hpjp.hibernate.pc.Book#1]    at org.hibernate.engine.internal.StatefulPersistenceContext.checkUniqueness(StatefulPersistenceContext.java:651)    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.performUpdate(DefaultSaveOrUpdateEventListener.java:284)    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.entityIsDetached(DefaultSaveOrUpdateEventListener.java:227)    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.performSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:92)    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:73)    at org.hibernate.internal.SessionImpl.fireSaveOrUpdate(SessionImpl.java:682)    at org.hibernate.internal.SessionImpl.saveOrUpdate(SessionImpl.java:674)

Conclusion

The merge method is to be preferred if you are using optimistic locking as it allows you to prevent lost updates.

The update is good for batch updates as it can prevent the additional SELECT statement generated by the merge operation, therefore reducing the batch update execution time.