skip to content

You load an order with its lines, let the persistence context close, edit the detached graph (change one line, add a new one, drop another), then call EntityManager.merge() on the order. What does the cascade configuration control here, and what happens to the line you dropped?

level: seniorimportance: should knowfreq 38%

answer

  1. merge copies state, returns managed copy
  2. argument stays detached
  3. no cascade MERGE → child edits silently lost
  4. dropped child deleted only with orphanRemoval
  5. uninitialised lazy collection = skipped, not emptied

basics

~20 s

CascadeType.MERGE decides whether the children are merged at all — edited ones update, new ones become inserts. The dropped line is deleted only if the association declares orphanRemoval = true; otherwise its row survives. And merge returns a managed copy: your detached object stays detached.

solid answer

~50 s

`merge(order)` copies state from the detached instance onto a managed instance the provider loads (or creates), and returns that managed copy — the object you passed remains detached, so keep working with the return value. With `CascadeType.MERGE` on the collection, the merge walks into the children: modified children produce `UPDATE`s, and children with no identifier are treated as new and become managed instances that are inserted. Without the cascade, the children are not merged at all and their edits are silently lost. The dropped child is the interesting case. Merge replaces the managed collection's contents with the detached collection's contents. That disassociates the missing element — a *no-op* unless the association declares `orphanRemoval = true`, in which case flush deletes it. Two caveats: an uninitialised lazy collection in the detached graph is ignored by merge rather than treated as empty, and merge issues a `SELECT` per unloaded entity, so merging a large graph can be surprisingly chatty.

code

java · 9 lines
java
// detached, outside any transaction
order.getLines().get(0).setQty(5);          // edit
order.getLines().add(new OrderLine("SKU-9", 1)); // new, id == null
order.getLines().remove(dropped);            // disassociated

// inside a transaction
Order managed = em.merge(order); // use the RETURN value from here on
// UPDATE for the edited line, INSERT for the new one,
// DELETE for `dropped` only if orphanRemoval = true

go deeper

for a junior

Know that merge returns a managed copy and that children need CascadeType.MERGE to be saved with the parent.

for a middle

Explain per-element outcomes (update / insert / disassociate) and why orphanRemoval is required for deletion.

for a senior

Add the uninitialised-collection rule, the SELECT-per-entity cost, and why merging a client-built aggregate is risky for partial updates.

for a principal

Argue for the API-level pattern — load-and-apply inside the transaction versus merging a detached graph — and where the aggregate boundary should stop cascading.

## What merge does, precisely `merge` is a state-copying operation, not an attach operation. Given a detached instance: 1. The provider looks for a managed instance with the same identifier in the current persistence context; if there is none, it loads one with a `SELECT` (or creates a new one when the identifier is absent). 2. It copies the field values from the detached instance onto that managed instance. 3. It returns the managed instance. The argument you passed is untouched and still detached. `Order managed = em.merge(order);` — subsequent changes must go to `managed`. This trips people constantly: they call `em.merge(order)`, then mutate `order`, and nothing is written. ## What cascade controls in this flow Without `CascadeType.MERGE` on the `lines` association, the children are not merged, so the edits made to them while detached never reach the database. This is the quiet failure mode — nothing throws, the parent updates, the child edits vanish. With `CascadeType.MERGE` (usually via `ALL`), merge recurses along the association: - **Modified existing child** (has an id, matches a row) — state is copied onto the managed child, dirty checking produces an `UPDATE` at flush. - **New child** (null id) — merging a transient instance creates a *new* managed entity and schedules an `INSERT`. Note the copy semantics again: the new managed child is not the object you added to your detached list, so if you need its generated id you must read it from the merged graph. - **Unchanged child** — copied, no dirty state, no SQL. ## The dropped child The merged collection's contents replace the managed collection's contents. The element you removed from the detached list is therefore no longer in the managed collection — it has been *disassociated*. - **Without `orphanRemoval`**: disassociation means nothing on a bidirectional mapping (the inverse side drives no DML), so the row stays, still pointing at the parent. Reload the order and the "deleted" line reappears. On a unidirectional `@OneToMany` with a join column, Hibernate nulls the FK instead; with a `not null` column, that fails. - **With `orphanRemoval = true`**: the element counts as an orphan and flush emits `delete from order_line where id = ?`. So the honest answer to "how do I edit a detached aggregate and save it in one call" is `cascade = ALL, orphanRemoval = true` on the owned collection — with the ownership assertion that implies. ## The uninitialised-collection subtlety Merge only reconciles collections that are actually initialised in the detached graph. If `order.lines` is an uninitialised lazy collection (you never touched it before the context closed), Hibernate leaves the persistent collection alone rather than interpreting it as empty. That is a deliberate safety valve: without it, every detached-object merge would wipe every untouched collection. It also means a serialization round trip that strips the Hibernate collection and hands you a plain empty `ArrayList` *does* look like "the user deleted everything" — a real production hazard when DTO mapping is sloppy. ## Cost Each entity in the merged graph that is not already in the persistence context costs a `SELECT` — Hibernate must have the current database state to copy onto. Merging a deep graph therefore produces a burst of queries plus the dirty-check `UPDATE`s. When you know the identifier and intend a wholesale overwrite, a targeted `find` + explicit field assignment inside the transaction is usually cheaper and clearer than merging a large detached graph. Cascading `MERGE` on associations that reach shared reference data is also a hazard: you can overwrite a `Customer` row with the stale copy that travelled out to a client. ## Checklist for the interview answer 1. Merge copies state and returns a managed copy; the argument stays detached. 2. `CascadeType.MERGE` decides whether children participate at all. 3. New children (no id) are inserted; existing ones are updated. 4. Dropped children are only deleted under `orphanRemoval`. 5. Uninitialised collections are skipped, not emptied. 6. Merge costs a `SELECT` per unloaded entity — do not cascade it onto shared data.

  • You call em.merge(order) and then set a field on the same `order` variable. Why is that change not saved?
    Because merge does not attach the instance you passed — it copies its state onto a managed instance and returns that one. Your variable is still detached, so it is not dirty-checked and its later mutations are invisible to the persistence context. You must assign the result (`order = em.merge(order)`) or work with the returned reference.
  • A DTO-to-entity mapper produces a detached order whose lines list is empty because the client never sent them. What does merge do, and why is that dangerous under orphanRemoval?
    An explicitly empty plain collection is not the same as an uninitialised Hibernate collection: merge treats it as the new contents, so every existing line is disassociated. Without orphanRemoval the rows just survive, but with orphanRemoval Hibernate deletes them all. This is why partial-update endpoints should not merge a whole reconstructed aggregate — load the managed entity and apply only the fields the request actually carried.

saying these in an interview costs you the question

  • Believing merge attaches the passed instance instead of returning a managed copy
  • Expecting a child dropped from a detached collection to be deleted without orphanRemoval
  • Assuming merge is free — it selects each unloaded entity in the graph
  • Cascading MERGE onto shared reference entities and overwriting them with stale state
  • Thinking an uninitialised lazy collection in a detached graph is merged as empty

context