Compound Primary Keys with Hibernate and JPA Annotations
A common requirement of many data driven applications is to define tables that use composite primary keys, which essentially means that instead of having one column in a database table to uniquely identify a record, the table uses two columns that together, represent a unique combination.
Compound primary keys are very common, and to be honest, I feel just a tad guilty about getting this far in my tutorials about Hibernate without addressing the concept, but I assure you, there is a very good reason for the delay. You see, when a database table uses a composite primary key, the Hibernate layer is forced to create a separate class that maps directly to that primary key, and then that class must become an embedded property of the JPA annotated class that maps to the database table of interest. Since the @Embeddable annotation wasn't covered until the previous tutorial, any earlier discussion of compound primary keys would have been a bit premature.
In this chapter, we will explore the creation of compound primary key classes, and discover how those classes can be embedded within a JPA annotated class.
Compound Keys and the Interest Table
I have a sweet little database table named interest, that has one numeric property called rate, and two non-unique, numeric properties called userId and bankId, which together, uniquely represent a record in the interest table.
So, how do we map compound primary keys in Hibernate using JPA annotations? Well, when we run into compound primary keys, the Java developer is forced to create a separate, unique class that will represent the primary key combination. For the interest table, we will create a separate class called CompoundKey to manage the compound primary key.
package com.examscam.mappings;
import javax.persistence.Embeddable;
/* First Iteration of the CompoundKey Class */
@Embeddable
public class CompoundKey implements
java.io.Serializable{
private Long userId;
private Long bankId;
public CompoundKey() {}
public CompoundKey(Long user, Long bank) {
userId = user;
bankId = bank;
}
public Long getBankId() {return bankId;}
public void setBankId(Long bankId) {
this.bankId = bankId;
}
public Long getUserId() {return userId;}
public void setUserId(Long userId) {
this.userId = userId;
}
}
Basic Requirements of Compound Key
Classes
There are a couple of quick notes that you should make about the CompoundKey class. First of all, the class is not decorated with the @Entity annotation, but instead, the class uses the @Embeddable annotation, emphasizing that this primary key class will actually be used by, or embedded into, another JPA annotated class that is responsible for defining the database table mappings.
The other important addition to the CompoundKey class is the implementation of the java.io.Serializable interface. All compound keys must implement this interface, otherwise, when Hibernate maps the compound key to the database, you'll get the following exception:
org.hibernate.MappingException:
composite-id class must implement Serializable Primary Key Comparisons
Our CompoundKey class has a two argument constructor that allows you to create instances in the following manner:
CompoundKey key01 =
new CompoundKey(new Long(1), new Long(2));
CompoundKey key02 =
new CompoundKey(new Long(1), new Long(2));
Now, from looking at that code, are key01 and key02 equal to each other? Think about it from a Hibernate perspective; if we have two instances of the primary key class, perhaps generated by two separate Hibernate Sessions calling load or get, and both of them have the exact same values for userId and bankId, are those two instances logically and comparably the same? Well, it's not really up for debate ? two primary keys with two identical key values MUST BE EQUAL, but, as it stands right now, the
key01.equals(key02); method call will return false, and that is disastrous to our Hibernate applications.
Comparing Instances with .equals(Object
o)
In Hibernate, all compound primary key classes must override the .equals() and the .hashCode() methods inherited from the Object class. Actually,
all Hibernate Entity classes should override .equals() and .hashCode()
with their own, custom .equals() and .hashCode() methods so that the
same persistent object loaded from separate Hibernate Sessions will
equate to true. You see, when objects are created, they are given a unique memory location, and the default implementation of the .equals() method is to compare the memory locations of two instances. As a result, the following code generates an output of false.
CompoundKey key01 =
new CompoundKey(new Long(1), new Long(2));
CompoundKey key02 =
new CompoundKey(new Long(1), new Long(2));
boolean flag = key01.equals(key02):
System.out.println(flag); // prints out false!!! However, we can override the default implementation of the .equals() method, and generate a boolean value based on the comparison of the various instance variables within the class. With this in mind, we can override the .equals() method of the CompoundKey class by returning a true value if the two instances being compared have the same value for the userId and bankId properties:
public boolean equals(Object key) {
boolean result = true;
if (!(key instanceof CompoundKey)) {return false;}
Long otherUserId = ((CompoundKey)key).getUserId();
Long otherBankId = ((CompoundKey)key).getBankId();
if (bankId == null || otherBankId == null) {
result = false;
}else {
result = bankId.equals(otherBankId);
}
if (userId == null || otherUserId == null) {
result = false;
}else {
result = userId.equals(otherUserId);
}
return result;
}
Overriding .equals() and hashCode()
package com.examscam.mappings;import javax.persistence.Embeddable;
/* Final Iteration of the CompoundKey Class */
@Embeddable
public class CompoundKey implements java.io.Serializable{
private Long userId; private Long bankId;
public CompoundKey() { }
public CompoundKey(Long user, Long bank) {
userId = user; bankId = bank;
}
public Long getBankId() {return bankId;}
public void setBankId(Long bankId) {
this.bankId = bankId;
}
public Long getUserId() {return userId;}
public void setUserId(Long userId) {
this.userId = userId;
}
public boolean equals(Object key) {
boolean result = true;
if (!(key instanceof CompoundKey)) {return false;}
Long otherUserId = ((CompoundKey)key).getUserId();
Long otherBankId = ((CompoundKey)key).getBankId();
if (bankId == null || otherBankId == null) {
result = false;
}else {
result = bankId.equals(otherBankId);
}
if (userId == null || otherUserId == null) {
result = false;
}else {
result = userId.equals(otherUserId);
}
return result;
}
public int hashCode() {
int code = 0;
if (userId!=null) {code +=userId;}
if (bankId!=null) {code +=bankId;}
return code;
}
}
Overriding the hashCode() Method
Any time you override the .equals() method, you must override the .hashCode() method in a way that ensures that two objects whose .equals() comparison generates a true result will also generate a common .hashCode() value.
I'm going to simply leverage the values of the userId and bankId properties as I override the hashCode() method. This
is obviously an oversimplified implementation of hashCode(), as it will produce duplicates for instances that have keys that add up to the same value, but for this very simplified example, it will suffice.
public int hashCode() {
int code = 0;
if (userId!=null) {code +=userId;}
if (bankId!=null) {code +=bankId;}
return code;
}
Once the .hashCode() and .equals() methods are properly implemented and overridden, the following code, which uses .equals() to compare two CompoundKey instances that share common attribute values, but are stored in different memory locations, will generate a boolean comparison result of true.
CompoundKey key01 =
new CompoundKey(new Long(1), new Long(2));
CompoundKey key02 =
new CompoundKey(new Long(1), new Long(2));
boolean flag = key01.equals(key02):
System.out.println(flag); // now returns true!!!
Using the CompoundKey Class
So, once you have a primary key class that implements Serializable, is decorated with the @Embeddable tag, and has overridden the .equals() and the .hashCode() methods, you are ready to embed your compound key class within a JPA annotated entity class. For this example, we will create a class called Interest, with two properties, one of type double to represent an interest rate, and a property of type CompoundKey that we will name id. With the @Id annotation over the getter for the id field, our Interest class would look like this:
package com.examscam.mappings;
import javax.persistence.*; import org.hibernate.Session;
import com.examscam.HibernateUtil;
@Entity
public class Interest {
private CompoundKey id;
private double rate;
@Id
public CompoundKey getId() {return id;}
public void setId(CompoundKey id) {this.id=id;}
public double getRate() {return rate;}
public void setRate(double rate) {this.rate=rate;}
public static void main(String args[]) {
Interest rate = new Interest();
rate.setRate(18.5);
Long wayne=new Long(99); Long mario=new Long(88);
CompoundKey key = new CompoundKey(wayne, mario);
rate.setId(key);
HibernateUtil.recreateDatabase();
Session session = HibernateUtil.beginTransaction();
session.save(rate);
HibernateUtil.commitTransaction();
}
}Running the Interest Class
Running the main method of the Interest class successfully adds a new record to the interest table, with the appropriate fields being initialized according to the code.
public static void main(String args[]) {
Interest rate = new Interest();
rate.setRate(18.5);
Long wayne=new Long(99); Long mario=new Long(88);
CompoundKey key = new CompoundKey(wayne, mario);
rate.setId(key);
HibernateUtil.recreateDatabase();
Session session = HibernateUtil.beginTransaction();
session.save(rate);
HibernateUtil.commitTransaction();
}
Of course, to have Hibernate recognize the Interest class, it must be added to the AnnotationConfiguration object in the HibernateUtil class. Notice that the CompoundKey class does not need to be added. As the CompoundKey class is marked as being @Embeddable, only the embedding class, Interest, needs to be explicitly added to the AnnotationConfiguration object.
public static Configuration getInitializedConfiguration() {
AnnotationConfiguration config =
new AnnotationConfiguration();
/* add all of your JPA annotated classes here!!! */
config.addAnnotatedClass(Interest.class);
config.configure();
return config;
}
More Compound Key Mappings
Personally, I like the idea of implementing compound primary keys by simply creating a compound key class, marking it as being @Embeddable, and then creating an instance variable in the embedding class that simply gets marked with an @Id tag. To me, it's fairly simple, fairly straight forward, intuitive, and easy. However, there are alternatives, one of which is to use the @IdClass annotation in your persistent entity class, and then simply provide setters and getters for the properties defined in the primary key class.
Using the @IdClass JPA Annotation
With the @IdClass annotation, you simply point to the class implementing your compound primary key. The @IdClass is placed right after the @Entity tag, just before the class declaration. We'll create a new class called Fracture to demonstrate:
@Entity
@IdClass(com.examscam.mappings.CompoundKey.class)
public class Fracture { }With the @IdClass annotation, you don't declare an instance variable of type CompoundKey, but instead, just define instance variables for each of the primary key fields. You then mark the corresponding getter tags with standard @Id annotations.
@Entity
@IdClass(com.examscam.mappings.CompoundKey.class)
public class Fracture {
Long bankId;
Long userId;
String bone;
@Id
public Long getBankId() {return bankId;}
@Id
public Long getUserId() {return userId;}
public void setBankId(Long bankId){this.bankId = bankId;}
public void setUserId(Long userId){this.userId = userId;}
public String getBone() {return bone;}
public void setBone(String bone) {this.bone = bone;}
}
The Compound Fracture: Using @IdClass
package com.examscam.mappings;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import org.hibernate.Session;
import com.examscam.HibernateUtil;
@Entity
@IdClass(com.examscam.mappings.CompoundKey.class)
public class Fracture {
Long bankId; Long userId; String bone;
@Id
public Long getBankId() {return bankId;}
@Id
public Long getUserId() {return userId;}
public void setBankId(Long bankId){this.bankId = bankId;}
public void setUserId(Long userId){this.userId = userId;}
public String getBone() {return bone;}
public void setBone(String bone) {this.bone = bone;}
public static void main(String args[]) {
Fracture bone = new Fracture();
bone.setBone("arm");
bone.setBankId( new Long(99));
bone.setUserId(new Long(88));
HibernateUtil.recreateDatabase();
Session session=HibernateUtil.beginTransaction();
session.save(bone);
HibernateUtil.commitTransaction();
}
}
Running the main Method
Running the main method of the Fracture class successfully gets the Hibernate framework to write a record to the Fracture table, with the appropriate key fields populated.
public static void main(String args[]) {
Fracture bone = new Fracture();
bone.setBone("arm");
bone.setBankId( new Long(99));
bone.setUserId(new Long(88));
HibernateUtil.recreateDatabase();
Session session=HibernateUtil.beginTransaction();
session.save(bone);
HibernateUtil.commitTransaction();
}
Making Sure HibernateUtil is Updated
As we run these main methods that test the Fracture and the Interest classes, we must remember that all classes marked with the @Entity tag must be added to the AnnotationConfiguration object. The getInitializedConfiguration() method of the HibernateUtil class is where we typically perform this initialization.
public static Configuration getInitializedConfiguration() {
AnnotationConfiguration config =
new AnnotationConfiguration();
/* add all of your JPA annotated classes here!!! */
//config.addAnnotatedClass(ClientDetail.class);
//config.addAnnotatedClass(Address.class);
//config.addAnnotatedClass(Skill.class);
config.addAnnotatedClass(Interest.class);
config.addAnnotatedClass(Fracture.class);
config.configure();
return config;
}
The @EmbeddedId JPA Annotation
The third way I know of managing a compound primary key, and perhaps the easiest, is through the @EmbeddedId annotation. With the compound primary key class properly implemented, all you have to do in your persistent entity class is declare an instance variable in the type of your compound primary key class, and then mark the getter for the id field with the @EmbeddedId tag. Here's how it looks with the Prison class:
The Prison Compound: Using @EmbeddedId
package com.examscam.mappings;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import org.hibernate.Session;
import com.examscam.HibernateUtil;
@Entity
public class Prison {
private String city;
private CompoundKey id;
public String getCity() {return city;}
public void setCity(String city) {this.city=city;}
@EmbeddedId
public CompoundKey getId() {return id;}
public void setId(CompoundKey id) {this.id = id;}
public static void main(String args[]) {
Prison jail = new Prison();
jail.setCity("Milhaven");
Long wayne = new Long(99);
Long mario = new Long(88);
CompoundKey key = new CompoundKey(wayne, mario);
jail.setId(key);
HibernateUtil.recreateDatabase();
Session session = HibernateUtil.beginTransaction();
session.save(jail);
HibernateUtil.commitTransaction();
}
}
Running the Prison Class
After adding the Prison.class to the AnnotationConfiguration in the HibernateUtil class, the main method can be executed, and as we would expect, the prison table in the database, which contains a compound primary key, is updated successfully.
public static void main(String args[]) {
Prison jail = new Prison();
jail.setCity("Milhaven");
Long wayne = new Long(99);
Long mario = new Long(88);
CompoundKey key = new CompoundKey(wayne, mario);
jail.setId(key);
HibernateUtil.recreateDatabase();
Session session = HibernateUtil.beginTransaction();
session.save(jail);
HibernateUtil.commitTransaction();
}
|