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.

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.