Sobes.tech
Middle+

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:

  1. One query retrieves all Author objects.
  2. Then, for each Author, a separate query loads the books collection. If there are 100 authors, 100 additional queries are executed for Book. Total: 1 (for authors) + 100 (for books) = 101 queries.

Solutions to the N+1 problem:

  1. 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 DISTINCT operator prevents duplicate rows that can occur with one-to-many joins.

  2. Changing fetch type to EAGER: Change fetch = FetchType.LAZY (default for collections) to fetch = 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.

  3. Using FetchMode in 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();
    
  4. Using BatchSize annotation: 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 books collection for the first author, Hibernate will load books for 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.

  5. 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.