JPA FetchType.Eager is NOT a solution for n+1 queries issue
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_0If 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.
JPA default fetch types
Remember that JPA associations have different default fetch types:
| Association | Default |
|---|---|
@OneToMany | LAZY |
@ManyToMany | LAZY |
@ManyToOne | EAGER |
@OneToOne | EAGER |
@ElementCollection | LAZY |
The eager defaults for @ManyToOne and @OneToOne are particularly important because they can introduce unexpected queries if you do not define the fetch type explicitly.
How to solve the n+1 problem?
Use @Query with JOIN FETCH
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_idWe can avoid to write a query with fetch using a feature added in JPA 2.1, @EntityGraph
Use @EntityGraph
JPA also provides entity graphs, introduced in JPA 2.1.
With Spring Data JPA, we can use them directly on repository methods:
@EntityGraph(attributePaths = "books")
@Query("SELECT a FROM Author a")
List<Author> findAllAuthorsWithBooks();The entity graph tells the persistence provider that books must be part of the fetch plan for this operation.
This gives us an important advantage over configuring the relationship globally as EAGER: different queries can use different fetch plans.
For example:
List<Author> findAll();can retrieve only what is normally needed for authors, while:
@EntityGraph(attributePaths = "books")
@Query("SELECT a FROM Author a")
List<Author> findAllAuthorsWithBooks();explicitly requests books when the use case needs them.
This keeps fetching decisions close to the query instead of hard-coding them permanently into the entity model.
Named EntityGraph
For graphs that are reused in several places, we can define a named entity graph:
@Entity
@NamedEntityGraph(
name = "Author.books",
attributeNodes = @NamedAttributeNode("books")
)
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(
mappedBy = "author",
fetch = FetchType.LAZY
)
private List<Book> books = new ArrayList<>();
}Then reference it from the repository:
@EntityGraph(
value = "Author.books",
type = EntityGraph.EntityGraphType.FETCH
)
@Query("SELECT a FROM Author a")
List<Author> findAllAuthorsWithBooks();Entity graphs become even more useful when multiple associations or nested associations need to be loaded.
For example, an application could request:
Author
└── books
└── publisherwithout changing all of those associations permanently to EAGER.
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.