5.6.2.1. 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();
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.1.1. Consulta básica#
Empecemos por una consulta SQL muy básica:
0// SELECT e.* FROM Estudiante e
1CriteriaQuery<Estudiante> criteria = cb.createQuery(Estudiante.class); // Obtendremos estudiantes.
2Root<Estudiante> estudiante = criteria.from(Estudiante.class); // Consultamos la entidad Estudiante
3criteria.select(estudiante); // SELECT e.* FROM Estudiante e;
4
5TypedQuery<Estudiante> query = em.createQuery(criteria);
Como queremos obtener objetos Estudiante (primera línea) y consultamos la
entidad Estudiante (segunda línea) es obvio que la consulta es:
SELECT e.* FROM Estudiante e
Si quisiéramos obtener un campo, entonces deberíamos alterar las líneas 1 y 3:
// SELECT e.nombre FROM Estudiante e;
CriteriaQuery<String> criteria = cb.createQuery(String.class); // Obtendremos cadenas
Root<Estudiante> estudiante = criteria.from(Estudiante.class); // Consultamos la entidad Estudiante
criteria.select(estudiante.get("nombre"));
TypedQuery<String> query = em.createQuery(criteria);
Y si varios[1]:
// SELECT e.nombre, e.nacimiento FROM Estudiante e;
CriteriaQuery<Object[]> criteria = cb.createQuery(Object[].class);
Root<Estudiante> estudiante = criteria.from(Estudiante.class); // Consultamos la entidad Estudiante
criteria.select(cb.array(estudiante.get("nombre"), estudiante.get("nacimiento")));
TypedQuery<Object[]> query = em.createQuery(criteria);
En este último caso, puede usarse también Tuple como en JPQL:
CriteriaQuery<Tuple> criteria = cb.createQuery(Tuple.class); // o cb.createTupleQuery();
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
criteria.select(cb.tuple(
estudiante.get("nombre").alias("nombre"),
estudiante.get("nacimiento").alias("nacimiento")
));
TypedQuery<Tuple> query = em.createQuery(query);
Pero en ambos casos, deberemos especificar los tipos (String,
Centro) al obtenerlos.
Truco
Posiblemente, la solución más elegante es usar Record:
record NombreDate(String nombre, LocalDate nacimiento) {};
CriteriaQuery<NombreDate> criteria = cb.createQuery(NombreDate.class); // o cb.createTupleQuery();
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
var nombre = estudiante.get("nombre").as(String.class).alias("nombre");
var nacimiento = estudiante.get("nacimiento").as(LocalDate.class).alias("nacimiento");
criteria.select(cb.construct(NombreDate.class, nombre, nacimiento));
TypedQuery<NombreDate> query = em.createQuery(query);
el cual nos permite ahorrarnos el tipo al recuperar los datos:
query.getResultList().forEach(e -> {
// e.nombre() es String; y e.nacimiento(), LocalDate.
System.out.printf("%s: %s.\n", e.nombre(), e.nacimiento().format(df));
});
Justamente a continuación introduciremos el Metamodel, que nos ahorrará indicar los tipos incluso al hacer las definiciones.
Prudencia
No es una buena práctica referir los atributos de las clases con
una cadena (estudiante.get("nombre")), ya que los errores de
digitalización o de tipos no pueden detectarse en tiempo de compilación. Por
ese motivo, es bastante recomendable generar el Metamodel, que nos permite
escribir:
estudiante.get("nombre");
como:
estudiante.get(Estudiante_.nombre);
donde Estudiante_ es una clase generada por el compilador.
Nota
La elección de los atributos nombre y nacimiento no ha sido
casual; si hubiéramos escogido otro atributo como centro, que hace
referencia a la entidad Centro, JPA habría hecho internamente una
composición con JOIN, lo cual no es nuestra intención por ahora; ya que más
adelante lo trataremos al estudiar las relaciones.
5.6.2.1.2. Metamodel#
Para la habilitar la creación del metamodelo, debemos añadir a
la sección <build> de pom.xml[2]:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
<configuration>
<release>${maven.compiler.release}</release>
<annotationProcessorPaths>
<path>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-processor</artifactId>
<version>${hibernate.version}</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).
Prudencia
Si usamos javadoc para la documentación, esta herramienta busca las
clases del código fuente exclusivamente dentro de src/, mientras que
las [meta]clases generadas automáticamente por hibernate-processor se
incluyen en target/generated-sources/annotations/. Esta circunstancia
debemos indicarla:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<!-- La última versión puede consultarse en el repositorio de Maven -->
<version>3.11.2</version>
<configuration>
<source>${maven.compiler.release}</source>
<show>private</show>
<!-- Debemos añadir annotations para que no den problemas las clases anotadas del Metamodel -->
<sourcepath>${project.build.sourceDirectory};${project.build.directory}/generated-sources/annotations</sourcepath>
</configuration>
</plugin>
Ahora podríamos reescribir el ejemplo anterior así:
record NombreDate(String nombre, LocalDate nacimiento) {};
CriteriaQuery<NombreDate> criteria = cb.createQuery(NombreDate.class); // o
cb.createTupleQuery();
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
var nombre = estudiante.get(Estudiante_.nombre).alias("nombre");
var centro = estudiante.get(Estudiante_.nacimiento).alias("nacimiento");
criteria.select(cb.construct(NombreDate.class, nombre, nacimiento));
TypedQuery<NombreDate> query = em.createQuery(criteria);
Nota
El Metamodel también genera Estudiante_.NOMBRE o
Estudiante_.CENTRO, pero sólo son constantes de tipo String que se
sustituyen por las cadenas "nombre" o "centro", allí donde están. Por
tanto, sólo protegen de los fallos de digitalización, pero no permiten la
comprobación estática de tipos o la refactorización de código.
5.6.2.1.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> criteria = cb.createQuery(Estudiante.class);
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
criteria.select(estudiante);
criteria.where(cb.isNull(estudiante.get(Estudiante_.centro)));
TypedQuery<Estudiante> query = em.createQuery(criteria);
Para obtener los estudiantes de un centro determinado (suponiendo que éste
se almacene en la variable centro) la condición habría sido:
criteria.where(cb.equal(estudiante.get(Estudiante_.centro), centro));
5.6.2.1.4. Ordenación#
Para ordenar resultados basta con aplicar el orden a criteria:
CriteriaQuery<Estudiante> criteria = cb.createQuery(Estudiante.class);
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
criteria.select(estudiante);
criteria.orderBy(cb.desc(estudiante.get(Estudiante_.nacimiento)));
TypedQuery<Estudiante> query = em.createQuery(criteria);
5.6.2.1.5. Agrupación#
También existe el equivalente a GROUP BY:
record CentroCuantos(Centro centro, Long cantidad) {};
CriteriaQuery<CentroCuantos> criteria = cb.createQuery(CentroCuantos);
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
var centro_p = estudiante.get(Estudiante_.centro);
var centro = centro_p.alias("centro");
var cantidad_p = cb.count(estudiante);
var cantidad = cantidad_p.alias("cantidad");
criteria.select(cb.construct(CentroCuantos.class, centro, cantidad))
.groupBy(centro_p); // No puede usarse el alias, de ahí la variable intermedia centro_p
TypedQuery<CentroCuantos> query = em.createQuery(criteria);
Si ahora quisiéramos ordenar resultados por la cantidad de estudiantes que tiene cada centro deberíamos añadir:
criteria.orderBy(cb.asc(cantidad_p));
ya que no puede usarse el alias. O, si quisiéramos ordenar por el nombre del centro:
criteria.orderBy(cb.asc(centro_p.get(Centro_.nombre)));
5.6.2.1.6. Relaciones#
Como en JPQL también se puede relacionar fácilmente entidades.
INNER JOIN o, simplemente, JOIN
Es la relación más sencilla y relaciona dos entidades en las que cada clave foránea en una entidad hace referencia a un identificador de la otra. El resultado es que aparecen todos los registros relacionados entre sí. Por tanto:
SELECT e.*,c.* FROM Estudiante e JOIN Centro c ON e.centro = c.id;
muestra todos los estudiantes matriculados junto a los datos del centro en el que está matriculado. La relación es conmutativa y:
SELECT e.*,c.* FROM Centro c JOIN Estudiante e ON e.centro = c.id;
devuelve exactamente el mismo resultado. Para implementar esta relación en Criteria API, podemos hacer:
// SELECT c.* FROM Estudiante e INNER JOIN Centro c ON e.centro = c.id;
CriteriaQuery<Centro> criteria = cb.createQuery(Centro.class);
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
Join<Estudiante, Centro> centro = estudiante.join(Estudiante_.centro, JoinType.INNER);
criteria.select(centro);
TypedQuery<Centro> query = em.createQuery(criteria);
query.getResultList().forEach(System.out::println); // Imprime centros.
La segunda posibilidad, que devuelve el mismo resultado se escribe así:
// SELECT e.* FROM Centro c INNER JOIN Estudiante e ON e.centro = c.id;
CriteriaQuery<Estudiante> criteria = cb.createQuery(Estudiante.class);
Root<Centro> centro = criteria.from(Centro.class);
Join<Centro, Estudiante> estudiante = centro.join(Centro_.estudiantes, JoinType.INNER);
criteria.select(estudiante);
TypedQuery<Estudiante> query = em.createQuery(criteria);
query.getResultList().forEach(System.out::println); // Imprime estudiantes.
pero obsérvese que requiere que hubiéramos definido la relación como bidireccional.
Prudencia
Hemos afirmado que las consultas son equivalentes, pero no es tal, ya que en el primer caso hemos obtenido centros y en el segundo hemos obtenido estudiantes. La consulta realmente equivalente habría exigido no obtener una entidad pura. Por ejemplo:
// SELECT e.*, c.* FROM Estudiante e JOIN Centro c ON e.centro = c.id;
record EstudianteCentro(Estudiante estudiante, Centro centro) {};
CriteriaQuery<EstudianteCentro> criteria = cb.createQuery(EstudianteCentro.class);
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
Join<Estudiante, Centro> centro = estudiante.join(Estudiante_.centro, JoinType.INNER);
var estudiante_a = estudiante.alias("estudiante");
var centro_a = centro.alias("centro");
criteria.select(EstudiateCentro.class, estudiante_a, centro_a);
De los dos ejemplos anteriores se pueden sacar tres conclusiones:
La relación se establece entre una entidad y el atributo que la relaciona con la otra como en JPQL y a diferencia de lo que ocurre en SQL, en que se establece la relación entre dos tablas. Por ese motivo, el segundo ejemplo es imposible de llevar a cabo si no se hizo la relación bidireccional.
A diferencia de lo que ocurre en la consulta SQL, el primero de los ejemplos no devuelve centros duplicados, aunque varios alumnos compartan un mismo centro. Esto se debe al propio comportamiento de JPA. Por ese motivo, el join explícito no se diferencia en nada de hacer un join implícito como este:
// Aunque no se exprese el JOIN, éste se realiza para obtener objetos Centro: // SELECT c.* FROM Estudiante e JOIN Centro c ON e.centro = c.id; CriteriaQuery<Centro> criteria = cb.createQuery(Centro.class); Root<Estudiante> estudiante = criteria.from(Estudiante.class); criteria.select(estudiante.get(Estudiante_.centro)); TypedQuery<Centro> query = em.createQuery(criteria); query.getResultList().forEach(System.out::println); // Imprime centros.
¿Por qué es implícito? Porque se piden objetos
Centrocompletos y para ello JPA no tiene más remedio que recurrir a la relación. Hay aquí que hacer apostilla importante, si no se pidieran objetosCentro, sino objetosEstudiante, pero alguna de las condiciones de la consulta (clausulaWHERE) involucrara a algún atributo deCentro, entonces JPA no tendría más remedio que internamente construir la consulta usando un JOIN. Ahora bien, eso no implica que en los estudiantes obtenidos el atributoCentrose haya obtenido también. Si la carga se hubiera definido como perezosa, a pesar del JOIN, no se habrían obtenidos los centros relacionados y cargar el centro de un estudiante concreto implicaría una consulta adicional.Para emular el comportamiento de SQL y obtener centros repetidos tenemos que evitar obtener entidades puras:
// SELECT c.* FROM Estudiante e INNER JOIN Centro c ON e.centro = c.id; CriteriaQuery<Tuple> criteria = cb.createQuery(Tuple.class); Root<Estudiante> estudiante = criteria.from(Estudiante.class); Join<Estudiante, Centro> centro = estudiante.join(Estudiante_.centro, JoinType.INNER); var centro_a = centro.alias("centro"); criteria.select(cb.tuple(centro_a)); TypedQuery<Tuple> query = em.createQuery(criteria); query.getResultList().forEach(t -> { // Imprime centros repetidos. System.out.println("%s.\n", t.get("centro", Centro.class)); });
En este último caso, podría evitarse la repetición usando
DISTINCT, tal como se hace en SQL:criteria.select(cb.tuple(centro_a)) .distinct(true);
Carga inmediata
En los tres ejemplos en los que obtenemos centros (el join implícito, el join explícito y el join explícito que obtiene tuplas), el hecho de que pidamos centros provoca que estos se carguen directamente gracias a la consulta). Sin embargo, si nuestra consulta hubiera sido una en la que se usa carga perezosa como:
// SELECT c.* FROM Centro c;
CriteriaQuery<Centro> criteria = cb.createQuery(Centro.class);
Root<Centro> centro = criteria.from(Centro.class);
criteria.select(centro);
TypedQuery<Centro> query = em.createQuery(criteria);
List<Centro> centros = query.getResultList();
Esos centros, como son entidad padre de estudiante, no han cargado de forma
efectiva la lista de estudiantes matriculados, ya que de forma predeterminada la
carga es perezosa. En consecuencia, usar .getEstudiantes() implica una
consulta posterior para cada uno de los centros. Añadir la línea:
Join<Centro, Estudiante> estudiante = centro.join(Centro_.estudiantes, JoinType.INNER);
no provoca la carga inmediata, porque aunque forzamos a que se construya la
consulta con un JOIN, no se obtienen expresamente estudiantes en la consulta;
y el ORM construye los objetos Centro sin atender a que en la consulta se
obtuvieron estudiantes. La consecuencia es que el JOIN es inútil y hemos hecho
una consulta a la base de datos más costosa para nada.
Si nuestra intención es obligar a una carga inmediata gracias al JOIN interno, debemos añadir esta línea:
// SELECT c.* FROM Centro c INNER JOIN Estudiante e ON c.id = e.centro;
CriteriaQuery<Centro> criteria = cb.createQuery(Centro.class);
Root<Centro> centro = criteria.from(Centro.class);
centro.fetch(Centro_.estudiantes, TypeJoin.INNER);
criteria.select(centro);
TypedQuery<Centro> query = em.createQuery(criteria);
List<Centro> centros = query.getResultList();
Ver también
Consulte para más información el epígrafe dedicado a optimización.
LEFT JOIN
Una relación LEFT JOIN obtiene los registros relacionados entre sí (como
INNER JOIN) más aquellos presentes en la entidad de la izquierda que no
están relacionados con ninguno de la derecha. Debido a ello, los campos de la
derecha aparecen a NULL en estos registros adicionales. Por ese motivo:
SELECT e.*,c.* FROM Estudiante e LEFT JOIN Centro c ON e.centro = c.id;
no equivale a:
SELECT e.*,c.* FROM Centro c LEFT JOIN Estudiante e ON e.centro = c.id;
ya que la primera consulta, además de los estudiantes matriculados, devuelve los no matriculados; pero no los centros sin estudiantes; mientras que la segunda consulta devuelve los estudiantes matriculados y los centros sin estudiantes; pero no los estudiantes no matriculados.
La primera relación se escribe en Criteria API así:
// SELECT e.*,c.* FROM Estudiante e LEFT JOIN Centro c ON e.centro = c.id;
record EstudianteCentro(Estudiante estudiante, Centro centro) {};
CriteriaQuery<EstudianteCentro> criteria = cb.createQuery(EstudianteCentro.class);
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
Join<Estudiante, Centro> centro = estudiante.join(Estudiante_.centro, JoinType.LEFT);
criteria.select(cb.construct(EstudianteCentro.class, estudiante.alias("estudiante"), centro.alias("centro")));
mientras que la segunda:
// SELECT e.*,c.* FROM Centro c LEFT JOIN Estudiante e ON e.centro = c.id;
record EstudianteCentro(Estudiante estudiante, Centro centro) {};
CriteriaQuery<EstudianteCentro> criteria = cb.createQuery(EstudianteCentro.class);
Root<Centro> centro = criteria.from(Centro.class);
Join<Centro, Estudiante> estudiante = centro.join(Centro_.estudiantes, JoinType.LEFT);
criteria.select(cb.construct(EstudianteCentro.class, estudiante.alias("estudiante"), centro.alias("centro")));
Pero nótese que esta segunda requiere una relación bidireccional.
Es muy común usar LEFT JOIN para obtener indirectamente los centros sin
alumnos matriculados:
// SELECT c.* FROM Centro c LEFT JOIN Estudiante e ON c.id = e.centro WHERE e.id IS NULL;
CriteriaQuery<Centro> criteria = cb.createQuery(Centro.class);
Root<Centro> centro = criteria.from(Centro.class);
Join<Centro, Estudiante> estudiante = centro.join(Centro_.estudiantes, JoinType.LEFT);
criteria.select(centro).where(cb.isNull(estudiante.get("id")));
Advertencia
JPA no define las relaciones RIGHT JOIN, ya que pueden considerarse
redundantes al ser equivalentes a una relación LEFT JOIN con las
entidades cambiadas de orden. Pero eso, como en el ejemplo de arriba, puede
obligarnos a tener que definir relaciones bidireccionales para determinadas
consultas.
Subconsultas
Criteria API también ofrece la posibilidad de hacer subconsultas. De hecho, el problema anterior podría haberse resuelto con esta otra consulta[3]:
SELECT c.* FROM Centro c WHERE c.id NOT IN (SELECT e.centro WHERE Estudiante e);
Esta solución, además, no obliga a hacer la relación bidireccional. Veamos cómo implementarla con Criteria API:
CriteriaQuery<Centro> criteria = cb.createQuery(Centro.class);
Root<Centro> centro = criteria.from(Centro.class);
// Subconsulta
Subquery<Long> subquery = criteria.subquery(Long.class) // e.centro es Long
Root<Estudiante> subEstudiante = subquery.from(Estudiante.class);
subquery.select(subEstudiante.get(Estudiante_.centro));
// Consulta con la subconsulta.
criteria.select(centro)
.where(cb.not(centro.in(subquery)));
Nota
Nótese que aquí podemos comparar directamente centros en vez de identificadores de centros que sería la traducción literal:
subquery.select(subEstudiante.get(Estudiante_.centro).get(Centro_.id));
// Consulta con la subconsulta.
criteria.select(centro)
.where(cb.not(centro.get(Centro_.id).in(subquery)));
O bien, si usamos el operador EXISTS de SQL:
SELECT c.* FROM Centro c WHERE NOT EXISTS (SELECT 1 FROM Estudiante e WHERE e.centro = c.id);
que traducido a Criteria API queda:
CriteriaQuery<Centro> criteria = cb.createQuery(Centro.class);
Root<Centro> centro = criteria.from(Centro.class);
// Subconsulta
Subquery<Long> subquery = criteria.subquery(Long.class) // 1 es Long
Root<Estudiante> subEstudiante = subquery.from(Estudiante.class);
subquery.select(cb.literal(1L))
// Como en el caso anterior, no es necesario comparar explícitamente IDs.
.where(cb.equal(subEstudiante.get(Estudiante_.centro), centro));
// Consulta con la subconsulta
criteria.select(centro)
.where(cb.not(cb.exists(subquery)));
Consejo
En términos de rendimiento, de las tres alternativas presentadas la más eficiente es esta última:
INrequiere generar toda las filas de la subconsulta y, además, genera resultados inesperados cuando algunos de los resultados de la subconsulta es nulo ya que en SQL estándar la operación2 IN (1, NULL)devuelveUNKNOWNnoFALSE. Justamente en este ejemplo, puede haber Estudiantes sin centro, por lo que tendríamos este problema y la alternativa ni siquiera es válida en este caso.LEFT JOINnecesita generar todas las consultas y luego filtrar aquellas nulas. Esto puede generar un resultado intermedio grande que muchas veces lo hace menos eficiente que la alternativa conEXISTS.EXISTSdetiene la comprobación cuando encuentra la primera coincidencia.
Cadena de consultas
Observemos parte del código ya presentado anteriormente:
CriteriaQuery<Estudiante> criteria = cb.createQuery(Estudiante.class);
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
Join<Estudiante, Centro> centro = estudiante.join(Estudiante_.centro, JoinType.INNER);
criteria.select(estudiante);
Partimos de Estudiante (estudiante) y relacionamos con Centro a través
del atributo centro de Estudiante, de ahí que la relación se haya escrito
utilizando el objeto estudiante.
O sea, para relacionar con Centro, juntamos estudiante (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 = estudiante.join(Estudiante_.centro, JoinType.INNER);
Join<Estudiante, Curso> curso = estudiante.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 = estudiante.join(Estudiante_.centro, JoinType.INNER);
Join<Centro, ComunidadA> comunidad = centro.join(Centro_.comunidad, JoinType.INNER);
5.6.2.1.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»:
// UPDATE Estudiante SET centro = NULL WHERE nombre LIKE 'J%';
CriteriaUpdate<Estudiante> update = cb.createCriteriaUpdate(Estudiante.class);
Root<Estudiante> estudiante = update.from(Estudiante.class);
update.set(estudiante.get(Estudiante_.centro), null);
update.where(cb.like(estudiante.get(Estudiante_.nombre), "J%"));
em.createQuery(update).executeUpdate();
También es posible borrar:
// DELETE FROM Estudiante WHERE nombre LIKE 'J%';
CriteriaDelete<Estudiante> delete = cb.createCriteriaDelete(Estudiante.class);
Root<Estudiante> estudiante = delete.from(Estudiante.class);
delete.where(cb.like(estudiante.get(Estudiante_.nombre), "J%"));
em.createQuery(delete).executeUpdate();
Notas al pie