skip to content

Inside one Hibernate Session you load a row by primary key, another session commits an update to that row, and you load it by the same primary key again — you still see the old field values. Explain why, and say whether the database isolation level is what produced that behaviour.

level: middleimportance: must knowfreq 55%

answer

  1. Identity map keyed by type + id
  2. Second find = no SQL at all
  3. Repeatable read by identity, not by isolation
  4. Query rows fresh, managed entity state kept
  5. refresh / clear / new session to re-read

basics

~20 s

The persistence context is an identity map keyed by entity type plus id. The second lookup returns the instance already in it without re-reading, so you get repeatable reads at application level regardless of the database isolation level. Only refresh, clear or a new session re-reads.

solid answer

~50 s

The first load put the entity in the persistence context, indexed by class and identifier. The second find() for the same identifier is answered from that map — no SQL at all — so the other session's committed change cannot be observed. This is an application-level repeatable read, produced by the ORM, not by the database. It holds even on READ COMMITTED, and it also holds when a query does hit the database: if a row maps to an entity that is already managed, Hibernate discards the freshly read column values and returns the existing instance, because two managed instances of the same row would break the unit-of-work guarantee that the same id yields the same object. The practical consequences are that you cannot refresh state by loading again — you need refresh(), clear() or a new persistence context — and that long-lived sessions can operate on stale data without any error appearing.

code

java · 7 lines
java
Order a = em.find(Order.class, 42L);   // SELECT ...
// another session commits: update orders set status='CANCELLED' where id=42
Order b = em.find(Order.class, 42L);   // no SQL
assert a == b;                          // same instance
// b.getStatus() is still the value loaded first

em.refresh(a);                          // SELECT ... : now current

go deeper

for a junior

Say that the session keeps one object per row and hands the same object back, so nothing is re-read.

for a middle

Frame it as repeatable read by identity provided by the ORM, independent of isolation, and name refresh, clear and a new persistence context as the ways to re-read.

for a senior

Discuss the query asymmetry — fresh rows, retained entity state — and the incident shape of long-lived sessions that make decisions on stale data with no error.

for a principal

Set persistence-context lifetime as an architectural rule: one context per unit of work, explicit refresh points, and a write-time concurrency check wherever read-then-write spans a request.

## The identity map A persistence context holds every managed entity in a map keyed by entity type plus primary key. That map is what people call the first-level cache, and it is not optional or configurable: it is the unit-of-work guarantee that within one session there is exactly one Java object per database row. Code can rely on reference equality, and Hibernate can rely on having one place to record each entity's loaded-state snapshot for dirty checking. That guarantee produces the behaviour in the question. em.find(Order.class, 42L) the second time finds the key present and returns the same instance immediately. No SELECT is issued, so nothing anyone else committed can be seen. ## Why it is not isolation Isolation levels describe what a database transaction may observe when it does read. If the ORM does not read, isolation has nothing to act on. So the observed stability is an ORM property layered on top of whatever the engine offers, and it appears identically under READ COMMITTED, REPEATABLE READ or SERIALIZABLE. The distinction matters in two directions. Upward: do not conclude that your database is giving repeatable reads because your entities look stable — it may not be, and other parts of the same transaction (aggregate queries, projections, native SQL) will show the newer data. Downward: do not conclude that stable means current — you may be making decisions on values that were replaced minutes ago, with no exception to warn you. ## Queries behave differently, and then the same A JPQL or Criteria query always executes SQL; the identity map is keyed by id, not by query text, so it can never answer a query. The rows come back fresh. But then Hibernate resolves each row to an entity: if that identifier is already managed, the newly read column values are thrown away and the existing instance is returned. So a query gives you fresh membership — new rows appear, deleted ones vanish from the result — with possibly stale field values for anything you already had. That asymmetry surprises people who try to refresh state by re-running a query. ## How to get fresh state deliberately - em.refresh(entity) re-reads that row and overwrites the instance's fields, discarding unflushed changes to it. This is the targeted tool. - em.clear() detaches everything; the next load is a real read. Blunt but useful between phases of a long unit of work. - em.detach(entity) does the same for one instance. - A new EntityManager per unit of work is the cleanest answer, and the reason short, request-scoped sessions are the default advice. Note that refresh and reload do not by themselves prevent lost updates; they only narrow the window. Detecting a concurrent modification at write time is a separate mechanism. ## Where teams get hurt The classic incident is a long-lived session — a batch job or a desktop-style conversation — that loads data once and keeps working. Every decision it makes uses the snapshot from when it started, while the world moves on. Nothing fails; the data is just wrong. The fix is boundary discipline: one persistence context per unit of work, closed at the end, plus a concurrency check at write time for anything read then written. A second trap is testing. A test that writes through the same session it later reads from will pass even if the write was never sent to the database, because the identity map returns the in-memory instance. Flushing and clearing, or using a separate persistence context for the assertions, is what makes such a test meaningful. ## The one-line summary The first-level cache gives you repeatable reads by identity inside a session, for free and unconditionally. It says nothing about database isolation and nothing about freshness.

  • If a JPQL query does hit the database, why doesn't it update the fields of an entity you already have?
    Because the persistence context must keep exactly one instance per row, and that instance may already carry unflushed modifications. Overwriting it from the result set would silently discard the application's changes and break reference identity. Hibernate therefore discards the freshly read column values and returns the managed instance; refresh() is the explicit opt-in to the opposite behaviour.
  • How does this affect a test that writes and then reads back the entity?
    The read can be answered from the identity map, so the assertion passes even if the SQL was wrong or never sent. To test the mapping and the write path, flush and clear the persistence context before reading, or perform the read in a separate persistence context, so the data really comes from the database.

The session is a notebook where you copy each record the first time you look it up. Asking again reads your own notebook, not the filing cabinet — steady, but only as current as the moment you copied it.

saying these in an interview costs you the question

  • Explaining the stability by saying the database is in REPEATABLE READ
  • Believing loading the entity again re-reads the row from the database
  • Thinking refresh() and a second find() are equivalent
  • Assuming the first-level cache also answers JPQL queries
  • Treating stable in-memory state as evidence that the data is current

context