skip to content

Under Hibernate's default flush mode, a JPQL query sometimes triggers a flush of pending changes and sometimes does not. What decides that, and why do native SQL queries behave differently?

level: middleimportance: should knowfreq 42%

answer

  1. query spaces = tables touched
  2. overlap → flush, no overlap → skip
  3. native SQL opaque → flush everything
  4. addSynchronizedEntityClass / QuerySpace
  5. also drives 2nd-level cache invalidation

basics

~20 s

Hibernate compares the tables a JPQL query reads with the tables the pending actions would modify, and flushes only when they overlap. It cannot parse native SQL, so it conservatively flushes everything unless you declare the query's synchronized entities or query spaces.

solid answer

~50 s

Under `AUTO`, Hibernate does not blindly flush before every query. It computes the **query spaces** — essentially the set of tables — that the query reads, and compares them against the tables affected by entries in the pending action queue. If there is no overlap, the pending changes cannot influence the result, so the flush is skipped. That is a deliberate optimization, and it is why you can queue an `Order` insert and still run a `Country` lookup without paying for a flush. A native SQL query is opaque: Hibernate is not going to parse arbitrary SQL to work out which tables it touches. To stay correct it assumes the worst and flushes the entire persistence context before running it. You can narrow that with `NativeQuery.addSynchronizedEntityClass(...)` or `addSynchronizedQuerySpace(...)`, which tells Hibernate exactly which tables the statement touches — that both limits the auto-flush and controls which second-level cache regions get invalidated.

code

java · 5 lines
java
var q = em.createNativeQuery("select count(*) from orders where status = 'OPEN'")
          .unwrap(org.hibernate.query.NativeQuery.class)
          .addSynchronizedEntityClass(Order.class);

Object count = q.getSingleResult(); // flushes only pending Order actions

go deeper

for a junior

Know that the default mode flushes before queries so you read your own writes; the overlap detail can be recognized rather than derived.

for a middle

Explain query spaces and the overlap check, and why a native query defaults to flushing everything.

for a senior

Use it diagnostically: recognise repeated full flushes caused by native queries inside write loops, and apply synchronized spaces with the cache-invalidation consequence in mind.

for a principal

Set the convention for mixing native SQL with an ORM unit of work — where native access is allowed, how spaces are declared, and how cache correctness is preserved.

## The problem auto-flush solves With write-behind, in-memory changes and the database disagree until a flush. If a query ran against the database while an unsent `UPDATE` sat in the action queue, the result could contradict the objects the same code just modified. The `AUTO` flush mode fixes this by flushing *before* queries — but flushing before every query would be wasteful, because most queries have nothing to do with what is pending. ## Query spaces Hibernate's compromise is the **query space**: an identifier for a table (or a collection table) that a query reads or an action writes. Because JPQL and Criteria queries are parsed by Hibernate into an internal AST, it knows precisely which entities — hence which tables — the query touches. Each pending action likewise knows its table. Before executing a query under `AUTO`, Hibernate intersects the two sets: - **Overlap** → flush the pending actions, then run the query. - **No overlap** → skip the flush; pending changes cannot alter this result. So queuing an insert into `orders` and then querying `Country` skips the flush, while querying `Order` triggers it. The optimization is real and generally invisible — but it means "AUTO flushes before queries" is a simplification, and code that relied on an incidental flush before an unrelated query can be surprised. ## Native queries: the opaque case When you write `em.createNativeQuery("select ... from orders o join customers c ...")`, Hibernate has a string it does not interpret. It cannot tell whether the SQL reads `orders`, so it cannot prove there is no overlap. The conservative default is to flush the whole action queue before the statement runs. Correct — but it means one stray native query in a batching loop can force a full flush every iteration. The fix is to declare the tables yourself: - `unwrap(NativeQuery.class).addSynchronizedEntityClass(Order.class)` — name the entity; - or `addSynchronizedQuerySpace("orders")` — name the table directly. Once declared, Hibernate treats the native query like any other: it flushes only if the declared spaces overlap the pending actions. The same declaration also drives second-level cache invalidation for that statement, which matters for native DML — an undeclared native `UPDATE` leaves cached regions stale. The reverse hazard exists too: if the native SQL writes to tables you did **not** declare, Hibernate does not know to invalidate them, and it may skip a flush that was actually needed. Declare accurately or not at all. ## Practical consequences 1. **Do not rely on a query as a way to flush.** If you need pending state in the database, call `flush()` explicitly; the overlap check may legitimately decide not to flush. 2. **Mixed JPQL and native code is a performance trap.** Native queries inside a write loop force repeated full flushes; declaring synchronized spaces, or moving the native query out of the loop, removes the cost. 3. **Stored procedures behave like native queries** — fully opaque, so the same conservative rules and the same declaration mechanism apply. 4. **`FlushMode.ALWAYS`** is the sledgehammer version: no overlap analysis, flush before everything. It is the safe fallback when you cannot reason about what a statement touches, at a cost. 5. **Bulk JPQL `update`/`delete` statements bypass the persistence context entirely.** They are still preceded by the auto-flush of *pending* changes, but their own effects never appear in already-loaded entities — those instances stay stale until you clear or refresh the context.

  • Why does declaring synchronized entity classes on a native query matter beyond flushing?
    The same declaration tells Hibernate which second-level cache regions the statement affects. A native INSERT/UPDATE/DELETE that does not declare its tables leaves those cache regions holding stale data, because Hibernate has no way to know what changed. Declaring the spaces keeps both the flush decision and cache invalidation correct.
  • Can you rely on running a query as a way to push pending changes to the database?
    No. Under AUTO the flush happens only if the query's tables overlap the pending actions, so an unrelated query legitimately skips it, and under COMMIT or MANUAL no pre-query flush happens at all. If the write must reach the database at a specific point, call flush() explicitly.

saying these in an interview costs you the question

  • Claiming AUTO always flushes before every query
  • Assuming Hibernate parses native SQL to work out affected tables
  • Using an unrelated query as an implicit 'flush trigger'
  • Running native DML without declaring synchronized spaces and expecting the second-level cache to stay correct
  • Expecting a bulk JPQL update to refresh entities already loaded in the persistence context

context