JPA One-to-Many Mapping

JPA One-to-Many Mapping

0 Comments

When you work in Enterprise Spring applications, you will often need to establish mappings between your entities. JPA one-to-many mapping is one such example. At the database level, one-to-many mapping means that one row in a table is mapped to multiple rows in another table.

In this post, I’ll explain how to implement JPA one-to-many mapping in a Spring Boot application.

The Example Application

To understand one-to-many mapping, think of a shopping cart that can contain many products. In the database, we will have a ShoppingCart table and a Product table.

Below is the ER diagram.

The Application POM

For this post, we will create a Spring Boot Maven project. The application POM is this.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.5</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>guru.springframewok</groupId>
  <artifactId>jpa-one-to-many</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>jpa-one-to-many</name>
  <description>Demo project for One to Many relationship</description>
  <properties>
    <java.version>17</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <scope>runtime</scope>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <excludes>
            <exclude>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
            </exclude>
          </excludes>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>

The Entities

We will first create a ShoppingCart entity, like this.

ShoppingCart.java

package guru.springframewok.jpaonetomany.model;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Getter
@Setter
@Entity
public class ShoppingCart {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @OneToMany(cascade = CascadeType.ALL,mappedBy = "shoppingCart",fetch = FetchType.EAGER)
    private Set<Product> products= new HashSet<>();

    public ShoppingCart addProduct(Product product){
        product.setShoppingCart(this);
        this.products.add(product);
        return this;
    }
}

In the preceding code, @Getter and @Setter are Lombok annotations. They will generate the getter and setter methods of the fields.

If you are new to Lombok, I suggest going through my series of posts Spring Boot with Lombok

On Line 17, the Set<Product> field is annotated with @OnetoMany to indicate that one shopping cart can have a set of products.

The cascade = CascadeType.ALL attribute specifies that any action (PERSIST, REMOVE, REFRESH, MERGE, DETACH) on the target entity (ShoppingCart) will propagate to the associated entity (Product).

The mappedBy attribute signals the underlying Hibernate ORM that the key for the relationship is on the other side – that is on the product side.

Finally, we have the attribute fetch = FetchType.EAGER. This instructs Hibernate to load the Set<Product> collection fully at the time their parent (ShoppingCart) is fetched.

Note: By default, @OneToMany annotation has FetchType.LAZY.

We will next create the owning side of the relationship, which is Product. We call Product the owning side because it will hold the foreign key of ShoppingCart.

The code of the Product class is this:

Product.java

package guru.springframewok.jpaonetomany.model;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.math.BigDecimal;

@Getter
@Setter
@EqualsAndHashCode(exclude = {"shopingCart"})
@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @ManyToOne
    private ShoppingCart shoppingCart;
    private String name;
    private String description;
    private BigDecimal price;

    public Product() {
    }

    public Product(String name, String description, BigDecimal price) {
        this.name=name;
        this.description = description;
        this.price = price;
    }
}

In the preceding Product class, the ShoppingCart field is annotated with @ManyToOne. This makes the mapping bidirectional. You can now navigate the association in both directions in your domain model

The Application.properties File

We are using embedded H2. We shall set its properties in the application.properties file, like this.

spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:cartdb
spring.jpa.defer-datasource-initialization=true

The BootstrapData Class

We will create a BootsrapData class. This class will load data once the application starts up. This will allow us to test how data are getting persisted and retrieved.

BootstrapData.java

package guru.springframewok.jpaonetomany.bootstrap;

import guru.springframewok.jpaonetomany.model.Product;
import guru.springframewok.jpaonetomany.model.ShoppingCart;
import guru.springframewok.jpaonetomany.repositories.ShoppingCartRepository;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.util.Set;

@Component
public class BootstrapData implements ApplicationListener<ContextRefreshedEvent> {
    private ShoppingCartRepository shoppingCartRepository;
    public BootstrapData(ShoppingCartRepository shoppingCartRepository) {
        this.shoppingCartRepository=shoppingCartRepository;
    }

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        ShoppingCart shoppingCart = new ShoppingCart();
        Product product1=new Product("Luxor Pen","Perfect gift to your office colleague",new BigDecimal(88));
        Product product2=new Product("Wind of life","A best seller for self motivation",new BigDecimal(30));
        shoppingCart.addProduct(product1);
        shoppingCart.addProduct(product2);
        shoppingCartRepository.save(shoppingCart);
        loadData();
    }

    private void loadData() {
        Iterable<ShoppingCart> cart=shoppingCartRepository.findAll();
        for(ShoppingCart shoppingCartIterable: cart){
           shoppingCartIterable.getProducts().stream().forEach((items) -> {
                System.out.println(items.getName());
                System.out.println(items.getDescription());
                System.out.println(items.getPrice());
                System.out.println("_____________________");
            });
        }
    }
}

The class is a Spring component and implements the ApplicationListener<ContextRefreshedEvent> interface. Spring will call the onApplicationEvent(ContextRefreshedEvent) event either during initializing or refreshing the ApplicationContext. In this method, the code creates and persists Product and ShoppingCart objects.

We also have a loadData() method to retrieve persistent shopping cart and its products. We then print out the product information.

Running the Application

When we build and run the application, we get the following output on the console.

Let’s check the H2 console by pointing the browser to http://localhost:8080/h2-console

Then, on logging in, we can see the PRODUCT and SHOPPING_CART tables getting created.

Next, let’s query the SHOPPING_CART table. In the output, notice that one table has been created.

Next, let’s query the PRODUCT table.

In the preceding figure notice the foreign key column SHOPPING_CART_ID that identifies the ShoppingCart Id that the product belongs to.

Finally, let’s add the following before Line 28 in the BootStrapData class.

shoppingCartRepository.delete(shoppingCart);

When you rerun your application, the shopping cart will get saved first and the products will be printed on the console. But when the delete() method is called to delete the shopping cart, the corresponding products in the shopping cart will also get deleted.

You can verify this through the H2 Console. This is the effect of the cascade attribute we set on the @OneToMany annotation in the ShoppingCart class.

You can find the corresponding code of this post on Github.

To learn in-depth about different types of JPA relationships, you can go through my Udemy course Hibernate and Spring Data JPA: Beginner to Guru

About SFG Contributor

Staff writer account for Spring Framework Guru

    You May Also Like

    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.