skip to content

In plain Hibernate, what does the query hint "org.hibernate.readOnly" (or Session.setDefaultReadOnly(true)) actually change about how loaded entities are managed, and why does it make large reads cheaper?

level: middleimportance: must knowfreq 55%

answer

  1. No snapshot → no dirty check
  2. Half the memory per entity
  3. Flush skips READ_ONLY entries
  4. Still in L1 — clear() still needed
  5. setReadOnly(false) re-snapshots current state

basics

~20 s

Entities are put in the persistence context without a loaded-state snapshot, so Hibernate cannot dirty-check them and never flushes an UPDATE for them. That removes roughly half the per-entity memory and all flush-time comparison work.

solid answer

~50 s

Normally Hibernate stores two things per managed entity: the entity itself and a **loaded-state snapshot** — a copy of every mapped scalar value as it came back from the database. At flush time it compares each entity against its snapshot and emits UPDATEs for the differences. Marking a load read-only (`query.setHint("org.hibernate.readOnly", true)`, `session.setDefaultReadOnly(true)`, or `session.setReadOnly(entity, true)`) tells Hibernate to register the entity with status READ_ONLY and skip keeping the snapshot. Consequences: no dirty checking, no UPDATE, no version bump, about half the memory per row, and a flush that no longer scans those entities. What it does **not** do: the entities are still in the first-level cache, so a huge read still grows the heap unless you `clear()` or stream; and mutating a read-only entity silently does nothing. If you later call `setReadOnly(entity, false)`, Hibernate snapshots the *current* state, so earlier edits stay invisible.

code

java · 7 lines
java
List<Order> orders = session.createQuery("from Order o where o.year = :y", Order.class)
        .setParameter("y", 2025)
        .setHint("org.hibernate.readOnly", true)
        .getResultList();

Session reporting = sessionFactory.openSession();
reporting.setDefaultReadOnly(true);

go deeper

for a junior

Know the one-line version: read-only entities are not dirty-checked, so Hibernate never updates them, and it is the right hint for report queries.

for a middle

Explain the loaded-state snapshot, that dropping it is where the memory and flush-time savings come from, and name the three scopes (query hint, session default, per instance).

for a senior

Position it inside a large-read strategy: read-only plus cursor plus periodic clear, plus MANUAL flush as a guard, and be explicit that read-only alone does not bound heap usage.

for a principal

Frame it as one point on a spectrum — managed writable entities, read-only entities, projections, stateless access — and set a team convention for which read paths default to which, so accidental writes and heap blowups are structurally impossible rather than remembered.

