Home/Learn/Hibernate & JPA/One-to-Many Mapping

One-to-Many Mapping

Intermediate
Relationships

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

Java — Bidirectional One-to-Many
// 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.

Java — Cascade + orphanRemoval
// 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.

Java — Cartesian Product + equals/hashCode
// 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 Aria
1

What does mappedBy mean in @OneToMany and which side is the owning side?

MediumAmazon
2

What is the difference between CascadeType.REMOVE and orphanRemoval=true?

MediumFlipkart
3

Why is CascadeType.REMOVE dangerous on a large collection?

MediumUber
4

What causes a Cartesian product in JPQL and how do you avoid it?

HardNetflix
5

Why should you not implement equals/hashCode on the auto-generated @Id field?

HardGoldman Sachs

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.

Loading discussion…