JPA and Hibernate allow you to define relationships 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 relationships 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 for a relationship. The obvious way to handle them is to create an entity for the join table and add the attribute there. But that sounds easier as it is. As you will see, there are several things you need to take care of to model it correctly.
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 modeled with 3 entities, which you can see in the following diagram. The Book and Publisher entity are pretty obvious and model the two main domain objects. The third one is the BookPublisher entity which models the relationship between the Book and the Publisher and keeps the Format as an additional attribute.
OK, if you have some experience with database modelling, you probably expected such an entity model. It is pretty close to the database model and not too difficult. The tricky part is to define the BookPublisher entity in a way that allows you easy read and write access and assures referential integrity at all time. But before we get into that, let’s have a quick look at the Book and Publisher entities.
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 relationship to the BookPublisher entity. The interesting parts of the mapping are in the BookPublisher entity which I will show you in the next section.
The BookPublisher entity
OK, I promised you that the mapping of the BookPublisher entity is more interesting than the ones I showed you before. And I intend to keep that promise.
As you have seen in the diagram, the BookPublisher entity maps the relationship between the Book and the Publisher entities and stores the format of the book as an additional attribute. At the first look, the required mapping might seem easy. You only need 2 many-to-one relationships and the additional attribute.
So how can you use
the same database column
for two mappings?
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 also mapped by the many-to-one relationships. So how can you use the same database column for two mappings? And what do you need to do to keep them in sync?
Let’s have a look at the primary key first. As you can see in the following code snippet, I define the inner class BookPublisherId and annotate it with @Embeddable so that I can use it later on as an embedded id.
The mapping of the BookPublisherId is quite simple. You just need to annotate the two attributes with a @Column annotation. But there are a few other things you need to take care of if you want to use an embeddable object as a primary key. Your class needs to implement the Serializable interface, and you need to implement the hashCode and equals methods. OK, that’s all for the primary key class. Let’s have a look at the BookPublisher mapping.
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 class and use its mapping definition to map the attributes to the database columns.
The important part
for these mappings
are the propertie
sinsertable and updatable
which Hibernate requires you
to set to false …
In the following lines, you can see the mapping definition of the 2 many-to-one relationships to the Book and Publisher entities. The important part for these mappings are the properties insertable and updatable which Hibernate requires you to set to false because the database columns fk_book and fk_publisher are already used in the mapping of the BookPublisherId class.
If the relationship mappings can’t be changed, you obviously need to initialize them in the BookPublisher constructor. As you can see in the code snippet, the constructor expects a Book and a Publisher entity and a Format enum value. These are used to create a new instance of the BookPublisherId class as the primary key, to initialize the relationship to the Book and Publisher entity and to set the format attribute. And you also need to add the new BookPublisher entity to the relationships of the Book and Publisher entity.
That’s all you need to do to define the mapping, and you can now use it in your application.
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 persist the Book and the Publisher entity so that their primary key gets initialized before you can instantiate a BookPublisher entity for them.
Summary and cheat sheet
As you have seen, you need to define an entity for the relationship table to map to-many relationships with additional attributes. You can use the foreign key columns of the related entities as the primary key of the relationship entity. And you can also use them to map the relationships and provide a comfortable way to navigate the relationship. But Hibernate then requires you to set the insertable and updatable properties of the relationship mappings to false to make them immutable.