Skip to content

Instantly share code, notes, and snippets.

@massahud
Last active May 17, 2018 17:12
Show Gist options
  • Save massahud/11399909 to your computer and use it in GitHub Desktop.
Save massahud/11399909 to your computer and use it in GitHub Desktop.
JPA BATCH E JOIN FETCH

JPA Batch e Join Fetch

Quando uma entidade possui relacionamentos no JPA e a entidade do relacionamento é acionada, o provedor de persistência executa um select no banco. Isso leva a um problema conhecido como ORM n+1, que ocorre quando n entidades é obtida e para cada entidade da lista precisamos acessar um relacionamento, o JPA executará a um select para obter a lista, e para cada elemento da lista executará um select, totalizando n+1 selects no banco. Este é um motivo de lentidão de páginas JSF que contém tabelas e listas que deveriam ser simples.

Colocar o relacionamento como EAGER não é uma solução correta, pois EAGER apenas informa que o relacionamento deve ser carregado, podendo o provedor fazer um novo select para cada relacionamento EAGER de uma entidade.

Existem duas soluções comuns implementadas em provedores de persistência para resolver este problema, JOIN FETCH e BATCH FETCH, que serão explicadas abaixo.

JOIN FETCH

Join fetch faz exatamente o que seu nome indica, o JOIN das tabelas da entidade e deu seu relacionamento, o JPA possui suporte a JOIN FETCH diretamente no JPQL, através da sintaxe JOIN FETCH.

No exemplo abaixo as entidades empregado retornadas terão seu relacionamento empresa já carregados.

SELECT e FROM Empregado e JOIN FETCH e.empresa

Quando o relacionamento pode ser nulo, deve-se utilizar LEFT JOIN FETCH, senão apenas as entidades que possuírem o relacionamento não nulo serão retornadas.

O join fetch tabém pode ser executado via query hints, específicas para cada provedor. No caso do eclipselink, a query hint é eclipselink.join-fetch, que também pode ser acessada via a propriedade estática org.eclipse.persistence.config.QueryHints.JOIN_FETCH. Isso permite que uma query JPQL não necessariamente sempre faça os mesmos fetchs, dando liberdado para o usuário da query fazer fetch apenas do que precisar.

O exemplo acima ficaria da seguinte forma utilizando query hints no eclipselink:

TypedQuery<Empregado> query = em.createQuery("SELECT e FROM Empregado e", 
    Empregado.class);
query.setHint(QueryHints.JOIN_FETCH, "e.empresa");

No eclipselink os relacionamentos podem ser anotados com @JoinFetch para que seja sempre realizado join fetch.

BATCH FETCH

Batch fetch é outra solução para o problema ORM N+1, a diferença é que o provedor não realiza joins no select principal, ele faz outros selects para obter todos os relacionamentos em batch. Para cada batch configurado, um select extra é realizado.

Isso resolve um problema que ocorre com JOIN FETCHs, que é a obtenção de dados repetidos no join. A query da seleção de empregados do exemplo anterior retornaria uma empresa para cada empregado, no entanto a quantidade de empregados é ordens de grandeza maior que a de empresas, então a mesma empresa seria retornaria repetidas vezes, uma para cada empregado que trabalhe nela. Com o batch, primeiro o provedor obtém os empregados, e em seguida as empresas dos empregados, num select que retornará no máximo o número de empresas, podendo inclusive obtê-las do cache, dependendo de como foi implementado o batch no provedor.

O batch não é padrão JPA, mas o eclipselink o implementa através da query hint eclipselink.batch (QueryHints.BATCH) ou da anotação @BatchFetch nos relacionamentos.

@Entity
class Empregado {
...
@ManyToOne
@JoinColumn(name="EMPR_ID")
@BatchFetch
private Empresa empresa;
...

ou

TypedQuery<Empregado> query = em.createQuery("SELECT e FROM Empregado e WHERE e.idade > 30", 
    Empregado.class);
query.setHint(QueryHints.BATCH, "e.empresa");

Tipos

Existem 3 tipos de batch fetch, que são definidos pela hint QueryHints.BATCH_TYPE ou como valor da anotação @BatchFetch

