Simplifique suas consultas com o Query By Example do Hibernate

Durante o desenvolvimento de uma aplicação, é comum precisarmos de um filtro por vários parâmetros ao mesmo tempo.

tela_pesquisa_muitos_campos

Algumas vezes, não sabemos a priori quais parâmetros devem ser considerados, e devemos ser capazes de executar a busca independente das informações que foram passadas.

Por exemplo, imagine que estamos desenvolvendo uma aplicação em que as pessoas podem fazer pedidos para restaurantes. Para isto, temos em nosso banco de dados duas tabelas, uma representando o restaurante e outra os tipos de comida.

Se estivermos trabalhando em uma aplicação Java e usando frameworks ORM como o Hibernate, teríamos os seguintes modelos:

@Entity
public class Restaurante {

  @Id
  @GeneratedValue
  private Long id;

  private String nome;

  private String endereço;

  @ManyToOne
  private TipoDeComida tipoDeComida;

  //Getters and Setters.
}

@Entity
public class TipoDeComida {

  @Id
  @GeneratedValue
  private Long id;

  private String nome;

  //Getters and Setters
}

Para que o usuário possa escolher o Restaurante, ele vai poder pesquisar pelo nome, endereço e o tipo de comida que lhe interessar.

Considerando que todos os parâmetros estejam sempre preenchidos, poderíamos simplesmente escrever uma HQL para realizar a pesquisa, dado um objeto do tipo Restaurante:

public List<Restaurante> busca(Restaurante restaurante) {
  String hql = "from Restaurante r where r.nome like :nome" +
    " and r.endereco like :endereco" +
    " and r.tipoDeComida.nome like :tipoDeComida";
  return session.createQuery(hql)
   .setParameter("nome", restaurante.getNome())
   .setParameter("endereco", restaurante.getEndereco())
   .setParameter("tipoDeComida", restaurante.getTipoDeComida().getNome())
   .list()
}

Mas e se o usuário não soubesse que tipo de comida escolher, ou não soubesse o nome do restaurante? Para isso, precisamos fazer uma consulta considerando que os campos do filtro são opcionais.

Precisamos então de diversas condições no nosso código que verificam se as informações foram preenchidas:

String hql = "from Restaurante r where 1=1"
if(! restaurante.getEndereco().equals("")) {
  hql += " and r.endereco like :endereco"
}

if(! restaurante.getNome().equals("")) {
  hql += " and r.nome like :nome"
}

if(! restaurante.getTipoDeComida().getNome().equals("")) {
  hql += " and r.tipoDeComida.nome like :tipoDeComida"
}
// ifs novamente para atribuir os parâmetros na query

Porém, para adicionar as condições na nossa query, estamos concatenando cada cláusula na string da HQL. Solução que evidentemente não é a mais adequada, pois além de deixar o código com diversos ifs, ainda é preciso tomar cuidado com espaços nas concatenações da HQL.

Para evitar toda essas concatenações, podemos utilizar a Criteria, do Hibernate. Com ela podemos simplesmente adicionar uma nova restrição caso a condição seja satisfeita.

Criteria criteria = session.createCriteria(Restaurante.class);

if(! restaurante.getEndereco().equals("")) {
  criteria.add(Restrictions.eq("r.endereco", restaurante.getEndereco()));
}

if(! restaurante.getNome().equals("")){
  criteria.add(Restrictions.eq("r.nome", restaurante.getNome()));
}

Essa é uma solução que resolve nosso problema, mas quantas linhas foram necessárias para fazer esta consulta? Mesmo utilizando o Criteria ainda são 3 linhas de código para cada cláusula, dado que para cada parâmetro do filtro teríamos que fazer um if para adicionar a query. Claramente, já é melhor do que a concatenação de HQLs, mas será que ainda não dá para ficar melhor? E se pudessemos resolver nosso problema com apenas uma linha de código?

Não seria muito melhor se apenas passássemos para o Hibernate uma instância do Restaurante, com as informações que vierem populadas e ele descobrisse quais informações devem ser consideradas no filtro? Justamente para isso o Hibernate possui uma funcionalidade chamada Query By Example. Desta forma, ao invés de criarmos várias condições e adicionar uma Restriction em cada uma, podemos simplesmente adicionar um Example da seguinte forma:

