Jpa Criteria Api

Guía de la Criteria API en Jakarta EE / Spring Data JPA

Nota: Esta documentación está en español y cubre los conceptos fundamentales de la Criteria API: CriteriaBuilder, Root y CriteriaQuery.


¿Qué es la Criteria API?

La Criteria API permite construir consultas a la base de datos de forma programática usando Java puro. A diferencia de JPQL (que usa cadenas de texto), las consultas con Criteria API son:

  • Typesafe → los errores se detectan en tiempo de compilación
  • Portables → funcionan independientemente del motor de base de datos
  • Dinámicas → se pueden construir condiciones en tiempo de ejecución

Los tres pilares de la Criteria API

EntityManager

     ├──► CriteriaBuilder   → fábrica de objetos de consulta
     │         │
     │         └──► CriteriaQuery<T>   → define la estructura de la consulta
     │                    │
     │                    └──► Root<T>   → punto de partida (FROM)

     └──► TypedQuery<T>   → ejecuta la consulta

1. CriteriaBuilder

¿Qué es? Es la fábrica principal. Se obtiene desde el EntityManager y sirve para crear todos los demás componentes de la consulta.

¿Cómo se obtiene?

EntityManager em = ...; // inyectado por Spring o JPA
CriteriaBuilder cb = em.getCriteriaBuilder();

Métodos principales

Crear una consulta

CriteriaQuery<Libro> cq = cb.createQuery(Libro.class);

Comparaciones

Método Descripción
cb.equal(expr, valor) campo = valor
cb.notEqual(expr, valor) campo != valor
cb.like(expr, patron) campo LIKE 'patron'
cb.gt(expr, valor) campo > valor
cb.lt(expr, valor) campo < valor
cb.ge(expr, valor) campo >= valor
cb.le(expr, valor) campo <= valor
cb.between(expr, v1, v2) campo BETWEEN v1 AND v2

Operadores lógicos (componer predicados)

Método Descripción
cb.and(p1, p2) p1 AND p2
cb.or(p1, p2) p1 OR p2
cb.not(p) NOT p

Ordenamiento

cb.asc(expresion)   // ORDER BY campo ASC
cb.desc(expresion)  // ORDER BY campo DESC

2. CriteriaQuery<T>

¿Qué es? Define la estructura completa de la consulta: qué entidad se consulta, qué se selecciona, qué condiciones aplican y cómo se ordenan o agrupan los resultados.

¿Cómo se crea?

CriteriaQuery<Libro> cq = cb.createQuery(Libro.class);

El tipo genérico <T> indica el tipo del resultado que devolverá la consulta.

Métodos principales

from() — define el FROM

Root<Libro> libro = cq.from(Libro.class);

select() — define el SELECT

cq.select(libro);                          // SELECT l FROM Libro l
cq.select(libro.get("titulo"));           // SELECT l.titulo FROM Libro l

where() — define el WHERE

cq.where(cb.equal(libro.get("autor"), "García Márquez"));

Se pueden pasar múltiples predicados (se combinan con AND):

cq.where(predicado1, predicado2, predicado3);

orderBy() — define el ORDER BY

cq.orderBy(cb.asc(libro.get("titulo")));
cq.orderBy(cb.desc(libro.get("anio")), cb.asc(libro.get("titulo")));

groupBy() y having() — agrupación

cq.groupBy(libro.get("genero"));
cq.having(cb.gt(cb.count(libro), 5L)); // solo géneros con más de 5 libros

3. Root<T>

¿Qué es? Representa la entidad raíz de la consulta, equivalente al alias en el FROM de JPQL (FROM Libro ll sería el Root). Desde el Root se navega hacia los atributos y entidades relacionadas.

¿Cómo se obtiene?

Root<Libro> libro = cq.from(Libro.class);
libro.get("titulo")   // accede al campo "titulo" del Libro
libro.get("autor")    // accede al campo "autor"

Con metamodelo estático (tipado seguro):

libro.get(Libro_.titulo)   // usando clase metamodelo generada

Joins — navegar relaciones

Cuando la consulta necesita acceder a una entidad relacionada:

// JOIN simple
Join<Libro, Autor> autor = libro.join("autores");

// JOIN encadenado
Join<Autor, Direccion> dir = libro.join("autores").join("direccion");

