Many-to-Many Relationships and Join Tables in JPA
Many-to-Many Relationships and Join Tables in JPA
In relational databases, a many-to-many relationship occurs when rows in one table can be linked to many rows in another, and vice versa. Students enroll in courses, products belong to categories, users have roles. JPA models this with @ManyToMany and a join table that sits between the two entities.
How the Join Table Works
A many-to-many relationship requires a third table - the join table - that holds pairs of foreign keys. JPA can manage this table automatically so you never write INSERT statements for it yourself.
+------------------+ +---------------------+ +------------------+
| students | | enrollments (join) | | courses |
+------------------+ +---------------------+ +------------------+
| * id BIGINT PK | | student_id FK | --> | * id BIGINT PK |
| name VARCHAR | --> | course_id FK | | title VARCHAR |
+------------------+ +---------------------+ | credits INT |
+------------------+
On the left side, a student can appear in many rows of the join table. On the right side, a course can also appear in many rows. Together, this forms the many-to-many relationship.
Defining the Entities
You annotate both sides with @ManyToMany. One side must be designated as the owning side - this is the side whose changes actually update the join table. The other side uses mappedBy to indicate it is the inverse side:
@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 = "enrollments",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
// getters and setters
}
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private int credits;
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
// getters and setters
}
The @JoinTable annotation lets you name the join table and specify which columns hold each side of the foreign key. If you omit it, Hibernate generates a default table name like student_courses. The owning side (Student) controls the join table writes. Changes on the inverse side (Course) are ignored by Hibernate.
Adding and Removing Associations
Since the join table is managed by the ORM, adding an association is as simple as adding an element to the set. Hibernate generates the INSERT for the join table automatically:
Student alice = entityManager.find(Student.class, 1L);
Course math = entityManager.find(Course.class, 10L);
// Add enrollment - Hibernate inserts into the join table
alice.getCourses().add(math);
// Remove enrollment - Hibernate deletes from the join table
alice.getCourses().remove(math);
Always operate on the owning side. If you add a student to a course on the inverse side, Hibernate will not persist that change to the join table.
Real-World Scenario
In a university system, thousands of students enroll in hundreds of courses each semester. The enrollment join table is the backbone of the system. With JPA, you load a student and navigate directly to their courses without writing a single JOIN query. The ORM fetches the join table and the related courses behind the scenes.
Key Points
- Many-to-many relationships require a join table to link the two entities.
- Use
@ManyToManyon both sides and@JoinTableon the owning side. - The owning side controls writes to the join table; the inverse side uses
mappedBy. - Adding or removing elements from the collection automatically manages the join table rows.
- Use
Setinstead ofListto avoid duplicate entries in the join table.