// Objeto restaurante populado, mas poderia vir direto do request pelo seu
// framework MVC
Restaurante restaurante = new Restaurante();
restaurante.setNome("Pico");
restaurante.setEndereco("Vergueiro");

TipoDeComida tipo = new TipoDeComida();
tipo.setNome("Contemporânea");

restaurante.setTipoDeComida(tipo);

// Query sendo feita
Example example = Example.create(restaurante);
List<Restaurante> restaurantes = s.createCriteria(Restaurante.class)
    .add(example).list();

Portanto, com apenas duas linhas (poderia ser uma linha se deixássemos a criação do Example inlined) conseguimos fazer a busca com base apenas nos campos preenchidos pelo nosso usuário. Ao executarmos essa Criteria, teremos o seguinte SQL gerado:

select
  this_.id as id1_2_1_,
  this_.endereco as endereco2_2_1_,
  this_.nome as nome3_2_1_,
  this_.tipoDeComida_id as tipoDeCo4_2_1_,
  tipodecomi2_.id as id1_3_0_,
  tipodecomi2_.nome as nome2_3_0_
from
  Restaurante this_
left outer join
  TipoDeComida tipodecomi2_
  on this_.tipoDeComida_id=tipodecomi2_.id
where
(
  this_.endereco=?
  and this_.nome=?
)

No entanto, executando o criteria acima é possível notar que todas as comparações feitas entre atributos foi com igual. Mas e se quisermos utilizar like? O Example permite que você configure a forma como ele fará as comparações. Para habilitar o like basta usar o método enableLike() e passar como parâmetro o MatchMode. Desta forma, se quisermos comparar string usando like e % no começo e fim de cada string, teríamos o seguinte código:

Example example = Example.create(restaurante).enableLike(MatchMode.ANYWHERE);

Executando novamente a query, veremos os likes sendo utilizados:

  this_.endereco like ?
  and this_.nome like ?

Assim, conseguimos realizar uma busca com campos opcionais para a classe Restaurante. Mas note que mesmo tendo adicionado as informações do tipo de comida dentro do objeto restaurante, ele está sendo ignorado no nosso filtro. O Example, por conta da Criteria, funciona sempre no nível do objeto pai (no caso o restaurante) e não desce para os filhos (no caso, o tipo de comida). Mas queremos deixar o tipo de comida como um campo do filtro.  Como adicionar esta restrição na nossa consulta?

Para resolver este problema basta usar a mesma ideia de quando é necessário adicionar restrições para um objeto de outro nível daquele considerado, ou seja, criar uma sub Criteria. Assim, depois de criar uma criteria para o tipo de comida basta adicionar um novo Example para esta instância. Portanto, temos o seguinte código:

Example exampleRestaurante = Example.create(restaurante)
    .enableLike(MatchMode.ANYWHERE);
Example exampleTipoDeComida = Example.create(restaurante.getTipoDeComida());

List<Restaurante> restaurantes = 
    session.createCriteria(Restaurante.class).add(exampleRestaurante)
      .createCriteria(“tipoDeComida”).add(exampleTipoDeComida)
      .list();

E agora nossa consulta vai ser executada com o where completo:

where
(
  this_.endereco like ?
  and this_.nome like ?
)
and (
  tipodecomi1_.nome=?
)

Quando uma informação não for preenchida, ela será ignorada pelo where, sem você precisar ficar programando ifs intermináveis. Essa é a grande vantagem do Query By Example. E você já usou esse recurso em seus projetos? Quais outras funcionalidades do Hibernate você gosta?

Esse e diversos outros recursos do Hibernate e da JPA você aprende no nosso Curso Persistência com JPA, Hibernate e EJB lite

Tags: ,

