5.6.2.1. JPQL#
Es un lenguaje de consulta prácticamente idéntico a SQL que sustituye tablas y campos por objetos y atributos; y abstrae de las particularidades de los SGBD. Deriva de HQL, un lenguaje de consulta que diseñó Hibernate, por lo que sus semejanzas son notorias.
5.6.2.1.1. Consulta básica#
Para obtener todos los elementos de un relación basta hacer:
// Construimos la consulta.
TypedQuery<Centro> query = em.createQuery("FROM Centro", Centro.class);
// Obtenemos resultados.
List<Centro> centros = query.getResultList();
donde la palabra Centro en la cadena no es el nombre de la tabla,
sino el nombre de la clase Centro
que, en este caso, coinciden.
Recordemos que el lenguaje hace referencia a objetos y atributos.
Nota
La cadena también podría haber sido «SELECT c FROM
Centro c
»
El segundo argumento de .createQuery
permite tipar el resultado de la
consulta (objetos Centro
en este caso).
Nota
Aunque no sea recomendable, podríamos no haber tipado la consulta:
Query query = em.createQuery("FROM Centro", Centro.class);
// warning: el compilador no puede asegurar el tipo del resultado.
List<Centro> centro = query.getResultList();
Podemos también obtener campos individuales en vez de objetos Estudiante
:
TypedQuery<String> query = em.createQuery("SELECT c.nombre FROM Centro c", String.class);
List<String> nombres = query.getResultList();
o un conjunto de campos individuales:
TypedQuery<Object[]> query = em.createQuery("SELECT c.id, c.nombre FROM Centro c", Object[].class);
List<Object[]> registros = query.getResultList();
for(Object[] registro: registro) {
Long id = (Long) registro[0];
String nombre = (String) registro[1];
System.out.printf("id=%d -- nombre=%s\n", id, nombre);
}
Prudencia
Los nombres son nombres de atributos, no nombres de columnas. Recuerde que siempre se refieren clases y atributos, no tablas y columnas.
Truco
Cuando se obtiene varias columnas, en vez de obtener un
Object[].class
se puede pedir la obtención de una tupla (Tuple
) y
asignar alias a los nombres, lo que hará más amable la obtención posterior de
los campos individules:
// Obsérvese los alias incluidos en la consulta: AS...
TypedQuery<Tuple> query = em.createQuery(
"SELECT c.id AS id, c.nombre AS nombre FROM Centro c",
Tuple.class
);
List<Tuple> campos = query.getResultList();
System.out.println("=== Lista de campos ===");
for(Tuple registro: campos) {
Long id = registro.get("id", Long.class);
String nombre = registro.get("nombre", String.class);
System.out.printf("ID=%d -- nombre=%s\n", id, nombre);
}
Sin alias en la consulta, aún podríamos haber obtenido valores con el ordinal:
Long id = registro.get(0, Long.class);
Una vez hemos tratado cómo construir la consulta, centrémos en la obtención de
resultados, esto es, en aquello que por ahora hemos despachado con un simple
.getResultList()
:
TypedQuery<Centro> query = em.createQuery("FROM Centro", Centro.class);
List<Centro> centros = query.getResultList();
Lo cierto es que, cuando los resultados son muchos, esta estrategia puede
resultarnos poco eficiente. Para empezar tenemos dos métodos que permiten
reducir el número de resultados: .setFirstResult
y .setMaxResult
, que se
corresponden el estándar SQL OFFSET i ROWS FETCH FIRST n ROWS ONLY
[1].
TypedQuery<Centro> query = em.createQuery("FROM Centro", Centro.class)
.setFirstResult(100) // Saltamos los 100 primeros registros.
.setMaxResult(10); // Obtenemos únicamente 10 registros.
Como se ve estos argumentos permite paginar la respuesta.
Además, hay tres métodos que permiten obtener resultados a partir de la consulta ya construida:
.getResultList()
,que como hemos visto devuelve una lista.
.getStreamList()
,que devuelve un flujo que va obteniendo de forma perezosa los resultados.
Advertencia
Como la obtención es perezosa, para que podamos consumir el flujo, el objeto
EntityManager
a partir del cual se construyó la consulta debe continuar abierto..getSingleResult()
,que se usa cuando se espera obtener un único resultado. Véase más adelante como se usa .getSingleResult.
5.6.2.1.2. Condiciones#
Como en el caso de SQL, JPQL permite aplicar condiciones usando la
sintaxis de WHERE
:
// Incluir valores dentro de la cadena no es recomendable
TypedQuery<Centro> centros = sesion.createQuery("FROM Centro c WHERE c.nombre LIKE '%Castillo%'", Centro.class);
Ahora bien, en este caso, es mejor parametrizar la consulta en vez de incluir directamente los valores dentro de la cadena:
TypedQuery<Centro> centros = sesion.createQuery("FROM Centro c WHERE c.nombre LIKE :patron", Centro.class)
.setParamenter("patron", "%Castillo%");
La principal limitación es que podemos usar los operadores básicos que
existen en SQL (como el LIKE
del ejemplo), pero no las funciones que
los SGBD tienen definidas y que, habitualmente, son exclusivas y no forman
parte del estándar. Por ejemplo, supongamos que queremos obtener los alumnos
con menos de 20 años. Hay tres posibilidades:
Que hubiéramos definido un campo calculado
edad
enEstudiante
. El problema de esta solución es que para que pueda usarse en la expresión debe ser un atributo persistente y almacenarse en la base de datos.Utilizar SQL nativo, que tiene el inconveniente de que depende del SGBD.
Buscarnos las vueltas para reducir la evaluación lógica a operadores sencillos. Por ejemplo, en este caso, podemos calcular en Java, qué fecha era hace 20 años para poder comparar directamente con el campo nacimiento.
LocalDate fecRef = LocalDate.now().minusYears(20); TypedQuery<Estudiante> estudiantes = em.createQuery("FROM Estudiante e WHERE e.edad > :limite", Estudiante.class) .setParameter("limite", fecRef);
Lo que sí podemos usar son los campos definidos por la relación bidireccional entre dos tablas, aunque no tengan reflejo en la base de datos. Por ejemplo:
TypedQuery<Estudiante> query = em.createQuery(
"SELECT c.estudiantes FROM Centro c WHERE c.nombre = :patron"
).setParameter("patron", "%Castillo%");
Prudencia
JPA aplana la lista, de modo que no se obtiene una lista de listas, sino, simplemente, una lista de estudiantes.
Cuando debido a la condición se espera obtener un único resultado (p.e. se usa
una clave primaria o un campo con valores únicos), puede usarse el método
.getSingleResult()
. Por ejemplo:
try(EntityManager em = emf.createEntityManager()) {
try {
TypedQuery<Centro> query = em.createQuery("FROM Centro c WHERE id = :idCentro", Centro.class)
.setParameter("idCentro", 11004866L);
Centro centro = query.getSingleResult();
System.out.println(centro);
}
catch(NoResultException err) {
System.err.println("No hay ningún centro con tal id");
}
catch(NonUniqueResultException err) {
// Esto no puede ocurrir nunca.
assert false: "Imposible que haya dos valores para una clave primaria";
}
}
5.6.2.1.3. Ordenación#
JPQL dispone de la cláusula ORDER BY
para ordenar los resultados:
TypedQuery<Estudiante> query = em.createQuery("FROM Estudiante e ORDER BY e.nombre DESC");
5.6.2.1.4. Agrupación#
También puede usarse GROUP BY
y funciones agregadas:
TypedQuery<Tuple> query = em.createQuery(
"SELECT e.centro.nombre AS nombre, COUNT(e) AS estudiantes FROM Estudiante e GROUP BY e.centro.nombre",
Tuple.class
);
List<Tuple> resultados = query.getResultList();
for(Tuple t: resultados) {
String nombre = t.get("nombre", String.class);
Long cantidad = t.get("estudiantes", Long.class);
System.out.println("%s: %d estudiantes", nombre, cantidad);
}
Nota
No es posible agrupar por e.centro
.
Nota
Obsérvese que al estar consultando Estudiante
, los centros sin
estudiantes no aparecen listados. En cambio, podríamos haber abordado la
consulta así aprovechando la relación bidireccional:
TypedQuery<Tuple> query = em.createQuery(
"SELECT c.nombre AS nombre, SIZE(c.estudiantes) AS estudiantes FROM Centro c GROUP BY c.nombre",
Tuple.class
);
List<Tuple> resultados = query.getResultList();
for(Tuple t: resultados) {
String nombre = t.get("nombre", String.class);
Integer cantidad = t.get("estudiantes", Integer.class);
System.out.println("%s: %d estudiantes", nombre, cantidad);
}
Y en este caso, sí aparecerán todos los centros.
5.6.2.1.5. Joins#
JPQL también permite hacer joins, la diferencia fundamental respecto a su equivalente de SQL es que no se usan las entidades sino las referencias entre ellas:
TypedQuery<Estudiante> query = em.createQuery(
"FROM Estudiante e JOIN e.centro c WHERE c.nombre = :nombre",
Estudiante.class
).setParameter("nombre", "IES Castillo de Luna");
Nota
La consulta es equivalente a esta otra:
TypedQuery<Estudiante> query = em.createQuery(
"SELECT e FROM Centro c JOIN c.estudiantes e WHERE c.nombre = :nombre",
Estudiante.class
).setParameter("nombre", "IES Castillo de Luna");
en que hemos intercambiado el orden de las entidades.
JPQL soporta tres joins distintos:
INNER JOIN
que es el que se ha escrito más arriba simplemente con
JOIN
, aunque se puede escribirINNER JOIN
si se desea. En este caso, los estudiantes sin centro asignado no se obtendrán.LEFT JOIN
Como en el caso de SQL, se obtendrán también los estudiantes no matriculados en ningún centro (o sea, no relacionados con ningún centro:
TypedQuery<Estudiante> query = em.createQuery( "FROM Estudiante e LEFT JOIN e.centro c WHERE c.nombre = :nombre" ).setParameter("nombre", "IES Castillo de Luna");
FETCH JOIN
Es una variante de
INNER JOIN
que obtiene los mismos resultados, pero aprovecha la consulta para cargar la entidad relacionada inmediatamente:// Obtiene estudiantes con su centro cargado. TypedQuery<Estudiante> query = sesion.createQuery( "FROM Estudiante e JOIN FETCH e.centro c" );
Nota
Obsérvese que no tiene sentido:
TypedQuery<Estudiante> query = sesion.createQuery( "SELECT e FROM Centro c JOIN FETCH c.estudiantes e" );
porque en lo devuelto (estudiantes) no hay ninguna lista de estudiantes que precargar. Lo que sí podría tener sentido es:
TypedQuery<Centro> query = sesion.createQuery( "FROM Centro c JOIN FETCH c.estudiantes e" );
Ver también
Consulte el epígrafe sobre optimización donde se discute con más detalle la carga perezosa.
5.6.2.1.6. Actualización y borrado#
Aunque menos habitual, JPQL también permite hacer operaciones de actualización y borrado:
// Desvincula de cualquier centro a las personas
// cuyo nombre empieza por "J".
int filasAfectadas = em.createQuery(
"UPDATE Estudiante SET centro = null WHERE nombre LIKE :patron"
).setParameter("patron", "J%")
.executeUpdate();
// Borra a todas las personas que se llaman Juan
int filasEliminadas = em.createQuery(
"DELETE FROM Estudiante WHERE nombre = :nombre"
).setParameter("nombre", "Juan")
.executeUpdate();
Notas al pie