Hibernate Tip: Many-to-Many Association with additional Attributes

By Thorben Janssen

Association Mapping, Best Practice, Mapping, Primary Key

Hibernate Tips is a series of posts in which I describe a quick and easy solution for common Hibernate questions. If you have a question for a future Hibernate Tip, please post a comment below.

Question:

I got several questions, like the following one by Carme, about the mapping of a many-to-many association that contains additional attributes:

“What about a many-to-many with extra attributes?”

So, it’s about a time to answer them in a Hibernate Tip post.

Solution:

Modeling a many-to-many association in your table model requires an association table, that references the primary keys of the associated records. If you want to store any additional information for this relationship, you can do that by adding columns to the association table.

You can see an example of such a relationship in the following diagram.

You can publish a book in different formats, like ebook, paperback, and hardcover. Each format can be published by a different publisher. That’s why the format is stored in the format column of the BookPublisher table.

Mapping the association

With JPA and Hibernate, the most popular approach maps a many-to-many association to an attribute of type java.util.Set. That mapping hides the association table and makes the association very easy to use.

@Entity
public class Book {

	@Id
	@GeneratedValue
	private Long id;

	@ManyToMany
	@JoinTable(name = "book_publisher", 
			joinColumns = { @JoinColumn(name = "book_id") }, 
			inverseJoinColumns = { @JoinColumn(name = "author_id") })
	private Set<Publisher> publishers = new HashSet<Publisher>();
	
	...
}

But it also makes it impossible to map any additional columns that are part of the association table. You can only map these columns by using a domain model that’s closer to the actual table model.

Mapping the Association Table

You need to model the book_publisher table as an entity with 2 many-to-one relationships to the Book and Publisher entities.

@Entity
class BookPublisher {

    @EmbeddedId
    private BookPublisherId id = new BookPublisherId();

    @ManyToOne
    @MapsId("bookId")
    private Book book;

    @ManyToOne
    @MapsId("publisherId")
    private Publisher publisher;

    private Format format;
	
	...
}

The most interesting part of this mapping is the mapping of the composite primary key. It consists of the ids of the associated Book and Publisher entities.

You can map that with an embeddable that represents the primary key and 2 @MapsId annotations. The annotations tell Hibernate to which attribute of the embeddable it shall assign the primary key value of the associated entity.

The implementation of the embeddable is pretty simple. It needs to implement the Serializable interface and the attributes bookId and publisherId.

@Embeddable
public static class BookPublisherId implements Serializable {

	private static final long serialVersionUID = 1L;

	private Long bookId;
	private Long publisherId;

	public BookPublisherId() {

	}

	public BookPublisherId(Long bookId, Long publisherId) {
		super();
		this.bookId = bookId;
		this.publisherId = publisherId;
	}

	public Long getBookId() {
		return bookId;
	}

	public void setBookId(Long bookId) {
		this.bookId = bookId;
	}

	public Long getPublisherId() {
		return publisherId;
	}

	public void setPublisherId(Long publisherId) {
		this.publisherId = publisherId;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((bookId == null) ? 0 : bookId.hashCode());
		result = prime * result
				+ ((publisherId == null) ? 0 : publisherId.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		BookPublisherId other = (BookPublisherId) obj;
		return Objects.equals(getBookId(), other.getBookId()) && Objects.equals(getPublisherId(), other.getPublisherId());
	}
}

Mapping as a Bidirectional Association

And if you want to map them as bidirectional associations, you need to model the referencing side of the association on the Book and Publisher entity.

@Entity
public class Book {

	@Id
	@GeneratedValue
	private Long id;

	@OneToMany(mappedBy = "publisher")
	private Set<BookPublisher> bookPublishers = new HashSet<>();
	
	...
}
@Entity
public class Publisher {

	@Id
	@GeneratedValue
	private Long id;

	@OneToMany(mappedBy = "publisher")
	private Set<BookPublisher> bookPublishers = new HashSet<>();
	
	...
}

Using the Mapping

That’s all you need to do to map a many-to-many association with additional attributes. You can then use the 2 many-to-one associations in the same way as any other many-to-one association.

Book b = new Book();
b.setTitle("Hibernate Tips - More than 70 solutions to common Hibernate problems");
em.persist(b);

Publisher p = new Publisher();
p.setName("Thorben Janssen");
em.persist(p);

BookPublisher bp = new BookPublisher();
bp.setBook(b);
bp.setPublisher(p);
p.getBookPublishers().add(bp);
b.getBookPublishers().add(bp);
em.persist(bp);

Learn more:

If you want to learn more about primary key mappings, you should also read the following articles:

 

Hibernate Tips Book







Get more recipes like this one in my new book Hibernate Tips: More than 70 solutions to common Hibernate problems.

It gives you more than 70 ready-to-use recipes for topics like basic and advanced mappings, logging, Java 8 support, caching, and statically and dynamically defined queries.

Get it now!



Tags

Association Mapping, Best Practice, Mapping, Primary Key


About the author

Thorben is an independent consultant, international speaker, and trainer specialized in solving Java persistence problems with JPA and Hibernate.
He is also the author of Amazon’s bestselling book Hibernate Tips - More than 70 solutions to common Hibernate problems.

Books and Courses

Coaching and Consulting

Leave a Reply

Your email address will not be published. Required fields are marked

This site uses Akismet to reduce spam. Learn how your comment data is processed.

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}