Relationships — @OneToMany, @ManyToOne, @ManyToMany, Cascade & FetchType
Every relationship in your entity model requires a deliberate answer to "what happens to the children when the parent changes" and "should this load immediately or on demand" — getting these two decisions wrong is the most common source of both accidental cascading deletes and N+1 query problems.
Learning objectives
- Explain owning side vs. inverse side in a bidirectional relationship and what mappedBy actually does.
- Choose cascade and orphanRemoval settings deliberately based on a relationship's actual ownership semantics.
- Explain why FetchType.LAZY should be the default and identify JPA's actual (often-surprising) default for @ManyToOne/@OneToOne.
Getting fetch strategy and cascade configuration wrong is the single most common source of both N+1 query problems (a later chapter) and accidental cascading deletes in real production Hibernate applications.
📖 Story
Think about a family tree: a parent can have many children; each child has exactly one biological parent. If you delete a parent's record, should all their children's records be deleted too? For a family tree, obviously not. But for a Customer and their Orders — should deleting the customer delete their orders too, or leave them orphaned, pointing at nothing?
@Entity public class Customer { @Id @GeneratedValue private Long id; @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true) private List<Order> orders = new ArrayList<>(); } @Entity public class Order { @Id @GeneratedValue private Long id; @ManyToOne(fetch = FetchType.LAZY) private Customer customer; // <- this side OWNS the foreign key }
Every relationship in your entity model demands this exact decision, made deliberately, every single time — not left to whatever default Hibernate happens to apply.
@OneToMany/@ManyToOne — a one-to-many relationship, expressed from both sides. Owning side — the side that actually controls the foreign key column; the other side is the inverse side, marked mappedBy. Cascade — which operations (persist, merge, remove) on a parent automatically propagate to its children. FetchType (LAZY/EAGER) — whether a related entity/collection loads immediately, or only on first access. OrphanRemoval — automatically deleting a child when it's removed from its parent's collection.
Let's unpack every piece of this chapter's Customer/Order example.
Owning side vs. inverse side
mappedBy = "customer" on the @OneToMany side tells Hibernate: "don't create a separate join table for this — the foreign key already lives on the Order side's customer field." The @ManyToOne side is the OWNING side; it's the one that actually persists the relationship. Forgetting mappedBy (or getting the owning side backward) is a common, confusing mistake that leads to Hibernate quietly generating an unexpected extra join table.
Cascade — propagating operations, deliberately
customer.getOrders().add(newOrder); entityManager.persist(customer); // Because cascade = CascadeType.ALL, this ALSO persists newOrder — no // separate entityManager.persist(newOrder) call needed.
This is genuinely convenient for a true "part of" relationship (an order's line items can't meaningfully exist without the order). It would be DANGEROUS on the wrong relationship — cascading remove from Order to Customer would delete the customer just because one order was removed, almost certainly not what you want.
FetchType — LAZY should be your default
@ManyToOne(fetch = FetchType.LAZY) // <- NOT the JPA default! See below. private Customer customer;
Here's a genuine gotcha: plain JPA's DEFAULT for @ManyToOne/@OneToOne is actually EAGER, not LAZY — the opposite of what most developers assume. Leaving it at the default means every single Order load also loads its full Customer, every time, whether you need it or not.
OrphanRemoval — cleanup without a full cascade
customer.getOrders().remove(someOrder); // Because orphanRemoval = true, someOrder gets DELETED at the next flush — // even without CascadeType.REMOVE on the whole relationship.
Hibernate implements a @OneToMany collection as a specially instrumented wrapper that tracks additions/removals against the persistence context — when the transaction flushes, Hibernate translates observed changes into the appropriate INSERT/UPDATE/DELETE for the child rows. For @ManyToMany, Hibernate manages a separate join table transparently — adding to either side's collection results in an INSERT into that join table at flush time.
- This chapter's
Customer/Orderrelationship, exactly as written —cascade = ALLandorphanRemoval = trueon theCustomer-owns-Orderside, so removing an order from a customer's collection deletes it, and deleting a customer deletes all their orders too — is completely standard, real production configuration for a genuine parent-owns-child relationship. - A
Student/Course@ManyToManyrelationship (enrollment) is the classic join-table scenario — neither entity should ever be deleted just because an enrollment relationship changes.
- Default every collection association to
FetchType.LAZY— treatEAGERas something you opt into deliberately. - Choose cascade behavior per relationship based on genuine ownership — never apply
CascadeType.ALLbroadly as a blanket default, as this chapter's story warns. - Get the owning side (and
mappedBy) right deliberately for every bidirectional relationship.
⚠️ Why this keeps happening
Cascade and fetch type are both configured with a single annotation attribute, easy to set once (often copied from an example) and never revisit — even though each relationship's correct configuration depends on ITS OWN specific ownership semantics.
- Applying
CascadeType.ALLto every relationship by default, risking an accidental cascading delete on a relationship that was never meant to propagate removal. - Not knowing
@ManyToOne/@OneToOnedefault to EAGER in plain JPA — this chapter's Concept Overview flagged this exact gotcha. - Forgetting
mappedByon the inverse side, causing Hibernate to create an unexpected extra join table.
FetchType.EAGER on a widely-used entity is a very common, very real root cause of unnecessarily large queries — auditing entities for accidental EAGER defaults (especially @ManyToOne/@OneToOne) is a genuinely high-value performance review.
An overly broad CascadeType.ALL combined with a client-controlled delete request is worth reviewing carefully — an attacker (or careless client) triggering a parent delete could unintentionally cascade-delete related data they were never meant to remove.
Track query counts and payload sizes for endpoints touching entities with EAGER associations — a rising average query count per request is a direct symptom of an overly broad fetch strategy.
Document cascade and fetch-type decisions explicitly, right on the relationship annotation — "line items don't exist without their order, cascade+orphanRemoval intentional" — so a future engineer doesn't reverse-engineer the intent.
- Build this chapter's
Customer/Orderbidirectional relationship, correctly settingmappedBy, and confirm adding to the customer's order collection persists correctly. - Add
orphanRemoval = trueand confirm removing an order from a customer's collection deletes it at flush time with no explicit remove() call. - Change
@ManyToOne's fetch type from JPA's actual default (EAGER) to LAZY, and compare the generated SQL for loading the parent before and after.
✓ Quick recap
- The owning side controls the foreign key; the inverse side is marked
mappedBy. - Cascade and orphanRemoval propagate operations from parent to children — configure both deliberately per relationship's actual ownership semantics.
- Default every collection to
FetchType.LAZY; remember@ManyToOne/@OneToOnedefault to EAGER in plain JPA, a genuine and common gotcha.
Want a visual for this concept?
Generate a diagram tailored to “Relationships — @OneToMany, @ManyToOne, @ManyToMany, Cascade & FetchType” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →