SQL Nativo no CriteriaBuilder do JPA (EclipseLink)

Autor
Damian
Terlecki
9 minutos de leitura
JPA

Ao usar o JPA CriteriaBuilder, mais cedo ou mais tarde você encontrará algumas limitações que resultam da especificação. Um exemplo disso pode ser a limitação do número máximo de resultados retornados em uma subconsulta, ou a alteração da ordem de classificação dos valores NULL. O EclipseLink, como alternativa ao Hibernate, oferece uma abstração bastante interessante na forma da interface JpaCriteriaBuilder e da classe ExpressionBuilder.

Query TOP N

A query TOP N é uma consulta padrão na qual queremos recuperar N registros do banco de dados, na maioria das vezes ordenados por algum atributo de nosso interesse. Por exemplo, podemos querer descobrir os 5 produtos mais bem avaliados (sintaxe do banco de dados H2):

SELECT id, name FROM products ORDER BY rating LIMIT 5;

Podemos construir facilmente tal consulta usando a classe CriteriaBuilder (na verdade, através de CriteriaQuery.orderBy() e Query.setMaxResults()), para a qual obtemos uma referência do EntityManager. Caso queiramos colocá-los como uma subconsulta, a questão se torna mais complicada.

A versão atual do JPA 2.2 não nos fornece tal possibilidade. Podemos, é claro, dividir nosso problema em duas consultas ou considerar uma solução nativa. O EclipseLink, no entanto, nos fornece a classe ExpressionBuilder, que é útil nesta situação.

ExpressionBuilder é uma classe usada por componentes internos do EclipseLink. O que é importante do ponto de vista do cliente é que ela retorna objetos do tipo Expression que correspondem à interface CriteriaBuilder. Além disso, um método particularmente útil desta classe é public Expression sql(String sql, List arguments).

Como primeiro parâmetro, podemos fornecer uma consulta SQL nativa. Através do segundo parâmetro do método, podemos injetar os argumentos. A função retornará um objeto que podemos usar dependendo do propósito – como uma subconsulta, condição ou lista de atributos.

CriteriaBuilder – Subconsulta TOP N

Imagine uma loja simples – alguns clientes, pedidos e produtos. Suponha que você queira encontrar todos os clientes que pediram um dos três produtos com a classificação mais baixa. Talvez você queira dar-lhes um desconto para que sua loja não seja associada a pensamentos negativos. Um diagrama de classes simplificado de tal loja poderia ser assim:

Diagrama de classes para o exemplo de query TOP N

A consulta TOP N esperada que queremos que o CriteriaBuilder gere é:

SELECT t0.id, t0.firstname, t0.lastname
FROM customer t0
WHERE EXISTS(SELECT 1
             FROM orders t1
             WHERE ((t0.id = t1.customer_id)
                 AND (t1.product_id IN (SELECT id
                                        FROM products
                                        ORDER BY rating LIMIT 3))))

Começaremos pelo EntityManager, que você provavelmente injetará com base na plataforma preferida. Em seguida, o ExpressionBuilder será usado na consulta mais aninhada:

import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Subquery;
import java.util.Collections;
/*...*/
public class MyService {
    /*...*/    
    @Transactional(readOnly = true)
    public List<Customer> findCustomersWithBadProducts() {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<Customer> criteriaQuery = criteriaBuilder.createQuery(Customer.class);
        Root<Customer> customer = criteriaQuery.from(Customer.class);
        Subquery<Order> orderSubQuery = criteriaQuery.subquery(Order.class);
        Root<Order> order = orderSubQuery.from(Order.class);
    
        criteriaQuery.where(
                criteriaBuilder.exists(
                        orderSubQuery.where(
                                criteriaBuilder.equal(customer.get("id"), order.get("customerId")),
                                order.get("productId").in(new ExpressionBuilder()
                                        .literal("id")
                                        .sql("SELECT ? FROM products ORDER BY rating LIMIT ?",
                                                Collections.singletonList(3)
                                        )
                                )
                        )
                )
        );
    
        return em.createQuery(criteriaQuery).getResultList();
    }
}

Voilà! Note que o método sql selecionará uma expressão já presente no construtor para a primeira substituição, como mostrado no exemplo. Isso é especialmente útil quando você deseja adicionar a parte nativa da consulta no final dela.

Ao especificar objetos do tipo Path como parâmetros, também não obteremos o resultado esperado, porque o método toString() será chamado neles em vez da substituição do nome da coluna do banco de dados. Finalmente, você provavelmente não escolheria este caminho a menos que esteja implementando um suporte complexo de critérios de API.