Monday, December 15, 2008

JPA: Entity as identity and unidirectional OneToMany

Recently, I have been trying to model an existing database using Java Persistence API. I have tried both OpenJPA and Hibernate, settling on the latter because it gives more informative exceptions and errors. OpenJPA however has better documentation than Hibernate JPA, in my opinion. Therefore I will refer to both in this article. Forgive me for the rather long title, it sums up some of the experiences I've made.

I could have written quite lengthy about entity as identity, compound identities and relations, but to save time, both yours and mine, I will provide an example:

Two classes Picture and Comment are related to one another. One picture is identified by an integer. In addition, a picture has a set of comments. One comment is identified by a picture and an integer. Thus, a comment can not be identified without a picture, and therefore comments only exists in the relation between a picture and its comments.

JPA implementations default to using a table for every entity and a join-table for their relations. In my example, the pictures are in their own table but the comments are in the same table as the relation. In other words, the comments table has a foreign key to the pictures table giving the owning picture. Thus, the tables are:

pictures: id, user, title, description
comments: pid(picture id), id, user, comment

The user field is another relation, which is trivial and therefore won't be discussed any further. The primary key of "pictures" is the id field, while the primary keys of the comments are the "pid" and the "id". Thus a "pid" and an "id" identifies a comment. The "id" field of the comment could also be called the comment number.

Now, I tried modelling this in several ways, using for instance Hibernates CollectionOfElements annotation. I explored other ways since this method is specific to Hibernate. The way I settled on, uses only the JPA standard annotations, but I'm not entirely sure why it works. The key to making it work was using the @MapKey annotation, which I have not seen used on anything other than Maps. I should investigate more about the @MapKey annotation. Without further explanation, these are the two classes I settled on. Getters and setters are omitted. Any comments you may have are appreciated!



@Entity
@Table(name="pictures")
public class Picture implements Serializable {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private int id;

@ManyToOne
@JoinColumn(name="uid")
private User user;
@Basic
private String title;
@Basic
private String caption;

@OneToMany
@MapKey
@JoinColumn(name="pid")
private List<picturecomment> comments;
}

@Entity
@Table(name = "pictures_comments")
@IdClass(PictureComment.PictureCommentId.class)
public class PictureComment implements Serializable {

@Id
private int id;
@Id
@Column(name="pid")
private int pictureid;

@ManyToOne
@JoinColumn(name = "uid")
private User user;
@Basic
private String comment;



public static class PictureCommentId implements Serializable {
@Column(name="pid")
public int pictureid;
public int id;

public boolean equals(Object o) {
if (!(o instanceof PictureCommentId)) {
return false;
}
PictureCommentId p = (PictureCommentId) o;
return pictureid == p.pictureid && id == p.id;
}

public int hashCode() {
return id ^ pictureid;
}
}
}




Hibernate generates the following SQL statement:
select comments0_.pid as pid2_, comments0_.id as id2_, comments0_.id as id4_1_, comments0_.pid as pid4_1_, comments0_.comment as comment4_1_, comments0_.uid as uid4_1_, user1_.id as id2_0_ from pictures_comments comments0_ left outer join users user1_ on comments0_.uid=user1_.id where comments0_.pid=127

Some links to the resources which I found useful:
http://www.hibernate.org/118.html
http://www.jpox.org/docs/1_2/jpa_orm/one_to_many_map.html
http://openjpa.apache.org/builds/1.2.0/apache-openjpa-1.2.0/docs/manual/manual.html#ref_guide_pc_oid_entitypk