skip to content

You run the JPQL statement UPDATE Product p SET p.price = p.price * 1.1 through Query.executeUpdate(). What happens to entities already loaded in the current persistence context, and what happens to Hibernate's second-level cache?

level: middleimportance: must knowfreq 60%

answer

  1. one SQL statement, no entities loaded
  2. L1 not refreshed - stale, then write-back
  3. L2 regions invalidated for HQL, not for undeclared native SQL
  4. auto-flush before, nothing after
  5. no callbacks, no cascade

basics

~20 s

The statement becomes one SQL UPDATE run directly on the database. Managed entities already in the persistence context are not refreshed and keep stale values; Hibernate does evict the affected regions of the second-level cache. Clear or refresh afterwards.

solid answer

~50 s

A bulk JPQL UPDATE is translated to a single SQL UPDATE and executed directly. It does not load entities, does not run dirty checking, and does not fire lifecycle callbacks such as @PreUpdate. Before executing, Hibernate auto-flushes pending changes that touch the same tables, so your unflushed work is not lost. After executing, the first-level cache is untouched: any Product you had already loaded still holds the old price, and if you then modify and flush it, dirty checking will write the stale value back over the bulk change. The second-level cache is handled - Hibernate invalidates the affected entity, collection and query-cache regions for the statement's query spaces - but only for HQL/JPQL; a native SQL statement needs its affected entities declared for the same cleanup. The practical rule: run bulk statements early in a transaction, or call clear() afterwards and re-read what you need. executeUpdate() returns the number of rows affected.

code

java · 7 lines
java
Product p = em.find(Product.class, 1L);      // price = 100
int rows = em.createQuery(
        "update Product p set p.price = p.price * 1.1")
    .executeUpdate();                        // DB now 110
p.getPrice();                                // still 100
em.clear();                                  // or em.refresh(p)
Product fresh = em.find(Product.class, 1L);  // re-read: 110

go deeper

for a junior

Say that the statement goes straight to the database, that already-loaded entities are not updated, and that you clear or refresh afterwards.

for a middle

Add the mechanics: auto-flush before, no reconciliation after, no callbacks or cascades, and the write-back hazard from stale snapshots.

for a senior

Discuss second-level cache invalidation for HQL versus declared query spaces for native SQL, and how you sequence bulk work inside a transaction.

for a principal

Frame it as a boundary decision - bulk mutations belong in their own transaction or job, isolated from code that manipulates managed entities, with the invalidation contract written down.

## What executeUpdate actually does JPQL has UPDATE and DELETE statement forms, and HQL adds INSERT ... SELECT. They are executed with Query#executeUpdate(), which returns the row count. Hibernate parses the statement, maps entity and attribute names to tables and columns, and emits SQL that the database applies set-at-a-time. Nothing is read into memory. That is the whole point: updating a million rows costs one statement instead of a million SELECTs, a million dirty checks and a million UPDATEs. The cost of skipping the object layer is that everything the object layer normally does is skipped too: - No entity instances are created, so no lifecycle callbacks or entity listeners run: @PreUpdate, @PostUpdate, @PreRemove are not invoked. - No cascading and no orphan removal. - No dirty checking, no snapshot maintenance. - Attribute converters and derived-value mappings are not applied to your SET expression the way they are for entity writes, so what you write is close to raw SQL semantics. ## The first-level cache is not updated The persistence context is a map of identifier to managed instance plus a snapshot of each instance's loaded state. A bulk statement changes rows in the database without telling that map anything. So a Product loaded at price 100 still reports 100 after the statement raised it to 110, and calling find() again returns the same cached instance rather than re-reading. Two failure modes follow. First, plain staleness - code that reads the entity afterwards sees the old value. Second, and worse, write-back: if you now set another field on that instance and the transaction flushes, dirty checking compares the instance against the stale snapshot and emits an UPDATE containing the old price, silently undoing the bulk change. The mitigations are ordering and explicit invalidation. Run bulk statements before loading the entities they touch; or after executing call em.clear() to detach everything, or em.refresh(p) for the specific instances you must keep. Note clear() also discards unflushed changes, so flush first if you have any - although Hibernate's auto-flush before the bulk statement usually took care of that. ## The second-level cache is handled, with a caveat A common but wrong belief is that bulk statements leave the shared second-level cache stale forever. For HQL/JPQL bulk statements Hibernate schedules a cleanup that invalidates the cache regions belonging to the statement's query spaces - the entity regions for affected tables, related collection regions, and query cache regions. It is a coarse invalidation, not a targeted update: the whole region for the entity is dropped, so the next reads go to the database. The caveat is native SQL. Hibernate cannot parse arbitrary SQL to learn which tables it touches, so a native mutation must declare its affected entities or tables (its query spaces). Otherwise the second-level cache genuinely does go stale. ## Auto-flush before, nothing after Bulk statements are queries as far as flush mode is concerned. Under the default AUTO flush mode Hibernate flushes pending changes whose tables overlap the statement's query spaces before executing it, so your in-memory work is applied first and the bulk statement sees it. There is no symmetric step afterwards - Hibernate never re-reads to reconcile the persistence context. ## Multi-table entities If the entity spans several tables - joined inheritance, or a secondary table - a single SQL statement cannot express the operation. Hibernate then uses an id-table strategy: it selects the matching identifiers into a temporary table and issues one statement per underlying table against it. Which flavour (global temporary table, local temporary table, an inline list of ids) is chosen depends on the dialect and can be configured, and it explains the unexpected CREATE/INSERT statements people see in the log for a one-line bulk delete. ## When to reach for it Use a bulk statement when the change is expressible in SQL and applies to many rows: flag flips, price adjustments, archiving, purging. Use entity mutation when you need callbacks, cascades, validation or per-row business logic. Mixing both in one transaction is where bugs live - so keep bulk work at the start, or in its own transaction.

  • Why is a stale managed entity after a bulk update worse than simply reading an old value?
    Because dirty checking compares the instance against the snapshot taken at load time. If you touch any field on that entity and the transaction flushes, Hibernate writes the full row from the stale in-memory state, overwriting the columns the bulk statement changed. The data loss is silent - no exception, no conflict.
  • Are entity lifecycle callbacks such as @PreUpdate invoked by a bulk UPDATE?
    No. Bulk statements never materialise entity instances, so no callbacks, entity listeners or interceptors for entity state changes fire. If auditing or derived fields depend on those hooks, a bulk statement must set those columns explicitly or the work has to be done in SQL, for example via a trigger.
  • Does a native SQL UPDATE get the same second-level cache cleanup?
    Not automatically. Hibernate does not parse native SQL, so it cannot infer the affected tables. You must declare the affected entities or query spaces on the native query; otherwise Hibernate keeps serving stale entries from the second-level cache and from the query cache after the statement commits.

It is like editing the warehouse ledger directly while your colleague keeps working from a printout on their desk: the ledger is right, the printout is stale, and if they hand their printout back in it overwrites your edit.

saying these in an interview costs you the question

  • Saying bulk updates leave the second-level cache stale - for HQL/JPQL Hibernate invalidates the affected regions
  • Assuming Hibernate refreshes managed entities after the statement
  • Expecting @PreUpdate or cascade behaviour from a bulk statement
  • Calling clear() with unflushed intentional changes and losing them
  • Believing executeUpdate returns the number of matched entities rather than rows affected by the SQL

context