Introducción a JPA 2.0 (III)

En esta tercer artículo del tutorial de introducción a JPA, vamos a ver como realizar persistencia, lo que nos permitirá entender mejor que son los métodos callback y las clases listener. También introduciremos el concepto de transacción.

Hasta ahora, todo el material que hemos visto en tanto en el primer como en el segundo artículo, se ha limitado a teoría sobre la forma de declarar y configurar nuestras entidades para su mapeo. En esta parte sin embargo, además de los conceptos teóricos que vamos a analizar, vamos a tener la oportunidad de realizar dicho mapeo con una base de datos real, y completar así el concepto de mapeo relacional de objetos (ORM). Por esto, es muy aconsejable que dispongas de un entorno de desarrollo compatible con JPA. Si no es tu caso, puedes preparar uno de manera sencilla accediendo al anexo que acompaña a este tutorial. Los archivos de configuración que veremos en este capítulo están adaptados a la configuración de este entorno en concreto, por lo que si vas a utilizar uno distinto (otra base de datos, otro proveedor de persistencia, etc), deberás modificar dichos archivos con la información correspondiente a tu entorno. Comencemos.

3.1 Persistencia

El concepto de persistencia implica el hecho de almacenar nuestras entidades (objetos Java de tipo POJO) en un sistema de almacenamiento, normalmente una base de datos relacional (tablas, filas, y columnas). Más alla del proceso de almacenar entidades en una base de datos, todo sistema de persistencia debe permitir recuperar, actualizar y eliminar dichas entidades. Estas cuatro operaciones se conocen como operaciones CRUD (Create, Read, Update, Delete; Crear, Leer, Actualizar, Borrar). JPA maneja todas las operaciones CRUD a través de la interface EntityManager. Comencemos definiendo una entidad que manejaremos a través de los futuros ejemplos:

package es.davidmarco.tutorial.jpa; 

import javax.persistence.Entity; 
import javax.persistence.GeneratedValue; 
import javax.persistence.Id; 

@Entity 
public class Pelicula { 
    @Id 
    @GeneratedValue 
    private Long id; 
    private String titulo; 
    private int duracion; 
     
    // Getters y Setters 
}
	

Para que JPA pueda persistir esta entidad, necesita un archivo de configuración XML llamado persistence.xml, el cual debe estar ubicado en el directorio META-INF de nuestra aplicación. En nuestro caso, este archivo mostraría este aspecto:

 
 
     
        org.eclipse.persistence.jpa.PersistenceProvider 
        es.davidmarco.tutorial.jpa.Pelicula 
         
             
             
             
             
             
             
         
     
 
	

El archivo persistence.xml contiene información necesaria para JPA, como el nombre de la unidad de persistencia, el tipo de transacciones que vamos a utilizar (concepto que veremos a continuación), las clases que deseamos que sean manejadas por el proveedor de persistencia, y los parámetros para conectar con nuestra base de datos.

3.2 Transacciones

En el archivo de configuración anterior hemos incluido la siguiente línea:

 
	

	

En ella se indica el nombre de la unidad de persistencia (el cual pasaremos más adelante a EntityManager para informarle de los parámetros de mapeo de un conjunto de entidades a gestionar, en nuestro caso solo una), y el tipo de transacción (RESOURCE_LOCAL) que utilizaremos al manejar este conjunto de entidades. El concepto de transacción representa un contexto de ejecución dentro del cual podemos realizar varias operaciones como si fuera una sola, de manera que o todas ellas son realizadas satisfactoriamente, o el proceso se aborta en su totalidad (cualquier operación ya realizada se revertirá si ocurre un error a lo largo de la transacción). Esto es muy importante para garantizar la integridad de los datos que queremos persistir: imaginemos que estamos persistiendo una entidad que contiene una lista de entidades en su interior (una asociación uno-a-muchos). Si cualquiera de las entidades de esta lista no pudiera ser persistida por algún motivo, no deseamos que el resto de entidades de la lista, así como la entidad que las contiene, sean persistidas tampoco. Si permitiéramos que esto ocurriera, nuestros datos en la base de datos no reflejarían su estado real como objetos, y ni dichos datos ni nuestra aplicación serían fiables. JPA nos permite configurar en cada unidad de persistencia el tipo de transacción que queremos usar, que puede ser:

  • Manejada por la aplicacion (RESOURCE_LOCAL)
  • Manejada por el contenedor (JTA)

