Spring Data JPA One to One Unidirectional Mapping

In this tutorial, we will learn how to perform one-to-one mapping using Spring Data JPA (Hibernate as a JPA provider).

We will take the Order and the Address (billing address) entities to perform One to One Unidirectional mapping.

One Order can have only one Address (billing address). If we load the Order entity then we can get the associated billing address using unidirectional mapping but vice versa is not possible.

Database level, the Orders table has a relationship field (foreign key) that refers to the Addresses table.

1. Creating Spring Boot Project

Spring Boot provides a web tool called https://start.spring.io to bootstrap an application quickly. Just go to https://start.spring.io and generate a new spring boot project.

Use the below details in the Spring boot creation:

Project Name: spring-data-jpa-course

Project Type: Maven

Choose dependencies:  Spring Data JPA, MySQL Driver, Lombok

Package name: net.javaguides.springboot

2. Maven Dependencies

Add the following dependencies to your pom.xml file:

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>

		<dependency>
			<groupId>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

3. Configure MySQL database

Let's use the MySQL database to store and retrieve the data in this example and we gonna use Hibernate properties to create and drop tables.

Open the application.properties file and add the following configuration to it:
spring.datasource.url=jdbc:mysql://localhost:3306/demo?useSSL=false
spring.datasource.username=root
spring.datasource.password=Mysql@123

spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect

spring.jpa.hibernate.ddl-auto = create-drop
Make sure that you will create a demo database before running the Spring boot application.
Also, change the MySQL username and password as per your MySQL installation on your machine.

4. Create JPA Entities

Let's create an entity package inside a base package "net.javaguides.springboot".

Within the entity package, create an Order and Address classes with the following content:

Order.java

package net.javaguides.springdatajpacourse.entity;

import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import jakarta.persistence.*;
import java.math.BigDecimal;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name="orders")
public class Order {

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

    @Column(name="order_tracking_number")
    private String orderTrackingNumber;

    @Column(name="total_quantity")
    private int totalQuantity;

    @Column(name="total_price")
    private BigDecimal totalPrice;

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

    @Column(name="date_created")
    @CreationTimestamp
    private Date dateCreated;

    @Column(name="last_updated")
    @UpdateTimestamp
    private Date lastUpdated;

    // Unidirectional
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "billing_address_id", referencedColumnName = "id")
    private Address billingAddress;

    public Long getId() {
        return id;
    }

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

    public String getOrderTrackingNumber() {
        return orderTrackingNumber;
    }

    public void setOrderTrackingNumber(String orderTrackingNumber) {
        this.orderTrackingNumber = orderTrackingNumber;
    }

    public int getTotalQuantity() {
        return totalQuantity;
    }

    public void setTotalQuantity(int totalQuantity) {
        this.totalQuantity = totalQuantity;
    }

    public BigDecimal getTotalPrice() {
        return totalPrice;
    }

    public void setTotalPrice(BigDecimal totalPrice) {
        this.totalPrice = totalPrice;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public Date getDateCreated() {
        return dateCreated;
    }

    public void setDateCreated(Date dateCreated) {
        this.dateCreated = dateCreated;
    }

    public Date getLastUpdated() {
        return lastUpdated;
    }

    public void setLastUpdated(Date lastUpdated) {
        this.lastUpdated = lastUpdated;
    }

    public Address getBillingAddress() {
        return billingAddress;
    }

    public void setBillingAddress(Address billingAddress) {
        this.billingAddress = billingAddress;
    }

    @Override
    public String toString() {
        return "Order{" +
                "id=" + id +
                ", orderTrackingNumber='" + orderTrackingNumber + '\'' +
                ", totalQuantity=" + totalQuantity +
                ", totalPrice=" + totalPrice +
                ", status='" + status + '\'' +
                ", dateCreated=" + dateCreated +
                ", lastUpdated=" + lastUpdated +
                ", billingAddress=" + billingAddress +
                '}';
    }
}
The above Order class represents an order entity in a database. It is annotated with @Entity, which marks it as a JPA entity class.

The @Table annotation is used to specify the name of the database table that this entity maps to.

The Order class has several fields annotated with @Column, which maps each field to a corresponding column in the database table. 

The @Id annotation specifies that the id field is the primary key of the Order entity, and the @GeneratedValue annotation specifies that the value of the id field is automatically generated by the database.

The @CreationTimestamp and @UpdateTimestamp annotations are used to automatically set the dateCreated and lastUpdated fields to the current timestamp when the entity is created or updated, respectively.

The @OneToOne annotation is used to specify a one-to-one relationship between the Order entity and the Address entity. The cascade attribute is set to CascadeType.ALL, which means that any changes made to the Order entity will be propagated to the associated Address entity.

Address.java

package net.javaguides.springdatajpacourse.entity;

import jakarta.persistence.*;

@Entity
@Table(name="addresses")
public class Address {

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

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

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

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

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

    @Column(name="zip_code")
    private String zipCode;

