5.6.2.2. Criteria API#

Criteria API es un mecanismo alternativo a JPQL para escribir consultas, que es más verborreico, pero en compensación facilita la construcción dinámica de las consultas en tiempo de ejecución, ya que no se basa en una cadena, sino en la aplicación de métodos a objetos. Se inspiró en Hibernate Criteria.

Lo primero antes de construir la consulta es crear un objeto CriteriaBuilder a partir del objeto EntityManager:

CriteriaBuilder cb = em.getCriteriaBuilder();

Truco

Es mejor crear un único objeto CriterioBuilder para construir todas las consultas.

A partir de ahora debemos definir qué es lo que se querrá obtener (recordemos que estamos haciendo un SELECT) y sobre qué tabla/entidad se quiere hacer la consulta. Por ejemplo:

5.6.2.2.1. Consulta básica#

CriteriaQuery<Estudiante> criteria = cb.createQuery(Estudiante.class); // Obtendremos estudiantes.
Root<Estudiante> root = criteria.from(Estudiante.class); // Consultamos la entidad Estudiante
criteria.select(root); // SELECT e.* FROM Estudiante e;

TypedQuery<Estudiante> estudiantes = em.createQuery(criteria);

Obsérvese que hacemos la consulta al equivalente SQL:

SELECT e.* FROM Estudiante e;

Que obtengamos estudiantes (e.*) es consecuencia de cb.createQuery(Estudiante.class) y de que hemos aplicado el método .select a root sin más. Que la consulta se haya hecho sobre Estudiante es consecuencia de criteria.from(Estudiante.class).

Si quisiéramos obtener un campo:

CriteriaQuery<String> criteria = cb.createQuery(String.class); // Obtendremos cadenas
Root<Estudiante> root = criteria.from(Estudiante.class); // Consultamos la entidad Estudiante
criteria.select(root.get("nombre")); // SELECT e.nombre FROM Estudiante e;

TypedQuery<String> query = em.createQuery(criteria);

Y si varios:

CriteriaQuery<Object[]> criteria = cb.createQuery(Object[].class);
Root<Estudiante> root = criteria.from(Estudiante.class); // Consultamos la entidad Estudiante
criteria.multiselect(root.get("nombre"), root.get("centro")); // SELECT e.nombre, e.centro FROM Estudiante e;

List<Object[]> campos = em.createQuery(criteria);

Truco

En este último caso, puede usarse también Tuple como en JPQL:

CriteriaQuery<Tuple> criteria = cb.createQuery(Tuple.class); // o cb.createTupleQuery();
Root<Estudiante> root = criteria.from(Estudiante.class);
criteria.select(cb.tuple(
   root.get("nombre").alias("nombre"),
   root.get("centro").alias("centro")
));

TypedQuery<Tuple> query = em.createQuery(query);

Prudencia

No es una buena práctica referir los atributos de las clases con una cadena (root.get("nombre")), ya que los errores de digitalización no pueden detectarse en tiempo de compilación. Por ese motivo, es bastante recomendable generar el Metamodel, que nos permite escribir:

root.get("nombre");

como:

root.get(Estudiante_.nombre);

donde Estudiante_ es una clase generada por el compilador.

5.6.2.2.2. Metamodel#

Para la habilitar la creación del metamodelo, debemos añadir como dependencia hibernate-processor:

<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-processor</artifactId>
    <version>7.0.0.Beta4</version>
    <scope>provided</scope>
</dependency>

Obsérvese su ámbito, ya que necesitamos la librería para el desarrollo, no para la ejecución de la aplicación.

Además, debemos añadir una sección <build> a pom.xml:

<build>
   <plugins>
       <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <version>3.14.0</version>
           <configuration>
               <release>21</release> <!-- Versión de JDK -->
               <annotationProcessorPaths>
                   <path>
                       <groupId>org.hibernate.orm</groupId>
                       <artifactId>hibernate-processor</artifactId>
                       <version>7.0.0.Beta4</version>
                   </path>
               </annotationProcessorPaths>
           </configuration>
       </plugin>
   </plugins>
</build>

Con esto, deberíamos tener disponibles en el proyecto las metaclases correspondientes a nuestro modelo (Centro_ y Estudiante_ en el ejemplo).

Ahora podríamos reescribir el ejemplo anterior así:

CriteriaQuery<Tuple> criteria = cb.createQuery(Tuple.class); // o
cb.createTupleQuery();
Root<Estudiante> root = criteria.from(Estudiante.class);
criteria.select(cb.tuple(
   root.get(Estudiante_.nombre).alias("nombre"),
   root.get(Estudiante_.centro).alias("centro")
));

TypedQuery<Tuple> query = em.createQuery(query);

Nota

