Introducción a JPA 2.0 (I)

Con este artículo da comienzo a una serie de cuatro artículos que introducen JPA en su versión 2.0. En ellos vamos a ver, de forma introductoria, como realizar ORM de forma sencilla mediante JPA. Los artículos seguirán este orden:

  1. ORM Básico
  2. Asociaciones y Herencia
  3. Persistencia, Transacciones, Callbacks y Lísteners
  4. JPQL

En la página de archivo están disponibles todos los artículos que forman este tutorial, así como el anexo donde se explica como poner en marcha un entorno de desarrollo JPA 2.0.

1.1 ¿Qué es JPA?

En Java solucionamos problemas de negocio a través de objetos, los cuales tienen estado y comportamiento. Sin embargo, las bases de datos relacionales almacenan la información mediante tablas, filas, y columnas, de manera que para almacenar un objeto hay que realizar una correlación entre el sistema orientado a objetos de Java y el sistema relacional de nuestra base de datos. JPA (Java Persistence API, API de Persistencia en Java) es una abstracción sobre JDBC que nos permite realizar dicha correlación de forma sencilla, realizando por nosotros toda la conversión entre nuestros objetos y las tablas de una base de datos. Esta conversión se llama ORM (Object Relational Mapping - Mapeo Relacional de Objetos), y puede configurarse a través de metadatos (mediante xml o anotaciones). Por supuesto, JPA también nos permite seguir el sentido inverso, creando objetos a partir de las tablas de una base de datos, y también de forma transparente. A estos objetos los llamaremos desde ahora entidades (entities).

JPA establece una interface común que es implementada por un proveedor de persistencia de nuestra elección (como Hibernate, Eclipse Link, etc), de manera que podemos elegir en cualquier momento el proveedor que más se adecue a nuestras necesidades. Así, es el proveedor quién realiza el trabajo, pero siempre funcionando bajo la API de JPA.

1.2 Una entidad simple

Este es el ejemplo más simple de entidad:

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 
} 
	

Las entidades suelen ser POJOs. La clase Pelicula se ha anotado con @Entity, lo cual informa al proveedor de persistencia que cada instancia de esta clase es una entidad. Para ser válida, toda entidad debe:

  • Proporcionar un constructor por defecto (ya sea de forma implícita o explícita)
  • Ser una clase de primer nivel (no interna)
  • No ser final
  • Implementar la interface java.io.Serializabe si va a ser accedida remotamente

La configuración de mapeo puede ser especificada mediante un archivo de configuración XML, o mediante anotaciones. A esta configuración del mapeo la llamamos metadatos. Para mantener las cosas simples, este tutorial utilizará la segunda opción (anotaciones). Cual de ellas elegir depende de preferencias tanto personales como del proyecto. El descriptor XML tiene como ventaja que no es necesario recompilar nuestro código para cambiar la configuración de mapeo; por contra, exige mantener un archivo externo. La configuración mediante anotaciones permite tener en un mismo lugar el código Java y sus instrucciones de comportamiento; por contra, exige una recompilación cada vez que deseamos cambiar el comportamiento del mapeo, además de resultar en código menos portable (otra aplicación no-JPA que use nuestras entidades deberá incluir todas las librerías JPA en su classpath para poder compilar correctamente). Cuando ambas opciones son utilizadas al mismo tiempo, la configuración XML prevalece sobre las anotaciones, de manera que si el descriptor XML establece unos atributos para una propiedad que también ha sido configurada mediante anotaciones, serán los primeros los que el proveedor de persistencia tenga en cuenta.

1.3 Identidad

Todas las entidades tienen que poseer una identidad que las diferencie del resto, por lo que deben contener una propiedad marcada con la anotación @Id (es aconsejable que dicha propiedad sea de un tipo que admita valores null, como Integer en lugar de int). Existen formas más complejas de identidad (@EmbeddedId, @IdClass) que no vamos a explicar aquí, para mantener los ejemplos sencillos.

De manera adicional, y en lo que respecta a este tutorial, la identidad de una entidad va a ser gestionada por el proveedor de persistencia, así que será dicho proveedor quien le asigne un valor la primera vez que almacene la entidad en la base de datos. Para tal efecto, le añadimos a la propiedad de identidad la anotación @GeneratedValue.

