skip to content

Cascade Types & orphanRemoval

Which lifecycle operations propagate across an association, and the difference between removing an orphan and cascading a delete. A favorite trap question because CascadeType.REMOVE on @ManyToMany deletes rows other parents still need.

part ofHibernateoverview, primer and where to startread it →
on this pageshow

questions

6

In JPA, if you call EntityManager.persist() on a parent entity whose collection holds brand-new child entities, what happens to those children by default, and what does configuring cascade on that association change?

level: juniorimportance: must knowfreq 62%

answer

  1. persist applies to one instance only
  2. transient child → "unsaved transient instance"
  3. cascade = how the call travels the graph
  4. cascade ≠ owning side / FK
  5. ALL silently includes REMOVE

basics

~20 s

By default persist applies only to the instance you pass. The new children stay transient, and at flush Hibernate typically fails with "object references an unsaved transient instance". Declaring cascade = PERSIST (or ALL) makes the provider run persist on each child too.

solid answer

~50 s

JPA entity-manager operations are single-instance: `persist(parent)` makes *that* object managed and nothing else. A new child sitting in the parent's collection is still transient, so at flush time Hibernate either refuses to write the graph — `TransientObjectException` / "object references an unsaved transient instance - save the transient instance before flushing" — or the child is simply never inserted if nothing forces the check. Adding `cascade = CascadeType.PERSIST` (or `ALL`, which contains it) on the association tells the provider to propagate the persist call along that association, transitively, and also to newly reachable children discovered at flush time. Two things cascade does **not** do: it does not set the foreign key for you — on a bidirectional association you still assign `child.setParent(parent)` on the owning `@ManyToOne` side — and it does not choose insert order for you; Hibernate orders parent INSERT before child INSERT because the child carries the FK.

code

java · 17 lines
java
@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "order", cascade = CascadeType.PERSIST)
    private List<OrderLine> lines = new ArrayList<>();

    public void addLine(OrderLine line) {
        lines.add(line);
        line.setOrder(this); // owning side writes the FK
    }
}

Order order = new Order();
order.addLine(new OrderLine("SKU-1", 2));
em.persist(order); // children persisted through the cascade

go deeper

for a junior

Know that persist affects only the object you pass, that cascade propagates it, and that you still have to set the owning side.

for a middle

Add the flush-time mechanics: reachability, insert ordering, and exactly which exception fires and why.

for a senior

Frame cascade as an aggregate-boundary decision and mention why ALL is a poor default on associations to shared entities.

for a principal

Discuss cascade as a modelling contract that encodes ownership, and the cost of graph-walking cascades on large collections versus explicit persistence.