Nosotros hemos decidido manejar las transacciones desde la aplicación, y por tanto lo haremos desde nuestro código a través de la interface EntityTransaction. Aunque en una aplicación real utilizaríamos transacciones manejadas por el contenedor, en nuestros ejemplos utilizaremos transacciones manejadas por la aplicación. De esta manera mantendremos los ejemplos simples (no necesitamos instalar y configurar un contenedor), además de alcanzar una mejor comprensión sobre el funcionamiento de las transacciones.

3.3 Estado de una entidad

Para JPA, una entidad puede estar en uno de los dos estados siguientes:

  • Managed (gestionada)
  • Detached (separada)

Cuando persistimos una entidad (como veremos en el punto 3.4), automáticamente se convierte en una entidad gestionada. Todos los cambios que efectuemos sobre ella dentro del contexto de una transacción se verán reflejados también en la base de datos, de forma transparente para la aplicación.

El segundo estado en el que puede estar una entidad es separado, en el cual los cambios realizados en la entidad no están sincronizados con la base de datos. Una entidad se encuentra en estado separado antes de ser persistida por primera vez, y cuando tras haber estado gestionada es separada de su contexto de persistencia (como veremos en la sección 3.6).

3.4 Persistir una entidad

Ya estamos listos para persistir nuestra primera entidad:

package es.davidmarco.tutorial.jpa; 

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

public class Main { 

    public static void main(String[] args) { 
        EntityManagerFactory emf = 
            Persistence.createEntityManagerFactory("introduccionJPA"); 
        EntityManager em = emf.createEntityManager(); 
        EntityTransaction tx = em.getTransaction(); 
         
        Pelicula pelicula = new Pelicula(); 
        pelicula.setTitulo("Pelicula uno"); 
        pelicula.setDuracion(142); 
         
        tx.begin(); 
        try { 
            em.persist(pelicula); 
            tx.commit(); 
        } catch(Exception e) { 
            tx.rollback() 
        } 
         
        em.close(); 
        emf.close(); 
    } 
}
	

En el ejemplo anterior hemos obtenido una instancia de EntityManager a través de la la clase factoría EntityManagerFactory a la que le hemos pasado como argumento el nombre de la unidad de persistencia que vamos a utilizar (y que hemos configurado previamente en el archivo persistence.xml). A continuación hemos obtenido una transacción desde EntityManager, y hemos creado una instancia de la entidad Pelicula. En las líneas de código siguientes, hemos iniciado la transacción, persistido la entidad, y confirmado la transacción mediante el método commit() de EntityManager. En caso de que el proveedor de persistencia lance una excepción, la capturamos en el bloque catch, donde cancelamos la transacción mediante el metodo rollback() de EntityManager (en los próximos ejemplos, y para simplificar el código, vamos a eliminar el bloque try/catch, incluyendo el código que cancela la transacción en caso de error). Cuando llamamos a em.persist(pelicula) la entidad es persistida en nuestra base de datos, pelicula y queda gestionada por el proveedor de persistencia mientras dure la transacción. Por ello, cualquier cambio en su estado será sincronizado automáticamente y de forma transparente para la aplicación:

tx.begin();
em.persist(pelicula);
pelicula.setTitulo("otro titulo");
tx.commit();
	

En el ejemplo anterior hemos modificado el estado de la entidad pelicula después de haber sido persistida, pero dentro de la misma transacción que realizó la persistencia, y por tanto la nueva información se sincronizará con la base de datos. Es importante tener presente que esta sincronización puede no ocurrir hasta que instamos a la transacción para completarse (tx.commit()) (el momento en que la sincronización se lleve a cabo depende enteramente de la implementación concreta del proveedor de persistencia que usemos). Sin embargo, podemos forzar a que cualquier cambio pendiente se guarde en la base de datos antes de terminar la transacción invocando el método flush() de EntityManager:

