JPA OneToOne Relationship

By | December 12, 2017

In JPA, two types of the @OneToOne relationships are bidirectional and unidirectional. In each type, we can use different mapping approaches:

  • One-to-one association that maps a foreign key column.
  • One-to-one association where both source and target share the same primary key values.
  • One-to-one association from an embeddable class to another entity.

1. Bidirectional OneToOne association that maps a foreign key column

Figure 1: The bidirectional @OneToOne relationship mapping a foreign key.

In the example:

  • Entity Customer references a single instance of Entity CustomerRecord.
  • Entity CustomerRecord references a single instance of Entity Customer.
  • Entity Customer is the owner of the relationship.

The following mapping default apply: Table customer contains a foreign key to table customer_record. 

1.1 JPA implementation

One-to-one association that maps a foreign key column.
java/com/example/OneToOne/domain/bidirection/Customer.java

package com.example.OneToOne.domain.bidirection;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;

@Entity
public class Customer {
    @Id
    private Long customerId;

    private String email;

    @OneToOne(cascade = CascadeType.ALL, optional=false)
    @JoinColumn(name = "customer_record_id", unique=true, nullable=false, updatable=false)
    private CustomerRecord customerRecord;

    public void setCustomerRecord(CustomerRecord customerRecord) {
        this.customerRecord = customerRecord;
        customerRecord.setCustomer(this);
    }
    // Generate Getter and Setter
}

java/com/example/OneToOne/domain/bidirection/CustomerRecord.java

package com.example.OneToOne.domain.bidirection;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToOne;

@Entity
public class CustomerRecord {
    @Id
    private Long customerRecordId;

    private String billingInformation;

    @OneToOne(mappedBy="customerRecord", optional=false)
    private Customer customer;

    // Generate Getter and Setter
}

Try to add Customer and CustomerRecord entities.
java/com/example/OneToOne/service/CustomerData.java

package com.example.OneToOne.service;

import com.example.OneToOne.domain.bidirection.Customer;
import com.example.OneToOne.domain.bidirection.CustomerRecord;
import com.example.OneToOne.repository.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class CustomerData implements CommandLineRunner {

    @Autowired
    private CustomerRepository customerRepository;

    @Override
    public void run(String... args) throws Exception {
        Customer customer = new Customer();
        customer.setCustomerId(1L);
        customer.setEmail("jack@gmail.com");

        CustomerRecord customerRecord = new CustomerRecord();
        customerRecord.setCustomerRecordId(2L);
        customerRecord.setBillingInformation("123 Main Street, Somewhere, CA 94000");
        customer.setCustomerRecord(customerRecord);
        customerRepository.save(customer);
    }
}

Run and see console log and database as below:

insert into customer_record (billing_information, customer_record_id) values (?, ?)
insert into customer (customer_record_id, email, customer_id) values (?, ?, ?)

customer_record table
----------------------------------------
customer_record_id | billing_information
2                    123 Main Street, Somewhere, CA 94000

customer table
----------------------------------------
customer_id  | email               | customer_record_id
1              jack@gmail.com        2

1.2 Evaluation

Let’s fetch a customer and see consoles log:

customerRepository.findById(1L);

Hibernate requires two queries instead of only single query.

Hibernate: select customer0_.customer_id as customer1_2_0_, customer0_.customer_record_id as customer3_2_0_, customer0_.email as email2_2_0_, customerre1_.customer_record_id as customer1_3_1_, customerre1_.billing_information as billing_2_3_1_, customer2_.custo
mer_id as customer1_2_2_, customer2_.customer_record_id as customer3_2_2_, customer2_.email as email2_2_2_ from customer customer0_ inner join customer_record customerre1_ on customer0_.customer_record_id=customerre1_.customer_record_id inner join customer cust
omer2_ on customerre1_.customer_record_id=customer2_.customer_record_id where customer0_.customer_id=?

Hibernate: select customer0_.customer_id as customer1_2_2_, customer0_.customer_record_id as customer3_2_2_, customer0_.email as email2_2_2_, customerre1_.customer_record_id as customer1_3_0_, customerre1_.billing_information as billing_2_3_0_, customer2_.custo
mer_id as customer1_2_1_, customer2_.customer_record_id as customer3_2_1_, customer2_.email as email2_2_1_ from customer customer0_ inner join customer_record customerre1_ on customer0_.customer_record_id=customerre1_.customer_record_id left outer join customer
 customer2_ on customerre1_.customer_record_id=customer2_.customer_record_id where customer0_.customer_record_id=?