## The machinery read-only switches off When Hibernate loads an entity in a normal `Session`, it does two things. First it registers the instance in the **persistence context** (the first-level cache), a map keyed by entity type plus identifier that guarantees you get the same Java object for the same row inside one session. Second — and this is the part people forget — it keeps a **loaded state snapshot**: an `Object[]` holding a copy of every mapped scalar property exactly as it came out of the `ResultSet`. That snapshot is the basis of **automatic dirty checking**. At flush time (before a query that overlaps the pending changes, or at commit) Hibernate walks every managed entity, compares current field values against the snapshot property by property, and emits an `UPDATE` for each entity that differs. This is why you never call a "save" method on an already-managed entity: mutating the object is the update. Read-only mode turns that machinery off for the entities it covers. The entity is still registered in the persistence context, but its entity entry gets status `READ_ONLY` and Hibernate does not retain the snapshot. With no snapshot there is nothing to compare against, so: - no `UPDATE` is ever generated for that instance; - an `@Version` column is not incremented; - flush skips the instance instead of walking its properties. ## The three scopes 1. **Per query** — `query.setHint("org.hibernate.readOnly", true)`. In Hibernate 6 the constant is `HibernateHints.HINT_READ_ONLY`; the native API is `Query.setReadOnly(true)`. Every entity returned by that query is read-only. 2. **Per session default** — `session.setDefaultReadOnly(true)`. Everything loaded afterwards in that session is read-only unless overridden. Ideal for a reporting or export session. 3. **Per instance** — `session.setReadOnly(entity, true)` / `false`, applied after load. ## Why it is faster and smaller Two effects. **Memory**: the snapshot is a second copy of the entity's scalar state, so dropping it removes roughly 40–50% of the retained footprint per entity for typical field-heavy entities. On a query that returns hundreds of thousands of rows this is the difference between fitting in heap and not. **CPU**: dirty checking is O(entities × properties) *per flush*. In a loop that flushes repeatedly with a large persistence context, that scan dominates. Read-only entities are excluded from it. ## What read-only does not do - **It is not a memory cap.** Read-only entities still live in the persistence context and are still strongly referenced until the session closes or you call `clear()`/`evict()`. A read-only load of ten million rows still exhausts the heap; read-only only makes each row cheaper. - **It does not make the object immutable in Java.** You can call setters; the change is simply never written. Silent data loss is a real risk, so treat read-only as a promise you are making, not a guard the framework enforces. - **It is not a substitute for care with collections.** Read-only suppresses dirty checking of the entity's own scalar state and single-ended associations; Hibernate's rules for collections owned by a read-only entity are narrower than people assume, so do not rely on read-only to protect a mutated collection — do not mutate at all. - **Deletes still work.** Calling `remove`/`delete` on a read-only entity deletes the row. - **The query itself is unchanged.** The same SQL is issued; you are not asking the database for anything different, and the connection is not put in a read-only transaction. ## The re-enabling trap If you flip an entity back with `session.setReadOnly(entity, false)`, Hibernate must build a snapshot to resume dirty checking — and it snapshots the entity's **current** in-memory state. Any modification you made while it was read-only is therefore baked in as "the loaded value" and will never be detected as a change. Candidates who assume the change is "replayed" get this wrong. ## Where it belongs in practice Read-only is the default posture for: report and analytics queries over entities, export/streaming jobs, cache-warming loads, and any pass that walks a large object graph only to compute something. Combine it with `FlushMode.MANUAL` if you want belt-and-braces protection against a stray flush, and with a cursor plus periodic `clear()` when the result set is large. If you truly never need managed instances at all, a projection into a DTO or a stateless API avoids the persistence context entirely — read-only is the middle setting: still real entities with lazy associations available, just not writable and not tracked.

  • Does marking a query read-only stop the loaded entities from filling up the heap?
    No. Read-only removes the loaded-state snapshot, which cuts per-entity memory roughly in half, but the entities themselves are still registered in the persistence context and strongly reachable until the session closes. For a genuinely large result you still need a cursor plus a periodic `clear()`, a projection, or a stateless API.
  • If I load an entity read-only, change a field, and then call setReadOnly(entity, false), will the change be persisted?
    No. When Hibernate makes a read-only entity modifiable again it has to construct a loaded-state snapshot, and it takes that snapshot from the entity's current in-memory values. The earlier edit becomes part of the baseline, so dirty checking sees no difference and no UPDATE is emitted. You would have to re-apply the change after flipping it back.
  • How does read-only relate to setting the flush mode to MANUAL?
    They solve overlapping problems from different angles. Read-only makes individual entities untracked, so nothing about them can be written. MANUAL flush mode suppresses automatic flushing for the whole session, so nothing at all is written until you ask. In a reporting session people often set both: read-only for the memory and CPU win, MANUAL as a safety net against an accidental write path.

Loading normally is like photocopying every document as it arrives so you can diff the copy against the original later. Read-only means you skip the photocopier: faster, less paper — but you have also given up any ability to notice edits.

saying these in an interview costs you the question

  • Claiming read-only prevents the entities from being cached in the persistence context, so memory is bounded.
  • Thinking read-only means the object becomes immutable and setters throw — they silently do nothing.
  • Assuming edits made while read-only are flushed once you call setReadOnly(entity, false).
  • Confusing the org.hibernate.readOnly hint with a read-only database transaction or a read replica.
  • Saying read-only changes the SQL Hibernate issues for the query.

context