Mapping composite primary keys in JPA – How to work around a bug in Hibernate Annotations

2

A table without single-column primary key, Java developers don’t like them because it’s more work than just putting @Id on a field. When you’re using Hibernate Annotations you might also run into an annoying bug. In this article I will explain how to map a composite primary key with JPA-annotations and how to work around the bug in Hibernate Annotations.

On the AMIS SOA training program we got a nice assignment. The instructors gave us a WSDL, a database and some instructions. We had to create something with the SOA Suite based on the WSDL and the instructors would connect to our service to test whether we succeeded. Our team (Alex, Patrick B. and me) decided to make a ‘something’ with BPEL and PL/SQL.
When I came home after the training I thought why not also do it in java, just to refresh my knowledge. I generated some Java classes based on the WSDL with XFire, that went quite smooth (thanks to earlier experience with XFire [AM1]:).
The next step was connecting to the database with Hibernate Annotations. I used all the standard JPA annotations, so this should also work with Toplink Essentials or any other JPA-implementation. I forgot that some tools exist to generate the mapping, so I did it by hand.

These are the database tables:

After some simple tables a table with a composite Id was in my way. I usually avoid those tables, but now I had no control over the tables (well actually I have, but that’s considered cheating the assignment ;-) )
After searching on google and dzone I didn’t get any further, the closest thing to a solution was a piece of information in the Hibernate manual [HI1]. That link got me confused and it isn’t really clear what you have to do. I guess I’m also a bit rusty with Hibernate. To clean off rust you can use Cola, so I opened a can, drank it and I almost immediately had a result. I remembered I wrote a blog about generating annotated classes for JPA [AM2]. So let’s give that a shot.
My idea was to create a mapping for the annoying table and see what was generated. After that I can probably remove some annotations and attributes on existing annotations. The table in the way was the CC_REGISTRATIONS table. To create a solution I just created mappings for the primary key columns (att_id and ssn_id) and a random normal column (status).  This way you don’t have to scroll through pages of code and it’s easier for me to see a solution.

The result of the generating process was two classes (yes, I also expected one): CcRegistrationsEntity and  CcRegistrationsEntityPK. The CcRegistrationsEntity is just like your normal entity. The only differences are an extra annotation on the class definition: @IdClass(CcRegistrationsEntityPK.class) and two @Id annotations (on attId and ssnId) instead of one.

package nl.amis.csi;


import javax.persistence.*;
import java.math.BigInteger;


@Entity
@IdClass(CcRegistrationsEntityPK.class)
@Table(schema = "CCSI", name = "CC_REGISTRATIONS")
public class CcRegistrationsEntity {
    private BigInteger ssnId;
    private BigInteger attId;
    private String status;

    @Id
    @Column(name = "SSN_ID", nullable = false, length = 10)
    public BigInteger getSsnId() { ... }

    public void setSsnId(BigInteger ssnId) { ... }

    @Id
    @Column(name = "ATT_ID", nullable = false, length = 10)
    public BigInteger getAttId() { ... }

    public void setAttId(BigInteger attId) { ... }

    @Basic
    @Column(name = "STATUS", length = 1)
    public String getStatus() { ... }

    public void setStatus(String status) { ... }

    public boolean equals(Object o) { ... }

    public int hashCode() { ... }
}



The CcRegistrationsEntityPK class has the primary key fields of the table and -very important- an overridden equals and hashCode method.

package nl.amis.csi;

import java.io.Serializable;

public class CcRegistrationsEntityPK implements Serializable {
    private java.math.BigInteger ssnId;
    private java.math.BigInteger attId;

    public java.math.BigInteger getSsnId() { ... }

    public void setSsnId(java.math.BigInteger ssnId) { ... }

    public java.math.BigInteger getAttId() { ... }

    public void setAttId(java.math.BigInteger attId) { ... }

    public boolean equals(Object o) {... }

    public int hashCode() { ... }
}

Now this all looks quite reasonable, but I got an error:
Caused by: java.sql.SQLException: ORA-00904: “CSIREGISTR0_”.”SSNID”: invalid identifier
That’s strange, there should be an underscore. Renaming the getSsnId to getSsn_id is a solution, but a very ugly one. After some searching I found out this actually is a bug in Hibernate Annotations [HI2]. Luckily there is a comment on that bug that is very helpful. When you move the @Column annotations for the Id columns to the IdClass everything works fine.

** Update 20-05 – How you really should do it **

After the comment of p3t0r I contacted him and the word ‘prefer’ was a real understatement. With the @IdClass annotation you have redundant information in your classes and there is a much cleaner solution (which I didn’t get at the time). But here it is (I actually did it myself, p3t0r just gave me a lot of directions). First the CsiRegistrations2 class:

package nl.amis.hibernate.tables;

import javax.persistence.*;

@Entity
@Table(name = "CC_REGISTRATIONS")
public class CsiRegistrations2 {

    private CsiRegistrations2PK pk;

    private String status;

    @EmbeddedId
    public CsiRegistrations2PK getPk() {...}

    public void setPk(CsiRegistrations2PK pk) {...}

    @Column
    public String getStatus() {...}

    public void setStatus(String status) {...}
}

As you can see the id’s are replaced with a CsiRegistrations2PK object and annotated with an @EmbeddedId. The CsiRegistrations2 class:

package nl.amis.hibernate.tables;

import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
import java.math.BigInteger;

@Embeddable
public class CsiRegistrations2PK implements Serializable {

    private BigInteger sessionId;
    private BigInteger attendeeId;

    @Column(name = "SSN_ID", nullable = false, length = 10)
    public BigInteger getSessionId() {...}

    public void setSessionId(BigInteger sessionId) {...}

    @Column(name = "ATT_ID", nullable = false, length = 10)
    public BigInteger getAttendeeId() {...}

    public void setAttendeeId(BigInteger attendeeId) {...}
}

This is almost the same as the previous example, but with an @Embeddable.
The reason why the bu
g on Hibernate Annotations is o
pen for so long is because there is a better solution I guess. And again a confirmation my theory: blaming a bug is just a sign of a bad developer ;-)

** end of update **

Conclusion

It’s very educational to write JPA-annotations yourself, but when you’re stuck don’t try for too long, let a generator do the work for you and try to figure out why a piece of code is generated. It’s a pity that there is a bug in Hibernate Annotations, but on the other hand it’s the first real bug I found. I found it very hard to find a blog/article that explains how to map composite primary keys, it isn’t that difficult after all, so I probably used the wrong keywords.

Sources

[HI1] http://www.hibernate.org/hib_docs/annotations/reference/en/html/entity.html#d0e1662
[HI2] http://opensource.atlassian.com/projects/hibernate/browse/EJB-286
[AM1]  http://technology.amis.nl/blog/?p=2533
[AM2] http://technology.amis.nl/blog/?p=2361

Share.

About Author

2 Comments

  1. Personally I prefer to use javax.persistence.EmbeddedId / javax.persistence.Embeddable to nicely group the attributes of the key in a single object without having to duplicate them in the parent object. Also, having the embeddable annotation on the Id object instantaneously tell other developers how the object should be be used…