skip to content

When Hibernate flushes a persistence context, which entity lifecycle states can produce SQL statements and which are ignored entirely? Include what happens when a managed entity holds a reference to an object that was never persisted.

level: seniorimportance: should knowfreq 40%

answer

  1. flush iterates the context; untracked = invisible
  2. managed -> INSERT/UPDATE, removed -> DELETE
  3. read-only managed = no snapshot = no UPDATE
  4. managed -> transient reference = TransientObjectException
  5. managed -> detached reference = FK written, edits ignored

basics

~20 s

Flush walks only the entities the context tracks. Managed ones can yield INSERT or UPDATE, removed ones yield DELETE. Transient and detached objects are not tracked, so they produce nothing — except a transient object reachable from a managed entity, which raises a TransientObjectException unless PERSIST cascades.

solid answer

~50 s

Flush iterates the persistence context, so **state decides visibility**. Managed instances that were persisted and never flushed produce an **INSERT**; managed instances whose values differ from their loaded snapshot produce an **UPDATE**; removed instances produce a **DELETE**. Transient and detached objects are not in the context at all, so no amount of mutation on them makes SQL — that silence is by design, not a bug. The interesting case is a *reachable* transient: while flushing, Hibernate cascades over the associations of managed entities. If it finds a reference to an unpersisted object and the association does not declare `cascade = PERSIST`, it refuses to write a dangling foreign key and throws `TransientObjectException` / `TransientPropertyValueException`. With the cascade declared, the referenced object is persisted first and the association is written. Detached references behave differently again: Hibernate will happily store the foreign key, since a row for that id already exists.

code

java · 8 lines
java
Order order = em.find(Order.class, 1L);       // managed

order.setCustomer(new Customer("Ada"));       // transient reference
em.flush();  // TransientObjectException unless cascade = PERSIST

order.setCustomer(detachedCustomer);          // has an id, row exists
detachedCustomer.setName("changed");
em.flush();  // FK written; the name change is NOT persisted

go deeper

for a junior

State that only entities the context tracks can produce SQL: managed for INSERT/UPDATE, removed for DELETE, and nothing for transient or detached.

for a middle

Add the snapshot comparison behind UPDATEs and the TransientObjectException rule with cascade = PERSIST as the fix.

for a senior

Use the model diagnostically — silent no-op versus loud failure, the detached-reference asymmetry, and spurious UPDATEs from in-place mutation or converters.

for a principal

Talk about designing the boundary so these states are unambiguous: which layer owns managed entities, where cascades are declared, and whether read-only contexts should be the default for query paths.

