Spring Data JPA in Spring Boot[part-2]: one-to-one association

Spring Data JPA in Spring Boot[part-2]: one-to-one association

In the previous article[Part-1], I've talked about the entities and JPA specification and its implementation with hibernate and some necessary annotations regarding JPA and hibernate.

In this blog, I'm going to demonstrate how we can create mappings or relationships between entities. There can be three types of associations.

  • One to One

  • One to Many And Many to One.

  • Many to Many

Also, those can be unidirectional and bidirectional. Let's talk about the One to One Today.

Code Setup

I've generated a standard Spring Boot project from the starter for this article. I've added 3 additional dependencies to this project. Those are the following

  • Spring Boot starter JPA

  • H2 database

  • Lombok

Here I've used the default ORM tool Hibernate which is configured by default in the Spring Boot.

One To One association Using foreign keys

A common approach for implementing one-to-one mapping is using foreign keys.

Here, car_id in the employee table is the foreign key to the car table.

Let's implement the Employee entity.

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private int id;

    @Column(nullable = false)
    private String name;

    private String address;

    @Column(unique = true, nullable = false)
    private String phone;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name="car_id", referencedColumnName = "id")
    private Car car;
}

This is a simple code, nothing to explain explicitly except the OneToOne part. I've defined the columns using the @Columns annotation which is not mandatory but I've used here for validating some columns. And tells Spring boot that this class is a entity class using the @Entity annotation. Also, tells Lombok to generate the constructor, getters and setters using the following annotations.

  • @NoArgsConstructor

  • @Getter

  • @Setter

Obviously those are not a part of the JPA. We can also generate those using our IDE or manual coding. I've explained details about those in my previous blog. Please checkout this link.

The CascadeType.ALL means it will propagate all database, JPA, Hibernate specific operations from the parent to the child entity. In this case, Employee to Car.

Now, lets move on the Car part. Here we can do unidirectional or bidirectional mapping. Let's check out both.

Unidirectional example:

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Car {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private int id;

    @Column(nullable = false, unique = true)
    private String number;

    private String model;
}

And the Bidirectional example:

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Car {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private int id;

    @Column(nullable = false, unique = true)
    private String number;

    private String model;

    @OneToOne(mappedBy = "car")
    private Employee employee;
}

Here, I've used the important @OneToOne annotation. Also, I've to use the @JoinColumn annotation in which owns the foreign key(here is Employee) and specify the foreign key in Employee and the primary key in Car Entity. If we don't specify any name, hibernate will assume a default foreign key. The default is only applied for a single join. In the bidirectional relation example, the mappedBy specifies the owner of a relationship. If we don't specify below code snippet, that would be a unidirectional example.

@OneToOne(mappedBy = "car")
private Employee employee;

For bidirectional example, Hibernate will execute the following SQL queries. I've collected those from the debugger console.

drop table if exists car cascade

drop table if exists employee cascade 

drop sequence if exists car_seq

drop sequence if exists employee_seq

create sequence car_seq start with 1 increment by 50

create sequence employee_seq start with 1 increment by 50

create table car (
    id integer not null,
    model varchar(255),
    number varchar(255) not null unique,
    primary key (id)
)

create table employee (
    car_id integer unique,
    id integer not null,
    address varchar(255),
    name varchar(255) not null,
    phone varchar(255) not null unique,
    primary key (id)
)

alter table if exists employee 
   add constraint FK37dk34dryn7qctdvd015geq6a 
   foreign key (car_id) 
   references car

Let's focus on the 'create table' and 'alter table' queries. Here car_id is the foreign key in employee table. And this car_id is the primary key of the car table(id field in the car table). Let's check out the SQL query generated by the Hibernate for the unidirectional case.

create table car (
    id integer not null,
    model varchar(255),
    number varchar(255) not null unique,
    primary key (id)
)

create table employee (
    car_id integer unique,
    id integer not null,
    address varchar(255),
    name varchar(255) not null,
    phone varchar(255) not null unique,
    primary key (id)
)

alter table if exists employee 
   add constraint FK37dk34dryn7qctdvd015geq6a 
   foreign key (car_id) 
   references car

We can clearly see that in both cases, Hibernate generates and executes the same SQL query.

The owner: In this example, the owner of the relationship/association is the Car entity. I've specified this using (mappedBy="car") in the OneToOne Annotation in the bidirectional example. Same goes for the unidirectional example even if we don't specify the owner. But the foreign key owner is the car table. In other word, car is the child that's why it is the owner.

Utility methods: For syncing both sides, using helper methods is considered as best practice in the child side. In our Car class, lets add 2 utility method. One is for the adding a parent and another is for removing the parent.

    public void addEmployee(Employee employee) {
        employee.setCar(this);
        this.employee = employee;
    }

    public void removeEmployee() {
        if(employee != null) {
            employee.setCar(null);
            this.employee = null;
        }
    }

