Post

Spring) JPA Persistence Context / Transactions / Entity States

Spring) JPA Persistence Context / Transactions / Entity States

This post was migrated from Tistory. You can find the original here.

JPA

Persistence Context


Persistence context: a space created to manage Entity objects efficiently and conveniently. It maintains and moves the lifecycle (how long it lives) and location (where it lives) of an object.

To access and manipulate the persistence context, you need an EntityManager, which is created through an EntityManagerFactory.


An EntityManagerFactory is normally created once per database and used for the entire lifetime of the application.
(Before Spring Boot JPA, you’d create a persistence.xml file under /resources/META-INF/ to hold this configuration.)

1
2
EntityManagerFactory emf = Persistence.createEntityManagerFactory("memo");
EntityManager em = emf.createEntityManager();

Transactions

A transaction is a logical concept used to preserve the integrity and consistency of data in a database — it is a logical unit of work performed to change the state of the database.
Integrity: the data values are correct
Consistency: the values of certain pieces of data agree with each other

1
2
3
4
5
6
7
8
START TRANSACTION; # Start a transaction

INSERT INTO memo (id, username, contents) VALUES (1, 'Robbie', 'Robbie Memo');
INSERT INTO memo (id, username, contents) VALUES (2, 'Robbert', 'Robbert Memo');
SELECT * FROM memo;

COMMIT; # Commit the transaction
SELECT * FROM memo;

Just as a database transaction can contain multiple SQL statements and only permanently applies the changes at the end, JPA holds all the information about changed objects being managed by the persistence context in a write-behind store, and only requests the SQL from the DB all at once at the end to apply the changes.

1
2
3
4
5
6
7
8
9
10
11
12
EntityTransaction et = em.getTransaction(); // Get the EntityTransaction from the EntityManager.
et.begin(); // Start the transaction.
try {
    ...
    em.persist(entity);
    et.commit(); // If it completes successfully, apply the changes to the db
} catch (Exception ex) {
    et.rollback(); // Roll back on failure
} finally {
    em.close(); // Close the manager that was used
}
emf.close(); // Close the factory that was used

If even a single SQL statement in the transaction fails, all changes are rolled back (consistency).
em.flush() is called automatically after em.commit(). It’s flush that’s responsible for sending the SQL statements from the write-behind store to the DB.

Persistence Context Features

Internally, the persistence context has a first-level cache. It’s structured like a Map, where the key is the primary key (i.e., the identifier value) and the value is the corresponding entity object.

Entity lookup

If the id being looked up doesn’t exist in the cache, JPA runs a SELECT against the DB and caches the result; if it does exist, the cached entity object is returned immediately.

Using the first-level cache reduces the number of queries and guarantees object identity.

1
2
3
        Memo memo1 = em.find(Memo.class, 1);
        Memo memo2 = em.find(Memo.class, 1);
        System.out.println(memo1 == memo2); //true

Object identity means the two references point to the same object in memory.

Entity removal


For removal, just like lookup, if the object isn’t in the first-level cache it’s fetched with a SELECT first, and then the entity is marked as deleted. The actual DELETE SQL runs at commit time.

Entity modification
What if an UPDATE SQL statement were added to the write-behind store every single time an Entity stored in the persistence context changed? That would be inefficient, since it would issue multiple queries for something that could be done with a single SQL statement.


JPA solves this UPDATE problem by keeping track of the initial state (the “loaded state”), and this process is called dirty checking.
When em.flush() is called, JPA compares the entity’s current state against its initial state, and if there are changes, generates an UPDATE SQL statement, stores it in the write-behind store, and then sends all the SQL statements to the DB.

Entity States

  • Transient: not managed by the persistence context.
    Memo memo = new Memo();
  • Managed: managed by the persistence context.
    em.persist(entity)
  • Detached: was managed, but has since been detached.
    em.detach(entity)
  • Removed: in the DELETED state.
    em.remove(entity)

em.merge(entity): used to bring a detached entity back into the managed state.
If the entity isn’t already in the persistence context, JPA looks it up in the DB and runs an UPDATE SQL statement with the values from the entity passed in as an argument. If it doesn’t exist in the DB, a newly created entity is stored in the context and an INSERT SQL statement is executed.

What’s the difference between em.persist() and em.merge()?

1
2
3
  Memo mergedMemo = em.merge(memo);
  System.out.println(em.contains(memo)); //false
  System.out.println(em.contains(mergedMemo)); //true

Even after calling merge() on a detached memo, that memo object is still not stored in the persistence context — only the returned entity is.

Is there a difference in behavior?

1
2
3
4
5
            Memo memo = new Memo();
            memo.setId(2L);
            memo.setContents("11");
            memo.setUsername("choi22");
            em.persist(memo);

When there’s no matching value in the cache, persist doesn’t check the DB first — it just goes straight to an INSERT. So if a row with identifier 2 already exists in the DB, the code above throws a SQL error.
Using merge instead would look up and load the row from the DB, and then perform an UPDATE if there are changes.

Up next

Let’s take a look at how Spring Boot handles JPA.

2023.07.13 - [spring] - Spring) Spring Boot’s JPA @Transactional/SimpleJpaRepository (23-07-09)

This post is licensed under CC BY-NC 4.0 by the author.