What is the N+1 problem in Hibernate and how can it be solved?
sobes.tech AI
Answer from AI
The N+1 problem in Hibernate occurs when fetching data, where a single query is executed to retrieve parent objects, and then a separate query is executed for each child object (or collection of child objects). This results in N+1 queries to the database, where N is the number of parent objects, instead of an optimal single query or a few queries with joins.
Example scenario with the N+1 problem:
Suppose there are classes Author and Book, where an author can have many books.
// Author entity
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY) // LAZY fetch by default for collections
private List<Book> books;
// getters and setters
}
// Book entity
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne
private Author author;
// getters and setters
}
If we want to display all authors and their book titles:
Session session = sessionFactory.openSession();
List<Author> authors = session.createQuery("FROM Author", Author.class).list(); // 1 query for Author
for (Author author : authors) {
System.out.println("Author: " + author.getName());
for (Book book : author.getBooks()) { // N queries for Book (one for each author)
System.out.println("- Book: " + book.getTitle());
}
}
session.close();
In this example:
- One query retrieves all
Authorobjects. - Then, for each
Author, a separate query loads thebookscollection. If there are 100 authors, 100 additional queries are executed forBook. Total: 1 (for authors) + 100 (for books) = 101 queries.
Solutions to the N+1 problem:
-
Using JOIN FETCH in JPQL/HQL: Explicitly loads related entities in a single query.
Session session = sessionFactory.openSession(); List<Author> authors = session.createQuery("SELECT DISTINCT a FROM Author a JOIN FETCH a.books", Author.class).list(); // Loads Author and their Books in one query for (Author author : authors) { System.out.println("Author: " + author.getName()); for (Book book : author.getBooks()) { System.out.println("- Book: " + book.getTitle()); } } session.close();The
DISTINCToperator prevents duplicate rows that can occur with one-to-many joins. -
Changing fetch type to EAGER: Change
fetch = FetchType.LAZY(default for collections) tofetch = FetchType.EAGER.@Entity public class Author { // ... other fields @OneToMany(mappedBy = "author", fetch = FetchType.EAGER) // EAGER fetch private List<Book> books; // ... getters and setters }Not recommended for collections or entities with many relations, as it can lead to loading excessive data and performance issues (Cartesian product effect). Suitable for ManyToOne/OneToOne relations where the related object is always needed.
-
Using
FetchModein Criteria API: Allows specifying how related entities are loaded.Session session = sessionFactory.openSession(); Criteria criteria = session.createCriteria(Author.class) .setFetchMode("books", FetchMode.JOIN); // Uses LEFT OUTER JOIN to load books List<Author> authors = criteria.list(); for (Author author : authors) { System.out.println("Author: " + author.getName()); for (Book book : author.getBooks()) { System.out.println("- Book: " + book.getTitle()); } } session.close(); -
Using
BatchSizeannotation: Specifies Hibernate to load related objects (or collections) in groups of a certain size, reducing the number of queries, but not to a single query.@Entity @BatchSize(size = 10) // Hibernate loads Authors in batches of 10 public class Author { // ... other fields @OneToMany(mappedBy = "author", fetch = FetchType.LAZY) @BatchSize(size = 10) // Hibernate loads books for Authors in batches of 10 private List<Book> books; // ... getters and setters }When iterating over the
bookscollection for the first author, Hibernate will loadbooksfor the next 9 authors as well (if they were loaded in the same session). This significantly reduces the number of queries compared to N+1. -
Using Entity Graphs: Explicitly define which related objects or collections should be loaded during a query.
@NamedEntityGraph(name = "author-with-books", attributeNodes = @NamedAttributeNode("books") ) @Entity public class Author { // ... fields and relationships }Session session = sessionFactory.openSession(); jakarta.persistence.EntityGraph<Author> entityGraph = session.createEntityGraph(Author.class); entityGraph.addAttributeNodes("books"); List<Author> authors = session.createQuery("SELECT a FROM Author a", Author.class) .setHint("jakarta.persistence.fetchgraph", entityGraph) // or fetchgraph depending on desired behavior .getResultList(); for (Author author : authors) { System.out.println("Author: " + author.getName()); for (Book book : author.getBooks()) { System.out.println("- Book: " + book.getTitle()); } } session.close();
The choice of specific solution depends on the context, the one-to-one/many-to-one/many-to-many relationships, data volume, and required flexibility. JOIN FETCH and Entity Graphs are often preferred for loading all related data in one query, while BatchSize is effective when working with a large number of entities and when JOIN FETCH results in too large datasets. EAGER loading should be used cautiously.