Hibernate Many-to-Many Relationship Mapping

Illustration for Hibernate Many-to-Many Relationship Mapping
By Last updated:

In enterprise applications, entities often share complex relationships. One such case is when multiple records of one entity relate to multiple records of another — a many-to-many relationship.

For example:

  • A Student can enroll in many Courses.
  • A Course can have many Students.

In relational databases, this is typically implemented with a join table. Hibernate makes handling these relationships intuitive using the @ManyToMany annotation, eliminating boilerplate SQL while still providing flexibility and performance tuning.

This guide explores Hibernate Many-to-Many relationship mapping with annotations, CRUD operations, queries, performance considerations, pitfalls, and best practices.


Core Concept: What is a Many-to-Many Relationship?

  • Database side: A third (join) table stores foreign keys from both entities.
  • Hibernate side: Entities are mapped with @ManyToMany and optionally @JoinTable.

Analogy: Think of students and courses as a networking group. Students can join multiple groups (courses), and each group can have many members (students). The membership table is the connector.


Setting up Hibernate Many-to-Many Mapping

Entity Example: Student ↔ Course

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.ALL })
    @JoinTable(
        name = "student_course",
        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;

    @ManyToMany(mappedBy = "courses")
    private Set<Student> students = new HashSet<>();

    // getters and setters
}

Database Schema Generated

create table students (
   id bigint not null auto_increment,
   name varchar(255),
   primary key (id)
);

create table courses (
   id bigint not null auto_increment,
   title varchar(255),
   primary key (id)
);

create table student_course (
   student_id bigint not null,
   course_id bigint not null,
   primary key (student_id, course_id)
);

CRUD Operations with Many-to-Many

Create

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

Student student1 = new Student();
student1.setName("Alice");

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

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

student1.getCourses().add(course1);
student1.getCourses().add(course2);

course1.getStudents().add(student1);
course2.getStudents().add(student1);

session.persist(student1);

tx.commit();
session.close();

Read

Session session = sessionFactory.openSession();
Student s = session.get(Student.class, 1L);
System.out.println("Student: " + s.getName());
s.getCourses().forEach(c -> System.out.println("Course: " + c.getTitle()));
session.close();

Update

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

Student s = session.get(Student.class, 1L);
Course c = new Course();
c.setTitle("Chemistry");

s.getCourses().add(c);
c.getStudents().add(s);

session.persist(c);
tx.commit();
session.close();

Delete

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

Student s = session.get(Student.class, 1L);
session.remove(s);

tx.commit();
session.close();

Hibernate Querying with Many-to-Many

Using HQL

List<Student> students = session.createQuery("from Student s join fetch s.courses", Student.class).list();

Using Criteria API

CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Student> query = cb.createQuery(Student.class);
Root<Student> root = query.from(Student.class);
root.fetch("courses", JoinType.INNER);
List<Student> students = session.createQuery(query).getResultList();

Performance Considerations

  • Lazy loading (default): Courses are fetched only when accessed.
  • Eager loading: Loads all data immediately but risks N+1 queries.
  • Batch fetching: Use @BatchSize to reduce round trips.
  • Caching: Enable second-level cache for repeated queries.

Analogy: Lazy loading is like ordering food only when you need it, while eager fetching is like ordering the entire menu upfront.


Common Pitfalls

  • Forgetting mappedBy → duplicate join tables.
  • Overusing CascadeType.ALL → unintended deletions.
  • Eager fetching large collections → performance bottlenecks.
  • Ignoring equals() and hashCode() in entities → duplicate entries in Set.

Best Practices

  • Always prefer lazy loading unless absolutely needed.
  • Define mappedBy correctly to avoid extra join tables.
  • Use DTOs when returning data to API clients.
  • Keep join tables lean, avoid extra columns unless necessary.
  • Combine Hibernate with Spring Boot for easier integration and transaction management.

📌 Hibernate Version Notes

  • Hibernate 5.x

    • Legacy APIs like SessionFactory initialization from Configuration.
    • Uses javax.persistence package.
  • Hibernate 6.x

    • Migration to Jakarta Persistence (jakarta.persistence).
    • Enhanced HQL and Criteria support.
    • Better SQL generation and fetch optimizations.

Conclusion and Key Takeaways

  • Many-to-many relationships are common in real-world systems (students ↔ courses, users ↔ roles).
  • Hibernate simplifies join table management via @ManyToMany.
  • Balance between eager and lazy fetching to avoid performance issues.
  • Always keep best practices in mind for production systems.

FAQ

1. What’s the difference between Hibernate and JPA?
Hibernate is an implementation of JPA (Java Persistence API) with additional features.

2. How does Hibernate caching improve performance?
By storing entities/queries in memory, Hibernate reduces database hits.

3. What are the drawbacks of eager fetching?
Higher memory usage, N+1 problem, and slower response times.

4. How do I solve the N+1 select problem in Hibernate?
Use join fetch, batch fetching, or entity graphs.

5. Can I use Hibernate without Spring?
Yes, you can configure Hibernate standalone with SessionFactory.

6. What’s the best strategy for inheritance mapping?
It depends: SINGLE_TABLE is fastest, JOINED is normalized, TABLE_PER_CLASS is rarely recommended.

7. How does Hibernate handle composite keys?
Using @Embeddable and @EmbeddedId annotations.

8. How is Hibernate 6 different from Hibernate 5?
Uses jakarta.persistence namespace, improved query API, better fetch strategies.

9. Is Hibernate suitable for microservices?
Yes, but lightweight ORMs (like JOOQ) may be better for simple services.

10. When should I not use Hibernate?
For extremely high-performance applications needing raw SQL control (e.g., big data, real-time systems).