The labs on this site need JavaScript to run. The same content is written out below.
Learn › JPA › Persistence context
The lifecycle of an entity
Why is the same setName sometimes not applied to the DB?
An entity object is in one of four states: new, managed, detached, or removed (Hibernate calls the new state transient). JPA watches the changes of values and applies them to the DB only when the object is in the managed state.
Checked against real execution · 14 combinations were run on Hibernate 7.4.5 and H2, and even the SQL sent on each line was checked against the results
Concepts in this lab
- 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.
- 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.
- 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.
- 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.
- Transactional write-behind and flush
- Transactional write-behind means that INSERT, UPDATE, and DELETE statements are not sent to the database right away. They are collected and sent together. However, for an entity whose primary key is generated with the IDENTITY strategy (such as AUTO_INCREMENT in MySQL), the INSERT runs immediately when you request the save, because the identifier must be obtained. A flush sends the collected changes to the database. It happens automatically just before the transaction commits and just before a JPQL query runs. A flush only sends SQL to the database. It does not commit the transaction.
Try these
- Default settings — You change the name of a detached object. It is not applied, and there is no error.
- Changing it while managed — Even if you only change the name of a loaded object, an UPDATE is sent at commit.
- em.clear after the change — You changed it while the object was managed, yet the change is lost. This is because it had not been sent to the DB yet.
- Forgetting persist after new — No SQL is sent at all.
- Changing it after persist — An UPDATE is sent once more after the INSERT.
- Changing it after closing em — This is a common mistake: modifying an entity outside the transaction.
- Changing it after remove — Only a DELETE is sent, and it is sent at commit.
Running with the default settings
- em.find(Member.class, 1L): A SELECT reads member 1 and keeps it in the persistence context. From now on, m is in the managed state. The persistence context remembers the value it first read (Alice) and watches m for changes.
- em.detach(m): Takes m out of the persistence context. m changed from managed to detached. No SQL is sent.
- m.setName("Bob"): Changed the name of a detached object. The value of the Java object changes, but the persistence context no longer watches this object. No error occurs either.
- em.contains(m) is false. It means that the persistence context no longer manages m.
- commit: A flush happens just before the commit, but m is not managed, so it is not compared. No UPDATE is sent, and the name in the DB is still "Alice".
Result of this run
- A detached object is like an ordinary Java object. If you change its values, no exception is thrown, and the change is not applied to the DB. If you load the same id again with em.find after detaching, the SELECT is sent again and you get a new object that is different from m.
- To apply the changes of a detached object to the DB, you have to copy its values into a managed object with merge. Merge is covered in the next lab.
Code
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Member m = em.find(Member.class, 1L);
em.detach(m);
m.setName("Bob");
System.out.println(em.contains(m));
em.getTransaction().commit();
Assumptions of this model
All 14 combinations (7 paths, each with two choices for when the name is changed) 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, and the rows left in the DB were confirmed to match the screen. The setting "When the name is changed" is hidden for the paths where it does not affect the result: only new, load only, and remove. The following facts stated in the summaries were also run separately and confirmed: if you call em.flush() after changing a value and then detach, the change is applied; if you load the same id again after detaching, the SELECT is sent again and a different object comes back; if you call persist again on a removed object before the commit, no DELETE is sent; the values of the Java object can still be read after it was deleted; if you change a value after persist, the INSERT contains the value from the time of persist. The id of the member is set directly in the code. With the IDENTITY strategy, where the DB decides the id, the INSERT is sent right on the persist line. The SQL statements on the screen are shortened to be easy to read. In Spring, the begin, commit, and close here happen on your behalf when a @Transactional method starts and ends (in a web request with Open Session in View (OSIV) turned on, em is closed when the request ends).
Previous lab: Persistence context · Next lab: Detached entities and merge