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;
}
}