There are several ways to map Many To Many relationship in JPA and Hibernate by using @ManyToMany, @OneToMany, and @ManyToOne, including

  • Joined entity unidirectional and bidirectional mapping with a single primary key, @OneToMany, and @ManyToOne

  • Joined entity unidirectional and bidirectional mapping with composite primary keys, @OneToMany, and @ManyToOne

  • Without joined entity unidirectional and bidirectional mapping with @ManyToMany

This guide will show you how to map along with the pros and cons of each approach

Consider the relationship between the publisher and books. One publisher may publish many books, one book may be published by multiple publishers

1) Joined entity unidirectional mapping with single primary key and @ManyToOne

The joined entity would be defined with a single @Id and 2 @ManyToOne. There would be no child collection association mappings on the parent entity

@Entity
public class BookPublisher {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "book_id")
    private Book book;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "publisher_id")
    private Publisher publisher;

    @Column(name = "published_date")
    private Date publishedDate;

    ...
}

@Entity
public class Book {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    ...
}

@Entity
public class Publisher {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    ...
}

FetchType.LAZY is used for @ManyToOne instead of the default FetchType.EAGER to prevent a potential performance issue

@JoinColumn specifies the foreign key column. It is optional, default name to the underscore string join of the association field name and its primary key column name

Pros and cons

  • Single primary key mapping is less complex than composite primary keys

  • Avoid the potential performance issue of @OneToMany

  • The parent entity can not quickly navigate or cascade CRUD operations to the child collection. However, you can do that manually via JPQL query

Hans-on tutorials

2) Joined entity bidirectional mapping with a single primary key, @ManyToOne, and @OneToMany

In addition to defining the joined entity like above unidirectional mapping, we would define child collection association with @OneToMany on the parent entities

@Entity
public class BookPublisher {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "book_id")
    private Book book;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "publisher_id")
    private Publisher publisher;

    @Column(name = "published_date")
    private Date publishedDate;

    ...
}

@Entity
public class Book {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    @OneToMany(mappedBy = "book", cascade = CascadeType.ALL)
    private Set<BookPublisher> bookPublishers = new HashSet<>();

    ...
}

@Entity
public class Publisher {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    @OneToMany(mappedBy = "publisher", cascade = CascadeType.ALL)
    private Set<BookPublisher> bookPublishers = new HashSet<>();

    ...
}

@JoinColumn specifies the foreign key column. It is optional for this mapping, default name to the underscore string join of the association field name and its primary key column name

On the opposite, the mappedBy attribute on @OneToMany is required to specify for bidirectional mapping. If mappedBy is absent, JPA and Hibernate will auto-create redundant joined tables such as the below book_book_publishers

+--------------------+---------+------+-----+---------+-------+
| Field              | Type    | Null | Key | Default | Extra |
+--------------------+---------+------+-----+---------+-------+
| book_id            | int(11) | NO   | PRI | NULL    |       |
| book_publishers_id | int(11) | NO   | PRI | NULL    |       |
+--------------------+---------+------+-----+---------+-------+

Pros and cons

  • Single primary key mapping is less complex than composite primary keys

  • Both sides of the relationship can quickly access and cascade CRUD operations to each other

  • @OneToMany can cause a performance issue on a large child collection

Hands-on tutorials

3) Joined entity unidirectional and bidirectional mapping with composite primary keys, @ManyToOne, and @OneToMany

The composite primary keys would be defined by a custom class annotated with @Embeddable and implemented Serializable. Its equals and hashCode methods should be also defined

@Embeddable
public class BookPublisherId implements Serializable {  
    @Column(name = "book_id")
    private Integer bookId;

    @Column(name = "publisher_id")
    private Integer publisherId;

    ...
}

The joined entity would be defined with 1 @Embeddable object field marked with @EmbeddedId and 2 @ManyToOne marked with @MapsId which pointing to the key fields defined in the @Embeddable class

@Entity
public class BookPublisher {  
    @EmbeddedId
    private BookPublisherId id;

    @ManyToOne
    @MapsId("bookId")
    @JoinColumn(name = "book_id")
    private Book book;

    @ManyToOne
    @MapsId("publisherId")
    @JoinColumn(name = "publisher_id")
    private Publisher publisher;

    @Column(name = "published_date")
    private Date publishedDate;

    public BookPublisher(Book book, Publisher publisher, Date publishedDate) {
        this.id = new BookPublisherId(book.getId(), publisher.getId());
        this.book = book;
        this.publisher = publisher;
        this.publishedDate = publishedDate;
    }

    ...
}

@Entity
public class Book {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    ...
}

@Entity
public class Publisher {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    ...
}

To persist the joined entity in this mapping, the @EmbededId value need to be filled manually as Hibernate would not be able to set the value via reflection, otherwise, you would get the following error in the console

Caused by: org.hibernate.PropertyAccessException: Could not set field value by reflection

In the above entity mapping for BookPublisher, we did the configuration via its constructor

@Entity
public class BookPublisher {  
    ...

    public BookPublisher(Book book, Publisher publisher, Date publishedDate) {
        this.id = new BookPublisherId(book.getId(), publisher.getId());
        ...
    }

    ...
}

That would be enough for unidirectional mapping. In the case of bidirectional mapping, place @OneToMany on the parent entity if you need quickly navigate and cascade CRUD operations to child collection associations like the above approach 2

Pros and cons

  • More complex than a single primary key mapping

  • Bidirectional mapping with @OneToMany can cause a performance issue for large child collection associations

Hands-on tutorials

4) Without joined entity unidirectional and bidirectional mapping with @ManyToMany

This mapping would not use any joined entity. @ManyToMany would be placed on one side for unidirectional or on both side for bidirectional mapping

@Entity
public class Book {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "book_publisher",
        joinColumns = @JoinColumn(name = "book_id", referencedColumnName = "id"),
        inverseJoinColumns = @JoinColumn(name = "publisher_id", referencedColumnName = "id"))
    private Set<Publisher> publishers = new HashSet<>();

    ...
}

@Entity
public class Publisher {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @ManyToMany(mappedBy = "publishers")
    private Set<Book> books = new HashSet<>();

    ...
}

Pros and cons

  • With bidirectional mapping, both sides of the relationship can quickly access and cascade CRUD operations to each other

  • The simplest many to many mapping. However, it only works for joined tables without extra columns, and like @OneToMany, @ManyToMany can also cause a performance issue on a large child collection

Hands-on tutorials