One-to-Many Mapping
Intermediate@OneToMany on the parent with @ManyToOne on the child side creates a bidirectional relationship; a join column on the many side holds the FK; mappedBy avoids a redundant join table.
Overview
The one-to-many / many-to-one relationship is the most common association in JPA — an Order has many OrderItems, a Department has many Employees. It maps to a foreign key column on the "many" (child) side's table. JPA supports unidirectional @OneToMany (FK on child, but only parent side declares the relationship), bidirectional @OneToMany + @ManyToOne (both sides declare the relationship, the @ManyToOne side is the "owner"), and a less-common join-table variant. Bidirectional is the standard approach for most use cases. The two most critical things to understand: the mappedBy attribute (which side owns the FK), and the cascade + orphanRemoval settings that control how child persistence relates to the parent.
Bidirectional @OneToMany + @ManyToOne
In a bidirectional relationship: - The **many** (child) side owns the FK column and is the "owning side" — annotated with @ManyToOne + @JoinColumn. - The **one** (parent) side uses @OneToMany(mappedBy = "fieldName") where fieldName is the @ManyToOne field in the child.
`mappedBy` tells JPA: "the FK is managed by the other side; do not create a join table here."
Critical: always maintain **both sides** of a bidirectional relationship in Java. JPA uses the owning side (child) to determine what SQL to write, but Hibernate's in-memory state depends on both sides being consistent.
// PARENT — one Order has many OrderItems
@Entity
@Table(name = "orders")
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(
mappedBy = "order", // "order" = field name in OrderItem
cascade = CascadeType.ALL, // persist/remove propagates to items
orphanRemoval = true, // delete orphaned items from DB
fetch = FetchType.LAZY // do NOT load items unless requested
)
private List<OrderItem> items = new ArrayList<>();
// Helper method — keeps BOTH sides consistent
public void addItem(OrderItem item) {
items.add(item);
item.setOrder(this); // set the owning side
}
public void removeItem(OrderItem item) {
items.remove(item);
item.setOrder(null);
}
}
// CHILD — many OrderItems belong to one Order
@Entity
@Table(name = "order_items")
public class OrderItem {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // owning side — FK is here
@JoinColumn(name = "order_id") // column name in order_items table
private Order order;
private String productName;
private int quantity;
private BigDecimal price;
}CascadeType and orphanRemoval
Cascade controls which EntityManager operations propagate from parent to child:
- **CascadeType.PERSIST** — persisting the parent also persists new children. - **CascadeType.MERGE** — merging the parent also merges attached/detached children. - **CascadeType.REMOVE** — deleting the parent deletes all children (careful with large collections). - **CascadeType.ALL** — all of the above.
**orphanRemoval=true** deletes a child when it is removed from the parent's collection — even without cascade REMOVE. This is the right choice when the child has no meaning outside the parent (composition). Do not use it when children can belong to multiple parents.
// With cascade=ALL + orphanRemoval=true:
@Transactional
public void removeFirstItem(Long orderId) {
Order order = orderRepo.findById(orderId).orElseThrow();
OrderItem first = order.getItems().get(0);
order.removeItem(first); // removes from collection
// orphanRemoval=true → Hibernate emits: DELETE FROM order_items WHERE id=?
// No explicit em.remove() needed!
}
// With cascade=ALL:
@Transactional
public Order create(OrderRequest req) {
Order order = new Order();
req.getItems().forEach(i -> order.addItem(new OrderItem(i)));
return orderRepo.save(order); // cascade=PERSIST → also INSERTs all items
}
// DANGER: CascadeType.REMOVE on a large collection
// Hibernate loads ALL children into memory, then deletes one by one
// Use a bulk delete query instead for large collections:
@Modifying
@Transactional
@Query("DELETE FROM OrderItem i WHERE i.order.id = :orderId")
void deleteItemsByOrder(@Param("orderId") Long orderId);Common Pitfalls — Cartesian Product and equals/hashCode
**Cartesian product on multiple JOIN FETCHes**: Fetching two @OneToMany collections with JOIN FETCH in a single JPQL query produces a Cartesian product. Use separate queries or @EntityGraph with subgraph instead.
**equals/hashCode on mutable entities**: If you use entities in Sets or as Map keys, implement equals/hashCode based on a business key (not the auto-generated DB id which is null before persist). Hibernate recommends using a natural ID or a UUID assigned in the constructor.
// DANGER: Cartesian product — two JOIN FETCH on one query
// DON'T:
@Query("SELECT o FROM Order o JOIN FETCH o.items JOIN FETCH o.tags WHERE o.id = :id")
// items(5) × tags(3) = 15 rows → Hibernate deduplicates but SELECT is wasteful
// DO: separate query per collection
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findWithItems(@Param("id") Long id);
// Then lazy-load tags separately in a second query when needed
// equals/hashCode — use business key, not @Id
@Entity
public class OrderItem {
@Id @GeneratedValue private Long id;
@Column(nullable = false, unique = true)
private String lineItemRef = UUID.randomUUID().toString(); // assigned in constructor
@Override
public boolean equals(Object o) {
if (!(o instanceof OrderItem other)) return false;
return Objects.equals(lineItemRef, other.lineItemRef);
}
@Override
public int hashCode() {
return Objects.hash(lineItemRef);
}
}Key Points to Remember
- 1The @ManyToOne side is the owning side — it holds the FK column; @OneToMany(mappedBy=) tells JPA not to create an extra join table.
- 2Always maintain both sides of a bidirectional relationship: set child.setParent(parent) AND parent.getChildren().add(child).
- 3CascadeType.ALL propagates all operations to children; orphanRemoval=true deletes children removed from the collection.
- 4Never use CascadeType.REMOVE (or ALL) on large collections — Hibernate loads every child into memory before deleting; use a bulk @Modifying @Query instead.
- 5Two @OneToMany JOIN FETCHes in one JPQL query creates a Cartesian product — use separate queries or @EntityGraph subgraphs.
- 6Implement equals/hashCode on a business key (UUID or natural key), not the auto-generated ID (which is null before persist).
Interview Questions
Sign in to ask AriaWhat does mappedBy mean in @OneToMany and which side is the owning side?
What is the difference between CascadeType.REMOVE and orphanRemoval=true?
Why is CascadeType.REMOVE dangerous on a large collection?
What causes a Cartesian product in JPQL and how do you avoid it?
Why should you not implement equals/hashCode on the auto-generated @Id field?
Ask Aria about One-to-Many Mapping
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.