A partir de ahora usaremos las metaclases para referir atributos.

5.6.2.2.3. Condiciones#

CriteriaBuilder tiene definidos métodos que implementan los operadores básicos de SQL. Por ejemplo, para escoger los estudiantes no matriculados:

CriteriaQuery<Estudiante> query = cb.createQuery(Estudiante.class);
Root<Estudiante> root = query.from(Estudiante.class);
query.select(root);
query.where(cb.isNull(root.get(Estudiante_.centro)));

TypedQuery<Estudiante> estudiantes = sesion.createQuery(query);

Para obtener los estudiantes de un centro determinado (suponiendo que éste se almacene en la variable centro) la condición habría sido:

query.where(cb.equal(root.get(Estudiante_.centro), centro));

5.6.2.2.4. Ordenación#

Para ordenar resultados basta con aplicar el orden a criteria:

CriteriaQuery<Estudiante> criteria = cb.createQuery(Estudiante.class);
Root<Estudiante> root = criteria.from(Estudiante.class);
criteria.select(root);
criteria.orderBy(cb.desc(root.get(Estudiante_.nacimiento)));

TypedQuery<Estudiante> query = em.createQuery(criteria);

5.6.2.2.5. Agrupación#

También existe el equivalente a GROUP BY:

CriteriaQuery<Tuple> criteria = cb.createQuery(Tuple);
Root<Estudiante> root = criteria.from(Estudiante.class);
criteria.select(cb.tuple(
   root.get(Estudiante_.centro).alias("centro"),
   cb.count(root).alias("cantidad")
));
criteria.groupBy(root.get(Estudiante_.centro));

TypedQuery<Tuple> query = sesion.createQuery(query);

Si ahora quisiéramos ordenar resultados por la cantidad de estudiantes que tiene cada centro deberíamos añadir:

criteria.orderBy(cb.asc(cb.count(root)));

ya que no puede usarse el alias. O, si quisiéramos ordenar por el nombre del centro:

criteria.orderBy(cb.asc(root.get(Estudiante_.centro).get(Centro_.nombre)));

5.6.2.2.6. Joins#

Como en JPQL también se puede relacionar fácilmente entidades. Esto permite hacer un INNER JOIN:

CriteriaQuery<Estudiante> criteria = cb.createQuery(Estudiante.class);
Root<Estudiante> root = criteria.from(Estudiante.class);
Join<Estudiante, Centro> centro = root.join(Estudiante_.centro, JoinType.INNER);
criteria.select(root);

TypedQuery<Estudiante> query = em.createQuery(criteria);

La consulta equivale a:

SELECT e.* FROM Estudiante e INNER JOIN Centro c ON e.centro = c.id;

Obsérvese que el JOIN viene representado por la variable centro que se define en la línea remarcada. En cambio, si nuestra intención hubiera sido:

SELECT c.* FROM Estudiante e INNER JOIN Centro c ON e.centro = c.id;

la consulta debería haberse construido así:

CriteriaQuery<Centro> criteria = cb.createQuery(Centro.class);
Root<Estudiante> root = criteria.from(Estudiante.class);
Join<Estudiante, Centro> centro = root.join(Estudiante_.centro, JoinType.INNER);
criteria.select(centro);

TypedQuery<Estudiante> query = em.createQuery(criteria);

Nota

Obtendremos muchos centros repetidos así que lo suyo sería haber hecho un DISTINCT:

criteria.select(cb.distinct(centro));

que devolvería la lista de centros con algún estudiante matriculado.

Si intentáramos obtener lo contrario, la relación de centros sin alumnos matriculados, en SQL podríamos obtenerla con una de estas dos consultas, que son equivalentes:

SELECT c.* FROM Centro c LEFT JOIN Estudiante e ON e.centro = c.id WHERE e.centro IS NULL;
SELECT c.* FROM Estudiante e RIGHT JOIN Centro c ON e.centro = c.id WHERE e.centro IS NULL;

Al intentar trasladar esto a Criteria API nos encontraríamos con los siguientes problemas:

La primera expresión podríamos traducirla así:

CriteriaQuery<Centro> criteria = cb.createQuery(Centro.class);
Root<Centro> root = criteria.from(Centro.class);
Join<Centro, Estudiante> estudiante = root.join(Centro_.estudiantes, JoinType.LEFT);
criteria.select(centro);
criteria.where(cb.isEmpty(root.get(Centro_.estudiantes)));

Obsérvese que en la relación root hace referencia a un objeto Centro, luego debemos indicar el campo que liga Centro con Estudiante. Es la lista de estudiantes… pero sólo si definimos como bidireccional la relación. Si la relación es unidireccional el único campo que establece la relación está en Estudiante y no podremos resolver de este modo el problema. Por su parte, la condición es trivial: la lista de estudiantes debe estar vacía.

