skip to content

How does readOnly=true interact with Hibernate FlushMode and dirty checking?

level: seniorimportance: should knowfreq 45%

answer

  1. AUTO vs COMMIT vs MANUAL
  2. HibernateJpaDialect sets MANUAL on begin
  3. Loaded-state snapshot = dirty checking
  4. setDefaultReadOnly skips snapshot -> less memory
  5. Doesn't touch isolation/locks/L2 cache

basics

~10 s

Spring's Hibernate integration sets the Session FlushMode to MANUAL when readOnly=true. That disables the automatic flush before queries and at commit, so Hibernate skips dirty-checking of managed entities, saving the snapshot-and-compare work.

solid answer

~40 s

When a transaction is readOnly, Spring's HibernateJpaDialect (via beginTransaction) sets the Hibernate Session's FlushMode to MANUAL. Normally Hibernate uses FlushMode.AUTO: it flushes before a query that might read stale data and again at commit, and to do that it maintains a loaded-state snapshot of every managed entity and diffs each at flush time (dirty checking) to generate UPDATEs. MANUAL suppresses those automatic flushes entirely, so the diffing never runs at commit and no UPDATEs are emitted. Additionally, entities loaded in a read-only session can be treated as read-only so Hibernate need not retain the dehydrated snapshot, reducing persistence-context memory. The effect: cheaper reads and no accidental writes via auto-flush. It does not change isolation level, locking, or the second-level cache. Explicit flush() overrides MANUAL and still works.

code

java · 8 lines
java
// Conceptually what Spring's HibernateJpaDialect does on a readOnly tx:
Session session = entityManager.unwrap(Session.class);
if (definition.isReadOnly()) {
    session.setHibernateFlushMode(FlushMode.MANUAL); // no auto-flush
    // entities may be loaded read-only, skipping the dirty-check snapshot
}
// ... your query-only work ...
// At commit, Spring does NOT flush -> no dirty-checking UPDATEs.

go deeper

for a junior

Know FlushMode is set to MANUAL so no auto-flush happens.

for a middle

Distinguish AUTO/COMMIT/MANUAL and tie MANUAL to skipped dirty checking.

for a senior

Explain the loaded-state snapshot, memory savings, and read-your-writes gotcha.

for a principal

Places it against isolation/locking/L2 cache and connection-lifecycle concerns.

## FlushMode background Hibernate's `FlushMode` (JPA's `FlushModeType`) controls when the persistence context is synchronized to the database: - `AUTO` (default) — flush before queries that could be affected by pending changes, and at transaction commit. - `COMMIT` — flush only at commit. - `MANUAL` (Hibernate-specific; JPA has no direct equivalent) — never flush automatically; only an explicit `flush()` synchronizes. ## What Spring does for readOnly With `JpaTransactionManager` + Hibernate, when `TransactionDefinition.isReadOnly()` is true, Spring's `HibernateJpaDialect.beginTransaction(...)` sets the session to `FlushMode.MANUAL` (older Hibernate: `FlushMode.NEVER`) and restores the previous mode when the transaction ends. So auto-flush is disabled for the life of that transaction. ## Dirty checking, and why skipping it matters For each managed entity, Hibernate keeps a *loaded state* array (a snapshot taken at load time). At flush, it walks every managed entity and compares current field values against that snapshot to decide which need UPDATE statements — this is *automatic dirty checking*. For a large persistence context this snapshot-and-compare is measurable CPU and the snapshot itself is memory. In a read-only transaction there is nothing to persist, so: - No auto-flush => the dirty-checking pass at commit never runs. - Hibernate can mark loaded entities read-only (see `Session.setDefaultReadOnly(true)` / `@QueryHint org.hibernate.readOnly`), so it can skip retaining the loaded-state snapshot, cutting memory. ## What it does NOT touch - **Isolation level / locking** — unchanged; `readOnly` is orthogonal to `@Transactional(isolation = ...)`. - **Second-level cache** — not enabled or altered. - **Explicit flush** — `entityManager.flush()` still works and overrides MANUAL. ## Gotchas - If you query with `EntityManager` after modifying an entity in a MANUAL session, Hibernate will NOT flush first, so your query may not reflect the in-memory change — a source of subtle read-your-writes surprises. - Nested/propagated transactions: the flush mode is set on transaction begin, so a method joining an existing readOnly tx inherits its flush behavior (see the propagation follow-up).

  • Does readOnly change the transaction isolation level?
    No. isolation and readOnly are independent attributes of @Transactional. readOnly only affects flush behavior and the JDBC read-only hint; it does not weaken or strengthen isolation.
  • Why might a query inside a readOnly method return stale in-memory data?
    Because FlushMode.MANUAL suppresses the pre-query auto-flush, so pending in-memory entity changes are not synchronized before the query runs — you may not read your own uncommitted writes.

saying these in an interview costs you the question

  • Claiming readOnly sets a lower isolation level
  • Saying it enables the second-level cache
  • Thinking explicit flush() is blocked in MANUAL mode
  • Believing MANUAL still flushes at commit

context