21 Comentários

  1. Kalil Peixoto 12/05/2015 at 10:53 #

    Márcio e Lucas, excelente post, realmente desconhecia essa possibilidade que o Hibernate provia. Quantos if´s eu já fiz e tive de manter por causa de filtros em queries, kkk!!

    Sabe me dizer se essa funcionalidade também existe para outras implementações, como EclipseLink ou TopLink?

    Abraço!!

  2. Alberto 12/05/2015 at 11:10 #

    O Example realmente é bem útil nessas situações. Evita um monte de erro besta enquanto montamos esse tipo de query. Muito bem explicado por sinal, parabéns!

  3. Icaro 12/05/2015 at 11:34 #

    Eu não vejo como uma boa ideia usar entities da aplicação como sendo examples para queries. A não ser que se use classes à parte, do contrário, por exemplo, atributos default ou qualquer modelagem de entidade pode ter side effect sobre consultas…

    O que eu costumo fazer é ter entidades de consulta, com as quais extendo anotações próprias para possibilitar varias coisas como: join fetchs, max results, ordering etc. No formato de uma API simples e de uso integrado ao Hibernate.

  4. Márcio Shibao 12/05/2015 at 11:57 #

    Existe sim Kalil, você pode usar o setExampleObject(), ficaria algo do tipo:
    ReadAllQuery query = new ReadAllQuery();
    Restaurante restaurante = new Restaurante();
    restaurante.setNome("Pico");
    query.setExampleObject(restaurante);

  5. Adriano Almeida 12/05/2015 at 13:00 #

    Oi @Icaro, realmente são abordagens distintas. Ao trabalhar com qualquer ferramenta de ORM é preciso ficar bem esperto com esses detalhes que você citou. Sua sugestão é uma abordagem bem mais segura, porém mais trabalhosa. É um tradeoff mesmo, com vantagens e desvantagens para os dois lados.

  6. Bruno 12/05/2015 at 13:07 #

    Excelente post, me ajudou muito. Obrigado!

  7. Flávio Almeida 12/05/2015 at 17:07 #

    São recursos atraentes como esse que torna ainda mais difícil a tarefa de usarmos apenas a casquinha da especificação JPA. Já usava este recurso antes com Hibernate, porém gostei bastante da forma que você o apresentou. Abraço!

  8. Ivan Lampert 13/05/2015 at 08:03 #

    Uma coisa que me incomoda constantemente (talvez seja apenas futilidade minha) é a questão de, os campos declarados na criteria, para composição da HQL serem ‘Strings’. Uma vez que, caso o atributo seja renomeado dentro da entidade, acarreta em refatoração nos pontos de uso na camada de persistência.

    ex.: “.createCriteria(“tipoDeComida”).add(exampleTipoDeComida)”; ao alterar na entidade ‘Restaurante’ o atributo ‘tipoDeComida’ para ‘tipoComida’, nos força a procurar por todos os pontos onde este atributo foi referenciado de forma Texto, e refatorá-lo para o novo nome.

    Alguém conhece alguma forma de referenciar o atributo na camada de persistência sem que seja por meio de Strings? Já me foi sugerido a hipótese de, dentro da Entidade, declarar constantes onde seria algo do tipo:
    ‘public static final String TIPO_COMIDA = “tipoDeComida”, para que, ao alterar o nome do atributo, seja necessário refatorar apenas a constante, uma vez que, a Constante que deva ser utilizada para as criterias.
    O que acha?

    Ps.: Ótima postagem; clara, simples, objetiva e abordada de uma maneira bem didática!

  9. Rafael 13/05/2015 at 08:24 #

    Icaro,interessante esse ponto que vc levantou, poderia exemplificar?

  10. Márcio Shibao 13/05/2015 at 10:06 #

    Oi @Ivan, no mundo Java o mais perto que conheço seria o metamodel que existe na JPA2. Em resumo ele cria uma classe com os atributos estáticos que podem ser refernciados nas suas querys, mais informações em https://docs.jboss.org/hibernate/entitymanager/3.5/reference/en/html/metamodel.html.
    Outra possibilidade é a IDE IntelliJ que pelo menos para hql ele consegue identificar a classe que está sendo referenciada na sua query e acusa erro de compilação se o atributo estiver escrito errado.

  11. Luciana Campello 13/05/2015 at 15:29 #

    Parabéns pelo post, muito útil!
    Tenho algumas dúvidas…
    Tem como fazer comparação com between ou usar funções agregadas como SUM ou COUNT?
    Tem como eu especificar para que a pesquisa seja por LIKE em apenas um atributo do meu objeto ou o LIKE tem que ser exatamente para todos os atributos que forem do tipo String?
    Mais uma vez parabéns!!!! 😀

  12. Lucas Takeshi 14/05/2015 at 15:06 #

    Oi @Luciana,
    aparentemente o Example não possui uma forma de criar comparações usando o between. Então ela tem que ser feita na Criteria através do método add, passando a restrição between como parâmetro. No entanto, como o Example cria comparações para todos os atributos que estão preenchidos, a consulta terá duas comparações diferentes para o mesmo campo. Para não ter este problema, o Example possui o método excludeProperty, no qual você passa como parâmetro um atributo que ele deve ignorar na hora de criar as comparações.
    O Example também não possui um suporte para funções agregadas como SUM e COUNT.
    Já as comparações com Strings, o Example acaba usando o LIKE para todos os atributos quando você faz um enableLike. Para especificar que apenas a comparação de um atributo seja feita usando o LIKE é necessário usar a mesma ideia do between, ou seja, adicionar a restrição direto da criteria com o add e ignorar o atributo no Example com o excludeProperty.

  13. Raphael 21/05/2015 at 02:27 #

    Eu já uso há algum tempo para casos simples e ajuda muito.

    Para casos mais complexo uso o criteria mesmo.

  14. Ricardo Johannsen 27/05/2015 at 10:40 #

    Muito bom, principalmente pra montar aquelas queries dinâmicas gigantescas

  15. Jardel 28/05/2015 at 02:27 #

    Parabéns pelo post amigo, eu não o conhecia o by example vou pesquisar mais a fundo para saber mais detalhe e começar a usar agora, muito simples e útil além de ser mais elegante =D

  16. Marcelo 02/06/2015 at 09:23 #

    Parabéns pelo post! Foi muito esclarecedor.

  17. Lucas Francisco 02/06/2015 at 12:05 #

    Excelente post, a didática de apresentar o benefício e uma melhor qualidade sempre diante de uma situação não tão elegante é marca registrada da Caelum 😉

  18. Djefferson william da silva 23/10/2015 at 17:42 #

    Ola @ivan, tem uma opção muito boa, que já venho usando a alguns anos que é o QueryDSL ( http://www.querydsl.com ), onde ele gera classes tipadas que representam suas entidades, porém com comportamentos de consulta, ex:

    QUsuario usuario = QUsuario.usuario;

    usuario.nome.eq(“asd”);
    usuario.nome.contains(“”);
    usuario.permissoes.in(new Permissa(1));

    E coisas do tipo, onde uma consulta fica algo assim :

    JPAQuery query = new JPAQuery(entityManager);
    List usuarios = query.from(QUsuario.usuario)
    .where(QUsuario.nome.like(“MariaTChaTChaTCha”)
    .and(QUsuario.dataNasc.lt(new Date()))).list();

    E coisas do tipo, é bem interessante e completo, e tem para sql’s nativos, para JPA, para LuceneSeach, HibernateSeach, possibilita a criação de consultas dinâmicas e uma infinidade de aplicações.

  19. Antonio Luciano Lima da Silva 14/03/2016 at 10:43 #

    De onde eu pego esse objeto session do exemplo da consulta HQL?

    Session session = this.manager.unwrap(Session.class);

    fiz assim, mas não esta dando certo, até faz a consulta, porem a minha tabela de dados fica vazia, e mesmo fazendo a pesquisa nos campos não obtém nem um resultado.

    Alguém tem uma ideia! Fico muito grato…

  20. Henrique Santana 18/10/2016 at 17:30 #

    Na linha
    List restaurantes = s.createCriteria(Restaurante.class).add(example).list();

    Você chama “s.createCriteria”, em qual momento você declarou este ‘s’?

  21. Cesar 21/11/2016 at 17:46 #

    Boa tarde. Parabéns pelo post, mas estou com um problema…

    Em uma tela JSF os campos que não são preenchidos retorna uma string vazia (“”), com isso o hibernate acaba colocando esse atributo na condição where. Sabe como posso resolver isso?
    Pesquisei e o máximo que cheguei foi nisso:

    Example example = Example.create(cat)
    .excludeZeroes() //exclude zero valued properties
    .excludeProperty(“color”) //exclude the property named “color”
    .ignoreCase() //perform case insensitive string comparisons
    .enableLike(); //use like for string comparisons
    List results = session.createCriteria(Cat.class).add(example)

    Não encontrei uma forma dele desconsiderar as strings vazias….

    Grato!

Deixe uma resposta