November 9th, 2009Hibernate One-To-One Associations with a Shared Primary Key
Problem
Mapping One to One association with JPA is pretty straitforward. The following entities:
Employee.java
package com.application.onetoone.model;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;
@Entity
@Table(name = "EMPLOYEES")
public class Employee {
private Integer employeeId;
private Desk desk;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name="employee_id")
public Integer getEmployeeId() {
return employeeId;
}
@OneToOne (cascade=CascadeType.ALL, fetch=FetchType.LAZY)
@JoinColumn(name="desk_id", unique=true)
public Desk getDesk() {
return desk;
}
public void setEmployeeId(Integer employeeId) {
this.employeeId = employeeId;
}
public void setDesk(Desk desk) {
this.desk = desk;
}
}
and
Desk.java
package com.application.onetoone.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.Table;
@Entity
@Table(name = "DESKS")
public class Desk {
private Integer deskId;
private String description;
private Employee employee;
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
@Column(name="desk_id")
public Integer getDeskId() {
return deskId;
}
@Column(name="description")
public String getDescription() {
return description;
}
@OneToOne(mappedBy="desk", fetch=FetchType.EAGER)
public Employee getEmployee() {
return employee;
}
public void setDeskId(Integer deskId) {
this.deskId = deskId;
}
public void setDescription(String description) {
this.description = description;
}
public void setEmployee(Employee employee) {
this.employee = employee;
}
}
generate the following database schema:

The owning entity – Employee – contains additional column – desk_id, which maps to the ownded entity – Desk. Having the configuration above I can:
assign a desk to employee:
Employee employee = new Employee ();
desk = deskDao.load(2);
if (null == desk.getEmployee()) {
employee.setDesk(desk);
}
ask where a given employee is sitting:
String description = employee.getDesk().getDescription();
or ask who is sitting at given desk:
Integer employeeId = desk.getEmployee().getEmployeeId();
and, since they are not glued, an employee can empty his current desk and get a new one:
Employee employee = new Employee ();
desk = deskDao.load(4);
if (null == desk.getEmployee()) {
employee.setDesk(desk);
}
A relation between employee and a seat is temporal. An employee can leave, a seat can be freed and reassing to someone else. There are however relations, which are everlasting and can not be changed over the time of entity life. Let’s say, I want to identify an employee – I give him a first name, last name and a date of birth. In order to to this I crete a separate entity:
EmployeeDetails.java
package com.application.onetoone.model;
import java.util.Calendar;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
@Entity
@Table(name = "EMPLOYEE_DETAILS")
public class EmployeeDetails {
private Integer employeeId;
private String firstName;
private String lastName;
private Calendar dateOfBirth;
private Employee employee;
@Id
@Column(name="employee_id")
public Integer getEmployeeId() {
return employeeId;
}
@Column(name="first_name")
public String getFirstName() {
return firstName;
}
@Column(name="last_name")
public String getLastName() {
return lastName;
}
@Temporal(TemporalType.DATE)
@Column(name="date_of_birth")
public Calendar getDateOfBirth() {
return dateOfBirth;
}
@OneToOne(mappedBy="employeeDetails", fetch=FetchType.EAGER)
public Employee getEmployee() {
return employee;
}
public void setEmployeeId(Integer employeeId) {
this.employeeId = employeeId;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setDateOfBirth(Calendar dateOfBirth) {
this.dateOfBirth = dateOfBirth;
}
public void setEmployee(Employee employee) {
this.employee = employee;
}
}
which I want to share a primary key with it’s parent entity – employee, which I need to amend:
package com.application.onetoone.model;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;
@Entity
@Table(name = "EMPLOYEES")
public class Employee {
private Integer employeeId;
private Desk desk;
private EmployeeDetails employeeDetails;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name="employee_id")
public Integer getEmployeeId() {
return employeeId;
}
@OneToOne (cascade=CascadeType.ALL, fetch=FetchType.LAZY)
@JoinColumn(name="desk_id", unique=true)
public Desk getDesk() {
return desk;
}
@OneToOne (cascade=CascadeType.ALL, fetch=FetchType.LAZY)
@PrimaryKeyJoinColumn
public EmployeeDetails getEmployeeDetails() {
return employeeDetails;
}
public void setEmployeeId(Integer employeeId) {
this.employeeId = employeeId;
}
public void setDesk(Desk desk) {
this.desk = desk;
}
public void setEmployeeDetails(EmployeeDetails employeeDetails) {
this.employeeDetails = employeeDetails;
}
}
The entities above generate the following database schema:

As you can see, there is no foreign key generated between Employee and EmployeDetails. This is default Hibernate behaviour and was already raised as a bug, however, it is not fixed as of now. Additionally, the configuration above does not assign any key to EmployeeDetails and the following code:
Employee employee = new Employee ();
Desk desk = new Desk ();
desk.setDescription("T114A45");
employee.setDesk(desk);
EmployeeDetails employeeDetails = new EmployeeDetails ();
employeeDetails.setFirstName("John");
employeeDetails.setLastName("Doe");
employee.setEmployeeDetails(employeeDetails);
employeeDao.save(employee);
throws a HibernateSystemException with a following message: ids for this class must be manually assigned before calling save().
How do I assign it ?
Solution one – simple and ugly
Without going beyond the JPA annotation there is nothing smart that can be done. employee_id is not known before the Employee record is actually persisted, so it seems that the only way to get it right is:
Employee employee = new Employee ();
Desk desk = new Desk ();
desk.setDescription("T114A45");
employee.setDesk(desk);
// save it
employee = employeeDao.save(employee);
EmployeeDetails employeeDetails = new EmployeeDetails ();
employeeDetails.setFirstName("John");
employeeDetails.setLastName("Doe");
// set employee_id
employeeDetails.setEmployeeId(employee.getEmployeeId());
employee.setEmployeeDetails(employeeDetails);
// save again
employeeDao.save(employee);
The part of the code responsilbe for a proper mapping is left to the client. Obviously, it does the job, but this looks like workaround rather than solution.
Solution two – Hibernate GenericGenerator
JPA does not provide support for custom key generators, but Hibernate is closing the gap. First, we have to tell Hibernate, where the source of the primary key. Hibernate @GenericGenerator annotations provides the solution:
EmployeeDetails.java
package com.application.onetoone.model;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter;
@Entity
@Table(name = "EMPLOYEE_DETAILS")
public class EmployeeDetails {
private Integer employeeId;
private String firstName;
private String lastName;
private Date dateOfBirth;
private Employee employee;
@Id
@GeneratedValue(generator = "foreign")
@GenericGenerator(name = "foreign", strategy = "foreign",
parameters = {@Parameter(name = "property", value = "employee")}
)
@Column(name="employee_id")
public Integer getEmployeeId() {
return employeeId;
}
@Column(name="first_name")
public String getFirstName() {
return firstName;
}
@Column(name="last_name")
public String getLastName() {
return lastName;
}
@Temporal(TemporalType.DATE)
@Column(name="date_of_birth")
public Date getDateOfBirth() {
return dateOfBirth;
}
@OneToOne(mappedBy="employeeDetails", fetch=FetchType.EAGER)
public Employee getEmployee() {
return employee;
}
public void setEmployeeId(Integer employeeId) {
this.employeeId = employeeId;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setDateOfBirth(Date dateOfBirth) {
this.dateOfBirth = dateOfBirth;
}
public void setEmployee(Employee employee) {
this.employee = employee;
}
}
@GenericGenerator with strategy set to foreign tells Hibernate to obtain a primary key value from one of its properties.
The last thing left to do is to make sure, the property used to generate id value is not null. Since EmployeeDetails is an owned entity, it makes perfect sense to change the the owning entity – Employee:
package com.application.onetoone.model;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;
@Entity
@Table(name = "EMPLOYEES")
public class Employee {
private Integer employeeId;
private Desk desk;
private EmployeeDetails employeeDetails;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name="employee_id")
public Integer getEmployeeId() {
return employeeId;
}
@OneToOne (cascade=CascadeType.ALL, fetch=FetchType.LAZY)
@JoinColumn(name="desk_id", unique=true)
public Desk getDesk() {
return desk;
}
@OneToOne (cascade=CascadeType.ALL, fetch=FetchType.LAZY)
@PrimaryKeyJoinColumn
public EmployeeDetails getEmployeeDetails() {
return employeeDetails;
}
public void setEmployeeId(Integer employeeId) {
this.employeeId = employeeId;
}
public void setDesk(Desk desk) {
this.desk = desk;
}
public void setEmployeeDetails(EmployeeDetails employeeDetails) {
this.employeeDetails = employeeDetails;
// for @GenericGenerator
employeeDetails.setEmployee(this);
}
}
Now we can create an employee:
Employee employee = new Employee ();
EmployeeDetails employeeDetails = new EmployeeDetails ();
employeeDetails.setFirstName("John"); employeeDetails.setLastName("Doe");
employee.setEmployeeDetails(employeeDetails);
Desk desk = new Desk ();
desk.setDescription("T114A45");
employee.setDesk(desk);
employeeDao.save(employee);
Ask where employee is sitting:
String desk = employeeDetailsDao.find("lastName", "Doe").get(0).getEmployee().getDesk().getDescription();
or who is sitting here:
String employeeName = deskDao.find("description", "T114A45").get(0).getEmployee().getEmployeeDetails().getLastName();