O Problema Silencioso de Performance: Como resolver o Problema do N+1 no Spring Data JPA
Sua API roda super rápido na sua máquina local, mas fica lento em produção? O log do console rola infinitamente com dezenas de SELECTs seguidos? Parabéns, você acabou de encontrar o Problema do N+1. Neste post, vamos entender como o FetchType.LAZY pode se virar contra você em listagens e como resolver esse gargalo de forma elegante usando JOIN FETCH e @EntityGraph.
Na Máquina Local
O cenário do N+1 acontece quando estamos construindo listagens. Imagine que você tem uma entidade Autor e uma entidade Livro (relacionamento @OneToMany).
Na sua máquina local, o banco de dados tem 5 autores. Você cria um endpoint para listar todos os autores e a quantidade de livros que cada um escreveu. A requisição bate 50ms. Você aprova o PR e faz o deploy.
Seis meses depois, o banco de produção tem 5.000 autores. De repente, a mesma tela de listagem demora longos segundos(ou minutos) para carregar, a CPU do banco de dados vai a 100% de processamento e o servidor cai. O que aconteceu?
O que é o Problema do N+1?
O problema acontece quando o framework faz 1 query principal para buscar a lista de pais, e depois faz N queries adicionais para buscar os filhos de cada um dos pais.
Olhe para este código no seu Service:
List<Autor> autores = autorRepository.findAll(); // 1 Query para trazer 5000 autores
for (Autor autor : autores) {
// Para CADA autor, o Hibernate vai no banco buscar os livros
int qtdLivros = autor.getLivros().size(); // N Queries!
}
Neste cenário de 5.000 autores, o seu Spring Boot executou 5.001 queries SQL para carregar apenas uma única tela. Isso acaba com o pool de conexões do banco de dados e gera uma latência de rede insustentável.
O FetchType.LAZY
A regra número um de performance no Hibernate é: Nunca use FetchType.EAGER. Todos os seus relacionamentos (@ManyToOne, @OneToMany) devem ser marcados como LAZY (Carregamento Preguiçoso).
O LAZY garante que os Livros só sejam buscados no banco se você explicitamente chamar o getLivros().
O problema é que, ao fazer um findAll() seguido de um .map(DTO::new) para converter os dados para o Front-end, você inevitavelmente acessa a lista de livros, ativando o gatilho preguiçoso milhares de vezes dentro do loop.
Precisamos avisar ao Spring Data: "Ei, eu sei que a relação é LAZY, mas nesta consulta específica, eu VOU precisar dos livros. Traga todos eles de uma vez em uma única ida ao banco."
Temos duas formas excelentes de fazer isso.
Solução 1: O JOIN FETCH (Via JPQL)
A forma mais tradicional e explícita de resolver o problema é escrevendo uma query HQL/JPQL customizada na sua interface do Repository. O segredo está no FETCH.
@Repository
public interface AutorRepository extends JpaRepository<Autor, Long> {
//ERRADO: Vai gerar N+1 se acessar a lista depois
@Query("SELECT a FROM Autor a")
List<Autor> buscarTodosComProblema();
//CERTO: O FETCH avisa o Hibernate para trazer tudo em 1 única query
@Query("SELECT a FROM Autor a JOIN FETCH a.livros")
List<Autor> buscarTodosTrazendoLivros();
}
Quando usamos o JOIN FETCH, o Hibernate monta uma query SQL gigante fazendo um INNER JOIN real, trazendo os dados do Autor e dos Livros em uma única viagem ao banco de dados e populando os objetos Java na memória.
Resultado: Em vez de 5.001 queries, você executa apenas 1 query.
Solução 2: O @EntityGraph (Sem escrever SQL)
Muitos desenvolvedores detestam escrever strings de JPQL nos Repositories, especialmente para operações simples. Para manter o código limpo, o Spring Data JPA suporta os Entity Graphs.
O @EntityGraph permite que você sobrescreva o comportamento LAZY pontualmente para um método específico do repositório, apenas passando o nome do atributo que você quer forçar o carregamento.
@Repository
public interface AutorRepository extends JpaRepository<Autor, Long> {
//CERTO e ELEGANTE: 1 única query sem escrever JPQL
@EntityGraph(attributePaths = {"livros"})
List<Autor> findAll();
// Você também pode usar com buscas por parâmetros
@EntityGraph(attributePaths = {"livros"})
List<Autor> findByNomeContaining(String nome);
}
O Spring Data pega essa anotação e injeta o LEFT OUTER JOIN FETCH automaticamente na consulta gerada por baixo dos panos. É limpo, é à prova de erros de digitação e mantém o seu repository totalmente padronizado.
Dica de Ouro: Ligue o log do SQL
Você só sabe que tem um problema de N+1 se estiver vigiando o que a sua API conversa com o banco. Nunca desenvolva localmente sem estas duas propriedades ligadas no seu application-dev.yml:
spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
Se ao acessar uma rota no seu Postman/Insomnia o seu console exibir dezenas de SELECTs rolando na tela, pare tudo. Você tem um vazamento de performance.
Conclusão
Resolver o problema do N+1 é um daqueles conhecimentos que separam os desenvolvedores Plenos/Sêniores dos Juniores. Bancos de dados são extremamente rápidos para retornar 10.000 linhas em uma única chamada, mas são incrivelmente lentos para responder 10.000 chamadas pedindo uma única linha cada.