hibernate-009: Unidirectional vs bidirectional @ManyToMany in Hibernate

Hunor Vadasz-Perhat - Feb 10 - - Dev Community

🚀 Many-to-Many Relationship in Hibernate
In a Many-to-Many relationship:

  • An entity can have multiple related entities, and vice versa.
  • A join table is required to store the relationships.

We’ll go through both unidirectional and bidirectional mappings using Student and Course entities.


1️⃣ Unidirectional @ManyToMany

✅ In a unidirectional Many-to-Many:

  • Only one entity knows about the relationship.
  • The foreign key mapping is stored in a join table.

📌 Example: A Student can enroll in many Courses, but Course doesn’t reference Student.

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToMany
    @JoinTable(
        name = "student_course", // ✅ Join table name
        joinColumns = @JoinColumn(name = "student_id"), // ✅ FK for Student
        inverseJoinColumns = @JoinColumn(name = "course_id") // ✅ FK for Course
    )
    private List<Course> courses = new ArrayList<>();
}
Enter fullscreen mode Exit fullscreen mode
@Entity
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
}
Enter fullscreen mode Exit fullscreen mode

Only Student knows about Course, and Course has no reference back.


🔹 Generated SQL (Creates a Join Table)

CREATE TABLE student (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255)
);

CREATE TABLE course (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255)
);

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 student(id),
    FOREIGN KEY (course_id) REFERENCES course(id)
);
Enter fullscreen mode Exit fullscreen mode

✅ The student_course table links students to courses.


📌 Saving Data

Course course1 = new Course();
course1.setTitle("Mathematics");

Course course2 = new Course();
course2.setTitle("Physics");

Student student = new Student();
student.setName("John Doe");
student.getCourses().add(course1);
student.getCourses().add(course2);

entityManager.persist(course1);
entityManager.persist(course2);
entityManager.persist(student);
Enter fullscreen mode Exit fullscreen mode

🚀 The student is now enrolled in two courses!


📌 Querying Data

Student student = entityManager.find(Student.class, 1L);
List<Course> courses = student.getCourses();
courses.forEach(course -> System.out.println(course.getTitle())); // ✅ Works!
Enter fullscreen mode Exit fullscreen mode

You can query courses from a student, but not the other way around.


2️⃣ Bidirectional @ManyToMany

✅ In a bidirectional Many-to-Many:

  • Both entities reference each other.
  • One entity is the owning side (@JoinTable).
  • The other entity is the inverse side (mappedBy).

📌 Example: A Student can enroll in many Courses, and a Course can have many Students.

Owning Side (Student) - Uses @JoinTable

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToMany
    @JoinTable(
        name = "student_course", // ✅ Join table
        joinColumns = @JoinColumn(name = "student_id"), // ✅ FK for Student
        inverseJoinColumns = @JoinColumn(name = "course_id") // ✅ FK for Course
    )
    private List<Course> courses = new ArrayList<>();
}
Enter fullscreen mode Exit fullscreen mode

Inverse Side (Course) - Uses mappedBy

@Entity
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;

    @ManyToMany(mappedBy = "courses") // ✅ References the field in Student
    private List<Student> students = new ArrayList<>();
}
Enter fullscreen mode Exit fullscreen mode

mappedBy = "courses" tells Hibernate:

  • "The join table is already defined in Student.courses."
  • "Don’t create another join table in Course."

🔹 Generated SQL (No Duplicate Join Table!)

CREATE TABLE student (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255)
);

CREATE TABLE course (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255)
);

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 student(id),
    FOREIGN KEY (course_id) REFERENCES course(id)
);
Enter fullscreen mode Exit fullscreen mode

✅ The student_course table is correctly mapped without duplication.


📌 Saving Data (Same as Before)

Course course1 = new Course();
course1.setTitle("Mathematics");

Course course2 = new Course();
course2.setTitle("Physics");

Student student = new Student();
student.setName("John Doe");
student.getCourses().add(course1);
student.getCourses().add(course2);

course1.getStudents().add(student); // ✅ Add student to course
course2.getStudents().add(student);

entityManager.persist(course1);
entityManager.persist(course2);
entityManager.persist(student);
Enter fullscreen mode Exit fullscreen mode

Now, both Student and Course are correctly linked.


📌 Querying Both Directions

Get Courses from Student

Student student = entityManager.find(Student.class, 1L);
List<Course> courses = student.getCourses();
courses.forEach(course -> System.out.println(course.getTitle())); // ✅ Works!
Enter fullscreen mode Exit fullscreen mode

Get Students from Course

Course course = entityManager.find(Course.class, 1L);
List<Student> students = course.getStudents();
students.forEach(student -> System.out.println(student.getName())); // ✅ Works!
Enter fullscreen mode Exit fullscreen mode

✅ Unlike the unidirectional version, now you can access both Student → Course and Course → Student.


3️⃣ Summary: Unidirectional vs. Bidirectional @ManyToMany

Feature Unidirectional (@ManyToMany) Bidirectional (@ManyToMany + mappedBy)
@ManyToMany used? ✅ Yes ✅ Yes (Both Sides)
@JoinTable used? ✅ Yes (Owning Side) ✅ Yes (Only in Owning Side)
mappedBy used? ❌ No ✅ Yes (Inverse Side)
Extra join table? ✅ Yes ✅ Yes (But Correctly Shared)
Reference back? ❌ No ✅ Yes (Both Can Access Each Other)

Best Practice: Use bidirectional @ManyToMany if you need to query both ways.

🚀 Unidirectional @ManyToMany is simpler if you only query in one direction.


🎯 Final Takeaways

  • Unidirectional @ManyToMany = Only one entity knows about the other (@JoinTable in the owning side).
  • Bidirectional @ManyToMany = Both entities reference each other (mappedBy used on the inverse side).
  • Use bidirectional when both sides need to access each other.
  • Always place the @JoinTable annotation on the owning side.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .