6. Transacciones y JPA en aplicaciones Java EE
6.1. Transacciones
La gestión de transacciones es fundamental en cualquier aplicación que trabaja con de datos. Idealmente, una transacción define una unidad de trabajo que debe cumplir los criterios ACID: Atomicidad, Consistencia, Aislamiento (la "I" viene del inglés Isolation) y Durabilidad. La consistencia la debe proporcionar el programador, realizando operaciones que lleven los datos de un estado consistente a otro. La durabilidad la proporciona el hecho de que usemos un sistema de bases de datos. Las dos características restantes, la atomicidad y el aislamiento son las más interesantes, y las que vienen determinadas por el sistema de gestión de persistencia que usemos. Veámoslas con más detalle.
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.
Para garantizar la atomicidad en JDBC se llama al método setAutoCommit(false)
de la conexión para demarcar el comienzo de la transacción y a commit()
o rollback()
al final de la transacción para confirmar los cambios o deshacerlos. Veremos que JPA que utiliza una demarcación explícita de las transacciones, marcando su comienzo con una llamada a un método begin()
y su final también con llamadas a commit()
o rollback()
. Siempre debemos tener muy presente que JPA (cuando no utilizamos un servidor de aplicaciones) se basará completamente en la implementación de JDBC para tratar con la base de datos.
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.
6.1.1. 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:
public interface EntityTransaction {
public void begin();
public void commit();
public void rollback();
public void setRollbackOnly();
public boolean getRollbackOnly();
public boolean isActive();}
Sólo hay seis métodos en la interfaz EntityTransaction
. El método begin()
comienza una nueva transacción en el recurso. Si la transacción está activa, isActive()
devolverá true
. Si se intenta comenzar una nueva transacción cuando ya hay una activa se genera una excepción IllegalStateException
. Una vez activa, la transacción puede finalizarse invocando a commit()
o deshacerse invocando a rollback()
. Ambas operaciones fallan con una IllegalStateException
si no hay ninguna transacción activa. El método setRollbackOnly()
marca una transacción para que su único final posible sea un rollback()
.
Se lanzará una PersistenceException
si ocurre un error durante el rollback y se lanzará una RollbackException
(un subtipo de PersistenceException
) si falla el commit. Tanto PersistenceException
como IllegalException
son excepciones de tipo RuntimeException
.
El método setRollbackOnly()
se utiliza para marcar la transacción actual como inválida y obligar a realizar un rollback cuando se ejecuta JPA con transacciones gestionadas por el contenedor de EJB (CMT: Containter Managed Transactions). En ese caso la aplicación no define explícitamente las transacciones, sino que es el propio componente EJB el que abre y cierra una transacción en cada método.
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.
La forma habitual de definir las transacciones en JPA es la definida en el siguiente código:
EntityManagerFactory emf = Persistence
.createEntityManagerFactory("empleados-mysql");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
// Operaciones sobre entidades
tx.commit();
} catch (RuntimeException ex) {
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();
}
}
Primero se obtiene la transacción y se llama al método begin()
. Después se realizan todas las operaciones dentro de la transacción y se realiza un commit()
. Todo ello se engloba en un bloque de try/catch
. Si alguna operación falla lanza una excepción que se captura y se ejecuta el rollback()
. Un último try/catch
captura una posible excepción en el caso en que el rollback produzca alguna excepción. En cualquier caso se cierra el entity manager.
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.
Si la transacción hace un rollback pasarán entonces dos cosas. Lo primero es que la transacción en la base de datos será deshecha. Lo siguiente que sucederá será que el contexto de persistencia se limpiará (clear), desconectando todas las entidades que se gestionaban. Tendremos entonces un montón de entidades desconectadas de la base de datos con un estado no sincronizado con 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:
public class SubeSueldo {
private static Log logger = LogFactory.getLog(SubeSueldo.class);
public static void main(String[] args) {
EntityManagerFactory emf = Persistence
.createEntityManagerFactory("empleados-mysql");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
EmpleadoDao empleadoDao = new EmpleadoDao(em);
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) {
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();
}
}
static private String leerTexto(String mensaje) {
String texto;
try {
BufferedReader in = new BufferedReader(new InputStreamReader(
System.in));
System.out.print(mensaje);
texto = in.readLine();
} catch (IOException e) {
texto = "Error";
}
return texto;
}
}
6.1.2. 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.
La especificación JTA define una arquitectura para la construcción de servidores de aplicaciones transaccionales y define un conjunto de interfaces para los componentes de esta arquitectura. Los componentes son los mismos que se define en el protocolo 2PC: la aplicación, los gestores de recursos, el gestor de transacciones y el gestor de comunicaciones. De esta forma se define una arquitectura que posibilita la definición de transacciones distribuidas utilizando el algoritmo.
Un requisito fundamental para que un recurso pueda incorporarse a las transacciones JTA es que las conexiones con el recurso sean de tipo XAConnection
. Para ello, en el caso de una base de datos JDBC, es necesario un driver JDBC que implemente las interfaces javax.sql.XADataSource
, javax.sql.XAConnection
y javax.sql.XAResource
. Es necesario también configurar
una fuente de datos XA con el servidor de aplicaciones y darle un nombre JNDI.
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 que puede inyectarse en cualquier recurso gestionado por el servidor de aplicaciones (servlet, ejb, backing bean):
@Resource UserTransaction utx;
Una vez obtenido el objeto UserTransaction
podemos usar los métodos de la interfaz para demarcar la transacción:
-
public void begin()
-
public void commit()
-
public void rollback()
-
public int getStatus()
-
public void setRollbackOnly()
-
public void setTransactionTimeout(int segundos)
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 método setRollbackOnly()
modifica la transacción actual de forma que el único resultado posible de la misma sea de rollback (falllo).
El método setTransactionTimeout(int segundos)
permite modificar el timeout asociado con la transacción actual. El parámetro determina la duración del timeout en segundos. Si se le pasa como valor cero, el timeout de la transacción es el definido por defecto.
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;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
EntityManager em = emf.createEntityManager();
try {
tx.begin(); (1)
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("<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(); (2)
out.println("</ul>");
out.println("</BODY>");
out.println("</HTML");
} catch (Exception e) {
try {
tx.rollback(); (3)
} catch (javax.transaction.SystemException e1) {
e1.printStackTrace();
throw new ServletException(e1);
}
}
}
}
1 | Comienzo de la transacción |
2 | Commit para confirmar los cambios |
3 | Si ha habido un error se captura la excepción y se hace un rollback |
6.1.3. Concurrencia y niveles de aislamiento
La gestión de la concurrencia en transacciones es un problema complejo. Por ejemplo, supongamos un sistema de reservas de vuelos. Sería fatal que se vendiera el mismo asiento de un vuelo a dos personas distintas por el hecho de que se han realizado dos accesos concurrentes al siguiente método:
public void reservaAsiento(Pasajero pasajero, int numAsiento, long numVuelo) {
EntityManager em = emf.createEntityManager();
AsientoKey key = new AsientoKey(numAsiento, numVuelo);
em.getTransaction().begin();
Asiento asiento = em.find(Asiento.class, key);
if (! asiento.getOcupado()) {
asiento.setOcupado(true);
asiento.setPasajero(pasajero);
pasajero.setAsiento(asiento);
}
em.getTransaction().commit();
em.close();
}
Si no se controlara el acceso concurrente de las transacciones a la tabla de asientos, podría suceder que dos transacciones concurrentes accedieran al estado del asiento a la misma vez (antes de haberse realizado el UPDATE de su atributo ocupado
), lo vieran libre y se lo asignaran a un pasajero y después a otro. Uno de los pasajeros se quedaría sin billete.
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.
6.1.4. Gestión concurrencia con JPA
Existe concurrencia cuando múltiples aplicaciones acceden al mismo tiempo a las mismas entidades. La gestión de la concurrencia asegura que la integridad subyacente de la base de datos se preserva a pesar del acceso concurrente.
Una configuración habitual a la hora de trabajar con transacciones es mantener bloqueos de lectura de corta duración y bloqueos de escritura de larga duración. Debido a esto la mayoría de los proveedores de persistencia retrasan la escritura en la base de datos hasta el final de la transacción, excepto cuando se llama explícitamente a flush
o se realiza una query.
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.
Control optimista de la concurrencia
El control optimista supone que todo irá bien y que van a existir pocos conflictos en la modificación de datos. El control optimista de la concurrencia lanza un error sólo al final de una unidad de trabajo, cuando se escriben los datos.
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
private int version;
private String correo;
...
}
Aunque el estándar no lo permite, en Hibernate es posible definir un comportamiento optimista en entidades que no definen una columna de versión. Para ello basta con definir la entidad de la siguiente forma:
@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);
El tipo de bloqueo puede ser uno de los siguientes:
Tipo de bloque | Descripción |
---|---|
|
Obtiene un bloqueo de lectura optimista en la entidad si tiene un atributo de versión |
|
Obtiene un bloqueo de lectura optimista en la entidad si tiene un atributo de versión e incrementa el valor de versión del atributo |
|
Obtiene un bloqueo de lectura de larga duración sobre un dato para prevenir que sea borrado o modificado. Otras transacciones pueden leer los datos mientras que se mantiene el bloque, pero no pueden modificarlo o borrarlo. |
|
Obtiene un bloque de escritura de larga duración sobre un dato para prevenir que sea leído, modificado o borrado. |
|
Obtiene un bloqueo e incrementa el atributo de versión |
|
Sinónimo de |
|
Sinónimo de |
|
No se realiza ningún bloqueo sobre los datos. |
Por ejemplo, si se quisiera evitar el problema del asiento del vuelo bloqueando explícitamente el registro, podríamos escribir el siguiente código:
public void reservaAsiento(Pasajero pasajero, int numAsiento, long numVuelo) {
EntityManager em = emf.createEntityManager();
AsientoKey key = new AsientoKey(numAsiento, numVuelo);
em.getTransaction().begin();
Asiento asiento = em.find(Asiento.class, key);
em.lock(asiento,LockType.READ);
if (! asiento.getOcupado()) {
asiento.setOcupado(true);
asiento.setPasajero(pasajero);
pasajero.setAsiento(asiento);
}
em.getTransaction().commit();
em.close();
}
Un problema del estándar JPA es que el funcionamiento correcto de estos bloqueos es dependiente del proveedor de persistencia y del nivel de aislamiento definido en la conexión de la base de datos. El nivel de aislamiento se puede configurar en JPA en la unidad de persistencia, en el fichero persistence.xml
y es dependiente del proveedor de persistencia. En el caso de Hibernate, se define con la propiedad hibernate.connection.isolation
, que puede tener los siguientes valores. El valor por defecto de MySQL es 4.
Valor | Descripción |
---|---|
1 |
TRANSACTION_READ_UNCOMMITTED |
2 |
TRANSACTION_READ_COMMITTED |
4 |
TRANSACTION_REPEATABLE_READ |
8 |
TRANSACTION_SERIALIZABLE |
6.2. JPA en aplicaciones Java EE
En las sesiones anteriores hemos descrito el funcionamiento de JPA gestionado por la aplicación. En este modo las clases de la aplicación se encargan de obtener el entity manager y de gestionar las transacciones. La forma de obtener el entity manager es llamando explícitamente al método createEntityManager()
de la unidad de persistencia. Después hay que comprobar en los DAO si las transacciones están correctamente abiertas y controlar con cuidado su rollback.
Además, la unidad de persistencia debe configurarse en la propia aplicación, describiendo sus características en el fichero persistence.xml
6.2.1. JPA gestionado por el contenedor
A diferencia de las aplicaciones standalone Java SE, en las aplicaciones web el código de la aplicación (servlets, páginas JSP, clases Java, etc.) se ejecuta dentro de un contenedor. Es el contenedor quien llama al servlet, o a las clases Java cuando lo determina el ciclo de vida de la aplicación.
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.
6.2.2. Configuración de la fuente de datos MySQL en WildFly
Cuando estamos trabajando con JPA en una aplicación web el acceso a la base de datos hay que hacerlo a través de una fuente de datos creada y gestionada por el servidor de aplicaciones.
Vamos a ver cómo configurar una fuente de datos MySQL en WildFly.
-
En primer lugar localiza el driver MySQL
mysql-connector-java-5.1.33.jar
(lo puedes encontrar en el repositorio local de Maven.m2/repository/mysql/mysql-connector-java/5.1.33/
y copialo a alguna carpeta que no esté oculta:
$ cp /home/expertojava/.m2/repository/mysql/mysql-connector-java/5.1.33/mysql-connector-java-5.1.33.jar $HOME/Escritorio
-
Conéctate a la consola de administración de WildFly y selecciona la opción Deployments > Add
Runtime > Manage Deployments > Add y añade el JAR. Ponle como nombre mysql_connector (no es importante) y activa la opción Enable

-
En Configuration > Connector > Datasources pulsa en la pestaña XA DATASOURCES para crear una nueva fuente de datos de tipo XA. Pulsa el botón Add e introduce los siguientes nombres:
-
Name:
MensajesDS
-
JNDI Name:
java:/datasources/MensajesDS
-

-
Selecciona el driver que acabamos de añadir y escribe como nombre de clase XA DataSource:
com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
:

-
Añade la propiedad
URL
y el valorjdbc:mysql://localhost:3306/jpa_mensajes
-
Y en la última pantalla define el usuario y la contraseña de la conexión
-
Username:
root
-
Password:
expertojava
-
Y prueba la conexión pulsando el botón.
-
Por último activamos la fuente de datos:

6.2.3. Dependencias Maven en el pom.xml
Todas las librerías necesarias para implementar JPA ya las tiene el servidor de aplicaciones. La única dependencia necesaria para evitar los errores de compilación es la siguiente, que incluye todas las librerías de Java EE:
<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:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.expertojava.jpa</groupId>
<artifactId>mensajes-web</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>mensajes-web</name>
<properties>
<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>
<!-- Test -->
<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>
<!-- Hibernate validator -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.1.3.Final</version>
<scope>test</scope>
</dependency>
<dependency>
<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>
6.2.4. 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
)
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
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> (1)
<properties>
<property name="hibernate.dialect"
value="org.hibernate.dialect.MySQLDialect"/>
<property name="hibernate.id.new_generator_mappings" value="false"/> (2)
<property name="hibernate.hbm2ddl.auto" value="update" />
<property name="hibernate.show_sql" value="true" />
</properties>
</persistence-unit>
</persistence>
1 | Nombre de la fuente de datos |
2 | Necesario para que la generación automática de claves primarias funcione sin crear una tabla adicional. Si no se define esta propiedad, Hibernate crea una nueva tabla llamda sequence_generator donde guarda la secuencia de claves primarias. |
6.2.5. Inyección de entity managers y recursos gestionados por el contenedor
La forma de obtener entity managers también cambia, ya que la aplicación no puede crear explícitamente contextos de persistencia sino que es el contenedor el que se encarga de gestionarlos y proporcionarlos a los objetos de la aplicación. Esta forma de trabajar con JPA se llama persistencia gestionada por el contenedor.
La forma más sencilla de obtener los recursos es utilizando la inyección de dependencias. El contenedor se encarga de crear el recurso y de configurarlo según se haya indicado en el fichero de configuración del servidor de aplicaciones. Una vez listo, lo inyecta en la variable que hayamos definido como punto de inyección utilizando la anotación propia del recurso. Por ejemplo, para usar un entity manager, basta con declararlo y anotarlo de forma apropiada:
@PersistenceContext(unitName="mensajes")
EntityManager em
La especificación CDI define los siguientes tipos de recursos que pueden inyectarse en componentes:
-
Fuentes de datos JDBC, colas JMS, tópicos y factorías de conexiones (ConenectionFactorys), sesiones JavaMail y otros recursos transaccionales que incluyen conectores JCA
-
Entity managers (EntityManagers) y factorías de entity managers (EntityManagerFactorys) JPA
-
Componentes EJB locales y remotos
-
Servicios web RESTful
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.
La anotación @PersistenceContext
puede tener los siguientes elementos adicionales:
-
unitName
: el nombre de la unidad de persistencia (definido en el fichero de configuraciónpersistence.xml
) -
type
: tipo de contexto de persistencia:PersistenceContextType.TRANSACTION
(valor por defecto) oPersistenceContextType.EXTENDED
El tipo de contexto de persistencia (TRANSACTION
o EXTENDED
) define el ámbito (scope) del entity manager, la extensión de su ciclo de vida. En JPA gestionado por el contenedor el programador no tiene que llamar explícitamente al método close()
para cerrar el entity manager. Se encarga de ello el contenedor dependiendo del alcance definido.
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
.
Página JSP y configuración
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Start Page</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<h1>Hello World!</h1>
<h2>Nuevo mensaje</h2>
<form action="<%=request.getContextPath()%>/nuevoAutorMensaje">
<p>Introduce tu login: <input type="text" name="login"><br/>
Introduce tu mensaje: <input type="text" name="mensaje"></p>
<input type="submit" value="Enviar">
</form>
<h2>Acciones</h2>
<ul>
<li><a href="<%=request.getContextPath()%>/listaAutores">Listar autores</a></li>
</ul>
</body>
</html>
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
bean-discovery-mode="all">
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
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"/>
<property name="hibernate.id.new_generator_mappings" value="false"/>
<property name="hibernate.hbm2ddl.auto" value="update" />
<property name="hibernate.show_sql" value="true" />
</properties>
</persistence-unit>
</persistence>
Servlets
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;
import java.util.List;
@WebServlet(name="listaAutores", urlPatterns="/listaAutores")
public class ListaAutores extends HttpServlet {
@Inject
AutorServicio autorServicio;
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws
ServletException,
IOException {
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
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("<h1>Lista de autores</h1>");
out.println("<ul>");
List<Autor> lista = autorServicio.listAllAutores();
for (Autor a : lista) {
out.println("<li> " + a.getNombre() + "</li>");
}
out.println("</ul>");
out.println("</BODY>");
out.println("</HTML");
}
}
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 {
@Inject
AutorServicio autorServicio;
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws
ServletException,
IOException {
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String login = request.getParameter("login");
String mensaje = request.getParameter("mensaje");
System.out.println(login);
System.out.println(mensaje);
autorServicio.createAutorMensaje(login, login, mensaje);
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
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;
public Autor createAutorMensaje(String nombre,
String correo,
String texto) {
Autor autor = new Autor(nombre, correo);
autor = autorDao.create(autor);
Mensaje mensaje = new Mensaje(texto, autor);
mensaje = mensajeDao.create(mensaje);
return autor;
}
public List<Autor> listAllAutores() {
List<Autor> autores = autorDao.listAllAutores();
return autores;
}
}
DAOs
package org.expertojava.jpa.mensajes.persistencia;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
abstract class Dao<T, K> {
@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);
}
public void delete(T t) {
t = em.merge(t);
em.remove(t);
}
public abstract T find(K id);
}
package org.expertojava.jpa.mensajes.persistencia;
import org.expertojava.jpa.mensajes.modelo.Autor;
import javax.persistence.Query;
import java.util.List;
public class AutorDao extends Dao<Autor, Long> {
String FIND_ALL_AUTORES = "SELECT a FROM Autor a ";
@Override
public Autor find(Long id) {
return em.find(Autor.class, id);
}
public List<Autor> listAllAutores() {
Query query = em.createQuery(FIND_ALL_AUTORES);
return (List<Autor>) query.getResultList();
}
}
package org.expertojava.jpa.mensajes.persistencia;
import org.expertojava.jpa.mensajes.modelo.Mensaje;
import javax.persistence.Query;
import java.util.List;
public class MensajeDao extends Dao<Mensaje, Long> {
String FIND_ALL_MENSAJES = "SELECT m FROM Mensaje m";
@Override
public Mensaje find(Long id) {
return em.find(Mensaje.class, id);
}
public List<Mensaje> listAllMensajes() {
Query query = em.createQuery(FIND_ALL_MENSAJES);
return (List<Mensaje>) query.getResultList();
}
}
6.3. Ejercicios
6.3.1. (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.
private static void pulsaIntro(String msg) {
try {
BufferedReader in = new BufferedReader(
new InputStreamReader(System.in));
System.out.println(msg);
in.readLine();
} catch (IOException e) {
}
}
Escribe un pequeño informe con las conclusiones que hayas encontrado en el fichero ejercicio6.txt
en la raíz del repositorio.
6.3.2. (1 punto) Aplicación básica JPA en web
Crea en el proyecto de ejercicios un nuevo módulo Maven llamado mensajes-web
con las siguientes coordenadas:
<groupId>org.expertojava.jpa</groupId>
<artifactId>mensajes-web<artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>mensajes-web</name>
Y crea en él la aplicación ejemplo mensajes-web
que puedes encontrar en el repositorio ejemplos-jpa
de Bitbucket.
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.