1.4 Configuración por defecto

JPA aplica a las entidades que maneja una configuración por defecto, de manera que una entidad es funcional con una mínima cantidad de información (las anotaciones @Entity y @Id en nuestro caso). Con esta configuración por defecto, todas las entidades del tipo Pelicula serán mapeadas a una tabla de la base de datos llamada PELICULA, y cada una de sus propiedades será mapeada a una columna con el mismo nombre (la propiedad id será mapeada en la columna ID, la propiedad titulo será mapeada en la columna TITULO, etc). Sin embargo, no siempre es posible ceñirse a los valores de la configuración por defecto: imaginemos que tenemos que trabajar con una base de datos heredada, con nombres de tablas y filas ya definidos. En ese caso, podemos configurar el mapeo de manera que se ajuste a dichas tablas y filas:

@Entity 
@Table(name = "TABLA_PELICULAS") 
public class Pelicula { 
    @Id 
    @GeneratedValue 
    @Column(name = "ID_PELICULA") 
    private Long id;     
    
    // ... 
}
	

La anotación @Table nos permite configurar el nombre de la tabla donde queremos almacenar la entidad mediante el atributo name. Así mismo, mediante la anotación @Column podemos configurar el nombre de la columna donde se almacenará una propiedad, si dicha propiedad puede contener un valor null, etc. Si lo deseas puedes consultar la especificación completa de JPA 2.0.

1.5 Lectura temprana y lectura demorada

Cuando leemos una entidad desde la base de datos, ciertas propiedades pueden no ser necesarias en el momento de la creación del objeto. JPA nos permite leer una propiedad desde la base de datos la primera vez que un cliente intenta leer su valor (lectura demorada), en lugar de leerla cuando la entidad que la contiene es creada (lectura temprana). De esta manera, si la propiedad nunca es accedida, nos evitamos el coste de crearla. Esto puede ser útil si la propiedad contiene un objeto de gran tamaño:

@Basic(fetch = FetchType.LAZY)
private Imagen portada;
	

La propiedad portada es un objeto que representa una imagen de la portada de una película (un objeto de gran tamaño). Puesto que el coste de crear este objeto al leer la entidad Pelicula es muy alto, lo hemos marcado como una propiedad de lectura demorada mediante la anotación @Basic(fetch = FetchType.LAZY). El comportamiento por defecto de la mayoría de tipos Java es el contrario (lectura temprana). Este comportamiento, a pesar de ser implícito, puede ser declarado explícitamente de la siguiente manera:

@Basic(fetch = FetchType.EAGER)
private Imagen portada;
	

Solo los objetos de gran tamaño y ciertos tipos de asociación (concepto que veremos en el segundo artículo de este tutorial) deben ser leidos de forma demorada. Si, por ejemplo, marcamos todas las propiedades de tipo int, String o Date de una entidad con lectura demorada, cada vez que accedamos por primera vez a cada una de ellas el proveedor de persistencia tendrá que hacer una llamada a la base de datos para leerla. Esto, evidentemente, va a provocar que se efectúen multitud de llamadas a la base de datos cuando con solo una (al crear la entidad en memoria) podrían haberse leído todas con apenas coste. Por tanto, es importante tener presente que la manera de configurar el tipo de lectura de una propiedad puede afectar enormemente al rendimiento de nuestra aplicación.

Por otro lado, la anotación @Basic solo puede ser aplicada a tipos primitivos, sus correspondientes clases wrapper, BigDecimal, BigInteger, Date, arrays, algunos tipos del paquete java.sql, enums, y cualquier tipo que implemente la interface Serializable. Además del atributo fetch, la anotación @Basic admite otro atributo, optional, el cual permite definir si la propiedad sobre la que se está aplicando la anotación puede contener el valor null (esto es debido a que en bases de datos relacionales, algunas columnas pueden definir una constricción de tipo non null, la cual impide que se inserte un valor null; por tanto, con @Basic(optional=false) nos ajustaríamos a la citada constricción).

1.6 Tipos enumerados

JPA puede mapear los tipos enumerados (enum) mediante la anotación Enumerated:

@Enumerated
private Genero genero;
	

