skip to content

Does a JPQL or HQL query read from the session's first-level cache? Explain what Hibernate does when a query returns a row whose entity instance is already present in the persistence context with different in-memory values.

level: middleimportance: must knowfreq 55%

answer

  1. query always hits the DB; the cache can't answer WHERE
  2. hydration: id hit -> return existing instance, discard read values
  3. protects unflushed changes and object identity
  4. refresh() is the only overwrite
  5. scalar projections bypass entities -> can disagree with them

basics

~20 s

No — a JPQL query always goes to the database. But when hydrating results, Hibernate checks each row's id against the persistence context and returns the already-managed instance, discarding the freshly read column values. So query results can show your in-memory state, not the database's.

solid answer

~50 s

A JPQL/HQL/Criteria query is never *answered* from the first-level cache: the cache is keyed by primary key and cannot evaluate a `WHERE` clause, so SQL is always sent (typically after an automatic flush so the query sees your pending writes). The interaction happens on the way back. As Hibernate hydrates each row it looks up (entity type, id) in the persistence context; if an instance is already there, it **returns that instance and throws away the values just read**. It does not overwrite your in-memory object. The consequences surprise people: an entity you modified but have not flushed still shows your values after a query re-reads it; a row another transaction changed still shows the old values. Only `refresh()` overwrites an existing instance from the database. Scalar projections (`SELECT o.status`) bypass entities altogether and therefore *do* show raw database values — which is how the same query can report two different truths in one session.

code

java · 13 lines
java
Order o = em.find(Order.class, 1L);   // total = 100 in DB
// another transaction commits total = 120

List<Order> list = em.createQuery(
        "select o from Order o where o.id = 1", Order.class).getResultList();
assert list.get(0) == o;              // same instance
assert o.getTotal().equals(new BigDecimal("100"));  // stale on purpose

BigDecimal scalar = em.createQuery(
        "select o.total from Order o where o.id = 1", BigDecimal.class)
        .getSingleResult();           // 120 - projections bypass the entity

em.refresh(o);                        // now o.getTotal() == 120

go deeper

for a junior

Know that queries always go to the database and that already-loaded entities come back as the same objects you already hold.

for a middle

Explain hydration against the identity map, that freshly read values are discarded for managed ids, and that refresh() is the way to update them.

for a senior

Use it diagnostically: entity-versus-projection disagreements, staleness in long transactions, and the automatic flush that precedes queries.

for a principal

Frame it as a consistency-model decision — session length determines how long the application pins a snapshot of a row, and refresh points or short units of work are the levers.

## Two different questions "Does a query use the first-level cache?" conflates two things: 1. **Can the query be answered without SQL?** No. The persistence context is an identity map keyed by primary key; it cannot evaluate `WHERE status = 'NEW' ORDER BY created`. Every JPQL/HQL/Criteria/native query goes to the database. (A separate, optional *query cache* exists but is off by default and is not the first-level cache.) 2. **Does the persistence context affect what the query returns?** Very much so, on result hydration. ## Flush before the query Under Hibernate's automatic synchronisation, before executing a query that touches tables with pending changes, Hibernate flushes so the SQL sees your uncommitted work. Without that, a query would miss the row you just persisted in the same transaction. So step one of a query is often a burst of INSERT/UPDATE/DELETE you did not explicitly ask for. ## Hydration and the identity map For each row in the result set, Hibernate reads the identifier and looks the key up in the persistence context: - **Miss** — it hydrates a new instance from the column values, registers it in the context together with a snapshot, and returns it. - **Hit** — it returns the *existing managed instance* and **discards the freshly read values**. That second rule is the whole answer to the interview question. It exists to preserve the identity guarantee: one instance per row per session. Overwriting the instance would silently destroy unflushed changes the application has made and would break every other reference already pointing at that object. ### Case A — your own unflushed change ``` Order o = em.find(Order.class, 1L); o.setStatus(CANCELLED); // not flushed if flushing is suppressed List<Order> news = em.createQuery("select o from Order o where o.status = 'NEW'", Order.class) .getResultList(); ``` With automatic flushing on, the UPDATE goes out first and the row no longer matches, so the query returns nothing — consistent. But if flushing is suppressed for that query, the database still holds `NEW`, the row comes back, and hydration hands you the very instance whose status you set to `CANCELLED`. You then hold a "NEW orders" list containing a cancelled order. Nothing is broken; the query filtered on database state while the object shows session state. ### Case B — another transaction's committed change Your session loaded order 1 with `total = 100`. Another transaction commits `total = 120`. Your query re-reads the row and sees 120 in the result set — and then discards it, returning your instance that still says 100. The session gives you application-level repeatable reads whether or not the database isolation level does. ### Case C — scalar projections ``` em.createQuery("select o.total from Order o where o.id = 1", BigDecimal.class).getSingleResult(); ``` No entity is hydrated, so no identity map lookup happens; you get the raw database value, 120. Now the same session reports 100 through the entity and 120 through the projection. Every "my DTO and my entity disagree" bug of this shape has this cause. ## Getting fresh values on purpose - `em.refresh(entity)` — SELECT and **overwrite** the managed instance's fields, discarding unflushed local changes. Object identity is preserved, so all existing references see the new values. - `em.detach(entity)` (or `clear()`) then re-query — you get a *new* instance with fresh values; the old reference becomes a stale detached object, which is often worse than refreshing. - A query hint or lock mode requesting a refresh (`LockModeType.PESSIMISTIC_*` re-reads under a lock, and Hibernate's `CacheMode.REFRESH` targets the second-level cache, not this one). There is no flag that makes ordinary query hydration overwrite managed instances — and there should not be, because it would break unflushed work. ## Practical rules 1. Do not use a query as a way to "re-read" an entity you already hold; use `refresh()`. 2. If a long transaction must see other transactions' committed changes, plan explicit refresh points, or keep the unit of work short. 3. When a projection and an entity disagree in the same session, check whether the entity is managed and stale rather than suspecting the SQL. 4. Expect a query to trigger a flush; if you see unexplained UPDATEs just before a SELECT in the log, that is the automatic synchronisation doing its job.

  • Then why does a query issued right after persist() in the same transaction see the new row at all?
    Because of automatic flushing. Before running a query whose result could be affected by pending changes, Hibernate synchronises the persistence context to the database, so the INSERT is executed first and the SELECT sees it inside the same transaction. Nothing is committed by that flush; it is only visible to the current transaction until commit.
  • How is this different from the second-level cache's effect on queries?
    The second-level cache is scoped to the SessionFactory and, when enabled for an entity, can serve loads by id across sessions without SQL; a separate query cache can even store result identifier lists. Neither of those is involved here. The first-level cache never prevents a query from executing — it only decides which object instance represents each returned row within the current session.

saying these in an interview costs you the question

  • Claiming JPQL queries are served from the first-level cache when the entities are already loaded.
  • Expecting query results to overwrite the fields of entities already managed in the session.
  • Blaming the database or the SQL when an entity looks stale after a query re-read it.
  • Using detach-and-requery instead of refresh(), then reasoning with a stale reference.
  • Not realising a query normally triggers a flush of pending changes first.

context