tx.begin();
em.persist(pelicula);
pelicula.setTitulo("otro titulo");
em.flush();
// otras operaciones
tx.commit();
	

En el ejemplo anterior los cambios efectuados sobre la entidad película (setTitulo()) serán persistidos (si no lo estuvieran ya) antes de finalizar la transacción. Por supuesto, si ésta terminase fallando (por ejemplo durante el bloque // otras operaciones) todos los cambios efectuados durante la transacción, incluida la persistencia inicial de la entidad, serían revertidos (recuerda el concepto de transacción: todo o nada).

Así mismo, podemos realizar la sincronización en sentido inverso, actualizando una entidad con los datos que actualmente se encuentran en la base de datos. Esto es útil cuando persistimos una entidad, y tras haber cambiado su estado deseamos recuperar el estado persistido (desechando así los últimos cambios realizados sobre la entidad, que como sabemos podrían haber sido sincronizados con la base de datos). Esto podemos llevarlo a cabo mediante el método refresh() de nuestro gran amigo EntityManager:

tx.begin();
em.persist(pelicula);
pelicula.setTitulo("otro titulo");
em.refresh(pelicula);
tx.commit();
	

En el ejemplo anterior la llamada a em.refresh() deshace los cambios realizados en pelicula en la línea anterior. Otro metodo muy útil en EntityManager es contains():

boolean gestionada = em.contains(pelicula);
// lógica de la aplicacion
	

El método contains() devuelve true si el objeto Java que le pasamos como parámetro se encuentra en estado gestionado por el proveedor de persistencia, y false en caso contrario.

3.5 Leer una entidad

Ahora que ya sabemos como persistir una entidad en nuestra base de datos, demos un paso más y veamos como realizar el proceso inverso: leer una entidad previamente persistida en la base de datos para construir un objeto Java. Podemos llevar a cabo esto de dos maneras distintas:

  • Obteniendo un objeto real
  • Obteniendo una referencia a los datos persistidos

Mediante el primer mecanismo los datos son leídos (por ejemplo, con el método find()) desde la base de datos y almacenados en una instancia de la entidad:

Pelicula pelicula = em.find(Pelicula.class, id);
	

Por contra, el segundo mecanismo nos permite obtener una referencia a los datos almacenados en la base de datos, de manera que el estado de la entidad será leído de forma demorada, más concretamente en el primer acceso a cada propiedad y no en el momento de la creación de la entidad:

Pelicula pelicula = em.getReference(Pelicula.class, id);
	

En ambos casos, el valor de id debe ser el valor de id con el que fue persistida la entidad. Esta forma de lectura es muy limitada, de manera que luego veremos un sistema mucho mas dinámico para buscar entidades persistidas: JPQL.

3.6 Actualizar una entidad

Antes de explicar como actualizar una entidad, vamos a explicar como separar una entidad del contexto de persistencia, explicación que dejamos pendiente al final del punto 3.3. Podemos separar una o todas las entidades gestionadas actualmente por el contexto de persistencia mediante los métodos detach() y clear(), respectivamente. Una vez que una entidad se encuentra separada, esta deja de estar gestionada, y por tanto los cambios en su estado dejan de ser sincronizados con la base de datos. Si intentáramos llamar de nuevo al método persist() sobre una entidad separada, se lanzará una excepción (de hecho, la invocación de cualquier método que trabaje sobre entidades gestionadas lanzará una excepción cuando sea usado sobre entidades separadas).

La pregunta que se nos plantearía en este momento es: ¿cómo podemos volver a tener nuestra entidad gestionada y sincronizada? Mediante el método merge() de EntityManager:

tx.begin();
em.persist(pelicula);
tx.commit();
em.detach(pelicula);
// otras operaciones
em.merge(pelicula);
	

En el ejemplo anterior la entidad pelicula es separada del contexto de persistencia mediante la llamada al método detach(). La última línea, sin embargo, indica al proveedor de persistencia que vuelva a gestionar la entidad, y de manera adicional sincronice los cambios que se hayan podido realizar mientras la entidad estuvo separada (este pequeño milagro ocurre gracias al ID de la entidad).

3.7 Eliminar una entidad

La última operación CRUD que nos queda por ver es la de eliminar una entidad. Cuando realizamos esta operación, la entidad es eliminada de la base de datos y separada del contexto de persistencia. Sin embargo, la entidad seguirá existiendo como objeto Java en nuestro código hasta que el ámbito de la variable termine, hasta que hipotéticamente sea puesta a null y el colector de basura elimine la instancia de memoria, etc:

em.remove(pelicula);
pelicula.setTitulo("ya no soy una entidad, solo un objeto Java normal");
	

Cuando existe una asociación uno-a-uno y uno-a-muchos entre dos entidades, y eliminamos la entidad dueña de la relación, la/s entidad/es del otro lado de la relación no son eliminada/s de la base de datos (este es el comportamiento por defecto), pudiendo dejar así entidades huerfanas (aquellas que siguen estando persistidas pero sin ninguna entidad que las referencie). Sin embargo podemos configurar nuestras asociaciones para que eliminen de manera automática todas las entidades subordinadas de la relación:

@Entity 
public class Pelicula { 
    ... 
    @OneToOne(orphanRemoval = true) 
    private Descuento descuento; 

    // Getters y setters 
} 
	

En el ejemplo anterior informamos al proveedor de persistencia que cuando elimine una entidad de tipo Pelicula debe eliminar también de la base de datos la entidad Descuentoasociada.

3.8 Operaciones en cascada

La operación de eliminación de entidades huérfanas que acabamos de ver es un tipo de operación llamada en cascada. Este tipo de operaciones permiten establecer la forma en que deben propagarse ciertos eventos (como persistir, eliminar, actualizar, etc) entre entidades que forman parte de una asociación. La forma general de establecer como se realizarán estas operaciones en cascada se define mediante el atributo cascade de las anotaciones de asociación:

@OneToOne(cascade = CascadeType.REMOVE)
private Descuento descuento;
	

El ejemplo anterior es similar a su predecesor. En él, el tipo de operación en cascada que deseamos propagar (CascadeType.REMOVE) se indica mediante constantes de la clase CascadeType. Estas constantes pueden tener los siguientes valores:

  • PERSIST
  • REMOVE
  • MERGE
  • REFRESH
  • DETACH
  • ALL

La última constante, CascadeType.ALL, hace referencia a todas las posibles operaciones que pueden ser propagadas, y por tanto engloba en si misma el comportamiento del resto de constantes. También podemos configurar varias operaciones en cascada de la lista superior usando un array de constantes CascadeType:

@OneToOne(cascade = { 
        CascadeType.MERGE, 
        CascadeType.REMOVE, 
        }) 
private Descuento descuento; 
	

Recuerda que cualquier entidad que utilices en tu código debe estar debidamente registrada en el archivo persistence.xml, de manera que JPA pueda gestionarla:

es.davidmarco.tutorial.jpa.Descuento 
	

3.9 Métodos callback

Como hemos visto a lo largo de este tercer artículo del tutorial de introducción a JPA, una entidad puede estar en dos estados diferentes: gestionada y separada. Los métodos callback son métodos que se ejecutan cuando se producen ciertos eventos relacionados con el ciclo de vida de una entidad. Estos eventos se clasifican en cuatro categorías:

  • Eventos de persistencia (métodos callback asociados anotados con @PrePersists y @PostPersist)
  • Eventos de actualización (métodos callback asociados anotados con @PreUpdate y @PostUpdate)
  • Eventos de borrado (métodos callback asociados anotados con @PreRemove y @PostRemove)
  • Eventos de carga (método callback asociado anotado con @PostLoad)

Las anotaciones superiores están destinadas a marcar métodos dentro de la entidad, los cuales serán ejecutados cuando el evento correspondiente suceda:

@Entity 
public class Pelicula { 
    ... 
    @PrePersist 
    @PreUpdate 
    private void validar() { 
        // validar parametros antes de persistir/actualizar la entidad 
    } 
} 	
	

En el ejemplo anterior hemos añadido a nuestra entidad un método que reaccionará antes dos eventos: el momento antes de realizarse la persistencia (@PrePersist), y el momento previo a la actualización (@PreUpdate). Dentro de él podemos realizar ciertas acciones necesarias antes de que se produzcan los dos citados eventos, como comprobar que el estado de la entidad (la información que contiene) es correcta. Al escribir métodos callback, debemos seguir algunas reglas para que nuestro código sea válido:

  • Un método callback no puede ser declarado static ni final
  • Cada anotación de ciclo de vida puede aparecer una y solo una vez en cada entidad
  • Un método callback no puede lanzar excepciones de tipo checked
  • Un método callback no puede invocar métodos de las clases EntityManager y/o Query

Ten presente que cuando existe herencia entre entidades, los métodos @Pre/PostXxx de las superclases son invocados antes que los de las subclases. Así mismo, los eventos de ciclo de vida se propagan en cascada cuando existen asociaciones.

3.10 Clases listener

Cuando necesitamos aplicar un mismo método callback a varias entidades, es preferible extraer de la entidad este método y ponerlo en una clase externa, la cual podrá ser aplicada a varias entidades (reduciendo así la duplicación de código). Estas clases son llamadas clases listener:

public class ValidadorListener { 
    @PrePersist 
    @PreUpdate 
    public void validar(Pelicula pelicula) { 
        // validar parametros antes de persistir/actualizar la entidad 
    } 
} 
	

En el ejemplo anterior hemos definido una clase listener, moviendo las anotaciones que representan a métodos callback en ella. En este momento, podemos aplicar la clase listener a nuestras entidades:

@Entity
@EntityListeners(ValidadorListener.class)
public class Pelicula { ... }
	

En tiempo de ejecución, cuando se produzca un evento que se encuentre definido en la clase listener, el proveedor de JPA pasará a su método validar() una referencia de la entidad Pelicula. Y si deseamos que una clase listener pueda ser aplicada a más de un tipo de entidad podemos aprovechar la naturaleza polimórfica de Java, usando una superclase (o mejor aún, una interface) común a dichas entidades. En su defecto siempre podemos usar un argumento de tipo Object en el método callback:

public class ValidadorListener { 
    @PrePersist 
    @PreUpdate 
    public void validar(Object obj) {     
        // hacer casting explícito y validar parametros antes de persistir/actualizar la entidad 
    } 
} 
	

De manera adicional, la anotación @EntityListeners puede hacer referencia a varias clases listener usando un array de objetos .class. Cuando una entidad es asociada con varias clases listener, los métodos callback de dichas clases que hagan referencia a un mismo evento serán invocados en el orden en que fueron declarados dentro del array. Así mismo, los métodos callback de las clases listener serán invocados de manera previa a los métodos callback que hagan referencia al mismo tipo de evento y que estén declarados dentro de la entidad:

@Entity 
@EntityListeners({ 
        ValidadorListener.class, 
        OtroListener.class}) 
public class Pelicula { 
    // ... 
     
    @PrePersist 
    @PreUpdate 
    private void validar() { 
        // validar parametros antes de persistir y actualizar la entidad 
    } 
} 
	

En el ejemplo anterior, los métodos @PrePersist y @PreUpdate se ejecutarán en este orden: ValidatorListener, OtroListener, Pelicula.

Resumen

A lo largo de este artículo hemos visto como realizar persistencia con JPA, así como algunos detalles interesantes sobre transaccionalidad y métodos callback.

En el próximo y último artículo veremos qué es y cómo usar JPQL, un potente lenguage orientado a objetos con el que podemos manejar entidades y grupos de entidades en base a multitud de criterios.