Skip to content

Jpa OneToOne N+1 Issue#

Fix N+1 Issue With OneToOne Relationship in Jpa.#

  • In Jpa if we define OneToOne relationship as below.
CustomerEntity.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.springboot.project.entity;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;

import java.util.Date;
import java.util.List;
import java.util.UUID;

@Getter
@Setter
@Entity
@Table(name = "customers", indexes = {
        @Index(name = "uniqueEmailIndex", columnList = "email", unique = true),
        @Index(name = "uniquePhoneIndex", columnList = "phone", unique = true),
        @Index(name = "uniqueMultiIndex", columnList = "email, phone", unique = true)
})
public class CustomerEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;
    private String fullName;
    @Column(unique = true)
    private String email;
    private String address;
    @Column(unique = true)
    private String phone;
    @Enumerated(EnumType.STRING)
    private Gender gender;
    private Date dob;

    @JoinColumn(name = "loyalty_card")
    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private LoyaltyCardEntity loyaltyCard;

    @Fetch(FetchMode.SUBSELECT)
    @OneToMany(mappedBy = "customer", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<OrderEntity> orders;

}
LoyaltyCardEntity.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.springboot.project.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;

import java.util.Date;
import java.util.UUID;

@Getter
@Setter
@Entity
@Table(name = "loyalty_cards")
public class LoyaltyCardEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    private Integer points;

    private Date issueDate;

    @PrePersist
    private void preCreate() {
        this.issueDate = new Date();
    }

}
  • Then when we query to get a CustomerEntitty, we will see in the log that every time we get a LoyaltyCardEntity, a hibernate query will be generated base on how many LoyaltyCard in the list.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Hibernate: select c1_0.id,c1_0.address,c1_0.dob,c1_0.email,c1_0.full_name,c1_0.gender,c1_0.loyalty_card,c1_0.phone from customers c1_0 where 1=1 and 1=1 and 1=1 offset ? rows fetch first ? rows only
Hibernate: select count(c1_0.id) from customers c1_0 where 1=1 and 1=1 and 1=1
Hibernate: select l1_0.id,l1_0.issue_date,l1_0.points from loyalty_cards l1_0 where l1_0.id=?
Hibernate: select o1_0.customer_id,o1_0.id,o1_0.created_date,o1_0.last_updated_date,o1_0.order_name,o1_0.order_status from orders o1_0 where o1_0.customer_id in(select c1_0.id from customers c1_0 where 1=1 and 1=1 and 1=1)
Hibernate: select l1_0.id,l1_0.issue_date,l1_0.points from loyalty_cards l1_0 where l1_0.id=?
Hibernate: select l1_0.id,l1_0.issue_date,l1_0.points from loyalty_cards l1_0 where l1_0.id=?
Hibernate: select l1_0.id,l1_0.issue_date,l1_0.points from loyalty_cards l1_0 where l1_0.id=?
Hibernate: select l1_0.id,l1_0.issue_date,l1_0.points from loyalty_cards l1_0 where l1_0.id=?
Hibernate: select l1_0.id,l1_0.issue_date,l1_0.points from loyalty_cards l1_0 where l1_0.id=?
Hibernate: select l1_0.id,l1_0.issue_date,l1_0.points from loyalty_cards l1_0 where l1_0.id=?
Hibernate: select l1_0.id,l1_0.issue_date,l1_0.points from loyalty_cards l1_0 where l1_0.id=?
Hibernate: select l1_0.id,l1_0.issue_date,l1_0.points from loyalty_cards l1_0 where l1_0.id=?
Hibernate: select l1_0.id,l1_0.issue_date,l1_0.points from loyalty_cards l1_0 where l1_0.id=?
Hibernate: select l1_0.id,l1_0.issue_date,l1_0.points from loyalty_cards l1_0 where l1_0.id=?

...
  • So how do you think if there are 10000 customers with 10000 loyaltycards. This will make the database execute thousands queries and it will lead to bad performance.

  • To avoid it, we will use the Entity Graphs which are a feature introduced in JPA 2.1 that provides a more sophisticated method of dealing with performance loading. They provide a way to define which attributes (fields) of an entity should be loaded from the database when the entity is fetched. This can be particularly useful when dealing with entities that have relationships with other entities.

  • The main goal of the JPA Entity Graph is to improve the runtime performance when loading the entity’s related associations and basic fields1. The JPA provider loads all the graphs in one select query and then avoids fetching associations with more SELECT queries. This is considered a good approach for improving application performance.

  • In Spring Boot, we can define an Entity Graph using a combination of @NamedEntityGraph and@EntityGraph annotations. We can also define ad-hoc entity graphs with just the attributePaths argument of the @EntityGraph annotation.

With @NamedEntityGraph#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package com.springboot.project.entity;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.NamedAttributeNode;
import jakarta.persistence.NamedEntityGraph;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;

import java.util.Date;
import java.util.List;
import java.util.UUID;

@Getter
@Setter
@Entity
@Table(name = "customers", indexes = {
        @Index(name = "uniqueEmailIndex", columnList = "email", unique = true),
        @Index(name = "uniquePhoneIndex", columnList = "phone", unique = true),
        @Index(name = "uniqueMultiIndex", columnList = "email, phone", unique = true)
})
@NamedEntityGraph(name = "CustomerEntity.loyaltyCard", attributeNodes = @NamedAttributeNode("loyaltyCard"))
public class CustomerEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;
    private String fullName;
    @Column(unique = true)
    private String email;
    private String address;
    @Column(unique = true)
    private String phone;
    @Enumerated(EnumType.STRING)
    private Gender gender;
    private Date dob;

    @JoinColumn(name = "loyalty_card")
    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private LoyaltyCardEntity loyaltyCard;

    @Fetch(FetchMode.SUBSELECT)
    @OneToMany(mappedBy = "customer", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<OrderEntity> orders;

}
  • Then in the repository we just define the findAll method with @EntityGraph annotation.
CustomerRepository.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.springboot.project.repository;


import com.springboot.project.entity.CustomerEntity;
import jakarta.annotation.Nonnull;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;

import java.util.UUID;

@Repository
public interface CustomerRepository extends JpaRepository<CustomerEntity, UUID>,
        JpaSpecificationExecutor<CustomerEntity> {

    CustomerEntity findCustomerByEmail(String email);

    /**
     *
     * If we use @EntityGraph with attributePaths
     * Ex: @EntityGraph(attributePaths = {"loyaltyCard"})
     * we don't need to declare the @NamedEntityGraph in
     * the CustomerEntity.
     * In the example above the `loyaltyCard` is the field name of the entity relationship
     *
     */
    @EntityGraph(value = "CustomerEntity.loyaltyCard")
    @Nonnull Page<CustomerEntity> findAll(@Nonnull Specification<CustomerEntity> spec, @Nonnull Pageable pageable);


}

Without @NamedEntityGraph#

  • Then in the repository we just define the findAll method with @EntityGraph annotation using attributePaths.
CustomerRepository.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.springboot.project.repository;


import com.springboot.project.entity.CustomerEntity;
import jakarta.annotation.Nonnull;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;

import java.util.UUID;

@Repository
public interface CustomerRepository extends JpaRepository<CustomerEntity, UUID>,
        JpaSpecificationExecutor<CustomerEntity> {

    CustomerEntity findCustomerByEmail(String email);

    @EntityGraph(attributePaths = {"loyaltyCard"})
    @Nonnull Page<CustomerEntity> findAll(@Nonnull Specification<CustomerEntity> spec, @Nonnull Pageable pageable);


}