The first query is to check for the existing of customer entity. The second makes sure that key reference (customer_record_id foreign key) of customer entity should be null or not regardless of setting optional = false. As as result, this might affect application performance.

2. Bidirectional OneToOne association that shares primary key

Figure 2: The bidirectional @OneToOne relationship using the same primary.

In the example:

  • Entity CardHolder references a single instance of Entity CardHolderDetails.
  • Entity CardHolderDetails references a single instance of Entity CardHolder.
  • Entity CardHolder is the owner of the relationship.

The following mapping default apply: The primary key of table card_holder_details is the same of the primary key of table card_holder. 

2.1 JPA Implementation

java/com/example/OneToOne/domain/bidirection/CardHolder.java

@Entity
public class CardHolder {
    @Id
    @Column(name = "ch_id")
    private Long id;

    private String email;
    private String password;

    @OneToOne(mappedBy = "cardHolder", cascade = CascadeType.ALL, optional = false)
    private CardHolderDetails cardHolderDetails;
    // Generate Getter and Setter.
}

java/com/example/OneToOne/domain/bidirection/CardHolderDetails.java

@Entity
public class CardHolderDetails {
    @Id
    @Column(name = "ch_details_id")
    private Long id;

    private String name;
    private String phoneNumber;
    private String dateOfBirth;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private CardHolder cardHolder;
    
    // Generate Getter and Setter
}

CardHolderDetails Entity has the same identifier like the CardHolder entity. So the identifier of CardHolder could be automatically generated by @GeneratedValue annotation, exclusive of the identifier of CardHolderDetails. Adding a new cardholder and cardholderdetail into database as below:

        CardHolder cardHolder = new CardHolder();
        cardHolder.setId(1L);
        cardHolder.setEmail("jacky@gmail.com");
        cardHolder.setPassword("password1");

        CardHolderDetails cardHolderDetails = new CardHolderDetails();
        cardHolderDetails.setName("Lucky Luck");
        cardHolderDetails.setDateOfBirth("10/10/1970");
        cardHolderDetails.setPhoneNumber("001 9383 93838");
        cardHolderDetails.setCardHolder(cardHolder); // Important

        cardHolder.setCardHolderDetails(cardHolderDetails);
        cardHolderRepository.save(cardHolder);

        // Can not delete cardHolder b/c CardHolderDetails depends on
        // it by sharing primary key. Try to delete CardHolderDetails first,
        // then for cardHolder.
        // cardHolderRepository.delete(cardHolder);

Looking at console log and database:

insert into card_holder (email, password, ch_id) values (?, ?, ?)
insert into card_holder_details (date_of_birth, name, phone_number, card_holder_ch_id) values (?, ?, ?, ?)

card_holder table
------------------------------------------
ch_id    | email           | password
1          jacky@gmail.com   password1

card_holder_details table
-----------------------------------------
card_holder_ch_id | name       |date_of_birth   | phone_number
1                   Lucky Luck  10/10/1970        001 9383 93838

The table relationship doesn’t feature any additional foreign key column since the card_holder_details table primary key references the card_holder table primary key.

2.2 Evaluation

cardHolderRepository.findById(1L);

select cardholder0_.ch_id as ch_id1_0_0_, cardholder0_.email as email2_0_0_, cardholder0_.password as password3_0_0_ from card_holder cardholder0_ where cardholder0_.ch_id
=?

select cardholder0_.card_holder_ch_id as card_hol4_1_0_, cardholder0_.date_of_birth as date_of_1_1_0_, cardholder0_.name as name2_1_0_, cardholder0_.phone_number as phone_
nu3_1_0_ from card_holder_details cardholder0_ where cardholder0_.card_holder_ch_id=?

The Hibernate also hits database two times as the bidirectional OneToOne relationship that maps a foreign key column above. According to Vlad Mihalcea, the shared primary key approach reduces the memory footprint of the child-side table indexes since it requires a single indexed column instead of two, and and enabling the second-level cache direct retrieval.

3. Unidirectional OneToOne association that shares the same primary key values

Figure 3: The Unidirectional @OneToOne relationship sharing the same PK.

In the example:

  • Entity Employee references a single instance of Entity EmployeeInfo.
  • Entity EmmployeeInfo does not references Entity Employee.
  • Entity Employee is the owner of the relationship.

The following mapping defaults apply: Table employee contains a foreign key to table employee_info.

3.1 JPA implementation

One-to-one association where both source and target share the same primary key values will use @MapsId annotation.
java/com/example/OneToOne/domain/unidirection/Employee.java

@Entity
public class Employee {
    @Id
    @Column(name = "employee_id")
    private Long employeeId;

