The labs on this site need JavaScript to run. The same content is written out below.
Learn › JPA › Persistence context
Detached entities and merge
Why were other values erased when I only fixed the name with merge?
A detached object, which is outside the persistence context, is not applied to the DB even if you change its values. There are two ways to apply it. One is the merge operation, where you pass the whole object to em.merge. The other is dirty checking, where you load the entity again and put in only the values to change.
Checked against real execution · 18 combinations were run on Hibernate 7.4.5 and H2, and the SQL of each line and the values left in the DB were checked against the results
Concepts in this lab
- Detached entity
- The state of an entity that the persistence context managed but no longer manages. When you detach an entity with em.detach, empty the persistence context with em.clear, or close it with em.close, the entities that were in it become detached. A detached entity still has its identifier (id) value, but it is like an ordinary Java object, so even if you change its values, no UPDATE is sent and no exception is thrown. An object created with new from the values received from a screen is also treated as detached if it has the id of a row that already exists in the DB. You often meet it in code that modifies an entity after the transaction has ended.
- Merge operation
- The feature that copies the values of a detached object onto a managed entity so that they are applied to the DB, called with em.merge. It finds the managed entity with the id of the object you passed (reading it with a SELECT if it is not in the persistence context), overwrites it with the values of all the fields of the object you passed, and then returns that managed entity. The object you passed does not become managed, so you must do the later work with the returned object. You can pass a newly created object as well as a detached one, and in that case a new managed entity is created and the values are copied. Because it overwrites all the fields, a field left empty in the object you passed also becomes null in the DB. To change only some values, it is safer to load the entity and put in only the values to change, which is the dirty checking approach.
- Dirty checking
- The feature that automatically runs UPDATE statements for changed entities. When the values of an entity managed by the persistence context change, JPA compares the entity with the state it first read at flush time (usually just before the commit), and it runs an UPDATE statement for each entity whose values differ. By default, Hibernate puts all columns in the UPDATE statement, not only the changed columns. To include only the changed columns, use @DynamicUpdate. So if you only change the values of an entity that you loaded, the change is applied to the database without calling a separate save method.
- Persistence context
- The logical space in JPA that holds and manages entities. Entities that you load or save are kept here, and JPA tracks the changes made to them and applies those changes to the database when the transaction ends. It usually shares its lifecycle with one transaction (in a web request with Open Session in View (OSIV) turned on, it lasts until the request ends). Features such as the first-level cache, dirty checking, and transactional write-behind work on top of it.
- The four entity states
- The classification of how an entity object relates to the persistence context, also called the entity lifecycle. New means that the object was only created with new and JPA does not know about it (Hibernate calls this state transient). Managed means that the persistence context manages the object, because you handed it over with em.persist or loaded it. Detached means that the object was managed and then came off. Removed means that the object was made a target to delete with em.remove. Features such as dirty checking, where changing a value sends an UPDATE, and the first-level cache work only for entities in the managed state. So when a value is not applied to the DB, first check which state the object is in now.
Try these
- Default settings — You merge an object with only the name filled in. city becomes NULL.
- Load again and move only the name — city stays as it is (dirty checking).
- Do nothing — Changing only the values of a detached object does not apply them.
- Merge a loaded object — All fields are filled in, so city is kept.
- Fix the passed object after merge — The passed object is still detached, so the change is not applied.
- Fix the returned object after merge — The returned object is managed, so the change is applied.
Running with the default settings
- new Member() and setId(1L): You created a new object to hold the values received from the screen, and put in 1, the id of the member to fix. name and city are still null. It has the id of a row that already exists in the DB, but the persistence context does not manage it, so it is treated as detached.
- m.setName("Bob"): Changed the name of the detached object. Only the value in memory changes, and no SQL is sent. No persistence context is watching this object.
- First step of em.merge(m): It looks for a managed entity with the id of m (1). The newly opened persistence context has nothing, so it reads the entity from the DB with a SELECT (Alice, Seoul). This is the SELECT that is sent during merge.
- Second step of em.merge(m): It overwrites that managed entity with the values of all the fields of m. name becomes Bob, and city is overwritten with the null that m had left empty. merge returns this managed entity as r. The object m that you passed stays detached.
- The result of em.contains(m) is false, and the result of em.contains(r) is true. They are different objects, and what the persistence context manages is r.
- commit: In the flush just before the commit, the values of the managed entity r are compared with the values first read (Alice, Seoul), and an UPDATE is sent. In row 1 of the DB, name is Bob and city became NULL. You meant to fix only the name, but city was erased.
Result of this run
- merge does not pick only the changed fields. It overwrites with all the fields of the object you passed. An object built from just the name received from the screen has a null city, so the city in the DB becomes NULL too. This is how accidents happen where fields that are not on the edit screen get erased.
- merge sends one SELECT to get a managed entity. If an entity with the same id is already in the persistence context, it overwrites that entity without a SELECT, and if the id is not in the DB, it confirms with a SELECT that there is none and then sends an INSERT.
Code
// An object built from the values received from the screen. city was not received
Member m = new Member();
m.setId(1L);
m.setName("Bob");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Member r = em.merge(m);
System.out.println(em.contains(m) + ", " + em.contains(r));
em.getTransaction().commit();
Assumptions of this model
All 18 combinations were run on real Hibernate 7.4.5 and an H2 database, and the kind of SQL sent on each line, the result of em.contains, whether the object returned by merge is different from the one you passed, and the name and city left in the DB were confirmed to match the screen. The setting "If you change city to Busan after the merge" has meaning only when merge is chosen. The "SQL sent in this transaction" does not include the SELECT that loaded m in the earlier request. The following facts stated in the summaries were also run separately and confirmed: if you merge an object whose id is not in the DB, an INSERT is sent after the SELECT; if you merge a loaded object without changing its values, only the SELECT is sent and there is no UPDATE; if an entity with the same id is already managed, merge does not send a SELECT; even if city in the DB was changed to another value after you loaded the object, merge overwrites it with the old value of m. The id of the member is set directly in the code, and no @Version field is used. With @Version, an attempt to overwrite with an old value can be stopped by an exception. By default, Hibernate puts all the columns in the UPDATE statement for both merge and dirty checking, so the difference between the two ways is not in the shape of the UPDATE statement but in the values it carries. The SQL statements on the screen are shortened to be easy to read. The save method of Spring Data JPA calls merge internally when it decides that the entity is not new (this is based on the official documentation and was not run here).
Previous lab: The lifecycle of an entity · Next lab: Entity mapping and primary keys