Analytics

Sunday, August 23, 2009

One to One shared primary key association in JPA with Hibernate

Overview

One of the less commonly used mappings in JPA and Hiberate is the @OneToOne mapping with a shared primary key. Conceptually this type of mapping is simple to understand. However, recently I tried to implement this relationship between two objects and ran into some petty issues. No clear example was listed on the net, so I decided to post a solution on a blog for possible reference for others.

OneToOne Shared Primary Key

A one to one shared primary key is not the most common mapping pattern, but is still used by many domain object designers. More common is the one to one mapping with a separate foreign key. A one to one shared primary key is simply where two objects have a one to one relationship where they share a primary key.

For example, let's use the prominant Person to Address example.

A Person can have one Address only, and an Address can only have one Person living in it. Let's consider Person the owner of the relationship, meaning its primary key gets generated first and essentially objectwise an Address is "set" in the Person object. A one to one shared primary key relationship would mean that a Person table does not have a FK column to Address, rather the Address table's primary key value is the same as Person, and acts as a foreign key to the Person table.



In contrast a one to one relationship with a foreign key would result in the Person table having a field like ADDRESS_ID, which acts as a foreign key to the Address table. The Person primary key generation is independent of the Address primary key. This means the primary key to Person and Address are not the same generally, but could be by coincidence.


Essentially, one to one with a shared primary key saves a column in the database and usually forces a bidirectional relationship through an ORM such as hibernate.

References and Example

When attempting to implement one to one shared mapping with JPA, I had a little trouble. The official JPA documentation does not give an example with a generated id, but only gives one where the ids are manually set. The Hibernate annotations site does the same. In addition there are a few blog postings which address the issue, but they are muddled and don't specify clearly the implementation details. Thus, I had to experiment and put together knowledge from all over to reach a solution.

In Hiberate Core (without JPA or Annotation), the concept and mapping implementation is explained nicely. 5.1.13 One-to-One.

The most informational resource was the book: Java Persistence with Hibernate - 7.1 Single Valued entity associations, pg278-282. This gave the implemenation with annotations.

In order to use a one to one with a shared primary key, ids on both sides of the object need a @GeneratedValue. The owner can use a generic generator where it gets a fresh number every time, but the other side of the one to one needs a custom hibernate extension, which is from @GenericGenerator. This grabs the primary key from the owner object and places it in the dependent object. In our example, Address will have the hibernate extension @GenericGenerator.

Person class



package com.assarconsulting.addressbook.model;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.Table;

@Entity
@Table(name = "PERSON")
public class Person {

@Id
@GeneratedValue
@Column(name = "PERSON_ID")
private Long id;

@Column(name = "NAME", nullable=false)
private String name;

@Column(name = "AGE", nullable=false)
private int age;

@OneToOne(cascade = CascadeType.ALL, mappedBy = "person")
private Address address;

public Person() {
}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getStreetNumber() {
return address.getStreetNumber();
}

public String getStreetName() {
return address.getStreetName();
}

public String getCity() {
return address.getCity();
}

public String getState() {
return address.getState();
}

public String getZipCode() {
return address.getZipCode();
}

public Address getAddress() {
return address;
}

public void setAddress(Address address) {
this.address = address;
address.setPerson(this);
}

}



Address Class



package com.assarconsulting.addressbook.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;

@Entity
@Table(name = "ADDRESS")
@org.hibernate.annotations.GenericGenerator(name="person-primarykey", strategy="foreign",
parameters={@org.hibernate.annotations.Parameter(name="property", value="person")
})
public class Address {

@Id
@GeneratedValue(generator = "person-primarykey")
@Column(name = "PERSON_ID")
private Long id;

@Column(name = "STREET_NUMBER", nullable=false)
private String streetNumber;

@Column(name = "STREET_NAME", nullable=false)
private String streetName;

@Column(name = "CITY", nullable=false)
private String city;

@Column(name = "STATE", nullable=false)
private String state;

@Column(name = "ZIP_CODE", nullable=false)
private String zipCode;

@OneToOne
@PrimaryKeyJoinColumn
private Person person;

public Address() {
}

public Address(String streetNumber, String streetName, String city,
String state, String zipCode) {
super();
this.streetNumber = streetNumber;
this.streetName = streetName;
this.city = city;
this.state = state;
this.zipCode = zipCode;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getStreetNumber() {
return streetNumber;
}

public void setStreetNumber(String streetNumber) {
this.streetNumber = streetNumber;
}

public String getStreetName() {
return streetName;
}

public void setStreetName(String streetName) {
this.streetName = streetName;
}

public String getCity() {
return city;
}

public void setCity(String city) {
this.city = city;
}

public String getState() {
return state;
}

public void setState(String state) {
this.state = state;
}

public String getZipCode() {
return zipCode;
}

public void setZipCode(String zipCode) {
this.zipCode = zipCode;
}

public Person getPerson() {
return person;
}

public void setPerson(Person person) {
this.person = person;
}
}

41 comments:

  1. Not a great example, as a person could own more than one home, or if they didn't they could move home. Having the person id in the address table would then be a nuisance.
    P.S Why don't the cursor keys work in this edit box!!!!

    ReplyDelete
  2. The statement "a person could own more than one home, or if they didn't they could move home." is quite true in the real world, but is not much value in this context. The important point illustrated in the example is implementation of one to one shared primary key in JPA.

    In order to make the example work we can just make a simple assumption that one Person can have only on Address. If this doesn't make much sense then substitute the Address object with a Heart object and say "A Person can have only one Heart." The important illustration is the implementation of JPA one to one.

    In regards to the edit box, I am not sure.

    ReplyDelete
  3. Fair enough. I guess you might use this pattern in situations where you want to split data (based on the same identifier), across multiple tables for performance reasons.
    I would like to suggest that you might cut your example code above down to show just the relevant bits. It would much clearer for the reader.

    ReplyDelete
  4. Thanks Nirav. contrary to that idiot the post actually helped. Cheers mate.

    ReplyDelete
  5. Many thanks mate! It really helped me a lot.

    ReplyDelete
  6. Nice explaination..I am using one-to-one mapping which have shared primary key.Issue is first time hibernate always issues an insert for child even when its present, which obviously fails.


    After going through lot of blogs, I realise mapping works fine when
    property name="hbm2ddl.auto=create

    If you change above to "update" it stops working as insert fails for UserDetails saying there is already a duplicate key in DB

    Can you tell me how to resolve the above issue?

    ReplyDelete
  7. used the same example as in your post and that also failed for Address when i made property name="hbm2ddl.auto=update

    ReplyDelete
  8. That was exactly what I was looking for without succes in the official documentation and on the net.
    Many thanks !

    ReplyDelete
  9. Thanks ,the example helped a lot and provides good understanding.

    ReplyDelete
  10. @Abhilasha.

    Thanks for the positive feedback.

    ReplyDelete
  11. Has anybody found a way of doing that without annotations? @Nirav Assar Thx for the article.

    ReplyDelete
  12. Thanks a ton for this dude ! there is way to many annotations and many tutorials online that are not simple ; I just used ur code as a template and got my stuff sorted !

    Cheers

    J

    ReplyDelete
  13. No problem great it worked out for you. JPA is good, but check out GORM, with groovy and grails. I am hooked on it now and mapping is way easier.

    ReplyDelete
  14. I am trying to do the same thing with some changes. In my case I have another value in Person that is a primary key and is a foreign key in the Address table.
    so like in Person
    primary key(person_id,person_age)
    and in the Address table it would be like
    foreign key (person_id, person_age) references person(person_id, person_age).

    The example you showed only works if there is one primary key on the table, can you post one where its using composite key. I am still trying to use the generator for the person_id and would set the person_age on the person.

    ReplyDelete
  15. Hello Nirav, great example, but somebody could do it via JPA 2 standard annotations? We want to stay attached to JEE6 standards. Thanks!

    ReplyDelete
  16. Gonzalo, I am not sure of the variance between JEE 5 and 6 concerning the annotation, but I am sure there is not a huge difference. You could probably map this example to the JEE6 spec for JPA. Good luck, and thanks!

    ReplyDelete
  17. Thanks for the example, it helped a lot. I wanted to map a view to the master table and use the same primary key for both. I would not have thought about that solution.

    Many thanks again!

    ReplyDelete
  18. @Nirav:

    Great article! I just did some reading and found a very CONCISE and intuitive method for achieving this. It is the @PrimaryKeyJoinColumn method.

    http://download.oracle.com/javaee/5/api/javax/persistence/OneToOne.html

    http://docs.jboss.org/hibernate/annotations/3.5/reference/en/html_single/#entity-mapping-association

    ReplyDelete
  19. Excellent post, thank you.

    ReplyDelete
  20. Thank you very much for this. Recently upgrading Hibernate broke these for me and your explanation was just what I needed.

    ReplyDelete
  21. Great article, indeed an uncovered subject. You refferd a bidirectional relationship. Could you please refer the case where a person holds an addredd but the address doesn't point back to the person (a unidirectional relationship)? whould you change it and how?

    Thanks in advance

    ReplyDelete
  22. Good article.

    I'm just trying out if it works with javax.persistence.SequenceGenerator as well, so that the solution may be provider-independant.

