OneToOne with Hibernate

October 7, 2009

I have a use case within my database schema that involves a one-to-one relationship. Typically, I map most of my tables with an automatic auto-incrementing primary key. The relationship table then has a single foreign key to its single relationship. This works great within Hiberante and HQL without issue. That is until you begin to use Hibernate’s second level cache. Hibernate’s second level cache works by caching all the individual properties and the ids of the foreign key relationships. In other words, it does not store the actual instances themselves. When requested from the cache, Hibernate hydrates the properties into the object form. It then loads the relationships by retrieving from the session (or second level cache) with the foreign key. When you have a OneToOne mapped by a join column such as:

@Entity
public class Person
{
    @Id
    @Column(name="id")
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Integer id;
 
    @OneToOne(fetch=FetchType.LAZY, mappedBy="person")
    private PersonDetails personDetails;
}
 
@Entity
public class PersonDetails
{
    @Id
    @Column(name="id")
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Integer id;
 
    @OneToOne(fetch=FetchType.LAZY)
    @JoinColumn(name="person_id")
    private Person person;
}

In this case, Hibernate will cache the person details table with [ id, person_id ]. When it hydrates the cache entry, it knows how to lookup Person based on the person_id cache value. However, when person is cached, it only caches [ id ] since there is no foreign key from person to person_details. As such, Hibernate can only lookup the person_details by using “from person_details where person_id = :id”. You are prolly wondering where I am going with all of this. The point is that even though you are caching both Person and PersonDetails in the cache (or query cache), Hibernate will always invoke a SQL statement whenever person.getPersonDetails() is invoked. This is due to the fact that Person does not have the foreign key relationship forcing Hibernate to issue a query to find it. That can basically kill your database and cache performance.

So what is the proper way to avoid this type of issue? The answer is to use the same key for both tables. Thus, you have a identity auto incrementing primary key on person and then a manual primary key on person_details that is also the foreign key back to person. In the end, they will both always have the same id. When this is done, Hibernate will always know how to go between Person and PersonDetails since the cache value and foreign key relationship is always its own primary key. Hibernate can optimize this and HQL statements as well. To properly do this, you would have the following entities:

@Entity
public class Person
{
    @Id
    @Column(name="id")
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Integer id;
 
    @PrimaryKeyJoinColumn
    @OneToOne(fetch=FetchType.LAZY)
    private PersonDetails personDetails;
}
 
@Entity
public class PersonDetails
{
    @Id
    @Column(name="id")
    @GeneratedValue(generator="foreign")
    @GenericGenerator(name="foreign", strategy="foreign", 
                                  parameters={ @Parameter(name="property", value="person") })
    private Integer id;
 
    @PrimaryKeyJoinColumn
    @OneToOne(fetch=FetchType.LAZY)
    private Person person;
}

That tells Hibernate that the relationships are controlled by the primary key. It also tells Hibernate to use the auto-incrementing identity primary key from the database for person and then to use that person’s primary key as the id of the person_details. So, the following statement:

Person p = new Person();
PersonDetails pd = new PersonDetails();
p.setPersonDetails(pd);
pd.setPerson(p);
getEntityManager().persist(p);

would result in the following SQL:

INSERT INTO person (...) VALUES (?, ?, ...);
-- this returns the assigned primary key
INSERT INTO person_details (..., id) VALUES (?, ?, ..., X)
-- X here would be the assigned primary key of the prior insert statement

So, IMO you should always use OneToOne with PrimaryKeyJoinColumn and ensure the foreign keys are the same as the primary keys. This will improve your cache results and simplify your functionality.

3 Responses to “OneToOne with Hibernate”

  1. insert works but
    session.get() still retrieves the person_details eagerly

  2. Great article !! I had the same issue and wanted to verify online and found this issue.

    I am wondering if hibernate should support some notion of cache to associate the primary key of the associated one-to-one relationship for an entity when the associated entity doesnt share the same primary key as parent entity. Something similar to what the second level cache does for one-to-many

  3. Other option which is not so clean would be to fake the one-to-one as one-to-many. At least avoids avoids extra selects for the related entity.

Leave a Reply