skip to content

When does Hibernate actually send SQL to the database under JpaTransactionManager, and how does flush-at-commit work?

level: seniorimportance: should knowfreq 40%

answer

  1. write-behind: changes buffered, flushed later
  2. FlushModeType.AUTO = before queries + at commit
  3. commit -> final flush (dirty check) -> JDBC COMMIT
  4. managed entity update needs no explicit save
  5. readOnly=true -> MANUAL flush -> no flush at commit

basics

~20 s

Hibernate batches changes in the persistence context and flushes them as SQL either automatically before queries or when JpaTransactionManager commits. At commit the manager triggers a final flush, then commits the JDBC transaction. Nothing is durable until that commit.

solid answer

~40 s

JpaTransactionManager doesn't push each save to the DB immediately. Hibernate accumulates entity state changes in the persistence context and flushes (emits INSERT/UPDATE/DELETE) at defined points governed by FlushModeType. With the default AUTO, Hibernate flushes before a query that could be affected and always at commit. When the @Transactional method returns, JpaTransactionManager.doCommit calls em.getTransaction().commit(), and Hibernate performs a final flush of all pending dirty state via automatic dirty checking, then the JDBC COMMIT makes it durable. Consequences: you rarely need explicit save() for a managed entity, constraint violations often surface only at commit/flush time (not at the save call), and setting the transaction read-only can switch the flush mode to MANUAL so the commit skips flushing. You can force early SQL with em.flush() to surface errors sooner.

code

java · 17 lines
java
@Transactional
public void rename(long id, String name) {
    Account a = em.find(Account.class, id); // now managed
    a.setName(name);                        // dirty, but NO SQL yet

    // ... method continues; still no UPDATE issued ...

    // Force early SQL to surface a unique-constraint error HERE, not at commit:
    em.flush(); // emits UPDATE now; still not committed
} // JpaTransactionManager.doCommit -> final flush (if any) -> JDBC COMMIT -> durable

@Transactional(readOnly = true)
public Account load(long id) {
    Account a = em.find(Account.class, id);
    a.setName("oops"); // flush mode is MANUAL under readOnly -> this change is NOT persisted
    return a;
}

go deeper

for a junior

Know changes are written at commit, not immediately, and commit makes them durable.

for a middle

Explain flush vs commit and that dirty checking updates managed entities without save().

for a senior

Discuss FlushModeType AUTO, delayed constraint errors, and explicit flush() to fail fast.

for a principal

Reason about readOnly MANUAL-flush optimization, commit-time exception escape, and batching/ordering effects on mixed JDBC access.

**Deferred writes.** JPA/Hibernate is *write-behind*. When you call `persist`, `merge`, or mutate a managed entity, Hibernate records the intent in the persistence context (the unit of work) but does not immediately hit the database. The actual SQL is emitted at a **flush**. **What a flush is.** Flushing is Hibernate synchronizing the in-memory persistence context with the database: it runs **automatic dirty checking** (compares each managed entity against its loaded snapshot), orders the operations, and executes the batched `INSERT`/`UPDATE`/`DELETE` statements on the JDBC connection. A flush is NOT a commit — the transaction is still open and the changes are not yet durable/visible to other transactions. **When flushes happen — FlushModeType.** - **AUTO (default):** Hibernate flushes (1) automatically **before executing a query** whose results could be affected by pending changes (to keep queries consistent with in-memory state), and (2) **before commit**. - **COMMIT:** flush only at commit, not before queries (risking stale query results vs. pending changes). - **MANUAL / (Hibernate) NEVER:** never flush automatically; you must call `em.flush()` explicitly. Spring uses this for read-only transactions. **Flush-at-commit with JpaTransactionManager.** When the `@Transactional` method completes successfully, Spring's transaction interceptor asks `JpaTransactionManager` to commit. In `doCommit` it calls `EntityTransaction.commit()` on the bound `EntityManager`. Hibernate's commit path performs a final flush (unless flush mode is MANUAL/read-only) so all dirty state is written, then issues the JDBC `COMMIT`. Only after that COMMIT are the changes durable and visible to other transactions. **Practical consequences / gotchas.** - **No explicit save needed for managed entities.** Loading an entity in a transaction, mutating a field, and returning triggers an `UPDATE` at commit via dirty checking — even without calling `repository.save()`. - **Delayed constraint errors.** A unique-constraint or not-null violation may not throw at the `save()` line; it surfaces at flush/commit. This is a classic 'the exception points at the wrong place' confusion. Call `em.flush()` to fail fast where you can handle it. - **Post-commit is too late to catch.** If the failure happens during the commit-time flush, it escapes *after* your method body; catch-and-recover logic inside the method won't see it. Use explicit `flush()` or `saveAndFlush()`. - **Read-only optimization.** `@Transactional(readOnly = true)` sets the Hibernate session's flush mode to MANUAL (historically `FlushMode.NEVER`), so the commit does no flush — dirty changes are silently NOT persisted. Great for read paths, dangerous if you accidentally mutate. - **Ordering surprises.** Because Hibernate reorders and batches, the SQL order at flush may differ from your call order; interleaving native JDBC that reads uncommitted rows can miss pending changes unless you flush first. - **Exceptions and rollback:** if flush at commit fails, Spring marks the transaction rolled back and wraps the `PersistenceException` as a `DataAccessException`.

  • Why might a NOT NULL violation throw at commit rather than at the save() call?
    Hibernate defers SQL until flush. With FlushMode AUTO the INSERT/UPDATE is emitted at the commit-time flush (or before an intervening query), so the DB error surfaces then, not when save() returned.
  • What does @Transactional(readOnly = true) do to flushing?
    Spring sets the Hibernate flush mode to MANUAL, so JpaTransactionManager's commit does not flush. Accidental entity mutations are not persisted, and it enables driver/DB read-only optimizations.
  • How do you make a persistence error occur where you can catch it?
    Call em.flush() (or repository.saveAndFlush()) inside the transaction so the SQL and any exception happen at that line, before the commit boundary.

saying these in an interview costs you the question

  • Believing save()/persist() immediately writes to the database
  • Thinking a mutated managed entity needs an explicit save to be updated
  • Not knowing readOnly=true suppresses the commit-time flush (silent lost update)
  • Expecting to catch a commit-time constraint violation inside the method body

context