Tipos de join disponibles: JOIN (inner por defecto), LEFT JOIN, RIGHT JOIN:

libro.join("autores", JoinType.LEFT);

Flujo completo: construir y ejecutar una consulta

Paso a paso

// 1. Obtener CriteriaBuilder
CriteriaBuilder cb = em.getCriteriaBuilder();

// 2. Crear CriteriaQuery con el tipo de resultado
CriteriaQuery<Libro> cq = cb.createQuery(Libro.class);

// 3. Definir la entidad raíz (FROM)
Root<Libro> libro = cq.from(Libro.class);

// 4. Construir predicados (condiciones)
Predicate porAutor = cb.equal(libro.get("autor"), "Borges");
Predicate porTitulo = cb.like(libro.get("titulo"), "%laberinto%");

// 5. Aplicar SELECT, WHERE, ORDER BY
cq.select(libro)
  .where(cb.and(porAutor, porTitulo))
  .orderBy(cb.asc(libro.get("anio")));

// 6. Crear TypedQuery y ejecutar
TypedQuery<Libro> query = em.createQuery(cq);
List<Libro> resultados = query.getResultList();     // varios resultados
// Libro unico = query.getSingleResult();           // un solo resultado

Predicados dinámicos (consultas opcionales)

Un caso muy común es construir filtros que solo se aplican si el parámetro viene con valor:

List<Libro> buscar(String autor, String titulo) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Libro> cq = cb.createQuery(Libro.class);
    Root<Libro> libro = cq.from(Libro.class);

    List<Predicate> predicados = new ArrayList<>();

    if (autor != null) {
        predicados.add(cb.equal(libro.get("autor"), autor));
    }
    if (titulo != null) {
        predicados.add(cb.like(libro.get("titulo"), "%" + titulo + "%"));
    }

    cq.where(predicados.toArray(new Predicate[0]));
    return em.createQuery(cq).getResultList();
}

Uso en Spring: @Repository

Spring permite inyectar el EntityManager directamente en un bean @Repository:

@Repository
class LibroRepository {

    private final EntityManager em;

    LibroRepository(EntityManager em) {
        this.em = em;
    }

    List<Libro> findByAutorYTitulo(String autor, String titulo) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Libro> cq = cb.createQuery(Libro.class);
        Root<Libro> libro = cq.from(Libro.class);

        cq.where(
            cb.equal(libro.get("autor"), autor),
            cb.like(libro.get("titulo"), "%" + titulo + "%")
        );

        return em.createQuery(cq).getResultList();
    }
}

Spring activa la traducción automática de excepciones JPA gracias a @Repository.


JPA Specifications (Spring Data)

Spring Data ofrece la interfaz Specification<T> para encapsular predicados reutilizables:

// Definir especificaciones reutilizables
static Specification<Libro> tieneAutor(String autor) {
    return (libro, cq, cb) -> cb.equal(libro.get("autor"), autor);
}

static Specification<Libro> tituloContiene(String texto) {
    return (libro, cq, cb) -> cb.like(libro.get("titulo"), "%" + texto + "%");
}

El repositorio debe extender JpaSpecificationExecutor<T>:

interface LibroRepository extends JpaRepository<Libro, Long>,
                                   JpaSpecificationExecutor<Libro> {}

Uso combinado con operadores lógicos:

// Solo por autor
libroRepository.findAll(tieneAutor("Borges"));

// Autor AND titulo
libroRepository.findAll(
    where(tieneAutor("Borges")).and(tituloContiene("laberinto"))
);

Resumen visual

CriteriaBuilder  ──────────────────────────────────────────
       │                                                    │
       ├── createQuery(Clase.class) ──► CriteriaQuery<T>   │
       │                                      │            │
       │                               from() ├──► Root<T> │
       │                               select()│           │
       │                               where() │           │
       │                               orderBy()│          │
       │                               groupBy()│          │
       │                                        │          │
       └── Predicados, Order, etc. ◄────────────┘          │

EntityManager.createQuery(cq) ──► TypedQuery<T>            │
                                        │                  │
                               getResultList()             │
                               getSingleResult()           │

Documentación elaborada a partir del Jakarta EE Tutorial y la guía de Spring Data JPA.