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 javax.persistencejakarta.persistence namespace change, the ORM 6 query-engine rewrite, the Hibernate Search 6+ Elasticsearch backend), so the official documentation above wins on any discrepancy.

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

AUTO (default)

Flushes automatically before a query whose results could plausibly be affected by pending changes in the context, and always before transaction commit.

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.

ALWAYS

Flushes before every query unconditionally, even ones AUTO would judge unaffected — rarely needed, mainly a debugging aid.

MANUAL

No automatic flush at all — the application must call flush() explicitly (the mode StatelessSession and batch-processing loops often want, see Bulk Operations & Batching).

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.