Solving the N+1 Query Problem in Spring Data JPA with @EntityGraph
Stop the N+1 query problem in Spring Data JPA. Learn how to use @EntityGraph to declaratively control eager fetching for specific repository methods without compromising your lazy loading defaults.
18 Jul 2026, 20:14 UTC

The Performance Trap of Lazy Loading
You build a feature to list books along with their authors and reviews. You set the relationships to FetchType.LAZY to keep the application lean. Everything works in development with three records, but in production, a single request to list 50 books triggers 101 separate database queries: one to fetch the books, and 100 more to fetch the authors and reviews for each book. This is the N+1 query problem.
The common reflex is to switch to FetchType.EAGER, but that is a global setting. If you make authors eager, every single part of your application that touches a Book entity will pull in authors, even when they aren't needed, leading to massive memory overhead and slow responses.
Declarative Fetching with @EntityGraph
The solution is to keep your entities lazy by default but tell Spring Data JPA exactly what to fetch eagerly for specific repository methods. The @EntityGraph annotation allows you to define a "fetch plan" that overrides the lazy loading behavior for a single query, using a SQL JOIN to pull related data in one trip to the database.
There are two primary types of graphs: EntityGraphType.FETCH and EntityGraphType.LOAD. A FETCH graph treats all specified attributes as eager and everything else as lazy. A LOAD graph treats specified attributes as eager, but respects the existing FetchType defined on other entity attributes.
Implementing a Targeted Fetch Plan
To implement this, you first define a NamedEntityGraph on your entity to describe the relationship path, then reference that graph in your repository.
1. Define the Graph on the Entity
@Entity
@NamedEntityGraph(
name = "Book.authorReviewGraph",
attributeNodes = {
@NamedAttributeNode("author"),
@NamedAttributeNode("reviews")
}
)
public class Book {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Author author;
@OneToMany(mappedBy = "book", fetch = FetchType.LAZY)
private List<Review> reviews;
// Getters and setters
}
2. Apply the Graph to the Repository
Run this in your repository interface. This method will now execute a single JOIN query instead of multiple SELECTs.
public interface BookRepository extends JpaRepository<Book, Long> {
@EntityGraph(value = "Book.authorReviewGraph", type = EntityGraphType.LOAD)
List<Book> findAll();
}
Verification and Risks
To verify the fix, enable SQL logging in your application.properties:
spring.jpa.show-sql=true
logging.level.org.hibernate.SQL=DEBUG
Check your console. Without the graph, you will see a stream of SELECT statements. With the graph, you should see a single SELECT statement with LEFT OUTER JOIN clauses for the author and reviews tables.
The Cartesian Product Warning
Be cautious when using @EntityGraph with multiple @OneToMany or @ManyToMany collections. If a book has 10 authors and 10 reviews, the database returns a Cartesian product of 100 rows for a single book. This can lead to significant memory pressure and duplicate results in your list.
If you encounter this, use a Set instead of a List for your collections to handle duplicates, or consider splitting the fetch into two targeted queries.
Pagination Limitations
When combining @EntityGraph with Pageable, Hibernate may perform the join in memory (fetching all rows and then paginating in Java) rather than in the database. This is a major performance risk for large datasets. To check for this, look for a warning in the logs: "firstResult/maxResults specified with collection fetch; applying in memory!"
Actionable Summary
Stop using FetchType.EAGER as a shortcut for performance. Instead:
- Keep all entity relationships
LAZY. - Identify high-traffic read endpoints that suffer from N+1 queries.
- Create a
@NamedEntityGraphfor those specific requirements. - Apply
@EntityGraphto the corresponding repository method. - Verify the resulting SQL to ensure a single JOIN is used and no in-memory pagination is occurring.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.