@ManyToMany
@ManyToMany
A many-to-many relationship exists when each row in table A can relate to multiple rows in table B, and each row in B can also relate to multiple rows in A. A classic example is the relationship between Student and Course: one student enrolls in many courses, and one course has many students. In a relational database this requires a join table (sometimes called a bridge or association table) that holds pairs of foreign keys. Hibernate can manage that join table automatically — or you can take control of it yourself when the relationship has extra attributes.
The Basic Mapping
Annotate the collection field on one side with @ManyToMany and tell Hibernate about the join table with @JoinTable:
The entity that declares @JoinTable is the owning side. The other entity uses mappedBy to point back to the field on the owner. Hibernate generates a join table in DDL that looks like this:
@ManyToMany with a List using a bag strategy: when you remove a single element it issues a DELETE ALL for that owner, then re-inserts the remaining rows. A Set avoids this by generating a single targeted DELETE for only the removed element. Always prefer Set for many-to-many collections.
Managing Both Sides — Helper Methods
With bidirectional associations you are responsible for keeping both ends of the in-memory object graph consistent. A helper method on the owning side is the conventional pattern:
Fetch Type and the Default
The default fetch type for @ManyToMany is FetchType.LAZY, which is almost always what you want. Switching to EAGER causes Hibernate to always load the entire join table for every parent you touch, even when you do not need the associated collection.
When the Join Table Has Extra Columns
Suppose enrollment also stores a grade and an enrolled_at timestamp. A plain @ManyToMany cannot model this because it cannot map extra columns on the join table. The solution is to promote the join table to a full entity:
Now Student has a @OneToMany to Enrollment, and Course has the same. This pattern is sometimes called a join entity or association entity and is almost always the better long-term choice — real-world join tables almost always grow extra attributes over time.
@EmbeddedId composed of both foreign keys. This works but is significantly more verbose. Using a simple surrogate @GeneratedValue primary key (as shown above) is cleaner and performs identically. Choose the composite PK only when the natural key is meaningful outside Hibernate.
Deleting and Cascading
By default, persisting or removing a Student does not cascade to the join table rows or to the Course entities. You can enable cascade for specific operations:
CascadeType.REMOVE on a many-to-many. Cascading remove through the owning side will delete the associated entities (the Course rows themselves), not just the join table rows, destroying data shared with other entities. To remove only the relationship, call the helper method that removes the element from both collections and let Hibernate delete the orphaned join-table row.
Querying
Joining across a many-to-many in JPQL is straightforward; Hibernate translates the collection traversal into a join through the join table automatically:
You do not need to reference the join table by name in JPQL — Hibernate knows it from the mapping metadata. This is one of the productivity advantages of working at the object level rather than at the SQL level.
Summary
@ManyToMany maps a bidirectional many-to-many relationship through an automatically-managed join table. The owning side declares @JoinTable; the inverse side uses mappedBy. Use Set instead of List for collection type, keep both sides of the in-memory graph consistent through helper methods, and never cascade REMOVE through this association. When the join table needs extra attributes, replace the annotation with a dedicated join entity backed by two @ManyToOne relationships — this is almost always the right call for production systems.