Introducción a JPA 2.0 (IV)

En el último artículo del tutorial de introducción a JPA vamos a presentar JPQL (Java Persistence Query Language, Lenguaje de Consulta de Persistencia en Java), un potente lenguaje de consulta orientado a objetos que va incluido con JPA. Aunque no es imprescindible, es recomendable tener unos conocimientos mínimos de lenguaje SQL. Si no es tu caso, algunos conceptos de JPQL te pueden parecer extraños. Además, independientemente del uso de sistemas ORM como JPA, siempre es aconsejable saber realizar consultas en una base de datos mediante SQL nativo.

En el primer y segundo artículo vimos como configurar nuestras entidades para el mapeo. En el tercer artículo vimos como realizar operaciones de persistencia a través de la interface EntityManager, además de como definir nuestros propios métodos callback y clases listener. Ahora es el momento de potenciar las capacidades de consulta de nuestra aplicación.

4.1 JPQL básico

Como vimos en el artículo anterior, al usar la interface EntityManager estamos limitados a realizar consultas en la base de datos proporcionando la identidad de la entidad que deseamos obtener, y solo podemos obtener una entidad por cada consulta que realicemos. JPQL nos permite realizar consultas en base a multitud de criterios (como por ejemplo el valor de una propiedad, o condiciones booleanas), y obtener más de un objeto por consulta. Veamos el ejemplo de sintaxis JPQL más simple posible:

SELECT p FROM Pelicula p
	

La sentencia anterior obtiene todas las instancias de la clase Pelicula desde la base de datos. La expresión puede parecer un poco extraña la primera vez que se ve, pero es muy sencilla de entender. Las palabras SELECT y FROM tienen un significado similar a las sentencias homónimas del lenguaje SQL, indicando que se quiere seleccionar (SELECT) cierta información desde (FROM) cierto lugar. La segunda p es un alias para la clase Pelicula, y ese alias es usado por la primera p (llamada expresion) para acceder a la clase (tabla) a la que hace referencia el alias, o a sus propiedades (columnas). El siguiente ejemplo nos ayudará a comprender esto mejor:

SELECT p.titulo FROM Pelicula p
	

El ejemplo anterior es más sencillo de comprender, ¿verdad?. El alias p nos permite utilizar la expresión p.titulo para obtener los títulos de todas las películas almacenadas en la base de datos. La expresiones JPQL utilizan la notación de puntos, convirtiendo tediosas consultas en algo realmente simple:

SELECT c.propiedad.subPropiedad.subSubPropiedad FROM Clase c
	

JPQL también nos permite obtener resultados en base a más de una propiedad:

SELECT p.titulo, p.duracion FROM Pelicula p
	

Todas las sentencias anteriores (que más tarde veremos como ejecutar) devuelven, o un único valor, o un conjunto de ellos. Podemos eliminar los resultados duplicados mediante la clausula DISTINCT:

SELECT DISCTINCT p.titulo FROM Pelicula p
	

Así mismo, el resultado de una consulta puede ser el resultado de una función agregada aplicada a la expresión:

SELECT COUNT(p) FROM Pelicula p
	

COUNT() es una función agregada de JPQL, cuya misión es devolver el número de ocurrencias tras realizar una consulta. Por tanto, en el ejemplo anterior, el valor devuelto por la función agregada es el resultado de la sentencia al completo. Otras funciones agregadas son AVG para obtener la media aritmética, MAX para obtener el valor máximo, MIN para obtener el valor mínimo, y SUM para obtener la suma de todos los valores.

4.2 Sentencias condicionales

Ahora que ya sabemos como realizar consultas básicas, vamos a introducir conceptos algo más complejos (pero aún simples). El primero de ellos es el de consulta condicional, el cual es aplicado añadiendo la clausula WHERE en nuestra sentencia JPQL. Mediante una consulta condicional, restringimos los resultados devueltos por una consulta, en base a ciertos criterios lógicos (desde ahora la mayoría de los ejemplos constarán de varias líneas, pero ten presente que todos ellos representan una única sentencia JPQL, no varias):

SELECT p FROM Pelicula p
WHERE p.duracion < 120
	

La sentencia anterior obtiene todas las instancias de Pelicula almacenadas en la base de datos con una duración inferior a 120 minutos. Esto es llevado a cabo gracias al operador de comparación menor qué (<). Las sentencias condicionales pueden contener más de una condición:

SELECT p FROM Pelicula p
WHERE p.duracion < 120 AND p.genero = 'Terror'
	

La sentencia anterior obtiene todas las instancias de Pelicula con una duración inferior a 120 minutos y cuya propiedad genero sea igual a Terror. Si en el caso anterior utilizamos un operador de comparación (<), en esta última sentencia hemos utilizado dos operadores de comparación (< y =), así como un operador lógico (AND). Los otros dos operadores lógicos disponibles en JPQL son OR y NOT. El primero de ellos, aplicado en el ejemplo anterior en lugar de AND, permitiría obtener todas películas con una duración inferior a 120 minutos, o las del género de Terror (solo una de las dos condiciones sería suficiente). El segundo de ellos, aplicado sobre un expresión, la niega:

SELECT p FROM Pelicula p
WHERE p.duracion < 120 AND NOT (p.genero = 'Terror')
	

Gracias a la sentencia anterior se obtendrían todas las instancias de Pelicula con una duración menor a 120 minutos que no (NOT) son del género Terror. Veamos otro operador de comparación:

SELECT p FROM Pelicula p
WHERE p.duracion BETWEEN 90 AND 150
	

La sentencia anterior obtiene todas las instancias de Pelicula con una duración entre (BETWEEN) 90 y (AND) 150 minutos. BETWEEN puede ser convertido en NOT BETWEEN, en cuyo caso se obtendrían todas las películas que una duración que no (NOT) se encuentren dentro del margen (BETWEEN) 90-150 minutos. Otro operador de comparación muy útil es [NOT] LIKE (NOT es opcional, como en los ejemplos anteriores), el cual nos permite comparar una cadena de texto completa o solo definida en parte (esto último gracias al uso de comodines) con los valores de una propiedad almacenada en la base de datos. Veamos un ejemplo para comprenderlo mejor:

SELECT p FROM Pelicula p
WHERE p.titulo LIKE 'El%'
	

La sentencia anterior obtiene todas las instancias de Pelicula cuyo título sea como (LIKE) El% (el simbolo de porcentaje es un comodín que indica que en su lugar pueden haber entre cero y más caracteres). Resultados devueltos por esta consulta incluirían películas con un título como El Caballero Oscuro, El Violinista en el Tejado, o si existe, El. El otro comodín aceptado por LIKE es el carácter de barra baja (_), el cual representa un único carácter indefinido (ni cero caracteres ni más de uno; uno y solo uno). JPQL dispone de muchos operadores tanto de comparación como lógicos, y todavía más combinaciones posibles entre ellos, motivo por el cual no vamos a entrar en más detalles sobre el tema; te recomiendo que consultes la referencia completa de JPQL (en inglés).

4.3 Parámetros dinámicos

Podemos añadir parámetros dinámicamente a nuestras sentencias JPQL de dos formas: por posición y por nombre. La sentencia siguiente acepta un parámetro por posicion (?1):

SELECT p FROM Pelicula p
WHERE p.titulo = ?1
	

Y la siguiente, acepta un parámetro por nombre (:titulo):

SELECT p FROM Pelicula p
WHERE p.titulo = :titulo
	

En el momento de realizar la consulta, deberemos pasar los valores con los que queremos que sean sustituidos los parámetros dinámicos que hemos definido. Veremos como realizar esta operación en la sección 4.8.

Cuando realizamos una consulta en la base de datos, podemos ordenar los resultados devueltos mediante la claúsula ORDER BY (ordenar por), la cual admite ordenamiento ascendente (mediante la claúsula ASC, comportamiento por defecto si omitimos el tipo de ordenamiento), o en orden descendiente (mediante la claúsula DESC):

SELECT p FROM Pelicula p
ORDER BY p.duracion DESC
	

La sentencia anterior podría tener una claúsula WHERE como las vistas en ejemplos anteriores entre SELECT y ORDER BY, para restringir los resultados devueltos. Además, puedes incluir múltiples expresiones de ordenación en la misma sentencia:

SELECT p FROM Pelicula p
WHERE p.genero = 'Comedia'
ORDER BY p.duracion DESC, p.titulo ASC
	

En la sentencia anterior, hemos filtrado la selección de películas a las del género de comedia (mediante la clausula WHERE), y hemos ordenado los resultados en base a dos criterios: por duración (DESC indica de mayor a menor duración en minutos), y entre las que tienen la misma duración, por título (ASC, que ya hemos dicho que es redundante por ser el comportamiento por defecto, indica de la A a la Z).

4.5 Operaciones de actualización

JPQL puede realizar operaciones de actualización en la base de datos mediante la sentencia UPDATE:

UPDATE Articulo a
SET a.descuento = 15
WHERE a.precio > 50
	

La sentencia anterior actualiza (UPDATE) todas las instancias de Articulo presentes en la base de datos cuyo precio (WHERE) sea mayor de 50, aplicándoles (SET) un descuento de 15.

4.6 Operaciones de borrado

De forma muy similar a lo visto en la sección anterior, JPQL puede realizar operaciones de borrado en la base de datos mediante la sentencia DELETE:

DELETE FROM Pelicula p
WHERE p.duracion > 190
	

La sentencia anterior elimina (DELETE) todas las instancias de Pelicula cuya duración sea mayor de 190. Ni que decir tiene que las sentencías UPDATE y DELETE deben ser usadas con cierta precaución, sobre todo cuando trabajamos con información que se encuentra en producción.

4.7 Ejecución de sentencias JPQL

El lenguaje JPQL es integrado a través de implementaciones de la interface Query. Dichas implementaciones se obtienen a través de nuestro querido amigo EntityManager, mediante diversos métodos de factoría. De estos, los tres más usados (y los únicos que explicaremos aquí) son:

createQuery(String jpql)
createNamedQuery(String name)
createNativeQuery(String sql)
	

Empecemos viendo como crear un objeto Query y realizar una consulta a la base de datos:

package es.davidmarco.tutorial.jpa; 

import java.util.List; 

import javax.persistence.EntityManager; 
import javax.persistence.EntityManagerFactory; 
import javax.persistence.EntityTransaction; 
import javax.persistence.Persistence; 
import javax.persistence.Query; 

public class Main { 

    public static void main(String[] args) { 
        EntityManagerFactory emf = Persistence 
                .createEntityManagerFactory("introduccionJPA"); 
        EntityManager em = emf.createEntityManager(); 
         
        String jpql = "SELECT p FROM Pelicula p"; 
        Query query = em.createQuery(jpql); 
        List<Pelicula> resultados = query.getResultList(); 
        for(Pelicula p : resultados) { 
            // ... 
        } 
         
        em.close(); 
        emf.close(); 
    } 
} 
	

En el ejemplo anterior, obtenemos una implementación de Query mediante el método createQuery(String) de EntityManager, al cual le pasamos una sentencia JPQL en forma de cadena de texto. Con el objeto Query ya inicializado, podemos realizar la consulta a la base de datos llamando a su método getResultList(), el cual devuelve un objeto List con todas las entidades devueltas por la sentencia JPQL. Esta sentencia es una sentencia dinámica, ya que es generada cada vez que se ejecuta. De manera adicional, el ejemplo nos muestra que al usar una colección parametrizada (List<Pelicula>) nos evitamos tener que hacer ningún tipo de casting al manejar las entidades (al fin y al cabo JPA está devolviendo entidades de clases concretas, así que podemos aprovechar esta circunstancia usando colecciones genéricas).

4.8 Ejecución de sentencias con parámetros dinámicos

En la sección 4.3 vimos como escribir sentencias JPQL con parámetros dinámicos. Ahora que hemos visto como funciona la interface Query, estamos listos para usar dichos parámetros dinámicos (y entender su utilidad):

String jpql = "SELECT p FROM Pelicula p WHERE p.duracion > ?1 AND p.genero = ?2"
Query query = em.createQuery(jpql);
query.setParameter(1, 180);
query.setParameter(2, "Accion");
List<Pelicula> resultados = query.getResultList();
	

En el ejemplo anterior hemos insertado dinámicamente (mediante el método setParameter() los valores deseados para las expresiones p.duracion y p.genero, que en la sentencia JPQL original se corresponden con los parámetros por posicion ?1 y ?2, respectivamente. El primer argumento que pasamos a setParameter() indica que parámetro por posición deseamos sustituir por el valor del segundo argumento. Si el valor que pasamos como segundo argumento no se corresponde con el valor esperado (por ejemplo, al pasar una cadena de texto donde se espera un valor numérico), la aplicación lanzará una excepción de tipo IllegalArgumentException. Esto también sucederá si intentamos dar un valor a un parámetro dinámico inexistente (como query.setParameter(3, "Valor") en nuestro ejemplo anterior).

El otro tipo de parámetro dinámico (por nombre) es tan fácil de aplicar como su versión numérica:

String jpql = "SELECT p FROM Pelicula p WHERE p.duracion > :duracion AND p.genero = :genero"
Query query = em.createQuery(jpql);
query.setParameter("duracion", 180);
query.setParameter("genero", "Accion");
List<Pelicula> resultados = query.getResultList();
	

En el ejemplo anterior, en lugar de utilizar ?1 y ?2 en la sentencia JPQL, hemos utilizado :duracion y :genero como parámetros dinámicos. Para poder darle valor a estos parámetros en el momento de realizar la consulta, setParameter() provee una versión cuyo primer argumento acepta un valor de tipo String con el que poder identificar el parámetro dinámico (query.setParameter("duracion", 180)). Que versión usar depende de preferencias personales, pues ambos cumplen exactamente la misma misión; sin embargo, es evidente que los parámetros dinámicos por nombre son más fáciles de identificar, entender, mantener, y un largo etcétera de ventajas.

4.9 Consultas con nombre (estáticas)

Las consultas con nombre son diferentes de las sentencias dinámicas que hemos visto hasta ahora en el sentido de que, una vez definidas, no pueden ser modificadas: son leídas y transformadas en sentencias SQL cuando el programa arranca por primera vez, en lugar de cada vez que son ejecutadas. Este comportamiento estático las hace más eficientes, y por tanto ofrecen un mejor rendimiento. Las consultas con nombre son definidas mediante metadatos (recuerda que los metadatos se definen mediante anotaciones o configuración XML), como puedes ver en este ejemplo:

@Entity
@NamedQuery(name="buscarTodos", query="SELECT p FROM Pelicula p")
public class Pelicula { ... }
	

El ejemplo anterior define una consulta con nombre a través de la anotación @NamedQuery. Esta anotación necesita dos atributos: name (que define el nombre de la consulta), y query (que define la sentencia JPQL a ejecutar). El nombre de la consulta debe ser único dentro de su unidad de persistencia, y por tanto no pueden existir dos entidades dentro de la citada unidad de persistencia que definan consultas estáticas con el mismo nombre. Para evitar que podamos modificar por error la sentencia, es una buena idea utilizar una constante definida dentro de la propia entidad, y usarla como nombre de la consulta:

@Entity 
@NamedQuery(name=Pelicula.BUSCAR_TODOS, query="SELECT p FROM Pelicula p") 
public class Pelicula { 
    public static final String BUSCAR_TODOS = "Pelicula.buscarTodos"; 
    // ... 
} 
	

De manera adicional, esto nos permite crear una consulta con el mismo nombre en múltiples entidades (como BUSCAR_TODOS), pues ahora el nombre de la entidad se encuentra incluido en el nombre de la consulta, y por tanto seguimos sin violar la regla de nombres únicos para todas las consultas dentro de la misma unidad de persistencia. Consultas similares con nombres similares en entidades distintas harán nuestro código más fácil de escribir y mantener.

Una vez definida la consulta con nombre, podemos crear el objeto Query necesario mediante el segundo método de la lista que vimos en la sección 4.7: createNamedQuery():

Query query = em.createNamedQuery(Pelicula.BUSCAR_TODOS);
List<Pelicula> resultados = query.getResultList();
	

createNamedQuery() requiere un parámetro de tipo String que contenga el nombre de la consulta (el cual hemos definido a través del parámetro name de @NamedQuery). Una vez creado el objeto Query, podemos trabajar con él de la manera habitual para obtener los resultados.

4.10 Consultas nativas SQL

El tercer y último tipo de consultas que nos queda por ver requiere una sentencia SQL nativa en lugar de una sentencia JPQL:

String sql = "SELECT * FROM PELICULA";
Query query = em.createNativeQuery(sql);
// ...
	

Las consultas SQL nativas pueden ser definidas de manera estática como hicimos con las consultas con nombre, obteniendo los mismos beneficios de eficiencia y rendimiento. Para ello, necesitamos utilizar, de nuevo, metadatos:

@Entity 
@NamedNativeQuery(name=Pelicula.BUSCAR_TODOS, query="SELECT * FROM PELICULA") 
public class Pelicula { 
    public static final String BUSCAR_TODOS = "Pelicula.buscarTodos"; 
    // ... 
}
	

Tanto las consultas con nombre como las consultas nativas SQL estáticas son muy útiles para definir consultas inmutables (como buscar todas las instancias de una entidad en la base de datos). Las candidatas son aquellas consultas que se mantienen inmutables entre distintas ejecuciones del programa (sin parámetros dinámicos), y que son usadas frecuentemente.

Resumen

A lo largo de este tutorial hemos visto gran parte de lo que nos ofrece JPA en su versión 2.0. Las características y posibilidades de esta API van mucho, mucho más lejos de lo que aquí he intentado exponer, así que ahora que (espero) ya te sientes cómodo con la especificación, puedes lanzarte de lleno y llevarla aún más alla.

Por último, gracias a todos los que seguís este blog de programación, y a los que con vuestros correos electrónicos me animaís a seguir publicando contenidos. No dudéis en enviarme todo el feedback que consideréis oportuno, así como críticas, notificación de erratas, ideas para futuros artículos, etc.