5. Consultas
Hasta ahora hemos explicado cómo se realiza la definición de entidades y relaciones en JPA. En esta sesión vamos a explicar el lenguaje de consultas JPQL, el API que utiliza JPA para realizar consultas sobre esas tablas definidas. Explicaremos las sentencias principales de este API, sin entrar en demasiados detalles sobre la estructura de clases en las que se van a guardar estas consultas. Lo haremos en la próxima sesión, donde explicaremos cómo definir las clases DAO y Service que trabajan con entidades y proporcionan la capa de negocio de la aplicación.
5.1. Definición de clases
Vamos a definir las consultas sobre las clases Empleado
, Proyecto
y CuentaCorreo
, definidas como sigue (todas con identificador autogenerado de tipo Long
):
Empleado
:
-
nombre
:String
-
sueldo
:Float
-
direccion
:Direccion
, clase embebida -
departamento
:Departamento
, relación muchos-a-uno (departamento de un empleado) -
proyectos
:Set<Proyecto>
, relación muchos-a-muchos (proyectos de un empleado) -
facturas
:Set<FacturaGasto>
, relación uno-a-muchos (gastos de un empleado)
Departamento
:
-
nombre
:String
-
empleados
:Set<Empleado>
, relación a-muchos (empleados de un departamento)
Proyecto
:
-
nombre
:String
-
empleados
:Set<Empleado>
, relación a-muchos (empleados que trabajan en el proyecto)
FacturaGasto
:
-
concepto
:String
-
cantidad
:Double
-
fecha
:Date
-
empleado
:Empleado
, relación a-uno (empleado asociado a la cuenta de correo)
Direccion
(Clase embebida):
-
campus
:String
-
edificio
:String
Código JPA de estas clases:
@Entity
public class Empleado {
@Id
private Long id;
private String nombre;
private Double sueldo;
@ManyToOne
private Departamento departamento;
@ManyToMany
private Set<Proyecto> proyectos;
@OneToMany(mappedBy = "empleado")
private Set<FacturaGasto> gastos;
...
}
@Entity
public class Departamento {
@Id
private Long id;
private String nombre;
@Embedded
private Direccion direccion;
@OneToMany(mappedBy = "departamento")
private Set<Empleado> empleados;
...
}
@Entity
public class Proyecto {
@Id
private Long id;
private String nombre;
@ManyToMany(mappedBy = "proyectos")
private Set<Empleado> empleados;
...
}
@Entity
public class FacturaGasto {
@Id
private Long id;
@ManyToOne
private Empleado empleado;
private String concepto;
private Double cantidad;
private Date fecha;
...
}
@Embeddable
public class Direccion {
private String campus;
private String edificio;
...
}
El diagrama de clases que genera IntelliJ con estas entidades es el siguiente:

5.2. Definición de consultas JPQL
El API JPA proporciona la interfaz Query
para configurar y ejecutar consultas. Podemos obtener una instancia que implemente esa interfaz mediante un dos de métodos del entity manager: createQuery()
y createNamedQuery()
. El primer método se utilizar para crear una consulta dinámica y el segundo una consulta con nombre.
Una vez obtenida la consulta podemos pasarle los parámetros con setParameter()
y ejecutarla. Se definen dos métodos para ejecutar consultas: el método getSingleResult()
que devuelve un Object
que es la única instancia resultante de la consulta y el método getResultList()
que devuelve una lista de instancias resultantes de la consulta.
5.2.1. Parámetros de las consultas
JPQL soporta dos tipos de sintaxis para la ligadura (binding) de parámetros: posicional y por nombre. Vemos un ejemplo del primer caso:
QUERY = "SELECT e
FROM Empleado e
WHERE e.departamento = ?1 AND
e.salario > ?2"
La segunda forma de definir parámetros es por nombre:
QUERY = "SELECT e
FROM Empleado e
WHERE e.departamento = :dept AND
e.salario > :sal"
Veremos en el siguiente apartado ejemplos en los que se liga parámetros determinados a esas posiciones o nombres.
5.2.2. Consultas dinámicas
Se puede crear una consulta dinámica pasándole al entity manager la cadena con la consulta al método createQuery
. JPA transforma en ese momento la consulta en el código SQL que se ejecutará en la base de datos.
Veamos un ejemplo de consulta, que utiliza el paso de parámetros por nombre visto anteriormente:
public double salarioPorDeptoNombre(String nombreDepto, String nombreEmpleado) {
final String SALARIO_POR_DEPTO_NOMBRE = "SELECT e.sueldo " +
"FROM Empleado e " +
"WHERE e.departamento.nombre = :deptNombre AND " +
" e.nombre = :empNombre";
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
TypedQuery<Double> query = em.createQuery(SALARIO_POR_DEPTO_NOMBRE, Double.class)
.setParameter("deptNombre", nombreDepto)
.setParameter("empNombre", nombreEmpleado);
double sueldo = query.getSingleResult();
System.out.println(sueldo);
em.getTransaction().commit();
em.close();
return sueldo;
}
La clase TypedQuery
se introdujo en el API JPA 2.0 y es recomendble usarlo. Se construye la query pasando al método createQuery
un segundo parámetro con la clase del tipo devuelto por la query. De esta forma se evita tener que hacer un casting de lo que devuelve query.getSingleResult()
.
Un inconveniente de las consultas dinámicas es que se procesan en tiempo de ejecución de la aplicación, con la consiguiente pérdida de rendimiento. Su ventaja principal es que se pueden definir con mucha comodidad.
5.2.3. Consultas con nombre
Las consultas con nombre se definen junto con alguna entidad, utilizando la anotación @NamedQuery
. Mejoran el rendimiento con respecto a las consultas dinámicas, ya que son procesadas una única vez. El siguiente código muestra un ejemplo:
@Entity
@NamedQuery(name="Empleado.salarioPorNombreDepartamento",
query="SELECT e.sueldo " +
"FROM Empleado e " +
"WHERE e.departamento.nombre = : deptNombre AND " +
" e.nombre = : empNombre")
public class Empleado implements Serializable {
...
}
El nombre de la consulta es una cadena única. Utilizamos el convenio de colocar el nombre de la clase al comienzo de la cadena, separado por un punto del resto. Las consultas pueden definirse en cualquier clase de entidad y este convenio permite que sea la definición de la consulta sea fácil de localizar.
Si se necesita más de una consulta para una entidad, deben incluirse en la anotación @NamedQueries
, que acepta una array de una o más anotaciones @NamedQuery
:
@Entity
@NamedQueries({
@NamedQuery(name="Empleado.findAll",
query="SELECT e FROM Empleado e"),
@NamedQuery(name="Empleado.findById",
query="SELECT e FROM Empleado e WHERE e.id = :id"),
@NamedQuery(name="Empleado.findByNombre",
query="SELECT e FROM Empleado e WHERE e.nombre = :nombre")
})
public class Empleado implements Serializable {
...
}
Para ejecutar la consulta hay que llamar al método createNamedQuery
pasándole como parámetro el nombre de la consulta. El siguiente código muestra dos ejemplos:
public Empleado findEmpleadoPorNombre(String nombreEmpleado) {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
TypedQuery<Empleado> queryEmpleado =
em.createNamedQuery("Empleado.findByNombre", Empleado.class)
.setParameter("nombre", nombreEmpleado);
Empleado empleado = queryEmpleado.getSingleResult();
em.getTransaction().commit();
em.close();
return empleado;
}
public List<Empleado> findAllEmpleados() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
TypedQuery<Empleado> queryListaEmpleados =
em.createNamedQuery("Empleado.findAll", Empleado.class);
List<Empleado> empleados = queryListaEmpleados.getResultList();
em.getTransaction().commit();
em.close();
return empleados;
}
5.2.4. Ejecución de consultas
Veamos por último los métodos del interfaz Query
para ejecutar las consultas definidas. Son los métodos getSingleResult()
y getResultList()
.
Ambos métodos se deben lanzar sobre una Query
ya construida y en la que se han introducido los parámetros. El método getSingleResult()
se utiliza con consultas que devuelven un único resultado. Devuelve un Object
que contiene el resultado de la consulta. Después de llamarlo conviene hacer un casting al tipo (entidad o tipo básico) que esperamos. Puede suceder que la consulta ejecutada no devuelva ningún resultado o devuelva más de uno. En el primer caso se genera la excepción NoResultException
y en el segundo NonUniqueResultException
.
Si no tenemos seguridad de una consulta vaya a devolver un único valor deberíamos llamar a getResultList()
. Este método devuelve una List
de Object
. La utilización de la interfaz List
en lugar de Collection
es para soportar la devolución de colecciones ordenadas. En el caso en que la consulta no obtenga resultados, se devuelve una lista vacía. El siguiente código muestra un ejemplo de utilización de una consulta:
public void imprimeEmpleadosProyecto() {
List<Empleado> empleados = findEmpleadosProyecto("Reconocimiento de caras");
for (Empleado empleado : empleados) {
System.out.println(empleado.getNombre());
}
}
private List<Empleado> findEmpleadosProyecto(String nombreProyecto) {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
TypedQuery<Empleado> queryEmpleadosProyecto =
em.createQuery(
"SELECT e " +
"FROM Proyecto p JOIN p.empleados e " +
"WHERE p.nombre = ?1", Empleado.class);
List<Empleado> empleados = queryEmpleadosProyecto
.setParameter(1, nombreProyecto)
.getResultList();
em.getTransaction().commit();
em.close();
return empleados;
}
5.3. Java Persistence Query Language
JPQL es el lenguaje en el que se construyen las queries en JPA. Aunque en apariencia es muy similar a SQL, en la realidad operan en mundos totalmente distintos. En JPQL las preguntas se construyen sobre clases y entidades, mientras que SQL opera sobre tablas, columnas y filas en la base de datos.
Una consulta JPQL puede contener los siguientes elementos
SELECT c
FROM Category c
WHERE c.categoryName LIKE :categoryName
ORDER BY c.categoryId
-
Una cláusula
SELECT
que especifica el tipo de entidades o valores que se recuperan -
Una cláusula
FROM
que especifica una declaración de entidad que es usada por otras cláusulas -
Una cláusula opcional
WHERE
para filtrar los resultados devueltos por la query -
Una cláusula opcional
ORDER
BY para ordenar los resultados devueltos por la query -
Una cláusula opcional
GROUP BY
para realizar agregación -
Una cláusula opcional
HAVING
para realizar un filtrado en conjunción con la agregación
5.3.1. Consultas básicas
La consulta más sencilla en JPQL es la que selecciona todas las instancias de un único tipo de entidad:
SELECT e FROM Empleado e
La sintaxis de JPQL es similar a la de SQL. De esta forma los desarrolladores con experiencia en SQL pueden comenzar a utilizarlo rápidamente. La diferencia principal es que en SQL se seleccionan filas de una tabla mientras que en JPQL se seleccionan instancias de un tipo de entidad del modelo del dominio. La cláusula SELECT
es ligeramente distinta a la de SQL listando sólo el alias e
del Empleado
. Este tipo indica el tipo de datos que va a devolver la consulta, una lista de cero o más instancias Empleado
.
Código que ejecuta la consulta:
final String FIND_ALL_EMPLEADOS = "SELECT e FROM Empleado e ";
TypedQuery<Empleado> query = em.createQuery(FIND_ALL_EMPLEADOS, Empleado.class);
List<Empleado> empleados = query.getResultList();
if (empleados != null) {
for (Empleado empleado : empleados) {
System.out.println(empleado.getNombre() + " - " +
empleado.getDespacho().getCodigoDespacho());
}
}
La consulta devuelve una colección de entidades, junto con sus grafos de objetos relacionados cargados en memoria (de aquellas relaciones que sean EAGER). Por ejemplo cada empleado contiene también su despacho y todas las entidades con las que está relacionado.
Podemos restringir el tipo de dato devuelto. Por ejemplo, si sólo queremos los sueldos de los empleados podemos definir la siguiente consulta:
SELECT e.sueldo FROM Empleado e
Código que ejecuta la consulta:
final String FIND_ALL_SUELDOS_EMPLEADOS = "SELECT e.sueldo FROM Empleado e ";
TypedQuery<Double> query = em.createQuery(FIND_ALL_SUELDOS_EMPLEADOS, Double.class);
List<Double> sueldosEmpleados = query.getResultList();
if (sueldosEmpleados != null) {
for (double sueldoEmpleado : sueldosEmpleados) {
System.out.println(sueldoEmpleado);
}
}
Podemos también seleccionar una entidad cuyo tipo no listamos en la cláusula SELECT
, utilizando los atributos relación. Por ejemplo:
SELECT e.departamento FROM Empleado e
EL código que ejecuta la consulta:
final String FIND_ALL_DEPARTAMENTOS = "SELECT e.departamento FROM Empleado e ";
TypedQuery<Departamento> query = em.createQuery(FIND_ALL_DEPARTAMENTOS, Departamento.class);
List<Departamento> departamentos = query.getResultList();
if (departamentos != null) {
for (Departamento departamento : departamentos) {
System.out.println(departamento.getNombre());
}
}
Debido a la relación muchos-a-uno entre Empleado
y Departamento
la consulta devolverá una lista de instancias de tipo Departamento
. La consulta devuelve una lista con tantos elementos como empleados hayan definidos, con los departamentos repetidos.
5.3.2. Filtrando los resultados
Igual que SQL, JPQL contiene una cláusula WHERE
para especificar condiciones que deben cumplir los datos que se devuelven. La mayoría de los operadores disponibles en SQL están en JPQL, incluyendo las operaciones básicas de comparación: IN
, LIKE
y BETWEEN
, funciones como SUBSTRING
o LENGTH
y subqueries. Igual que antes, la diferencia fundamental es que se utilizan expresiones sobre entidades y no sobre columnas. El siguiente código muestra un ejemplo:
SELECT e
FROM Empleado e
WHERE e.departamento.nombre LIKE '%IA' AND
e.sueldo BETWEEN 2000 AND 2500
El código Java es el siguiente:
final String FIND_ALL_EMPLEADOS_WHERE = "SELECT e FROM Empleado e " +
"WHERE e.departamento.nombre LIKE '%IA' AND" +
" e.sueldo BETWEEN 2000 AND 2500";
TypedQuery<Empleado> query = em.createQuery(FIND_ALL_EMPLEADOS_WHERE, Empleado.class);
List<Empleado> empleados = query.getResultList();
if (empleados != null) {
for (Empleado empleado : empleados) {
System.out.println(empleado.getNombre() + " - " +
empleado.getSueldo();
}
}
}
5.3.3. Proyectando los resultados
Es posible recuperar un conjunto de atributos de las instancias, sin que formen una entidad. Esto es útil cuando tenemos una gran cantidad de atributos (columnas) y sólo vamos a necesitar listar algunos. Por ejemplo, la siguiente consulta selecciona sólo el nombre y el salario de los empleados:
SELECT e.nombre, e.sueldo
FROM Empleado e
El resultado será una colección de cero o más instancias de arrays de tipo Object
. Cada array contiene dos elementos, el primero un String
y el segundo un Double
:
final String FIND_NOMBRE_SUELDO = "SELECT e.nombre, e.sueldo FROM Empleado e";
TypedQuery<Object[]> query = em.createQuery(FIND_NOMBRE_SUELDO, Object[].class);
List<Object[]> tuplas = query.getResultList();
if (tuplas != null) {
for (Object[] tupla : tuplas) {
String nombre = (String) tupla[0];
Double sueldo = (Double) tupla[1];
System.out.println(nombre + " - " + sueldo);
}
}
5.3.4. Joins entre entidades
Al igual que en SQL es posible definir consultas que realicen una selección en el resultado de unir (join) entidades entre las que se ha establecido una relación a-muchos. Es posible hacerlo sin necesidad de escribir explícitamente la sentencia JOIN
. Por ejemplo, el siguiente código busca los gastos de todos los empleados del departamento DCCIA
y devuelve el String
concepto
:
SELECT f.concepto
FROM Empleado e, FacturaGasto f
WHERE e = f.empleado AND
e.departamento.nombre='DCCIA'
Código Java:
final String FIND_GASTOS_DCCIA = "SELECT f.concepto" +
" FROM Empleado e, FacturaGasto f" +
" WHERE e = f.empleado AND" +
" e.departamento.nombre='DCCIA'";
TypedQuery<String> query = em.createQuery(FIND_GASTOS_DCCIA, String.class);
List<String> conceptos = query.getResultList();
if (conceptos != null) {
for (String concepto : conceptos) {
System.out.println(concepto);
}
}
También es posible especificar joins en la cláusula FROM
utilizando el operador JOIN
. Una ventaja de este operador es que el join puede especificarse en términos de la propia asociación y que el motor de consultas proporcionará automáticamente el criterio de join necesario cuando genere el SQL. El siguiente código muestra la misma consulta reescrita para utilizar este operador:
SELECT f.concepto
FROM Empleado e JOIN e.gastos f
WHERE e.departamento.nombre='DCCIA'
Para especificar el JOIN
hay que indicar la entidad que tiene la relación a-muchos, el atributo que define la relación a-muchos y el alias que instancia los registros de la otra entidad.
JPQL soporta múltiples tipos de joins, incluyendo inner y outer joins, left joins y una técnica denominada fetch joins para cargar datos asociados a las entidades que no se devuelven directamente. No tenemos tiempo de detallar todos ellos. Vamos a ver algunos ejemplos más, para tener una idea de la potencia de la sintaxis.
Selecciona todos los departamentos distintos asociados a empleados (sin el operador JOIN
):
SELECT DISTINCT e.departamento FROM Empleado e
Otra forma de hacer la misma consulta utilizando el operador JOIN
:
SELECT DISTINCT d FROM Empleado e JOIN e.departamento d
Selecciona los proyectos distintos que pertenecen a empleados del departamento DLSI
:
SELECT DISTINCT p
FROM Departamento d JOIN d.empleados e JOIN e.proyectos p
WHERE d.nombre='DLSI'
Otro ejemplo algo más complicado. Selecciona los departamentos en el campus UA
en donde trabajan empleados que participan en el proyecto Reconocimiento de caras
:
SELECT DISTINCT d
FROM Empleado e JOIN e.departamento d JOIN e.proyectos p
WHERE d.direccion.campus='UA' AND
p.nombre='Reconocimiento de caras'
Si cambiáramos en Empleado
el tipo de recuperación de departamento
a LAZY
, la siguiente query recuperaría forzaría la recuperación:
SELECT e FROM Empleado e JOIN FETCH e.departamento
Podríamos comprobar el funcionamiento de este último ejemplo con el siguiente código.
En la clase Empleado
:
@Entity
public class Empleado {
...
@ManyToOne(fetch = FetchType.LAZY)
private Departamento departamento;
...
}
El código que ejecuta la consulta:
private static final String JOIN_FETCH =
"SELECT e FROM Empleado e JOIN FETCH e.departamento";
query = em.createQuery(JOIN_FETCH);
List<Empleado> empleados = query.getResultList();
em.close(); (1)
if (empleados != null) {
for (Empleado empleado : empleados) {
System.out.println(empleado.getDepartamento().getDireccion().getEdificio());
}
}
1 | Se cierra el entity manager para comprobar que se han cargado la relación LAZY definida con Departamento y que se recupera en empleado.getDepartamento() |
5.3.5. Paginación de resultados
Es posible realizar un paginado de los resultados, definiendo un número máximo de instancias a devolver en la consulta y un número de instancia a partir del que se construye la lista:
Query q = em.createQuery("SELECT e FROM Empleado e");
q.setFirstResult(20);
q.setMaxResults(10);
List empleados = q.getResultList();
5.3.6. Subqueries
Es posible anidar múltiples queries. Podemos utilizar el operador IN
para obtener las instancias que se encuentran en el resultado de la subquery. Por ejemplo, la siguiente expresión devuelve los empleados que participan en proyectos de tipo 'A'.
SELECT e FROM Empleado e
WHERE e.proyecto IN (SELECT p
FROM Proyecto p
WHERE p.tipo = 'A')
5.3.7. Valores nulos y colecciones vacías
Es posible utilizar los operadores IS NULL
o IS NOT NULL
para chequear si un atributo es o no nulo:
WHERE e.departamento IS NOT NULL
En el caso de colecciones hay que utilizar los operadores IS EMPTY
o IS NOT EMPTY
:
WHERE e.proyectos IS EMPTY
5.3.8. Funciones
Es posible utilizar llamadas a funciones predefinidas en JPQL. Veamos unos cuantos ejemplos:
-
CONCAT
concatena dos cadenas
CONCAT(string1, string2)
Por ejemplo:
WHERE CONCAT(e.nombre, e.apellido) LIKE 'Ju%cia'
-
SUBSTRING
extra una subcadenaSUBSTRING(string, position, length)
-
LOWER
devuelve los caracteres de una cadena convertidos en minúsculasLOWER(string)
-
UPPER
devuelve los caracteres de una cadena convertidos en mayúsculasUPPER(string)
-
LENGTH
devuelve la longitud de una cadenaLENGTH(string)
-
SIZE
devuelve el tamaño de una colección
WHERE SIZE (e.proyectos) > 4
-
CURRENT_TIME
,CURRENT_DATE
,CURRENT_TIMESTAMP
devuelven el tiempo del momento actual
5.3.9. Pattern matching
Veamos el conjunto de funciones y operadores que permiten introducir _pattern matching_ sobre campos de texto en las búsquedas.
LIKE
El operador LIKE
permite buscar un patrón en un texto y comprueba si una cadena especificada cumple un patrón especificado. Si se precede con el operador NOT
devuelve aquellos valores que no cumplen el patrón. El patrón puede incluir cualquier carácter y los siguientes caracteres libres:
-
El carácter tanto por ciento (%) empareja con 0 o más caracteres cualesquiera
-
El carácter subrayado (_) empareja un único carácter cualquiera
El operando izquierdo es siempre la cadena que se comprueba y el derecho el patrón. Ejemplos:
-
c.name LIKE '_r%'
esTRUE
para 'Brasil' and FALSE for 'Dinamarca' -
c.name LIKE '%'
es siempreTRUE
-
c.name NOT LIKE '%'
es siempreFALSE
Para emparejar un carácter subrayado o tanto por ciento tiene que ir precedido por un carácter de escape. Por ejemplo:
-
'100%' LIKE '%\%' ESCAPE '\'
se evalua aTRUE
. -
'100' LIKE '%\%' ESCAPE '\'
se evalua aFALSE
.
En las expresiones de arriba sólo el primer carácter tanto por ciento (%) representa un carácter libre. El segundo (que aparece tras el carácter de escape) representa un carácter %
real %.
LOCATE
El operador LOCATE(str, substr [, start])
busca una subcadena y devuelve su posición (comenzando a contar por 1). El tercer argumento, si está presente, especifica la posición a partir de la que comienza la búsqueda. Se devuelve 0 si no se encuentra la subcadena. Por ejemplo:
-
LOCATE('India', 'a')
devuelve 5 -
LOCATE('Alicante', 'a', 3)
devuelve 5 -
LOCATE('Mejico', 'a')
devuelve 0
TRIM
El operador TRIM([[LEADING|TRAILING|BOTH] [char] FROM] str) devuelve una cadena después de haber eliminado los caracteres del comienzo (LEADING), del final (TRAILING) o ambos (BOTH). Si no se especifica el carácter, se considera el espacio en blanco. Ejemplos:
-
TRIM(' UK ')
devuelve 'UK' -
TRIM(LEADING FROM ' UK ')
devuelve 'UK ' -
TRIM(TRAILING FROM ' UK ')
devuelve ' UK' -
TRIM(BOTH FROM ' UK ')
devuelve 'UK' -
TRIM('A' FROM 'ARGENTINA')
devuelve 'RGENTIN'
SUBSTRING
El operador SUBSTRING(str, pos [, long])
devuelve una subcadena de una cadena específica a partir de la posición pos
y con el número de caracteres long
(opcional). Si no se especifica el número de caracteres se devuelve la cadena completa. Las posiciones comienzan en 1, a diferencia de Java en donde el comienzo de la cadena viene indicado por el 0.
-
SUBSTRING('Italia', 3)
devuelve 'aly' -
SUBSTRING('Italia', 3, 2)
devuelve 'al'
5.3.10. Agrupaciones
JPQL proporciona las siguientes funciones de agrupación: AVG
,COUNT
,MAX
,MIN
,SUM
que pueden ser aplicadas a un número de valores. El tipo devuelto por las funciones es Long
o Double
, dependiendo de los valores implicados en la operación.
Por ejemplo, para obtener el mayor sueldo de un conjunto de empleados:
SELECT MAX(e.sueldo)
FROM Empleado e
Para obtener el número de elementos que cumplen una condición:
SELECT COUNT(e)
FROM Empleado e
WHERE ...
Podemos realizar consultas más complicadas utilizando GROUP BY
y HAVING
. Por ejemplo, podemos obtener los empleados y el número de proyectos en los que participan de la siguiente forma:
SELECT p.empleado, COUNT(p)
FROM Proyecto p
GROUP BY p.empleado
El código para recorrer la lista resultante sería el siguiente:
List result = em.createQuery("SELECT p.empleado, COUNT(p)" +
"FROM Proyecto p GROUP BY p.empleado").getResultList();
Iterator res = result.iterator();
while (res.hasNext()) {
Object[] tupla = (Object[]) res.next();
Empleado emp = (Empleado) tupla[0];
long count = ((Long) tupla[1]).longValue();
...
}
La siguiente consulta combina todos los elementos. Primero se filtrarían los resultados con la cláusula WHERE
, después los resultados son agrupados y por último se comprueba la cláusula HAVING
. Devuelve los empleados que participan en más de 5 proyectos creados entre dos fechas dadas.
SELECT p.empleado, COUNT(p)
FROM Proyecto p
WHERE p.fechaCreacion is BETWEEN :date1 and :date2
GROUP BY p.empleado
HAVING COUNT(p) > 5
5.3.11. Order by
Es posible ordenar la lista resultante de la consulta por alguno de los campos usando la directiva ORDER BY
. Un ejemplo de su uso:
public void muestraEmpleadosProyecto(String nombreProyecto) {
List<Empleado> result = em.createQuery(
"SELECT e " +
"FROM Proyecto p JOIN p.empleados e " +
"WHERE p.nombre = ?1 " +
"ORDER BY e.name")
.setParameter(1, nombreProyecto)
.getResultList();
for (empleado : result) {
System.out.println(empleado.nombre);
}
}
5.4. API Criteria
El API Criteria de JPA se introdujo en la especificación 2.0 de JPA y permite la definición de consultas dinámicas mediante en tiempo de ejecución de objetos y métodos específicos que permiten configurar la query. Al construirse las queries paso a paso de forma programativa mediante llamadas a interfaces estrictas, es posible detectar errores en su construcción en tiempo de compilación. Esto supone una importante ventaja con respecto a JPQL, en donde la construcción de una query se hace a través del parseo de la cadena con la expresión JPQL y sólo se puede detectar el error en el momento de la ejecución.
Las queries del API criteria son dinámicas y no pueden ser compiladas por lo que no pueden beneficiarse de la pre-compilación realizada por algunos proveedores de JPA.
El API es bastante avanzado y necesitaríamos una sesión completa para introducir sus elementos básicos. Vamos a ver sólo un par de ejemplos muy sencillos para comprobar el aspecto del API.
5.4.1. Ejemplos
Consulta que devuelve todos los empleados:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Empleado> cq = cb.createQuery(Empleado.class);
Root<Empleado> e = cq.from(Empleado.class);
cq.select(e);
query = em.createQuery(cq);
List<Empleado> empleados = query.getResultList();
Consulta que devuelve todos los empleados con sueldo mayor de 2000:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Empleado> cq = cb.createQuery(Empleado.class);
Root<Empleado> e = cq.from(Empleado.class);
cq.select(e);
cq.where(cb.greaterThan(e.get("sueldo"), 2500.0));
Query query = em.createQuery(cq);
List<Empleado> emps = query.getResultList();
5.5. Organización de las queries en DAOs
Una forma de ordenar las queries es incluyéndolas en los DAO que gestionan los distintos tipos de entidades. En cada DAO declararemos las queries relacionadas. Por ejemplo, en el siguiente código de EmpleadoDao
se incluyen algunas de las consultas vistas anteriormente.
public class EmpleadoDao extends Dao<Empleado, Long> {
String FIND_ALL_EMPLEADOS = "SELECT e FROM Empleado e ";
String FIND_ALL_SUELDOS_EMPLEADOS =
"SELECT e.sueldo FROM Empleado e";
String FIND_PROJECTOS_EMPLEADOS_DEPARTAMENTO = "SELECT DISTINCT p" +
" FROM Departamento d JOIN d.empleados e JOIN e.proyectos p" +
" WHERE d.nombre='DLSI'";
public EmpleadoDao(EntityManager em) {
super(em);
}
@Override
public Empleado find(Long id) {
EntityManager em = this.getEntityManager();
return em.find(Empleado.class, id);
}
public List<Empleado> listAllEmpleados() {
EntityManager em = this.getEntityManager();
TypedQuery<Empleado> query = em.createQuery(FIND_ALL_EMPLEADOS, Empleado.class);
return query.getResultList();
}
public List<Double> listAllSueldosEmpleados() {
EntityManager em = this.getEntityManager();
TypedQuery<Double> query = em.createQuery(FIND_ALL_SUELDOS_EMPLEADOS, Double.class);
return query.getResultList();
}
public List<Proyecto> listAllProyectosEmpleadosDepartamento(String nombreDepto) {
EntityManager em = this.getEntityManager();
TypedQuery<Proyecto> query = em.createQuery(FIND_PROJECTOS_EMPLEADOS_DEPARTAMENTO, Proyecto.class);
return query.getResultList();
}
}
5.6. Ejercicios
Vamos a trabajar con el proyecto filmoteca
. Define las siguientes consultas en PeliculaDao
y ActorDao
. Para cada una de ellas escribe un programa main que compruebe su funcionamiento. Cada consulta se puntúa con 0,25 puntos.
5.6.1. (0,75 puntos) Definición de consultas en PeliculaDao
- Consulta 1
-
List<Pelicula> peliculaPorNombre(String nombre)
, que tiene como parámetro el nombre de una película y devuelve una lista de películas que contienen ese nombre. - Consulta 2
-
Double puntuacionMediaCriticas(Long idPeliculas)
, que tiene como parámetro el identificador de la película y devuelve la puntuación media de todas las críticas de esa película. - Consulta 3
-
List<NumActoresPelicula> peliculasNumActores()
, sin ningún parámetro que devuelve una colección deNumActoresPelicula
(un objeto que contiene un identificador de película, un nombre de película y un entero que representa el número de actores que intervienen en la película), ordenado de mayor número de actores a menor. La claseNumActoresPelicula
está definida en el mismo paquete que el Dao.
5.6.2. (0,75 puntos) Definición de consultas en ActorDao
- Consulta 1
-
List<Actor> actorPorNombre(String nombre)
, que tiene como parámetro el nombre de un actor y devuelve el una lista de actores que contienen ese nombre. - Consulta 2
-
List<Pelicula> peliculasConActor(Long idActor)
, que tiene como parámetro el identificador de un actor y devuelve todas las películas en las que participa. - Consulta 3
-
List<Critica> criticasActor(Long idActor)
, que tiene como parámetro el identificador de un actor y devuelve todas las críticas de las películas en las que aparece el actor. - Consulta 4
-
List<Actor> actoresConMasPeliculasDe(int n)
, que tiene como parámetro un número y devuelve una colección con todos los actores que han participado en más de n películas.