How to effectively map and use Composite Primary keys in JPA?

Java Persistence API (JPA, for short) is a Java EE Specification for Object Relational Mapping (ORM, for short). It can be used in both Java SE and Java EE environments.

JPA, on its own, is a specification. It cannot run on its own. The API comes with interfaces and requires an implementation. There are various JPA providers who have implemented the JPA specification, such as Hibernate, EclipseLink, Apache OpenJPA, to name but a few. All Java EE complaint containers have a JPA provider already bundled so you, as a developer, must not worry on which JPA provider to use. The container will provide you with one. For the latest specification (currently at version 2.2 at this time of writing), you can visit JCP.org.

Now that we have an introduction out the way, and before I talk about my subject at hand, here is the number ONE fundamental concept of JPA that you must remember: Treat JPA as object modelling and object reference. I will explain later on why I say this.

Pre-requirements: To understand this post, you must have an intermediate knowledge of Java (Java annotations will be used here) and basic usage of JPA.

Before we begin, a scenario.

You are tasked to create an online book store, a simplified version. This book store must list all the authors who took part in writing the book, the book in their various languages as well as the price of the book. Basically, you will have something like this:

Book store ER diagram

As you can see, we have 2 tables that references 2 other tables. BookTitle table has a 1 key that reference Book (using the BOOK_ID column) and another that reference Language (using the LANGUAGE_ID column). The same concept applies for BookAuthor table.

What are Composite Primary Keys?

A composite key (also known as a Composite Primary Key) in a RDBMS is a combination of 2 or more table columns used to specify a primary key of the DB table (to uniquely identify each row in the table). Uniqueness is guaranteed when the columns are combined.

Now, in our example mentioned above, instead of creating an auto increment column as our table row primary key, we can combine the 2 foreign keys together to create a unique primary and guarantee uniqueness. The combination of columns of our foreign keys constitutes as a composite primary key in our RDBMS.

Composite Primary Keys in JPA

There are 2 approach to map your Composite Primary Keys in JPA:

1) Using @IdClass annotation.

One such approach is to use the @IdClass annotation. Each primary key field must be annotated with a @Id annotation. Since your entity now has more than one @Id annotation, JPA requires that you specify an ID class and assign it using @IdClass annotation.
For example, using the BookTitle, One will have to create a “composite” object that will be declared in your @IdClass.

For BookTitle class:
For BookTitleId class:
Things to remember when using @IdClass annotation:
  • The property/fields of your composite class must be identical to the property/fields of your entity.
  • There are no need for getter and setter methods inside your composite ID class as they are used only for object retrieval.
  • The composite ID class doesn’t, necessarily, be declared Serializable.
  • The equals() and hashCode() method were overridden for unique identification of the object, such as in collections using hashing.

Object retrieval is as simple as:

If your entity primary key are not auto sequence field (accompanied with the @GeneratedValue annotation) you will have to programmatically assign an unique value on the property.

2) Using the @Embeddable and @EmbeddedId annotation.

Using the example above:
For BookTitleId class:
Things to consider:
  • The composite ID class must be annotated with Embeddable annotation.
  • The composite ID class doesn’t, necessarily, be declared Serializable but it’s encouraged.
  • The composite ID class is mapped in your entity by using the EmbeddedId annotation.
  • It’s good practice to override the equals() and hashCode() method for unique object identification (and uniqueness in hashing by the JVM) on the composite ID class.
Right now (with the example presented above), object retrieval works exactly the same. In creating an entity, however, you will have to assign a value to id on BookTitle entity before persisting.

We, however have a scenario. Suppose that Book and Language entity had their ID auto generated on the RDBMS (their id annotated with @GeneratedValue respectively) and the BookTitle composite primary key references the ID of the respective parent table. We want to persist a new Book with an existing language (Book id = unknown, Language id = "en"). Then what value do we assign bookId on BookTitleId composite key class? There is no object reference that links Book to bookId on the Composite ID Class.

How do we resolve such problem?

One approach will be to individually persist each entity in an order of parent/child relationship, like in this example:
As you can see, we persist each entity one step at a time. A BookTitle cannot exist without a Book. So, the simplest idea will be to associate the relationships between/among them.

The Entity Relationship

If you noticed so far, there is an entity relationship between a BookTitle and a Book and a BookTitle with a Language. That way, we just associate a BookTitle to a Book and the JPA provider will “manage” your entity in the persistence context prior to committing the data to the RDBMS.

So, modifying the code above we get:

For Book class: For BookTitle class:
The BookTitleId composite ID class remains the same. There is a bi-directional relationship between Book and BookTitle (a book can have various titles per language, OneToMany but you can only have one book title per book, ManyToOne). Hence I have included addTitle method on Book entity. You will notice a particular annotation that I included on the BookTitle entity and let’s tackle it.

Enter @MapsId annotation.

The MapsId Javadoc states:
Designates a ManyToOne or OneToOne relationship attribute that provides the mapping for an EmbeddedId primary key, an attribute within an EmbeddedId primary key, or a simple primary key of the parent entity. The value element specifies the attribute within a composite key to which the relationship attribute corresponds. If the entity’s primary key is of the same Java type as the primary key of the entity referenced by the relationship, the value attribute is not specified.
Remember when I said that Treat JPA as object modelling and object reference.? Well, now we have done it. The MapsId annotation states that the ID of the entity must be mapped to the attribute/field of the composite ID class. So you don’t map it to the SQL table column but to the property/attribute/field of the Composite ID class. So the @MapsId("bookId") will map to BookTitleId.bookId class attribute.

Notice this additional requirements to attributes that have the MapsId annotation:
The insertable,updatableand nullable are all false.
  • You need nullable to be false. This means that an entity must be provided.
  • You need insertable to be false. This means that the JPA provider will not include the column on a SQL INSERT statement but on retrieval, an entity will be associated based on the column specified (in this case, Book entity is associated on the BOOK_ID DB table column).
  • You need updatable to be false. This means that the JPA provider will not include the column on a SQL UPDATE statement but on retrieval, an entity will be associated based on the column specified (in this case, Book entity is associated on the BOOK_ID DB table column).
MapsId can be used in attributes that has an entity association specified (using ManyToOne or OneToOne annotation).

In essence, with all the relationships declared and composite ID mapped, we can essentially allow our JPA provider to manage all our entity (new or already managed) prior to committing the SQL statements back to the RDBMS.

Finally, we can resolve our example by creating a new book and book title at the same time like so:
And voila!. ORM done right. The code is small and very simple to follow and read.

Happy coding. Please feel free to comment below about anything regarding this post. I am looking forward to your input/insight. :-)

Comments

  1. i think showing code should be more explicit

    ReplyDelete
    Replies
    1. That's weird, there is code but it doesn't seem to be displaying. Let me fix it.

      Delete

Post a Comment