skip to content

A unit of work removes a row and then persists a new entity carrying the same unique key value, and the flush fails with a unique-constraint violation even though the delete should have freed the key. Why does Hibernate order the statements that way, and how do you fix it?

level: seniorimportance: must knowfreq 40%

answer

  1. ActionQueue drained by type, not call order
  2. inserts → updates → collections → deletes last
  3. FK safety is the reason
  4. fix: flush() between remove and persist
  5. better fix: UPDATE instead of delete+insert

basics

~20 s

At flush time Hibernate executes actions in a fixed order — inserts first, then updates, then collection actions, with deletes last — not in the order you wrote them. The INSERT therefore runs while the old row still exists. Flush explicitly after the remove, or avoid the collision.

solid answer

~50 s

Hibernate does not replay your calls in program order. At flush it drains an **action queue** in a fixed sequence: orphan removals, entity inserts, entity updates, collection deletes/updates/recreations, and finally entity deletes. The ordering exists so foreign keys are satisfied without Hibernate computing a full dependency graph at runtime — parents get inserted before children reference them, and children are unlinked before parents disappear. The cost is exactly this scenario: your `remove()` runs *after* your `persist()`, so the `INSERT` hits a live unique index and fails. Fixes, in order of preference: 1. Call `em.flush()` between the remove and the persist, forcing the `DELETE` out first. 2. Don't delete-and-recreate — update the existing row instead. 3. Split the two operations into separate transactions if that is semantically acceptable. 4. Make the constraint deferrable at the database level, which the ORM cannot decide for you.

code

java · 8 lines
java
Tag old = em.find(Tag.class, 1L);   // unique code = 'X'
em.remove(old);
em.persist(new Tag("X"));           // INSERT is flushed BEFORE the DELETE -> violation

// fix: force the DELETE out first
em.remove(old);
em.flush();
em.persist(new Tag("X"));

go deeper

for a junior

Recall that Hibernate batches statements at flush and does not necessarily send them in call order, so delete-then-insert on the same key can collide.

for a middle

State the fixed action order, explain the foreign-key rationale, and apply the explicit flush fix correctly.

for a senior

Diagnose it from the exception at flush time, weigh the flush against redesigning to an UPDATE, and note the batching and lock-timing costs.

for a principal

Treat delete-then-recreate as a modelling smell, and set the boundary between ORM-managed units of work and schema-level tools such as deferrable constraints.

## Program order is not execution order The most common mental model — "Hibernate issues my statements as I call them" — is wrong. Every `persist`, dirty entity and `remove` becomes an entry in the session's `ActionQueue`. Only at flush time is that queue converted to SQL, and it is drained by *type*, in a fixed order: 1. orphan removals 2. entity inserts 3. entity updates 4. collection removals 5. collection updates 6. collection recreations 7. **entity deletes** Within a type, order follows insertion into the queue (and, with ordered inserts enabled for batching, is grouped by entity type). ## Why this order It is a cheap way to satisfy referential integrity in the common case. Rows must exist before other rows point at them, so inserts go first; rows must stop being referenced before they vanish, so deletes go last, after the collection updates that unlink children. Any other fixed order would break foreign keys in more situations, and computing a per-flush dependency graph over arbitrary object graphs would be expensive and still ambiguous in cycles. ## The unique-key collision The classic reproduction: a row with unique `code = 'X'` exists. You `remove()` it and `persist()` a new entity with `code = 'X'`. At flush, Hibernate emits the `INSERT` first — while the old row is still there — and the database rejects it with a unique-constraint violation. The exception surfaces as a `ConstraintViolationException` wrapped in a `PersistenceException`, and it points at the flush, not at your code, which is why it reads as mysterious. The same shape appears with a one-to-many where you clear a collection and add a replacement carrying the same unique business key, and with "delete the old membership row, insert the new one" patterns. ## Remedies **Explicit flush between the operations.** The direct fix: `em.remove(old); em.flush(); em.persist(fresh);`. The first flush drains the queue, sending the `DELETE`, and the later insert then hits a free index. It costs an extra round trip and it breaks statement batching across that boundary, but it is local, obvious and cheap. **Don't delete and recreate.** Frequently the intent is "replace the value", which is an `UPDATE` of the existing row. Updating avoids the collision entirely, preserves the row's identity and version, and produces less churn in the action queue. This is usually the best answer in an interview: question the delete-then-insert design first. **Separate transactions.** If the removal is genuinely a separate business step, committing it before the insert removes the collision — but only if partial completion is acceptable, since you lose atomicity across the two. **Deferrable constraints.** Some databases can defer unique-constraint checking to commit time, which makes the ordering irrelevant. This is a schema-level decision with its own consequences and is not something the ORM controls. ## Related ordering surprises - **Deletes last also delays lock acquisition.** A `DELETE` you issued conceptually early is only sent at flush, so its row locks are taken late in the transaction — relevant when reasoning about contention. - **`IDENTITY` generation breaks the batching benefits of ordering**, since each `persist()` must execute its `INSERT` immediately to obtain the key; those inserts genuinely do run in program order. - **Bulk JPQL `delete` bypasses the queue** and runs immediately when executed, which is another way to get the delete out first — at the cost of leaving the persistence context holding stale copies of the deleted rows. ## Answering it well Name the fixed order, explain that it exists for foreign-key safety, show the explicit flush as the mechanical fix, and then say the design-level fix is usually to replace delete-then-insert with an update. Candidates who jump straight to "disable the constraint" are giving the wrong answer.

  • Why doesn't Hibernate just execute the statements in the order the application called them?
    Because program order frequently violates referential integrity: a child inserted before its parent, or a parent deleted before its children are unlinked, would fail on foreign keys. The fixed insert-before-update-before-delete order satisfies those dependencies for the common object graph without Hibernate having to compute and topologically sort a dependency graph on every flush.
  • What does the extra flush() cost you?
    It ends the current batch, so statements accumulated so far are sent immediately and JDBC batching cannot span the boundary, adding a round trip. It also takes the DELETE's row locks earlier, lengthening the window in which other transactions can block. Both are usually acceptable next to a failing transaction, but they are a reason to prefer redesigning to an UPDATE.

A courier who sorts all parcels by category before delivering: every pickup happens after every drop-off, no matter what order you handed them over. Fine for most routes, disastrous when one parcel must leave a shelf before the next one fits.

saying these in an interview costs you the question

  • Assuming statements are sent in the order the code calls persist/remove
  • Blaming the database or proposing to drop the unique constraint
  • Adding flush() calls everywhere instead of at the one ordering boundary that needs it
  • Thinking em.remove() issues the DELETE immediately
  • Claiming that clearing a collection deletes its rows before any inserts are sent

context