Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Transactions and flushing

A transaction groups several changes so that they are saved all together or not at all. Flushing is the moment Hibernate turns the changes collected in the persistence context into insert, update and delete statements. These are two different things: a flush sends SQL, but only the commit makes the changes permanent and visible to others.

The full pattern

// src/main/java/com/example/blog/Main.java
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
    tx.begin();
    em.persist(new Post("Transactions", null));
    em.persist(new Post("Flushing", null));
    IO.println("persist() called twice");
 
    Long count = em.createQuery("select count(p) from Post p", Long.class).getSingleResult();
    IO.println("Count inside the transaction: " + count);
 
    tx.commit();
    IO.println("Committed");
} catch (RuntimeException e) {
    if (tx.isActive()) {
        tx.rollback();
    }
    throw e;
} finally {
    em.close();
}

EntityTransaction is imported from jakarta.persistence. Output:

Hibernate: select next value for Post_SEQ
Hibernate: select next value for Post_SEQ
persist() called twice
Hibernate: insert into Post (author_id,title,views,id) values (?,?,?,?)
Hibernate: insert into Post (author_id,title,views,id) values (?,?,?,?)
Hibernate: select count(p1_0.id) from Post p1_0
Count inside the transaction: 2
Committed

When the flush happens

The insert statements were not sent by persist, but right before the count query. In the default mode FlushModeType.AUTO Hibernate flushes:

  • before a query whose result may depend on unsaved changes;
  • at commit();
  • when you call em.flush() yourself.

Thanks to this the query counted both new posts, although nothing had been committed yet. Other connections do not see these rows until the commit.

Rollback

runInTransaction follows the same pattern: if the lambda throws an exception, it rolls the transaction back and rethrows the exception.

// src/main/java/com/example/blog/Rollback.java
try {
    emf.runInTransaction(em -> {
        em.persist(new Post("Half-done", null));
        em.flush();
        throw new IllegalStateException("Something went wrong");
    });
} catch (IllegalStateException e) {
    IO.println("Error: " + e.getMessage());
}
 
Long count = emf.callInTransaction(em ->
        em.createQuery("select count(p) from Post p", Long.class).getSingleResult());
IO.println("Posts in the database: " + count);
Hibernate: select next value for Post_SEQ
Hibernate: insert into Post (author_id,title,views,id) values (?,?,?,?)
Error: Something went wrong
Hibernate: select count(p1_0.id) from Post p1_0
Posts in the database: 0

The insert really reached the database because of em.flush(), but the rollback cancelled it. A flush is not a commit.

Keep transactions short

A transaction holds a database connection and possibly row locks. Do not call slow external services or wait for user input inside it. In Spring Boot you mark a service method with @Transactional, and Spring runs exactly this begin–commit–rollback pattern around it.

Common mistake

If an exception is caught but the transaction is not rolled back, it stays open. The next begin() on the same EntityManager fails:

// src/main/java/com/example/blog/Main.java
try {
    em.getTransaction().begin();
    em.persist(new Post("Draft", null));
    throw new IllegalStateException("Something went wrong");
} catch (IllegalStateException e) {
    IO.println("Error: " + e.getMessage());
}
em.getTransaction().begin();
Error: Something went wrong
Exception in thread "main" java.lang.IllegalStateException: Resource-local transaction already active

Always roll back in catch, as in the full pattern above, or use runInTransaction, which does it for you.

Thanks for reading!

Donate to the site — we work hard for you.