Appearance
Spring Boot JPA (Part 7) | OneToMany, ManyToOne and ManyToMany Mapping
The Shopping Cart and Line Items Analogy
Imagine shopping on an online marketplace. You create a single Shopping Cart order. Inside that order, you place five distinct items: a pair of running shoes, a book, a phone charger, an umbrella, and a water bottle.
This is a classic One to Many relationship. One Order contains Many Order Items. From the reverse perspective, every single Order Item belongs to exactly One Order: this is Many to One.
Now consider the relationship between Students and University Courses. One Student can enroll in Many Courses, and one Course contains Many Students. Neither side owns the other exclusively. This is Many to Many, which requires a dedicated third table — an enrollment ledger — to link the two sides together.
This lecture covers @ManyToOne, @OneToMany, why bidirectional mapping with @ManyToOne owning the foreign key is the gold standard, bidirectional synchronization helper methods, and mapping @ManyToMany with @JoinTable.
@ManyToOne and @OneToMany: The Golden Architecture
In a relational database, how is a One to Many relationship structured?
The foreign key column is always placed in the child table (the "Many" side):
Table: orders Table: order_items
+----+--------------+ +----+------------+----------+
| id | order_number | | id | item_name | order_id | <-- Foreign Key
+----+--------------+ +----+------------+----------+
| 1 | "ORD-101" | | 10 | "Shoes" | 1 |
+----+--------------+ | 11 | "Book" | 1 |
+----+------------+----------+Because the foreign key lives in order_items:
OrderItem(@ManyToOne) is the OWNING SIDE of the relationship.Order(@OneToMany) is the INVERSE SIDE of the relationship (must usemappedBy).
The Flaw of Unidirectional @OneToMany
A very common architectural mistake is attempting to create a unidirectional @OneToMany without the corresponding @ManyToOne:
java
// ANTI-PATTERN: Unidirectional @OneToMany produces an unwanted join table!
@Entity
public class Order {
@Id
private Long id;
@OneToMany(cascade = CascadeType.ALL)
private List<OrderItem> items = new ArrayList<>();
}Because OrderItem has no @ManyToOne reference, Hibernate cannot place an order_id foreign key in the order_items table. To enforce the relationship, Hibernate generates an unnecessary third table:
sql
CREATE TABLE orders_items (
order_id BIGINT,
items_id BIGINT
);Every time you add an item, Hibernate executes extra INSERT and UPDATE statements into this join table, severely degrading database performance.
The Production Pattern: Bidirectional @OneToMany with @ManyToOne
Always use bidirectional mapping with @ManyToOne owning the foreign key:
1. The Child Entity (OrderItem — The Owning Side):
java
package com.example.orderservice.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "order_items")
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String productName;
private int quantity;
private double unitPrice;
// Owning side: contains foreign key column 'order_id'
// Always use FetchType.LAZY on @ManyToOne (default is EAGER)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id", nullable = false)
private Order order;
// Getters and setters
public Long getId() { return id; }
public String getProductName() { return productName; }
public void setProductName(String productName) { this.productName = productName; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
public double getUnitPrice() { return unitPrice; }
public void setUnitPrice(double unitPrice) { this.unitPrice = unitPrice; }
public Order getOrder() { return order; }
public void setOrder(Order order) { this.order = order; }
}2. The Parent Entity (Order — The Non Owning Side):
java
package com.example.orderservice.entity;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNumber;
// Non-owning side: mappedBy points to the 'order' field in OrderItem
// Default fetch for @OneToMany is LAZY (which is correct)
@OneToMany(
mappedBy = "order",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<OrderItem> items = new ArrayList<>();
// CRITICAL: Helper methods to synchronize both sides in memory
public void addItem(OrderItem item) {
items.add(item);
item.setOrder(this);
}
public void removeItem(OrderItem item) {
items.remove(item);
item.setOrder(null);
}
// Getters and setters
public Long getId() { return id; }
public String getOrderNumber() { return orderNumber; }
public void setOrderNumber(String orderNumber) { this.orderNumber = orderNumber; }
public List<OrderItem> getItems() { return items; }
public void setItems(List<OrderItem> items) { this.items = items; }
}Why Synchronization Helper Methods Are Mandatory
In Java memory, objects do not automatically link when you modify a list:
java
Order order = new Order();
order.setOrderNumber("ORD-101");
OrderItem item = new OrderItem();
item.setProductName("Laptop");
// If you only do this:
order.getItems().add(item);Because OrderItem is the owning side, Hibernate inspects item.getOrder() to determine the foreign key value. If you never called item.setOrder(order), the foreign key remains null! When saved, Hibernate throws an error or inserts NULL into order_id.
Using order.addItem(item) guarantees that both items.add(item) and item.setOrder(this) execute simultaneously.
Many to Many Mapping (@ManyToMany)
In a Many to Many relationship, both sides contain collections:
Table: students Table: student_courses Table: courses
+----+----------+ +------------+-----------+ +----+--------------+
| id | name | | student_id | course_id | | id | course_title |
+----+----------+ +------------+-----------+ +----+--------------+
| 1 | "Bob" | | 1 | 101 | | 101| "Algorithms" |
+----+----------+ | 1 | 102 | | 102| "Database" |
+------------+-----------+ +----+--------------+1. The Owning Side (Student):
java
package com.example.orderservice.entity;
import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
@JoinTable(
name = "student_courses",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
public void addCourse(Course course) {
courses.add(course);
course.getStudents().add(this);
}
public void removeCourse(Course course) {
courses.remove(course);
course.getStudents().remove(this);
}
// Getters and setters
}2. The Non Owning Side (Course):
java
package com.example.orderservice.entity;
import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String courseTitle;
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
// Getters and setters
}Rules for @ManyToMany:
- Always use
Setinstead ofListto prevent duplicate associations and allow efficient row removal from the join table. - Never use
CascadeType.REMOVEorCascadeType.ALLon a@ManyToManyrelationship! Deleting a student should delete their join table enrollment records, but must never delete the underlying course entity.
Interview Questions & Pitfalls
Q1: Why is @ManyToOne always the owning side in a bidirectional One to Many relationship?
In relational databases, foreign keys can only exist in the child table (the Many side). The JPA entity representing the table with the physical foreign key column is by definition the owning side. Therefore, @ManyToOne owns the relationship and carries @JoinColumn, while @OneToMany is the inverse side and carries mappedBy.
Q2: What happens if you define a unidirectional @OneToMany relationship without a @ManyToOne?
Hibernate cannot place a foreign key in the child table because the child entity has no reference to the parent. To link them, Hibernate automatically generates an intermediate join table (e.g. orders_items), which requires extra INSERT and UPDATE statements on every persistence operation.
Q3: What are the default fetch types for @OneToMany and @ManyToOne?
@OneToMany defaults to FetchType.LAZY (which is efficient because collections should not be loaded unless requested). @ManyToOne defaults to FetchType.EAGER (which can cause severe N+1 query problems). Always explicitly configure @ManyToOne(fetch = FetchType.LAZY).
Q4: Why should Set be used instead of List in @ManyToMany relationships?
If a List is used in a @ManyToMany mapping, removing an item from the collection forces Hibernate to delete all rows for that student from the join table and re insert all remaining items one by one. Using Set allows Hibernate to issue a single targeted DELETE FROM student_courses WHERE student_id = ? AND course_id = ? statement.
Q5: Why is CascadeType.REMOVE dangerous in @ManyToMany mappings?
In a Many to Many relationship, entities on both sides are shared. If Student has CascadeType.REMOVE on courses, deleting a single student will automatically delete all courses that student was enrolled in from the courses table, breaking enrollment for all other students in those courses.