skip to content

Why is Hibernate's @DynamicUpdate part of the recipe for column-comparison optimistic locking, and what does Hibernate do with UPDATE statements by default without it?

level: middleimportance: should knowfreq 22%

answer

  1. default: one static UPDATE per entity, all columns in SET
  2. WHERE = id (+ version) only, by default
  3. DIRTY's WHERE varies per flush → needs dynamic generation
  4. also avoids rewriting untouched columns, keeps triggers honest
  5. cost: statement-cache hit rate and batching

basics

~20 s

By default Hibernate pre-generates one static UPDATE per entity at startup, setting every column. Column-comparison locking needs the statement built per flush — mandatory for DIRTY, whose compared columns vary — and @DynamicUpdate switches Hibernate to that per-flush generation.

solid answer

~50 s

At bootstrap Hibernate prepares one `UPDATE` per entity with a fixed shape: every mapped column in the `SET` clause, and only the identifier (plus the version, if any) in the `WHERE`. This is fast — the SQL string is built once and the JDBC statement caches well — and it is why a normal dirty-checked flush writes all columns even though you changed one. That fixed shape cannot express `OptimisticLockType.DIRTY`, whose `WHERE` clause depends on which fields you happened to modify in this flush. `@DynamicUpdate` tells Hibernate to build the statement at flush time from the actual dirty state, which makes the varying `WHERE` clause possible and simultaneously narrows `SET` to the changed columns. It is conventionally used with `ALL` as well: the full-row comparison is expressible statically, but writing back untouched columns is wasteful and can defeat database triggers or column-level auditing. The cost is per-flush SQL generation and a lower statement-cache hit rate.

code

sql · 5 lines
sql
-- default, pre-generated statement
update product set name = ?, price = ?, stock = ? where id = ?;

-- @DynamicUpdate + @OptimisticLocking(type = DIRTY), only price changed
update product set price = ? where id = ? and price = ?;

go deeper

for a junior

Know that by default Hibernate writes all columns using a pre-built statement, and @DynamicUpdate makes it write only the changed ones.

for a middle

Explain that DIRTY's WHERE clause varies per flush, so the statement must be generated at flush time, and mention the statement-cache cost.

for a senior

Add the effects on triggers, change-data-capture and JDBC batching, and treat it as a per-entity opt-in decision rather than a global setting.

for a principal

Position it as a trade between SQL shaping and plan-cache/batching efficiency, and scope it to entities where the write shape actually carries meaning.

## Hibernate's default: one statement per entity When the persistence unit boots, Hibernate generates and caches the SQL for each entity's basic operations. The `UPDATE` has a fixed shape: ```sql update product set name = ?, price = ?, stock = ? where id = ? ``` Every mapped column appears in `SET`, regardless of what changed, and the `WHERE` names only the identifier — plus the version column when the entity has one. Dirty checking still decides *whether* to issue the statement; it does not shape it. That is why people are surprised to see all columns written after changing one field. The design is deliberate. Building SQL once removes work from every flush, and because the string is identical every time, the JDBC driver's prepared-statement cache and the database's plan cache both hit reliably. In a hot write path that is a real advantage. ## Why column-comparison locking breaks the assumption Versionless locking needs old values in the `WHERE` clause: - With `OptimisticLockType.ALL`, the clause is `id = ? and <every column> = ?`. The *shape* is fixed, so this can in principle be pre-generated — the values differ per flush but the text does not. - With `OptimisticLockType.DIRTY`, the clause contains only the columns modified in this flush. Change the price and it is one thing; change the name and it is another. There is no single statement text, so a statement generated once at bootstrap cannot serve it. Hence `@DynamicUpdate`, which moves SQL generation to flush time: Hibernate compares the entity's current state with its loaded-state snapshot, and builds an `UPDATE` naming exactly the changed columns in `SET` and exactly the required columns in `WHERE`. ## Why pair it with ALL too Even where it is not strictly required, dynamic generation is the conventional companion: - **Fewer written columns.** Rewriting untouched columns with their existing values is pointless IO, and on wide rows or rows containing large text or binary columns it is expensive. - **Triggers and auditing.** Database triggers, change-data-capture and column-level audit tooling often key off *which* columns an `UPDATE` touches. A statement that always sets everything makes every update look like a full-row change. - **Fewer surprises with concurrent writers.** Writing back a column you never intended to touch re-asserts a stale value for it; with the full-row comparison it will be caught, but not writing it at all is simply better behaviour. ## What it costs SQL is now built on every flush that dirties an entity. The generation itself is cheap, but the consequence downstream is not free: different modification patterns produce different statement texts, so the prepared-statement cache holds more entries and hits less often, and the database sees more distinct plans. For entities updated at very high rates this is measurable, which is why `@DynamicUpdate` is an opt-in per entity rather than a global default. It is also worth knowing that batching interacts with this: JDBC batching groups identical statements, so varying statement text reduces how effectively updates batch together. If you rely on batched writes for a bulk path, verify the effect there. ## The counterpart `@DynamicInsert` is the same idea for inserts — omit columns that are null at insert time so database defaults apply. It is unrelated to locking but comes from the same trade-off family: less pre-generation, more per-operation work, better-shaped SQL. ## How to summarise it "Hibernate normally reuses one pre-built `UPDATE` per entity that sets all columns and filters on the id. Column-comparison locking, especially the dirty-columns variant, needs a statement whose shape depends on this flush, so `@DynamicUpdate` moves generation to flush time — at the price of statement-cache efficiency."

  • Does @DynamicUpdate change which entities Hibernate decides to update?
    No. Dirty checking still decides whether an entity changed and therefore whether an UPDATE is issued at all. @DynamicUpdate only changes the SQL that gets generated for the entities already deemed dirty — narrowing the SET clause and, for column-comparison locking, allowing the WHERE clause to vary.
  • Why isn't @DynamicUpdate the default for every entity?
    Because pre-generated SQL is faster overall in the common case: the statement text is identical on every flush, so the driver's prepared-statement cache and the database's plan cache hit reliably and JDBC batching groups the statements. Dynamic generation trades that for better-shaped statements, which is worth it only when the shape actually matters.

saying these in an interview costs you the question

  • Thinking Hibernate only writes the changed columns by default.
  • Believing @DynamicUpdate influences whether an entity is considered dirty.
  • Claiming it has no downsides and should be enabled globally.
  • Assuming the default UPDATE's WHERE clause already contains other columns besides the id and version.

context