Consultas fortemente tipadas com JPA

Considere que temos uma classe Venda, que é uma entidade JPA, conforme a seguir:

@Entity
public class Venda {
  @Id @GeneratedValue
  private Integer id;

  private Double valor;

  //getters e setters...
}

Também considere que temos um classe VendaDAO com uma consulta para listar e outra para somar o valor total das vendas, utilizando JPQL:

public class VendaDAO {

  @PersistenceContext
  private EntityManager em;

  public List<Venda> lista(){
    Query query = em.createQuery("select v from Venda v");
    return query.getResultList();
  }

  public Double total(){
    Query query = em.createQuery("select sum(v.valor) from Venda v");
    return (Double) query.getSingleResult();
  }

Trata-se de um código bem comum para quem já utilizou JPA. Mas as linhas destacadas no código de VendaDAO tem alguns problemas: há um warning na última linha do método lista e tivemos que fazer um cast no método total.

Porque warning e cast?

No método lista de VendaDAO, o compilador gera um warning com uma mensagem como: “The expression of type List needs unchecked conversion to conform to List<Venda>”. Acontece que o método getResultList de Query retorna uma List, que pode conter qualquer tipo de objeto. Porém, definimos o retorno como List<Venda> e, pelo mecanismo de type-erasure, não há como garantir em tempo de compilação que a lista retornada conterá apenas objetos do tipo Venda. O compilador então nos avisa de um trecho de código que poderá gerar um possível erro em tempo de execução. Poderíamos colocar @SupressWarning("unchecked") no método ou na classe mas isso não resolve o problema, apenas omite o aviso do compilador.

Já no método total de VendaDAO, tivemos que fazer um cast porque o método getSingleResult de Query retorna um Object e devemos retornar um Double. Com o cast, assumimos o risco do objeto retornado não ser do tipo Double, podendo gerar um ClassCastException em tempo de execução.

Java é uma linguagem fortemente tipada, com declarações explícitas dos tipos de variáveis e atributos. Mas as consultas com JPQL nos deixam na mão…

Usando TypedQuery

Desde o JPA 2.0, há uma maneira de evitar warnings ao utilizar getResultList e casts ao utilizar getSingleResult. Podemos passar a classe esperada ao invocarmos o createQuery, de maneira a obter um objeto do tipo TypedQuery.

public class VendaDAO {

  @PersistenceContext
  private EntityManager em;

  public List<Venda> lista(){
    TypedQuery<Venda> query = em.createQuery("select v from Venda v", Venda.class);
    return query.getResultList();
  }

