This article gives an overview of how to map a one to one entity relation with Hibernate JPA. After reading it you should know how to use JPA annotations to ensure your entities are in a one to one relation. There are several ways you can map this type of entity relation, including using the same primary key, or an additional join table. For this article, I picked one which makes the most sense, which is by using a foreign key join column. The code for this article’s examples can be found on allAroundJava GitHub.

Benefits of a one to one mapping

one to one relation person and passport entities

Let’s use a Person – Passport example to illustrate a one to one relation. Let’s assume that each Person can have zero or one Passports. From Person end, therefore, the relation to Passport is optional. On the other hand, a Passport cannot exist without its owner.
A one to one mapping gives us the possibility to access Passport’s owner just by referring to its property. This means no additional SQL code is required to fetch related Person entity. It works both ways if the relationship is bi-directional. If it wasn’t for the mapping we’d need to retrieve passport’s owner in the following way:


SELECT * FROM PERSON WHERE ID=?

With the mapping set up, Hibernate deals with an additional select for us. We also have an option of configuring cascade type helping us persist Passport whenever we want to store new Person in our database.

Example one to one JPA mapping

Here’s how to use @OneToOne annotation to successfully set up a one to one entity mapping.


@Entity
public final class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToOne(mappedBy = "person")
    private Passport passport;
    
    //constructor
    //getters
}

@Entity
public class Passport {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String passportNumber;

    @OneToOne(optional = false)
    @JoinColumn
    private Person person;

    //constructor
    //getters

}

The @OneToOne annotation on Passport’s side configures Person as an entity relation. The optional=false setting indicates that this relation is mandatory and we cannot persist a Passport without its Person owner.
@JoinColumn annotation describes that entity relation is configured with a foreign key column in Passport table.
This example mapping is made bi-directional from the start, but it’s worth pointing out that the Person side of mapping is definitely optional.
The above configuration results in such a schema

Person entity in one to one relation

Passport entity table in one to one relation

Here’s a simple test to verify if the mapping really works.


@Test
public void havingPassport_whenRetrievingIt_thenPersonCanBeAccessed() {
    Person person = new Person("John Smith");
    personDao.persist(person);

    Passport passport = new Passport("ABC 123", person);
    passportDao.persist(passport);

    Optional retrievedPassport = passportDao.getById(passport.getId());
    Assert.assertTrue(retrievedPassport.isPresent());
    Assert.assertEquals(person.getName(), retrievedPassport.get().getPerson().getName());
}

The above code produces the following log output.


10:39:08.463 [main] DEBUG org.hibernate.SQL - create table Passport (id bigint generated by default as identity, passportNumber varchar(255) not null, person_id bigint not null, primary key (id))
10:39:08.469 [main] DEBUG org.hibernate.SQL - create table Person (id bigint generated by default as identity, name varchar(255), primary key (id))
10:39:08.473 [main] DEBUG org.hibernate.SQL - alter table Passport add constraint UK_k9ly76isw9txkm7x3ygnbchbo unique (person_id)
10:39:08.476 [main] DEBUG org.hibernate.SQL - alter table Passport add constraint FKbn4ik956giujpfuboelv614q9 foreign key (person_id) references Person
10:39:08.517 [main] DEBUG com.allaroundjava.dao.BaseDao - Persisting class com.allaroundjava.model.Person
10:39:08.671 [main] DEBUG org.hibernate.SQL - insert into Person (id, name) values (null, ?)
10:39:08.696 [main] DEBUG com.allaroundjava.dao.BaseDao - Persisting class com.allaroundjava.model.Passport
10:39:08.698 [main] DEBUG org.hibernate.SQL - insert into Passport (id, passportNumber, person_id) values (null, ?, ?)
10:39:08.704 [main] DEBUG com.allaroundjava.dao.BaseDao - Fetching class com.allaroundjava.dao.PassportDao with id 1 from database
10:39:08.711 [main] DEBUG org.hibernate.SQL - select passport0_.id as id1_0_0_, passport0_.passportNumber as passport2_0_0_, passport0_.person_id as person_i3_0_0_, person1_.id as id1_1_1_, person1_.name as name2_1_1_ from Passport passport0_ inner join Person person1_ on passport0_.person_id=person1_.id where passport0_.id=?

Similarly, as with previous mappings, the bi-directional mapping does not change the table structure on the Person side, as it’s not the owner of the relation. It’s the Passport table which carries an additional foreign key column. What’s also interesting is that when a Passport is fetched by Hibernate, a Person is eagerly fetched with an inner join.
Let’s see how it looks when we’re trying to fetch Person then.


@Test
public void havingPassport_whenRetrievingPerson_thenPassportCanBeAccessed() {
    Person person = new Person("James Jones");
    personDao.persist(person);

    Passport passport = new Passport("CDE 321", person);
    passportDao.persist(passport);

    Optional retrievedPerson = personDao.getById(person.getId());
    Assert.assertTrue(retrievedPerson.isPresent());

    Passport retrievedPassport = retrievedPerson.get().getPassport();
    Assert.assertEquals(passport.getPassportNumber(), retrievedPassport.getPassportNumber());
}

The log output is very similar to above example, but with one significant distinction. This time, because Person can either have Passport or not, its data is retrieved in a single select query with a left join.


10:53:05.992 [main] DEBUG org.hibernate.SQL - insert into Person (id, name) values (null, ?)
10:53:06.025 [main] DEBUG com.allaroundjava.dao.BaseDao - Persisting class com.allaroundjava.model.Passport
10:53:06.028 [main] DEBUG org.hibernate.SQL - insert into Passport (id, passportNumber, person_id) values (null, ?, ?)
10:53:06.035 [main] DEBUG com.allaroundjava.dao.BaseDao - Fetching class com.allaroundjava.dao.PersonDao with id 1 from database
10:53:06.046 [main] DEBUG org.hibernate.SQL - select person0_.id as id1_1_0_, person0_.name as name2_1_0_, passport1_.id as id1_0_1_, passport1_.passportNumber as passport2_0_1_, passport1_.person_id as person_i3_0_1_ from Person person0_ left outer join Passport passport1_ on person0_.id=passport1_.person_id where person0_.id=?

1 comment add yours

  1. I love you! I’ve been struggling to solve this problem for hours and you’re my hero!!

Leave a Comment