Flushing and Dirty Checking
|
This section documents Hibernate ORM 7.4.x (User Guide, Introduction, Query Language Guide, Data Repositories Guide), Jakarta Persistence 3.2, Hibernate Search 8.4.x, and the Hibernate Validator / Hibernate Reactive references — which are the references these pages are written and verified against. This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Three older reference books were consulted as bibliography only while preparing these pages and are not the
primary or main source for any page. All three predate Jakarta Persistence 3.2 and Hibernate ORM 6/7 (the
This section’s bibliography lists the reference material consulted while preparing these pages. |
Nothing in Hibernate ever calls "save" on a managed entity’s field change — that is the point of automatic dirty checking. This page covers how it works, when it runs, and the two annotations that tune the SQL it generates.
Automatic dirty checking and the load-time snapshot
When an entity becomes managed (loaded by find, a query, or just-persisted), Hibernate takes a snapshot of
its state at that moment. At flush time, every managed entity in the persistence context is compared against
its snapshot, field by field; any entity with at least one changed field gets an UPDATE for exactly the
changed columns (by default, every column is included in the UPDATE regardless of which fields actually
changed — see @DynamicUpdate below to change that). No explicit update()/save() call is required or, for
a managed entity, meaningful — mutating it with a plain setter is the entire API:
@Transactional
public void rename(Long bookId, String newTitle) {
Book book = em.find(Book.class, bookId); // now managed, snapshot taken
book.setTitle(newTitle); // no further call needed
} // UPDATE issued at flush/commit
Flush modes
FlushModeType (JPA) / Hibernate’s own FlushMode control when the persistence context is synchronized to
the database, independent of when the transaction commits:
| Mode | Behavior |
|---|---|
|
Flushes automatically before a query whose results could plausibly be affected by pending changes in the context, and always before transaction commit. |
|
Flushes only at transaction commit — skips the pre-query auto-flush. Faster when a query is known not to touch data the current transaction just modified, but risks the query returning stale results if that assumption is wrong. |
|
Flushes before every query unconditionally, even ones |
|
No automatic flush at all — the application must call |
Set per-Session/EntityManager via setFlushMode(…), or per-query via Query.setFlushMode(…).
Auto-flush before queries: what "affected" means
Under AUTO, Hibernate’s heuristic for "could this query be affected" is table-based, not row-based: if any
pending change in the context touches a table the query’s FROM/JOIN clauses reference, the whole context is
flushed first. This is deliberately conservative (it would rather flush unnecessarily than return a stale
result) and is the reason interleaving writes and reads inside one transaction generally "just works" without
manual flush calls — at the cost of losing some batching opportunity, since each such flush can break up what
would otherwise be one batched round of INSERT/UPDATE statements.
Flush ordering
Within one flush, Hibernate groups pending SQL by statement type and orders them: `INSERT`s first, then `UPDATE`s, then collection removals, then `DELETE`s — not the order operations were called in Java. This ordering exists to satisfy foreign-key constraints without requiring the application to sequence its own calls carefully, but it also means flush-time constraint violations can surface in an order that looks unrelated to the code that triggered them; reading the flush order is often the fastest way to make sense of such an error.
@DynamicUpdate and @DynamicInsert
By default, Hibernate’s UPDATE/INSERT statements include every mapped column, whether or not it actually
changed (or has a non-null value) — this lets Hibernate use one prepared, cacheable SQL statement per entity
type regardless of which fields changed on a given flush.
@Entity
@DynamicUpdate // UPDATE only the columns that actually changed
@DynamicInsert // INSERT only the columns with non-null values (let DB defaults apply to the rest)
public class Book { /* ... */ }
@DynamicUpdate trades a cacheable statement for a smaller UPDATE — worthwhile on wide entities where most
flushes touch only one or two columns (reduces lock contention and write-ahead-log volume on those columns),
and required for optimistic-locking configurations that only want to check/update the columns that actually
changed (@OptimisticLocking(type = OptimisticLockType.DIRTY), see
Locking). @DynamicInsert matters mainly when several columns have
database-side DEFAULT expressions that should apply whenever the Java field is left null, rather than
Hibernate explicitly inserting NULL and overriding the default.