java - Qual é a solução para o problema N+1 no JPA e no Hibernate?




design-patterns orm (3)

O problema

O problema de consulta N + 1 ocorre quando você esquece de buscar uma associação e precisa acessá-la.

Por exemplo, vamos supor que temos a seguinte consulta JPA:

List<PostComment> comments = entityManager.createQuery(
    "select pc " +
    "from PostComment pc " +
    "where pc.review = :review", PostComment.class)
.setParameter("review", review)
.getResultList();

Agora, se PostComment entidades PostComment e percorrermos a associação post :

for(PostComment comment : comments) {
    LOGGER.info("The post title is '{}'", comment.getPost().getTitle());
}

O Hibernate irá gerar as seguintes instruções SQL:

SELECT pc.id AS id1_1_, pc.post_id AS post_id3_1_, pc.review AS review2_1_
FROM   post_comment pc
WHERE  pc.review = 'Excellent!'

INFO - Loaded 3 comments

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 1

INFO - The post title is 'Post nr. 1'

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 2

INFO - The post title is 'Post nr. 2'

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 3

INFO - The post title is 'Post nr. 3'

É assim que o problema de consulta N + 1 é gerado.

Como a associação de post não é inicializada ao buscar as entidades PostComment , o Hibernate deve buscar a entidade Post com uma consulta secundária e, para PostComment entidades N PostComment , mais N consultas serão executadas (daí o problema de consulta N + 1).

O conserto

A primeira coisa que você precisa fazer para solucionar esse problema é adicionar o registro e o monitoramento SQL adequados . Sem o registro, você não notará o problema de consulta N + 1 ao desenvolver um determinado recurso.

Segundo, para corrigi-lo, você pode simplesmente JUNTAR-SE À busca do relacionamento que está causando esse problema:

List<PostComment> comments = entityManager.createQuery(
    "select pc " +
    "from PostComment pc " +
    "join fetch pc.post p " +
    "where pc.review = :review", PostComment.class)
.setParameter("review", review)
.getResultList();

Se você precisar buscar várias associações filho, é melhor buscar uma coleção na consulta inicial e a segunda com uma consulta SQL secundária.

É melhor que esse problema seja detectado pelos testes de integração. Você pode usar uma declaração JUnit automática para validar a contagem esperada de instruções SQL geradas . O projeto db-util já fornece essa funcionalidade, é de código aberto e a dependência está disponível no Maven Central.

Entendo que o problema N + 1 é onde uma consulta é executada para buscar N registros e N consultas para buscar alguns registros relacionais.

Mas como isso pode ser evitado no Hibernate?


A solução nativa para 1 + N no Hibernate é chamada:

20.1.5 Usando busca em lote

Usando a busca em lote, o Hibernate pode carregar vários proxies não inicializados se um proxy for acessado. A busca em lote é uma otimização da estratégia de busca lenta e seletiva. Existem duas maneiras de configurar a busca em lote: no 1) nível de classe e no 2) nível de coleção ...

Verifique estas perguntas e respostas:

  • @BatchSize, mas muitos ida e volta no caso @ManyToOne
  • Evitando a busca ansiosa de n + 1 da associação de elementos de coleção filho

Com anotações, podemos fazer o seguinte:

Um nível de class :

@Entity
@BatchSize(size=25)
@Table(...
public class MyEntity implements java.io.Serializable {...

Um nível de collection :

@OneToMany(fetch = FetchType.LAZY...)
@BatchSize(size=25)
public Set<MyEntity> getMyColl() 

O carregamento lento e a coleta em lote representam otimização, que:

  • não requer nenhuma busca explícita em nossas consultas
  • será aplicado a qualquer quantidade de referências que sejam tocadas (preguiçosamente) após o carregamento da entidade raiz (enquanto a busca explícita afeta apenas os nomes mencionados na consulta)
  • resolverá o problema 1 + N com coleções (porque apenas uma coleção poderia ser buscada com consulta raiz) sem a necessidade de processamento adicional Para obter valores de raiz DISTINCT (verifique: Critérios.DISTINCT_ROOT_ENTITY vs Projections.distinct )

Você pode até fazê-lo funcionar sem precisar adicionar a anotação @BatchSize qualquer lugar, basta definir a propriedade hibernate.default_batch_fetch_size com o valor desejado para ativar a busca em lote globalmente. Veja a documentação do Hibernate para detalhes.

Enquanto você estiver nisso, provavelmente também desejará alterar o BatchFetchStyle , porque o padrão ( LEGACY ) provavelmente não é o que você deseja. Portanto, uma configuração completa para permitir a busca em lote globalmente seria assim:

hibernate.batch_fetch_style=PADDED
hibernate.default_batch_fetch_size=25

Além disso, estou surpreso que uma das soluções propostas envolva a busca de junções. A busca de junção raramente é desejável porque faz com que mais dados sejam transferidos a cada linha de resultado, mesmo que a entidade dependente já tenha sido carregada no cache L1 ou L2. Portanto, eu recomendaria desabilitá-lo completamente, definindo

hibernate.max_fetch_depth=0




orm