The complete code is

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Car {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private int id;

    @Column(nullable = false, unique = true)
    private String number;

    private String model;

    @OneToOne(mappedBy = "car")
    private Employee employee;

    public void addEmployee(Employee employee) {
        employee.setCar(this);
        this.employee = employee;
    }

    public void removeEmployee() {
        if(employee != null) {
            employee.setCar(null);
            this.employee = null;
        }
    }
}

Example-2: One To One association Using a shared primary key

Here, we will not use an additional foreign key. We will use the primary key as the foreign key.

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private int id;

    @Column(nullable = false)
    private String name;

    private String address;

    @Column(unique = true, nullable = false)
    private String phone;

    @OneToOne(cascade = CascadeType.ALL)
    @PrimaryKeyJoinColumn
    private Car car;
}
@Entity
@NoArgsConstructor
@Getter
@Setter
public class Car {
    @Id
    private int id;

    @Column(nullable = false, unique = true)
    private String number;

    private String model;

    @OneToOne
    @MapsId
    @JoinColumn(name = "id")
    private Employee employee;
}
  • PrimaryKeyJoinColum specifies the primary key which is used as a foreign key column. In this example using this annotation indicates that The primary key of the Car entity is used as a foreign key for the Employee entity.

  • MapsId defines that both primary key columns are mapped. This also defines the primary key of the Car entity will be copied.

  • In this example, Car's primary key uses User's primary key. So Car has no primary key generation strategy.

The generated query by Hibernate is

create table car (
    id integer not null,
    model varchar(255),
    number varchar(255) not null unique,
    primary key (id)
)

create table employee (
    id integer not null,
    address varchar(255),
    name varchar(255) not null,
    phone varchar(255) not null unique,
    primary key (id)
)

alter table if exists car 
   add constraint FK8gguo6842qd3od2b9dmp0ewql 
   foreign key (id) 
   references employee

Optional One To One Mapping with a join table

Both examples 1 & 2 are examples of mandatory one-to-one mapping. We can also make an optional one-to-one relationship by introducing a new table. This technique is also used in many-to-many relationships. In a one-to-one relationship, this technique is used to avoid null values.

For example, there should be a customer who has no credit card. If we establish a relationship using a foreign key in the customer table, in that case, we have to put null who has no credit card. In the above diagram, I've established that a customer can have only one credit card. Also, a credit card is only owned by a customer.

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    private String name;

    private String email;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinTable(
            name = "customer_card_mapping",
            joinColumns = {
                    @JoinColumn(name = "customer_id", referencedColumnName = "id"),

            },
            inverseJoinColumns = {
                    @JoinColumn(name = "card_id", referencedColumnName = "id")
            }
    )
    CreditCardInfo creditCardInfo;
}

The CreditCardInfo entity

@Entity
@NoArgsConstructor
@Getter
@Setter
@Table(name = "credit_card_info")
public class CreditCardInfo {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    private int cardNumber;

    @OneToOne(mappedBy = "creditCardInfo")
    private Customer customer;
}

Here, we use the @JoinTable annotation, it holds all the additional table's properties. Here the variable/bean name is important. Whatever the bean name is(in our example, creditCardinfo), will be used in the 2nd entity class's @OneToOne annotation mappedBy value.

@Entity
@NoArgsConstructor
@Getter
@Setter
@Table(name = "credit_card_info")
public class CreditCardInfo {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    private int cardNumber;

    @OneToOne(mappedBy = "creditCardInfo")
    private User user;
}

The Hibernate generated query looks like

create table credit_card_info (
    card_number integer not null,
    id integer not null,
    primary key (id)
)

create table customer (
    id integer not null,
    email varchar(255),
    name varchar(255),
    primary key (id)
)

create table customer_card_mapping (
    card_id integer unique,
    customer_id integer not null,
    primary key (customer_id)
)

alter table if exists customer_card_mapping 
   add constraint FKglt8lmioir98lap4bxkstdi3w 
   foreign key (card_id) 
   references credit_card_info

alter table if exists customer_card_mapping 
   add constraint FKlxgp2833i9tfp60qlmdf5aaxs 
   foreign key (customer_id) 
   references customer

Clearly you can see there are 3 tables here. The 3rd one(customer_card_mapping) is for the join table.

Conclusion

There are many to learn. I've added the code here[GitHub]. Next I'll talk about the One to Many or Many to One relationship. I hope this article helps a lot to you. If you like this article, please don't forget to like and share. If you have any query, Please let me know in the comment. I've added the references below where I've learnt those. I hope those will give you additional information. Happy coding.

References