Hibernate's @OptimisticLocking annotation accepts OptimisticLockType.ALL and OptimisticLockType.DIRTY. How do the generated UPDATE statements differ, and what concurrency anomaly does DIRTY still allow?
answer
- ALL = every column in WHERE; DIRTY = changed columns only
- DIRTY: disjoint edits both succeed
- cross-column invariant broken silently under DIRTY
- DIRTY requires @DynamicUpdate
- default to ALL; DIRTY hints the entity should be split
basics
~20 sALL puts every mapped column's loaded value in the UPDATE's WHERE clause; DIRTY puts only the columns this transaction modified. DIRTY therefore lets two transactions edit disjoint columns of the same row concurrently — convenient, but it cannot protect invariants that span columns.
solid answer
~50 s`ALL` compares the whole row: `where id = ? and name = ? and price = ? and stock = ?`. Any concurrent change anywhere in the row makes your update match nothing, so you get a stale-state failure. `DIRTY` compares only what you changed: if you edited `price`, the clause is `where id = ? and price = ?`. Two transactions that touch different columns both succeed, which raises write throughput on wide rows and reduces false conflicts. The anomaly `DIRTY` permits is a cross-column inconsistency. Column-level independence is only safe if your invariants are column-local. If a rule relates `price` to `discount`, or `status` to `approved_by`, one transaction can change `price` while another changes `discount`, each validating against a row state that no longer exists after both commit — the row ends up in a combination neither transaction would have allowed. `ALL` prevents this by treating the row as one unit. Choose `DIRTY` only for genuinely independent fields; default to `ALL`.
code
sql · 7 lines-- OptimisticLockType.ALL
update product set price = ?
where id = ? and name = ? and price = ? and stock = ?;
-- OptimisticLockType.DIRTY (only price was changed)
update product set price = ?
where id = ? and price = ?;go deeper
Know that ALL compares every column and DIRTY only the ones you changed.
Add that DIRTY lets disjoint edits both succeed, and that it needs @DynamicUpdate because the statement varies per flush.
Lead with the cross-column invariant that DIRTY breaks silently, and name the excluded-column escape hatch and the detachment limitation.
Frame the choice as picking the unit of consistency; if column-level independence is genuinely correct, question whether the entity should be split instead.
## Two ways to build the WHERE clause Both types replace a version check with a comparison against values read earlier; they differ in how many columns they compare. **ALL** — every mapped column of the entity goes in: ```sql update product set price = ? where id = ? and name = ? and price = ? and stock = ? ``` The row is treated as a single unit of consistency. Any concurrent change to any column, even one you do not care about, makes your update affect zero rows and Hibernate raises a stale-state exception. **DIRTY** — only the columns this flush modifies: ```sql update product set price = ? where id = ? and price = ? ``` The unit of consistency shrinks to the individual column. Someone else may change `stock` concurrently and both updates succeed. ## What you gain from DIRTY Fewer false conflicts. On a wide row edited by several independent processes — a pricing job, an inventory feed, a support tool changing a description — `ALL` makes every one of them a potential conflict for every other, and users see failures for edits that never actually overlapped. `DIRTY` lets those writers coexist. The statements are also smaller, which matters when the row has many columns. ## What DIRTY costs It silently gives up any invariant that spans more than one column. Concretely: a rule says `discount` may never exceed 50% of `price`. Transaction A reads (price 100, discount 40) and lowers the price to 60 — legal against what it read. Transaction B reads the same row and raises the discount to 45 — also legal against what it read. Under `DIRTY`, A's `WHERE` mentions only `price` and B's only `discount`, so neither collides, both commit, and the row settles at (60, 45), which violates the rule. Nothing failed, nothing was logged; the data is simply wrong. Under `ALL`, the second update matches zero rows and one transaction is told to retry. The same trap appears with state machines: a `status` column and the fields that justify a status are usually a single logical value, and letting them move independently produces rows in states your code believes are unreachable. There is a second, subtler effect. `DIRTY` compares only the columns you changed, so it also cannot detect that the row was concurrently changed in a way that *invalidates your decision to write at all* — for instance a row transitioning to `CANCELLED` while you update its shipping address. If a read influenced your write, `ALL` is the type that reflects that dependency. ## Mechanics you should mention `DIRTY` needs `@DynamicUpdate`: the set of compared columns varies per flush, so Hibernate cannot use its pre-generated static statement. `ALL` is conventionally paired with it too, so the `SET` clause stays limited to changed columns. Both types share the limitations of versionless locking generally: the comparison values come from the persistence context's loaded-state snapshot, so protection ends at detachment; nullable columns require null-safe comparisons that bloat the SQL; and columns that compare poorly (large text or binary, some floating point) may need `@OptimisticLock(excluded = true)`, which creates a deliberate blind spot. Note that excluding columns under `ALL` gives you something close to a curated middle ground between the two types. ## How to choose Default to `ALL`: it matches the mental model "the row is the unit of consistency", which is what a version column also gives you, and it fails loudly rather than corrupting quietly. Move to `DIRTY` only when you have identified a real contention problem *and* can state that the columns involved carry no cross-column rules. And if a row's fields are genuinely independent enough that `DIRTY` looks attractive, that is often a hint the entity should be split into separate entities with their own lifecycles.
- Give a concrete case where DIRTY loses data integrity but ALL does not.An invariant tying two columns, such as discount not exceeding half the price. One transaction lowers the price, another raises the discount; each is valid against the row it read. With DIRTY the two WHERE clauses mention different columns, so neither conflicts and both commit, leaving a combination that violates the rule. With ALL the second update matches zero rows and is forced to retry against the new state.
- Why can't DIRTY work with Hibernate's default static UPDATE statement?Because the compared column set changes from flush to flush, depending on what was modified. A statement pre-generated at bootstrap has a fixed WHERE clause, so the SQL must be built per flush — which is exactly what @DynamicUpdate enables. Without it there is nothing sensible for Hibernate to emit for DIRTY.
saying these in an interview costs you the question
- Describing DIRTY as simply a faster ALL, with no correctness difference.
- Claiming DIRTY detects any concurrent modification of the row.
- Choosing DIRTY by default to reduce optimistic-lock failures without checking for cross-column rules.
- Expecting DIRTY to work without @DynamicUpdate.
- Assuming column-level conflict resolution is equivalent to merging concurrent edits.