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?
answer
- persist applies to one instance only
- transient child → "unsaved transient instance"
- cascade = how the call travels the graph
- cascade ≠ owning side / FK
- ALL silently includes REMOVE
basics
~20 sBy 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 sJPA 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@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 cascadego deeper
Know that persist affects only the object you pass, that cascade propagates it, and that you still have to set the owning side.
Add the flush-time mechanics: reachability, insert ordering, and exactly which exception fires and why.
Frame cascade as an aggregate-boundary decision and mention why ALL is a poor default on associations to shared entities.
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