Home/Learn/Hibernate & JPA/Many-to-Many Mapping

Many-to-Many Mapping

Intermediate
Relationships

@ManyToMany uses a join table; prefer extracting the join table as an explicit entity (with extra columns like createdAt) to avoid limitations of the implicit join-table approach.

Overview

A many-to-many relationship (e.g., Student–Course, Post–Tag) requires a join table in the relational model. JPA's `@ManyToMany` annotation manages this table implicitly, but the implicit approach has serious limitations: you cannot add extra columns to the join table, and Hibernate's default collection update behaviour deletes and re-inserts all rows on every change. The production-recommended pattern is to **extract the join table as an explicit `@Entity`** (e.g., `Enrollment`) with two `@ManyToOne` relationships — one to each side. This gives you full control over extra columns, indexing, and Hibernate's flush behaviour.

Implicit @ManyToMany (simple, limited)

The implicit approach is quick to write but has pitfalls: no extra columns on the join table, Hibernate issues a DELETE-all + INSERT-all on every collection change, and you lose control over the join-table's primary key. Use it only for truly append-only, read-heavy relationships with small collections.

JPA — implicit @ManyToMany
@Entity
public class Student {
    @Id @GeneratedValue Long id;
    String name;

    @ManyToMany
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    List<Course> courses = new ArrayList<>();
}

@Entity
public class Course {
    @Id @GeneratedValue Long id;
    String title;

    // Bidirectional — mappedBy refers to the owning side field
    @ManyToMany(mappedBy = "courses")
    List<Student> students = new ArrayList<>();
}

Explicit Join Entity (recommended)

Extracting the join table as an entity lets you add metadata (enrolledAt, grade), use a composite or surrogate PK, and avoids Hibernate's delete-all behaviour. Replace the `@ManyToMany` on both sides with `@OneToMany(mappedBy=...)` pointing at the new join entity.

JPA — explicit join entity with @EmbeddedId
@Entity
@Table(name = "enrollment")
public class Enrollment {
    @EmbeddedId
    EnrollmentId id;            // composite PK

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("studentId")
    Student student;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("courseId")
    Course course;

    LocalDateTime enrolledAt = LocalDateTime.now();
    String grade;               // extra column — impossible with @ManyToMany
}

@Embeddable
public class EnrollmentId implements Serializable {
    Long studentId;
    Long courseId;
}

// Student — change @ManyToMany to @OneToMany
@Entity
public class Student {
    @Id @GeneratedValue Long id;

    @OneToMany(mappedBy = "student",
               cascade = CascadeType.ALL, orphanRemoval = true)
    List<Enrollment> enrollments = new ArrayList<>();
}

Performance: Set vs List and Collection Mutations

When using the implicit `@ManyToMany`, prefer `Set` over `List`. Hibernate's bag semantics for `List` issue a DELETE-all + INSERT-all even when adding one element. A `Set` only inserts the new row. With the explicit join entity, add/remove operations directly manipulate the `Enrollment` table rows — no mass deletes.

JPA — Set vs List collection semantics
// BAD: List triggers DELETE all + INSERT all on add/remove
@ManyToMany
List<Tag> tags = new ArrayList<>();

// BETTER: Set only inserts/deletes the changed row
@ManyToMany
Set<Tag> tags = new HashSet<>();

// BEST: explicit entity — precise SQL, extra columns allowed
// Remove an enrollment
student.getEnrollments()
       .removeIf(e -> e.getCourse().equals(course));
// Hibernate fires: DELETE FROM enrollment WHERE student_id=? AND course_id=?

Key Points to Remember

  • 1@ManyToMany implicitly manages a join table but cannot hold extra columns
  • 2Hibernate deletes and re-inserts all join-table rows on List mutations — use Set instead
  • 3Extract the join table as an explicit @Entity to add metadata columns and control SQL
  • 4@EmbeddedId with @MapsId maps the composite PK to the two foreign-key columns
  • 5With an explicit join entity, Student and Course become @OneToMany(mappedBy=...)
  • 6The owning side of a bidirectional @ManyToMany is the side with @JoinTable

Interview Questions

Sign in to ask Aria
1

What is the main limitation of using @ManyToMany with a List in Hibernate?

MediumThoughtWorks
2

How would you add an "enrolledAt" timestamp to a Student-Course join table in JPA?

MediumAtlassian
3

Why does Hibernate fire DELETE-all + INSERT-all when you add a single item to a @ManyToMany List?

HardOracle
4

What is @EmbeddedId and how does @MapsId work with it?

HardGoldman Sachs
5

What determines the owning side of a @ManyToMany relationship?

EasyInfosys

Ask Aria about Many-to-Many Mapping

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…