  public Double total(){
    TypedQuery<Double> query = em.createQuery("select sum(v.valor) from Venda v", Double.class);
    return query.getSingleResult();
  }

O código acima não teria warnings nem necessitaria mais de casts. Mas será que é fortemente tipado? Não.

Não há checagem em tempo de compilação dos tipos retornados pelas consultas. Só descobriremos o retorno real em tempo de execução.

No fim das contas, o JPQL é uma String. Podemos errar o nome dos atributos, o nome das classes e até mesmo a sintaxe do JPQL. E só descobriremos esses erros em tempo de execução.

Algo fortemente tipado de verdade resistiria a refatorações como renomear classes e mudar o tipo dos atributos.

Usando Criteria

Desde o JPA 2.0, há a API de Criteria. Comumente, essa API é utilizada para fazer filtros dinâmicos.

Porém, a grande vantagem da Criteria do JPA 2.0 é nos impedir de criar consultas com erros de sintaxe, através de checagem em tempo de compilação. Ao invés de definir as consultas em uma String, temos uma definição programática, com código Java.

Para utilizar a API de Criteria, o primeiro passo é obter um objeto do tipo CriteriaBuilder a partir do EntityManager:

CriteriaBuilder builder = em.getCriteriaBuilder();

Então, utilizamos o CriteriaBuilder para criar um objeto do CriteriaQuery, passando o retorno esperado da nossa consulta. Para o cálculo do total vendido, seria algo como:

CriteriaQuery<Double> criteria = builder.createQuery(Double.class);

A partir do CriteriaQuery, devemos chamar o método from passando a classe da entidade que queremos consultar:

Root<Venda> root = criteria.from(Venda.class);

A chamada do método from retorno um objeto do tipo Root<Venda>, que representa a raiz da nossa consulta, equivalente ao alias v em from Venda v de um JPQL. Podemos usá-lo como ponto de partida para acessar atributos da entidade:

Path<Double> valor = root.get("valor");

O código root.get("valor") seria equivalente a v.valor em um JPQL. Podemos passar o objeto Path<Double> retornado para o método sum do CriteriaBuilder, que define a função de soma:

Expression<Double> soma = builder.sum(valor);

É retornado um objeto do tipo Expression<Double>, que devemos passar para o método select de nosso CriteriaQuery.

criteria.select(soma);

Finalmente, podemos criar um TypedQuery<Double> a partir do CriteriaQuery, usando-o para obter o resultado através do método getSingleResult.

TypedQuery<Double> query = em.createQuery(criteria);
Double total = query.getSingleResult();

Juntando o código todo, teríamos:

CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Double> criteria = builder.createQuery(Double.class);
Root<Venda> root = criteria.from(Venda.class);
Path<Double> valor = root.get("valor");
Expression<Double> soma = builder.sum(valor);
criteria.select(soma);
TypedQuery<Double> query = em.createQuery(criteria);
Double total = query.getSingleResult();

O código ficou bem mais complicado. A vantagem é que teríamos uma consulta fortemente tipada. Mas será que é mesmo? Quase…

Como não definimos a consulta em uma String, não conseguiríamos cometer erros de sintaxe como trocar from por form ou select por sletc. Até aí tudo bem.

Renomear a classe Venda para Negociacao, por exemplo, não seria um problema, já que nossa IDE provavelmente trocaria sem grandes problemas o código criteria.from(Venda.class) por criteria.from(Negociacao.class).

O principal problema acontece ao mudarmos o nome ou o tipo de atributos. Não teríamos um erro de compilação. Só descobriremos possíveis erros quando alguém realmente executar aquele trecho de código. Portanto, o acesso aos atributos não é fortemente tipado.

Repare no trecho de código a seguir:

Path<Double> valor = root.get("valor");

Há ainda uma String com o nome do atributo valor. Ao renomearmos o atributo, teríamos que lembrar de mudar esse trecho de código específico. Além disso, o tipo de Path<Double> teve que ser colocado manualmente. Uma mudança do tipo do atributo para BigDecimal não geraria nenhum erro de compilação. E a IDE também não nos ajudaria.

Precisamos de alguma maneira de saber o nome e o tipo dos atributos de uma classe de maneira fortemente tipada…

Static Metamodel

Associada à API de Criteria, a especificação JPA 2.0 definiu o Static Metamodel, uma maneira fortemente tipada de recuperar informações de atributos de classes anotadas com @Entity.

Para cada entidade, é criada uma classe associada que contém informações sobre o tipo dos atributos, tudo acessível maneira estática. Por exemplo, para a entidade Venda será criada automaticamente uma classe Venda_, no mesmo pacote, com um conteúdo parecido com:

@Generated(value="Dali", date="2015-10-02T16:06:29.157-0300")
@StaticMetamodel(Venda.class)
public class Venda_ {
	public static volatile SingularAttribute<Movimentacao, Integer> id;
	public static volatile SingularAttribute<Movimentacao, Double> valor;
}

Essas classes associadas, chamadas de metamodelo estático, são geradas por meio de processamento de anotações. As implementações de JPA como Hibernate, EclipseLink e OpenJPA devem fornecer classes processadoras que gerem esse código.

IDEs como o Eclipse e NetBeans e ferramentas de automação como Maven, Ant e Gradle possuem maneiras de regerar automaticamente essas classes sempre que alguma entidade for modificada. No Eclipse, por exemplo, há o Dali, um subprojeto do plugin WTP que faz esse processamento das entidades. Para habilitá-lo, basta que o Project Facet do JPA seja usado e que seja configurado o Source Folder das classes em Properties -> JPA -> Canonical Metamodel.

Usando o Static Metamodel com uma Criteria

O trecho do código anterior que não estava fortemente tipado era aquele em que usávamos o atributo valor da classe Venda através de uma String. Usando a classe Venda_, o trecho ficaria algo como:

Path<Double> valor = root.get(Venda_.valor);

Se eventualmente mudarmos o tipo do atributo valor para BigDecimal, esse trecho de código iria logo falhar com um erro de compilação como “Type mismatch: cannot convert from Path<BigDecimal> to Path<Double>”, indicando que o tipo do Path está incorreto.

Caso mudássemos o nome do atributo para quantia, imediatamente teríamos um erro de compilação como “valor cannot be resolved or is not a field in type Venda_”, indicando que o metamodelo estático da entidade Venda já não tem mais o atributo valor.

Considerações finais

A nova versão da classe VendaDAO, usando Criteria com metamodelo estático e com variáveis intermediárias omitidas, ficaria algo como:

public class VendaDAO {

  @PersistenceContext
  private EntityManager em;

  public List<Venda> lista(){
    CriteriaBuilder builder = em.getCriteriaBuilder();
    CriteriaQuery<Venda> criteria = builder.createQuery(Venda.class);
    criteria.from(Venda.class);
    return em.createQuery(criteria).getResultList();
  }

  public Double total(){
    CriteriaBuilder builder = em.getCriteriaBuilder();
    CriteriaQuery<Double> criteria = builder.createQuery(Double.class);
    Root<Venda> root = criteria.from(Venda.class);
    criteria.select(builder.sum(root.get(Venda_.valor)));
    return em.createQuery(criteria).getSingleResult();
  }

Sem warnings nem casts, resistente a erros de sintaxe e a refatorações. Porém, um código mais complexo e um pouco maior. E as consultas são bem simples…

A grande questão é saber se, para seu projeto e sua equipe, as vantagens de ter consultas fortemente tipadas compensam a complexidade da API de Criteria.

Esse e outros recursos do JPA são apresentados no curso Persistência com JPA, Hibernate e EJB lite.

E você? Já usou a API Criteria do JPA com o Static Metamodel? Como foi a experiência?

Tags: , ,

13 Comentários

  1. Rafael Rossignol 20/10/2015 at 11:21 #

    Muito legal o artigo, eu sempre vi o criteria por aí e nunca tive vontade de usar, acredito que o código fica muito grande e muito mais complexo de entender do que o JPQL.
    No mundo ideal o JPA faria parte da especificação padrão do java e as NamedQueries deveriam ser validadas em tempo de compilação. Sem artificios como o tal do StaticMetamodel (que é até interessante, mas…)

  2. Alexandre Aquiles 20/10/2015 at 13:55 #

    Rafael, realmente… O uso da API de Criteria do JPA deixa o código bem complicado, né? Acho que, por isso, muita gente não usa no dia-a-dia.

  3. Fabricio Vallim 20/10/2015 at 16:11 #

    Parabéns pelo artigo Alexandre.

    A grande maioria dos projetos que participei optava pela Criteria do Hibernate devido à verbosidade imposta pela API de Criteria da JPA. Vejo o uso da Criteria válido somente para atender consultas dinâmicas. Em outros cenários, as Named Queries resolvem bem. Como vocês fazem essa escolha?

  4. Alexandre Aquiles 20/10/2015 at 17:11 #

    Fabricio,

    Realmente, a Criteria do Hibernate é bem mais fácil de usar! O conceito por trás da Criteria do JPA foi criar consultas type-safe. Ideia louvável, API nem tanto… Ficou difícil de usar.

    No dia-a-dia, faço o mesmo que você! Acabo usando mais Named Queries e até mesmo queries baseadas em Strings. Para consultas dinâmicas, depende do projeto. Se resolvermos arcar com o (pequeno) risco de nos atrelarmos ao Hibernate, acho muito válido usar a Criteria do Hibernate! O importante é ter em mente que esse ponto pode dificultar a migração para outro servidor de aplicação ou implementação do JPA. Tem que ser uma escolha e não uma surpresa…

  5. Daniel 20/10/2015 at 19:46 #

    Muito bom artigo, apesar de não concordar com exagero de tipagem.

    Na minha opinião é totalmente impraticável e desnecessário o uso do Criteria e ainda mais com Static Metamodel.
    Olhando para query “select sum(valor) from Venda” é simples claro e objetivo qual é o intuito da query, basta bater o olho e saber do que se trata. Usa só o TypedQuery que já ajuda bastante.

    Já com Criteria, vira um código com muitas linhas que perde toda legibilidade de uma leitura simples. Com query mais complexas então, fica pior.
    E com Static Metamodel, toda e qualquer alteração nas entidades tem que recriar as classes. Imagina no começo de um novo sistema quantas vezes isso terá de ser feito. (Sim, o Dali faz isso sozinho, mas ainda assim…)

    Não vejo problema de manter a query como String pois um simples teste unitário e/ou serviço de integração contínua que já pegaria o erro logo após o push do commit.
    Outra coisa que ajuda a manter a query compatível com a entidade é ter ambientes de desenvolvimento e homologação antes do código ir para produção.

    Errar o nome do atributo é o de menos e mais simples de encontrar e corrigir. Quantos outros problemas que o compilador não pega e que podem ocorrem que não tem nada a ver com tipagem? Regras de negócio bugadas, tratamento errado dos dados, etc?

    Complicar o código e exigir regerar classes de metadados só burocratizam o trabalho do desenvolvedor.

    Prefiro linguagens tipadas e compiladas para sistemas maiores pois facilitam o refactory e navegação dentro do código, mas todo exagero é maléfico no minha opinião. E para o caso deste post considero um exagero.

    Abraços!

  6. Alexandre Aquiles 20/10/2015 at 20:35 #

    Daniel,

    É bem por aí!

    O intuito desse post é mostrar a possibilidade de descobrirmos erros nas nossas queries JPA em tempo de compilação, que muitos desenvolvedores desconhecem.

    Acho que o Static Metamodel não chega a ser um problema, porque as IDEs e ferramentas de build geram sem muito esforço. Um passo a mais após a compilação.

    Mas o código em si fica horrível! Não é à toa que a maioria das equipes acabam não usando esse recurso.

  7. Dilnei Cunha 06/11/2015 at 11:30 #

    Post muito legal Alexandre, já faz um tempo que venho utilizando JPA Criteria e só vejo benefícios, sim tem o problema do código ficar mais burocrático mas é questão de se adaptar, o uso dos static metamodel acho interessante por ser typesafe, acho ruim sair catando String pelo código quando mudanças ocorrem 🙂

  8. Alexandre Aquiles 06/11/2015 at 12:41 #

    Legal, Dilnei! A maioria das pessoas corre da API. Mas ser type-safe é uma grande vantagem!

  9. Jônatas Menezes 06/11/2015 at 14:53 #

    Parabéns pelo artigo.
    Não concordo em utilizar porque também acho complexo, mas é interessante saber como usar.

    Só uma correção:
    O nome do EntityManager é ‘em’ mas em alguns momentos utilizou ‘manager’. 🙂

  10. Alexandre Aquiles 06/11/2015 at 14:59 #

    Bem observado, Jônatas! Corrigido!

  11. Marciel 28/03/2016 at 02:25 #

    Ótimo post. Existe diferença de performance entre uma e outra (JPQL e Critéria)?

  12. Alexandre Aquiles 28/03/2016 at 20:39 #

    Marciel,

    Não há uma diferença significativa na performance.

    Fiz umas medições com o código desse post, tirando a média de 100 tentativas de listagem de uma tabela com 10 mil registros.

    Com JPQL levou 43.1 ms.
    Com Criteria levou 40.33 ms.

    A diferença é muito pequena pra tirar qualquer conclusão. Além disso, fazer micro-benchmarks bem feitos é difícil!

    Um certeza: o gargalo está na comunicação do Java com o BD.

  13. Seiken 15/09/2016 at 12:48 #

    Post bem interessante. A primeira vez que tive contato com Criteria levei um susto, primeiramente por sempre ter visto em tutoriais o uso e a complexidade, e depois por nunca ter usado de forma alguma, mas enfim, aqui na empresa todos os serviços utilizavam Criteria então tive que aprender, hoje estou acostumado e adaptado e sinceramente acho muito simples, é tudo questão de entender e usar, não tenho do que reclamar e pretendo utilizar em outras situações que eu vá precisar.

Deixe uma resposta