    public Long getId() {
        return id;
    }

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

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }

    public String getCountry() {
        return country;
    }

    public void setCountry(String country) {
        this.country = country;
    }

    public String getZipCode() {
        return zipCode;
    }

    public void setZipCode(String zipCode) {
        this.zipCode = zipCode;
    }

    @Override
    public String toString() {
        return "Address{" +
                "id=" + id +
                ", street='" + street + '\'' +
                ", city='" + city + '\'' +
                ", state='" + state + '\'' +
                ", country='" + country + '\'' +
                ", zipCode='" + zipCode + '\'' +
                '}';
    }
}
The annotations used:
@Entity: This annotation indicates that the class is a JPA entity and should be managed by the persistence context.

@Table: This annotation specifies the name of the table to which the entity is mapped.

@Id: This annotation indicates that the field is the primary key of the entity.

@GeneratedValue: This annotation specifies the strategy used for generating the primary key value.

@Column: This annotation specifies the mapping of the field to the corresponding column in the database.

5. Create Spring Data JPA Repositories

The next thing we’re gonna do is to create repositories to access Order and Address entities data from the database.

The JpaRepository interface defines methods for all the CRUD operations on the entity, and a default implementation of the JpaRepository called SimpleJpaRepository.

Let's create a repository package inside a base package "net.javaguides.springdatarest".

Within the repository package, create OrderRepository and AddressRepository interfaces with the following content:

OrderRepository.java

package net.javaguides.springdatajpacourse.repository;

import net.javaguides.springdatajpacourse.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderRepository extends JpaRepository<Order, Long>{
	Order findByOrderTrackingNumber(String orderTrackingNumber);
}

AddressRepository.java

package net.javaguides.springdatajpacourse.repository;

import net.javaguides.springdatajpacourse.entity.Address;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AddressRepository extends JpaRepository<Address, Long> {
}

6. Testing One-to-One Mapping

Let's write the JUnit test to perform CRUD operations on one-to-one unidirectional mapping using Spring Data JPA:

package net.javaguides.springdatajpacourse.repository;

import net.javaguides.springdatajpacourse.entity.Address;
import net.javaguides.springdatajpacourse.entity.Order;
import net.javaguides.springdatajpacourse.entity.OrderItem;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.math.BigDecimal;

@SpringBootTest
public class OneToOneUnidirectionalMappingTest {

    @Autowired
    private OrderRepository orderRepository;

    @Test
    void saveOrderOperation(){
        // create Order object
        Order order = new Order();

        order.setOrderTrackingNumber("1000");
        order.setStatus("COMPLETED");
        order.setTotalPrice(new BigDecimal(2000));
        order.setTotalQuantity(5);

        Address billingAddress = new Address();
        billingAddress.setStreet("kothrud");
        billingAddress.setCity("pune");
        billingAddress.setState("Maharashra");
        billingAddress.setCountry("India");
        billingAddress.setZipCode("11048");

        order.setBillingAddress(billingAddress);

        // save both order and address ( Cascade type - ALL)
        orderRepository.save(order);
    }

    @Test
    void fetchOrderOperation(){
        // display Order information

        // fetch order with it's address
        Order order1 = orderRepository.findById(1L).get();

        System.out.println(order1.toString());
    }

    @Test
    void updateOrderOperation(){
        // fetch order with it's address
        Order order1 = orderRepository.findById(1L).get();

        System.out.println(order1.toString());

        order1.setStatus("DELIVERED");

        order1.getBillingAddress().setZipCode("11047");

        // update order along with that address
        orderRepository.save(order1);
    }

    @Test
    void deleteOrderOperation(){
        // remove order with it's address
        orderRepository.deleteById(1L);
    }
}
Let's understand CascadeType.ALL. Well, the CascadeType.ALL contain important PERSIST, MERGE, and REMOVE operations and let's understand each of them.

Cascading the one-to-one persist operation

The CascadeType.PERSIST comes along with the CascadeType.ALL configuration, so we only have to persist the Order entity, and the associated Address entity is persisted as well:

 // create Order object
        Order order = new Order();

        order.setOrderTrackingNumber("1000");
        order.setStatus("COMPLETED");
        order.setTotalPrice(new BigDecimal(2000));
        order.setTotalQuantity(5);

        Address billingAddress = new Address();
        billingAddress.setStreet("kothrud");
        billingAddress.setCity("pune");
        billingAddress.setState("Maharashra");
        billingAddress.setCountry("India");
        billingAddress.setZipCode("11048");

        order.setBillingAddress(billingAddress);

        // save both order and address ( Cascade type - ALL)
        orderRepository.save(order);

Cascading the one-to-one merge operation

The CascadeType.MERGE is inherited from the CascadeType.ALL setting, so we only have to merge the Order entity, and the associated Address is merged as well:

 // fetch order with it's address
        Order order1 = orderRepository.findById(1L).get();

        System.out.println(order1.toString());

        order1.setStatus("DELIVERED");

        order1.getBillingAddress().setZipCode("11047");

        // update order along with that address
        orderRepository.save(order1);

Cascading the one-to-one delete operation

The CascadeType.REMOVE is also inherited from the CascadeType.ALL configuration, so the Order entity deletion triggers an Address entity removal too:

        // remove order with it's address
        orderRepository.deleteById(1L);

Udemy Course

Comments

  1. Hello. Can one to one relationship be used in Spring MVC + Thymeleaf?

    I can save the Order and the Address without problems, but I can't find a way to save the address inside the Order.

    ReplyDelete

Post a Comment

Leave Comment