Queries com a “nova” JPA 2 criteria API

{lang: 'pt-BR'}

Eu sempre achei a linguagem SQL muito interessante, por ter um quê de linguagem natural. Aliás, a idéia inicial era realmente criar uma linguagem de consultas, bem próxima à maneira como nós, humanos, nos comunicamos. O tempo passou, a linguagem cresceu, e as consultas SQL se tornaram verdadeiros monstros. É difícil encontrar uma query mais complexa que contenha fluidez similar a uma sentença em inglês. Além disso, eu sempre achei chato misturar SQL no meio da minha aplicação (mas isso é uma birra minha que não tem nada a ver com a história).

Há cerca de 3 anos fui apresentado a uma API de consultas do Hibernate (Hibernate Criteria API). A idéia é que programaticamente poderíamos construir consultas SQL, ao invés de usar a linguagem de consultas do Hibernate (HQL). Achei a idéia muito boa e fiz uso desta API por alguns anos.

No mundo JPA (Java Persistence API), somente com a JPA 2, passamos a ter acesso a uma API de criteria. Existem inúmeras vantagens e desvantagens em usar uma API de criteria, mas as que eu gosto de destacar são:

Vantagens:
  1. Verificação de erros – Muitos erros podem ser detectados em tempo de compilação;
  2. Segurança – como as consultas são construídas pelo motor da API, você fica praticamente imune a SQL injections;
  3. Queries dinâmicas podem ser construídas mais facilmente, ao invés montar strings complexas.
  4. Tipagem forte – a JPA criteria API leva vantagem em relação à Hibernate Criteria API em relação a verficação de tipos.
Desvantagens:
  1. Complexidade – uma vez que a maioria dos desenvolvedores está acostumada com o SQL/HQL/JPQL, migrar para uma API de criteria não é simples.

Executando uma query JPQL:

Query query = entityManager.createQuery("SELECT p FROM Product p");
List results = query.getResultList();

A mesma query JPQL, mas de forma tipada, seria:

TypedQuery<Product> typedQuery = entityManager.createQuery("SELECT p FROM Product p", Product.class);
List<Product> results = typedQuery.getResultList();

Executando esta consulta usando a JPA Criteria API:

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> query = builder.createQuery(Product.class);
Root<Product> from = query.from(Product.class);
CriteriaQuery<Product> select = query.select(from);

TypedQuery<Product> typedQuery = entityManager.createQuery(select);
List<Product> results = typedQuery.getResultList();

Perceba que os dois últimos comandos são bem parecidos com a maneira como a JPQL funciona. A primeira vista, esta estratégia parece ser bem mais complexa e trabalhosa, mas se encurtarmos um pouco o código, temos uma estrutura bem parecida com uma query SQL:

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> query = builder.createQuery(Product.class);

TypedQuery<Product> typedQuery = entityManager.createQuery(
    query.select(
       query.from(Product.class)
    )
);
List<Product> results = typedQuery.getResultList();

Veja um exemplo de como fazermos uma consulta com a cláusula where.
Usando a JPQL:

TypedQuery<Product> typedQuery = entityManager.createQuery(
    "SELECT p "+
    "FROM Product p "+
    "WHERE p.price > :price", 
Product.class);
typedQuery.setParameter("price", price);
List<Product> results = typedQuery.getResultList();

Usando a JPA Criteria API:

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> query = builder.createQuery(Product.class);
Root<Product> from = query.from(Product.class);
TypedQuery<Product> typedQuery = entityManager.createQuery(
    query.select(from )
    .where(
       builder.gt(from.get("price"), price)
    )
);
List<Product> results = typedQuery.getResultList();

Sim, tudo bem, eu admito, está tudo muito mais complicado do que as queries JPQL. Mas eu ainda acho que queries com muitos componentes dinâmicos são mais fáceis de fazer, quando usamos a JPA Criteria API.

Vamos dar uma olhada numa query que envolva múltiplas entidades (joins).

Usando a JPQL:

TypedQuery<Product> typedQuery = entityManager.createQuery(
      "SELECT p "+
      "FROM Product p "+
      "WHERE p.supplier.name=:supplier", 
Product.class);
typedQuery.setParameter("supplier", supplierName);
List<Product> results = typedQuery.getResultList();

Usando a JPA Criteria API:

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> query = builder.createQuery(Product.class);
Root<Product> from = query.from(Product.class);
TypedQuery<Product> typedQuery = entityManager.createQuery(
    query.select(from )
    .where(
       builder.equal(from.join("supplier").get("name"), supplierName)
    )
);
List<Product> results = typedQuery.getResultList();

Para fazer uma ordenação, basta usar o método CriteriaQuery.orderBy():

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> query = builder.createQuery(Product.class);
Root<Product> from = query.from(Product.class);
TypedQuery<Product> typedQuery = entityManager.createQuery(
    query.select(from )
    .where(
       builder.gt(from.get("price"), price)
    )
    .orderBy(builder.asc(from.get("name")))
);
List<Product> results = typedQuery.getResultList();

Vejamos uma consulta resultado de uma tela de busca onde o usuário pode filtra por diversos critérios: maior preço, menor preço, nome e categoria. A estratégia será adicionar mais ou menos cláusulas ao predicado usado pelo where.

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> query = builder.createQuery(Product.class);
Root<Product> from = query.from(Product.class);

Predicate predicate = builder.and();

// product.price > minPrice
if (minPrice != null && minPrice > 0){
     predicate = builder.and(predicate, builder.ge(from.get("price"), minPrice));
}
// product.price < maxPrice
if (maxPrice != null && maxPrice > 0){
    predicate = builder.and(predicate, 
        builder.le(from.get("price"), maxPrice));
}

// product.name like %productName%
if (productName != null && productName.length > 2){
    predicate = builder.and(predicate, 
        builder.like(from.<String>get("name"), "%"+productName+"%"));
}

// product.category.name like %categoryName%
if (categoryName != null && categoryName.length() > 2){    
    predicate = builder.and(predicate, 
        builder.like(
            from.join("category").<String>get("name"), 
            "%"+categoryName+"%"));
}

TypedQuery<Product> typedQuery = entityManager.createQuery(
    query.select(from )
    .where( predicate )
    .orderBy(builder.asc(from.get("name")))
);
List<Product> results = typedQuery.getResultList();

Recentemente precisei fazer uma consulta a partir de um relacioamento unidirecional que me deu um certo trabalho. Fazendo algumas simplificações, eu queria obter todos professores (Teacher) de turmas (ClassSection) de uma determinada escola (School). Turma aponta para professor, mas professor não aponta para turma.

Teacher School

Aconsulta ficou mais ou menos assim:

CriteriaBuilder builder  = entityManager.getCriteriaBuilder();
CriteriaQuery<Teacher> query = builder.createQuery(Teacher.class);		
Root<Teacher> fromTeacher = query.from(Teacher.class);
Root<ClassSection> fromClassSection = query.from(ClassSection.class);

Join<ClassSection, Teacher> teacherJoin = fromClassSection.join("teacher");
Join<ClassSection, School> schoolJoin = fromClassSection.join("school");

TypedQuery<Teacher> typedQuery = getEntityManager().createQuery(query
    .select(fromTeacher)
    .where(builder.and(
            builder.equal(fromTeacher, teacherJoin), 
            builder.equal(schoolJoin.get("id"), schoolId)
    ))
    .orderBy(builder.asc(fromTeacher.get("firstName")))
    .distinct(true)
);

List<Teacher> teachers = typedQuery.getResultList();

Perceba que faço a cahamada query.from() duas vezes, uma para Teacher e outra para ClassSection, entreatanto, o método createQuery() sempre terá o tipo da classe que será retornada como resultado da consulta.

Para saber mais:
http://www.objectdb.com/java/jpa/query/criteria
http://www.altuure.com/2010/09/23/jpa-criteria-api-by-samples-part-i/
http://www.altuure.com/2010/09/23/jpa-criteria-api-by-samples-%E2%80%93-part-ii/

{lang: 'pt-BR'}
  • teste

    Obrigado pelo tutorial. Muito bom.
    Att

  • Marcos Vinicius Moraes Gabriel

    Ótimo material

  • Edjane Guimarães

    Olá boa noite, muito bom o material! Estou desenvolvendo uma aplicação usando jpa Criteria e gostaria de tirar uma dúvida. Com o JPA Criteria consigo trazer um objeto imagem do banco de dados? Pois já tentei de várias formas e não consigo, esse é um exemplo de como estou tentando fazer isso:
    [code]
    // Esse método é usado para filtrar as imagens do modelo selecionado, não funciona
    public List listarImagensModelo(String modelo) {

    CriteriaBuilder cb = manager.getCriteriaBuilder();
    CriteriaQuery qm = cb.createQuery(ModeloCaderno.class);
    Root im = qm.from(ModeloCaderno.class);
    //aqui quero buscar a imagem de um determinado modelo
    Predicate predicate = cb.equal(im.get(“imagem”), modelo);

    qm.select(im);
    qm.where(predicate);

    TypedQuery query = manager.createQuery(qm);
    List imagens = query.getResultList();

    return imagens;
    }

    [/code]

    Essa é minha mensagem de erro: Type specified for TypedQuery [br.com.lefacil.modelo.papelaria.ModeloCaderno] is incompatible with query return type [class [B]

    E aqui é meu objeto no modelo
    [code]
    @Lob
    @Basic(fetch=FetchType.LAZY)
    @Column(length=100000)
    public byte[] getImagem() {
    return imagem;
    }

    public void setImagem(byte[] imagem) {
    this.imagem = imagem;
    }

    Meu bean esta como @SessionScoped

    [/code]

    Desde já muito obrigada!

    • http://blog.werneckpaiva.com/ Ricardo Paiva

      Não sei se eu tenho todos os dados para te ajudar, mas eu notei alguns detalhes.

      1. O tipo da classe é ModeloCaderno e o tipo de retorno é modelocaderno, em minúsculo. Não sei se é apenas um copy and paste que não foi fiel ao código, ou se você está usando um generic que chamou de modelocaaderno. Se for esse o caso, explica o erro que você está recebendo.

      2. o atributo “modelo” da classe ModeloCaderno é realmente string?

      Com o código que estou vendo não consigo ver mais nada. Espero que ajude.

      PS: Você está armazenando imagem no banco? Cuidado porque isso engorda muito a base de dados. Tenha certeza de que você precisa disso.

      • Edjane Guimarães

        Olá Ricardo, muito obrigada pela resposta! Não tinha visto que na hora que copiei o código o nome da classe foi em minusculo, foi algum problema na hora de colar. Sim o atributo modelo é uma String.
        Pensei muito em relação a guardar imagens direto no banco de dados, ou em uma pasta no servidor, optei por guardar no banco porque nesse caso serão imagens de resolução baixa e não serão muitas, acho que nesse caso é mais “seguro” devido há alguns problemas que já vi.
        Vou tentar fazer mais uns testes por aqui.
        Obrigada!