What does Hibernate's hibernate.jdbc.batch_versioned_data setting control, and why might a JDBC driver's behaviour force you to turn it off?
answer
- versioned update: where ... and version = ?
- conflict detected by affected-row count = 0
- executeBatch may return SUCCESS_NO_INFO (-2)
- -2 hides the count -> silent lost update
- default true since Hibernate 5.0 (old Oracle drivers)
basics
~20 sIt controls whether UPDATE and DELETE statements for entities carrying a version column are sent in JDBC batches. Hibernate decides a row was changed concurrently from the affected-row count; if a driver returns SUCCESS_NO_INFO from executeBatch, that check cannot be made, so batching must be disabled.
solid answer
~50 sEntities with a version column are updated with a statement whose `where` clause includes the expected version. Hibernate concludes the row was modified concurrently when the statement reports **zero affected rows**. That row count is the whole detection mechanism. `java.sql.Statement.executeBatch()` returns an array of counts, but the JDBC specification lets a driver return **`Statement.SUCCESS_NO_INFO` (-2)** for an element, meaning "it worked, count unknown". If that happens, Hibernate cannot distinguish a matched row from a stale one, and a concurrent-modification failure would pass silently as a lost update. `hibernate.jdbc.batch_versioned_data` says whether Hibernate is allowed to batch versioned updates and deletes. It has defaulted to **true since Hibernate 5.0** because mainstream drivers do report real counts; historically it defaulted to false, and old Oracle drivers were the canonical reason. Set it to false when your driver or dialect does not return per-statement counts. Unversioned entities are unaffected — their batching is governed by batch size and ordering alone.
code
sql · 5 lines-- zero affected rows means another transaction already moved the version on
update orders
set status = 'SHIPPED', version = 8
where id = 42
and version = 7;go deeper
Recall that the setting decides whether statements for entities with a version column may be batched.
Explain that conflict detection relies on the affected-row count and that batching can obscure it.
Name SUCCESS_NO_INFO, the silent lost-update failure mode, the default since Hibernate 5.0, and how to verify a driver.
Treat it as a correctness constraint on a throughput setting: how you validate driver behaviour per environment and what you accept if you disable it.
## The mechanism the setting protects When an entity carries a version column, Hibernate writes updates as: ```sql update orders set status = ?, version = 8 where id = ? and version = 7 ``` If another transaction already advanced the version, no row matches and the statement affects **zero rows**. Hibernate reads that count and raises a concurrency failure. Deletes for versioned entities work the same way. The affected-row count is not a nicety here; it is the entire detection mechanism, and versioning is worthless without it. ## Where batching collides with it A JDBC batch is executed with `executeBatch()`, which returns an `int[]` of results — one entry per statement in the batch. The JDBC specification permits two special values: - `Statement.SUCCESS_NO_INFO` (**-2**): the statement executed successfully, but the driver cannot say how many rows it affected; - `Statement.EXECUTE_FAILED` (-3): that element failed. A driver returning `-2` is fully specification-compliant. But for Hibernate it is fatal to version checking: "succeeded, count unknown" cannot be distinguished from "matched zero rows". If Hibernate batched versioned updates against such a driver, a stale write would be silently swallowed — the classic lost update, with the safety net removed by a performance setting. That is the failure mode the setting exists to prevent. ## The setting and its default `hibernate.jdbc.batch_versioned_data` is a boolean: - **true** — batch updates and deletes for versioned entities (default since Hibernate 5.0); - **false** — exclude them from batching; they are sent one at a time so the row count is reliable. Before Hibernate 5.0 the default was `false`, precisely because older Oracle JDBC drivers returned `SUCCESS_NO_INFO` for batched updates. Modern drivers for PostgreSQL, MySQL and current Oracle return real counts, which is why the default was flipped. Some dialects also carry a capability flag so Hibernate can decide sensibly per database. Note the scope: only **versioned** entities are affected. Everything else batches according to `hibernate.jdbc.batch_size` and the ordering settings regardless of this flag. ## What to check on a new database or driver If you are running an unfamiliar driver and rely on version columns, verify empirically rather than trusting the default. A small test can call `executeBatch()` on a batch of updates and inspect the returned array for `-2`. If any element is `-2`, set the property to `false` — you trade some write throughput on versioned entities for the guarantee that a concurrent-modification failure is actually reported. The cost of getting this wrong is unpleasant precisely because it is silent: no exception, no log line, just an update that overwrites someone else's change under load. It will not show up in tests that run one transaction at a time. ## Interaction with statement rewriting Driver-side statement rewriting deserves a related caution. MySQL's `rewriteBatchedStatements=true` and PostgreSQL's `reWriteBatchedInserts=true` change how batched statements are transmitted; historically, rewritten MySQL batches have been a source of imprecise per-statement counts. Rewriting for *inserts* is harmless in this respect — inserts have no version predicate to check — but if you enable aggressive rewriting for updates on a driver you have not verified, test the versioned path under concurrency before trusting it. ## How the pieces fit Think of three independent switches on the write path. `hibernate.jdbc.batch_size` decides whether batching happens at all. `hibernate.order_inserts`/`order_updates` decide whether batches can grow large. `hibernate.jdbc.batch_versioned_data` decides whether the subset of statements that carries a concurrency check may participate. The first two are throughput knobs; the third is a correctness knob wearing a performance costume, which is why it is worth knowing even though it is rarely changed. ## Interview framing The crisp answer is: *versioned updates detect conflicts through the affected-row count; batched execution can hide that count behind `SUCCESS_NO_INFO`; the setting decides whether Hibernate is allowed to take that risk, and it defaults to true on modern drivers that report counts properly.* Being able to name `-2` and the historical Oracle case is what separates a memorised answer from an understood one.
- If batch_versioned_data is left on with a driver that returns SUCCESS_NO_INFO, what is the observable failure?Nothing observable at the time — that is the danger. The stale update reports success, so the second writer's change silently overwrites the first writer's, and no exception is raised. It appears later as data that lost an edit under concurrency, and it will not reproduce in single-threaded tests.
- Does this setting affect entities that have no version attribute?No. Their updates and deletes carry no version predicate and no concurrency check depends on the row count, so they batch according to the batch size and the statement-ordering settings alone. The flag only governs the subset of statements whose correctness depends on reading an accurate affected-row count.
saying these in an interview costs you the question
- Not knowing that conflict detection depends on the affected-row count
- Claiming batching and version columns are simply incompatible in all cases
- Believing the setting still defaults to false in current Hibernate versions
- Assuming a driver returning SUCCESS_NO_INFO is broken rather than specification-compliant
- Thinking the flag also disables batching for entities without a version attribute