quarta-feira, 30 de abril de 2014

Cuidado ao realizar consultas com JOIN FETCH e WHERE no Hibernate

Olá, pessoal!

Gostaria hoje de falar um pouco sobre os cuidados que precisamos ter ao usar FETCH JOIN no JPA/Hibernate. Apesar de ser um recurso muito útil, a falta do seu completo entendimento pode trazer confusões e surpresas nada agradáveis. Mas que, para nossa sorte, podem ser facilmente contornadas.

Mas antes, uma introdução rápida sobre o que faz um JOIN FETCH em uma consulta. O JOIN FETCH é um recurso do JPA no qual permite que uma única consulta (em JPQL) à determinada entidade também traga outras entidades associadas. Podemos dizer que o JOIN FETCH é uma alternativa ao mapeamento entre entidades com o fetch em EAGER, tendo a grande vantagem de podermos escolher quando vamos aplicar este "EAGER" na entidade.

Vamos a um exemplo. Imagine que temos as entidades Pessoa e Endereco:

@Entity
public class Pessoa {
   @Id
   private Integer id;
   @Column
   private String nome;
   @OneToMany(mappedBy= "pessoa")
   private Set<Endereco> enderecos;
}

@Entity
public class Endereco {
   @Id
   private Integer id;
   @Column
   private String rua;
   @Column
   private Integer numero;
   @JoinColumn("pessoa_id")
   private Pessoa pessoa;
}

Explicação rápida: as entidades Pessoa e Endereço contém um atributo "id", identificador único de cada entidade1. A Pessoa pode ter mais de um Endereco cadastrado, e cada Endereco contém a chave estrangeira para a Pessoa que reside naquele Endereco.

Assim, com o FETCH JOIN, podemos fazer uma consulta na Pessoa que nos traga todos os Endereços dela de uma só vez! É para isto que ele é utilizado. Podemos fazer este JPQL desta maneira:

SELECT pessoa FROM Pessoa pessoa JOIN FETCH enderecos endereco

Agora, temos a vantagem de consultar as pessoas com os seus endereços, não precisando ir novamente no banco de dados para recuperar apenas os endereços!

Mas, se estendermos este exemplo para algo mais elaborado, temos que prestar atenção. Ao escrevermos o JPQL, temos que levar em conta um aspecto especial com o JOIN FETCH.

Digamos que agora você deseja trazer todas as pessoas com endereço no qual a rua contenha a palavra "Alameda". Em uma consulta SQL, escreveríamos:

SELECT * from Pessoa pessoa
JOIN Endereco endereco ON endereco.pessoa_id = pessoa.id
WHERE endereco.rua LIKE "%Alameda%"

Esta consulta traria todas as pessoas que tem endereço no qual a rua contém a palavra "Alameda".

Agora, com a facilidade do JPQL, você quer fazer o mesmo: trazer as Pessoas com Endereco onde a rua contém a palavra "Alameda", mas com todos os seus endereços carregados em uma única consulta, conforme o primeiro exemplo em JPQL. No final das contas, você só quer adicionar um inocente WHERE.

Deste modo, é natural pensar que o JPQL ficaria da maneira abaixo (estou abstraindo o setParameter(), para simplificar):

SELECT pessoa FROM Pessoa pessoa
JOIN FETCH enderecos endereco
WHERE endereco.rua LIKE "%Alameda%"

Esta consulta vai executar? Vai. Trará resultados? Trará. O resultado vai ser o que você queria? Provavelmente não!

Esta consulta irá trazer todas as Pessoas que tem Endereco que contenham a palavra "Alameda" na rua, contudo não irá trazer os demais endereços que não contenham "Alameda" para esta mesma pessoa! Por exemplo, se a Pessoa tiver um Endereco contendo "Alameda" e outro "Avenida", ele vai trazer a Pessoa apenas com o Endereco com "Alameda" no nome da rua.

O que fazer neste caso? Agora vem o "jump of the cat": o correto é fazer outro JOIN sem FETCH. Desta maneira:

SELECT pessoa FROM Pessoa pessoa
JOIN FETCH enderecos
JOIN enderecos endereco
WHERE endereco.rua LIKE "%Alameda%"

Agora a consulta é realizada com a condição no JOIN no qual não tem o FETCH associado. O JOIN com FETCH ficará encarregado de trazer as Pessoas com todos os seus Enderecos. Percebam que também removi o alias do "JOIN FETCH enderecos", que existia no JPQL anterior, pois não é necessário2.

1. Evite o uso de chaves compostas quando puder, pois elas complicam as consultas com o JPA. Se você não for o responsável pelo banco de dados, negocie com o DBA a criação de chaves simples nas tabelas, mesma aquelas que a chave composta pareça a solução correta para o banco, pois a criação de uma constraint ou unique index resolverá na maioria dos casos para o DBA.

2. Alguns frameworks JPA não permitem o alias para JOIN FETCH, exatamente para evitar este problema que ilustramos. O Hibernate permite, mas recomendo não utilizar alias com JOIN FETCH.

2 comentários :