Consultas fortemente tipadas com JPA

Consultas fortemente tipadas com JPA
alexandre.aquiles
alexandre.aquiles

Compartilhe

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". 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 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: ```java CriteriaBuilder builder = em.getCriteriaBuilder(); CriteriaQuery criteria = builder.createQuery(Double.class); Root root = criteria.from(Venda.class); Path valor = root.get("valor"); Expression soma = builder.sum(valor); criteria.select(soma); TypedQuery 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: ```java
 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 to Path", 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?

Veja outros artigos sobre Programação