Fetch Strategies - JOIN FETCH, Graphs and Batch Fetching
Fetch Strategies - JOIN FETCH, Graphs and Batch Fetching
Lazy loading prevents unnecessary queries, but sometimes you need the related data in the same request. Hibernate provides several fetch strategies - JOIN FETCH, Entity Graphs, and batch fetching - that let you load related data efficiently without resorting to eager mapping on the entity.
The N+1 Problem
The most common performance problem in ORM applications is the N+1 query problem. You load N parent entities, then each parent triggers a separate query to load its lazy collection:
+-- Load 100 authors ---- SELECT * FROM authors (1 query)
| +-- Author 1 books SELECT * FROM books WHERE author_id = 1 (1 query)
| +-- Author 2 books SELECT * FROM books WHERE author_id = 2 (1 query)
| +-- ...
| +-- Author 100 books SELECT * FROM books WHERE author_id = 100 (1 query)
+-- Total: 101 queries (N+1)
This is terribly slow on large datasets. JOIN FETCH, Entity Graphs, and batch fetching all solve this problem in different ways.
JOIN FETCH
A JPQL JOIN FETCH tells Hibernate to load the related collection in the same SQL query as the parent, using a SQL JOIN. This is the most straightforward fix for N+1:
@Query("SELECT a FROM Author a JOIN FETCH a.books WHERE a.id = :id")
Author findAuthorWithBooks(@Param("id") Long id);
Hibernate generates a single SQL query with a JOIN between authors and books. One round trip to the database, all data loaded. You can fetch multiple collections with separate JOIN FETCH clauses, but be careful - fetching two collections with JOIN FETCH produces a Cartesian product.
@EntityGraph
Spring Data JPA provides @EntityGraph as a declarative alternative. You define which relationships to fetch without writing JPQL:
@EntityGraph(attributePaths = {"books", "books.reviews"})
@Query("SELECT a FROM Author a WHERE a.id = :id")
Author findAuthorWithBooksAndReviews(@Param("id") Long id);
The @EntityGraph overrides the default fetch type for the specified paths. Books and their reviews are loaded eagerly for this query, while all other relationships remain lazy. You can also define named entity graphs on the entity class:
@NamedEntityGraph(name = "Author.withBooks",
attributeNodes = @NamedAttributeNode("books"))
@Entity
public class Author {
// ...
}
Batch Fetching
Batch fetching is a middle ground between lazy loading and JOIN FETCH. Instead of loading all collections in one big JOIN, Hibernate issues a second query to load multiple collections at once:
@OneToMany(mappedBy = "author")
@BatchSize(size = 20)
private List<Book> books = new ArrayList<>();
When you access author1.getBooks(), Hibernate loads the books for that author AND for up to 19 other uninitialized Author proxies in the same persistence context. It does this by issuing SELECT ... WHERE author_id IN (?, ?, ...). This reduces 100 separate queries down to 5 or fewer.
Choosing the Right Strategy
Use JOIN FETCH when you know you need the related data in a specific query. Use @EntityGraph when you want a clean declaration of what to fetch. Use batch fetching when you have many parent entities and want a compromise between one massive JOIN and individual lazy queries.
Real-World Scenario
A blog platform displays an author's profile page showing the author and their latest 20 posts with comments. Using @EntityGraph with attributePaths for "posts" and "posts.comments" loads everything in one query. On the author listing page showing 50 authors without posts, no extra fetching is needed. For a page listing 500 recent posts grouped by author with post counts, batch fetching on the posts collection reduces the query count from 501 to about 25.
Key Points
- The N+1 problem occurs when loading N parents triggers N additional queries for their collections.
- JOIN FETCH loads related data in a single SQL query using a JOIN, eliminating N+1 entirely.
- @EntityGraph provides a declarative way to override fetch strategies per query.
- Batch fetching loads multiple collections at once with a WHERE IN clause, reducing round trips.
- Choose the strategy based on whether you always need the data (JOIN FETCH) or sometimes do (@EntityGraph).