## Flush is a walk over the context A flush synchronises the in-memory state of the persistence context with the database. It does that by iterating what the context tracks — nothing else exists as far as flush is concerned. Every question about "why did/didn't SQL happen" reduces to whether the object was in the context and in which state. ## State by state **Managed, newly persisted, not yet flushed → INSERT.** `persist()` registered the entity and queued an insert (for identity-column generators the INSERT may already have run at `persist()` time, because Hibernate needs the generated key immediately; for sequence and table generators it is genuinely deferred). Either way, by the end of flush the row exists. **Managed, loaded, values changed → UPDATE.** Hibernate holds a *snapshot* of the property values as of the last synchronisation. At flush it compares the live object against that snapshot and emits an UPDATE for the difference. Nothing changed means no statement at all. **Managed, marked read-only → nothing.** Hibernate can hold an entity without a snapshot (`session.setReadOnly`, a read-only query hint). It stays managed and lazily loadable, but it cannot produce an UPDATE. **Removed → DELETE.** The instance is still tracked, and the queued delete executes. **Transient → nothing.** The context never heard of it. **Detached → nothing.** The context stopped tracking it and dropped its snapshot; there is nothing to compare and nothing to iterate. Collections are tracked separately: a managed entity's persistent collection carries its own snapshot, so adding or removing elements can yield INSERTs, DELETEs or UPDATEs against the join or child table without the owning row itself changing. ## The reachable-transient rule During flush Hibernate does not only look at each managed entity's own columns; it cascades over the associations to see what else must be written. Suppose: ``` Order order = em.find(Order.class, 1L); // managed Customer fresh = new Customer("Ada"); // transient order.setCustomer(fresh); // managed -> transient reference ``` To write `order.customer_id`, Hibernate needs `fresh`'s primary key, and `fresh` has none. Two outcomes: - **No cascade** on the association: `TransientObjectException` ("object references an unsaved transient instance — save the transient instance before flushing"), or in modern Hibernate `TransientPropertyValueException` naming the offending property. This is a deliberate guard; the alternative would be a NULL or invented foreign key. - **`cascade = {PERSIST}` (or ALL)**: Hibernate persists `fresh` first, obtains its id, and then writes the association. Ordering is handled by the action queue. Contrast with a **detached** reference: `order.setCustomer(detachedCustomer)` is fine at flush, because the detached object carries an identifier and the row already exists — Hibernate just writes the foreign key. It will not, however, propagate any *field changes* you made to that detached customer; only the association is written. That asymmetry (transient reference explodes, detached reference silently ignores your edits) causes real bugs and is worth stating explicitly in an interview. ## When flush happens Flush is triggered at transaction commit, by an explicit `flush()`, and — under the default automatic synchronisation — before a query whose results could be affected by pending changes. The trigger does not change the state rules above; it only changes *when* they are applied. What it does change is visibility: a query in the same transaction sees your pending INSERTs only if a flush precedes it. ## Diagnosing with the state model - *A field change produced no UPDATE.* The instance was detached (or read-only, or you mutated a copy rather than the managed instance). Check `em.contains(x)` at the mutation point. - *An UPDATE appeared that nobody asked for.* Something mutated a managed entity — often a mapper normalising a string, a lazy `@PostLoad` fixup, or a mutable type such as a `Date` whose contents were changed in place. - *TransientObjectException at commit.* A managed entity reaches a `new`ed object; persist it explicitly or declare the cascade. - *A change to a detached object I attached to a managed parent was ignored.* Expected: only the foreign key is written. Merge that object if you want its own columns updated. - *Duplicate rows.* An object Hibernate judged transient (null id / null version) went through `merge()` and became an INSERT. ## The one-sentence model Flush writes what the persistence context tracks: managed means "could produce INSERT or UPDATE", removed means "will produce DELETE", transient and detached mean "invisible — unless a managed entity points at a transient one, which is an error rather than a silent skip".

  • An UPDATE is emitted for an entity your code never intentionally modified. How do you find the cause?
    Something mutated the managed instance so that it no longer matches its loaded snapshot. Common culprits are in-place mutation of a mutable field (a Date or a collection), a converter or @PostLoad that normalises a value on read, or a type mismatch where the loaded value and the Java value never compare equal (for example a trailing-space CHAR column). Enable SQL logging with parameter values to see which column changed, then check the mapping and any AttributeConverter or lifecycle callback for that property.
  • Why does Hibernate refuse a transient reference instead of just writing NULL for the foreign key?
    Because both alternatives are wrong. Writing NULL loses data the application clearly intended to store and would often violate a NOT NULL constraint at an unrelated moment; inventing an identifier is impossible for generated keys. Failing loudly at flush points at the exact property and lets the developer choose between persisting the referenced object explicitly and declaring cascade = PERSIST, which is the decision Hibernate cannot make for you.

saying these in an interview costs you the question

  • Expecting mutations on detached objects to be picked up 'because the entity is the same row'.
  • Assuming a transient reference from a managed entity is silently skipped rather than an error.
  • Thinking cascade = MERGE or REMOVE will save a reachable transient instance (only PERSIST/ALL does).
  • Believing an entity marked read-only can still produce an UPDATE.
  • Assuming edits to a detached object are written when you attach it to a managed parent.

context