La configuración por defecto de JPA mapeará cada valor ordinal de un tipo enumerado a una columna de tipo numérico en la base de datos. Por ejemplo, siguiendo el ejemplo anterior, podemos crear un tipo enum que describa el género de una película:

public enum Genero { 
    TERROR, 
    DRAMA, 
    COMEDIA, 
    ACCION, 
} 
	

Si la propiedad genero del primer ejemplo tiene el valor Genero.COMEDIA, en la columna correspondiente de la base de datos de insertará el valor 2 (que es el valor ordinal de Genero.COMEDIA). Sin embargo, si en el futuro introducimos un nuevo tipo de genero en una posición intermedia, o reordenamos las posiciones de los géneros, nuestra base de datos contendrá valores erróneos que no se corresponderán con los nuevos valores ordinales del tipo enumerado. Para evitar este problema potencial, podemos forzar a la base de datos a utilizar una columna de texto en lugar de una columna numérica: de esta manera, el valor almacenado será el nombre del valor enum, y no su valor ordinal:

@Enumerated(EnumType.STRING)
private Genero genero;
	

1.7 Transient

Ciertas propiedades de una entidad pueden no representar su estado. Por ejemplo, imaginemos que tenemos una entidad que representa a una persona:

@Entity 
public class Persona { 
    @Id 
    @GeneratedValue 
    private Long id; 
    private String nombre; 
    private String apellidos 
    private Date fechaNacimiento; 
    private int edad; 
     
    // getters y setters 
} 
	

Podemos considerar que la propiedad edad no representa el estado de Persona, ya que si no es actualizada cada cumpleaños, terminará conteniendo un valor erróneo. Ya que su valor puede ser calculado gracias a la propiedad fechaNacimiento, no vamos a almacenarlo en la base de datos, si no a calcular su valor en tiempo de ejecución cada vez que lo necesitemos. Para indicar al proveedor de persistencia que ignore una propiedad cuando realice el mapeo, usamos la anotación @Transient:

@Transient
private int edad;
	

Ahora, para obtener el valor de edad utilizamos su metodo getter:

public int getEdad() { 
    // calcular la edad y devolverla 
}
	

1.8 Colecciones básicas

Una entidad puede tener propiedades de tipo java.util.Collection y/o java.util.Map que contengan tipos básicos (todos los tipos wrapper, String, BigDecimal, BigInteger, tipos enumerados y tipos insertables; estos últimos los veremos en la próxima sección). Los elementos de estas colecciones serán almacenados en una tabla diferente a la que contiene la entidad donde están declarados. El tipo de colección tiene que ser concreto (como ArrayList o HashMap) y nunca una interface.

private ArrayList<String> comentarios;
	

El código anterior es mapeado por defecto con unos valores predeterminados por JPA, como vimos en la sección 1.4. Y por supuesto, podemos cambiar dicha configuración por defecto mediante diversas anotaciones, entre las que se encuentran:

@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "TABLA_COMENTARIOS")
private ArrayList<String> comentarios;
	

@ElementCollection permite definir el tipo de lectura (temprana o demorada), así como la clase de objetos que contiene la colección (obligatorio en caso de que la colección no sea de tipo genérico). Por otro lado, @CollectionTable nos permite definir el nombre de la tabla donde queremos almacenar los elementos de la colección. Si nuestra colección es de tipo Map, podemos añadir la anotación @MapKeyColumn(name = "NOMBRE_COLUMNA"), la cual nos permite definir el nombre de la columna donde se almacenarán las claves del Map.

1.9 Tipos insertables

Los tipos insertables (embeddable types) son objetos que no tienen identidad, por lo que para ser persistidos deben ser primero insertados dentro de una entidad (u otro insertable que será a su vez insertado en una entidad, etc). Cada propiedad del tipo insertable será mapeada a la tabla de la entidad que lo contenga, como si fuera una propiedad declarada en la propia entidad. Definimos un tipo insertable con la anotación @Embeddable:

@Embeddable 
public class Direccion { 
    private String calle; 
    private int codigoPostal; 
    
    // ... 
} 
	

Y lo insertamos en una entidad (u otro tipo insertable) con la anotación @Embedded

@Entity 
public class Persona { 
    // ... 
    