La segunda expresión si tiene teóricamente, pero sólo teóricamente, solución:

// Esto NO VALE. Ni se le ocurra intentarlo.
CriteriaQuery<Centro> criteria = cb.createQuery(Centro.class);
Root<Estudiante> root = criteria.from(Estudiante.class);
Join<Estudiante, Centro> centro = root.join(Estudiante_.centro, JoinType.RIGHT);
criteria.select(centro);
criteria.where(cb.isNull(root.get(Estudiante_.centro)));

El problema es que Criteria API no implementa RIGHT JOIN sino solamente los tipos ya vistos: INNER JOIN y LEFT JOIN.

Subconsultas

En consecuencia, el único modo de hacer la consulta con una relación unidireccional es recurrir a una subconsulta:

SELECT c.* FROM Centro c WHERE c.id NOT IN (SELECT e.id FROM Estudiante e);

lo que da pie a que intentemos implementar está consulta con subconsulta usando Criteria API:

CriteriaQuery<Centro> criteria = cb.createQuery(Centro.class);
Root<Centro> root = criteria.from(Centro.class);
// Preparamos la subconsulta
Subquery<Long> subquery = criteria.subquery(Long.class);
Root<Estudiante> subroot = subquery.from(Estudiante.class);
subquery.select(subroot.get(Estudiante_.centro));
// Consulta con subconsulta
criteria.select(root);
criteria.where(cb.not(root).in(subquery));

Nota

Nótese que aquí podemos comparar directamente centros en vez de identificadores de centros.

Cadena de consultas

Observemos parte del código ya presentado anteriormente:

CriteriaQuery<Estudiante> criteria = cb.createQuery(Estudiante.class);
Root<Estudiante> root = criteria.from(Estudiante.class);
Join<Estudiante, Centro> centro = root.join(Estudiante_.centro, JoinType.INNER);
criteria.select(root);

Partidos de Estudiante (root) y relacionamos con Centro a través del atributo centro de Estudiante, de ahí que la relación se escriba así:

Join<Estudiante, Centro> centro = root.join(Estudiante_.centro, JoinType.INNER);

O sea, para relacionar con Centro, juntamos root (que es Estudiante) a través de su atributo llamado centro. Si deseamos que en la consulta participe una segunda relación, deberemos tener en cuenta con qué entidad se relaciona. Si la tercera entidad fuera la entidad Grupo relacionada con Estudiante entonces tendríamos que hacer:

Join<Estudiante, Centro> centro = root.join(Estudiante_.centro, JoinType.INNER);
Join<Estudiante, Curso> curso = root.join(Estudiante_.curso, JoinType.INNER);

En cambio, si la tercera entidad fuera la entidad ComunidadA relacionada con Centro, entonces la relación se establecería así:

Join<Estudiante, Centro> centro = root.join(Estudiante_.centro, JoinType.INNER);
Join<Centro, ComunidadA> comunidad = centro.join(Centro_.comunidad, JoinType.INNER);

Carga inmediata

COmo en el caso de JPQL puede forzarse una carga inmediata de la entidad relacionada sustituyendo el método .join por el método .fetch en los ejemplos de este epígrafe en que existen JOINs explícitos:

// ...
Join<Centro, Estudiante> estudiante = root.fetch(Centro_.estudiantes, JoinType.LEFT);
// ...

Ahora bien, ¿qué ocurre cuando la relación no es explícita?

CriteriaQuery<Estudiante> criteria = cb.createQuery(Estudiante.class);
Root<Estudiante> root = criteria.from(Estudiante.class);
root.fetch(root.get(Estudiante_.centro, JoinType.LEFT));
criteria.select(root);

La respuesta es que basta con hacerla explícito para poder usar el método .fetch.

Ver también

Consulte para más información el epígrafe dedicado a optimización.

5.6.2.2.7. Actualización y borrado#

Al igual que JPQL, también se puede actualizar objetos. Por ejemplo, esto desmatricularía a todos los estudiantes cuyo nombre empieza por «J»:

CriteriaUpdate<Estudiante> update = cb.createCriteriaUpdate(Estudiante.class);
Root<Estudiante> root = update.from(Estudiante.class);
update.set(root.get(Estudiante_.centro), null);
update.where(cb.like(root.get(Estudiante_.nombre), "J%"));

em.createQuery(update).executeUpdate();

También es posible borrar:

CriteriaDelete<Estudiante> delete = cb.createCriteriaDelete(Estudiante.class);
Root<Estudiante> root = delete.from(Estudiante.class);
delete.where(cb.like(root.get(Estudiante_.nombre), "J%"));

em.createQuery(delete).executeUpdate();