  • JOIN: é o padrão, realiza a mesma consulta da lista fazendo join com a tabela das entidades do relacionamento.
    O select anterior se tornaria dois selects da seguinte forma em SQL:
SELECT e.* FROM EMPREGADO e WHERE e.IDADE > 30;

SELECT distinct emp.* FROM EMPRESA emp, EMPREGADO e WHERE emp.ID = e.EMPR_IR AND e.IDADE > 30;
  • EXISTS: utiliza exists no select para obter as entidades do relacionamento. A diferença do EXISTS ocorre em relacionamentos ManyToOne, onde o JOIN utiliza DISTINCT para obter apenas as entidades, e no EXISTS não é necessário o DISTINCT.
SELECT e.* FROM EMPREGADO e WHERE e.IDADE > 30;

SELECT emp.* FROM EMPRESA emp WHERE EXISTS 
(SELECT e.EMPRE_ID FROM EMPREGADO e WHERE emp.ID = e.EMPRE_ID AND e.IDADE > 30);
  • IN: o IN utiliza o operador IN para obter os dados. Ele possui um limite de ids que pode utilizar por select, dividindo a query em várias queries até obter todos as entidades necessárias. O tamanho padrão é 256 no eclipselink, e pode ser modificado pela hint QueryHints.BATCH_SIZE ou pelo atributo size da anotação @BatchFecth.
SELECT e.* FROM EMPREGADO e WHERE e.IDADE > 30;

SELECT distinct emp.* FROM EMPRESA emp WHERE emp.ID IN (1, 2, 3, 4, ..., 256);
SELECT distinct emp.* FROM EMPRESA emp WHERE emp.ID IN (257, 258, 259, ...);

Qual tipo utilizar

  • Para queries médias não paginadas, não existe muita diferença de desempenho em utilizar JOIN ou EXISTS, e o IN depende da quantidade de linhas que a query retornar e do BATCH_SIZE.

  • Para queries que retornam um número muito grande de dados não paginados, em relacionamentos ManyToOne o EXISTS parece ser um pouco mais eficiente que JOIN por não utilizar distinct, e o IN não é recomendado.

  • Já para queries paginadas, o IN passa a ser a opção recomandada, pois em queries paginadas JOIN e EXISTS não fazem paginação em seus selects, portanto queries muito pesadas podem acabar sendo executadas para se obter apenas a quantidade de entidades relacionadas à paginação. Os SQLS anteriores ficariam da seguinte forma se a query fosse paginada obtendo apenas os primeiros 10 itens:

JOIN:

SELECT * FROM (SELECT s.*, ROWNUM rnum FROM
(SELECT e.* FROM EMPREGADO e WHERE e.IDADE > 30) WHERE ROWNUM <= 10)
WHERE rnum > 0;

SELECT distinct emp.* FROM EMPRESA emp, EMPREGADO e WHERE emp.ID = e.EMPR_IR AND e.IDADE > 30;

EXISTS:

SELECT * FROM (SELECT s.*, ROWNUM rnum FROM
(SELECT e.* FROM EMPREGADO e WHERE e.IDADE > 30) WHERE ROWNUM <= 10)
WHERE rnum > 0;

SELECT emp.* FROM EMPRESA emp WHERE EXISTS 
(SELECT e.EMPRE_ID FROM EMPREGADO e WHERE emp.ID = e.EMPRE_ID AND e.IDADE > 30);

IN:

SELECT * FROM (SELECT s.*, ROWNUM rnum FROM
(SELECT e.* FROM EMPREGADO e WHERE e.IDADE > 30) WHERE ROWNUM <= 10)
WHERE rnum > 0;

SELECT distinct emp.* FROM EMPRESA emp WHERE emp.ID IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

Como se pode ver, em **JOIN **e EXISTS a query do fetch retornará todas as empresas para utilizar apenas as empresas dos 10 empregados paginados, já o IN limita o retorno a apenas as empresas dos 10 empregados paginados.

Dicas

  • Relacionamentos OneToOne são EAGER por padrão no eclipselink, portanto ele executa automaticamente um select para cada relacionamento OneToOne de cada entidade de uma lista de entidades retornadas. Anotar relacionamentos OneToOne com @BatchFetch/@JoinFetch ou adicionar utiizar as hints para ele pode ajudar a resolver problemas de desempenho em queries que retornam muitas entidades.

  • JOIN FETCH (não confundir com BATCH FETCH do tipo JOIN) deve ser utilizado com cautela. Se muitos relacionamentos existirem poderá ficar mais lento que executar as N+1 queries sem utilizar FETCH, pois cada join aumenta a complexidade da query, uma mesma entidade pode ser retornada repetidas vezes, e ele utiliza outer joins para cada relacionamento não obrigatório.

  • É possível fazer fetch de atributos aninhados usando a notação de ponto. Por exemplo, para um empregado pré carregar o endereço de sua empresa:

TypedQuery<Empregado> query = em.createQuery("SELECT emp from Empregado emp", Empregado.class);
query.setHint(QueryHints.BATCH, "emp.empresa.endereco");
  • Tanto JOIN FETCH quanto BATCH podem ser aplicados para vários relacionamentos em uma mesma query, basta executar Query.setHint uma vez para cada relacionamento:
query.setHint(QueryHints.BATCH, "emp.empresa.endereco");
query.setHint(QueryHints.BATCH, "emp.gerente");

Referências e links

@nosrednawall
Copy link

Muito bom, valeu

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment