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 en Estudiante. 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 escribir INNER 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