    private String name;

    @OneToOne(orphanRemoval = true)
    @MapsId
    EmployeeInfo info;

    //Generate Setter and Getter
}

Primary key of Employee Entity used the same primary key of EmployeeInfo Entity thanks to @MapsId annotation.
java/com/example/OneToOne/domain/unidirection/EmployeeInfo.java

@Entity
public class EmployeeInfo {
    @Id
    private Long employeeInfoId;

    private String jobTitle;
    //Generate Setter and Getter.
}

Add a new employee and employeeInfo data.
java/com/example/OneToOne/service/EmployeeData.java

@Component
public class EmployeeData implements CommandLineRunner {

    @Autowired
    private EmployeeRepository employeeRepository;

    @Override
    public void run(String... args) throws Exception {
        Employee employee = new Employee();
        //employee.setEmployeeId(1L); error
        employee.setName("Peter T");

        EmployeeInfo employeeInfo = new EmployeeInfo();
        employeeInfo.setEmployeeInfoId(2L);
        employeeInfo.setJobTitle("Developer");
        employee.setInfo(employeeInfo);
        employeeRepository.save(employee);
    }

Run and see console log and database as below:

Hibernate: insert into employee_info (job_title, employee_info_id) values (?, ?)
Hibernate: insert into employee (name, info_employee_info_id) values (?, ?)

employee_info table
------------------------------
job_title | employee_info_id
2           Developer

employee table
------------------------------
name      | info_employee_info_id
Peter T     2

Hibernate automatically generates info_employee_info_id primary key name although we defined employeeId as employee_id in database.

3.2 Evaluation

Let’s fetch an employee and see console log:

Employee employee1 = employeeRepository.findById(2L)
                .orElseThrow(() -> new EntityNotFoundException());

Unfortunately, Hibernate still has two queries as the bidirectional. However, it seems likely the queries is simpler without inner join two tables.

select employee0_.info_employee_info_id as info_emp2_4_0_, employee0_.name as name1_4_0_ from employee employee0_ where employee0_.info_employee_info_id=?

select employeein0_.employee_info_id as employee1_5_0_, employeein0_.job_title as job_titl2_5_0_ from employee_info employeein0_ where employeein0_.employee_info_id=?

4. Bidirectional One-to-one association from an embeddable class to another entity

  • JPA uses three main Object-Relational mapping elements: type, embeddable and entity. The major difference between an entity and an embeddable is the presence of an identifier. Identifiers are mandatory for entity elements, and an embeddable type is forbidden to have an identity of its own.
  • An embeddable type groups multiple properties in a single reusable component.

Figure 4: The Bidirectional @OneToOne from an embeddable class to another entity.

4.1 JPA implementation

java/com/example/OneToOne/domain/bidirection/Staff.java

import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

Entity
public class Staff {
    @Id
    @GeneratedValue
    private Long id;

    @Embedded
    private LocationDetails location;
    
    // Generate Setter and Getter
}

java/com/example/OneToOne/domain/bidirection/LocationDetails.java

import javax.persistence.CascadeType;
import javax.persistence.Embeddable;
import javax.persistence.OneToOne;

@Embeddable
public class LocationDetails {
    private int officeNumber;

    @OneToOne(cascade = CascadeType.ALL)
    private ParkingSpot parkingSpot;

    // Generate setter and getter.
}

The Embeddable class does not require any identifier. Instead, Staff entity has officeNumber field and a parkingSport foreign key. Noted that Hibernate will throw an error like this if not using cascade = CascadeType.ALL anotation.

object references an unsaved transient instance - save the transient instance before flushing

By using cascade = CascadeType.ALL anotation, you tell hibernate to save them to the database when saving their parent.
java/com/example/OneToOne/service/StaffData.java

        Staff staff = new Staff();

        LocationDetails location = new LocationDetails();
        location.setOfficeNumber(911);
        ParkingSpot parkingSpot = new ParkingSpot();
        parkingSpot.setGarage("Hilton Garage");
        location.setParkingSpot(parkingSpot);

        staff.setLocation(location);
        staffRepository.save(staff);

Run and see console log and database as below:

insert into parking_spot (garage, id) values (?, ?)
insert into staff (office_number, parking_spot_id, id) values (?, ?, ?)

staff table
------------------------------
id   | office_number   | parking_spot_id
1      911               2

parking_spot table
------------------------------
id   | garage 
2      Hilton Garage

4.2 Evaluation

When trying get an staff information, Hibernate also hits database two times. One benefit of this mapping is Embeddable types can reuse state through composition

Download full code from Github

References:

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.