    @Embedded 
    private Direccion direccion; 
} 
	

Ahora, la tabla que contenga las entidades de tipo Persona contendrá sus propias columnas, más las definidas en el tipo insertable Direccion.

1.10 Tipos de acceso

Para terminar este primer artículo del tutorial de introducción a JPA 2.0, vamos a ver que son los tipos de acceso y como aplicarlos correctamente. JPA permite definir dos tipos de acceso:

  • Acceso a variable (Field access)
  • Acceso a propiedad (Property access)

El tipo de acceso que usará una entidad está definido por el lugar donde situemos sus anotaciones de mapeo. Si las anotaciones están en las variables que conforman la clase (como hemos hecho hasta ahora), estaremos indicando a JPA que debe realizar acceso a variable. Si, por el contrario situamos las anotaciones de mapeo en los métodos getter, estaremos indicando un acceso a propiedad. A efectos prácticos, no existe diferencia alguna entre ambas opciones (más allá de gustos personales y de organización de código). Sin embargo, en determinadas ocasiones debemos ser consecuentes con el tipo de acceso que elijamos, evitando mezclar tipos de acceso (lo cual puede inducir a JPA a actuar de forma errónea). Un ejemplo típico es el uso de clases insertables: salvo que se especifique lo contrario, las clases insertables heredan el tipo de acceso de la entidad donde son insertadas, ignorando cualquier anotación que se haga hecho sobre ellas previamente. Veamos un ejemplo donde se muestra este problema:

@Embeddable 
public class Insertable { 
    private int variable; 
     
    @Column(name = "VALOR_DOBLE") 
    public int getVariable() { 
        return variable * 2; 
    } 
     
    public void setVariable(int variable) { 
        this.variable = variable; 
    } 
} 

@Entity 
public class Entidad 
    @Id 
    @GeneratedValue 
    private Long id; 
    @Embedded 
    private Insertable insertable; 
} 
	

En el ejemplo anterior, la clase Insertable define un tipo de acceso a propiedad (ya que las anotaciones se han definido en los métodos getter), y queremos que se acceda a través de estos métodos al valor de las variables (tal vez necesitemos realizar cierto procesamiento sobre la variable, como multiplicarla por dos antes de devolverla). Sin embargo, la clase Entidad define un tipo de acceso a variable (ya que las anotaciones se han definido en las variables). Puesto que el tipo insertable va a heredar el tipo de acceso de la entidad donde se encuentra definido, cuando accedamos al valor de Entidad.insertable.variable obtendremos un valor no esperado (el valor de variable, en lugar de su valor multiplicado por dos que devuelve getVariable()). Para evitar estos problemas debemos indicar explícitamente el tipo de acceso de Insertable mediante la anotación @Access:

@Embeddable
@Access(AccessType.PROPERTY)
public class Insertable { ... }
	

Anotando la clase Insertable con @Access(AccessType.PROPERTY), cuando accedamos a Entidad.insertable.variable lo haremos a través de su método getter, a pesar de que en Entidad se está usando un tipo de acceso a variable. Un efecto de definir explícitamente el tipo de acceso, es que las anotaciones que no correspondan con ese tipo de acceso serán ignoradas (a no ser que vengan acompañadas a su vez con la anotación @Access):

@Embeddable 
@Access(AccessType.FIELD) 
public class Insertable { 
    private int variable; 
     
    @Column(name = "VALOR_DOBLE") 
    public int getVariable() { 
        return variable * 2; 
    } 
     
    public void setVariable(int variable) { 
        this.variable = variable; 
    } 
} 
	

En el ejemplo anterior, se ha definido un acceso a variable de forma explícito (con @Access(AccessType.FIELD)), por lo que la anotación @Column será ignorada (no accederemos a la variable a través del método getter, a pesar de estar anotado). Toda la discusión sobre tipos de acceso está directamente relacionada con la capacidad de JPA para acceder a todas las variables y métodos de una clase, independientemente de su nivel de acceso (public, protected, package-default, o private).

Resumen

En este primer artículo del tutorial de introducción a JPA 2.0 hemos visto los principios de JPA y las anotaciones necesarias para realizar ORM básico.

En el próximo artículo veremos como trabajar con asociaciones y con herencia cuando realizamos ORM.