## What cascading actually is A JPA `EntityManager` exposes lifecycle operations — `persist`, `merge`, `remove`, `refresh`, `detach` — and every one of them applies to exactly one object: the argument you pass. Cascading is a per-association setting (`@OneToMany(cascade = ...)`, `@ManyToOne(cascade = ...)`, etc.) that instructs the provider to apply that same operation to whatever the association points at, and then recursively onward from there. So `cascade` is not a database feature and not a mapping of the FK. It is a rule about how a lifecycle call travels through the object graph in memory. ## The default is: nothing propagates With no `cascade` attribute, the default is an empty set. Consider: ```java Order order = new Order(); order.addLine(new OrderLine("SKU-1", 2)); // both objects are transient em.persist(order); ``` `persist(order)` assigns `order` to the persistence context and (for identity/sequence generators) may reserve its id. `OrderLine` is untouched — it is still a plain transient object the provider knows nothing about. What happens next depends on how the association is mapped: - **Bidirectional `@OneToMany(mappedBy = "order")`** — the collection is the inverse side and does not drive DML. But Hibernate still walks the collection at flush and, finding a transient element it is not allowed to persist, throws `TransientObjectException`: *"object references an unsaved transient instance - save the transient instance before flushing"*. - **Owning `@ManyToOne` on the child pointing at a transient parent** — Hibernate must write the FK column and has no id for the parent, so the same exception fires. - **You never wire the child anywhere** — then nothing at all is inserted and the child is silently lost. This is the failure mode juniors most often report as "my children didn't save". ## Turning cascading on ```java @OneToMany(mappedBy = "order", cascade = CascadeType.PERSIST) private List<OrderLine> lines = new ArrayList<>(); ``` Now `persist(order)` also calls persist on every element of `lines`, which becomes every element reachable transitively (a line's own cascaded children get persisted too). Hibernate then orders the SQL sensibly at flush: the parent INSERT runs first so the generated id is available for the children's FK column. Cascading is also re-evaluated at flush, which is why adding a fresh child to an already-managed parent inside a transaction inserts it without an explicit `persist` call — this is *persistence by reachability* along cascade-PERSIST paths. ## Cascade does not fix the owning side The most common follow-on bug: cascading works, rows appear, but the FK column is null or an extra UPDATE shows up. ```java order.getLines().add(line); // inverse side only line.setOrder(order); // REQUIRED: owning side sets the FK ``` In a bidirectional association the `@ManyToOne` side owns the FK column. Hibernate reads the FK from the child, not from the parent's collection. Cascade decides *whether the child is persisted*; the owning side decides *what its FK value is*. A helper method on the parent (`addLine`) that sets both directions is the standard defence. ## Choosing PERSIST vs ALL `CascadeType.ALL` is a shorthand for all six standard operations, including `REMOVE`. That means deleting the parent deletes the children — which is exactly right for a composition (an order and its lines) and exactly wrong for an association where the target has an independent life (an order and its customer). Junior-level correct instinct: cascade **from** the aggregate parent **to** its owned children, and start with the narrowest set that makes your code work, usually `PERSIST` and `MERGE`. ## Practical checklist 1. New children not saved → missing `CascadeType.PERSIST` (or a missing explicit `persist`). 2. "object references an unsaved transient instance" → a managed object points at a transient one along a non-cascaded path. 3. Children saved but FK null → owning side never set. 4. Deleting a parent unexpectedly deletes reference data → `CascadeType.ALL` used where `REMOVE` was not intended.

  • You added CascadeType.PERSIST and the child rows appear, but order_id is null. What is wrong?
    The owning side of the association was never set. In a bidirectional one-to-many, the `@ManyToOne` on the child owns the FK column, and Hibernate writes the FK from that field — the parent's `mappedBy` collection is read-only for persistence purposes. Adding the child to the parent's list is not enough; you must also call `child.setParent(parent)`, ideally inside a helper method so the two directions cannot drift.
  • If a child is added to an already-managed parent in the middle of a transaction, do you have to call persist on it?
    Not if the association carries `CascadeType.PERSIST` or `ALL`. Cascading is re-evaluated during flush, so Hibernate detects the newly reachable transient instance and persists it — persistence by reachability. Without that cascade you must call `em.persist(child)` yourself, otherwise flush throws a transient-object exception or the child is never inserted.

Cascade is a forwarding rule on a mailbox: the letter is addressed to one entity, and the rule says "also forward a copy to everything on this shelf". Without the rule, the neighbours never hear about it.

saying these in an interview costs you the question

  • Believing persist automatically saves the whole reachable object graph
  • Thinking cascade sets the foreign key, so the owning side can be skipped
  • Reaching for CascadeType.ALL by default without noticing it includes REMOVE
  • Claiming the "unsaved transient instance" error is a database constraint problem

context

open as a page

Which cascade operations can be configured on a JPA association, what does each one propagate, and why does the specification make cascading opt-in rather than the default?

level: middleimportance: must knowfreq 55%

basics

~20 s

PERSIST, MERGE, REMOVE, REFRESH, DETACH, and ALL (all five). Each forwards the matching EntityManager call along the association. Cascading is opt-in because propagation is only correct where the parent truly owns the target — REMOVE especially would destroy shared data.

open as a page

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?

level: middleimportance: must knowfreq 60%

basics

~20 s

CascadeType.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.

open as a page

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%

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.

open as a page

Why is declaring CascadeType.REMOVE (or CascadeType.ALL) on a JPA @ManyToMany association — or on the @ManyToOne side of a parent-child relationship — considered dangerous?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Because the target is shared. Removing one post would delete tags other posts still use, or on a @ManyToOne would delete the parent and, through it, every sibling. Cascade must only follow edges where the source truly owns the target.

open as a page

Across a large JPA domain model, how do you decide which associations should carry cascade settings and orphanRemoval and which should carry none, and what breaks when that boundary is drawn in the wrong place?

level: principalimportance: should knowfreq 30%

basics

~20 s

Cascade only along composition edges inside one aggregate: root to children that have no identity of their own and are referenced by nothing outside. Across aggregate roots, reference by id with no cascade. Wrong boundaries cause shared-data deletion, unbounded graph loads, and hidden write amplification.

open as a page