Mastering @EntityGraph in Spring Boot 3: Eliminate N+1 Queries Efficiently

This article explains the classic N+1 query issue in Spring Data JPA, demonstrates how JPQL JOIN FETCH and the @EntityGraph annotation can declaratively load associations, and provides advanced examples—including named entity graphs and combining @EntityGraph with custom @Query—to improve performance and code maintainability.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Mastering @EntityGraph in Spring Boot 3: Eliminate N+1 Queries Efficiently

1. Introduction

When using JPA, entity relationships are often configured with LAZY loading to avoid unnecessary overhead. Accessing these lazy associations outside a transaction triggers LazyInitializationException, and in pagination or complex queries it leads to the N+1 query problem, severely degrading performance.

The traditional JOIN FETCH solves the issue but lacks reusability. A declarative, reusable approach is needed to control association loading precisely.

2. Practical Cases

2.1 Classic N+1 Problem

Given the following repository method:

private final OrderRepository orderRepository;
List<Order> orders = this.orderRepository.findAll();

Executing this generates separate SQL statements for the t_order table, then for each order its related t_customer and t_order_item rows, resulting in 1 + N + N queries (e.g., 21 queries for 10 orders).

2.2 JPQL Optimization with JOIN FETCH

Using JPQL to fetch associations in a single query reduces round‑trips:

@Query("SELECT o FROM Order o JOIN FETCH o.customer JOIN FETCH o.items")
List<Order> findOrders();

The generated SQL joins order, customer, and order_item tables, eliminating the N+1 problem.

2.3 Using @EntityGraph for Automatic Fetching

@EntityGraph

lets Spring Data JPA modify queries to eagerly load specified associations without writing explicit joins.

// Load both Customer and OrderItem
@EntityGraph(attributePaths = {"customer", "items"})
List<Order> findAll();

// Load only Customer
@EntityGraph(attributePaths = {"customer"})
Optional<Order> findById(Long id);

Running findAll() produces two SQL statements: one joining order with customer, and a separate query for order_item. The findById case joins only the customer table.

2.4 Advanced Usage: Named Entity Graphs

Define a reusable graph directly on the entity:

@Entity
@Table(name = "t_order")
@NamedEntityGraph(name = "Order.EG", attributeNodes = {
    @NamedAttributeNode("customer"),
    @NamedAttributeNode("items")
})
public class Order { }

Reference it in a repository method:

@EntityGraph(value = "Order.EG", type = EntityGraph.EntityGraphType.LOAD)
List<Order> findAll();

The resulting SQL loads the order together with its customer and items efficiently.

2.5 Combining @EntityGraph with Custom @Query

Custom queries can also benefit from @EntityGraph:

@Query("SELECT o FROM Order o WHERE o.status = :status")
@EntityGraph(attributePaths = {"customer"})
List<Order> findByStatus(@Param("status") Integer status);

This query filters orders by status while automatically fetching the associated customer, avoiding additional joins or N+1 queries.

3. Entity Definitions and Sample Data

Entity classes used in the examples:

@Entity
@Table(name = "t_customer")
public class Customer {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String phone;
    private String address;
    @OneToMany(mappedBy = "customer", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<Order> orders = new ArrayList<>();
}

@Entity
@Table(name = "t_order")
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private Customer customer;
    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<OrderItem> items = new ArrayList<>();
    @Column(columnDefinition = "int default 0")
    private Integer status;
}

@Entity
@Table(name = "t_order_items")
public class OrderItem {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String productName;
    private Integer quantity;
    private Double unitPrice;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;
}

Sample table structures (images omitted for brevity) illustrate the relationships between t_customer, t_order, and t_order_items.

4. Conclusion

Using @EntityGraph —either directly or via named graphs—provides a clean, reusable way to solve the N+1 query problem in Spring Data JPA. It works seamlessly with custom JPQL queries, improves performance, and keeps repository code concise and maintainable.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

performanceBackend DevelopmentSpring BootjpaN+1 problemEntityGraph
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.