What is the difference between mapping a JPA association with CascadeType.REMOVE and mapping it with orphanRemoval = true, and when does each one actually delete a row?
answer
- REMOVE = parent deleted → children deleted
- orphanRemoval = child unlinked → child deleted
- only @OneToOne / @OneToMany
- clear()+addAll(), never setList(new ...)
- bulk JPQL delete bypasses both
basics
~20 sCascadeType.REMOVE only fires when you explicitly remove the parent — the children go with it. orphanRemoval = true does that too, and additionally deletes a child the moment it is taken out of the parent's collection or its reference is set to null, even though the parent survives.
solid answer
~40 sBoth end in a `DELETE` for the child, but they are triggered by different events. `CascadeType.REMOVE` propagates one specific call: `em.remove(parent)` becomes `em.remove(child)` for each associated child. If the parent is never removed, nothing happens. `orphanRemoval = true` says the child cannot exist unowned. Hibernate compares the collection against its loaded snapshot at flush; any element that disappeared is *orphaned* and gets deleted — not just unlinked. Removing the parent also deletes the children, so orphanRemoval implies remove-cascade behaviour for that association. Orphan removal is only legal on `@OneToOne` and `@OneToMany` — a single-owner association. It is meaningless on `@ManyToMany`/`@ManyToOne` because the target has other owners. The classic trap: replacing the collection instance (`setLines(new ArrayList<>())`) instead of mutating it throws "A collection with cascade=all-delete-orphan was no longer referenced". Use `clear()` then `addAll()`.
code
java · 10 lines@Entity
public class Order {
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderLine> lines = new ArrayList<>();
}
Order order = em.find(Order.class, 1L);
order.getLines().remove(0); // orphanRemoval -> DELETE at flush
// order.setLines(new ArrayList<>()); // WRONG: "collection ... no longer referenced"
order.getLines().clear(); // right way to empty itgo deeper
State the trigger difference clearly: parent removed versus child unlinked, and that orphanRemoval covers both.
Add the flush-time snapshot diffing, the legal association types, and the collection-replacement exception.
Discuss the operational cost — per-row DELETEs, forced initialisation of large collections — and the bulk-delete and DB-level ON DELETE CASCADE interplay.
Frame it as an aggregate-lifecycle contract: orphanRemoval asserts single ownership, and you would reject it wherever a child can be referenced from another aggregate.
## Two different triggers, one outcome Both settings can end with the same SQL — `delete from order_line where id = ?` — but they are wired to different events, and mixing them up produces either orphan rows in the database or unexpected deletions. ### CascadeType.REMOVE — propagate an explicit removal ```java @OneToMany(mappedBy = "order", cascade = CascadeType.REMOVE) private List<OrderLine> lines; ``` The only trigger is `em.remove(order)`. Hibernate then calls remove on each element it can reach along that edge. Note what this costs: to remove the children it must *know* them, so a lazy collection gets initialised, every child is loaded into the persistence context, and one `DELETE` is issued per row (so that `@PreRemove` callbacks fire and the second-level cache is evicted correctly). Deleting a parent with 10 000 lines means 10 001 statements. If you never remove the parent, `CascadeType.REMOVE` never does anything. Take a line out of the collection and — on a bidirectional mapping — the row simply stays in the table with its FK intact, because the inverse collection drives no DML. On a unidirectional `@OneToMany` with a join column, Hibernate instead issues `update order_line set order_id = null where id = ?`, which fails outright if the FK column is `not null`. Either way, no delete. ### orphanRemoval = true — the child cannot exist unowned ```java @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) private List<OrderLine> lines; ``` This declares a *lifecycle dependency*: an `OrderLine` outside an `Order` is meaningless, so the provider deletes it. Two triggers now: 1. `order.getLines().remove(line)` — at flush, Hibernate diffs the persistent collection against the snapshot it captured when the collection was loaded, finds `line` missing, and schedules a `DELETE` for it. 2. `em.remove(order)` — the children are removed as well; the spec defines orphan removal as applying the remove operation to the disassociated target, and removing the parent disassociates everything. That is why `orphanRemoval = true` makes `CascadeType.REMOVE` redundant on the same association (people still write both; it is harmless). For a `@OneToOne`, setting the reference to `null` is the disassociation event. ## Where orphanRemoval is legal Only on `@OneToOne` and `@OneToMany`. The attribute does not exist on `@ManyToOne`/`@ManyToMany`, and the reason is semantic: orphan removal asserts *exactly one* owner, and a many-valued source side means other rows may still reference the target. If you find yourself wanting it on a many-to-many, the model is wrong — that association is not composition. ## The traps **Replacing the collection instance.** Hibernate wraps your collection in a `PersistentCollection` that carries the snapshot. Assigning a brand-new list throws away the wrapper, and with delete-orphan semantics Hibernate cannot compute which elements were removed: ```java order.setLines(new ArrayList<>(newLines)); // HibernateException: A collection with // cascade="all-delete-orphan" was no longer referenced by the owning entity instance ``` The fix is to mutate in place: `order.getLines().clear(); order.getLines().addAll(newLines);`. **Moving a child between parents.** Removing a line from order A and adding it to order B, in the same persistence context, still registers the orphan against A — Hibernate deletes it. Reparenting under orphan removal is unsafe; create a new child instead. **Uninitialised collections during merge.** Orphan removal is computed from the loaded snapshot. If a detached graph's collection was never initialised, Hibernate has nothing to diff and does not delete anything. **Not a database constraint.** Neither setting emits `ON DELETE CASCADE`. If another table references the child by FK and the ORM does not know about it, the delete fails at the database. Conversely, if you *do* declare `ON DELETE CASCADE` in DDL, the database deletes rows the persistence context still holds as managed entities — stale state until the context is cleared. **Bulk statements bypass both.** `delete from Order o where o.status = 'X'` in JPQL is translated to a single SQL `DELETE`; no entity is loaded, so no cascade and no orphan removal run. You either delete the children with their own bulk statement or rely on a database FK cascade. ## The one-line distinction to say out loud "`CascadeType.REMOVE` answers *what happens when I delete the parent*. `orphanRemoval` answers *what happens when I detach a child from its parent*. The second implies the first."
- Why does Hibernate throw "A collection with cascade=all-delete-orphan was no longer referenced by the owning entity instance"?Because you replaced the collection field with a new instance instead of mutating the managed one. Hibernate wraps a loaded collection in a PersistentCollection holding the snapshot it needs to compute which elements were orphaned; assigning a fresh ArrayList discards that wrapper, so it can no longer diff. The fix is `clear()` followed by `addAll()` on the existing collection.
- Do cascade REMOVE and orphanRemoval apply to a bulk JPQL delete statement?No. A bulk `DELETE FROM Entity e WHERE ...` is translated straight to SQL; no entities are loaded into the persistence context, so no lifecycle operation is cascaded and no orphan diffing happens. Children are left behind and the statement may fail on a foreign-key constraint. You must delete children with their own bulk statement, or configure `ON DELETE CASCADE` in the schema and accept that already-managed entities become stale.
CascadeType.REMOVE is demolishing a building and everything inside it. orphanRemoval is a rule that anything carried out of the building is destroyed at the door — the building is still standing.
saying these in an interview costs you the question
- Saying orphanRemoval and CascadeType.REMOVE are just aliases
- Expecting removal from an inverse collection to delete the row without orphanRemoval
- Reassigning the collection field (setLines(new ArrayList<>())) to clear it
- Trying to put orphanRemoval on a @ManyToMany or @ManyToOne
- Assuming a bulk JPQL delete triggers cascades