Sobes.tech
Middle+

O que é o problema N+1 no Hibernate e como pode ser resolvido?

sobes.tech IA

Resposta da IA

O problema N+1 no Hibernate ocorre ao consultar dados, quando uma consulta é feita para obter os objetos pai, e depois, para cada objeto filho (ou coleção de objetos filhos), uma consulta separada é feita. Isto leva a N+1 consultas à base de dados, onde N é o número de objetos pai, em vez de uma consulta ótima ou algumas poucas consultas com junções.

Exemplo de cenário com o problema N+1:

Suponha que existam classes Author e Book, onde um autor pode ter muitos livros.

// Entidade Author
@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "author", fetch = FetchType.LAZY) // Fetch LAZY por padrão para coleções
    private List<Book> books;

    // getters e setters
}

// Entidade Book
@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;

    @ManyToOne
    private Author author;

    // getters e setters
}

Se quisermos mostrar todos os autores e os títulos dos seus livros:

Session session = sessionFactory.openSession();
List<Author> authors = session.createQuery("FROM Author", Author.class).list(); // 1 consulta para Author

for (Author author : authors) {
    System.out.println("Author: " + author.getName());
    for (Book book : author.getBooks()) { // N consultas para Book (uma por autor)
        System.out.println("- Book: " + book.getTitle());
    }
}
session.close();

Neste exemplo:

  1. É feita uma consulta para obter todos os Author.
  2. Depois, num ciclo, para cada Author, é feita uma consulta separada para carregar a coleção books. Se houver 100 autores, serão feitas 100 consultas adicionais à tabela Book. No total, 1 (para autores) + 100 (para livros) = 101 consultas.

Soluções para o problema N+1:

  1. Usar JOIN FETCH em JPQL/HQL: Carrega explicitamente as entidades relacionadas numa única consulta.

    Session session = sessionFactory.openSession();
    List<Author> authors = session.createQuery("SELECT DISTINCT a FROM Author a JOIN FETCH a.books", Author.class).list(); // Carrega Author e seus Books numa única consulta
    
    for (Author author : authors) {
        System.out.println("Author: " + author.getName());
        for (Book book : author.getBooks()) {
            System.out.println("- Book: " + book.getTitle());
        }
    }
    session.close();
    

    A cláusula DISTINCT é usada para evitar a duplicação de linhas no resultado, que pode ocorrer com joins um-para-muitos.

  2. Alterar o tipo de carregamento para EAGER: Alterar fetch = FetchType.LAZY (padrão para coleções) para fetch = FetchType.EAGER.

    @Entity
    public class Author {
        // ... outros campos
        @OneToMany(mappedBy = "author", fetch = FetchType.EAGER) // carregamento EAGER
        private List<Book> books;
        // ... getters e setters
    }
    

    Não recomendado para coleções ou entidades com muitas ligações, pois pode levar ao carregamento de dados redundantes e problemas de desempenho (efeito "produto cartesiano"). É adequado para ligações ManyToOne/OneToOne, onde se espera que o objeto relacionado seja sempre necessário.

  3. Usar FetchMode na Criteria API: Permite indicar como as entidades relacionadas devem ser carregadas.

    Session session = sessionFactory.openSession();
    Criteria criteria = session.createCriteria(Author.class)
        .setFetchMode("books", FetchMode.JOIN); // Usa LEFT OUTER JOIN para carregar 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. Usar a anotação BatchSize: Indica ao Hibernate que carregue as entidades relacionadas (ou coleções) em grupos de um tamanho definido, reduzindo o número de consultas, embora não as reduza a uma única.

    @Entity
    @BatchSize(size = 10) // Hibernate carregará os Author em lotes de 10
    public class Author {
        // ... outros campos
        @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
        @BatchSize(size = 10) // Hibernate carregará os books em lotes de 10 por Author
        private List<Book> books;
        // ... getters e setters
    }
    

    Ao percorrer a coleção books do primeiro autor, o Hibernate carregará books para os próximos 9 autores (se estiverem carregados na mesma sessão). Isto reduz significativamente o número de consultas em comparação com N+1.

  5. Usar Entity Graphs: Permitem definir explicitamente quais objetos relacionados ou coleções devem ser carregados numa consulta.

    @NamedEntityGraph(name = "author-with-books",
        attributeNodes = @NamedAttributeNode("books")
    )
    @Entity
    public class Author {
        // ... campos e relações
    }
    
    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) // ou fetchgraph dependendo do comportamento desejado
        .getResultList();
    
    for (Author author : authors) {
        System.out.println("Author: " + author.getName());
        for (Book book : author.getBooks()) {
            System.out.println("- Book: " + book.getTitle());
        }
    }
    session.close();
    

A escolha da solução específica depende do contexto, da relação um-para-um/um-para-muitos/muitos-para-muitos, do volume de dados e da flexibilidade requerida. JOIN FETCH e os Entity Graphs são frequentemente preferidos para carregar todos os dados relacionados numa única consulta, enquanto BatchSize é eficiente ao trabalhar com muitas entidades e pode ser útil quando JOIN FETCH gera resultados demasiado grandes. A carga EAGER deve ser usada com cautela.