DEV Community

Hunor Vadasz-Perhat
Hunor Vadasz-Perhat

Posted on

hibernate-009: Unidirectional vs bidirectional @ManyToMany in Hibernate

πŸš€ 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.

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

Rather than just generating snippets, our agents understand your entire project context, can make decisions, use tools, and carry out tasks autonomously.

Read full post

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs