Science And Technology">
Frameworks de Persistencia - JPA
Frameworks de Persistencia - JPA
Frameworks de Persistencia - JPA
Tabla de contenidos
1. Primer contacto con JPA .................................................................................................. 4
1.1. Introducción a JPA ................................................................................................. 4
Los orígenes de JPA ............................................................................................. 4
Java Persistence API ............................................................................................ 5
Implementaciones de JPA ..................................................................................... 6
Para saber más ..................................................................................................... 7
1.2. Ejemplo práctico: una sencilla aplicación JPA ....................................................... 7
Configuración del proyecto Maven ........................................................................ 8
Configuración de la conexión a la BD de IntelliJ ................................................. 11
Clases entidad ..................................................................................................... 13
El fichero persistence.xml y la base de datos ..................................................... 18
Ficheros de configuración de logging .................................................................. 22
Programas Java standalone que trabajan con JPA ............................................. 22
Definición de tests con DbUnit ............................................................................ 28
1.3. Ejercicios .............................................................................................................. 32
(0,5 puntos) Construir el módulo mensajes ......................................................... 32
(1,5 puntos) Contruir el módulo filmoteca ............................................................ 33
2. Entity Manager y contexto de persistencia ..................................................................... 35
2.1. Introducción .......................................................................................................... 35
Entidades gestionadas y desconectadas ............................................................. 35
APIs de Java EE 7 .............................................................................................. 36
Obtención del entity manager factory y de los entity manager ............................ 37
Transacciones ...................................................................................................... 39
2.2. Operaciones CRUD del entity manager ............................................................... 39
Persist para hacer persistente una entidad ......................................................... 40
Find para buscar entidades por clave primaria ................................................... 41
Actualización de entidades .................................................................................. 43
Remove para borrar entidades ............................................................................ 43
2.3. Operaciones en cascada ...................................................................................... 45
Persist en cascada .............................................................................................. 45
Borrado en cascada ............................................................................................ 46
2.4. Queries ................................................................................................................. 46
2.5. Operaciones sobre el contexto de persistencia ................................................... 47
Sincronización con la base de datos con flush ............................................... 48
Desconexión de entidades .................................................................................. 49
Merge ................................................................................................................... 51
Actualización de las colecciones en las relaciones a-muchos ............................. 51
2.6. Implementación de un DAO con JPA .................................................................. 53
2.7. Ejercicios .............................................................................................................. 58
(0,5 puntos) Contexto de persistencia ................................................................. 58
(0,5 puntos) Clases DAO del módulo filmoteca ............................................ 58
(0,5 puntos) Clase PeliculaServicio .......................................................... 58
3. Mapeado entidad-relación: tablas ................................................................................... 59
3.1. Acceso al estado de la entidad ............................................................................ 59
Acceso por campo ............................................................................................... 59
1
Frameworks de persistencia - JPA
2
Frameworks de persistencia - JPA
3
Frameworks de persistencia - JPA
Entre los conceptos principales que trataremos sobre JPA destacamos los siguientes:
En este contexto se creó en mayo de 2003 el grupo de trabajo que iba a definir la siguiente
especificación de EJB (EJB 3.0). Sun propuso a Gavin King formar parte del grupo. Cuando
llegó el momento de decidir el modelo de gestión de entidades persistentes se decidió apostar
por la solución que ya había adoptado de hecho la comunidad: el enfoque basado en POJOs
de Hibernate. Tras tres años de trabajo, en abril de 2006 se realizó la votación que apruebó
la nueva especificación y ésta se incorporó a la especificación oficial de Java EE 5 con el
4
Frameworks de persistencia - JPA
Es posible utilizar JPA no sólo como parte de aplicaciones Java EE que corren en un servidor
de aplicaciones, sino también como una librería de acceso a datos en una aplicación Java
aislada (standalone). Lo haremos así en esta primera sesión de la asignatura.
JPA permite realizar el mapeo entre el esquema de datos y las clases Java de dos formas. Una
es partir de un esquema de datos ya existente y construir las clases Java a partir de él. La otra
es hacerlo al revés, definir las relaciones entre las clases Java mediante anotaciones y generar
el esquema de la base de datos a partir de ellas. Para un proyecto nuevo es recomendable
1
https://jcp.org/en/jsr/detail?id=338
5
Frameworks de persistencia - JPA
utilizar este último enfoque, ya que el modelo de clases es más restrictivo que los esquemas
SQL y si lo hacemos al revés podemos encontrar algunas relaciones en SQL difíciles de
expresar con JPA.
Las distintas versiones de JPA han ido haciendo cada vez más potente esta tecnología. Por
ejemplo, JPA 2.0 incluyó:
• Conversores que permiten customizar el código de conversión entre los tipos de las clases
java y los de la base de datos
• Actualizaciones y borrados en bloque mediante el API Criteria
• Consultas que ejecutan procedimientos almacenados en la base de datos
• Generación del esquema
• Grafos de entidades que permiten la recuperación o la mezcla parcial de objetos en
memoria
• Mejoras en el lenguaje de consultas JPQL/Criteria: subconsultas aritméticas, funciones
genéricas de base de datos, cláusula Join ON
Implementaciones de JPA
JPA es un estándar aprobado en un JSR que necesita ser implementado por desarrolladores
o empresas. Al ser una especificación incluida en Java EE cualquier servidor de aplicaciones
compatible con Java EE debe proporcionar una implementación de este estándar.
Sólo las implementaciones más avanzadas han implementado a fecha de hoy la última versión
2.1 de la especificación. Las implementaciones más populares son:
2
• Hibernate (JPA 2.1, usado por JBoss/WildFly - RedHat)
3
• EclipseLink (JPA 2.1, usado por GlassFish - Oracle)
4
• OpenJPA (JPA 2.0)
6
Frameworks de persistencia - JPA
Definimos dos entidades JPA, Autor y Mensaje que se deben mapear con dos tablas de
la base de datos. Deberemos representar la relación uno-a-muchos entre autor y mensajes.
Un autor va a estar relacionado con todos los mensajes que ha creado. Todos los mensajes
tendrán obligatoriamente un autor. Las entidades tendrán la siguiente información:
5
http://www.hibernate.org/docs
6
http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html/
7
https://jcp.org/en/jsr/detail?id=338
8
http://www.manning.com/bauer3/
7
Frameworks de persistencia - JPA
La siguiente figura muestra gráficamente las clases Java (entidades) y las tablas asociadas:
A continuación vamos a crear paso a paso todos los elementos del proyecto. Definiremos
también algunos programas ejemplo para ilustrar el funcionamiento de distintos usos de JPA:
creación de entidades, modificación, borrado y consulta. Todo ello en el proyecto llamado
mensajes .
Este primer subproyecto (o módulo) lo crearemos con las siguientes coordenadas Maven:
• groupId: org.expertojava.jpa
• artifactId: mensajes
• packaging: jar
• name: mensajes
Las librerías necesarias para trabajar con Maven se pueden comprobar en el fichero POM
del proyecto. Todas las versiones de todas las dependencias están actualizadas a la última
versión disponible.
8
Frameworks de persistencia - JPA
pom.xml
<groupId>org.expertojava.jpa</groupId>
<artifactId>mensajes</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>mensajes</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.1-api</artifactId>
<version>1.0.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.3.7.Final</version>
</dependency>
<dependency>
<groupId>org.dbunit</groupId>
<artifactId>dbunit</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.33</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
9
Frameworks de persistencia - JPA
<version>1.7.12</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.1.3.Final</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>el-impl</artifactId>
<version>2.2</version>
</dependency>
</dependencies>
</dependencies>
<build>
<finalName>${project.name}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Hibernate JPA: librerías para usar Hibernate como una implementación de JPA y librería
de log slf4j-api necesaria para Hibernate.
DbUnit: librería dbunit:2.5.0 , carga automáticamente la librería junit:4.11 que
necesita.
10
Frameworks de persistencia - JPA
Driver MySQL para acceder a la base de datos MySQL desde JPA y desde DbUnit.
Hibernate necesita el fichero de logging slf4j-simple.jar , sobre el que definimos
una configuración de logging basada en log4j.
Hibernate validator implementa la validación Bean Validation de las clases entidad.
En la siguiente imagen se muestran todas las librerías que finalmente se descarga Maven.
Son librerías necesarias para que las anteriores librerías puedan funcionar:
En la pestaña Dependency Hierarchy del POM podemos explorar las relaciones entre las
distintas librerías.
IntelliJ tiene un conjunto de herramientas muy útiles para trabajar con bases de datos. Se
accede a ellas desde el panel Database situado en el lateral derecho:
2. Crea una nueva conexión con la base de datos MySQL con la opción + > Data Source
> MySQL
3. Inicializa los parámetros de la conexión, sólo tienes que indicar el usuario root y
la contraseña expertojava . Aparecerá también un aviso indicando que no está
descargado el driver de acceso a MySQL, pincha el enlace y lo descargará e instalará.
11
Frameworks de persistencia - JPA
Una vez configurada la conexión, vamos a utilizarla para crear la base de datos
jpa_mensajes que vamos a utilizar en las primeras aplicaciones ejemplo que vamos
a programar con JPA. En el panel de base de datos podremos ver un desplegable con
las bases de datos existentes. Para crear la nueva base de datos abre la consola SQL
pulsando el icono correspondiente del panel de base de datos:
Y ejecuta el comando:
Verás que se ha creado una base de datos con ese nombre bajo las ya existentes por
defecto en MySQL. Con esto es suficiente para que podamos empezar a trabajar con JPA.
4. Por último, para que aparezca la base de datos recién creada bajo el icono de la fuente
de datos MySQL es necesario seleccionarla en la pantalla de propiedades. Pulsa en el
botón de propiedades de la fuente de datos (o selecciona con el botón derecho la opcion
Properties) y entrarás en la ventana de configuración de la fuente de datos. Selecciona la
pestaña Schemas & Tables y escoge la base de datos jpa_mensajes :
12
Frameworks de persistencia - JPA
Ahora ya podrás ver la base de datos (por ahora sin ninguna tabla) bajo el icono de la
conexión a la fuente de datos:
Clases entidad
Las clases de entidad (entity classes) se codifican como clases Java con campos, getters,
setters y con los métodos equals y hashcode basados en claves propias naturales a los
que se añaden anotaciones JPA para especificar el mapeado con las tablas correspondientes
de la base de datos. Vamos a ver un primer ejemplo con las clases Autor y Mensaje y
la relación una-a-muchos definida entre ellos. En las sesiones siguientes entraremos más a
fondo a explicar las distintas anotaciones.
Autor
Veamos la primera clase, Autor , que contiene alguna información sobre los autores que
escriben los mensajes.
Debemos etiquetar la clase con la anotación @Entity para indicarle a JPA que se debe
mapear con una tabla. Todos los atributos de la entidad se mapearán automáticamente con
columnas de la tabla SQL. En esos atributos podemos añadir otras anotaciones que permiten
configurar las distintas características de las columnas SQL. Estas anotaciones se pueden
definir sobre el atributo o sobre los métodos getters. Una forma curiosa de organizar la
9
estructura de una clase entidad es la que utilizan en la documentación de Hibernate , en la
que agrupan atributo, getter y setter.
src/main/java/org/expertojava/jpa/mensajes/modelo/Autor.java
package org.expertojava.jpa.mensajes.modelo;
import javax.persistence.*;
9
http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html_single/#mapping-declaration
13
Frameworks de persistencia - JPA
import java.util.HashSet;
import java.util.Set;
@Entity
public class Autor {
@Id
@GeneratedValue
@Column(name = "autor_id")
Long id;
@Column(name="email", nullable = false, unique = true)
private String correo;
private String nombre;
@OneToMany(mappedBy = "autor", cascade = CascadeType.ALL, fetch =
FetchType.EAGER)
private Set<Mensaje> mensajes = new HashSet<Mensaje>();
@Version
private int version;
public Autor() {
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
@Override
public int hashCode() {
return id != null ? id.hashCode() : 0;
}
14
Frameworks de persistencia - JPA
@Override
public String toString() {
return "Autor{" +
"id=" + id +
", correo='" + correo + '\'' +
", nombre='" + nombre + '\'' +
", mensajes=" + mensajes +
'}';
}
}
@Entity : La clase es una entidad que se va a mapear con una tabla de la base de datos.
Los campos de la clase se mapearán con columnas de la base de datos. Por defecto
el nombre de la tabla será el nombre de la clase Java. Se puede modificar usando la
anotación @Table .
@Id : indica que el campo anotado (en nuestro caso Long id ) va a ser el identificador
de la entidad. La columna con la que se mapea en la base de datos es la clave primaria
de la tabla.
@GeneratedValue : El identificador se genera automáticamente por parte de la base
de datos cuando la entidad se hace persistente.
@Column : Sirve para indicar características del esquema de la columna en la que se
mapea el campo. El elemento name sirve para indicar el nombre de la columna en el
mapeo. Si no estuviera, con un una columna con el nombre del atributo de la clase Java.
En este caso, obligamos a que no la columna no sea null . Veremos que la columna
correo de la tabla tendrá el modificador NOT NULL .
Todos los atributos se mapean con campos de la tabla. En el caso de no utilizar la
anotación @Column se mapea con un campo con el mismo nombre que el atributo.
@OneToMany : Sirve para definir una relación uno-a-muchos entre Autor y Mensaje .
La anotación cascade indica que las acciones de borrado, persist y merge se propagan
en cascada a los mensajes. La anotación @mappedBy indica el atributo que define la
clave ajena en el otro lado de la relación. Y la anotación EAGER indica que traeremos
a memoria todos los mensajes con los que está relacionado el autor. Los veremos más
adelante.
@Version : Un atributo versión que usa JPA para implementar la gestión optimista de
la concurrencia. Lo veremos en la última sesión.
Constructor vacío, necesario para JPA.
Métodos equals y hashCode basados en el identificador autogenerado.
Vemos que se trata de una clase normal Java con cuatro campos ( id , correo , nombre y
mensajes ) y los métodos get y set. JPA mapea esta clase en una tabla de la base de datos
utilizando las anotaciones comentadas anteriormente.
La implementación de Hibernate obliga a definir una pareja de métodos get y set para
cada atributo. En el caso de la clave primaria, al ser generada automáticamente por la base
de datos, definimos el método set como privado para que no pueda ser actualizado desde
fuera de la clase. Los métodos get sirven para recuperar la información de un atributo de
un objeto. Los métodos set sirven para actualizarlo. Cuando JPA sincronice el estado del
objeto con la base de datos escribirá en ella los cambios realizados.
Además es necesario definir los métodos equals y hashCode , a parte del conveniente
toString .
15
Frameworks de persistencia - JPA
Mensaje
Veamos ahora la otra clase, Mensaje , con la información de los mensajes que crean los
usuarios (identificador del mensaje, texto, fecha y usuario propietario del mensaje).
src/main/java/org/expertojava/jpa/mensajes/modelo/Autor.java
package org.expertojava.jpa.mensajes.modelo;
import javax.persistence.*;
import java.util.Date;
@Entity
public class Mensaje {
@Id
@GeneratedValue
@Column(name = "mensaje_id")
private Long id;
@NotNull
@Size(min=3)
@Column(nullable = false)
private String texto;
@ManyToOne
private Autor autor;
@Version
private int version;
public Mensaje() {}
@Override
public String toString() {
return "Mensaje{" +
"id=" + id +
16
Frameworks de persistencia - JPA
@NotNull y @Size son dos anotaciones de Bean Validation que definen restricciones
en el contenido del atributo. Ambas restricciones son mantenidas por JPA, lanzando
excepciones si se produce una creación o actualización de una entidad que no las cumple.
@ManyToOne : Relación inversa que etiqueta un atributo de tipo Autor . El atributo se
mapea en una columna de la tabla Mensaje en la que se guarda la clave ajena al autor
asociado.
El fichero Mensaje.java define la clase entidad Mensaje . La entidad tiene los atributos
id (identificador único del mensaje), texto (el texto del mensaje) y autor (el autor del
mensaje, una instancia entidad de tipo Autor con la que se define la relación inversa a la
definida en autor).
En el ejemplo estamos definiendo una relación uno a muchos entre autor y mensajes. Estas
relaciones se definen en JPA definiendo campos del tipo de la otra entidad y anotándolos
según el tipo de relación (uno-a-uno, uno-a-muchos o muchos-a-muchos). En nuestro ejemplo
relacionamos un mensaje con el autor que lo ha escrito y el autor con todos sus mensajes.
La definición de estas relaciones facilita mucho la programación porque evita la realización
explícita de muchas consultas SQL. Por ejemplo, si en una consulta recuperamos una
colección de mensajes, JPA recuperará al mismo tiempo el autor asociado y lo guardará en
el campo autor . De esta forma podremos utilizarlo inmediatamente sin tener que realizar
ninguna consulta adicional. Veremos un ejemplo.
Veremos que para actualizar una relación uno a muchos como esta y añadir un mensaje a un
autor hay que actualizar el atributo de la entidad que se mapea con al tabla que contiene la
clave ajena. En este caso se trata de la entidad Mensaje , cuyo campo autor se mapea
con la columna de la clave ajena de la tabla. Al crear un nuevo mensaje y hacerlo persistente
ya estamos añadiendo un elemento nuevo a la relación y las consultas que se realicen sobre
los mensajes de un autor devolverán el nuevo mensaje. Veremos que es necesario también
17
Frameworks de persistencia - JPA
src/main/resources/META-INF/persistence.xml
<persistence version="2.1"
xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence">
<persistence-unit name="mensajes-mysql"
transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</
provider>
<class>org.expertojava.jpa.mensajes.modelo.Autor</class>
<class>org.expertojava.jpa.mensajes.modelo.Mensaje</class>
<validation-mode>CALLBACK</validation-mode>
<properties>
18
Frameworks de persistencia - JPA
</properties>
</persistence-unit>
</persistence>
• update : se actualiza el esquema de las tablas si ha habido algún cambio en las clases
Java. Si no existen, se crean.
• validate : se valida que el esquema se puede mapear correctamente con las clases
Java. No se cambia nada de la base de datos.
• create : se crea el esquema, destruyendo los datos previos.
• create-drop : se crea el esquema y se elimina al final de la sesión.
Para eso vamos a hacer un pequeño test. En src/test/java/ podemos crear la clase
org.expertojava.jpa.mensajes.TestEmf :
19
Frameworks de persistencia - JPA
src/test/java/org/expertojava/jpa/mensajes/TestEmf.java
package org.expertojava.jpa.mensajes;
import org.junit.Test;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
@Test
public void createEntityManagerTest() {
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("mensajes-mysql");
EntityManager em = emf.createEntityManager();
assertNotNull(em);
em.close();
}
}
Podemos generar el esquema de datos desde el panel Database de IntelliJ. Hay que pulsar
el botón derecho sobre la base de datos y seleccionar la opción Copy DDL para copiar el
esquema de datos al portapapeles. En nuestro caso es el siguiente:
20
Frameworks de persistencia - JPA
Por último, para evitar mensajes de error de IntelliJ, debemos asegurarnos de actualizar la
faceta JPA y de asociarle la fuente de datos recién creada:
1. Seleccionamos Project Structure y nos aseguramos de que la faceta JPA está configurada
en el módulo que estamos usando y que está correctamente definida la ruta del fichero
persistence.xml :
21
Frameworks de persistencia - JPA
src/main/resources/commons-logging.properties
org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger
src/main/resources/log4j.properties
# Coloca el nivel root del logger en INFO (muestra mensajes de INFO hacia
arriba)
log4j.rootLogger=INFO, A1
# A1 se redirige a la consola
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%d{dd/MM/yyyy HH:mm:ss} - %p -
%m %n
Programa NuevoAutorMensaje
Veamos el primer ejemplo de programa Java que usa las clases definidas anteriormente. En
él comprobaremos las sentencias JPA necesarias para crear nuevas instancias de entidad y
hacerlas persistentes en la base de datos.
22
Frameworks de persistencia - JPA
src/main/java/org/expertojava/jpa/mensajes/main/NuevoAutorMensaje.java
package org.expertojava.jpa.mensajes.main;
import org.expertojava.jpa.mensajes.modelo.Autor;
import org.expertojava.jpa.mensajes.modelo.Mensaje;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Date;
try {
em.getTransaction().begin();
23
Frameworks de persistencia - JPA
Todas las entidades que se crean en un entity manager son gestionadas por él y viven en su
contexto de persistencia. Cuando el entity manager se cierra, las entidades siguen existiendo
como objetos Java, pero a partir de ese momento se encuentran desconectadas (detached)
de la base de datos.
Cuando están gestionadas por el entity manager, JPA gestiona los cambios que se producen
en las entidades, utiliza el proveedor de persistencia para generar las sentencias SQL
asociadas a los cambios y vuelca (flush) esas sentencias en la base de datos.
El ejecución de los comandos SQL en la base de datos se realizan dependiendo del modo
de actualización del entity manager. Por defecto el modo es AUTO y es el proveedor de
persistencia (en nuestro caso, Hibernate) el que decide cuándo ejecutar los comandos en la
base de datos. En el caso de Hibernate, se hace de forma inmediata. Puedes comprobarlo si
está activado el log que muestra las sentencias SQL. Si está en modo COMMIT las sentencias
se envían a la base de datos al realizar el commit de la transacción.
Podemos comprobar también en el ejemplo que el entity manager es quien proporciona los
métodos para trabajar con la base de datos, añadiendo nuevos objetos, ejecutando consultas,
etc. Por ejemplo, el método persist hace persistente en la base de datos el objeto que se
le pasa como parámetro.
Podemos ejecutar el programa desde IntelliJ o desde línea de comandos usando el plugin
exec de Maven:
$ cd jpa-expertojava/mensajes
24
Frameworks de persistencia - JPA
$ mvn install
$ mvn exec:java -
Dexec.mainClass=org.expertojava.jpa.main.NuevoAutorMensaje
Programa NuevoMensaje
A continuación vemos un programa en el que se muestra cómo añadir un mensaje a un autor
ya existente. Es un ejemplo de utilización de la recuperación de entidades por clave primaria.
src/main/java/org/expertojava/jpa/mensajes/main/NuevoMensaje.java
package org.expertojava.jpa.mensajes.main;
import org.expertojava.jpa.mensajes.modelo.Autor;
import org.expertojava.jpa.mensajes.modelo.Mensaje;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Date;
em.getTransaction().commit();
em.close();
emf.close();
}
25
Frameworks de persistencia - JPA
Vemos que la recuperación de un autor a partir de su clave primaria se hace con el método
find del entity manager, pasando como parámetro la clase de entidad que queremos
recuperar y el valor de la clave primaria. El método devuelve null si no existe esa entidad.
Programa MensajesAutor
A continuación presentamos el programa que lista los mensajes añadidos a un determinado
autor. Es un ejemplo de utilización de la relación a-muchos que hemos definido entre un autor
y sus mensajes.
src/main/java/org/expertojava/jpa/mensajes/main/MensajesAutor.java
package org.expertojava.jpa.mensajes.main;
import org.expertojava.jpa.mensajes.modelo.Autor;
import org.expertojava.jpa.mensajes.modelo.Mensaje;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
26
Frameworks de persistencia - JPA
}
}
catch (Exception e) {
e.printStackTrace();
}
finally {
em.close();
emf.close();
}
}
Si miramos en la consola los mensajes SQL que se generan, veremos que el find que
recupera el autor también genera un select que recupera toda la colección de mensajes.
Después, basta con recorrer esta colección de mensajes que ya tenemos en memoria.
Programa BuscaMensajes
JPA tiene su propio lenguaje de consultas llamado JP-QL. Lo veremos en una próxima sesión.
El siguiente ejemplo ejecuta la consulta en la que se busca un patrón de texto en las entidades
Mensaje .
src/main/java/org/expertojava/jpa/mensajes/main/BuscaMensajes.java
package org.expertojava.jpa.mensajes.main;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.persistence.Query;
import org.expertojava.jpa.mensajes.Mensaje;
@SuppressWarnings("unchecked")
27
Frameworks de persistencia - JPA
// em.getTransaction().commit();
em.close();
emf.close();
}
Podemos comprobar en el ejemplo que una vez recuperados los mensajes tenemos
disponibles sus autores. JPA ha guardado en cada mensaje su autor . Esto sólo se hace
por defecto con las relaciones "a-uno". En las relaciones "a-muchos" JPA guarda un proxy en
lugar de la colección y JPA debe realizar una consulta SQL cuando se accede a la colección.
Lo veremos con más detalle en la sesión en la que hablemos del mapeo de relaciones entre
entidades.
28
Frameworks de persistencia - JPA
src/test/resources/META-INF/persistence.xml
...
<property name="hibernate.hbm2ddl.auto" value="create"/>
...
Vamos a ver rápidamente cómo usar DbUnit para poblar de datos las tablas sobre las que se
realizan los tests. Con DbUnit podemos definir un conjunto de datos asociados a cada clase
de tests y lanzar todos los tests de esa clase sobre esos mismos datos. La forma de hacerlo
es limpiando las tablas a las que pertenecen los datos e insertándolos a continuación (tantas
veces como tests haya que pasar).
El siguiente fichero muestra cómo construir un dataset de prueba usando XML para rellenar
las tablas:
src/test/resources/dbunit/dataset1.xml
src/test/java/org.expertojava.jpa.mensajes.modelo.TestMensajes
package org.expertojava.jpa.mensajes.modelo;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.operation.DatabaseOperation;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import java.sql.Connection;
import java.sql.DriverManager;
29
Frameworks de persistencia - JPA
@Test
public void persistAñadeUnNuevoAutor() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Autor autor = new Autor("Antonio
Martínez", "antonio.martinez@ua.es");
em.persist(autor);
em.getTransaction().commit();
Long id = autor.id;
Autor autor2 = em.find(Autor.class, id);
assertTrue(autor2.equals(autor));
em.close();
}
@Test
public void createEntityManagerTest() {
EntityManager em = emf.createEntityManager();
30
Frameworks de persistencia - JPA
assertNotNull(em);
em.close();
}
@Test
public void findDevuelveAutor() {
EntityManager em = emf.createEntityManager();
Autor autor = em.find(Autor.class, 1L);
assertTrue(autor.getCorreo().equals("antonio.martinez@ua.es"));
em.close();
}
@Test
public void findDevuelveAutorConMensajes() {
EntityManager em = emf.createEntityManager();
Autor autor = em.find(Autor.class, 1L);
assertTrue(autor.getMensajes().size() == 2);
em.close();
}
Se obtiene una conexión JDBC a la base de datos para que DbUnit inserte los datos de
pruebas
Se cargan los datos de prueba en memoria a partir del fichero dbunit/dataset1.xml
Antes de cada test se realiza una llamada al método CLEAN_INSERT de DbUnit con el
que se vacían las tablas a las que pertenecen los datos del dataset y se insertan. De esta
forma, antes de comenzar cualquier test las tablas se encuentran en el estado conocido
definido por los datos cargados.
Se realizan tests de forma idéntica a cómo los hacíamos con JUnit
Al terminar todos los tests se borran todos los datos de la base de datos y se cierra el
EntityManagerFactory
31
Frameworks de persistencia - JPA
1.3. Ejercicios
Para crear los proyectos haz un fork del repositorio java_ua/ejercicios-jpa y
descargarlo después en tu máquina:
Lo único que contiene el repositorio es un proyecto IntelliJ vacío con el fichero .gitignore .
En este repositorio vas a crear los distintos módulos que vamos a ir desarrollando en las
sesiones de ejercicios.
Abre el proyecto con IntelliJ y empezamos con los ejercicios de esta sesión.
• Una vez que has abierto el proyecto jpa-expertojava , crea un primer módulo dentro
de él. Utiliza el asistente para crear un módulo Maven vacío desde el panel de Project
Structure, que puedes abrir con la opción File > Project Structure:
# groupId :`org.expertojava.jpa`
# artifactId : mensajes
# version : la que aparece por defecto ( 1.0-SNAPSHOT )
# packaging : jar
32
Frameworks de persistencia - JPA
• groupId : org.expertojava.jpa
• artifactId : filmoteca
• version : 0.0.1-SNAPSHOT
• packaging : jar
Modifica el fichero POM para incluir las librerías necesarias para trabajar con JPA. Crea
una base de datos nueva llamada jpa_filmoteca y el fichero de configuración de
JPA persistence.xml . Crea en el persistence.xml una unidad de persistencia
sin clases con el nombre filmoteca . Crea por último el singleton que obtiene el
entityManagerFactory .
Para probar todo lo anterior, crea una clase de test como hemos anteriormente para probar
que se obtiene correctamente el entity manager. Lanza el test y comprueba que funciona
correctamente.
Creación de entidades
Crea las siguientes entidades en el paquete org.expertojava.jpa.filmoteca.modelo:
• Pelicula , con un identificador autogenerado como clave primaria (tipo Long ) y los
campos:
# titulo ( String )
# estreno (`Date )
# presupuesto ( Double ): presupuesto de la película en miles de euros
# recaudación ( Double ): recaudación de la película en miles de euros
# pais ( String )
# Relación a-muchos con la entidad Critica
• Critica , con un identificador autogenerado como clave primaria (tipo Long ) y los
campos:
# critico ( String )
# texto ( String )
# valoracion ( Integer ): número del 1 al 10
# relación a-uno con Pelicula
Lanza el test anterior y comprueba que las tablas se han creado correctamente en la BD.
33
Frameworks de persistencia - JPA
Puedes ejecutar los programas desde IntelliJ o desde línea de comando utilizando el plugin
exec de Maven:
$ mvn exec:java -
Dexec.mainClass=org.expertojava.jpa.filmoteca.main.NuevaPelicula
34
Frameworks de persistencia - JPA
• Define una conexión transaccional con la base de datos que debemos abrir y mantener
abierta mientras estamos realizado operaciones. En este sentido realiza funciones similares
a las de una conexión JDBC.
• Además, mantiene en memoria una caché con las entidades que gestiona y es responsable
de sincronizarlas correctamente con la base de datos cuando se realiza un flush. El conjunto
de entidades que gestiona un entity manager se denomina su contexto de persistencia.
El entity manager se obtiene a través de una factoría del tipo EntityManagerFactory , que
se configura mediante la especificación de una unidad de persistencia (persistence unit en
inglés) definida en el fichero XML persistence.xml . En el fichero pueden haber definidas
más de una unidad de persistencia, cada una con un nombre distinto. El nombre de la unidad de
persistencia escogida se pasa a la factoría. La unidad de persistencia define las características
concretas de la base de datos con la que van a trabajar todos los entity managers obtenidos a
partir de esa factoría y queda asociada a ella en el momento de su creación. Existe, por tanto,
una relación uno-a-uno entre una unidad de persistencia y su EntityManagerFactory
concreto. Para obtener una factoría EntityManagerFactory debemos llamar a un método
estático de la clase Persistence .
Las relaciones entre las clases que intervienen en la configuración y en la creación de entity
managers se muestran en la siguiente figura.
35
Frameworks de persistencia - JPA
referencia a una entidad, se dice que la entidad está gestionada (una managed entity en
inglés) por él. El entity manager guarda internamente todas las entidades que gestiona y las
utiliza como una caché de los datos en la base de datos. Por ejemplo, cuando va a recuperar
una entidad por su clave primaria, lo primero que hace es consultar en su caché si esta entidad
ya la ha recuperado previamente. Si es así, no necesita hacer la búsqueda en la base de datos
y devuelve la propia referencia que mantiene. Al conjunto de entidades gestionadas por un
entity manager se le denomina su contexto de persistencia (persistence context en inglés).
En un determinado momento, el entity manager debe volcar a la base de datos todos los
cambios que se han realizado sobre las entidades. También debe ejecutar las consultas JPQL
definidas. Para ello el entity manager utiliza un proveedor de persistencia (persistence provider
en inglés) que es el responsable de generar todo el código SQL compatible con la base de
datos.
En esta sesión estudiaremos las distintas operaciones que realiza el entity manager, así
como el concepto de contexto de persistencia y las distintas problemáticas relacionadas con
la gestión de esta caché de objetos persistentes y la sincronización con la base de datos
subyacente.
Una vez que se cierra la transacción y se cierra el entity manager, éste desaparece pero
las entidades gestionadas siguen estando en memoria, pero ahora en estado desconectado
(detached en inglés). Podrían volverse a gestionar por un entity manager usando el método
merge .
APIs de Java EE 7
Veremos una parte de la completa APIs de Java EE 7 sobre estas clases. Los siguientes
métodos de la interfaz EntityManager son los más importantes y los estudiaremos en
detalle en los siguientes apartados:
36
Frameworks de persistencia - JPA
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("mensajes-mysql");
Esta creación es un proceso costoso, ya que incluye el procesamiento de todas las anotaciones
de las clases de entidad declaradas en el persistence.xml , la generación del esquema
de datos asociados para compararlo con el existente en la base de datos con la que se realiza
la conexión y bastantes otros procesos relacionados con el mapeo de las entidades con la
base de datos.
EmfSingleton
package org.expertojava.jpa.mensajes.persistencia
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
10
https://docs.oracle.com/javaee/7/api/javax/persistence/EntityManager.html
11
https://docs.oracle.com/javaee/7/api/javax/persistence/EntityManagerFactory.html
37
Frameworks de persistencia - JPA
private EmfSingleton() {
}
EntityManager em = emf.createEntityManager();
EntityManager em =
EmfSingleton.getInstance().getEmf().createEntityManager();
Esta llamada no es nada costosa, ya que las implementaciones de JPA (como Hibernate)
implementan pools de entity managers. El método createEntityManager no realiza
ninguna reserva de memoria ni de otros recursos sino que simplemente devuelve alguno de
los entity managers disponibles.
38
Frameworks de persistencia - JPA
EmfSingleton.getInstance().getEmf();
EntityManager em = emf.createEntityManager();
// Cerramos la transacción, el em
tx.commit();
em.close();
Transacciones
Cualquier operación que conlleve una creación, modificación o borrado de entidades debe
hacerse dentro de una transacción. En JPA las transacciones se gestionan de forma
distinta dependiendo de si estamos en un entorno Java SE o en un entorno Java EE. La
diferencia fundamental entre ambos casos es que en un entorno Java EE las transacciones
se manejan con JTA (Java Transaction API), un API que implementa el two face commit
y que permite gestionar operaciones sobre múltiples recursos transaccionales o múltiples
operaciones transaccionales sobre el mismo recurso. En el caso de Java SE las transacciones
se implementan con el gestor de transacciones propio del recurso local (la base de datos) y
se especifican en la interfaz EntityTransaction .
em.getTransaction().begin();
createEmpleado("Juan Garcia", 30000);
em.getTransaction().commit();
39
Frameworks de persistencia - JPA
La operación persist() se utiliza con entidades nuevas que no existen en la base de datos.
Si se le pasa una instancia con un identificador que ya existe en la base de datos el proveedor
de persistencia puede detectarlo y lanzar una excepción EntityExistsException . Si no
lo hace, entonces se lanzará la excepción cuando se sincronice el conexto de persistencia con
la base de datos, al encontrar una clave primaria duplicada.
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.getTransaction().commit();
em.close();
40
Frameworks de persistencia - JPA
Por último actualizamos en memoria el otro lado de la relación llamando al método add()
de la colección. Esto no es necesario para actualizar la base de datos, sólo para actualizar
la relación en memoria. Es importante tener siempre presente que JPA no actualiza las
colecciones en memoria de las relaciones uno-a-muchos.
Una cuestión muy importante a tener en cuenta es que si el identificador del empleado es una
clave primaria generada por la base de datos no estará disponible hasta que el contexto de
persistencia realice la ejecución de los comandos SQL. Hay que tener cuidado y no realizar el
add en la colección con el identificador a null. Con Hibernate no hay problema, porque realiza
la actualización en la base de datos de inmediantamente y tenemos el id actualizado justo
después de ejecutar el persist (si el flush() está puesto a AUTO ). Pero tenemos que
tener cuidado, y comprobar el comportamiento de cada proveedor de persistencia.
em.persist(empleado);
em.flush(empleado);
em.refresh(empleado);
Pasamos la clase de la entidad que estamos buscando (en el ejemplo estamos buscando una
instancia de la clase Empleado ) y el identificador o clave primaria que identifica la entidad.
El entity manager buscará esa entidad en la base de datos y devolverá la instancia buscada.
La entidad devuelta será una entidad gestionada que existirá en el contexto de persistencia
actual asociado al entity manager.
41
Frameworks de persistencia - JPA
La llamada a find puede devolver dos posibles excepciones de tiempo de ejecución, ambas
de la clase PersistenceException : IllegalStateException si el entitiy manager
ha sido previamente cerrado o IllegalArgumentException si el primer argumento no
contiene una clase entidad o el segundo no es el tipo correcto de la clave primaria de la entidad.
Existe una versión especial de find() que sólo recupera una referencia (técnicamente, un
proxy) a la entidad, sin obtener los datos de los campos de la base de datos. Se trata del
método getReference() . Es últil cuando se quiere añadir un objeto con una clave primaria
conocida a una relación. Ya que únicamente estamos creando una relación, no es necesario
cargar todo el objeto de la base de datos. Sólo se necesita su clave primaria. Veamos la nueva
versión del ejemplo anterior:
Esta versión es más eficiente que la anterior porque no se realiza ningún SELECT
en la base de datos para buscar la instancia del Departamento . Cuando se llama a
getReference() , el proveedor devolverá un proxy al Departamento sin recuperarlo
realmente de la base de datos. En tanto que sólo se acceda a la clave primaria, no se
recuperará ningún dato. Y cuando se haga persistente el Empleado , se guardará en la clave
ajena correspondiente el valor de la clave primaria del Departamento .
Por ejemplo, el siguiente código sólo generaría un SELECT sobre la base de datos:
42
Frameworks de persistencia - JPA
Actualización de entidades
Para actualizar una entidad, primero debemos obtenerla para convertirla en gestionada.
Después podremos colocar los nuevos valores en sus atributos utilizando los métodos set
de la entidad. Por ejemplo, supongamos que queremos subir el sueldo del empleado 146 en
1.000 euros. Tendríamos que hacer lo siguiente:
em.getTransaction().begin();
Empleado empleado = em.find(Empleado.class, 2L);
double sueldo = empleado.getSueldo();
empleado.setSueldo(sueldo + 1000.0);
em.getTransaction().commit();
em.close();
Nótese la diferencia con las operaciones anteriores, en las que el EntityManager era el
responsable de realizar la operación directamente. Aquí no llamamos al EntityManager
sino a la propia entidad. Estamos, por así decirlo, trabajando con una caché de los datos de la
base de datos. Posteriormente, cuando se finalice la transacción, el EntityManager hará
persistentes los cambios mediante las correspondientes sentencias SQL.
La otra forma de actualizar una entidad es con el método merge() del EntityManager .
A este método se le pasa como parámetro una entidad no gestionada. El EntityManager
busca la entidad en su contexto de persistencia (utilizando su identificador) y actualiza los
valores del contexto de persistencia con los de la entidad no gestionada. En el caso en que la
entidad no existiera en el contexto de persistencia, se crea con los valores que lleva la entidad
no gestionada.
EntityManager em = emf.createEntityManager();
Empleado empleado = em.find(Empleado.class, 2L);
em.close();
empleado.setNombre("Pepito Pérez");
em = emf.createEntityManager();
em.getTransaction().begin();
empleado = em.merge(empleado);
empleado.setSueldo(20000.0);
em.getTransaction().commit();
em.close();
Es muy importante notar que no está permitido modificar la clave primaria de una entidad
gestionada. Si intentamos hacerlo, en el momento de hacer un commit la transacción lanzará
una excepción RollbackException . Para reforzar esta idea, es conveniente definir las
entidades sin un método set de la clave primaria. En el caso de aquellas entidades con una
generación automática de la clave primaria, ésta se generará en tiempo de creación de la
entidad. Y en el caso en que la aplicación tenga que proporcionar la clave primaria, lo puede
hacer en el constructor.
43
Frameworks de persistencia - JPA
los datos obtenidos y marcan como no activos aquellos que quieren dejar fuera de vista de
los casos de uso. Se suele utilizar para eliminar datos que se han introducido por error en la
base de datos o para trasladar de una tabla a otra los datos (se borra el dato de una y se
inserta en la otra). En el caso de entidades esto último sería equivalente a un cambio de tipo
de una entidad.
Para eliminar una entidad, la entidad debe estar gestionada, esto es, debe existir en el contexto
de persistencia. Esto significa que la aplicación debe obtener la entidad antes de eliminarla.
Un ejemplo sencillo es:
Borrar una entidad no es una tarea compleja, pero puede requerir algunos pasos, dependiendo
del número de relaciones en la entidad que vamos a borrar. En su forma más simple, el borrado
de una entidad se realiza pasando la entidad como parámetro del método remove() del
entity manager que la gestiona. En el momento en que el contexto de persistencia se sincroniza
con una transacción y se realiza un commit, la entidad se borra. Hay que tener cuidado, sin
embargo, con las relaciones en las que participa la entidad para no comprometer la integridad
de la base de datos.
44
Frameworks de persistencia - JPA
entidad que se va a borrar es el objetivo de una clave ajena en otras tablas, entonces debemos
limpiar esas claves ajenas antes de borrar la entidad.
Consideremos la secuencia de operaciones del siguiente códgo que muestran cómo se crea
un nuevo Empleado con una entidad Direccion asociada y cómo se hacen los dos
persistentes. La segunda llamada a persist() sobre la Direccion es algo redundante.
Una entidad Direccion se acopla a la entidad Empleado que la almacena y tiene sentido
que siempre que se cree un nuevo Empleado , se propague en cascada la llamada a
persist() para la Direccion .
El API JPA proporciona un mecanismo para definir cuándo operaciones como persist()
deben propagarse en cascada. Para ello se define el elemento cascade en todas las
anotaciones de relaciones ( @OneToOne , @OneToMany , @ManyToOne y @ManyToMany ).
Las operaciones a las que hay que aplicar la propagación se identifican utilizando el tipo
enumerado CasacadeType , que puede tener como valor PERSIST , REFRESH , REMOVE ,
MERGE y ALL .
Persist en cascada
Para activar la propagación de la persistencia en cascada debemos añadir el elemento
cascade=CascadeType.PERSIST en la declaración de la relación. Por ejemplo, en el caso
anterior, si hemos definido una relación muchos-a-uno entre Empleado y Direccion ,
podemos escribir el siguiente código:
@Entity
public class Empleado {
...
@ManyToOne(cascadeCascdeType.PERSIST)
Direccion direccion;
...
}
Para invocar la persistencia en cascada sólo nos tenemos que asegurar de que la nueva
entidad Direccion se ha puesto en el atributo direccion del Empleado antes de llamar
a persist() con él. La definición de la operación en cascada es unidireccional, y tenemos
que tener en cuenta quién es el propietario de la relación y dónde se va a actualizar la misma
45
Frameworks de persistencia - JPA
antes de tomar la decisión de poner el elemento en ambos lados. Por ejemplo, en el caso
anterior cuando definamos un nuevo empleado y una nueva dirección pondremos la dirección
en el empleado, por lo que el elemento cascade tendremos que definirlo únicamente en la
relación anterior.
Borrado en cascada
A primera vista, la utilización de un borrado en cascada puede parecer atractiva. Dependiendo
de la cardinalidad de la relación podría eliminar la necesidad de eliminar múltiples instancias
de entidad. Sin embargo, aunque es un elemento muy interesante, debe utilizarse con cierto
cuidado. Hay sólo dos situaciones en las que un remove() en cascada se puede usar
sin problemas: relaciones uno-a-uno y uno-a-muchos en donde hay una clara relación de
propiedad y la eliminación de la instancia propietaria debe causar la eliminación de sus
instancias dependientes. No puede aplicarse ciegamente a todas las relaciones uno-a-uno o
uno-a-muchos porque las entidades dependientes podrían también estar participando en otras
relaciones o podrían tener que continuar en la base de datos como entidades aisladas.
Habiendo realizado el aviso, veamos qué sucede cuando se realiza una operación de
remove() en cascada. Si una entidad Empleado se elimina, no tiene sentido eliminar el
despacho (seguirá existiendo) pero sí sus cuentas de correo (suponiendo que le corresponde
más de una). El siguiente código muestra cómo definimos este comportamiento:
@Entity
public class Empleado {
...
@OneToOne(cascade={CascadeType.PERSIST})
Despacho despacho;
@OneToMany(mappedBy="empleado",
cascade={CascadeType.PERSIST, CascadeType.REMOVE})
Collection<CuentaCorreo> cuentasCorreo;
...
}
Cuando se llama al método remove() el entity manager navegará por las relaciones entre el
empleado y sus cuentas de correo e irá eliminando todas las instancias asociadas al empleado.
Hay que hacer notar que este borrado en cascada afecta sólo a la base de datos y que
no tiene ningún efecto en las relaciones en memoria entre las instancias en el contexto de
persistencia. Cuando la instancia de Empleado se desconecte de la base de datos, su
colección de cuentas de correo contendrá las mismas instancias de CuentaCorreo que
tenía antes de llamar a la operación remove() . Incluso la misma instancia de Empleado
seguirá existiendo, pero desconectada del contexto de persistencia.
2.4. Queries
Uno de los aspectos fundamentales de JPA es la posibilidad de realizar consultas sobre las
entidades, muy similares a las consultas SQL. El lenguaje en el que se realizan las consultas se
denomina Java Persistence Query Language (JPQL). Desde la versión 2.0 de JPA es posible
46
Frameworks de persistencia - JPA
también utilizar el API Criteria que permite la detección de errores de sintaxis en la construcción
de las consultas. Lo veremos más adelante.
Una consulta se implementa mediante un objeto Query . Los objetos Query se construyen
utilizando el EntityManager como una factoría. La interfaz EntityManager proporciona
un conjunto de métodos que devuelven un objeto Query nuevo. Veremos algún ejemplo
ahora, pero profundizaremos en el tema más adelante.
Una consulta puede ser estática o dinámica. Las consultas estáticas se definen con metadatos
en forma de anotaciones o XML, y deben incluir la consulta propiamente dicha y un nombre
asignado por el usuario. Este tipo de consulta se denomina una consulta con nombre (named
query en inglés). El nombre se utiliza en tiempo de ejecución para recuperar la consulta.
En el ejemplo vemos que, al igual que en JDBC, es posible especificar consultas con
parámetros y posteriormente especificar esos parámetros con el método setParameter() .
Una vez definida la consulta, el método getResultList() devuelve la lista de entidades
que cumplen la condición. Este método devuelve un objeto que implementa la interfaz List ,
una subinterfaz de Collection que soporta ordenación. Hay que notar que no se devuelve
una List<Empleado> ya que no se pasa ninguna clase en la llamada y no es posible
parametrizar el tipo devuelto. Sí que podemos hacer un casting en los valores devueltos por
los métodos que implementan las búsquedas, como muestra el siguiente código:
47
Frameworks de persistencia - JPA
actualizando una caché, una copia que sólo se hace persistente en la base de datos cuando
el entity manager realiza un flush de las instancias en la base de datos.
Simplificando bastante, el entity manager realiza el siguiente proceso para todas las entidades:
Vamos a ver con algún detalle las operaciones del entity manager relacionadas con el contexto
de persistencia:
Cuando ocurre un volcado, el entity manager itera primero sobre las entidades gestionadas y
busca nuevas entidades que se han añadido a las relaciones y que tienen activada la opción
de persistencia en cascada. Esto es equivalente lógicamente a invocar a persist() con
48
Frameworks de persistencia - JPA
cada una de las entidades gestionadas antes de que se realice el volcado. El entity manager
también comprueba la integridad de todas las relaciones. Si una entidad apunta a otra que no
está gestionada o que ha sido eliminada, entonces se puede lanzar una excepción.
Si en el proceso de volcado alguno de los objetos a los que apunta la instancia que estamos
haciendo persistente no está gestionado, no tiene el atributo de persistencia en cascada y no
está incluido en una relación uno-a-uno o muchos-a-uno entonces se lanzará una excepción
IllegalStateException .
Desconexión de entidades
Como resultado de una consulta o de una relación, obtendremos una colección de entidades
que deberemos tratar, pasar a otras capas de la aplicación y, en una aplicación web, mostrar
en una página JSP o JSF. En este apartado vamos a ver cómo trabajar con las entidades
obtenidas y vamos a reflexionar sobre su desconexión del contexto de persistencia.
Una entidad desconectada (detached entity en inglés) es una entidad que ya no está asociada
a un contexto de persistencia. En algún momento estuvo gestionada, pero el contexto de
persistencia puede haber terminado, o la entidad puede haberse transformado de forma que
ha perdido su asociación con el contexto de persistencia que la gestionaba. Cualquier cambio
que se realice en el estado de la entidad no se hará persistente en la base de datos, pero
todo el estado que estaba en la entidad antes de desconectarse sigue estando ahí para ser
usado por la aplicación.
Hay dos formas de ver la desconexión de entidades. Por una parte, es una herramienta
poderosa que puede utilizarse por las aplicaciones para trabajar con aplicaciones remotas o
para soportar el acceso a los datos de la entidad mucho después de que la transacción ha
concluido. Además, son objetos que pueden hacer el papel de DTOs (Data Transfer Objets)
y ser devueltos por la capa de lógica de negocio y utilizados por la capa de presentación. La
otra posible interpretación es que puede ser una fuente de problemas frustrantes cuando las
entidades contienen una gran cantidad de atributos que se cargan de forma perezosa y los
clientes que usan estas entidades desconectadas necesitan acceder a esta información.
Existen muchas condiciones en las que una entidad se convierte en desconectada. Cada una
de las situaciones siguientes generarán entidades desconectadas:
• Cuando el contexto de persistencia se cierra con una llamada a close() del entity
manager
• Cuando se llama al método clear() del entity manager
• Cuando se produce un rollback de la transacción
• Cuando una entidad se serializa
Todos los casos se refieren a contextos de persistencias gestionados por la aplicación (Java
SE y aplicaciones web sin contenedor de EJB).
49
Frameworks de persistencia - JPA
que JPA guarda en el getter no es la propia entidad sino un proxy que generará una consulta a
la base de datos cuando se acceda a él. Es lo que se denomina carga perezosa (lazy loading).
Podemos definir el tipo de carga que queremos marcando las relaciones con los atributos
EAGER o LAZY . Por defecto las relaciones a-uno son de tipo eager y las a-muchos de tipo
lazy.
...
...
}
assertTrue(empleado.getCorreos().size()==2);
En el caso en el que las entidades no tengan atributos de carga perezosa, no debe haber
demasiados problemas con la desconexión. Todo el estado de la entidad estará todavía
disponible para su uso en la entidad.
50
Frameworks de persistencia - JPA
Merge
El método merge() permite volver a incorporar en el contexto de persistencia del entity manager
una entidad que había sido desconectada. Debemos pasar como parámetro la entidad que
queremos incluir. Hay que tener cuidado con su utilización, porque el objeto que se pasa como
parámetro no pasa a ser gestionado. Hay que usar el objeto que devuelve el método. Un
ejemplo:
Si una entidad con el mismo identificador que emp existe en el contexto de persistencia, se
devuelve como resultado y se actualizan sus atributos. Si el objeto que se le pasa a merge()
es un objeto nuevo, se comporta igual que persist() , con la única diferencia de que la
entidad gestionada es la devuelta como resultado de la llamada.
Clear
En ocasiones puede ser necesario limpiar ( clear ) contexto de persistencia y vaciar las
entidades gestiondas. Esto puede suceder, por ejemplo, en conextos extendidos gestionados
por la aplicación que han crecido demasiado. Por ejemplo, consideremos el caso de un entity
manager gestionado por la aplicación que lanza una consulta que devuelve varios cientos
de instancias entidad. Una vez que ya hemos realizado los cambios a unas cuantas de esas
instancias y la transacción se termina, se quedan en memoria cientos de objetos que no
tenemos intención de cambiar más. Si no queremos cerrar el contexto de persistencia en ese
momento, entonces tendremos que limpiar de alguna forma las instancias gestionadas, o el
contexto de persistencia irá creciendo cada vez más.
51
Frameworks de persistencia - JPA
em.getTransaction().begin();
AutorEntity autor = em.find(AutorEntity.class, 1L);
System.out.println(autor.getNombre() + " ha escrito " +
autor.getMensajes().size() + " mensajes");
Mensaje mens = new Mensaje("Nuevo mensaje");
mens.setAutor(autor);
em.persist(mens);
em.getTransaction().commit();
System.out.println(autor.getNombre() + " ha escrito " +
autor.getMensajes().size() + " mensajes");
Si comprobamos qué sucede veremos que no aparecerá ningún cambio entre el primer
mensaje de la aplicación y el segundo, ambos mostrarán el mismo número de mensajes.
¿Por qué? ¿Es que no se ha actualizado el nuevo mensaje del autor en la base de datos?.
Si miramos en la base de datos, comprobamos que la transacción sí que se ha completado
correctamente. Sin embargo cuando llamamos al método getMensajes() en la colección
resultante no aparece el nuevo mensaje que acabamos de añadir.
Este es un ejemplo del tipo de errores que podemos cometer por trabajar con contextos
de persistencia pensando que estamos conectados directamente con la BD. El problema se
encuentra en que la primera llamada a getMensajes() (antes de crear el nuevo mensaje)
ha generado la consulta a la base de datos y ha cargado el resultado en memoria. Cuando
hacemos una segunda llamada, el proveedor detecta que esa información ya la tiene en la
caché y no la vuelve a consultar.
La aplicación es la responsable de actualizar las relaciones en memoria. Por eso hay que
añadir manualmente el mensaje a la colección. Hay que hacerlo una vez que se ha hecho
persistente el mensaje, para que su identicador autogenerado esté actualizado. Lo haríamos
añadiendo al código anterior la línea que actualiza la colección:
em.getTransaction().begin();
Autor autor = em.find(Autor.class, 1L);
System.out.println(autor.getNombre() + " ha escrito " +
autor.getMensajes().size() + " mensajes");
Mensaje mens = new Mensaje("Nuevo mensaje");
mens.setAutor(autor);
em.persist(mens);
em.getTransaction().commit();
autor.getMensajes().add(mens);
System.out.println(autor.getNombre() + " ha escrito " +
autor.getMensajes().size() + " mensajes");
em.getTransaction().begin();
// ... añado un mensaje al autor
em.refresh(autor);
52
Frameworks de persistencia - JPA
em.getTransaction().commit();
System.out.println(autor.getNombre() + " ha escrito " +
autor.getMensajes().size() +
" mensajes");
Sin embargo, en desarrollos en los que utilizamos un API de persistencia de mayor nivel como
JPA, la utilización de un patrón DAO no es tan necesaria. Muchos argumentan incluso que
se trata de un antipatrón, un patrón que conviene evitar. No creemos que sea así, porque
un patrón DAO bien diseñado y adaptado a JPA permite eliminar errores derivados de la
mala utilización del contexto de persistencia y agrupar en una única clase todas las consultas
relacionadas con una misma clase de dominio.
12 13
Presentamos a continuación una adaptación del patrón DAO propuesto por Adam Bien ,
uno de los autores y expertos sobre Java EE más importantes en la actualidad.
Cada clase de entidad definirá una clase DAO en la que se definen los métodos CRUD sobre
la entidad (Create, Read, Update y Delete), así como el find que devuelve la entidad a partir
de su clave primaria. También definirán las consultas JPQL.
• create (Autor autor) : recibe una variable autor no gestionada y devuelve una
variable gestionada con la clave primaria actualizada
• update (Autor autor) : recibe una variable autor que puede no estar gestionada
(pero ya está en la base de datos) y actualiza el valor de la base de datos con los valores
de la variable
• delete (Autor autor) : borra la entidad autor de la base de datos
• find (Long id) : devuelve una entidad gestionada correspondiente a la clave primaria
que se pasa como parámetro.
Todos estos métodos se deben implementar a través de un entity manager que habremos
creado al comienzo del trabajo con los DAOs y le habremos pasado a los DAO en su creación.
O sea que los DAO trabajarán con un entity manager y una transacción abierta, de forma que
podremos englobar distintas operaciones de distintos DAOs en una misma transacción y un
mismo contexto de persistencia.
La capa responsable de abrir la transacción y el entity manager será la capa que utiliza los
DAO, la que implementa la lógica de negocio. Si se intenta llamar a cualquiera de los métodos
12
http://kenai.com/projects/javaee-patterns/sources/hg/show/DAOPattern?rev=454
13
http://www.adam-bien.com
53
Frameworks de persistencia - JPA
del DAO sin que se haya abierto una transacción en el entity manager se deberá lanzar una
excepción.
Vamos a definir todas las clases DAO en el paquete persistencia y las clases que usan
los DAO para implementar la lógica de negocio en el paquete service .
package org.expertojava.jpa.empleados.persistencia;
import javax.persistence.EntityManager;
public T create(T t) {
em.persist(t);
em.flush();
em.refresh(t);
return t;
}
public T update(T t) {
return (T) em.merge(t);
}
54
Frameworks de persistencia - JPA
create : se recibe un objeto entidad creado con los datos a hacer persistente y se llama
a persist() , flush() y refresh() para hacer persistente la entidad y actualizar
su clave primaria. Se devuelve la entidad gestionada.
update : de realiza un merge de la entidad y se devuelve la entidad resultante, ya
gestionada
delete : se hace un merge (la entidad que se pasa puede estar desconectada) y se
hace un remove
find : se define como un método abstracto que se implementará en los DAOs concretos
que extiendan esta clase abstracta genérica. Se define el perfil de la función: se devuelve
la entidad con la clave primaria que buscamos o null si no la encuentra.
Para cada clase entidad definiremos una clase DAO que extiende la clase anterior genérica.
En esta clase DAO concreta se implementan también todas las consultas relacionadas con la
entidad. Por ejemplo, a continuación vemos el ejemplo concreto de DAO EmpleadoDao :
package org.expertojava.jpa.empleados.persistencia;
import org.expertojava.jpa.empleados.modelo.Empleado;
import org.expertojava.jpa.empleados.modelo.Proyecto;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import java.util.List;
@Override
public Empleado find(Long id) {
EntityManager em = this.getEntityManager();
return em.find(Empleado.class, id);
}
Los DAO proporcionan métodos de un grano muy fino, un CRUD básico sobre las entidades
de cada clase. Trabajan con entidades gestionadas por un mismo entorno de persistencia y
en el código llamador hay que gestionar la transacción y el entity manager.
Es habitual definir clases de más nivel que contienen las operaciones de negocio en un
paquete servicio . Las operaciones de negocio se definen como métodos de estas clases
en los que se encapsulan las llamadas a los DAOs y todo el trabajo de las entidades
gestionadas. Los métodos reciben identificadores y datos planos y devuelven objetos entidad
desconectados de la base de datos
Por ejemplo, podemos definir la clase EmpleadoServicio con los métodos de negocio:
55
Frameworks de persistencia - JPA
import org.expertojava.jpa.empleados.modelo.Despacho;
import org.expertojava.jpa.empleados.modelo.Empleado;
import org.expertojava.jpa.empleados.persistencia.EmpleadoDao;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
56
Frameworks de persistencia - JPA
Todos los métodos de lógica de negocio son utilizados desde otras capas de la aplicación.
Por ejemplo, la capa de servicio. A diferencia de los métodos de los DAO, que toman y
devuelven entidades gestionadas, los métodos de negocio toman y devuelven objetos planos,
desconectados de cualquier transacción o gestor de persistencia. Son métodos atómicos.
Cuando terminan podemos estar seguros de que se ha realizado la operación. En el caso en
que haya algún problema, la operación no se realizará en absoluto (el estado de los objetos
quedará como estaba antes de llamar a la operación) y se lanzará una excepción runtime.
@Test
public void testServicioActualizaDespacho() {
EmpleadoServicio empleadoServicio = new EmpleadoServicio();
empleadoServicio.setEmf(emf);
empleadoServicio.intercambiaDespachos(1L,2L);
emp1 = empleadoServicio.findPorId(1L);
emp2 = empleadoServicio.findPorId(2L);
assertTrue(emp1.getDespacho().equals(desp2));
assertTrue(emp2.getDespacho().equals(desp1));
}
57
Frameworks de persistencia - JPA
2.7. Ejercicios
(0,5 puntos) Contexto de persistencia
En el módulo mensajes escribe nuevos tests que comprueben errores generados por
manejar incorrectamente el contexto de persistencia:
• Comprobación de que se lanza una excepción al intenta acceder a un atributo lazy después
de cerrar el contexto de persistencia
• Comprobación del error en el manejo de las colecciones en las relaciones a muchos
(apartado 2.5.4)
58
Frameworks de persistencia - JPA
Como hemos comentado en temas anteriores, la especificación del ORM se realiza mediante
anotaciones. JPA continua soportando además el formato de anotaciones basadas en ficheros
XML. Podemos consultar la lista de anotaciones definidas por JPA en la última parte de la
14
página del API de javax.persistence .
El ejemplo siguiente muestra el uso de las anotaciones en los campos. La anotación @Id
indica no sólo que el campo id es el identificador o clave primaria de la entidad, sino también
que se va a utilizar un acceso por campo. Los campos nombre y sueldo son hechos
persistentes en columnas con el mismo nombre.
@Entity
public class Empleado {
@Id private int id;
@Column(name="nom")
14
https://docs.oracle.com/javaee/7/api/javax/persistence/package-summary.html
59
Frameworks de persistencia - JPA
public Empleado() {}
En el código siguiente, la clase Empleado tiene una anotación @Id en el método getter
getId() , por lo que el proveedor podrá utilizar el acceso a la propiedad id para obtener
y establecer el estado de la entidad. Las propiedades nombre y sueldo se hacen
persistentes también automáticamente y se mapearán en columnas con el nombre nom (por
haberlo especificado con la anotación @column ) y sueldo . Nótese que es posible utilizar
nombres distintos para los campos. En el ejemplo, la propiedad sueldo está respaldada por
el campo paga . El proveedor ignora esta diferencia, ya que únicamente utiliza los getters y
setters.
@Entity
public class Empleado {
private int id;
private String nombre;
private Long paga;
public Empleado {}
60
Frameworks de persistencia - JPA
En esos casos el nombre que se utiliza para la tabla es el propio nombre de la entidad. Podría
darse el caso de que necesitáramos especificar un nombre distinto para la tabla, por ejemplo
si no estamos desarrollando la aplicación desde cero y partimos de un modelo de datos ya
creado. Podemos hacerlo con la anotación @Table en la que incluimos el nombre de la tabla.
El siguiente código muestra un ejemplo.
@Entity
@Table(name="EMP")
public class Empleado { ... }
Los nombres por defecto son los nombres de las clases, que en Java comienzan por
mayúscula y continúan con minúscula. ¿Cómo se mapean las mayúsculas y minúsculas en
la base de datos? Depende de la base de datos. Muchas bases de datos no distinguen
mayúsculas y minúsculas, por lo que en estos casos el nombre se convierte en mayúsculas.
En el caso de MySQL, sí que se distinguen entre mayúsculas y minúsculas, por lo que el nomre
de las tablas será idéntico al de las clases.
@Entity
@Table(name="EMP", schema="IT")
public class Empleado { ... }
Cuando se especifica de esta forma, el proveedor colocará el nombre del esquema como
prefijo del de la tabla cuando acceda a los datos. En este caso, los accesos se harán a la
tabla IT.EMP .
• Tipos primitivos Java: byte , int , short , long , boolean , char , float ,
double
• Clases wrapper de los tipos primitivos: Byte , Integer , Short , Long , Boolean ,
Character , Float , Double
• Arrays de bytes y char: byte[] , Byte[] , char[] , Character[]
• Tipos numéricos largos: java.math.BigInteger , java.math.BigDecimal
• Strings: java.lang.String
• Tipos temporales de Java: java.util.Date , java.util.Calendar
• Tipos temporales de JDBC: java.sql.Date , java.sql.Time ,
java.sql.Timestamp
• Tipos enumerados: cualquier tipo enumerado del sistema o definido por el usuario
• Objetos serializables: cualquier tipo serializable del sistema o definido por el usuario
61
Frameworks de persistencia - JPA
En algunos casos el tipo de la columna que está siendo mapeada no es exactamente el mismo
que el tipo Java. En casi todos los casos, el runtime del proveedor puede convertir el tipo
devuelto por la consulta JDBC en el tipo Java correcto del atributo. Si el tipo de la capa JDBC
no puede convertirse en el tipo Java del campo o la propiedad se lanza una excepción.
Cuando se hace persistente un campo o una propiedad, el proveedor comprueba que su tipo
es uno de los que está en la lista anterior. Si lo está, el proveedor lo transformará en el tipo
JDBC apropiado y lo pasará al driver JDBC.
Ahora que hemos comprobados que los campos (definidos en las variables de instancia) y las
propiedades (definidas en los getters y setters) son equivalentes en términos de persistencia,
los llamaremos de ahora en adelante atributos. Consideramos atributo un campo o una
propiedad de una clase estilo JavaBean.
Mapeo de columnas
Es posible anotar las características físicas de la columna de la base de datos en la que se
mapea un atributo utilizando la anotación @Column . Aunque es posible especificar bastantes
15
elementos, vamos a comentar sólo alguno de ellos (consultar la especificación de JPA para
obtener más información).
La primera característica que hay que mencionar es el nombre de la columna. Al igual que con
las tablas, es posible especificar los nombres de las columnas con las que se va a mapear
cada atributo. El siguiente código muestra un ejemplo.
@Entity
public class Empleado {
@Id
@Column(name="EMP_ID")
private int id;
private String nombre;
@Column(name=SAL)
private Long sueldo;
@Column(name=COM)
private String comentario;
// ...
}
La tabla resultante del mapeo se llamaría EMPLEADO y tendría como columnas EMP_ID ,
NOMBRE , SAL y COM . La primera columna sería la clave primaria de la tabla. La siguiente
figura muestra la tabla resultante en la base de datos.
15
https://docs.oracle.com/javaee/7/api/javax/persistence/package-summary.html
62
Frameworks de persistencia - JPA
Es posible también obligar a que un atributo no pueda dejarse a null utilizando el elemento
nullable=false . En la columna de la tabla se incluiría la restricción SQL NOT NULL . Por
ejemplo en el siguiente código obligaríamos a que el nombre del empleado nunca pudiera ser
null .
@Entity
public class Empleado {
@Id private int id;
@Column(nullable=false)
private String nombre;
// ...
}
Otra parámetro muy útil es el que nos permite generar la restricción UNIQUE sobre un campo
de la base de datos. Por ejemplo, podemos obligar a que el login se único así:
@Entity
public class Empleado {
@Id private int id;
@Column(nullable=false, unique=true)
String login
// ...
}
@Entity
public class Empleado {
@Id private int id;
@Column(length=40)
private String nombre;
// ...
}
Recuperación perezosa
El concepto de recuperación perezosa (lazy fetching en inglés) es muy importante para
gestionar de forma eficiente la base de datos. Ya veremos más adelante que también se aplica
a las relaciones entre entidades.
En ocasiones, sabemos que hay algunos atributos de la entidad a los que se accede con muy
poca frecuencia. En este caso podemos optimizar el rendimiento de los accesos a la base
de datos obteniendo sólo los datos que vamos a necesitar con frecuencia. Existen muchos
nombres para esta idea, entre los que se incluyen (en inglés) lazy loading, lazy fetching, on-
16
https://docs.oracle.com/javaee/7/api/javax/persistence/Column.html
63
Frameworks de persistencia - JPA
demand fetching o indirection. Todos significan lo mismo, que es que algunos datos no se
cargan en el objeto cuando éste es leído inicialmente de la base de datos sino que serán
recuperados sólo cuando sean referenciados o accedidos.
@Entity
public class Empleado {
// ...
@Basic(fetch=FetchType.LAZY)
@Column(name=COM)
private String comentario;
// ...
}
Antes de usar esta característica se debería tener claro unos cuantos aspectos. Lo primero es
que la declaración de un atributo como de recuperación perezosa no obliga a nada al proveedor
de persistencia. Sólo es una indicación para que pueda agilizar ciertas acciones sobre la base
de datos. El proveedor no está obligado a respetar la petición, ya que el comportamiento de
la entidad no queda comprometido haga una cosa u otra el proveedor.
Segundo, aunque en principio pueda parecer interesante definir ciertos atributos como de
carga perezosa, en la práctica no es correcto hacerlo con tipos simples (no relaciones a otras
entidades). La razón es que se gana poco haciendo que la base de datos devuelva parte de
una fila. Únicamente se gana algo y debería considerarse la recuperación perezosa cuando
tenemos muchas (decenas o cientos) columnas o cuando algunas columnas ocupan mucho
(por ejemplo, cadenas muy largas o lobs).
LOBs
El nombre que habitualmente se les da en base de datos a los objetos de tipo byte o caracter
que son muy grandes es el de large object o LOB como abreviatura. Las columnas de la base
de datos que almacenan estos tipos de objetos se deben acceder desde Java con llamadas
JDBC especiales. Para indicarle al proveedor que debería usar métodos de tipo LOB en el
driver de JDBC para acceder a ciertas columnas se debe utilizar la anotación @Lob .
En la base de datos se pueden encontrar dos tipos de LOBs: objetos grandes de tipo carácter,
llamados CLOBs y objetos grandes de tipo binario, llamados BLOBs. Como su nombre
indica, una columna CLOB guarda una larga secuencia de caracteres, mientras que un BLOB
guarda una larga secuencia de bytes no formateados. Los tipos Java que se mapean con
columnas CLOB son String , char[] y Character[] , mientras que byte[] , Byte[]
y Serializable se mapean con columnas de tipo BLOB.
64
Frameworks de persistencia - JPA
El siguiente código muestra el ejemplo de mapeo de una columna con un BLOB imagen.
Suponemos que la columna PIC guarda una imagen del empleado, que se mapea en el atributo
foto de tipo byte[] .
@Entity
public class Empleado {
@Id private int id;
@Basic(fetch=FetchType.LAZY)
@Lob @Column(name="PIC")
private byte[] foto;
// ...
}
Tipos enumerados
Otro tipo Java que puede ser mapeado en la base de datos es cualquier tipo enumerado del
sistema o definido por el usuario.
Al igual que en otros lenguajes de programación, a los valores de los tipos enumerados en Java
se les asigna un ordinal implícito que depende del orden de creación. Este ordinal no puede
ser modificado en tiempo de ejecución y es el que se utiliza para representar y almacenar el
valor del tipo enumerado. El proveedor, por tanto, mapeará un tipo enumerado en una columna
de tipo entero y sus valores en números enteros específicos.
Los ordinales asignados en tiempo de compilación a los valores de este tipo enumerado
son 0 para EMPLEADO_TIEMPO_PARCIAL , 1 para EMPLEADO_TIEMPO_COMPLETO y 2 para
EMPLEADO_EXTERNO . El siguiente código utiliza este tipo para definir un atributo de la
entidad:
@Entity
public class Empleado {
@Id private int id;
private TipoEmpleado tipo;
// ...
}
Podemos ver que el mapeado es trivial, ya que no hay que hacer nada especial y el proveedor
se encarga de realizar la transformación del tipo enumerado al tipo entero de la base de datos.
Sin embargo, hay que tener cuidado con una cosa. Si en algún momento cambiamos el
tipo enumerado podemos tener problemas, ya que puede cambiar el orden de los valores
en el tipo enumerado y no corresponderse con los ya existentes en la base de datos.
Por ejemplo, supongamos que necesitamos añadir un nuevo tipo de empleado a tiempo
completo: EMPLEADO_TIEMPO_COMPLETO_EXCEDENCIA y supongamos que lo añadimos
65
Frameworks de persistencia - JPA
Podríamos modificar la base de datos y ajustar todos las entidades, pero si los ordinales se
utilizan en algún otro lugar tendríamos que arreglarlo también. No es una buena política de
mantenimiento.
Una solución mejor sería almacenar el nombre del valor como una cadena en lugar de
almacenar el ordinal. Esto nos aislaría de los cambios en la declaración y nos permitiría añadir
nuevos tipos sin tener que preocuparnos sobre los datos existentes. Podemos hacer esto
añadiendo una anotación @Enumerated en el atributo y especificando un valor de STRING .
El siguiente código muestra cómo hacerlo:
@Entity
public class Empleado {
@Id private int id;
@Enumerated(EnumType.STRING)
private TipoEmpleado tipo;
// ...
}
Hay que hacer notar de esta forma no arreglamos el problema completamente. Ahora en
la base de datos se guardan las cadenas EMPLEADO_TIEMPO_COMPLETO y demás. Si en
algún momento modificamos el nombre de los valores del tipo enumerado también deberíamos
cambiar los datos de la base de datos. Pero esto es menos frecuente, ya que un cambio en
los valores de un tipo enumerado nos obliga a cambiar todo el código en el que aparezcan
los valores, y esto es bastante más serio que cambiar los datos de una columna de la base
de datos.
En general, definir el tipo enumerado como un ordinal es la forma más eficiente de trabajar,
pero siempre que no sea probable tener que añadir nuevos valores en medio de los ya
existentes.
Tipos temporales
Los tipos temporales son el conjunto de tipos basados en tiempo que pueden usarse en
el mapeo entidad-relación. La lista de tipos temporales soportados incluye los tres tipos
java.sql java.sql.Date , java.sql.Time y java.sql.Timestamp , e incluye
también los tipos java.util java.util.Date y java.util.Calendar .
@Entity
66
Frameworks de persistencia - JPA
Estado transitorio
Es posible definir en la entidad atributos que no se hacen persistentes utilizando la palabra
clave de Java transient o el atributo @Transient . Si se especifica alguna de estas
propiedades, el proveedor no aplicará las reglas por defecto al atributo marcado.
Los campos transitorios son útiles, por ejemplo, para cachear un estado en memoria que
no queremos recalcular o reinicializar. En el ejemplo siguiente usamos el campo transitorio
traduccion para guardar la traducción de la palabra "Empleado" en el locale actual, de
forma que se imprima correctamente el nombre. El uso del modificador Java transient
hace que el atributo sea temporal no sólo para la persistencia sino también para la máquina
virtual. Si el Empleado se serializa y se envía desde una MV a otra el valor del atributo
traduccion no se enviaría.
@Entity
public class Empleado {
@Id private int id;
private String nombre;
private Long sueldo;
transient private String traduccion;
// ...
ResourceBundle.getBundle("EmpResources").getString("Empleado");
}
return traduccion + ": " + id + " " + nombre;
}
}
Un ejemplo muy común es el tipo Direccion . Puede ser que en nuestro dominio una
dirección no tenga las características que le hagan definir una entidad persistente (no vamos a
67
Frameworks de persistencia - JPA
Las ventajas de agrupar un conjunto de campos en un nuevo tipo de datos Java son múltiples.
En primer lugar, abstraemos el modelo físico (representación en la tabla de la base de datos)
y obtenemos una representación más cercana al dominio de la aplicación. Podremos utilizar
objetos Direccion en distintas partes de la lógica de negocio. En segundo lugar, podemos
reutilizar este tipo en más de una entidad, dando consistencia a nuestro modelo físico. Por
último, es una forma muy portable de conseguir una características de SQL que nunca se ha
llegado a estandarizar: el uso de tipos definidos por el usuario.
Vamos a ver el ejemplo con más detalle. La siguiente figura muestra una tabla EMPLEADO
que contiene una mezcla de información propia del empleado y de columnas que definen su
dirección:
@Embeddable
public class Direccion {
68
Frameworks de persistencia - JPA
Para usar esta clase en una entidad hay que declararla con la anotación @Embedded . Se
muestra a continuación.
@Entity
public class Empleado {
@Id private int id;
private String nombre;
private Long sueldo;
@Embedded private Direccion direccion;
// ...
}
Es una buena práctica el utilizar siempre claves primarias generadas. La utilización de claves
naturales termina causando problemas. Una clave primaria tiene que ser única, constante y
no vacía. Es muy difícil encontrar estas características en los atributos del modelo de negocio.
Incluso si los encontramos, es muy frecuente que a lo largo de la vida de la base de datos
suceda algún cambio en el negocio que obligue a modificarlos. Por ejemplo, un NIF erróneo
que hay que modificar o un nombre de usuario que antes era inmutable y ahora nos piden
que sea modificable.
La utilización de claves primarias generadas en todas las tablas de nuestra base de datos
también proporciona una gran consistencia a la aplicación y permite procedimientos unificados
en la gestión de claves ajenas y referencias.
Por último, en un modelo orientado a objetos como el de JPA, el uso de claves primarias
autogeneradas refuerza la idea de que las entidades son objetos. La clave de una entidad
es similar a la dirección de memoria de un objeto. Los objetos mantienen referencias a
otros objetos de la misma forma que las entidades guardan claves ajenas a otras entidades.
Referencias y claves se utilizan para definir grafos de objetos relacionados entre si. Tanto las
claves como las referencias son internas de la aplicación y el usuario no debe conocerlas ni
utilizarlas directamente.
69
Frameworks de persistencia - JPA
Aun así, hay muchas bases de datos heredadas para las que será necesario utilizar claves
primarias ya existentes. Por ejemplo, es usual la utilización de claves primarias compuestas
para asegurarse la unicidad. JPA permite también el mapeo de este tipo de claves primarias.
Veremos en primer lugar las estrategias de JPA para implementar claves primarias
autogeneradas y después pasaremos a la gestión de claves compuestas para trabajar con
bases de datos heredadas.
En general, JPA permite utilizar como clave primaria los siguientes tipos:
Igual que con los mapeos básicos, la anotación @Column puede utilizarse para modificar el
nombre con el que el atributo identificador se hace persistente. Si no se utiliza sucede igual
que con los mapeos básicos y el campo se guarda en la columna con el mismo nombre.
Es importante tener en cuenta que en este caso el identificador de la entidad puede ser que no
esté disponible hasta que se haya realizado la inserción en la base de datos. Esto no significa
que no lo podamos utilizar. Podríamos por ejemplo, asignarlo a otra entidad en una relación
entre entidades o modificar sus atributos. Cuando se realiza un flush es cuando nos podemos
asegurar que se actualiza automáticamente esta clava primaria.
Con la opción AUTO dejamos que sea el proveedor de persistencia el que se ocupe de cómo
generar los identificadores. El siguiente código muestra un ejemplo:
@Entity
public class Empleado {
@Id @GeneratedValue(strategy=GenerationType.AUTO)
private int id;
// ...
}
Esta estrategia es muy portable, ya que se mapea a los distintos sistemas de generación de
identidades usados por los sistemas de gestión de base de datos, como identity, sequence
o HiLo.
70
Frameworks de persistencia - JPA
En la estrategia SEQUENCE crea una secuencia en DB2, PostgreSQL, Oracle, SAP DB; o un
generador en InterBase. El tipo devuelto es también long , short , int .
Por último, en la estrategia TABLE se usa una tabla de la base de datos que guarda las últimas
claves primarias generadas. Cada fila de la tabla corresponde a una clave primaria. La tabla
tiene dos columnas: pkColumnName y valueColumnName . La columna pkColumnValue
define el generador de clave primaria y la columna valor guarda la última generada.
La especificación de JPA permite utilizar hasta tres estrategias distintas para manejar claves
compuestas:
• Encapsular los atributos de la clave compuesta en una clase separada y marcarla como
@Embeddable como un componente normal. Usar un atributo de esta clase como clave
compuesta anotándolo con el atributo @Id como siempre.
• Encapsular los atributos de la clave compuesta en una clase separada sin ninguna
anotación. Usar un atributo de esta clase como clave compuesta anotándolo con
@EmbeddedId .
• Incluir la anotación @IdClass referenciando la clase que implementa la clave compuesta
y declarar explícitamente todos los atributos que forman la clave compuesta.
En todas las estrategias hay que definir una clase con los atributos de la clave compuesta.
Lo que cambia es la forma de utilizar esa clase. Veamos un ejemplo. Supongamos que
queremos mapear una tabla USUARIOS con una clave primaria compuesta formada por
USUARIO y DEPTO , ambos VARCHAR . Usando la primera opción, que debemos crear una
clase UsuarioId con la anotación @Embeddable :
@Embeddable
public class UsuarioId implements Serializable {
private String username;
private String departamento;
...
71
Frameworks de persistencia - JPA
Y en la entidad principal utilizamos esta clase como clave primaria, indicando los nombres de
las columnas con la anotación @AttributeOverride :
@Entity
@Table(name = "USUARIOS")
public class Usuario {
@Id
@AttributeOverrides({
@AttributeOverride(name="username", column=@Column(name="USUARIO")),
@AttributeOverride(name="depto", column=@Column(name="DEPTO"))
})
private UsuarioId usuarioId;
...
}
@Entity
@Table(name = "USUARIOS")
public class Usuario {
@EmbeddedId
@AttributeOverrides({
@AttributeOverride(name="username", column=@Column(name="USUARIO")),
@AttributeOverride(name="depto", column=@Column(name="DEPTO"))
})
private UsuarioId usuarioId;
...
}
@ManyToOne
@JoinColumns({
@JoinColumn(name="USUARIO", referencedColumnName = "USUARIO"),
@JoinColumn(name="DEPTO", referencedColumnName = "DEPTO")
})
private Usuario vendedor;
@OneToMany(mappedBy = "seller")
72
Frameworks de persistencia - JPA
JPA permite mapear estas relaciones de herencia en tablas de la base de datos. Existen tres
posibles estrategias para realizar este mapeo:
• Tabla única
• Tablas join
• Una tabla por clase
En la figura siguiente aparece la tabla única que guardaría entidades de ambos tipos. La
columna discriminante es la columna Tipo . La tabla contiene todos los registros. Los registros
que se corresponden con empleados contratados tienen el valor Contrato en la columna
discriminante y los empleados becarios Beca . Las columnas que no se correspondan con el
tipo de entidad están a NULL.
73
Frameworks de persistencia - JPA
El siguiente código muestra cómo sería la definición de la clase Empleado . En este caso
la estamos definiendo como abstract para impedir crear instancias y obligar a que las
instancias sean de las clases hijas. No es obligatorio hacerlo así, podría darse el caso de que
nos interesara también tener instancias de la clase padre sin especificar el tipo de empleado.
@Entity
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="Tipo",
discriminatorType=DiscriminatorType.STRING)
public abstract class Empleado {
...
}
Las subclases de la jerarquía se definen igual que en Java estándar (recordemos que las
entidades son clases Java normales) con la palabra clave extends . Lo único que hay
que añadir es el valor en la columna discriminante que se le asigna a esta clase. Para ello
se utiliza la anotación DiscriminatorValue y su atributo value . Por ejemplo, si el
EmpleadoBecario va a tener como valor la cadena Beca , hay que indicar:
@DiscriminatorValue(value="Beca")
import javax.persistence.*;
@Entity
@DiscriminatorValue(value="Contrato")
public class EmpleadoContratado extends Empleado {
private Long planPensiones;
Por último, la clase EmpleadoBecario se distingue por el valor Beca . En la clase se define
el nuevo atributo Long seguroMedico
74
Frameworks de persistencia - JPA
@Entity
@DiscriminatorValue(value="Beca")
public class EmpleadoBecario extends Empleado {
private Long seguroMedico;
En las clases hijas no se define la clave primaria. Debe estar definida en la clase padre. Lo
mismo sucede con los método equals() y hashCode() , los debemos definir en la clase
padre. En la comprobación de igualdad se debe utilizar la comprobación de identidad de clase
para identificar como distintos objetos de distintas subclases (podemos usar el asistente de
Eclipse para generar el método).
Con esto es suficiente. Una vez definidas, las entidades se usan como clases Java normales:
El API se puede aplicar a cualquier clase Java, no está limitada a entidades JPA. De hecho es
muy útil para validar datos que se reciben en las peticiones a los servicios antes de crear las
propias entidades. Sin embargo en los ejemplos que mostramos a continuación sólo utilizamos
el API para anotar atributos de entidades JPA.
Al igual que JPA, Bean Validation es parte de la especificación Java EE pero puede utilizarse
en aplicaciones Java standalone Java SE, junto con JPA. El propio gestor de persistencia
detecta las anotaciones y se encarga de asegurar las restricciones antes de actualizar los
datos en la base de datos. Para usar Bean Validation en una aplicación Java standalone basta
con incluir las librerías necesarias en el classpath (en nuestro caso, actualizaando el POM
de Maven).
17
La última especificación aprobada es la JSR 349 correspondiente a Bean Validation 1.1.
18
Puedes consultar en el siguiente enlace el API completo de Bean Validation . El líder de la
17
https://www.jcp.org/en/jsr/detail?id=349
75
Frameworks de persistencia - JPA
19
especificación es Emmanuel Bernard, de Red Hat. Mantiene una página sobre el API en la
que también publica novedades y noticias.
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.1.3.Final</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>el-impl</artifactId>
<version>2.2</version>
</dependency>
Por defecto, el entity manager de JPA realiza la comprobación de las restricciones Bean
Validation si el JAR se encuentra en el classpath. Se puede definir el comportamiento en el
fichero persistence.xml :
<persistence version="2.1"
xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</
provider>
<class>org.expertojava.jpa.mensajes.modelo.Autor</class>
<class>org.expertojava.jpa.mensajes.modelo.Mensaje</class>
<validation-mode>CALLBACK</validation-mode>
<properties>
</properties>
18
https://docs.oracle.com/javaee/7/api/javax/validation/package-summary.html
19
http://beanvalidation.org
76
Frameworks de persistencia - JPA
</persistence-unit>
</persistence>
Las restricciones definidas por Bean Validation son independientes de las definidas por las
anotaciones JPA, se validan y comprueban en memoria y no generan ninguna anotación
adicional en el esquema de base de datos.
Por ejemplo, la siguiente definición de la entidad Empleado obliga a que el nombre y los
apellidos no sean null (en memoria), que el tamaño de la cadena no sea mayor de 25
caracteres y que el sueldo no sea mayor de 50.000.
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Entity
public class Empleado {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private int idEmpleado;
@NotNull
@Size(max = 25)
@Column(nullable = false)
private String nombre;
@NotNull
@Size(max = 25)
@Column(nullable = false)
private String apellidos;
@NotNull
@DecimalMax(value = "50000.00")
@Column(nullable = false)
private Double sueldo;
20
https://docs.oracle.com/javaee/7/api/javax/validation/constraints/package-summary.html
77
Frameworks de persistencia - JPA
78
Frameworks de persistencia - JPA
Proceso de validación
Si algún valor no cumple la restricción, se lanza una excepción unchecked (de tiempo de
ejecución) de tipo javax.validation.ConstraintViolationException . El momento
en que se hace la validación depende de la implementación del API. Hibernate valida todas
las restricciones cuando hace un flush a la base de datos. Otros proveedores de persistencia
(EclipseLink, por ejemplo) lo hacen en el propio método persist o merge .
En todas las operaciones de la capa de servicio se deben capturar las excepciones y cerrar
la transacción y el entity manager de forma correcta. Vamos a actualizar el código de una
79
Frameworks de persistencia - JPA
de las operaciones que vimos en la sesión anterior con el manejo de las excepciones. La
llamada em.flush justo antes de realizar el commit de la transacción asegura que el
proveedor (en nuestro caso Hibernate) realiza la validación de las restricciones antes y que
se lanza la excepción ConstraintViolationException . En el caso de Hibernate, si no
lo pusiéramos, la excepción se lanzaría dentro del tx.commit() y sería envuelta por una
excepción de tipo RollbackException :
//
// Hay que actualizar todos los métodos de negocio hay para que
capturen
// las excepciones producidas por la capa de validación y de datos
//
EntityManager em = EmfSingleton.getInstance().createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
PeliculaDao daoPelicula = new PeliculaDao(em);
Pelicula pelicula = daoPelicula.find(idPelicula);
pelicula.setRecaudacion(recaudacion);
daoPelicula.update(pelicula);
em.flush();
tx.commit();
} catch (Exception ex) {
tx.rollback();
String mensaje = "Error al actualizar la pelicula "
+ id + " con la recaudación " + recaudacion;
logger.error(mensaje, ex);
throw(ex)
} finally {
em.close();
}
}
80
Frameworks de persistencia - JPA
3.8. Ejercicios
En esta sesión de ejercicios vamos a continuar con el proyecto filmoteca , en concreto
vamos a añadir lo aprendido en esta sesión a la clase Pelicula .
81
Frameworks de persistencia - JPA
Vamos a ilustrar estos conceptos con la relación que vimos en la sesión 1. Allí definíamos dos
entidades, Autor y Mensaje y una relación muchos-a-uno bidireccional de Mensaje a
Autor . Un autor puede escribir muchos mensajes.
La relación la representamos con la figura siguiente, en donde se muestran las dos entidades
unidas con una flecha con dos puntas que indica que la relación es bidireccional y con la
anotación de la cardinalidad de cada entidad bajo la flecha. La flecha presupone un atributo
con el mismo nombre y cardinalidad de la entidad a la que se apunta:
El modelo físico que se genera con el mapeado es el que se muestra en la siguiente figura. La
tabla MENSAJE contiene una clave ajena hacia la tabla AUTOR , en su columna AUTOR . Los
valores de esta columna guardan claves primarias de autores. De esta forma relacionamos
cada mensaje con el autor que lo ha escrito. Cuando queramos obtener todos los mensajes
de un determinado autor se deberá hacer un SELECT en la tabla MENSAJE buscando todas
las filas que tengan como clave ajena el autor que buscamos.
Vamos a ver las distintas características que hay que definir para especificar completamente
una relación en el modelo de entidades, utilizando como ejemplo esta relación entre Mensaje
y Autor .
Direccionalidad
En primer lugar debemos considerar la direccionalidad de la relación. Nos indica si desde
una entidad podemos obtener la otra. Una relación puede ser unidireccional cuando desde
la entidad origen se puede obtener la entidad destino o bidireccional, como es el caso del
ejemplo, cuando desde ambas partes de la relación se puede obtener la otra parte.
82
Frameworks de persistencia - JPA
Cardinalidad
La cardinalidad de una relación define el número de instancias de una entidad que pueden
estar relacionada con otras. Atendiendo a la cardinalidad, una relación puede ser uno-a-uno,
uno-a-muchos, muchos-a-uno o muchos-a-muchos.
En el caso del ejemplo tenemos una relación muchos-a-uno entre Mensaje y Autor . Al
ser una relación bidireccional, también podríamos considerar esta relación como una relación
uno-a-muchos entre Autor y Mensaje .
En el ejemplo visto, la relación se mapea haciendo que la tabla MENSAJE contenga una clave
ajena hacia la tabla AUTOR , como hemos visto en la figura. Decimos entonces que la entidad
Mensaje es la propietaria de la relación.
Para especificar las columnas que son claves ajenas en una relación se utilizan las anotaciones
@JoinColumn y mappedBy . La primera identifica la entidad propietaria de la relación y
permite definir el nombre de la columna que define la clave ajena (equivalente a la anotación
@Column en otros atributos). Su uso es opcional, aunque es recomendable, ya que hace el
mapeo más fácil de enteder. La segunda anotación es obligatoria se usa en el atributo de la
entidad no propietaria que no se mapea en ninguna columna de la tabla. Como veremos más
adelante, este elemento debe indicar el nombre del atributo que representa la clave ajena en
la otra entidad.
También es posible mapear una relación utilizando una tabla join, una tabla auxiliar con dos
claves ajenas hacia las tablas de la relación. No nos da tiempo en este tema de explicar cómo
hacerlo. Si estás interesado puedes consultar en la especificación de JPA.
Actualización de la relación
Ya hemos comentado previamente que una de las características fundamentales del
funcionamiento de JPA es que las instancias de las entidades viven en el contexto de
persistencia asociado al entity manager hasta que se realiza una operación de flush sobre la
base de datos. Normalmente esto sucede cuando se cierra la transacción. En ese momento
es cuando el proveedor de persistencia consulta las instancias del contexto y genera las
sentencias SQL que actualizan la base de datos. Es interesante tener activo el debug de
83
Frameworks de persistencia - JPA
Hibernate que muestra las sentencias generadas para comprobar en qué momento se realizan
y cuáles son.
Esto es, para que una relación se sincronice correctamente en la base de datos, se debe
actualizar la instancia a la que se refiere en la parte propietaria de la relación. En el caso del
ejemplo, para añadir una relación entre un Mensaje y un Autor , lo imprescindible a efectos
de actualización de la base de datos es la sentencia en la que se añade el autor asociado al
mensaje (el Mensaje es la clase propietaria de la relación, la que contiene la clave ajena).
JPA no actualiza automáticamente la relación en memoria. Si está definida la relación inversa
(la colección de mensajes de un autor) tenemos que actualizarla a mano en el programa,
añadiendo el mensaje a la colección. Todo esto se hace con dos líneas de código:
@Entity
public class Empleado {
// ...
@OneToOne
private despacho Despacho;
// ...
@Entity
public class Despacho {
84
Frameworks de persistencia - JPA
// ...
@OneToOne(mappedBy = "despacho")
private Empleado empleado;
// ...
}
La entidad Empleado es la propietaria de la relación y en su tabla hay una clave ajena (en
la columna despacho ) que apunta a la otra tabla. Se puede especificar esta clave ajena con
la anotación @JoinColumn :
em.getTrasaction().begin();
Empleado empleado = em.find(Empleado.class, 1L);
Despacho despacho = em.find(Despacho.class, 3L);
empleado.setDespacho(despacho);
em.getTransaction().commit();
85
Frameworks de persistencia - JPA
@Empleado
public class Empleado {
// ...
@OneToOne
@JoinColumn(name = "despacho_id", unique = true)
private despacho Despacho;
// ...
}
@Entity
public class Despacho {
// ...
@OneToOne(mappedBy="despacho")
private empleado Empleado;
// ...
}
Hay que destacar la utilización del elemento mappedBy en el atributo despacho . Con ese
elemento estamos indicando al proveedor de persistencia cuál es la clave ajena en la entidad
Empleado . En la tabla asociada a la entidad despacho no existe ninguna columna que
se refiere al empleado, sino que el get se obtiene haciendo una SELECT en la otra tabla (la
propietaria de la relación). En el elemento mappedBy en una entidad B hay que especificar
el nombre del atributo que en la otra entidad A guarda la referencia a B.
@Entity
public class Empleado {
....
public Despacho getDespacho() {
return despacho;
}
86
Frameworks de persistencia - JPA
@Entity
public class Despacho {
...
public void setEmpleado(Empleado empleado) {
this.empleado = empleado;
empleado.setDespacho(this);
}
em.getTransaction().begin();
Empleado empleado1 = em.find(Empleado.class, 1L);
Despacho despacho1 = empleado1.getDespacho();
Empleado empleado3 = em.find(Empleado.class, 3L);
empleado1.quitaDespacho();
empleado3.setDespacho(despacho1);
em.getTransaction().commit();
Al poner primero a null el despacho del empleado1 nos aseguramos de que no haya un
error de integridad en la base de datos que sucedería si se cambiara primero el despacho
del empleado3, antes de cambiar el despacho del empleado1. El valor de la columna
departamento_id no sería único.
El nuevo despacho del empleado3 es el despacho1 (y también se actualiza
automáticamente el otro lado de la relación en memoria: el nuevo empleado del
despacho1 es el empleado3).
87
Frameworks de persistencia - JPA
El ejemplo que presentamos al comienzo del capítulo es de este tipo. Otro ejemplo podría ser el
de la relación entre un departamento y los empleados. Un Departamento está relacionado
con muchos Empleado`s. Y un `Empleado pertenece a un único Departamento . De
esta forma el Empleado hace el papel de propietario de la relación y es quien llevará la clave
ajena hacia Departamento . Lo vemos en el siguiente código:
@Entity
public class Empleado {
...
@ManyToOne
@JoinColumn(name="departamento_id")
private departamento Departamento;
...
}
@Entity
public class Departamento {
...
@OneToMany(mappedBy="departamento")
private Set<Empleado> empleados = new HashSet();
...
}
@Entity
public class Empleado {
...
88
Frameworks de persistencia - JPA
En la clase Departamento podemos definir también métodos auxiliares que llamen a los
anteriores. Haremos esto sólo si es desde el punto de vista de la lógica de negocio.
@Entity
public class Departamento {
...
89
Frameworks de persistencia - JPA
@Empleado
public class Empleado {
...
@ManyToOne
@JoinColumn(name="categoria_id")
private categoria Categoria;
...
@Entity
public class Categoria {
// ...
}
@Entity
public class Empleado {
@Id
@GeneratedValue
90
Frameworks de persistencia - JPA
...
}
@Entity
public class Proyecto {
@Id
@GeneratedValue
private Long id;
String codigo;
@ManyToMany(mappedBy="proyectos");
private Set<Empleado> empleados = new HashSet();
...
La relación se mapea, a diferencia de los casos anteriores, utilizando una tabla join que
construye JPA automáticamente utilizando los nombres de las dos entidades. Es una tabla con
dos columnas que contienen claves ajenas a las tablas de las entidades. La primera columna
apunta a la tabla propietaria de la relación y la segunda a la otra tabla. La siguiente imagen
representa el mapeo de la relación anterior.
En el caso de ya existir la tabla join, o de necesitar un nombre específico para sus elementos,
podríamos utilizar la anotación @JoinTable de la siguiente forma:
@Entity
public class Empleado {
91
Frameworks de persistencia - JPA
...
@ManyToMany
@JoinTable(
name = "EMPLEADO_PROYECTO",
joinColumns = {@JoinColumn(name = "EMPLEADO_NOMBRE")},
inverseJoinColumns = {@JoinColumn(name = "PROYECTO_CODIGO")}
)
private Set<Projecto> proyectos = new HashSet<Projecto>();
...
}
@ManyToMany(mappedBy = "proyectos")
private Set<Empleado> empleados = new HashSet<Proyecto>();
em.getTransaction().begin();
em.getTransaction().commit();
92
Frameworks de persistencia - JPA
@Entity
public class Empleado {
...
public Set<Proyecto> getProyectos() {
return proyectos;
}
...
}
En la clase Proyecto :
@Entity
public class Proyecto {
...
public Set<Empleado> getEmpleados() {
return empleados;
}
93
Frameworks de persistencia - JPA
Por ejemplo, un Empleado tiene una colección de Patentes que ha desarrollado. Distintos
empleados pueden participar en la misma patente. Pero no nos interesa guardar la información
de qué empleados han desarrollado una patente determinada. La forma de especificarlo es:
@Entity
public class Empleado {
@Id String nombre;
@ManyToMany
private Set<Patentes> patentes = new HashSet();
// ...
}
@Entity
public class Patente {
// ...
}
En el mapeo de la relación se crea una tabla join llamada EMPLEADO_PATENTE con las dos
claves ajenas hacia EMPLEADO y PATENTE .
94
Frameworks de persistencia - JPA
cada vez que añadimos un Proyecto a una Empleado . Por ejemplo, la fecha en la que el
empleado entra en el proyecto y el cargo en el mismo. Lo vemos en la siguiente figura.
Una forma de mapearlo a JPA es creando una entidad nueva en la que se mapea la tabla.
Llamamos a la entidad ProyectoEmpleado . Tiene como clave primaria la pareja de claves
ajenas a las tablas de empleados y proyectos:
@Entity
@Table(name = "PROYECTO_EMPLEADO")
public class ProyectoEmpleado {
@Embeddable
public static class Id implements Serializable {
@Column(name = "PROYECTO_ID")
private int proyectoId;
@Column(name = "EMPLEADO_ID")
private int empleadoId;
public Id() {}
@EmbeddedId
95
Frameworks de persistencia - JPA
@Column(name = "FECHA")
private Date fecha = new Date();
@Column(name = "CARGO")
private String cargo;
@ManyToOne
@JoinColumn(name="PROYECTO_ID",
insertable = false,
updatable = false)
private Proyecto proyecto;
@ManyToOne
@JoinColumn(name="EMPLEADO_ID",
insertable = false,
updatable = false)
private Empleado empleado;
public ProyectoEmpleado() {}
this.id.proyectoId = proyecto.getId();
this.id.empleadoId = empleado.getId();
// Getters y setters
...
}
Este es un buen ejemplo para terminar el apartado, porque se utilizan en él múltiples elementos
que hemos ido viendo en las dos últimas sesiones. Se utiliza como clave primaria de la nueva
entidad una clave compuesta con las columnas PROYECTO_ID y EMPLEADO_ID . Hemos
utilizado la estrategia de clase estática anidada para definirla. En el constructor se actualizan
los valores de la clave primaria con las claves primarias del proyecto y el empleado. También
se actualizan las propiedades que definen la relación a-muchos ( proyecto y empleado ) y
se actualizan las relaciones inversas para asegurar la integridad referencial en memoria.
96
Frameworks de persistencia - JPA
Esta funcionalidad hace que la carga de instancias sea muy eficiente, pero hay que saber
utilizarla correctamente. Un posible problema puede surgir cuando la instancia original (el
Departamento ) queda desconectada (detached) del EntityManager (lo veremos en el tema
siguiente). Entonces ya no podremos acceder a ningún atributo que no se haya cargado.
Es posible que quereamos desactivar la propiedad por defecto de la carga perezosa. Para
ello debemos definir la opción fetch=FetchType.EAGER en la anotación que define el tipo
de relación:
@Entity
public class Empleado {
@Id String nombre;
@ManyToMany(fetch=FetchType.EAGER)
private Set<Patentes> patentes = new HashSet();
// ...
}
21
http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html_single/#collections
97
Frameworks de persistencia - JPA
4.5. Ejercicios
Continuamos con el proyecto filmoteca .
98
Frameworks de persistencia - JPA
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.
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)
• campus : String
99
Frameworks de persistencia - JPA
• edificio : String
@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;
...
}
100
Frameworks de persistencia - JPA
@Embeddable
public class Direccion {
private String campus;
private String edificio;
...
}
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.
QUERY = "SELECT e
FROM Empleado e
WHERE e.departamento = ?1 AND
e.salario > ?2"
QUERY = "SELECT e
101
Frameworks de persistencia - JPA
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.
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:
@Entity
@NamedQuery(name="Empleado.salarioPorNombreDepartamento",
102
Frameworks de persistencia - JPA
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:
103
Frameworks de persistencia - JPA
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:
104
Frameworks de persistencia - JPA
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
Consultas básicas
La consulta más sencilla en JPQL es la que selecciona todas las instancias de un único tipo
de entidad:
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 .
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:
105
Frameworks de persistencia - JPA
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:
if (departamentos != null) {
for (Departamento departamento : departamentos) {
System.out.println(departamento.getNombre());
}
}
106
Frameworks de persistencia - JPA
SELECT e
FROM Empleado e
WHERE e.departamento.nombre LIKE '%IA' AND
e.sueldo BETWEEN 2000 AND 2500
if (empleados != null) {
for (Empleado empleado : empleados) {
System.out.println(empleado.getNombre() + " - " +
empleado.getSueldo();
}
}
}
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 :
if (tuplas != null) {
for (Object[] tupla : tuplas) {
String nombre = (String) tupla[0];
Double sueldo = (Double) tupla[1];
System.out.println(nombre + " - " + sueldo);
}
107
Frameworks de persistencia - JPA
SELECT f.concepto
FROM Empleado e, FacturaGasto f
WHERE e = f.empleado AND
e.departamento.nombre='DCCIA'
Código Java:
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.
108
Frameworks de persistencia - JPA
Selecciona todos los departamentos distintos asociados a empleados (sin el operador JOIN ):
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'
En la clase Empleado :
@Entity
public class Empleado {
...
@ManyToOne(fetch = FetchType.LAZY)
private Departamento departamento;
...
}
query = em.createQuery(JOIN_FETCH);
109
Frameworks de persistencia - JPA
em.close();
if (empleados != null) {
for (Empleado empleado : empleados) {
System.out.println(empleado.getDepartamento().getDireccion().getEdificio());
}
}
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()
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:
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'.
En el caso de colecciones hay que utilizar los operadores IS EMPTY o IS NOT EMPTY :
Funciones
Es posible utilizar llamadas a funciones predefinidas en JPQL. Veamos unos cuantos
ejemplos:
110
Frameworks de persistencia - JPA
CONCAT(string1, string2)
Por ejemplo:
LOWER(string)
UPPER(string)
LENGTH(string)
Pattern matching
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
111
Frameworks de persistencia - JPA
• c.name LIKE '_r%' es TRUE para 'Brasil' and FALSE for 'Dinamarca'
• c.name LIKE '%' es siempre TRUE
• c.name NOT LIKE '%' es siempre FALSE
Para emparejar un carácter subrayado o tanto por ciento tiene que ir precedido por un carácter
de escape. Por ejemplo:
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:
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:
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.
112
Frameworks de persistencia - JPA
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.
SELECT MAX(e.sueldo)
FROM Empleado e
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:
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.
113
Frameworks de persistencia - JPA
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:
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.
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);
114
Frameworks de persistencia - JPA
cq.where(cb.greaterThan(e.get("sueldo"), 2500.0));
Query query = em.createQuery(cq);
List<Empleado> emps = query.getResultList();
String FIND_ALL_SUELDOS_EMPLEADOS =
"SELECT e.sueldo FROM Empleado e";
@Override
public Empleado find(Long id) {
EntityManager em = this.getEntityManager();
return em.find(Empleado.class, id);
}
115
Frameworks de persistencia - JPA
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.
116
Frameworks de persistencia - JPA
Una transacción debe ser atómica. Esto es, todas las operaciones de una transacción deben
terminar con exito o la transacción debe abortar completamente, dejando el sistema en el
mismo estado que antes de comenzar la transacción. En el primer caso se dice que la
transacción ha realizado un commit y en el segundo que ha efectuado un rollback. Por esta
propiedad, una transacción se debe tratar como una unidad de computación.
Un transacción también debe ejecutarse de forma aislada. Esto es, no se deben exponer a
otras transacciones concurrentes datos que todavía no se han consolidado con un commit. La
concurrencia de acceso a los recursos afectados en una transacción hace muy complicado
mantener esta propiedad. Veremos que JPA proporciona un enfoque moderno basado en
bloqueos optimistas (optimistic locking) para tratar la concurrencia entre transacciones.
Atomicidad
El entity manager de JPA define la interfaz EntityTransaction para gestionar las
transacciones. Esta interfaz intenta imitar el API de Java para gestión de transacciones
distribuidas: JTA. Pero tengamos siempre en cuenta que para su implementación se utiliza el
propio sistema de transacciones de la base de datos definida en la unidad de persistencia,
utilizando los métodos de transacciones de la interfaz Connection de JDBC. Estas
transacciones son locales (no distribuidas) y no se pueden anidar ni extender.
Estas transacciones locales se usan cuando estamos trabajando con JPA en aplicaciones Java
SE. Cuando desarrollamos aplicaciones JPA en entornos web debemos utilizar el estándar de
transacciones JTA, que hace que la transacción la controle el propio servidor de aplicaciones.
Para definir una transacción local hay que obtener un EntityTransaction a partir del entity
manager. El método del entity manager que se utiliza para ello es getTransaction() . Una
vez obtenida la transacción, podemos utilizar uno de los métodos de su interfaz:
117
Frameworks de persistencia - JPA
Hemos comentado que en JPA todas las excepciones son de tipo RunTimeException . Esto
es debido a que son fatales y casi nunca se puede hacer nada para recuperarlas. En muchas
ocasiones ni siquiera se capturan en el fragmento de código en el que se originan, sino en el
único lugar de la aplicación en el que se capturan las excepciones de este tipo.
118
Frameworks de persistencia - JPA
Cuando se deshace una transacción en la base de datos todos los cambios realizados durante
la transacción se deshacen también. La base de datos vuelve al estado previo al comienzo
de la transacción. Sin embargo, el modelo de memoria de Java no es transaccional. No hay
forma de obtener una instantánea de un objeto y revertir su estado a ese momento si algo va
mal. Una de las cuestiones más complicadas de un mapeo entidad-relación es que mientras
que podemos utilizar una semántica transaccional para decidir qué cambios se realizan en la
base de datos, no podemos aplicar las mismas técnicas en el contexto de persistencia en el
que viven las instancias de entidades.
Siempre que tenemos cambios que deben sincronizarse en una base de datos, estamos
trabajado con un contexto de persistencia sincronizado con una transacción. En un momento
dado durante la vida de la transacción, normalmente justo antes de que se realice un commit,
esos cambios se traducirán en sentencias SQL y se enviarán a la base de datos.
Un ejemplo del código anterior es el siguiente programa, en el que se suma 1000.0 al sueldo
de un empleado. Se pide una entrada de texto para provocar una excepción y deshacer la
transacción:
try {
tx.begin();
Empleado emp1 = empleadoDao.find(1L);
System.out.println("Sueldo antiguo: " + emp1.getSueldo());
emp1.setSueldo(emp1.getSueldo() + 1000.0);
em.flush();
System.out.println("Sueldo nuevo: " + emp1.getSueldo());
String excepcion = leerTexto("¿Proovocamos excepción? (s/n)");
if (excepcion.equals("s")) {
throw new RuntimeException("Runtime exception");
}
tx.commit();
} catch (RuntimeException ex) {
119
Frameworks de persistencia - JPA
try {
tx.rollback();
logger.error("Transacción deshecha", ex);
} catch (RuntimeException rbEx) {
logger.error("No se ha podido deshacer la transacción",
rbEx);
}
throw ex;
} finally {
em.close();
emf.close();
}
}
Transacciones JTA
JTA (Java Transaction API) es el estándar propuesto por Java EE para gestionar transacciones
distribuidas y transacciones dentro del servidor de aplicaciones. Es posible utilizar JTA en
componentes del servidor de aplicaciones (servlets, enterprise beans y clientes enterprise).
El API JTA tiene su origen en el protocolo two-phase commit (2PC) para gestionar
transacciones distribuidas.
Gestión de la transacción
Para gestionar una transacción deberemos en primer lugar obtener del servidor de
aplicaciones el objeto javax.transacion.UserTransaction . Este es un recurso CDI
120
Frameworks de persistencia - JPA
que puede inyectarse en cualquier recurso gestionado por el servidor de aplicaciones (servlet,
ejb, backing bean):
Una vez obtenido el objeto UserTransaction podemos usar los métodos de la interfaz
para demarcar la transacción:
Para comenzar una transacción la aplicación se debe llamar a begin() . Para finalizarla, la
transacción debe llamar o bien a commit() o bien a rollback() .
El siguiente ejemplo muestra el uso de JTA en un sencillo servlet que utiliza un Dao para
acceder a las entidades. Este es sólo un ejemplo rápido para comprobar el funcionamiento
de JTA. La forma final de definir las capas de nuestra aplicación será utilizando componentes
EJB para implementar la capa de servicios. Lo veremos en el siguiente módulo.
@WebServlet(name="holamundo", urlPatterns="/holamundo")
public class HolaMundo extends HttpServlet {
@PersistenceUnit(unitName = "mensajes")
EntityManagerFactory emf;
@Resource
UserTransaction tx;
121
Frameworks de persistencia - JPA
out.println("<HTML>");
out.println("<BODY>");
out.println("<h1>Lista</h1>");
out.println("<ul>");
AutorDao autorDao = new AutorDao(em);
Autor autor = autorDao.find(1L);
autor.setNombre("Nuevo Nombre");
List<Autor> lista = autorDao.listAllAutores();
for (Autor a: lista) {
out.println("<li> " + a.getNombre() + "</li>");
}
tx.commit();
out.println("</ul>");
out.println("</BODY>");
out.println("</HTML");
} catch (Exception e) {
try {
tx.rollback();
} catch (javax.transaction.SystemException e1) {
e1.printStackTrace();
throw new ServletException(e1);
}
}
}
}
Comienzo de la transacción
Commit para confirmar los cambios
Si ha habido un error se captura la excepción y se hace un rollback
122
Frameworks de persistencia - JPA
Se han propuesto múltiples soluciones para resolver el acceso concurrente a los datos. El
estándar SQL define los llamados niveles de aislamiento (isolation levels) que tratan este
problema. Los niveles más bajos solucionan los problemas más comunes y permiten al mismo
tiempo que la aplicación responda sin generar bloqueos. El nivel más alto alto garantiza que
todas las transacciones son serilizables, pero obliga a que se definan un número excesivo de
bloqueos.
Por defecto, los proveedores de persistencia utilizan un bloqueo optimista en el que, antes
de realizar un commit que modifique los datos, se comprueba que ninguna otra transacción
ha modificado o borrado el dato desde que fue leído. Esto se consigue definiendo una
columna versión en la tabla de la base de datos, con su correspondiente atributo en
la entidad. Cuando una fila se modifica, el valor de versión se incrementa. La transacción
original comprueba si la versión ha sido modificada por otra transacción y si es así se lanza
una excepción javax.persistence.OptimisticLockException y la transacción se
deshace.
El bloqueo pesimista va más allá que el optimista. El proveedor de persistencia crea una
transacción que obtiene un bloqueo de larga duración sobre los datos hasta que la transacción
se completa. Este bloque previene a otras transacciones de modificar o borrar los datos hasta
que el bloque termina. El bloque pesimista es una estrategia mejor que el optimista cuando
los datos se acceden frecuentemente y son modificados simultáneamente por múltiples
transacciones. Sin embargo, el uso de bloqueos pesimistas en entidades a las que no se
accede frecuentemente puede causar una caída en el rendimiento de la aplicación.
Para que JPA pueda trabajar con versiones es necesario que los objetos tengan un atributo
marcado con la anotación @Version . Este atributo puede ser del tipo int , Integer ,
short , Short , long , Long y java.sql.Timestamp .
@Entity
public class Autor {
@Id
private String nombre;
@Version
123
Frameworks de persistencia - JPA
@Entity
@org.hibernate.annotations.Entity (
optimisticLock = OptimisticLockType.ALL,
dynamicUpdate = true
)
public class Autor {
@Id
private String nombre;
private String correo;
...
}
Modos de bloqueo
En la última versión de JPA es posible especificar el modo de bloqueo aplicado a una entidad
usando una llamada al método lock o al método find del entity manager. Por ejemplo:
EntityManager em = ...;
Person person = ...;
em.lock(person, LockModeType.OPTIMISTIC);
String personPK = ...;
Person person = em.find(Person.class, personPK,
LockModeType.PESSIMISTIC_WRITE);
124
Frameworks de persistencia - JPA
Por ejemplo, si se quisiera evitar el problema del asiento del vuelo bloqueando explícitamente
el registro, podríamos escribir el siguiente código:
Valor Descripción
1 TRANSACTION_READ_UNCOMMITTED
2 TRANSACTION_READ_COMMITTED
4 TRANSACTION_REPEATABLE_READ
8 TRANSACTION_SERIALIZABLE
125
Frameworks de persistencia - JPA
Lo mismo sucede con los recursos. El contenedor se encarga de comunicarse y gestionar los
recursos externos, no la aplicación. Le da nombre a esos recursos y la aplicación los utiliza.
En el caso de una conexión con la base de datos, se debe definir en el contenedor una fuente
de datos y darle un nombre. El contenedor será el responsable de gestionar las peticiones a la
fuente de datos. Por ejemplo, construirá un pool de conexiones que hará mucho más eficiente
la conexión.
También habrá diferencia con la gestión de transacciones. Se trabajará con transacciones JTA
gestionadas por el servidor de aplicaciones, no por la propia base de datos. Debido a esto
ya no crearemos la transacciones a partir del entity manager, sino que utilizaremos objetos
UserTransaction inyectados por el servidor de aplicaciones. Además en los DAO ya no
comprobaremos si estamos dentro de una transacción porque el método del entity manager
sólo funciona cuando se trata de transacciones locales no JTA.
$ cp /home/expertojava/.m2/repository/mysql/mysql-connector-java/5.1.33/
mysql-connector-java-5.1.33.jar $HOME/Escritorio
Runtime > Manage Deployments > Add y añade el JAR. Ponle como nombre mysql_connector
(no es importante) y activa la opción Enable
126
Frameworks de persistencia - JPA
• Name: MensajesDS
• JNDI Name: java:/datasources/MensajesDS
127
Frameworks de persistencia - JPA
• Username: root
• Password: expertojava
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>
Sí que necesitamos las librerías de implementación de JPA para ejecutar los tests, por lo que
podemos cambiar su scope a test . El POM completo es el siguiente:
<groupId>org.expertojava.jpa</groupId>
<artifactId>mensajes-web</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>mensajes-web</name>
<properties>
128
Frameworks de persistencia - JPA
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.dbunit</groupId>
<artifactId>dbunit</artifactId>
<version>2.5.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.33</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.1-api</artifactId>
<version>1.0.0.Final</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.3.7.Final</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.1.3.Final</version>
<scope>test</scope>
</dependency>
<dependency>
129
Frameworks de persistencia - JPA
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>2.2.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>el-impl</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.name}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.3</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
<plugin>
<groupId>org.wildfly.plugins</groupId>
<artifactId>wildfly-maven-plugin</artifactId>
<version>1.0.2.Final</version>
<configuration>
<hostname>localhost</hostname>
<port>9990</port>
</configuration>
</plugin>
</plugins>
</build>
</project>
Persistence.xml
Una vez creada la fuente de datos y configurado su nombre JNDI basta con referenciarlo en
el fichero META-INF/persistence.xml (dentro del directorio resources )
130
Frameworks de persistencia - JPA
xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://
www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="mensajes">
<jta-data-source>java:/datasources/MensajesDS</jta-data-source>
<properties>
<property name="hibernate.dialect"
value="org.hibernate.dialect.MySQLDialect"/>
@PersistenceContext(unitName="mensajes")
EntityManager em
La especificación CDI define los siguientes tipos de recursos que pueden inyectarse en
componentes:
Recordemos que en JPA se definen dos tipos de recursos, entity managers y factorías de entity
managers. Se definen las anotaciones @PersistenceContext y @PersistenceUnit
para inyectar estos recursos.
131
Frameworks de persistencia - JPA
Si el ámbito es tipo TRANSACTION , el entity manager tiene la misma vida que la transacción.
Cuando comienza una transacción se crea y se cierra cuando la transacción termina. Al final
de la transacción las entidades quedan desconectadas.
El ámbito de tipo EXTENDED se utiliza en beans de sesión con estado, que extienden su vida
a lo largo de múltiples transacciones. En este caso las entidades continúan gestionadas una
vez ha terminado la transacción. Lo veremos con más detalle en el módulo de EJB.
Además, JPA define desde la especificación 2.1 la anotación @Transactional con la que
podemos anotar métodos o nombres de clases.
Utilizando CDIs podemos además inyectar los DAOs y la clase de servicios, quedando un
código muy limpio. El servidor de aplicaciones se encarga de instanciar los DAOs e inyectarles
el entity manager correcto.
Recuerda que para trabajar con CDIs tienes que crear el fichero
beans.xml en webapp/WEB-INF/beans.xml . Puedes hacerlo
pulsando el botón derecho sobre la carpeta WEB-INF y seleccionando
New > XML Configuration file > beans.xml.
Copiamos a continuación todo el código del ejemplo mensajes-web que puedes encontrar
en el repositorio ejemplos-jpa , dentro del directorio mensajes-web .
132
Frameworks de persistencia - JPA
<li><a href="<%=request.getContextPath()%>/listaAutores">Listar
autores</a></li>
</ul>
</body>
</html>
src/main/webapp/WEB-INF/beans.xml
src/main/resources/META-INF/persistence.xml
Servlets
src/main/java/org/expertojava/jpa/mensajes/servlets/ListaAutores.java
package org.expertojava.jpa.mensajes.servlets;
import org.expertojava.jpa.mensajes.modelo.Autor;
import org.expertojava.jpa.mensajes.servicio.AutorServicio;
import javax.inject.Inject;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
133
Frameworks de persistencia - JPA
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
@WebServlet(name="listaAutores", urlPatterns="/listaAutores")
public class ListaAutores extends HttpServlet {
@Inject
AutorServicio autorServicio;
src/main/java/org/expertojava/jpa/mensajes/servlets/NuevoAutorMensaje.java
package org.expertojava.jpa.mensajes.servlets;
import org.expertojava.jpa.mensajes.modelo.Autor;
import org.expertojava.jpa.mensajes.servicio.AutorServicio;
import javax.inject.Inject;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet(name="nuevoAutorMensaje", urlPatterns="/nuevoAutorMensaje")
public class NuevoAutorMensaje extends HttpServlet {
134
Frameworks de persistencia - JPA
@Inject
AutorServicio autorServicio;
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("<!DOCTYPE HTML PUBLIC \"" +
"-//W3C//DTD HTML 4.0 " +
"Transitional//EN\">");
out.println("<HTML>");
out.println("<BODY>");
out.println("<h3>Autor y mensaje correctos</h3>");
out.println("</BODY>");
out.println("</HTML");
}
}
Servicio
src/main/java/org/expertojava/jpa/mensajes/servicio/AutorServicio.java
package org.expertojava.jpa.mensajes.servicio;
import org.expertojava.jpa.mensajes.modelo.Autor;
import org.expertojava.jpa.mensajes.modelo.Mensaje;
import org.expertojava.jpa.mensajes.persistencia.AutorDao;
import org.expertojava.jpa.mensajes.persistencia.MensajeDao;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.transaction.Transactional;
import java.util.List;
@Transactional
public class AutorServicio {
@Inject
AutorDao autorDao;
@Inject
MensajeDao mensajeDao;
135
Frameworks de persistencia - JPA
DAOs
src/main/java/org/expertojava/jpa/mensajes/persistencia/Dao.java
package org.expertojava.jpa.mensajes.persistencia;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
@PersistenceContext(unitName = "mensajes")
EntityManager em;
public T create(T t) {
em.persist(t);
em.flush();
em.refresh(t);
return t;
}
public T update(T t) {
return (T) em.merge(t);
}
src/main/java/org/expertojava/jpa/mensajes/persistencia/AutorDao.java
package org.expertojava.jpa.mensajes.persistencia;
import org.expertojava.jpa.mensajes.modelo.Autor;
136
Frameworks de persistencia - JPA
import javax.persistence.Query;
import java.util.List;
@Override
public Autor find(Long id) {
return em.find(Autor.class, id);
}
src/main/java/org/expertojava/jpa/mensajes/persistencia/AutorDao.java
package org.expertojava.jpa.mensajes.persistencia;
import org.expertojava.jpa.mensajes.modelo.Mensaje;
import javax.persistence.Query;
import java.util.List;
@Override
public Mensaje find(Long id) {
return em.find(Mensaje.class, id);
}
137
Frameworks de persistencia - JPA
6.3. Ejercicios
(0,5 puntos) Ejercicios sobre transacciones
Escribe en el módulo mensajes uno o varios programas que permitan probar el
funcionamiento de al menos dos de los distintos modos de bloqueos vistos en la sesión de
teoría. Prueba a ejecutar concurrentemente el programa lanzando varios procesos desde
IntelliJ.
Puedes parar la ejecución del programa cuando te interese, llamando por ejemplo a la siguiente
función que se queda a la espera de una entrada del usuario.
Escribe un pequeño informe con las conclusiones que hayas encontrado en el fichero
ejercicio6.txt en la raíz del repositorio.
<groupId>org.expertojava.jpa</groupId>
<artifactId>mensajes-web<artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>mensajes-web</name>
Comprueba que todo funciona correctamente, y añade como mínimo 2 nuevos métodos de
negocio en la clase AutorServicio (o puedes también crear otra clase de servicio sobre
mensajes). Añade los servlets asociados y completa la página principal de la aplicación para
poder invocarlos.
138