JPA FetchType.Eager is NOT a solution for n+1 queries issue

Updated: 2024-08-20

FetchType.EAGER load all the dependencies avoiding n+1 queries?

No, this is false, this error that is diffused in some websites that collect social responses.

This confusion is due to the fact that in some specific situations the linked collections are loaded in the same query.

The other origin of confusion is that Eager load all the linked data after the root entity has been loaded giving the impression that the data is immediately available, for small lists of data you could not notice the performance impact of the multiple queries.

If we have this example of relationship between Authors and Books:

public class Author { 
 
    @Id 
    @GeneratedValue(strategy = GenerationType.IDENTITY) 
    private Long id; 
 
    private String name; 
 
    @OneToMany(mappedBy = "author", fetch = FetchType.EAGER) 
    private List<Book> books; 
} 

Many developers assume that with FetchType.EAGER when the Author is loaded, his books will be loaded in the same query, avoiding the n+1 issue.

Surprise, FetchType defines WHEN the data is collected and not HOW the data is collected.

In our case with EAGER for each author we will immediately load the books with the following sequence of requests (from the example code):

Hibernate: select a1_0.id,a1_0.name from author a1_0 
Hibernate: select b1_0.author_id,b1_0.id,b1_0.title from book b1_0 where b1_0.author_id=? 
Hibernate: select b1_0.author_id,b1_0.id,b1_0.title from book b1_0 where b1_0.author_id=? 

In our DB we have 2 authors and for each author there will be a request to fetch the books.
In case of 100 authors, as you can imagine, FetchType.EAGER will generate 101 requests.

When FetchType.EAGER load all the joined entities and when it doesn't and why?

Hibernate has uses a different implementation if you are trying to load an entity using its id (using find, get) or you are executing a query.

If you are loading using the id, e.g. Author author = session.get(Author.class, 1);, Hibernate uses the annotation defined in the entity.

For example, using FetchType.EAGER or using @Fetch(FetchMode.JOIN) get(Author.class, 1), Hibernate will generate the following request

select a1_0.id,a1_0.name,b1_0.author_id,b1_0.id,b1_0.title from author a1_0 left join book b1_0 on a1_0.id=b1_0.author_id where a1_0.id=? 

Using a query or findAll or similar, Hibernate will ignore the fetching strategy and generate N+1 requests.

Difference with Lazy

FetchType.LAZY could generate the same amount of requests, it will create the selects for the books only when the code requests information about the books.

If we select all the authors, but we don't require any information about their books using the books relationship, only 1 request is execute by Hibernate:

Hibernate: select a1_0.id,a1_0.name from author a1_0 

If your code starts to demand some information about the books of an author, hibernate will execute this query for each author:

Hibernate: select b1_0.author_id,b1_0.id,b1_0.title from book b1_0 where b1_0.author_id=? 

In summary, with FetchType.EAGER you will get all the dependencies with n+1 when the data is loaded. With FetchType.LAZYyou will maybe load some or all the dependencies with n+1.

How to solve the n+1 problem?

Use @Query

In our case we need to define a @Query in our Author repository using a JOIN FETCH:

// this query retrieve all the data avoiding n+1 
@Query("SELECT a FROM Author a LEFT JOIN FETCH a.books") 
List<Author> findAllAuthorsWithBooks(); 

This code tells to hibernate that it has to create a query that fetch the Authors and the entities that are linked to the a.books relationship.

When we execute this request all the authors and all their books will be loaded with the following generated query:

Hibernate: select a1_0.id,b1_0.author_id,b1_0.id,b1_0.title,a1_0.name 
 from author a1_0 left join book b1_0 on a1_0.id=b1_0.author_id 

We can avoid to write a query with fetch using a feature added in JPA 2.1, @EntityGraph

Use @EntityGraph

TODO

Show me the code

You can find the code used for this example here: https://github.com/marco76/spring-jpa-demo

This is the base used to test the different alternatives and the results.


WebApp built by Marco using SpringBoot 3.2.4 and Java 21, in a Server in Switzerland without 'Cloud'.