Dev Learning Lab

The labs on this site need JavaScript to run. The same content is written out below.

Fundamentals › JPA

Persistence context

What is a persistence context, and what does it do for you?

A persistence context is the place where the EntityManager (em in the code) holds entity objects and watches them. An entity is a Java object that corresponds to one row in the database, and here it is Member. An entity that you read from the database with em.find goes into the persistence context (an entity passed to em.persist, which you use to save a new entity, goes in too, but this lab does not use it). JPA watches the entities inside it, so if you only change a value, JPA builds an UPDATE statement and sends it when the transaction commits. If you call find again with the same id, it does not go to the database and gives you the object it was holding. On the other hand, an object you create yourself with new, or one you removed with em.detach, is an ordinary Java object that is not in the persistence context, so changing its values does not reach the database. This lab changes how you get the Member and runs the same code (change name to Bob and commit), and shows the name in the database after the commit and the number of UPDATE and SELECT statements that were sent.

Checked against real execution · Ran all 6 combinations on Hibernate 7.4.5 and H2 2.4.240, and checked against them (the rules were checked against the Jakarta Persistence 3.2 specification)

Concepts in this lab

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.
First-level cache
The store inside the persistence context that keeps entities by identifier. If you load the same identifier again within the same transaction, it returns the stored entity without sending a query to the database. As a result, entities with the same identifier are always the same object within the same transaction. By default it disappears when the transaction ends (in a web request with Open Session in View (OSIV) turned on, it lasts until the request ends), so it is different from an application-wide cache.
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.

Try these

Running with the default settings

  1. Setup in the database: before the code runs, the member table in the database already holds one row with id 1, (1, Alice). This row was put in ahead of time, outside the code on the screen. em in the code is an EntityManager object, and the place where em holds entities (Java objects that correspond to one row in the database, here Member) and watches them is the persistence context. It is empty now.
  2. em.getTransaction().begin(): starts a transaction. From here to commit is one unit of work.
  3. Member m = em.find(Member.class, 1L): em looks for the Member with id 1. It is not in the persistence context yet, so JPA builds a SELECT statement and sends it to the database, makes a Member object from the values it read, and puts it into the persistence context. JPA also remembers the values at this moment (Alice). m points to this object. The line with the SELECT is not in the code we wrote. JPA made it and sent it.
  4. m.setName("Bob"): only the name of the Java object changes to Bob. No SQL is sent on this line, and the name in the database is still Alice. The object inside the persistence context now has a current value (Bob) that differs from the first value that JPA remembers (Alice).
  5. em.getTransaction().commit(): at commit, JPA compares the current values of the entities held in the persistence context with the first values it remembered. The name changed from Alice to Bob, so JPA builds an UPDATE statement and sends it to the database. The code we wrote has no UPDATE and no call that says to save. The UPDATE was sent not on the m.setName line but on this commit line.
  6. After the commit finishes, if you query the member table in the database through another connection, the name of the row with id 1 is Bob. (This query is only for checking, so it is not counted in the number of SELECT statements sent.)

Result of this run

Code

em.getTransaction().begin();
// find: the entity read from the database goes into the persistence context
Member m = em.find(Member.class, 1L);
m.setName("Bob");
em.getTransaction().commit();

Assumptions of this model

This was checked against real execution on Hibernate 7.4.5 and H2 2.4.240 (in-memory database). A SessionFactory was built directly without Spring, on JDK 21. All 6 combinations were compiled and run with the lines of code on the screen exactly as they are (including the comment line). For each combination, the database and the SessionFactory were created anew, and one row (1, Alice) was put into the database with JDBC before the run. The SQL of this setup was not counted. What does not appear on the screen is the Member entity (with @Entity, an id with @Id, a name, a constructor, setName and getName) and the code that gets and closes the EntityManager, and the transaction was started and committed directly on em. What was checked against the run is the name read through another connection (plain JDBC) after the commit, the number of UPDATE and SELECT statements counted with Hibernate's StatementInspector (the SQL text and the lines that sent it were kept in the output of the checking run), and the m == m2 that the code printed on the screen. The facts that the step sentences rely on were also measured in a second run that inserts a measuring line right below each line of code. They are em.contains(m) (true after find, and false for an object created with new and after detach), that the UPDATE is sent not on the m.setName line but on the commit line, that when find is called again with the same id, the find combination does not add a SELECT and receives the same object whose name is already Bob even though the commit has not happened yet (and that the new and detach combinations add a SELECT and receive a new object whose name is Alice), and that Hibernate separately remembers the values from the time it read them (seen through an internal API, [Alice] after the read and no memory after detach). It was also run separately that if you change the name to Bob and then change it back to Alice before the commit, no UPDATE is sent, and this was used as the evidence that it compares with the first values, not with whether a changing call was made. It was also run that if you pass an object created with new to em.persist, em.contains becomes true and an INSERT is sent at commit. What the documentation defines is in the Jakarta Persistence 3.2 specification: Section 3.1 (each EntityManager has a persistence context, and it holds only one entity instance for any given identifier), Section 3.3 (the lifecycle of an entity instance: managed is the state of belonging to a persistence context, and detached is the state of not belonging or no longer belonging), Section 3.3.2 (becoming managed with persist), Section 3.3.4 (the state of a persistent entity is synchronized to the database at transaction commit), and Sections 3.3.6 and 3.3.7 (an instance removed from the persistence context lives on outside it). The specification page came back only as a summary, so the full original text of these sections was not read. The way of remembering the first values and comparing them, and the shape of the SQL that is sent, are not defined by the specification but are behavior observed on Hibernate 7.4.5, so the screen shows only the counts, not the SQL text. This lab has the shape of creating em directly, so it does not cover using it with Spring's @Transactional (where the persistence context is created and ended together with the transaction), write-behind, the flush just before a query runs, or saving a new entity with em.persist, and the details are covered in the lab "When does JPA send SQL?". A check against execution on MySQL was not done.

Previous lab: Entities · Next lab: Repositories

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these