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:
- É feita uma consulta para obter todos os
Author. - Depois, num ciclo, para cada
Author, é feita uma consulta separada para carregar a coleçãobooks. Se houver 100 autores, serão feitas 100 consultas adicionais à tabelaBook. No total, 1 (para autores) + 100 (para livros) = 101 consultas.
Soluções para o problema N+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. -
Alterar o tipo de carregamento para EAGER: Alterar
fetch = FetchType.LAZY(padrão para coleções) parafetch = 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.
-
Usar
FetchModena 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(); -
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
booksdo primeiro autor, o Hibernate carregarábookspara 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. -
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.