Saturday, April 18, 2009

JPA/Hibernate - One to One Mapping Quirks

Introduction

I have been working on a product where I am currently using Hibernate implementation of JPA (Java Persistence Architecture) as data persistence mechanism. While working, I came across an interesting finding about one-to-one mapping using annotations. In this short article, I would like to explain my findings that may be of help to some novice JPA/Hibernate developers.

Technical Details

Let us take Users.java and Contacts.java as domain examples to explain the scenario. In this case, User object has certain attributes and Contact object also has its own specific attributes. Assume a user can have only one contact, then one of the common ways of expressing one-to-one relationships is to have the dependent object share the same primary key as the controlling object. In UML, it is called "compositional relationships".

User.java

@Entity
@Table (name="Users")
public class Users implements Serializable{

private static final long serialVersionUID = -3174184215665687091L;

/** The cached hash code value for this instance. Setting to 0 triggers re-calculation. */
@Transient
private int hashValue = 0;

/** The composite primary key value. */
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Column (name="id")
private Long Id;

/** The value of the simple username property. */
@Column(name = "username", unique=true, nullable=false)
private String username;

/** The value of the contacts association. */
@OneToOne(cascade = {CascadeType.ALL}, fetch=FetchType.LAZY, optional=true)
@PrimaryKeyJoinColumn
@JoinColumn (name="id", nullable=false)
private Contacts contacts;

.....

public Users(){}

/**
* @return Id
*/
public Long getId(){
return this.Id;
}
/**
* @param id - The Id to set
*/
public void setId(Long id){
this.hashValue = 0;
this.Id = Id;
}
/**
* @return Contacts
*/
public Contacts getContacts(){
return this.contacts;
}
/**
* @param Contacts - The contacts to set
*/
public void setContacts(Contacts contacts){
this.contacts = contacts;
}

....
}

Contacts.java

@Entity
@Table (name="Contacts")
public class Contacts implements Serializable{

private static final long serialVersionUID = -1079313023058953957L;

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

/** The value of the users association. */
@OneToOne(optional=false)
@JoinColumn (name="id")
private Users users;

....

public Contacts (){}

/**
* @return Id
*/
public Long getId(){
return this.Id;
}
/**
* @param id - The Id to set
*/
public void setId(Long id){
this.hashValue = 0;
this.Id = id;
}
/**
* @return Users
*/
public Users getUsers(){
return this.users;
}
/**
* @param users - The users to set
*/
public void setUsers(Users users){
this.users = users;
}

....
}


The above set up will result in an error message that says like the following:

Exception raised - org.hibernate.id.IdentifierGenerationException:
ids for this class must be manually assigned before calling save(): com.vbose.Contacts

The problem is that Hibernate is confused in this case. It thinks that it should look at the Id column to determine the primary id of the table. In reality, we should have told Hibernate that the Users object is the thing that provides the id. In order to help Hibernate understand how to properly resolve the id, we need to annotate the id column of the Contacts with a special "Generator", like so:

The one below is an enhanced Contacts.java domain object:

@Entity
@Table (name="Contacts")
public class Contacts implements Serializable{

private static final long serialVersionUID = -1079313023058953957L;

@Id
@GeneratedValue(generator="foreign")
@GenericGenerator(name="foreign", strategy = "foreign", parameters={
@Parameter(name="property", value="users")
})
@Column (name="id")
private Long Id;

/** The value of the users association. */
@OneToOne(optional=false)
@JoinColumn (name="id")
private Users users;

....

public Contacts (){}

/**
* @return Id
*/
public Long getId(){
return this.Id;
}
/**
* @param id - The Id to set
*/
public void setId(Long id){
this.hashValue = 0;
this.Id = id;
}
/**
* @return Users
*/
public Users getUsers(){
return this.users;
}
/**
* @param users - The users to set
*/
public void setUsers(Users users){
this.users = users;
}

....
}


The additional annotation at the "Id" property says that the generated value of the Id column comes from a special generator that simply reads a foreign key value off of the users object. Please note that contacts object has to be set in Users and vice versa in order to establish bi-directional relationship before setting other properties of each objects. This bi-directional setup will prevent the users from getting the following exception
org.hibernate.id.IdentifierGenerationException: attempted to assign id from
null one-to-one property:
For e.g.

Users users = new Users();
Contacts contacts = new Contacts();
users.setContacts(contacts);
contacts.setUsers(users);

The above set up works for inserting a new record using cascade option set for the child object association at the parent level. So you can safely use entityManager.persist(object). However, please note the "optional" parameter set for Contacts one-to-one association at the Users object. This optional parameter plays a key role while saving the child association via its parent using cascade option. If you do not set the "optional" parameter at the parent as well as its child object, then you will likely get the above said exception while updating the parent and its child association via cascade option. This error is prominent if you add a new child through its parent while updating the parent that has reference to a new child entity.

For e.g. If you replace the existing contact instance with a new contact instance (with the foreign key assigned from its parent object as the child object's parent key as well as its foreign key to its parent) and update the changes using entityManager.merge(users) operation will result in the same above said error if the "optional" parameter is not set properly at the one-to-one association annotation.

The "optional" parameter should be set to true at the parent level and should be set to false at the child object level.

However please note the "optional" parameter at the one-to-one mapping is not really required if you just do plain update to the existing child entity or its parent entity.

Conclusion

The above said quirk is not documented in Hibernate reference document. I hope it will help any novice JPA/Hibernate developers who are facing problems with one-to-one mapping using annotations.