In this tutorial, we will learn how to perform one-to-one Bidirectional mapping using Spring Data JPA (Hibernate as JPA provider).
We will take Order
and the Address
(billing
address) entities to perform One to One Bidirectional mapping.
One Order
can have only one Address
(billing address). If we load the Order
entity then we can get the
associated billing address and if we load Address
then we can get the
associated order entity.
In this example, the order_id
column in the addresses table is the
foreign
key to the orders table.
Spring Boot provides a web tool called https://start.spring.io to bootstrap an application quickly. Just go to the website and generate a new Spring Boot project using the details below:
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>
Use the following configurations in your application.properties
file:
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
Ensure the demo database exists before running the application, and adjust the username and password according to your MySQL installation.
Let's create a entity
package inside a base package
"net.javaguides.springboot".
Within the entity
package, create an Order
and Address
classes with
the following content:
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;
@OneToOne(cascade = CascadeType.ALL, mappedBy = "order", fetch = FetchType.LAZY)
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 +
'}';
}
}
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;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "order_id", referencedColumnName = "id")
private Order order;
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;
}
public Order getOrder() {
return order;
}
public void setOrder(Order order) {
this.order = order;
}
@Override
public String toString() {
return "Address{" +
"id=" + id +
", street='" + street + '\'' +
", city='" + city + '\'' +
", state='" + state + '\'' +
", country='" + country + '\'' +
", zipCode='" + zipCode + '\'' +
'}';
}
}
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:
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);
}
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> {}
Let's write the JUnit test to perform CRUD operations on one-to-one bidirectional mapping using Spring Data JPA:
package net.javaguides.springdatajpacourse.repository;
import net.javaguides.springdatajpacourse.entity.Address;
import net.javaguides.springdatajpacourse.entity.Order;
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 OneToOneBidirectionalMappingTest {
@Autowired
private OrderRepository orderRepository;
@Autowired
private AddressRepository addressRepository;
@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);
billingAddress.setOrder(order);
// save both order and address ( Cascade type - ALL)
orderRepository.save(order);
}
@Test
void fetchBillingAddressOperation(){
// display Order information
// fetching address will also fetch associated order details
Address address = addressRepository.findById(1L).get();
System.out.println(address);
}
@Test
void updateAddressOperation(){
// fetch order with it's address
Address address = addressRepository.findById(1L).get();
address.setZipCode("11047");
address.getOrder().setStatus("DELIVERED");
// update address along with it's order
addressRepository.save(address);
}
@Test
void deleteOrderOperation(){
// remove address will also remove oder
addressRepository.deleteById(1L);
}
}
@Test
void fetchBillingAddressOperation(){
// display Order information
// fetching address will also fetch associated order details
Address address = addressRepository.findById(1L).get();
System.out.println(address);
}
@Test
void updateAddressOperation(){
// fetch order with it's address
Address address = addressRepository.findById(1L).get();
address.setZipCode("11047");
address.getOrder().setStatus("DELIVERED");
// update address along with it's order
addressRepository.save(address);
}
@Test
void deleteOrderOperation(){
// remove address will also remove oder
addressRepository.deleteById(1L);
}