|

How to persist additional attributes for an association with JPA and Hibernate


Take your skills to the next level!

The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.


JPA and Hibernate allow you to define associations between entities with just a few annotations, and you don’t have to care about the underlying table model in the database. Even join tables for many-to-many associations are hidden behind a @JoinTable annotation, and you don’t need to model the additional table as an entity.

That changes as soon as you have to persist additional attributes of an association. The obvious way to handle that is to create an entity for the join table and add the attributes there. But that immediately gets you to the next question: How do you model a primary key that consists of 2 foreign keys?

That’s easier than you might think.

Model

You know the example for this post from a typical bookstore. There are books in multiple formats (e.g., hardcover, paperback, ebook), and each format was published by a different publisher.

You can model that with 3 entity classes. The Book and Publisher entities are pretty obvious and model the two main domain objects. The third one is the BookPublisher entity which models the association between the Book and the Publisher and keeps the Format as an additional attribute.

OK, if you have some experience with database modeling, you probably expected such an entity model. It is pretty close to the table model and not too difficult.

Let’s take a closer look at the mapping definition.

The Book and Publisher entities

There is nothing too interesting about the Book and the Publisher entity. Both of them define a one-to-many association with the BookPublisher entity. The interesting parts of the mapping are in the BookPublisher entity, which I will show you in the next section.

@Entity
public class Book {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE)
	private Long id;

	@Version
	private int version;

	private String title;

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

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE)
	private Long id;

	@Version
	private int version;

	private String name;

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

The BookPublisher entity

OK, I promised you that the mapping of the BookPublisher entity is more interesting than the ones I showed you before. But that doesn’t mean that it’s more complex.

As you have seen in the diagram, the BookPublisher entity maps the association between the Book and the Publisher entities and stores the format of the book as an additional attribute. At first look, the required mapping might seem easy. You only need 2 many-to-one associations and the additional attribute.

But what about the primary key?

As you have seen in the diagram, the BookPublisher entity uses the combination of the foreign key of the Book entity and the foreign key of the Publisher entity as the primary key. Both of them are hidden by the many-to-one association mapping.

The best option to map such a composite primary key is to define an @Embeddable that represents it. In this example, I created the BookPublisherId class. It’s 2 attributes of type Long represent the 2 parts of the primary key.

@Entity
public class BookPublisher {

	@Embeddable
	public static class BookPublisherId implements Serializable {

		protected Long bookId;

		protected Long publisherId;

		public BookPublisherId() {
			
		}
		
		public BookPublisherId(Long bookId, Long publisherId) {
			this.bookId = bookId;
			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;
			
			if (bookId == null) {
				if (other.bookId != null)
					return false;
			} else if (!bookId.equals(other.bookId))
				return false;
			
			if (publisherId == null) {
				if (other.publisherId != null)
					return false;
			} else if (!publisherId.equals(other.publisherId))
				return false;
			
			return true;
		}
	}
	
	...
	
}

As you can see, the mapping of the BookPublisherId is simple.

You have to annotate the class with an @Embeddable annotation. It’s now a standard embeddable that you can use at an attribute in all your entity classes.

If you want to use an embeddable object as a primary key, there are 2 more things you need to do:

  1. Your class needs to implement the Serializable interface.
  2. You need to implement the hashCode and equals methods.

That’s all you need to do to define an embeddable that can represent a primary key. You can now use it as an attribute type and annotate it with @EmbeddedId.

Let’s take a look at the BookPublisher mapping next.

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

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

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

	@Enumerated(EnumType.STRING)
	private Format format;
	
	...
	
}

As you can see in the code snippet, the id attribute is of type BookPublisherId, and I annotated it with @EmbeddedId. That tells Hibernate to use the BookPublisherId class as the primary key class and use its mapping definition to map the attributes to the database columns.

In the following lines, you can see the mapping definition of the 2 many-to-one associations to the Book and Publisher entities. These provide the foreign keys that form the primary key of each BookPublisher entity object.

You can annotate them with a @MapsId annotations to tell Hibernate to use the primary keys of the referenced Book and Publisher entities as parts of the primary key of the BookPublisher entity. The provided Strings reference the corresponding attributes of the BookPublisherId object.

That’s all you need to do to define the mapping. Hibernate will now manage the primary key of all BookPublisher entities automatically based on the primary keys of the 2 associated entities.

How to use the mapping

You can use the BookPublisher entity in the same way as any other entity. The only thing you need to keep in mind is that you need to set the associations to the Book and the Publisher entity before persisting a new BookPublisher object.

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

Book b = new Book();
b.setTitle("My Book");
em.persist(b);

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

BookPublisher bp = new BookPublisher();
bp.setBook(b);
bp.setPublisher(p);
bp.setFormat(Format.EBOOK);
em.persist(bp);

em.getTransaction().commit();
em.close();

Summary

Hibernate’s standard mapping of a many-to-many association hides the mapping table. Due to that, you can’t use it if your association table includes additional columns.

In that case, you need to add an entity class that maps the association table and split the many-to-many association mapping into 2 many-to-one associations. That entity can then map the additional columns of the association table and usually uses a composite primary key that consists of the 2 foreign keys to the associated database tables.

The best way to map such a composite key is to define an @Embeddable with 2 attributes and annotate both associations with a @MapsId annotation.

6 Comments

  1. Avatar photo Thorben Janssen says:

    Hi,
    Unfortunately, Hibernate handles an @ElementCollection not very efficiently. I, therefore, always prefer to use an entity.
    Regards,
    Thorben

  2. Thank you very much for this post, I was finding many ways to solve, but I can’t. After saw your post, I solved that

    1. Avatar photo Thorben Janssen says:

      Awesome 🙂

  3. Did you intend to have two BookPublisher classes in seperate files?

    1. Avatar photo Thorben Janssen says:

      No, that’s just because I wanted to split the code into 2 snippets. Both code snippets should be in the same file.

  4. Avatar photo Arun Menon says:

    Thanks !!!! As always Nice and concise explanation. On a totally unrelated note, when I was implementing the code that you have given in the article I cant help noticing how tough it is to actually set up a simple maven-jpa-hibernate-h2 DB setup(Knowing the dependencies and their version). I decided to go ahead and use spring boot as it is much faster and then ran into trouble with transactions. Since I am using eclipse I believe the only work around will be to create a template POM and keep reusing that.

Comments are closed.