    But I'm right at the beginning. Maybe I will be able to give some more information at the end of the week.

    ReplyDelete
  23. Thanks a lot - your blog really helped us in solving a crucial problem. We went through all the hibernate documentation and tried everything but we kept getting various errors. The examples in the hibernate documentation and the hibernate book seem to be wrong as well.Thanks again.
    -Vandana

    ReplyDelete
  24. As I said earlier I wanted to try out javax.persistence.SequenceGenerator, but I due to a constant lack of time I keep using your solution.
    I only leave the generated value of the Address-class and therefore use an Address-constructor with a Person-object.

    I also tried some other ways, but this one is by now the only one that keeps working.

    ReplyDelete
  25. I'm running into a few issues with this example. I use SequenceGenerator that use an oracle database sequence to generate the PERSON primary key. Will this example work with sequence generator ?

    Person person = new Person();
    Address address = new Address();
    person.setAddress(addres);
    session.SaveOrUpdate(person);

    When I do the above I get the error IdentifierGenerationException - attempted to assign id from null one-ton-one property.

    However when I changed the code as below to explicitly set the bi-directional association, I get a integrity constraint violated - parent key not found error.

    Person person = new Person();
    Address address = new Address();
    person.setAddress(addres);
    address.setPerson(person);
    session.SaveOrUpdate(person);


    I'm trying to create a brand new person and address record. Is there something I'm doing wrong while saving or is this the issue with Sequence generator.

    ReplyDelete
  26. I was wrong with the above post. This example works as it is for SequenceGenerator. I must have missed something. Thanks for the example. Saved me a lot of time & headache.

    ReplyDelete
  27. Hibernate reference annotation 3.5.6 explains shared primary keys by using @PrimaryKeyJoinColumn which is quite different from what you explained here.
    Your thoughts on this?

    ReplyDelete
  28. This post saved my day.
    Thanks for sharing.

    ReplyDelete
  29. Thank you for good pointers, however...

    According to the official documentation the "mappedBy" should be specified on the non-owning side of the relationship.

    In your case it seems incorrect to use mappedBy = "person" in the
    Person class (which owns an Address)

    @OneToOne(cascade = CascadeType.ALL, mappedBy = "person")

    ReplyDelete
    Replies
    1. hi, did u get the answer for this ?

      Delete
  30. Using this example I am able to post the data but unable to retrieve. Getting hibernate PropertyAccessException at

    public void setAddress(Address address) {
    this.address = address;
    address.setPerson(this);
    }

    ReplyDelete
  31. We have followed the same foreign key generator and thanks for that. But in my case, i have a specific problem. Consider the Address is optional in your same example. Now, I fetch a record Person who has address and now i am making the address object in the person entity a null(to remove the address) and perform a entityManeger.MERGE(person) operation. In this case the record in the Address table is not getting deleted automatically. Let me know if you could help me in this.

    -AraZu

    ReplyDelete
  32. I just wanted to thank Jakey in the comments. I really appreciated his post!

    ReplyDelete
  33. We can achieve the same result in JPA without using Hibernate API. Below is the example.

    //This is the User table having userId as primary key
    @Entity
    @Table(name = "users")
    public class UserEntity implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "userid")
    private Long userId;

    private boolean active;

    @Column(name = "email")
    private String emailId;

    @Column(name = "firstname")
    private String firstName;

    @OneToOne
    @PrimaryKeyJoinColumn(name = "userid",referencedColumnName = "userProfileId")
    private UsersProfileEntity userProfile;
    }

    //This is the userProfile table sharing same primary key as User //table
    @Entity
    @Table(name="users_profile")
    public class UsersProfileEntity implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    private Long userProfileId;

    private Integer age;

    private String description;

    @OneToOne(mappedBy="userProfile", fetch=FetchType.LAZY)
    private UserEntity user;
    }




    //Now this is my DAO saving both the entities

    @Repository
    public class UserDaoImpl implements UserDao {

    @PersistenceContext
    EntityManager entityManager;

    public Long saveUser(UserEntity user) throws DAOException {
    entityManager.persist(user);
    user.setImageName(user.getUserId().toString());
    user.getUserProfile().setUserProfileId(user.getUserId());
    entityManager.persist(user.getUserProfile());
    entityManager.flush();
    return user.getUserId();
    }

    }


    I have truncated the code in order to keep the example simple.
    using this approach we can use the generated userId key inside UserEntity within UserProfileEntity. Hope this will be helpfull

    ReplyDelete
  34. Thank you for the propsed solution! It helped me for hibernate 4.0.1.Final

    ReplyDelete
  35. can you please pass the complete source code for the example posted

    ReplyDelete
  36. Thank you! This helped to solve my probelm.

    ReplyDelete