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.

8 comments:

Anonymous said...

Hey Vigil thanks for sharing this it helped me overcome this exact issue.

Vigil Bose said...

I have updated the post with some additional information with respect to setting the "optional" parameter at both the parent object and as well as its child object level for each of their one-to-one association counterpart. This is important to avoid the exception I mentioned in the post during entityManage.merge(parent object) via the cascade option set at the parent object level.

Basically, if you handle both bi-directional set up as well as the "optional" parameters, you can enjoy the rest of Hibernate world of persistence for all one-to-one mapping associations including entityManager.persist(object) as well as entityManager.merge(object) without any errors.

S Charan Kumar said...

hi,

i have tried similar example with
Question and Answer enities, for every Question there is a Answer question_id is the primary key in both the entities and question_id refers foreign key to Question from Answer Entity.

But still i am having exception of
org.hibernate.id.IdentifierGenerationException: attempted to assign id from null one-to-one property: question

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

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

@Column (name = "TEXT")
private String text;

@OneToOne(cascade = {CascadeType.ALL}, fetch=FetchType.LAZY, optional=true)
@PrimaryKeyJoinColumn
@JoinColumn (name = "QUESTION_ID", nullable = false)
private Answer answer;

public Answer getAnswer()
{
return answer;
}

public void setAnswer(Answer answer)
{
this.answer = answer;
}

public Long getId()
{
return id;
}

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

public String getText()
{
return text;
}

public void setText(String text)
{
this.text = text;
}

}

@Entity
@Table (name = "ANSWER")
public class Answer implements Serializable
{
@Id
@GeneratedValue(generator = "foreign")
@GenericGenerator(name = "foreign", strategy = "foreign",
parameters = {@Parameter(name = "property", value = "question")})
private Long id;

@Column (name = "answer")
private String answer;


@OneToOne (optional = false)
@JoinColumn (name = "QUESTION_ID")
private Question question;

public String getAnswer()
{
return answer;
}

public void setAnswer(String answer)
{
this.answer = answer;
}

public Long getId()
{
return id;
}

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

public Question getQuestion()
{
return question;
}

public void setQuestion(Question question)
{
this.question = question;
}
}



The above is my code please let me know what went wrong in my code.

Thanks... for your Post.

Asad said...

That is good post..well i am having a problem and it is driving me nuts.
i have a scenario in which each product it link to some other product.
table = product
id (primary key),
link(pointing to id)
this link can and cannot be null.its kind of parent child realtion in one table..after searching and found nothing i thought may b my design is bad so i split the product table
table = product
id

table = pr_link
prod_id ()
link_id ()
both are foreign keys from product table. it is one to one relation and iam unable to fix it..please help thank you..

Vigil Bose said...

Hi Mr. S Charan Kumar,

Why did you make "Answer" entity nullable = false and optional=true within your Question entity. You may need to take nullable = false out unless you are passing some default values in order to make it not nullable. What operation are trying to do with the entities. For insertion, I would recommend a bidirectional set up like the following.

Question q = new Question();
Answer a = new Answer();
q.setAnswer(a);
a.setQuestion(q);


Try it again and post back the results.

Regards,
Vigil

Vigil Bose said...

Hi Mr. Asad,

Could you please post your domain objects code in its entirety so that I have a full view of your problem.

Thanks,
Vigil

Anonymous said...

Your blog keeps getting better and better! Your older articles are not as good as newer ones you have a lot more creativity and originality now keep it up!

S Charan Kumar said...

hi,

sorry for a long time break, that works fine for me.