Many-to-Many Mappings
Learn how to map many-to-many relationships in Hibernate using @ManyToMany and a join table, with a Student/Course example.
Introduction
A student can enroll in many courses, and a course can have many students enrolled in it — neither side maps neatly to a single foreign key. This is a many-to-many relationship, and Hibernate models it with @ManyToMany, backed by a join table (also called a junction or link table) that stores pairs of foreign keys, one from each side.
- How to model a many-to-many relationship with @ManyToMany
- How the underlying join table is structured and configured
- How to save entities on both sides of the relationship
- How to fetch and navigate a many-to-many association
Modeling the Relationship
Student holds a collection of Courses, and Course holds a collection of Students. One side must own the relationship and declare @JoinTable; the other side stays inverse with mappedBy, exactly like the previous two relationship types.
@Entity@Table(name = "students")public class Student {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@Column(name = "name", nullable = false) private String name;
@ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE }) @JoinTable( name = "student_course", joinColumns = @JoinColumn(name = "student_id"), inverseJoinColumns = @JoinColumn(name = "course_id") ) private Set<Course> courses = new HashSet<>();
public Student() {} public Student(String name) { this.name = name; }
public Long getId() { return id; } public String getName() { return name; } public Set<Course> getCourses() { return courses; }
public void enroll(Course course) { courses.add(course); course.getStudents().add(this); }}@Entity@Table(name = "courses")public class Course {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@Column(name = "title", nullable = false) private String title;
@ManyToMany(mappedBy = "courses") private Set<Student> students = new HashSet<>();
public Course() {} public Course(String title) { this.title = title; }
public Long getId() { return id; } public String getTitle() { return title; } public Set<Student> getStudents() { return students; }}The Join Table
The owning side, Student, defines the @JoinTable that Hibernate creates and maintains automatically. It has no entity class of its own — it exists purely as a pair of foreign key columns.
CREATE TABLE student_course ( student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, PRIMARY KEY (student_id, course_id), FOREIGN KEY (student_id) REFERENCES students(id), FOREIGN KEY (course_id) REFERENCES courses(id));| Attribute | Purpose |
|---|---|
| name | The name of the join table, e.g. "student_course" |
| joinColumns | Foreign key column(s) pointing back to the owning entity (Student) |
| inverseJoinColumns | Foreign key column(s) pointing to the other entity (Course) |
Saving the Relationship
With CascadeType.PERSIST on the owning side, saving a Student that has newly created Courses attached will persist those courses too, along with rows in the join table linking them.
Session session = sessionFactory.openSession();Transaction tx = session.beginTransaction();
Student student = new Student("Vikram Joshi");Course java = new Course("Java Fundamentals");Course hibernate = new Course("Hibernate Deep Dive");
student.enroll(java);student.enroll(hibernate);
session.persist(student); // cascades to both courses and the join rows
tx.commit();session.close();Click Run to see what this code prints.
Fetching Related Entities
@ManyToMany defaults to LAZY fetching on both sides. Accessing the collection triggers a query joining through the link table.
Session session = sessionFactory.openSession();
Student student = session.get(Student.class, 1L);System.out.println(student.getName() + " is enrolled in:");
for (Course c : student.getCourses()) { System.out.println(" - " + c.getTitle());}
session.close();Click Run to see what this code prints.
Common Mistakes
- Declaring @JoinTable on both sides of the relationship — only the owning side should declare it; the other side uses mappedBy.
- Using CascadeType.REMOVE on a @ManyToMany relationship, which can unexpectedly delete a Course still referenced by other students.
- Using a List instead of a Set for many-to-many collections, which can cause duplicate join rows and inefficient deletes in some Hibernate versions.
- Forgetting to update both sides (student.enroll(course)) and ending up with join rows written only from one direction.
Best Practices
- Prefer Set over List for @ManyToMany collections to avoid duplicate-row issues.
- Avoid CascadeType.REMOVE on many-to-many relationships since a shared entity should not vanish when unlinked from just one side.
- Add a helper method like enroll() to keep both collections synchronized.
- For join tables that need extra columns (like an enrollment date), model the join table as its own entity instead of using plain @ManyToMany.
Frequently Asked Questions
A plain @ManyToMany join table cannot hold extra columns. Instead, model an explicit Enrollment entity with @ManyToOne references to both Student and Course, plus your extra fields — this is the recommended pattern once the relationship needs its own data.
Sets naturally prevent duplicate entries and let Hibernate generate more efficient SQL for managing join table rows, particularly on removal, compared to List which can trigger full collection recreation.
Functionally the data is the same either way, but the owning side is the one whose collection changes actually get written to the database — updates made only through the inverse side's collection are ignored by Hibernate.
Key Takeaways
- @ManyToMany relationships are backed by a join table with two foreign key columns.
- The owning side declares @JoinTable; the inverse side declares mappedBy.
- Prefer Set collections and avoid CascadeType.REMOVE for many-to-many associations.
- Use an explicit join-table entity when the relationship itself needs extra attributes.
Summary
You have now mapped all three core relationship types — one-to-one, one-to-many, and many-to-many. Next, you will look closer at cascading: precisely what happens to related entities when you save, update, or delete the entity that owns them.