Skip to content

2. Las entidades JPA

2.1. Ejemplo 1: Representación de objeto de una sola tabla

2.1.1. La tabla [personne]

Consideremos una base de datos que contiene una sola tabla, [personne], cuya función es almacenar cierta información sobre personas:

 
ID
clave primaria de la tabla
VERSION
versión de la fila en la tabla. Cada vez que
se modifica el registro, se incrementa su número de versión.
NOM
nombre de la persona
PRENOM
su nombre
DATENAISSANCE
su fecha de nacimiento
MARIE
número entero 0 (soltero) o 1 (casado)
NBENFANTS
número de hijos de la persona

2.1.2. La entidad [Personne]

Nos situamos en el siguiente entorno de ejecución:

La capa JPA [5] debe servir de puente entre el mundo relacional de la base de datos [7] y el mundo de objetos [4] manipulado por los programas Java [3]. Este puente se establece mediante configuración y hay dos formas de hacerlo:

  1. con archivos XML. Esta era prácticamente la única forma de hacerlo hasta la llegada de JDK 1.5
  1. con anotaciones de Java a partir de la versión 1.5 de JDK

En este documento, utilizaremos casi exclusivamente el segundo método.

El objeto [Personne] que representa la tabla [personne] presentada anteriormente podría ser el siguiente:


...

@SuppressWarnings("unused")
@Entity
@Table(name="Personne")
public class Personne implements Serializable{

    @Id
    @Column(name = "ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    @Column(name = "VERSION", nullable = false)
    @Version
    private int version;

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
    private String nom;

    @Column(name = "PRENOM", length = 30, nullable = false)
    private String prenom;

    @Column(name = "DATENAISSANCE", nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(name = "MARIE", nullable = false)
    private boolean marie;

    @Column(name = "NBENFANTS", nullable = false)
    private int nbenfants;

    // constructores
    public Personne() {
    }

    public Personne(String nom, String prenom, Date datenaissance, boolean marie,
            int nbenfants) {
        setNom(nom);
        setPrenom(prenom);
        setDatenaissance(datenaissance);
        setMarie(marie);
        setNbenfants(nbenfants);
    }

    // toString
    public String toString() {
...
    }

    // getters y setters
...
}

La configuración se realiza mediante anotaciones Java @Annotation. Las anotaciones de Java son interpretadas ya sea por el compilador o por herramientas especializadas en el momento de la ejecución. A excepción de la anotación de la línea 3, destinada al compilador, todas las demás anotaciones están destinadas a la implementación JPA utilizada, ya sea Hibernate o Toplink. Por lo tanto, serán interpretadas en el momento de la ejecución. Si no se cuenta con las herramientas capaces de interpretarlas, estas anotaciones se ignoran. De este modo, la clase [Personne] anterior podría utilizarse en un contexto ajeno a JPA.

Hay que distinguir dos casos de uso de las anotaciones JPA en una clase C asociada a una tabla T:

  1. la tabla T ya existe: en ese caso, las anotaciones JPA deben reproducir lo que ya existe (nombre y definición de las columnas, restricciones de integridad, claves externas, claves primarias, etc.)
  2. la tabla T no existe y se creará según las anotaciones que se encuentren en la clase C.

El caso 2 es el más fácil de manejar. Mediante las anotaciones JPA, indicamos la estructura de la tabla T que deseamos. El caso 1 suele ser más complejo. Es posible que la tabla T se haya creado, hace mucho tiempo, fuera de cualquier contexto JPA. Por lo tanto, su estructura podría no estar bien adaptada al puente relacional/objeto de JPA. Para simplificar, nos centraremos en el caso 2, en el que la tabla T asociada a la clase C se creará según las anotaciones JPA de la clase C.

Comentemos las anotaciones JPA de la clase [Personne]:

  • línea 4: la anotación @Entity es la primera anotación indispensable. Se coloca antes de la línea que declara la clase e indica que la clase en cuestión debe ser administrada por la capa de persistencia JPA. En ausencia de esta anotación, todas las demás anotaciones JPA serían ignoradas.
  • línea 5: la anotación @Table designa la tabla de la base de datos que la clase representa. Su argumento principal es name, que designa el nombre de la tabla. Si no se incluye este argumento, la tabla llevará el nombre de la clase, en este caso [Personne]. Por lo tanto, en nuestro ejemplo, la anotación @Table es innecesaria.
  • línea 8: la anotación @Id sirve para designar el campo de la clase que representa la clave primaria de la tabla. Esta anotación es obligatoria. Aquí indica que el campo id de la línea 11 representa la clave primaria de la tabla.
  • línea 9: la anotación @Column sirve para establecer el vínculo entre un campo de la clase y la columna de la tabla que representa dicho campo. El atributo name indica el nombre de la columna en la tabla. Si no se incluye este atributo, la columna lleva el mismo nombre que el campo. En nuestro ejemplo, el argumento name no era, por lo tanto, obligatorio. El argumento nullable=false indica que la columna asociada al campo no puede tener el valor NULL y que, por lo tanto, el campo debe tener necesariamente un valor.
  • línea 10: la anotación @GeneratedValue indica cómo se genera la clave primaria cuando es generada automáticamente por el SGBD. Este será el caso en todos nuestros ejemplos. Esto no es obligatorio. Así, nuestra persona podría tener un número de estudiante que sirviera como clave primaria y que no fuera generado por el SGBD, sino establecido por la aplicación. En ese caso, la anotación @GeneratedValue no estaría presente. El argumento strategy indica cómo se genera la clave primaria cuando la genera el SGBD. No todos los SGBD utilizan la misma técnica para generar los valores de la clave primaria. Por ejemplo:
Firebird
utiliza un generador de valores que se invoca antes de cada inserción
SQL server
el campo de clave primaria se define con el tipo Identity. El resultado es similar al generador de valores de Firebird, salvo que el valor de la clave solo se conoce después de insertar la fila.
Oracle
utiliza un objeto llamado SEQUENCE que, una vez más, desempeña la función de un generador de valores

La capa JPA debe generar órdenes SQL diferentes según los SGBD para crear el generador de valores. Mediante la configuración, se le indica el tipo de SGBD que debe manejar. De esta manera, puede determinar cuál es la estrategia habitual para generar valores de clave primaria de ese SGBD. El argumento strategy = GenerationType.AUTO le indica a la capa JPA que debe utilizar esta estrategia habitual. Esta técnica ha funcionado en todos los ejemplos de este documento para los siete SGBD utilizados.

  • línea 14: la anotación @Version designa el campo que se utiliza para gestionar los accesos concurrentes a una misma fila de la tabla.

Para comprender este problema de acceso concurrente a una misma fila de la tabla [personne], supongamos que una aplicación web permite actualizar los datos de una persona y analicemos el siguiente caso:

En el momento T1, un usuario U1 ingresa para modificar un registro de persona P. En ese momento, el número de hijos es 0. Cambia este número a 1, pero antes de que valide su modificación, un usuario U2 ingresa para modificar a la misma persona P. Dado que U1 aún no ha validado su modificación, U2 ve en su pantalla que el número de hijos es 0. U2 cambia el nombre de la persona P a mayúsculas. Luego, U1 y U2 validan sus modificaciones en ese orden. La modificación de U2 será la que prevalezca: en la base de datos, el nombre aparecerá en mayúsculas y el número de hijos se mantendrá en cero, aunque U1 crea haberlo cambiado a 1.

El concepto de versión de persona nos ayuda a resolver este problema. Tomemos el mismo caso de uso:

En el momento T1, un usuario U1 ingresa para modificar una persona P. En ese momento, el número de hijos es 0 y la versión es V1. Cambia el número de hijos a 1, pero antes de que valide su modificación, un usuario U2 ingresa para modificar a la misma persona P. Dado que U1 aún no ha validado su modificación, U2 ve que el número de hijos es 0 y que la versión es V1. U2 cambia el nombre de la persona P a mayúsculas. Luego, U1 y U2 validan sus modificaciones en ese orden. Antes de validar una modificación, se verifica que quien modifica a la persona P tenga la misma versión que la persona P actualmente registrada. Este será el caso del usuario U1. Por lo tanto, se acepta su modificación y se cambia la versión de la persona modificada de V1 a V2 para indicar que la persona ha sufrido un cambio. Al validar la modificación de U2, nos daremos cuenta de que U2 tiene una versión V1 de la persona P, mientras que actualmente la versión de esta es V2. Entonces podremos informarle al usuario U2 que alguien se le adelantó y que debe comenzar de nuevo con la nueva versión de la persona P. Él lo hará, recuperará una persona P de la versión V2 que ahora tiene un hijo, cambiará el nombre a mayúsculas y validará. Su modificación se aceptará si la persona P registrada sigue teniendo la versión V2. Al final, se tomarán en cuenta las modificaciones realizadas por U1 y U2, mientras que en el caso de uso sin versión, una de las modificaciones se perdía.

La capa [dao] de la aplicación cliente puede gestionar por sí misma la versión de la clase [Personne]. Cada vez que se modifique un objeto P, la versión de ese objeto se incrementará en 1 en la tabla. La anotación @Version permite transferir esta gestión a la capa JPA. El campo en cuestión no tiene por qué llamarse version como en el ejemplo. Puede tener cualquier nombre.

Los campos correspondientes a las anotaciones @Id y @Version están presentes debido a la persistencia. No serían necesarios si la clase [Personne] no tuviera que ser persistente. Por lo tanto, se observa que un objeto no tiene la misma representación dependiendo de si necesita o no ser persistente.

  • línea 17: nuevamente la anotación @Column para proporcionar información sobre la columna de la tabla [personne] asociada al campo nom de la clase Personne. Aquí encontramos dos nuevos argumentos:
    • unique=true indica que el nombre de una persona debe ser único. Esto se traducirá en la base de datos en la adición de una restricción de unicidad en la columna NOM de la tabla [personne].
    • length=30 establece en 30 el número de caracteres de la columna NOM. Esto significa que el tipo de esta columna será VARCHAR(30).
  • línea 24: la anotación @Temporal sirve para indicar qué tipo SQL se le debe asignar a una columna o campo de tipo fecha/hora. El tipo TemporalType.DATE se refiere a una fecha sola, sin hora asociada. Los otros tipos posibles son TemporalType.TIME para codificar una hora y TemporalType.TIMESTAMP para codificar una fecha con hora.

Ahora comentemos el resto del código de la clase [Personne]:

  • línea 6: la clase implementa la interfaz Serializable. La sérialisation de un objeto consiste en transformarlo en una secuencia de bits. La désérialisation es la operación inversa. La serialización y deserialización se utilizan, en particular, en aplicaciones cliente-servidor donde los objetos se intercambian a través de la red. Las aplicaciones cliente o servidor no tienen conocimiento de esta operación, la cual se realiza de manera transparente mediante los JVM. Sin embargo, para que esto sea posible, es necesario que las clases de los objetos intercambiados estén «etiquetadas» con la palabra clave Serializable.
  • Línea 37: un constructor de la clase. Cabe señalar que los campos id y version no forman parte de los parámetros. De hecho, estos dos campos son administrados por la capa JPA y no por la aplicación.
  • Líneas 51 y siguientes: los métodos get y set de cada uno de los campos de la clase. Cabe señalar que las anotaciones JPA pueden colocarse en los métodos get de los campos en lugar de en los campos mismos. La ubicación de las anotaciones indica el modo que debe utilizar JPA para acceder a los campos:
    • si las anotaciones se colocan a nivel de campo, JPA accederá directamente a los campos para leerlos o escribirlos
    • si las anotaciones se colocan a nivel de get, JPA accederá a los campos a través de los métodos get / set para leerlos o escribirlos

Es la posición de la anotación @Id la que determina la posición de las anotaciones JPA en una clase. Si se coloca a nivel de campo, indica un acceso directo a los campos; y si se coloca a nivel de get, indica un acceso a los campos mediante los métodos get y set. Las demás anotaciones deben colocarse de la misma manera que la anotación @Id.

2.1.3. El proyecto Eclipse de las pruebas

Realizaremos nuestros primeros experimentos con la entidad [Personne] mencionada anteriormente. Los llevaremos a cabo con la siguiente arquitectura:

  • en [7]: la base de datos que se generará a partir de las anotaciones de la entidad [Personne], así como de configuraciones adicionales realizadas en un archivo llamado [persistence.xml]
  • en [5, 6]: una capa JPA implementada por Hibernate
  • en [4]: la entidad [Personne]
  • en [3]: un programa de prueba de tipo consola

Realizaremos varios experimentos:

  • generar el esquema de BD a partir de un script de Ant y la herramienta Hibernate Tools
  • generar la BD e inicializarla con algunos datos
  • utilizar la tabla BD y realizar las cuatro operaciones básicas en la tabla [personne] (inserción, actualización, eliminación, consulta)

Las herramientas necesarias son las siguientes:

  • Eclipse y sus complementos descritos en el párrafo 5.2.
  • el proyecto [hibernate-personnes-entites], que se encuentra en la carpeta <ejemplos>/hibernate/direct/personas-entidades
  • los diversos archivos SGBD descritos en los anexos (párrafo 5 y siguientes).

El proyecto de Eclipse es el siguiente:

  • en [1]: la carpeta del proyecto de Eclipse
  • en [2]: el proyecto importado a Eclipse (Archivo / Importar)
  • en [3]: la entidad [Personne] objeto de las pruebas
  • en [4]: los programas de prueba
  • en [5]: [persistence.xml] es el archivo de configuración de la capa JPA
  • en [6]: las bibliotecas utilizadas. Se describieron en el párrafo 1.5.
  • en [8]: un script de Ant que se utilizará para generar la tabla asociada a la entidad [Personne]
  • en [9]: los archivos [persistence.xml] para cada uno de los SGBD utilizados
  • en [10]: los esquemas de la base de datos generada para cada uno de los SGBD utilizados

Describiremos estos elementos uno por uno.

2.1.4. La entidad [Personne] (2)

Aportamos una ligera modificación a la descripción realizada anteriormente de la entidad [Personne], así como información adicional:


package entites;

...

@SuppressWarnings({ "unused", "serial" })
@Entity
@Table(name="jpa01_personne")
public class Personne implements Serializable{

    @Id
    @Column(name = "ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    @Column(name = "VERSION", nullable = false)
    @Version
    private int version;

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
    private String nom;

    @Column(name = "PRENOM", length = 30, nullable = false)
    private String prenom;

    @Column(name = "DATENAISSANCE", nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(name = "MARIE", nullable = false)
    private boolean marie;

    @Column(name = "NBENFANTS", nullable = false)
    private int nbenfants;

    // constructores
    public Personne() {
    }

    public Personne(String nom, String prenom, Date datenaissance, boolean marie,
            int nbenfants) {
....
    }

    // toString
    public String toString() {
        return String.format("[%d,%d,%s,%s,%s,%s,%d]", getId(), getVersion(),
                getNom(), getPrenom(), new SimpleDateFormat("dd/MM/yyyy")
                        .format(getDatenaissance()), isMarie(), getNbenfants());
    }

    // métodos de lectura y escritura
...
}
  • línea 7: le damos el nombre [jpa01_personne] a la tabla asociada a la entidad [Personne]. En este documento, se crearán varias tablas en un esquema que siempre se llamará jpa. Al final de este tutorial, el esquema jpa contendrá numerosas tablas. Para que el lector pueda orientarse, las tablas relacionadas entre sí tendrán el mismo prefijo: jpaxx_.
  • línea 45: un método [toString] para mostrar un objeto [Personne] en la consola.

2.1.5. Configuración de la capa de acceso a datos

En el proyecto de Eclipse anterior, la configuración de la capa JPA se realiza mediante el archivo [META-INF/persistence.xml]:

Al ejecutarse, se busca el archivo [META-INF/persistence.xml] en el classpath de la aplicación. En nuestro proyecto de Eclipse, todo lo que se encuentra en la carpeta [/src] [1] se copia a una carpeta [/bin] [2]. Esta carpeta forma parte del classpath del proyecto. Por esta razón, se encontrará [META-INF/persistence.xml] cuando se configure la capa JPA.

Por defecto, Eclipse no coloca los códigos fuente en la carpeta [/src] del proyecto, sino directamente en la carpeta principal. Todos nuestros proyectos de Eclipse se configurarán para que los códigos fuente estén en [/src] y las clases compiladas en [/bin], tal como se muestra en el párrafo 5.2.1.

Veamos la configuración de la capa JPA realizada en el archivo [persistence.xml] de nuestro proyecto:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!-- proveedor -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <!-- clases persistentes -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- registros SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- conexión JDBC -->
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
            <property name="hibernate.connection.url" value="jdbc:mysql://localhost:3306/jpa" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
            <!-- creación automática del esquema -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- Dialecto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
            <!-- propiedades DataSource c3p0 -->
            <property name="hibernate.c3p0.min_size" value="5" />
            <property name="hibernate.c3p0.max_size" value="20" />
            <property name="hibernate.c3p0.timeout" value="300" />
            <property name="hibernate.c3p0.max_statements" value="50" />
            <property name="hibernate.c3p0.idle_test_period" value="3000" />
        </properties>
    </persistence-unit>
</persistence>

Para entender esta configuración, debemos repasar la arquitectura de acceso a los datos de nuestra aplicación:

  • El archivo [persistence.xml] configurará las capas [4, 5, 6]
  • [4]: implementación de Hibernate de JPA
  • [5]: Hibernate accede a la base de datos a través de un grupo de conexiones. Un grupo de conexiones es una reserva de conexiones abiertas con el SGBD. Un SGBD es utilizado por múltiples usuarios, aunque, por razones de rendimiento, no puede superar un número límite N de conexiones abiertas simultáneamente. Un código bien escrito abre una conexión con el SGBD por el menor tiempo posible: envía órdenes al SQL y cierra la conexión. Lo hará de manera repetida, cada vez que necesite trabajar con la base de datos. El costo de abrir y cerrar una conexión no es insignificante, y ahí es donde entra en juego el grupo de conexiones. Este, al iniciar la aplicación, abrirá N1 conexiones con el SGBD. Es a este al que la aplicación le solicitará una conexión abierta cuando la necesite. Dicha conexión se devolverá al grupo tan pronto como la aplicación ya no la necesite, preferiblemente lo más rápido posible. La conexión no se cierra y permanece disponible para el siguiente usuario. Por lo tanto, un grupo de conexiones es un sistema para compartir conexiones abiertas.
  • [6]: el controlador JDBC del SGBD utilizado

Ahora veamos cómo el archivo [persistence.xml] configura las capas [4, 5, 6] mencionadas anteriormente:

  • línea 2: la etiqueta raíz del archivo XML es <persistence>.
  • línea 3: <persistence-unit> sirve para definir una unidad de persistencia. Puede haber varias unidades de persistencia. Cada una de ellas tiene un nombre (atributo name) y un tipo de transacción (atributo transaction-type). La aplicación tendrá acceso a la unidad de persistencia a través de su nombre, en este caso jpa. El tipo de transacción RESOURCE_LOCAL indica que la aplicación gestiona por sí misma las transacciones con el SGBD. Este será el caso aquí. Cuando la aplicación se ejecuta en un contenedor EJB3, puede utilizar el servicio de transacciones de este. En este caso, se establecerá transaction-type=JTA (Java Transaction API). JTA es el valor por defecto cuando no se especifica el atributo transaction-type.
  • línea 5: la etiqueta <provider> sirve para definir una clase que implemente la interfaz [javax.persistence.spi.PersistenceProvider], interfaz que permite a la aplicación inicializar la capa de persistencia. Dado que se utiliza una implementación de JPA / Hibernate, la clase utilizada aquí es una clase de Hibernate.
  • línea 6: la etiqueta <properties> introduce propiedades específicas del provider concreto que se haya elegido. Así, dependiendo de si se ha elegido Hibernate, Toplink, Kodo, etc., se tendrán propiedades diferentes. Las que siguen son específicas de Hibernate.
  • línea 8: le pide a Hibernate que explore el classpath del proyecto para encontrar las clases que tengan la anotación @Entity y así poder administrarlas. Las clases @Entity también pueden declararse mediante las etiquetas <class>nom_de_la_classe</class>, directamente debajo de la etiqueta <persistence-unit>. Esto es lo que haremos con el provider JPA / Toplink.
  • Las líneas 10 a 12, que aquí aparecen como comentarios, configuran los registros de consola de Hibernate:
  • línea 10: para mostrar o no los comandos SQL emitidos por Hibernate en el SGBD. Esto es muy útil durante la fase de aprendizaje. Debido al puente relacional/objeto, la aplicación trabaja con objetos persistentes a los que aplica operaciones del tipo [persist, merge, remove]. Es muy interesante saber cuáles son los comandos SQL que realmente se emiten en estas operaciones. Al estudiarlas, poco a poco se va adivinando cuáles son las órdenes SQL que Hibernate generará al realizar tal operación sobre los objetos persistentes, y el puente relacional/objeto comienza a tomar forma en la mente.
  • línea 11: las órdenes SQL que se muestran en la consola se pueden formatear de manera atractiva para facilitar su lectura
  • línea 12: además, los comandos SQL que se muestren llevarán comentarios
  • Las líneas 15-19 definen la capa JDBC (capa [6] en la arquitectura):
  • línea 15: la clase del controlador JDBC del SGBD, en este caso MySQL5
  • línea 16: la URL de la base de datos utilizada
  • líneas 17 y 18: el usuario de la conexión y su contraseña
  • Aquí utilizamos elementos explicados en los anexos del párrafo 5.5. Se invita al lector a consultar esta sección sobre MySQL5.
  • línea 22: Hibernate necesita conocer el SGBD con el que está trabajando. De hecho, todos los SGBD tienen extensiones SQL propias, una forma específica de gestionar la generación automática de los valores de una clave primaria, ... lo que hace que Hibernate necesite conocer el SGBD con el que está trabajando para poder enviarle las órdenes SQL que este entenderá. [MySQL5InnoDBDialect] hace referencia al SGBD MySQL5 con tablas de tipo InnoDB que admiten transacciones.
  • Las líneas 24 a 28 configuran el grupo de conexiones c3p0 (capa [5] en la arquitectura):
  • líneas 24 y 25: el número mínimo (por defecto 3) y máximo de conexiones (por defecto 15) en el grupo. El número inicial de conexiones por defecto es 3.
  • línea 26: tiempo máximo, en milisegundos, de espera de una solicitud de conexión por parte del cliente. Pasado este tiempo, c3p0 le devolverá una excepción.
  • línea 27: para acceder a la orden BD, Hibernate utiliza órdenes SQL preparadas (PreparedStatement) que c3p0 puede almacenar en caché. Esto significa que si la aplicación solicita por segunda vez una orden SQL preparada que ya se encuentra en la caché, no será necesario prepararla (la preparación de una orden SQL tiene un costo) y se utilizará la que está en la caché. Aquí se indica el número máximo de órdenes SQL preparadas que puede contener la caché, considerando todas las conexiones (una orden SQL preparada pertenece a una conexión).
  • línea 28: frecuencia de verificación, en milisegundos, de la validez de las conexiones. Una conexión del grupo puede volverse inválida por diversas razones (el controlador JDBC invalida la conexión porque dura demasiado, el controlador JDBC presenta «errores», etc.).
  • línea 20: aquí se solicita que, al inicializar la unidad de persistencia, se genere la base de datos de los objetos @Entity. Hibernate cuenta ahora con todas las herramientas para emitir las órdenes SQL de generación de las tablas de la base de datos:
  • la configuración de los objetos @Entity le permite saber qué tablas debe generar
  • las líneas 15-18 y 24-28 le permiten establecer una conexión con el SGBD
  • la línea 22 le permite saber qué dialecto SQL utilizar para generar las tablas

De este modo, el archivo [persistence.xml] utilizado aquí recrea una base de datos nueva cada vez que se ejecuta la aplicación. Las tablas se recrean (create table) después de haber sido eliminadas (drop table) si ya existían. Cabe señalar que, obviamente, esto no debe hacerse en una base de datos en producción...

Las pruebas han demostrado que la fase de eliminación y creación de las tablas podía fallar. Esto ocurrió, en particular, cuando, para una misma prueba, se pasaba de una capa JPA/Hibernate a una capa JPA/Toplink o viceversa. A partir de los mismos objetos @Entity, las dos implementaciones no generan exactamente las mismas tablas, generadores, secuencias, etc., y en ocasiones ha sucedido que la fase de eliminación y creación falla y nos vemos obligados a eliminar las tablas manualmente. La sección «Anexos», párrafo 5 y siguientes, describe las aplicaciones que se pueden utilizar para realizar este trabajo manualmente. Cabe señalar que la implementación JPA/Hibernate ha demostrado ser la más eficaz en esta fase de creación inicial del contenido de la base de datos: los fallos han sido muy poco frecuentes.

Las herramientas utilizadas por la capa JPA / Hibernate se encuentran en la biblioteca [jpa-hibernate], presentada en el párrafo 1.5, página 8. Los controladores JDBC necesarios para acceder a los SGBD se encuentran en la biblioteca [jpa-divers]. Estas dos bibliotecas se han incluido en el classpath del proyecto que se analiza aquí. A continuación recordamos su contenido:

2.1.6. Generación de la base de datos con un script de Ant

Como acabamos de ver, Hibernate proporciona herramientas para generar la base de datos de imagen de los objetos @Entity de la aplicación. Hibernate puede:

  • generar el archivo de texto con las órdenes SQL que crean la base de datos. En ese caso, solo se utiliza el dialecto definido en [persistence.xml].
  • crear las tablas que representan los objetos @Entity en la base de datos de destino definida en [persistence.xml]. En este caso, se utiliza el archivo completo [persistence.xml].

Vamos a presentar un script de Ant capaz de generar el esquema de la base de datos, es decir, las tablas de los objetos @Entity. Este script no es mío: se basa en un script similar de [ref1]. Ant (Another Neat Tool) es una herramienta de procesamiento por lotes de tareas en Java. Los scripts Ant no son fáciles de entender para un principiante. Solo utilizaremos uno, el que comentaremos a continuación:

  • en [1]: la estructura de directorios de los ejemplos de este tutorial.
  • en [2]: la carpeta [personnes-entites] del proyecto de Eclipse que estamos analizando actualmente
  • en [3]: la carpeta <lib> que contiene las cinco bibliotecas JAR definidas en el párrafo 1.5.
  • en [4]: el archivo comprimido [hibernate-tools.jar] necesario para una de las tareas del script [ant-hibernate.xml] que vamos a estudiar.
  • en [5]: el proyecto de Eclipse y el script [ant-hibernate.xml]
  • en [6]: la carpeta [src] del proyecto

El script [ant-hibernate.xml] [5] utilizará los archivos JAR de la carpeta <lib> [3], en particular el archivo [hibernate-tools.jar] [4] de la carpeta [lib/hibernate]. Hemos reproducido la estructura de carpetas para que el lector vea que, para encontrar la carpeta [lib] a partir de la carpeta [personnes-entites] [2] del script [ant-hibernate.xml], hay que seguir la ruta: ../../../lib.

Analicemos el script [ant-hibernate.xml]:


<project name="jpa-hibernate" default="compile" basedir=".">

    <!-- nombre del proyecto y versión -->
    <property name="proj.name" value="jpa-hibernate" />
    <property name="proj.shortname" value="jpa-hibernate" />
    <property name="version" value="1.0" />

    <!-- Propiedades globales -->
    <property name="src.java.dir" value="src" />
    <property name="lib.dir" value="../../../lib" />
    <property name="build.dir" value="bin" />

    <!-- la ruta de clases del proyecto -->
    <path id="project.classpath">
        <fileset dir="${lib.dir}">
            <include name="**/*.jar" />
        </fileset>
    </path>

    <!-- Los archivos de configuración que deben estar en el classpath-->
    <patternset id="conf">
        <include name="**/*.xml" />
        <include name="**/*.properties" />
    </patternset>

    <!-- Limpieza del proyecto -->
    <target name="clean" description="Nettoyer le projet">
        <delete dir="${build.dir}" />
        <mkdir dir="${build.dir}" />
    </target>

    <!-- Compilación del proyecto -->
<target name="compile" depends="clean">
        <javac srcdir="${src.java.dir}" destdir="${build.dir}" classpathref="project.classpath" />
    </target>

    <!-- Copiar los archivos de configuración en la ruta de clases -->
    <target name="copyconf">
        <mkdir dir="${build.dir}" />
        <copy todir="${build.dir}">
            <fileset dir="${src.java.dir}">
                <patternset refid="conf" />
            </fileset>
        </copy>
    </target>

    <!-- Herramientas de Hibernate -->
    <taskdef name="hibernatetool" classname="org.hibernate.tool.ant.HibernateToolTask" classpathref="project.classpath" />

    <!-- Generar el archivo DDL de la base de datos -->
    <target name="DDL" depends="compile, copyconf" description="Génération DDL base">

        <hibernatetool destdir="${basedir}">
            <classpath path="${build.dir}" />
            <!-- Usar META-INF/persistence.xml -->
            <jpaconfiguration />
            <!-- exportar -->
            <hbm2ddl drop="true" create="true" export="false" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
        </hibernatetool>
    </target>

    <!-- Generar la base -->
    <target name="BD" depends="compile, copyconf" description="Génération BD">

        <hibernatetool destdir="${basedir}">
            <classpath path="${build.dir}" />
            <!-- Usar META-INF/persistence.xml -->
            <jpaconfiguration />
            <!-- exportar -->
            <hbm2ddl drop="true" create="true" export="true" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
        </hibernatetool>
    </target>
</project>
  • línea 1: el proyecto [ant] se llama «jpa-hibernate». Reúne un conjunto de tareas, una de las cuales es la tarea predeterminada: en este caso, la tarea llamada «compile». Se invoca un script ant para ejecutar una tarea T. Si esta no se especifica, se ejecuta la tarea predeterminada. basedir="." indica que, para todas las rutas relativas que se encuentren en el script, el punto de partida es la carpeta en la que se encuentra el script ant, en este caso la carpeta <ejemplos>/hibernate/direct/personas-entidades.
  • Líneas 3-11: definen variables de script con la etiqueta <property name="nomVariable" value="valeurVariable"/>. La variable se puede utilizar posteriormente en el script con la notación ${nomVariable}. Los nombres pueden ser cualesquiera. Analicemos las variables definidas en las líneas 9-11:
    • línea 9: define una variable llamada «src.java.dir» (el nombre es libre) que, en el resto del script, designará la carpeta que contiene los códigos fuente de Java. Su valor es «src», una ruta relativa a la carpeta designada por el atributo basedir (línea 1). Por lo tanto, se trata de la ruta «./src», donde «.» designa aquí la carpeta <ejemplos>/hibernate/direct/personas-entidades. Es precisamente en la carpeta <personas-entidades>/src donde se encuentran los códigos fuente de Java (véase [6] más arriba).
    • línea 10: define una variable llamada «lib.dir» que, en el resto del script, designará la carpeta que contiene los archivos JAR que necesitan las tareas Java del script. Su valor ../../../lib hace referencia a la carpeta <ejemplos>/lib (véase [3] más arriba).
    • línea 11: define una variable llamada «build.dir» que, en el resto del script, indicará la carpeta donde se deben generar los archivos .class resultantes de la compilación de los archivos fuente .java. Su valor «bin» indica la carpeta <personas-entidades>/bin. Ya hemos explicado que, en el proyecto de Eclipse analizado, la carpeta <bin> era aquella donde se generaban los archivos .class. Ant hará lo mismo.
    • líneas 14-18: la etiqueta <path> sirve para definir elementos del classpath que deberán utilizar las tareas ant. Aquí, la ruta «project.classpath» (el nombre es libre) reúne todos los archivos .jar del árbol de directorios de la carpeta <exemples>/lib.
    • líneas 21-24: la etiqueta <patternset> sirve para designar un conjunto de archivos mediante patrones de nombres. Aquí, el patternset denominado «conf» designa a todos los archivos con la extensión .xml o .properties. Este patternset servirá para designar los archivos .xml y .properties de la carpeta <src> (persistence.xml, log4j.properties) (véase [6]), que son archivos de configuración de la aplicación. Al ejecutar ciertas tareas, estos archivos deben copiarse a la carpeta <bin> para que se encuentren en el classpath del proyecto. Entonces se utilizará el patternset conf para identificarlos.
    • Líneas 27-30: la etiqueta <target> designa una tarea del script. Es la primera que encontramos. Todo lo anterior corresponde a la configuración del entorno de ejecución del script ant. La tarea se llama clean. Se ejecuta en dos pasos: se elimina la carpeta <bin> (línea 28) para luego volver a crearla (línea 29).
    • líneas 33-35: la tarea «compile», que es la tarea predeterminada del script (línea 1). Depende (atributo «depends») de la tarea «clean». Esto significa que, antes de ejecutar la tarea «compile», ant debe ejecutar la tarea «clean», c.a.d, para limpiar la carpeta <bin>. El objetivo de la tarea «compile» aquí es compilar los códigos fuente de Java de la carpeta <src>.
    • línea 34: llamada al compilador de Java con tres parámetros:
      • srcdir: la carpeta que contiene los códigos fuente de Java, en este caso la carpeta <src>
      • destdir: la carpeta donde se deben guardar los archivos .class generados, en este caso la carpeta <bin>
      • classpathref: la ruta de clases que se utilizará para la compilación; en este caso, todos los archivos .jar del árbol de directorios de la carpeta <lib>
  • (continuación)
    • líneas 38-45: la tarea copyconf, cuyo objetivo es copiar en la carpeta <bin> todos los archivos .xml y .properties de la carpeta <src>.
    • línea 48: definición de una tarea mediante la etiqueta <taskdef>. Esta tarea está pensada para ser reutilizada en otras partes del script. Es una forma de facilitar la programación. Dado que la tarea se utiliza en varios lugares del script, se define una sola vez con la etiqueta <taskdef> y luego se reutiliza mediante su nombre, cuando sea necesario.
      • La tarea se llama hibernatetool (atributo name).
      • Su clase se define mediante el atributo classname. En este caso, la clase indicada se encuentra en el archivo [hibernate-tools.jar] del que ya hemos hablado.
      • El atributo classpathref le indica a ant dónde buscar la clase anterior
  • (continuación)
    • Las líneas 51 a 60 se refieren a la tarea que nos interesa aquí: la generación del esquema de la base de datos de imagen de los objetos @Entity de nuestro proyecto Eclipse.
      • Línea 51: la tarea se llama DDL (como Data Definition Language, el SQL asociado a la creación de objetos de una base de datos). Depende de las tareas «compile» y «copyconf» en ese orden. Por lo tanto, la tarea DDL provocará, en ese orden, la ejecución de las tareas «clean», «compile» y «copyconf». Cuando se inicia la tarea DDL, la carpeta <bin> contiene los archivos .class de los fuentes .java, en particular de los objetos @Entity, así como el archivo [META-INF/persistence.xml] que configura la capa JPA / Hibernate.
      • líneas 53-59: se invoca la tarea [hibernatetool] definida en la línea 48. Se le pasan varios parámetros, además de los ya definidos en la línea 48:
      • línea 53: la carpeta de salida de los resultados generados por la tarea será la carpeta actual.
      • línea 54: el directorio <bin> será el directorio de la tarea classpath
      • línea 56: le indica a la tarea [hibernatetool] cómo puede conocer su entorno de ejecución: la etiqueta <jpaconfiguration/> le indica que se encuentra en un entorno JPA y que, por lo tanto, debe utilizar el archivo [META-INF/persistence.xml] que encontrará aquí, en su classpath.
      • La línea 58 establece las condiciones para la generación de la base de datos: drop=true indica que se deben emitir las órdenes SQL «drop table» antes de crear las tablas; create=true indica que se debe crear el archivo de texto con las órdenes SQL para la creación de la base de datos; outputfilename indica el nombre de este archivo SQL —en este caso, schema.sql— en la carpeta <ddl> del proyecto de Eclipse; export=false indica que los comandos SQL generados no deben ejecutarse en una conexión con SGBD. Este punto es importante: implica que, para ejecutar la tarea, no es necesario iniciar el SGBD de destino. delimiter establece el carácter que separa dos órdenes SQL en el esquema generado; format=true solicita que se aplique un formateo básico al texto generado.
  • (continuación)
    • Las líneas 63-72 definen la tarea denominada BD. Es idéntica a la tarea anterior DDL, salvo que en esta ocasión genera la base de datos (export="true" en la línea 70). La tarea abre una conexión con la tarea SGBD utilizando la información encontrada en [persistence.xml], para ejecutar allí el esquema SQL y generar la base de datos. Por lo tanto, para ejecutar la tarea BD, es necesario que se haya iniciado la tarea SGBD.

2.1.7. Ejecución de la tarea ant DDL

Para ejecutar el script [ant-hibernate.xml], primero debemos realizar algunas configuraciones en Eclipse.

  • en [1]: seleccionar [External Tools]
  • en [2]: crear una nueva configuración ant
  • en [3]: asignar un nombre a la configuración ant
  • en [5]: seleccionar el script ant con el botón [4]
  • en [6]: aplicar los cambios
  • en [7]: se ha creado la configuración ant DDL
  • en [8]: en la pestaña JRE, se define el JRE que se va a utilizar. El campo [10] normalmente viene prellenado con el JRE que usa Eclipse. Por lo tanto, normalmente no hay que hacer nada en este panel. Sin embargo, me encontré con un caso en el que el script ant no lograba encontrar el compilador <javac>. Este no se encuentra en un JRE (Java Runtime Environment), sino en un JDK (Java Development Kit). La herramienta ant de Eclipse encuentra este compilador a través de la variable de entorno JAVA_HOME (Inicio / Panel de control / Rendimiento y mantenimiento / Sistema / pestaña Avanzado / botón Variables de entorno) [A]. Si esta variable no se ha definido, se puede permitir que ant encuentre el compilador <javac> configurando en [10], no un JRE, sino un JDK. Este se encuentra en la misma carpeta que JRE y [B]. Usaremos el botón [9] para declarar el JDK entre los JRE disponibles y el [C], para poder seleccionarlo posteriormente en el [10].
  • en [12]: en la pestaña [Targets], se selecciona la tarea DDL. De este modo, la configuración ant, a la que hemos llamado DDL [7], corresponderá a la ejecución de la tarea llamada DDL [12], la cual, como sabemos, genera el esquema DDL de la base de datos de imágenes de los objetos @Entity de la aplicación.
  • en [13]: se valida la configuración
  • en [14]: se ejecuta

En la vista [console] se obtienen los registros de la ejecución de la tarea ant DDL:


Buildfile: C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\ant-hibernate.xml
clean:
   [delete] Deleting directory C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
    [mkdir] Created dir: C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
compile:
    [javac] Compiling 3 source files to C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
copyconf:
     [copy] Copying 2 files to C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
DDL:
[hibernatetool] Executing Hibernate Tool with a JPA Configuration
[hibernatetool] 1. task: hbm2ddl (Generates database schema)
[hibernatetool] drop table if exists jpa01_personne;
[hibernatetool] create table jpa01_personne (
[hibernatetool] ID integer not null auto_increment,
[hibernatetool] VERSION integer not null,
[hibernatetool] NOM varchar(30) not null unique,
[hibernatetool] PRENOM varchar(30) not null,
[hibernatetool] DATENAISSANCE date not null,
[hibernatetool] MARIE bit not null,
[hibernatetool] NBENFANTS integer not null,
[hibernatetool] primary key (ID)
[hibernatetool] ) ENGINE=InnoDB;
BUILD SUCCESSFUL
Total time: 5 seconds
  • Recordemos que la tarea DDL se llama [hibernatetool] (línea 10) y que depende de las tareas clean (línea 2), compile (línea 5) y copyconf (línea 7).
  • línea 10: la tarea [hibernatetool] utiliza el archivo [persistence.xml] de una configuración JPA
  • línea 11: la tarea [hbm2ddl] generará el esquema DDL de la base de datos
  • líneas 12-22: el esquema DDL de la base de datos

Recordemos que le habíamos pedido a la tarea [hbm2ddl] que generara el esquema DDL en un lugar específico:


<hbm2ddl drop="true" create="true" export="true" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
  • línea 74: el esquema debe generarse en el archivo ddl/schema.sql. Verifiquemos:
  • en [1]: el archivo ddl/schema.sql sí está presente (ejecuta F5 para actualizar el árbol de directorios)
  • en [2]: su contenido. Este es el esquema de una base de datos MySQL5. El archivo de configuración [persistence.xml] de la capa JPA especificaba, de hecho, un SGBD MySQL5 (línea 8 a continuación):


            <!-- conexión JDBC -->
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
...
            <!-- creación automática del esquema -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- Dialecto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
            <!--  propiedades DataSource c3p0 -->
...

Analicemos el puente entre el objeto y la relación que se ha establecido aquí, examinando la configuración del objeto @Entity Persona y el esquema DDL generado:

Cabe destacar algunos puntos:

  • A1-B1: el nombre de la tabla especificado en A1 es, efectivamente, el que se utiliza en B1. Cabe destacar que el drop precede al create en B1.
  • A2-B2: muestra el modo de generación de la clave primaria. El modo AUTO especificado en A2 se tradujo en el atributo autoincrement propio de MySQL5. El modo de generación de la clave primaria suele ser específico de SGBD.
  • A3-B3: muestra el tipo SQL, un bit específico de MySQL5, para representar un tipo boolean de Java.

Repitamos esta prueba con otro SGBD:

  • La carpeta [conf] [1] contiene los archivos [persistence.xml] para diversos SGBD. Tomemos, por ejemplo, el archivo de Oracle [2] y coloquémoslo en la carpeta [META-INF] [3] en lugar del anterior. Su contenido es el siguiente:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  proveedor -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <!--  Clases persistentes -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- registros SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- conexión JDBC -->
            <property name="hibernate.connection.driver_class" value="oracle.jdbc.OracleDriver" />
            <property name="hibernate.connection.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
            <!-- creación automática del esquema -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- Dialecto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect" />
            <!-- propiedades DataSource c3p0 -->
            <property name="hibernate.c3p0.min_size" value="5" />
            <property name="hibernate.c3p0.max_size" value="20" />
            <property name="hibernate.c3p0.timeout" value="300" />
            <property name="hibernate.c3p0.max_statements" value="50" />
            <property name="hibernate.c3p0.idle_test_period" value="3000" />
        </properties>
    </persistence-unit>
</persistence>

Se invita al lector a consultar en los anexos la sección sobre Oracle (párrafo 5.7), especialmente para comprender la configuración de JDBC.

Solo la línea 25 es realmente importante aquí: se le indica a Hibernate que, a partir de ahora, el SGBD es un SGBD de Oracle. Al ejecutar la tarea Ant DDL se obtiene el resultado [4] que se muestra arriba. Cabe señalar que el esquema de Oracle es diferente del esquema MySQL5. Este es uno de los puntos fuertes de JPA: el desarrollador no tiene que preocuparse por estos detalles, lo que aumenta considerablemente la portabilidad de sus desarrollos.

2.1.8. Ejecución de la tarea ant BD

Quizás recordemos que la tarea ant, denominada BD, hace lo mismo que la tarea ant DDL, pero además genera la base de datos. Por lo tanto, es necesario ejecutar la tarea SGBD. Nos centraremos en el caso de SGBD y MySQL5, e invitamos al lector a copiar el archivo [conf/mysql5/persistence.xml] en la carpeta [src/META-INF]. Para verificar el funcionamiento de la tarea, utilizaremos el complemento SQL Explorer (véase el párrafo 5.2.6) para verificar el estado del archivo jpa BD antes y después de ejecutar la tarea ant BD.

En primer lugar, debemos crear una nueva configuración ant para ejecutar la tarea BD. Se recomienda al lector que siga el procedimiento descrito para la configuración ant DDL en el párrafo 2.1.7. La nueva configuración ant se llamará BD:

  • en [1]: se duplica la configuración anterior denominada DDL
  • en [2]: la nueva configuración se denomina BD. Esta ejecuta la tarea ant BD [3], que genera físicamente la base de datos.
  • Una vez hecho esto, ejecute SGBD y MySQL5 (párrafo 5.5).

Ahora utilizamos el complemento SQL Explorer para explorar las bases de datos administradas por el SGBD. El lector debe familiarizarse previamente con este complemento si es necesario (véase el párrafo 5.2.6).

  • [1]: se abre la perspectiva SQL Explorer [Window / Open Perspective / Other]
  • [2]: si es necesario, se crea una conexión [mysql5-jpa] (véase el párrafo 5.5.5, página 252) y se abre
  • [3]: se inicia sesión con jpa / jpa
  • [4]: estamos conectados a MySQL5.
  • en [5]: la tabla BD jpa solo tiene una tabla: [articles]
  • en [6]: iniciamos la ejecución de la tarea ant BD. Como estamos en la perspectiva [SQL Explorer], no vemos la vista [Console], que nos muestra los registros de la tarea. Podemos mostrar esta vista [Window / Show View / ...] o regresar a la perspectiva Java [Window / Open Perspective / ...].
  • En [7]: una vez finalizada la tarea Ant BD, si es necesario, regrese a la perspectiva [SQL Explorer] y actualice el árbol de la JPA BD.
  • En [8]: se observa la tabla [jpa01_personne] que se ha creado.

Se invita al lector a volver a generar BD con otros SGBD. El procedimiento a seguir es el siguiente:

  • copiar el archivo [conf/<sgbd>/persistence.xml] en la carpeta [src/META-INF], donde <sgbd> es el SGBD que se probó
  • ejecutar <sgbd> siguiendo las instrucciones del anexo correspondientes a este
  • en el explorador SQL, crear una conexión a <sgbd>. Esto también se explica en los anexos para cada uno de los SGBD
  • Repita las pruebas anteriores

Llegados a este punto, hemos adquirido una serie de conocimientos:

  • comprendemos mejor el concepto de puente objeto-relacional. En este caso, se ha implementado mediante Hibernate. Más adelante utilizaremos Toplink.
  • Sabemos que este puente objeto-relacional se configura en dos lugares:
  • en los objetos @Entity, donde se indican los vínculos entre los campos de los objetos y las columnas de las tablas de BD
  • en [META-INF/persistence.xml], donde se le proporciona a la implementación JPA información sobre los dos elementos del puente objeto-relacional: los objetos @Entity (objeto) y la base de datos (relacional).
  • Hemos creado dos tareas Ant, llamadas DDL y BD, que nos permiten crear la base de datos a partir de la configuración anterior, incluso antes de escribir cualquier código Java.

Ahora que la capa JPA de nuestra aplicación está correctamente configurada, podemos comenzar a explorar API y JPA con código Java.

2.1.9. El contexto de persistencia de una aplicación

Aclaremos un poco el entorno de ejecución de un cliente JPA:

Sabemos que la capa JPA [2] crea un puente entre objetos [3] y relaciones [4]. Se denomina «contexto de persistencia» al conjunto de objetos gestionados por la capa JPA en el marco de este puente entre objetos y relaciones. Para acceder a los datos del contexto de persistencia, un cliente JPA [1] debe pasar por la capa JPA [2]:

  1. puede crear un objeto y solicitar a la capa JPA que lo haga persistente. El objeto pasa entonces a formar parte del contexto de persistencia.
  2. puede solicitar a la capa [JPA] una referencia de un objeto persistente existente.
  3. puede modificar un objeto persistente obtenido de la capa JPA.
  4. puede solicitar a la capa JPA que elimine un objeto del contexto de persistencia.

La capa JPA presenta al cliente una interfaz llamada [EntityManager] que, como su nombre lo indica, permite gestionar los objetos @Entity del contexto de persistencia. A continuación, presentamos los principales métodos de esta interfaz:

void persist(Object entity)
coloca entity en el contexto de persistencia
void remove(Object entity)
elimina entity del contexto de persistencia
<T> T merge(T entity)
fusiona un objeto entity del cliente no administrado por el contexto de persistencia
con el objeto entity del contexto de persistencia que tiene la misma clave primaria.
El resultado devuelto es el objeto entity del contexto de persistencia.
<T> T find(Class<T> entityClass,
 Object primaryKey)
coloca en el contexto de persistencia un objeto buscado en la base
base de datos mediante su clave primaria. El tipo T del objeto permite
que la capa JPA sepa qué tabla consultar.
El objeto persistente así creado se devuelve al cliente.
Query createQuery(String queryText)
crea un objeto Query a partir de una consulta JPQL
(Java Persistence Query Language). Una consulta JPQL es análoga
a una consulta SQL, salvo que se consultan objetos en lugar de tablas.
Query createNativeQuery(String queryText)
método similar al anterior, salvo que queryText es,
una orden SQL y no JPQL.
Query createNamedQuery(String name)
Método idéntico al de createQuery, salvo que la orden JPQL queryText ha
se ha externalizado a un archivo de configuración y se le ha asignado un nombre.
Este nombre es el parámetro del método.

Un objeto EntityManager tiene un ciclo de vida que no es necesariamente el de la aplicación. Tiene un inicio y un final. Así, un cliente JPA puede trabajar sucesivamente con diferentes objetos EntityManager. El contexto de persistencia asociado a un EntityManager tiene el mismo ciclo de vida que él. Son inseparables el uno del otro. Cuando se cierra un objeto EntityManager, su contexto de persistencia se sincroniza, si es necesario, con la base de datos y luego deja de existir. Es necesario crear un nuevo EntityManager para volver a tener un contexto de persistencia.

El cliente JPA puede crear un EntityManager y, por lo tanto, un contexto de persistencia con la siguiente instrucción:


        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
  • javax.persistence.Persistence es una clase estática que permite obtener una fábrica (factory) de objetos EntityManager. Esta fábrica está vinculada a una unidad de persistencia específica. Recordemos que el archivo de configuración [META-INF/persistence.xml] permite definir unidades de persistencia y que estas tienen un nombre:

    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">

En el ejemplo anterior, la unidad de persistencia se llama jpa. Con ella viene toda una configuración propia, en particular el SGBD con el que trabaja. La instrucción [Persistence.createEntityManagerFactory("jpa")] crea una fábrica de objetos de tipo EntityManagerFactory capaz de proporcionar objetos EntityManager destinados a gestionar contextos de persistencia vinculados a la unidad de persistencia denominada jpa. La obtención de un objeto EntityManager y, por lo tanto, de un contexto de persistencia, se realiza a partir del objeto EntityManagerFactory de la siguiente manera:

        EntityManager em = emf.createEntityManager();

Los siguientes métodos de la interfaz [EntityManager] permiten gestionar el ciclo de vida del contexto de persistencia:

void close()
Se cierra el contexto de persistencia. Fuerza la sincronización del contexto de persistencia con la base de datos:
  • si un objeto del contexto no está presente en la base de datos, se inserta mediante una operación (SQL INSERT)
  • si un objeto del contexto está presente en la base de datos y ha sido modificado desde que se leyó, se realiza una operación (SQL UPDATE) para guardar la modificación
  • si un objeto del contexto ha sido marcado como «eliminado» al finalizar una operación remove sobre él, se realiza una operación SQL DELETE para eliminarlo de la base de datos.
void clear()
El contexto de persistencia se vacía de todos sus objetos, pero no se cierra.
void flush()
el contexto de persistencia se sincroniza con la base de datos de la manera descrita para close()

El cliente JPA puede forzar la sincronización del contexto de persistencia con la base de datos mediante el método [EntityManager].flush anterior. La sincronización puede ser explícita o implícita. En el primer caso, es el cliente quien debe realizar las operaciones flush cuando desee llevar a cabo sincronizaciones; de lo contrario, estas se realizan en determinados momentos que especificaremos a continuación. El modo de sincronización se gestiona mediante los siguientes métodos de la interfaz [EntityManager]:

void setFlushMode(FlushModeType flushMode)
Hay dos valores posibles para flushmode:
FlushModeType.AUTO (por defecto): la sincronización se lleva a cabo antes de cada consulta SELECT realizada en la base de datos.
FlushModeType.COMMIT: la sincronización solo se lleva a cabo al finalizar las transacciones en la base de datos.
FlushModeType getFlushMode()
muestra el modo actual de sincronización

Resumamos. En el modo FlushModeType.AUTO, que es el modo predeterminado, el contexto de persistencia se sincronizará con la base de datos en los siguientes momentos:

  1. antes de cada operación SELECT al
  2. al finalizar una transacción en la base
  3. tras una operación flush o close en el contexto de persistencia

En el modo FlushModeType.COMMIT, ocurre lo mismo, salvo por la operación 1, que no se lleva a cabo. El modo normal de interacción con la capa JPA es un modo transaccional. El cliente realiza diversas operaciones en el contexto de persistencia, dentro de una transacción. En este caso, los momentos de sincronización del contexto de persistencia con la base de datos son los casos 1 y 2 anteriores en el modo AUTO, y solo el caso 2 en el modo COMMIT.

Terminemos con el API de la interfaz Query, interfaz que permite emitir órdenes JPQL en el contexto de persistencia o bien órdenes SQL directamente en la base de datos para recuperar datos de ella. La interfaz Query es la siguiente:

Tendremos que utilizar los métodos del 1 al 4 mencionados anteriormente:

  • 1 - El método getResultList ejecuta un SELECT que devuelve varios objetos. Estos se obtendrán en un objeto List. Este objeto es una interfaz. Esta ofrece un objeto Iterator que permite recorrer los elementos de la lista L de la siguiente manera:

        Iterator iterator = L.iterator();
        while (iterator.hasNext()) {
             // procesar el objeto iterator.next() que representa el elemento actual de la lista
...
}

La lista L también se puede utilizar con un for:


        for (Object o : L) {
             // utilizar el objeto o
}
  • 2 - El método getSingleResult ejecuta una orden JPQL / SQL / SELECT que devuelve un único objeto.
  • 3 - El método executeUpdate ejecuta una orden SQL de actualización o eliminación y devuelve el número de filas afectadas por la operación.
  • 4 - El método setParameter(String, Object) permite asignar un valor a un parámetro con nombre de una orden JPQL configurada
  • 5 - El método setParameter(int, Object) utiliza el parámetro, pero este no se identifica por su nombre, sino por su posición en la orden JPQL.

2.1.10. Un primer cliente JPA

Volvamos a la perspectiva Java del proyecto:

 

Ahora sabemos casi todo sobre este proyecto, excepto el contenido de la carpeta [src/tests], que vamos a examinar a continuación. La carpeta contiene dos programas de prueba de la capa JPA:

  • [InitDB.java] es un programa que inserta algunas filas en la tabla [jpa01_personne] de la base de datos. Su código nos proporcionará los primeros elementos de la capa JPA.
  • [Main.java] es un programa que realiza las operaciones CRUD en la tabla [jpa01_personne]. El análisis de su código nos permitirá abordar los conceptos fundamentales del contexto de persistencia y del ciclo de vida de los objetos de este contexto.

2.1.10.1. El código

El código del programa [InitDB.java] es el siguiente:


package tests;

import java.text.ParseException;
import java.text.SimpleDateFormat;

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

import entites.Personne;

public class InitDB {
    // constantes
    private final static String TABLE_NAME = "jpa01_personne";

    public static void main(String[] args) throws ParseException {
        // Unidad de persistencia
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        // recuperar un EntityManagerFactory a partir de la unidad de persistencia
        EntityManager em = emf.createEntityManager();
        // Inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // Eliminar los elementos de la tabla de personas
        em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
        // crear dos personas
        Personne p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // persistencia de las personas
        em.persist(p1);
        em.persist(p2);
        // visualización de personas
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // fin de transacción
        tx.commit();
        // fin de EntityManager
        em.close();
        // fin de EntityManagerFactory
        emf.close();
        // registro
        System.out.println("terminé ...");
    }
}

Este código debe interpretarse a la luz de lo explicado en el párrafo 2.1.9.

  • línea 19: se solicita un objeto EntityManagerFactory emf para la unidad de persistencia jpa (definida en persistence.xml). Esta operación normalmente solo se realiza una vez durante la vida de una aplicación.
  • línea 21: se solicita un objeto EntityManager de tipo em para gestionar un contexto de persistencia.
  • línea 23: se solicita un objeto Transaction para gestionar una transacción. Cabe recordar aquí que las operaciones en el contexto de persistencia se realizan dentro de una transacción. Veremos que esto no es obligatorio, pero que, de no hacerlo, pueden surgir problemas. Si la aplicación se ejecuta en un contenedor EJB3, las operaciones sobre el contexto de persistencia siempre se realizan dentro de una transacción.
  • línea 24: comienza la transacción
  • línea 26: ejecuta una orden de borrado SQL en la tabla «jpa01_personne» (nativeQuery). Esto se hace para vaciar la tabla de todo su contenido y así poder ver mejor el resultado de la ejecución de la aplicación [InitDB]
  • líneas 28-29: se crean dos objetos Personne, p1 y p2. Son objetos normales y, por el momento, no tienen nada que ver con el contexto de persistencia. En lo que respecta al contexto de persistencia, Hibernate indica que estos objetos se encuentran en un estado transitorio (transient), a diferencia de los objetos persistentes (persistent), que son administrados por el contexto de persistencia. Nos referiremos más bien a objetos no persistentes (expresión no utilizada en francés) para indicar que aún no son administrados por el contexto de persistencia, y a objetos persistentes para aquellos que sí lo son. Encontraremos una tercera categoría de objetos: los objetos desvinculados (detached), que son objetos que anteriormente eran persistentes, pero cuyo contexto de persistencia se ha cerrado. El cliente puede tener referencias a dichos objetos, lo que explica que no se destruyan necesariamente al cerrarse el contexto de persistencia. Se dice entonces que se encuentran en estado desvinculado. La operación [EntityManager].merge permite volver a vincularlos a un contexto de persistencia recién creado.
  • líneas 31-32: las personas p1 y p2 se integran al contexto de persistencia mediante la operación [EntityManager].persist. De este modo, se convierten en objetos persistentes.
  • líneas 35-37: se ejecuta una orden JPQL «select p from Personne p order by p.nom asc». Personne no es la tabla (esta se llama jpa01_personne), sino el objeto @Entity asociado a la tabla. Aquí tenemos una consulta JPQL (Java Persistence Query Language) en el contexto de persistencia y no una orden SQL en la base de datos. Dicho esto, aparte del objeto Personne que reemplazó a la tabla jpa01_personne, las sintaxis son idénticas. Un bucle for recorre la lista (de personas) resultante de la consulta select para mostrar cada elemento en la consola. Aquí se busca verificar que los elementos colocados en el contexto de persistencia en las líneas 31 y 32 se encuentren efectivamente en la tabla. De manera transparente, se llevará a cabo una sincronización del contexto de persistencia con la base de datos. De hecho, se emitirá una consulta select y se ha indicado que este es uno de los casos en los que se realiza una sincronización. Por lo tanto, es en este momento cuando, en segundo plano, JPA / Hibernate emitirá las dos órdenes SQL y insert, que insertarán a las dos personas en la tabla jpa01_personne. La operación persist no lo había hecho. Esta operación integra objetos en el contexto de persistencia sin que ello tenga consecuencias en la base de datos. Los cambios reales se realizan durante las sincronizaciones, en este caso justo antes de la operación select en la base de datos.
  • línea 39: se finaliza la transacción iniciada en la línea 24. Se llevará a cabo nuevamente una sincronización. Aquí no ocurrirá nada, ya que el contexto de persistencia no ha cambiado desde la última sincronización.
  • línea 41: se cierra el contexto de persistencia.
  • línea 43: se cierra la fábrica de EntityManager.

2.1.10.2. a ejecución del código

  • ejecutar el SGBD MySQL5
  • colocar conf/mysql5/persistence.xml en META-INF/persistence.xml si es necesario
  • ejecutar la aplicación [InitDB]

Se obtienen los siguientes resultados:

  • en [1]: la visualización de la consola en la perspectiva Java. Se obtiene lo esperado.
  • en [2]: se verifica el contenido de la tabla [jpa01_personne] con la perspectiva SQL Explorer, tal como se explicó en el párrafo 2.1.8. Se pueden observar dos puntos:
    • la clave primaria ID se generó automáticamente
    • lo mismo ocurre con el número de versión. Se observa que la primera versión tiene el número 0..

Aquí tenemos los primeros elementos de la cultura JPA. Hemos logrado insertar datos en una tabla. Vamos a aprovechar estos conocimientos para escribir la segunda prueba, pero antes hablemos de los registros.

2.1.11. Implementar los registros de Hibernate

Es posible conocer las órdenes SQL emitidas a la base de datos por la capa JPA / Hibernate. Es interesante conocerlos para ver si la capa JPA es tan eficiente como un desarrollador que hubiera escrito él mismo las órdenes SQL.

Con JPA / Hibernate, los registros SQL se pueden revisar en el archivo [persistence.xml]:


            <!-- Clases persistentes -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- registros SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- conexión JDBC -->
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />

  • líneas 4-6: los registros SQL no estaban activados por el momento. Ahora se activan eliminando la etiqueta de comentarios de las líneas 3 y 7.

Se vuelve a ejecutar la aplicación [InitDB]. Los mensajes de la consola pasan a ser los siguientes:

Hibernate: 
    delete 
    from
        jpa01_personne
Hibernate: 
    insert 
    into
        jpa01_personne
        (VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
    values
        (?, ?, ?, ?, ?, ?)
Hibernate: 
    insert 
    into
        jpa01_personne
        (VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
    values
        (?, ?, ?, ?, ?, ?)
[personnes]
Hibernate: 
    select
        personne0_.ID as ID0_,
        personne0_.VERSION as VERSION0_,
        personne0_.NOM as NOM0_,
        personne0_.PRENOM as PRENOM0_,
        personne0_.DATENAISSANCE as DATENAIS5_0_,
        personne0_.MARIE as MARIE0_,
        personne0_.NBENFANTS as NBENFANTS0_ 
    from
        jpa01_personne personne0_ 
    order by
        personne0_.NOM asc
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]
terminé ...
  • líneas 2-4: la orden SQL delete derivada de la instrucción:

        // eliminar elementos de la tabla de personas
        em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
  • líneas 5-18: las órdenes SQL insert derivadas de las instrucciones:

        // persistencia de personas
        em.persist(p1);
        em.persist(p2);
  • líneas 21-32: la orden «select» SQL derivada de la instrucción:

        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) 

Si se realizan salidas intermedias a la consola, se observará que el registro de los logs SQL de una instrucción I del código Java se realiza cuando se ejecuta dicha instrucción I. Esto no significa que la orden SQL que se muestra se ejecute en la base de datos en ese momento. De hecho, se almacena en caché para ejecutarse durante la próxima sincronización del contexto de persistencia con la base de datos.

Se pueden obtener otros registros a través del archivo [src/log4j.properties]:

  • en [1], el archivo [log4j.properties] es procesado por el archivo [log4j-1.2.13.jar] [2] de la herramienta llamada LOG4j (Logs for Java), disponible en la URL [http://logging.apache.org/log4j/docs/index.html]. Al estar ubicado en la carpeta [src] del proyecto Eclipse, sabemos que [log4j.properties] se copiará automáticamente a la carpeta [bin] del proyecto [3]. Una vez hecho esto, ahora se encuentra en el archivo classpath del proyecto y es de ahí de donde el archivo [2] lo obtendrá.

El archivo [log4j.properties] nos permite supervisar ciertos registros de Hibernate. En ejecuciones anteriores, su contenido era el siguiente:


# Dirigir los mensajes de registro a stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

# Opción de registro de raíz
log4j.rootLogger=ERROR, stdout

# Opciones de registro de hibernación (INFO solo muestra mensajes de inicio)
#log4j.logger.org.hibernate=INFO

# Registrar los argumentos en tiempo de ejecución de los parámetros de enlace de JDBC
#log4j.logger.org.hibernate.type=DEBUG

No haré muchos comentarios sobre esta configuración, ya que nunca me he tomado el tiempo de informarme a fondo sobre LOG4j.

  • Las líneas 1 a 8 se encuentran en todos los archivos log4j.properties con los que me he topado
  • Las líneas 10 a 14 están presentes en los archivos log4j.properties de los ejemplos de Hibernate.
  • Línea 11: controla los registros generales de Hibernate. Como la línea está comentada, estos registros están desactivados aquí. Pueden existir varios niveles de registros: INFO (información general sobre lo que hace Hibernate), WARN (Hibernate nos advierte de un posible problema), DEBUG (registros detallados). El nivel INFO es el menos detallado, mientras que el modo DEBUG es el más detallado. Al activar la línea 11, se puede saber qué está haciendo Hibernate, especialmente al iniciar la aplicación. Esto suele ser interesante.
  • La línea 12, si está activa, permite conocer los argumentos que se utilizan efectivamente al ejecutar las consultas SQL configuradas.

Comencemos por descomentar la línea 14


# Registro de los argumentos de tiempo de ejecución de los parámetros de enlace de JDBC
log4j.logger.org.hibernate.type=DEBUG

y volvamos a ejecutar [InitDB]. Los nuevos registros generados por esta modificación son los siguientes (vista parcial):

Hibernate: 
    insert 
    into
        jpa01_personne
        (VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
    values
        (?, ?, ?, ?, ?, ?)
07:20:03,843 DEBUG IntegerType:80 - binding '0' to parameter: 1
07:20:03,843 DEBUG StringType:80 - binding 'Durant' to parameter: 2
07:20:03,843 DEBUG StringType:80 - binding 'Sylvie' to parameter: 3
07:20:03,843 DEBUG DateType:80 - binding '05 juillet 2001' to parameter: 4
07:20:03,843 DEBUG BooleanType:80 - binding 'false' to parameter: 5
07:20:03,843 DEBUG IntegerType:80 - binding '0' to parameter: 6
  • Las líneas 8 a 10 son nuevos registros generados por la activación de la línea 14 de [log4j.properties]. Indican los 5 valores asignados a los parámetros formales ? de la consulta parametrizada de las líneas 2 a 7. Así, vemos que la columna VERSION recibirá el valor 0 (línea 8).

Ahora activemos la línea 11 de [log4j.properties]:

# Opciones de registro de Hibernate (INFO solo muestra mensajes de inicio)
log4j.logger.org.hibernate=INFO

y volvamos a ejecutar [InitDB]:

07:50:23,937  INFO Version:15 - Hibernate EntityManager 3.2.0.CR3
07:50:23,968  INFO Version:15 - Hibernate Annotations 3.2.0.CR3
07:50:23,984  INFO Environment:500 - Hibernate 3.2.0.cr5
07:50:23,984  INFO Environment:533 - hibernate.properties not found
07:50:23,984  INFO Environment:667 - Bytecode provider name : cglib
07:50:24,000  INFO Environment:584 - using JDK 1.4 java.sql.Timestamp handling
07:50:24,375  INFO AnnotationBinder:387 - Binding entity from annotated class: entites.Personne
07:50:24,421  INFO EntityBinder:340 - Bind entity entites.Personne on table jpa01_personne
07:50:24,609  INFO C3P0ConnectionProvider:50 - C3P0 using driver: com.mysql.jdbc.Driver at URL: jdbc:mysql://localhost:3306/jpa
07:50:24,609  INFO C3P0ConnectionProvider:51 - Connection properties: {user=jpa, password=****, autocommit=true, release_mode=auto}
07:50:24,609  INFO C3P0ConnectionProvider:54 - autocommit mode: true
07:50:25,296  INFO SettingsFactory:81 - RDBMS: MySQL, version: 5.0.37-community-nt
07:50:25,296  INFO SettingsFactory:82 - JDBC driver: MySQL-AB JDBC Driver, version: mysql-connector-java-3.1.9 ( $Date: 2005/05/19 15:52:23 $, $Revision: 1.1.2.2 $ )
07:50:25,312  INFO Dialect:141 - Using dialect: org.hibernate.dialect.MySQL5InnoDBDialect
07:50:25,312  INFO TransactionFactoryFactory:34 - Transaction strategy: org.hibernate.transaction.JDBCTransactionFactory
07:50:25,312  INFO TransactionManagerLookupFactory:33 - No TransactionManagerLookup configured (in JTA environment, use of read-write or transactional second-level cache is not recommended)
07:50:25,328  INFO SettingsFactory:134 - Automatic flush during beforeCompletion(): disabled
07:50:25,328  INFO SettingsFactory:138 - Automatic session close at end of transaction: disabled
07:50:25,328  INFO SettingsFactory:145 - JDBC batch size: 15
07:50:25,328  INFO SettingsFactory:148 - JDBC batch updates for versioned data: disabled
07:50:25,328  INFO SettingsFactory:153 - Scrollable result sets: enabled
07:50:25,328  INFO SettingsFactory:161 - JDBC3 getGeneratedKeys(): enabled
07:50:25,328  INFO SettingsFactory:169 - Connection release mode: auto
07:50:25,328  INFO SettingsFactory:193 - Maximum outer join fetch depth: 2
07:50:25,328  INFO SettingsFactory:196 - Default batch fetch size: 1
07:50:25,328  INFO SettingsFactory:200 - Generate SQL with comments: disabled
07:50:25,328  INFO SettingsFactory:204 - Order SQL updates by primary key: disabled
07:50:25,328  INFO SettingsFactory:369 - Query translator: org.hibernate.hql.ast.ASTQueryTranslatorFactory
07:50:25,328  INFO ASTQueryTranslatorFactory:24 - Using ASTQueryTranslatorFactory
07:50:25,328  INFO SettingsFactory:212 - Query language substitutions: {}
07:50:25,328  INFO SettingsFactory:217 - JPA-QL strict compliance: enabled
07:50:25,328  INFO SettingsFactory:222 - Second-level cache: enabled
07:50:25,328  INFO SettingsFactory:226 - Query cache: disabled
07:50:25,328  INFO SettingsFactory:356 - Cache provider: org.hibernate.cache.NoCacheProvider
07:50:25,328  INFO SettingsFactory:241 - Optimize cache for minimal puts: disabled
07:50:25,328  INFO SettingsFactory:250 - Structured second-level cache entries: disabled
07:50:25,343  INFO SettingsFactory:270 - Echoing all SQL to stdout
07:50:25,343  INFO SettingsFactory:277 - Statistics: disabled
07:50:25,343  INFO SettingsFactory:281 - Deleted entity synthetic identifier rollback: disabled
07:50:25,343  INFO SettingsFactory:296 - Default entity-mode: pojo
07:50:25,468  INFO SessionFactoryImpl:161 - building session factory
07:50:25,750  INFO SessionFactoryObjectFactory:82 - Not binding factory to JNDI, no JNDI name configured
07:50:25,765  INFO SchemaExport:154 - Running hbm2ddl schema export
07:50:25,765  INFO SchemaExport:179 - exporting generated schema to database
07:50:25,968  INFO SchemaExport:196 - schema export complete
Hibernate: 
    delete 
    from
        jpa01_personne
Hibernate: 
    ... 

La lectura de estos registros nos brinda mucha información interesante:

  • línea 7: Hibernate indica el nombre de una clase @Entity que ha encontrado
  • línea 8: indica que la clase [Personne] se vinculará a la tabla [jpa01_personne]
  • línea 9: indica el grupo de conexiones C3P0 que se utilizará, el nombre del controlador JDBC y la URL de la base de datos que se va a administrar
  • línea 10: proporciona otras características de la conexión JDBC: propietario, tipo de confirmación, etc.
  • línea 14: el dialecto utilizado para comunicarse con SGBD
  • línea 15: el tipo de transacción utilizada. JDBCTransactionFactory indica que la aplicación administra sus propias transacciones. No se ejecuta en un contenedor EJB3 que proporcionaría su propio servicio de transacciones.
  • Las siguientes líneas se refieren a opciones de configuración de Hibernate con las que no nos hemos encontrado. Se invita al lector interesado a consultar la documentación de Hibernate.
  • Línea 37: las órdenes SQL se mostrarán en la consola. Esto se solicitó en [persistence.xml]:

            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.format_sql" value="true" />
            <property name="use_sql_comments" value="true" />
  • líneas 43-45: el esquema de la base de datos se exporta a SGBD y c.a.d. La base de datos se vacía y luego se vuelve a crear. Este mecanismo se deriva de la configuración realizada en [persistence.xml] (línea 4 a continuación):

            ...
            <property name="hibernate.connection.password" value="jpa" />
            <!-- Creación automática del esquema -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- Dialecto -->
            ...

Cuando una aplicación «se cuelga» con una excepción de Hibernate que no se comprende, lo primero que se debe hacer es activar los registros de Hibernate en modo DEBUG en [log4j.properties] para tener una visión más clara:


# Opción de registrador raíz
log4j.rootLogger=ERROR, stdout

# Opciones de registro de Hibernate (INFO solo muestra mensajes de inicio)
log4j.logger.org.hibernate=DEBUG

En el resto de este documento, los registros están desactivados por defecto para que la visualización de la consola sea más legible.

2.1.12. Descubrir el lenguaje JPQL / HQL con la consola de Hibernate

Nota: Esta sección requiere el complemento Hibernate Tools (párrafo 5.2.5).

En el código de la aplicación [InitDB], utilizamos una consulta JPQL. JPQL (Java Persistence Query Language) es un lenguaje para realizar consultas al contexto de persistencia. La consulta encontrada fue la siguiente:

select p from Personne p order by p.nom asc

Esta seleccionaba todos los elementos de la tabla asociada a la @Entity [Personne] y los devolvía en orden ascendente por el nombre. En la consulta anterior, p.nom es el campo «nombre» de una instancia p de la clase [Personne]. Por lo tanto, una consulta JPQL opera sobre los objetos @Entity del contexto de persistencia y no directamente sobre las tablas de la base de datos. La capa JPA, por su parte, traducirá esta consulta JPQL en una consulta SQL adecuada para el SGBD con el que está trabajando. Así, en el caso de una implementación de JPA / Hibernate vinculada a un SGBD MySQL5, la consulta anterior JPQL se traduce en la siguiente consulta SQL:

select
  personne0_.ID as ID0_,
  personne0_.VERSION as VERSION0_,
  personne0_.NOM as NOM0_,
  personne0_.PRENOM as PRENOM0_,
  personne0_.DATENAISSANCE as DATENAIS5_0_,
  personne0_.MARIE as MARIE0_,
  personne0_.NBENFANTS as NBENFANTS0_ 
 from
  jpa01_personne personne0_ 
 order by
  personne0_.NOM asc

La capa JPA utilizó la configuración del objeto @Entity [Personne] para generar el pedido SQL correcto. Aquí se implementó el puente objeto-relacional.

El complemento [Hibernate Tools] (párrafo 5.2.5) ofrece una herramienta llamada «Consola de Hibernate» que permite

  • emitir órdenes JPQL o del superconjunto HQL (Hibernate Query Language) en el contexto de persistencia
  • obtener los resultados
  • conocer el equivalente SQL que se ejecutó en la base de datos

La consola de Hibernate es una herramienta de gran valor para aprender el lenguaje JPQL y familiarizarse con el puente JPQL / SQL. Se sabe que JPA se inspiró en gran medida en herramientas como Hibernate o Toplink. JPQL es muy similar al lenguaje HQL de Hibernate, pero no incluye todas sus funcionalidades. En la consola de Hibernate, se pueden emitir comandos HQL que se ejecutarán normalmente en la consola, pero que no forman parte del lenguaje JPQL y, por lo tanto, no se podrían utilizar en un cliente JPA. Cuando sea así, lo señalaremos.

Creemos una consola de Hibernate para nuestro proyecto actual de Eclipse:

  • [1]: cambiamos a una perspectiva [Hibernate Console] (Ventana / Abrir perspectiva / Otra)
  • [2]: creamos una nueva configuración en la ventana [Hibernate Configuration]
  • con el botón [4], seleccionamos el proyecto Java para el que se crea la configuración de Hibernate. Su nombre aparece en [3].
  • En [5], le damos el nombre que queramos a esta configuración. Aquí, hemos utilizado [3].
  • En [6], indicamos que utilizamos una configuración JPA para que la herramienta sepa que debe procesar el archivo [META-INF/persistence.xml]
  • en [7]: indicamos que en este archivo [META-INF/persistence.xml] se debe utilizar la unidad de persistencia llamada jpa.
  • En [8], validamos la configuración.

A continuación, se debe ejecutar el SGBD. En este caso, se trata del MySQL5.

  • en [1]: la configuración creada presenta un árbol de tres ramas
  • en [2]: la rama [Configuration] enumera los objetos que la consola utilizó para configurarse: en este caso, la @Entity Personne.
  • En [3]: la Session Factory es un concepto de Hibernate similar al de EntityManager de JPA. Establece el puente entre objetos y relaciones gracias a los objetos de la rama [Configuration]. En [3] se presentan los objetos del contexto de persistencia, en este caso nuevamente la @Entity Personne.
  • En [4]: la base de datos a la que se accede mediante la configuración que se encuentra en [persistence.xml]. En ella se encuentra la tabla [jpa01_personne].
  • en [1], se crea un editor HQL
  • en el editor HQL,
    • en [2], se elige la configuración de Hibernate que se utilizará si hay varias
    • en [3], se escribe el comando JPQL que se desea ejecutar
    • en [4], lo ejecutamos
  • en [5], se obtienen los resultados de la consulta en la ventana [Hibernate Query Result]. Aquí pueden surgir dos problemas:
    • no se obtiene nada (ninguna fila). La consola de Hibernate utilizó el contenido de [persistence.xml] para crear una conexión con SGBD. Sin embargo, esta configuración tiene una propiedad que indica que se vacíe la base de datos:

            <property name="hibernate.hbm2ddl.auto" value="create" />

Por lo tanto, es necesario volver a ejecutar la aplicación [InitDB] antes de volver a ejecutar el comando JPQL mencionado anteriormente.

  • (continuación)
    • No aparece la ventana [Hibernate Query Result]. Se solicita mediante [Window / Show View / ...]

La ventana [Hibernate Dynamic SQL preview] ([1] a continuación) permite ver la consulta SQL que se ejecutará para llevar a cabo el comando JPQL que estamos escribiendo. Tan pronto como la sintaxis del comando JPQL sea correcta, el comando SQL correspondiente aparecerá en esta ventana:

  • en [2], se borra el comando anterior HQL
  • en [3], se ejecuta uno nuevo
  • en [4], el resultado
  • en [5], el comando SQL que se ejecutó en base a

El editor HQL ofrece ayuda para escribir los comandos HQL:

  • en [1]: una vez que el editor sabe que p es un objeto Personne, puede sugerirnos los campos de p mientras escribimos.
  • en [2]: un comando HQL incorrecto. Se debe escribir where p.marie=true.
  • En [3]: el error se muestra en la ventana [SQL Preview]

Invitamos al lector a ejecutar otros comandos HQL / JPQL en la base.

2.1.13. Un segundo cliente JPA

Volvamos a la perspectiva Java del proyecto:

 
  • [InitDB.java] es un programa que insertaba algunas filas en la tabla [jpa01_personne] de la base de datos. El análisis de su código nos permitió obtener los primeros elementos de API y JPA.
  • [Main.java] es un programa que realiza las operaciones de CRUD en la tabla [jpa01_personne]. El análisis de su código nos permitirá repasar los conceptos fundamentales del contexto de persistencia y del ciclo de vida de los objetos de este contexto.

2.1.13.1. La estructura del código

[Main.java] encadenará una serie de pruebas, cada una de las cuales tiene como objetivo mostrar una faceta particular de JPA:

 

El método [main]

  • llama sucesivamente a los métodos test1 a test11. Presentaremos por separado el código de cada uno de estos métodos.
  • Además, utiliza métodos auxiliares privados: clean, dump, log, getEntityManager y getNewEntityManager.

Presentamos el método main y los métodos denominados utilitarios:


package tests;

...
import entites.Personne;

@SuppressWarnings("unchecked")
public class Main {

    //  constantes
    private final static String TABLE_NAME = "jpa01_personne";

    // Contexto de persistencia
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
    private static EntityManager em = null;

    // Objetos compartidos
    private static Personne p1, p2, newp1;

    public static void main(String[] args) throws Exception {
        // limpieza de la base de datos
        log("clean");clean();

        // volcado de tabla
        dump();

        // prueba1
        log("test1");test1();

...
        // prueba11
        log("test11");test11();

        // fin del contexto de persistencia
        if (em.isOpen())
            em.close();

        // cierre de EntityManagerFactory
        emf.close();
    }

    // recuperar el EntityManager actual
    private static EntityManager getEntityManager() {
        if (em == null || !em.isOpen()) {
            em = emf.createEntityManager();
        }
        return em;
    }

    // obtener un nuevo EntityManager
    private static EntityManager getNewEntityManager() {
        if (em != null && em.isOpen()) {
            em.close();
        }
        em = emf.createEntityManager();
        return em;
    }

    // mostrar el contenido de la tabla
    private static void dump() {
        // contexto de persistencia actual
        EntityManager em = getEntityManager();
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // visualización de personas
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // fin de transacción
        tx.commit();
    }

    // borrar BD
    private static void clean() {
        // contexto de persistencia
        EntityManager em = getEntityManager();
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // eliminar los elementos de la tabla PERSONNES
        em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
        // fin de transacción
        tx.commit();
    }

    // registros
    private static void log(String message) {
        System.out.println("main : ----------- " + message);
    }

    // creación de objetos
    public static void test1() throws ParseException {
...
    }

    // modificar un objeto del contexto
    public static void test2() {
...
    }

    // solicitar objetos
    public static void test3() {
...
    }

    // eliminar un objeto que pertenece al contexto de persistencia
    public static void test4() {
....
    }

    // desvincular, volver a vincular y modificar
    public static void test5() {
...
    }

    // eliminar un objeto que no pertenece al contexto de persistencia
    public static void test6() {
...
    }

    // modificar un objeto que no pertenece al contexto de persistencia
    public static void test7() {
...
    }

    // vuelva a vincular un objeto al contexto de persistencia
    public static void test8() {
...
    }

    // una consulta SELECT provoca una sincronización
    // de la base de datos con el contexto de persistencia
    public static void test9() {
....
    }

    // control de versiones (bloqueo optimista)
    public static void test10() {
...
    }

    // reversión de una transacción
    public static void test11() throws ParseException {
...
    }

}
  • línea 13: el objeto EntityManagerFactory emf construido a partir de la unidad de persistencia jpa definida en [persistence.xml]. Nos permitirá crear, a lo largo de la aplicación, diversos contextos de persistencia.
  • línea 14: un contexto de persistencia EntityManager que aún no se ha inicializado
  • línea 17: tres objetos [Personne] compartidos por las pruebas
  • línea 21: la tabla jpa01_personne se vacía y luego se muestra en la línea 24 para asegurarnos de que partimos de una tabla vacía.
  • líneas 27-31: secuencia de pruebas
  • líneas 34-35: cierre del contexto de persistencia si estaba abierto.
  • línea 38: cierre del objeto EntityManagerFactory emf.
  • líneas 42-47: el método [getEntityManager] establece el EntityManager (o contexto de persistencia) como el actual o como uno nuevo si no existe (líneas 43-44).
  • líneas 50-56: el método [getNewEntityManager] crea un nuevo contexto de persistencia. Si ya existía uno, se cierra (líneas 51-52)
  • líneas 59-72: el método [dump] muestra el contenido de la tabla [jpa01_personne]. Este código ya se ha encontrado en [InitDB].
  • líneas 75-85: el método [clean] vacía la tabla [jpa01_personne]. Este código ya se ha encontrado en [InitDB].
  • líneas 88-90: el método [log] muestra en la consola el mensaje que se le pasa como parámetro para que se note.

Ahora podemos pasar al análisis de las pruebas.

2.1.13.2. Prueba 1

El código de la prueba 1 es el siguiente:


// creación de objetos
    public static void test1() throws ParseException {
        // contexto de persistencia
        EntityManager em = getEntityManager();
        // creación de personas
        p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // persistencia de personas
        em.persist(p1);
        em.persist(p2);
        // fin de transacción
        tx.commit();
        // se muestra la tabla
        dump();

}

Este código ya se ha visto en [InitDB]: crea dos personas y las coloca en el contexto de persistencia.

  • línea 4: se solicita el contexto de persistencia actual
  • líneas 6-7: se crean las dos personas
  • líneas 9-15: las dos personas se colocan en el contexto de persistencia dentro de una transacción.
  • línea 15: debido al commit de la transacción, se sincroniza el contexto de persistencia con la base de datos. Las dos personas se agregarán a la tabla [jpa01_personne].
  • línea 17: se muestra la tabla

La salida de la consola de esta primera prueba es la siguiente:

main : ----------- test1
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]

2.1.13.3. Prueba 2

El código de la prueba 2 es el siguiente:


// modificar un objeto del contexto
    public static void test2() {
        // contexto de persistencia
        EntityManager em = getEntityManager();
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // se incrementa el número de hijos de p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // se modifica su estado civil
        p1.setMarie(false);
        // el objeto p1 se guarda automáticamente (verificación de cambios)
        // durante la próxima sincronización (commit o select)
        // fin de la transacción
        tx.commit();
        // se muestra la nueva tabla
        dump();
    }
  • El objetivo de la prueba 2 es modificar un objeto del contexto de persistencia y, a continuación, mostrar el contenido de la tabla para verificar si se ha realizado la modificación
  • línea 4: se recupera el contexto de persistencia actual
  • líneas 6-7: las operaciones se realizarán dentro de una transacción
  • líneas 9 y 11: se modifica el número de hijos de la persona p1, así como su estado civil
  • línea 15: fin de la transacción, por lo que se sincroniza el contexto de persistencia con la base de datos
  • línea 17: visualización de la tabla

La salida a consola de la prueba 2 es la siguiente:

1
2
3
4
5
6
7
8
main : ----------- test1
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]
main : ----------- test2
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,1,Martin,Paul,31/01/2000,false,3]
  • línea 4: la persona p1 antes de la modificación
  • línea 8: la persona p1 después de la modificación. Cabe señalar que su número de versión ha pasado a 1. Este se incrementa en 1 con cada actualización de la línea.

2.1.13.4. Prueba 3

El código de la prueba 3 es el siguiente:


    // solicitar objetos
    public static void test3() {
        // contexto de persistencia
        EntityManager em = getEntityManager();
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // se solicita la persona p1
        Personne p1b = em.find(Personne.class, p1.getId());
        // como p1 ya se encuentra en el contexto de persistencia, no se ha realizado ningún acceso a la base de datos
        // p1b y p1 son las mismas referencias
        System.out.format("p1==p1b ? %s%n", p1 == p1b);
        // al solicitar un objeto que no existe, se devuelve un puntero nulo
        Personne px = em.find(Personne.class, -4);
        System.out.format("px==null ? %s%n", px == null);
        // fin de la transacción
        tx.commit();
}
  • La prueba 3 se centra en el método [EntityManager.find], que permite buscar un objeto en la base de datos para colocarlo en el contexto de persistencia. A partir de ahora, ya no explicaremos la transacción que se lleva a cabo en todas las pruebas, salvo cuando se utilice de manera inusual.
  • Línea 9: se le solicita al contexto de persistencia la persona que tiene la misma clave primaria que la persona p1. Hay dos casos:
    • p1 ya se encuentra en el contexto de persistencia. Ese es el caso aquí. Por lo tanto, no se realiza ningún acceso a la base de datos. El método find simplemente devuelve una referencia al objeto persistido.
    • p1 no se encuentra en el contexto de persistencia. Por lo tanto, se realiza un acceso a la base de datos mediante la clave primaria proporcionada. La fila recuperada se coloca en el contexto de persistencia y find devuelve la referencia de este nuevo objeto persistido.
  • línea 12: se verifica que find ya haya devuelto la referencia del objeto p1 en el contexto
  • línea 14: se solicita un objeto que no existe ni en el contexto de persistencia ni en la base de datos. El método find devuelve entonces el puntero null. Esto se verifica en la línea 15.

La salida de consola de la prueba 3 es la siguiente:

1
2
3
main : ----------- test3
p1==p1b ? true
px==null ? true

2.1.13.5. Prueba 4

El código de la prueba 4 es el siguiente:


    // eliminar un objeto que pertenece al contexto de persistencia
    public static void test4() {
        // contexto de persistencia
        EntityManager em = getEntityManager();
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // se elimina el objeto persistente p2
        em.remove(p2);
        // fin de transacción
        tx.commit();
        // se muestra la nueva tabla
        dump();
}
  • La prueba 4 se centra en el método [EntityManager.remove], que permite eliminar un elemento del contexto de persistencia y, por lo tanto, de la base de datos.
  • línea 9: se elimina a la persona p2 del contexto de persistencia
  • línea 11: sincronización del contexto con la base de datos
  • línea 13: visualización de la tabla. Normalmente, la persona p2 ya no debería aparecer.

La salida de consola de la prueba 4 es la siguiente:

main : ----------- test1
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]
main : ----------- test2
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,1,Martin,Paul,31/01/2000,false,3]
main : ----------- test3
p1==p1b ? true
px==null ? true
main : ----------- test4
[personnes]
[1,1,Martin,Paul,31/01/2000,false,3]
  • línea 3: la persona p2 en test1
  • líneas 12-14: ya no existe al finalizar test4.

2.1.13.6. Prueba 5

El código de la prueba 5 es el siguiente:


// desvincular, volver a vincular y modificar
    public static void test5() {
        // nuevo contexto de persistencia
        EntityManager em = getNewEntityManager();
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // p1 desvinculado
        Personne oldp1=p1;
        // se vuelve a vincular p1 al nuevo contexto
        p1 = em.find(Personne.class, p1.getId());
        // verificación
        System.out.format("p1==oldp1 ? %s%n", p1 == oldp1);        
        // fin de transacción
        tx.commit();
        // se incrementa el número de hijos de p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // se muestra la nueva tabla
        dump();
    }
  • La prueba 5 se centra en el ciclo de vida de los objetos persistentes a lo largo de varios contextos de persistencia sucesivos. Hasta ahora, siempre habíamos utilizado el mismo contexto de persistencia en las diferentes pruebas.
  • Línea 4: se solicita un nuevo contexto de persistencia. El método [getNewEntityManager] cierra el anterior y abre uno nuevo. Esto tiene como consecuencia que los objetos p1 y p2 que posee la aplicación ya no se encuentran en un estado persistente. Pertenecían a un contexto que se ha cerrado. Se dice que están en un estado desvinculado. No pertenecen al nuevo contexto de persistencia.
  • líneas 6-7: inicio de la transacción. Aquí se utilizará de manera inusual.
  • línea 9: se anota la dirección del objeto p1, ahora desvinculado.
  • línea 11: se solicita al contexto de persistencia la persona p1 (utilizando la clave primaria de p1). Como el contexto es nuevo, la persona p1 no se encuentra en él. Por lo tanto, se realizará un acceso a la base de datos. El objeto recuperado se colocará en el nuevo contexto.
  • línea 13: se verifica que el objeto persistente p1 del contexto es diferente del objeto oldp1, que era el antiguo objeto p1 desvinculado.
  • línea 15: la transacción ha finalizado
  • línea 17: se modifica, fuera de la transacción, el nuevo objeto persistente p1. ¿Qué sucede en este caso? Queremos saberlo.
  • línea 19: se solicita la visualización de la tabla. Recordemos que, debido al select emitido por el método dump, se realiza automáticamente una sincronización del contexto de persistencia con la base de datos.

La salida en consola de la prueba 5 es la siguiente:

1
2
3
4
5
6
7
main : ----------- test4
[personnes]
[1,1,Martin,Paul,31/01/2000,false,3]
main : ----------- test5
p1==oldp1 ? false
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
  • línea 5: el método find sí accedió a la base de datos; de lo contrario, los dos punteros serían iguales
  • líneas 7 y 3: el número de hijos de p1 sí aumentó en 1. Por lo tanto, se tomó en cuenta la modificación, realizada fuera de una transacción. De hecho, esto depende del SGBD utilizado. En un SGBD, una orden SQL siempre se ejecuta dentro de una transacción. Si el cliente JPA no inicia por sí mismo una transacción explícita, entonces el SGBD iniciará una transacción implícita. Hay dos casos comunes:
    • 1 - Cada orden SQL individual forma parte de una transacción, que se abre antes de la orden y se cierra después. Se dice que estamos en modo autocommit. Por lo tanto, todo ocurre como si el cliente JPA realizara transacciones para cada orden SQL.
    • 2 - El SGBD no está en modo autocommit e inicia una transacción implícita con la primera orden SQL que el cliente JPA emite fuera de una transacción, y deja que el cliente la cierre. Todas las órdenes SQL emitidas por el cliente JPA pasan a formar parte de la transacción implícita. Esta puede finalizar por diferentes motivos: el cliente cierra la conexión, inicia una nueva transacción, etc.

Nos encontramos en una situación que depende de la configuración de SGBD. Por lo tanto, tenemos código no portátil. Más adelante mostraremos un código sin transacciones y veremos que no todos los SGBD tienen el mismo comportamiento con respecto a este código. Por lo tanto, consideraremos que trabajar sin transacciones es un error de programación.

  • Línea 7: cabe señalar que el número de versión ha pasado a 2.

2.1.13.7. Prueba 6

El código de la prueba 6 es el siguiente:


// eliminar un objeto que no pertenece al contexto de persistencia
    public static void test6() {
        // nuevo contexto de persistencia
        EntityManager em = getNewEntityManager();
        // Inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // se elimina p1, que no pertenece al nuevo contexto
        try {
            em.remove(p1);
            // fin de la transacción
            tx.commit();
        } catch (RuntimeException e1) {
            System.out.format("Erreur à la suppression de p1 : [%s,%s]%n", e1.getClass().getName(), e1.getMessage());
            // se revierte la transacción
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s,%s]%n", e2.getClass().getName(), e2.getMessage());
            }
        }
        // se muestra la nueva tabla
        dump();
    }
  • La prueba 6 busca eliminar un objeto que no pertenece al contexto de persistencia.
  • línea 4: se solicita un nuevo contexto de persistencia. Por lo tanto, el anterior se cierra y los objetos que contenía quedan desvinculados. Este es el caso del objeto p1 de la prueba 5 anterior.
  • líneas 6-7: inicio de la transacción.
  • línea 10: se elimina el objeto desvinculado p1. Sabemos que esto provocará una excepción, por lo que hemos rodeado la operación con un try/catch.
  • línea 12: no se llevará a cabo el commit.
  • líneas 16-21: una transacción debe terminar con un commit (todas las operaciones de la transacción se validan) o un rollback (todas las operaciones de la transacción se cancelan). Se produjo una excepción, por lo que se ejecuta un rollback en la transacción. No hay nada que revertir, ya que la única operación de la transacción falló, pero el rollback da por terminada la transacción. Es la primera vez que utilizamos la operación [EntityTransaction].rollback. Deberíamos haberlo hecho desde los primeros ejemplos. No lo hicimos para mantener el código sencillo. No obstante, el lector debe tener presente que el caso del rollback de la transacción siempre debe preverse en el código.
  • Línea 24: se muestra la tabla. Normalmente, no debería haber cambiado.

La salida a la consola de la prueba 6 es la siguiente:

1
2
3
4
5
6
7
8
main : ----------- test5
p1==oldp1 ? false
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test6
Erreur à la suppression de p1 : [java.lang.IllegalArgumentException,Removing a detached instance entites.Personne#1]
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
  • línea 6: la eliminación de p1 falló. El mensaje de la excepción explica que se intentó eliminar un objeto desvinculado, es decir, que no forma parte del contexto. Esto no es posible.
  • línea 8: la persona p1 sigue ahí.

2.1.13.8. Prueba 7

El código de la prueba 7 es el siguiente:


// modificar un objeto que no pertenece al contexto de persistencia
    public static void test7() {
        // nuevo contexto de persistencia
        EntityManager em = getNewEntityManager();
        // Inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // se incrementa el número de hijos de p1 que no pertenecen al nuevo contexto
        p1.setNbenfants(p1.getNbenfants() + 1);
        // fin de la transacción
        tx.commit();
        // se muestra la nueva tabla; no debería haber cambiado
        dump();
    }
  • La prueba 7 busca modificar un objeto que no pertenece al contexto de persistencia y ver el impacto que esto tiene en la base de datos. Es de suponer que no tiene ninguno. Eso es lo que muestran los resultados de la prueba.
  • línea 4: se solicita un nuevo contexto de persistencia. Por lo tanto, tenemos un contexto nuevo sin objetos persistidos en él.
  • Líneas 6-7: inicio de la transacción.
  • línea 9: se modifica el objeto desvinculado p1. Es una operación que no involucra al contexto de persistencia em. Por lo tanto, no cabe esperar una excepción ni nada por el estilo. Es una operación básica sobre un POJO.
  • línea 11: el commit provoca la sincronización del contexto con la base de datos. Este contexto está vacío. Por lo tanto, la base de datos no se modifica.
  • línea 24: se muestra la tabla. Normalmente, no debería haber cambiado.

La salida de consola de la prueba 7 es la siguiente:

1
2
3
4
5
6
7
main : ----------- test6
Erreur à la suppression de p1 : [java.lang.IllegalArgumentException,Removing a detached instance entites.Personne#1]
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test7
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
  • línea 7: la persona p1 no ha cambiado en la base de datos. Para la siguiente prueba, recordaremos, sin embargo, que en memoria su número de hijos es ahora de 5.

2.1.13.9. Prueba 8

El código de la prueba 8 es el siguiente:


    // Volver a vincular un objeto al contexto de persistencia
    public static void test8() {
        // nuevo contexto de persistencia
        EntityManager em = getNewEntityManager();
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // Se vuelve a vincular el objeto desvinculado p1 al nuevo contexto
        newp1 = em.merge(p1);
        // Ahora es newp1 el que forma parte del contexto, no p1
        // fin de la transacción
        tx.commit();
        // se muestra la nueva tabla; el número de hijos de p1 debe haber cambiado
        dump();
}
  • La prueba 8 vuelve a vincular al contexto de persistencia un objeto desvinculado.
  • línea 4: se solicita un nuevo contexto de persistencia. Por lo tanto, tenemos un contexto nuevo sin objetos persistentes en su interior.
  • Líneas 6-7: inicio de la transacción.
  • línea 9: se vuelve a vincular al contexto de persistencia el objeto desvinculado p1. La operación merge puede implicar varias situaciones:
    • caso 1: en el contexto de persistencia existe un objeto persistente ps1 que tiene la misma clave primaria que el objeto desvinculado p1. El contenido de p1 se copia en ps1 y merge pasa a ser la referencia de ps1.
    • Caso 2: en el contexto de persistencia no existe un objeto persistente ps1 que tenga la misma clave primaria que el objeto desvinculado p1. Entonces se consulta la base de datos para determinar si el objeto buscado existe en ella. Si es así, dicho objeto se incorpora al contexto de persistencia, se convierte en el objeto persistente ps1 y se vuelve al caso 1 anterior.
    • Caso 3: no existe, ni en el contexto de persistencia ni en la base de datos, un objeto con la misma clave primaria que el objeto desvinculado p1. Entonces se crea un nuevo objeto [Personne] (new), el cual se coloca en el contexto de persistencia. A continuación, se vuelve al caso 1.
    • En resumen: el objeto desvinculado p1 permanece desvinculado. La operación merge devuelve una referencia (en este caso, newp1) al objeto persistente ps1 derivado de merge. La aplicación cliente debe trabajar ahora con el objeto persistente ps1 y no con el objeto desvinculado p1.
    • Cabe señalar una diferencia entre los casos 1 y 3 en cuanto a la orden SQL programada para el merge: en los casos 1 y 2, se trata de la orden UPDATE, mientras que en el caso 3, es una orden INSERT.
  • línea 12: el commit provoca la sincronización del contexto con la base de datos. Este contexto ya no está vacío. Contiene el objeto newp1. Este se guardará en la base de datos.
  • línea 24: se muestra la tabla para verificarlo.

La salida de la consola de la prueba 8 es la siguiente:

main : ----------- test6
Erreur à la suppression de p1 : [java.lang.IllegalArgumentException,Removing a detached instance entites.Personne#1]
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test7
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test8
[personnes]
[1,3,Martin,Paul,31/01/2000,false,5]
  • El número de hijos de p1 era 4 en la prueba 6 (línea 4), luego pasó a ser 5 en la prueba 7, pero no se guardó en la base de datos (línea 7). Después de merge, newp1 se guardó en la base de datos: en la línea 10, se observan 5 hijos.
  • línea 10: el número de versión de newp1 pasó a 3.

2.1.13.10. Prueba 9

El código de la prueba 9 es el siguiente:


// una consulta SELECT provoca una sincronización
    // de la base de datos con el contexto de persistencia
    public static void test9() {
        // contexto de persistencia
        EntityManager em = getEntityManager();
        // Inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // se incrementa el número de hijos de newp1
        newp1.setNbenfants(newp1.getNbenfants() + 1);
        // visualización de personas: el número de hijos de newp1 debe haber cambiado
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // fin de la transacción
        tx.commit();
    }
  • La prueba 9 tiene como objetivo mostrar el mecanismo de sincronización del contexto que se produce automáticamente antes de un select.
  • Línea 5: no se cambia el contexto de persistencia. Por lo tanto, newp1 está dentro de él.
  • Líneas 7-8: inicio de la transacción.
  • línea 10: el número de hijos del objeto persistente newp1 se incrementa en 1 (5 → 6).
  • líneas 12-15: se muestra la tabla mediante un select. El contexto se sincronizará con la base de datos antes de la ejecución de select.
  • línea 17: fin de la transacción

Para ver la sincronización, activamos la visualización de los registros de Hibernate en modo DEBUG (log4j.properties):


# Opción de registro raíz
log4j.rootLogger=ERROR, stdout

# Opciones de registro de Hibernate (INFO solo muestra mensajes de inicio)
log4j.logger.org.hibernate=DEBUG

La salida de la consola de la prueba 9 es la siguiente:

main : ----------- prueba9
14:27:27,250 DEBUG JDBCTransaction:54 - begin
14:27:27,250 DEBUG ConnectionManager:415 - opening JDBC connection
14:27:27,250 DEBUG JDBCTransaction:59 - current autocommit status: true
14:27:27,250 DEBUG JDBCTransaction:62 - disabling autocommit
14:27:27,250 DEBUG JDBCContext:210 - after transaction begin
[personnes]
14:27:27,250 DEBUG QueryPlanCache:76 - located HQL query plan in cache (select p from Personne p order by p.nom asc)
14:27:27,250 DEBUG AbstractFlushingEventListener:58 - flushing session
...
14:27:27,250 DEBUG AbstractEntityPersister:3116 - entites.Personne.nbenfants is dirty
14:27:27,250 DEBUG DefaultFlushEntityEventListener:229 - Updating entity: [entites.Personne#1]
14:27:27,250 DEBUG Versioning:27 - Incrementing: 3 to 4
...
14:27:27,250 DEBUG AbstractFlushingEventListener:85 - Flushed: 0 insertions, 1 updates, 0 deletions to 1 objects
...
14:27:27,250 DEBUG ConnectionManager:463 - registering flush begin
14:27:27,250 DEBUG AbstractEntityPersister:2274 - Updating entity: [entites.Personne#1]
14:27:27,265 DEBUG AbstractEntityPersister:2276 - Existing version: 3 -> New version: 4
14:27:27,265 DEBUG AbstractBatcher:358 - about to open PreparedStatement (open PreparedStatements: 0, globally: 0)
14:27:27,265 DEBUG SQL:393 - update jpa01_personne set VERSION=?, NOM=?, PRENOM=?, DATENAISSANCE=?, MARIE=?, NBENFANTS=? where ID=? and VERSION=?
14:27:27,265 DEBUG AbstractBatcher:476 - preparing statement
14:27:27,265 DEBUG AbstractEntityPersister:1927 - Dehydrating entity: [entites.Personne#1]
14:27:27,265 DEBUG IntegerType:80 - binding '4' to parameter: 1
14:27:27,265 DEBUG StringType:80 - binding 'Martin' to parameter: 2
14:27:27,265 DEBUG StringType:80 - binding 'Paul' to parameter: 3
14:27:27,265 DEBUG DateType:80 - binding '31 janvier 2000' to parameter: 4
14:27:27,265 DEBUG BooleanType:80 - binding 'false' to parameter: 5
14:27:27,265 DEBUG IntegerType:80 - binding '6' to parameter: 6
14:27:27,265 DEBUG IntegerType:80 - binding '1' to parameter: 7
14:27:27,265 DEBUG IntegerType:80 - binding '3' to parameter: 8
14:27:27,265 DEBUG AbstractBatcher:366 - about to close PreparedStatement (open PreparedStatements: 1, globally: 1)
14:27:27,265 DEBUG AbstractBatcher:525 - closing statement
14:27:27,265 DEBUG ConnectionManager:472 - registering flush end
14:27:27,265 DEBUG HQLQueryPlan:150 - find: select p from Personne p order by p.nom asc
14:27:27,265 DEBUG QueryParameters:277 - named parameters: {}
14:27:27,265 DEBUG AbstractBatcher:358 - about to open PreparedStatement (open PreparedStatements: 0, globally: 0)
14:27:27,265 DEBUG SQL:393 - select personne0_.ID as ID0_, personne0_.VERSION as VERSION0_, personne0_.NOM as NOM0_, personne0_.PRENOM as PRENOM0_, personne0_.DATENAISSANCE as DATENAIS5_0_, personne0_.MARIE as MARIE0_, personne0_.NBENFANTS as NBENFANTS0_ from jpa01_personne personne0_ order by personne0_.NOM asc
...
14:27:27,265 DEBUG Loader:1164 - result row: EntityKey[entites.Personne#1]
...
14:27:27,265 DEBUG Loader:839 - total objects hydrated: 0
14:27:27,265 DEBUG StatefulPersistenceContext:748 - initializing non-lazy collections
[1,4,Martin,Paul,31/01/2000,false,6]
14:27:27,265 DEBUG JDBCTransaction:103 - commit
14:27:27,265 DEBUG SessionImpl:337 - automatically flushing session
...
14:27:27,265 DEBUG AbstractFlushingEventListener:91 - Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections
...
14:27:27,296 DEBUG JDBCTransaction:116 - committed JDBC Connection
...
  • línea 1: comienza la prueba 9
  • líneas 2-6: comienza la transacción JDBC. El modo autocommit de SGBD está desactivado (línea 5)
  • línea 7: salida provocada por la línea 12 del código Java. Las siguientes líneas del código Java provocarán un select y, por lo tanto, una sincronización del contexto de persistencia con la base de datos.
  • línea 8: la orden JPQL que queremos emitir ya se ha emitido. Hibernate la encuentra en su caché de «consultas preparadas».
  • línea 9: Hibernate anuncia que va a realizar un flush del contexto de persistencia
  • líneas 11-12: Hibernate (Hb) detecta que la entidad Persona#1 (con clave primaria 1) ha sido modificada (dirty).
  • líneas 12-13: Hb anuncia que actualiza este elemento y cambia su número de versión de 3 a 4.
  • línea 15: la sincronización del contexto provocará 0 inserciones, 1 actualización (update) y 0 eliminaciones (delete)
  • líneas 17-34: sincronización del contexto (flush). A tener en cuenta: el incremento de la versión (línea 19), la orden SQL update preparada (línea 21), los valores de los parámetros de la orden update (líneas 24-31).
  • línea 35: comienza la orden select
  • línea 38: la orden SQL que se va a ejecutar
  • línea 40: el select solo devuelve una fila
  • línea 42: Hb descubre que ya tiene en su contexto de persistencia la entidad Personne#1 que la consulta SELECT recuperó de la base de datos. Por lo tanto, no copia la línea obtenida de la base de datos en el contexto, operación que denomina «hidratación».
  • línea 43: verifica si los objetos devueltos por select tienen dependencias (por lo general, claves externas) que también habría que cargar (colecciones no perezosas). En este caso, no hay ninguna.
  • línea 44: visualización provocada por el código Java
  • línea 45: fin de la transacción JDBC solicitada por el código Java
  • línea 46: comienza la sincronización automática del contexto que tiene lugar durante el commit.
  • línea 48: Hb detecta que el contexto no ha cambiado desde la sincronización anterior.
  • línea 50: fin de commit.

Una vez más, los registros de Hibernate en modo DEBUG resultan muy útiles para saber exactamente qué hace Hibernate.

2.1.13.11. Prueba 10

El código de la prueba 10 es el siguiente:


// control de versiones (bloqueo optimista)
    public static void test10() {
        // Contexto de persistencia
        EntityManager em = getEntityManager();
        // Inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // incrementar la versión de newp1 directamente en la base de datos (consulta nativa)
        em.createNativeQuery(String.format("update %s set VERSION=VERSION+1 WHERE ID=%d", TABLE_NAME, newp1.getId())).executeUpdate();
        // fin de transacción
        tx.commit();
        // inicio de una nueva transacción
        tx = em.getTransaction();
        tx.begin();
        // se incrementa el número de hijos de newp1
        newp1.setNbenfants(newp1.getNbenfants() + 1);
        // fin de la transacción: debe fallar porque newp1 ya no tiene la versión correcta
        try {
            tx.commit();
        } catch (RuntimeException e1) {
            System.out.format("Erreur lors de la mise à jour de newp1 [%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(), e1.getCause().getClass().getName(), e1.getCause().getMessage());
            // se realiza un rollback de la transacción
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s,%s]%n", e2.getClass().getName(), e2.getMessage());
            }
        }
        // se cierra el contexto, que ya no está actualizado
        em.close();
        // volcado de la tabla: la versión de p1 debe haber cambiado
        dump();
    }
  • La prueba 10 tiene como objetivo mostrar el mecanismo que introduce el campo version de la @Entity Persona, que cuenta con el atributo JPA @Version. Ya explicamos que esta anotación hace que, en la base de datos, el valor de la columna asociada a la anotación @Version se incremente cada vez que se realice un update en la fila a la que pertenece. Este mecanismo, también conocido como bloqueo optimista (optimistic locking), exige que el cliente que desee modificar un objeto O en la base de datos cuente con la última versión del mismo. Si no la tiene, significa que el objeto ha sido modificado desde que lo obtuvo y se le debe notificar.
  • línea 4: no se cambia el contexto de persistencia. Por lo tanto, newp1 se encuentra dentro de él.
  • líneas 6-7: inicio de una transacción.
  • línea 9: la versión del objeto newp1 se incrementa en 1 (4 → 5) directamente en la base de datos. Las consultas del tipo nativeQuery omiten el contexto de persistencia y acceden directamente a la base de datos. El resultado es que el objeto persistente newp1 y su imagen en la base de datos ya no tienen la misma versión.
  • línea 10: fin de la primera transacción
  • líneas 13-14: inicio de una segunda transacción
  • línea 16: el número de hijos del objeto persistente newp1 aumenta en 1 (6 → 7).
  • línea 19: fin de la transacción. Por lo tanto, se lleva a cabo una sincronización. Esto provocará la actualización del número de hijos de newp1 en la base de datos. Esta actualización fallará porque el objeto persistente newp1 tiene la versión 4, mientras que en la base de datos el objeto que se debe actualizar tiene la versión 5. Se lanzará una excepción, lo que justifica el uso de try / catch en el código.
  • línea 21: se muestra la excepción y su causa.
  • línea 25: se revierte la transacción
  • línea 33: visualización de la tabla: deberíamos ver que la versión de newp1 es 5 en la base de datos.

La salida de la consola de la prueba 10 es la siguiente:

1
2
3
4
5
6
7
main : ----------- test9
[personnes]
[1,4,Martin,Paul,31/01/2000,false,6]
main : ----------- prueba10
Erreur lors de la mise à jour de newp1 [javax.persistence.RollbackException,Error while commiting the transaction,org.hibernate.StaleObjectStateException,Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [entites.Personne#1]]
[personnes]
[1,5,Martin,Paul,31/01/2000,false,6]
  • línea 5: el commit lanza correctamente una excepción. Es de tipo [javax.persistence.RollbackException]. El mensaje asociado es impreciso. Si nos fijamos en la causa de esta excepción (Exception.getCause), vemos que se trata de una excepción de Hibernate debida a que se intenta modificar una fila de la base de datos sin tener la versión correcta.
  • línea 7: vemos que la versión de newp1 en la base de datos sí se ha actualizado a 5 mediante nativeQuery.

2.1.13.12. Prueba 11

El código de la prueba 11 es el siguiente:


// reversión de una transacción
    public static void test11() throws ParseException {
        // contexto de persistencia
        EntityManager em = getEntityManager();
        // inicio de transacción
        EntityTransaction tx = null;
        try {
            tx = em.getTransaction();
            tx.begin();
            // se vuelve a vincular p1 al contexto al recuperarlo de la base de datos
            p1 = em.find(Personne.class, p1.getId());
            // se incrementa el número de hijos de p1
            p1.setNbenfants(p1.getNbenfants() + 1);
            // visualización de personas: el número de hijos de p1 debe haber cambiado
            System.out.println("[personnes]");
            for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
                System.out.println(p);
            }
            // creación de dos personas con el mismo nombre, lo cual está prohibido por la DDL
            Personne p3 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
            Personne p4 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
            // Persistencia de las personas
            em.persist(p3);
            em.persist(p4);
            // fin de la transacción
            tx.commit();
        } catch (RuntimeException e1) {
            // Hubo un problema
            System.out.format("Erreur dans transaction [%s,%s,%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(),
                    e1.getCause().getClass().getName(), e1.getCause().getMessage(), e1.getCause().getCause().getClass().getName(), e1.getCause().getCause()
                            .getMessage());
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s]%n", e2.getMessage());
            }
            // se abandona el contexto actual
            em.clear();
        }
        // volcado: la tabla no debe haber cambiado debido a la reversión
        dump();
    }
  • La prueba 11 se centra en el mecanismo de la transacción rollback. Una transacción funciona según el principio de «todo o nada»: las operaciones SQL que contiene se ejecutan todas con éxito (commit) o se cancelan todas en caso de que falle alguna de ellas (rollback).
  • línea 4: se continúa con el mismo contexto de persistencia. Quizás el lector recuerde que el contexto se cerró tras el fallo de la prueba anterior. En este caso, [getEntityManager] proporciona un contexto completamente nuevo, es decir, vacío.
  • líneas 7-27: un único try / catch para controlar los problemas que se puedan presentar
  • líneas 8-9: inicio de una transacción que contendrá varias operaciones SQL
  • línea 11: se busca p1 en la base de datos y se coloca en el contexto
  • línea 13: se incrementa el número de hijos de p1 (de 6 a 7)
  • líneas 15-18: se muestra el contenido de la base de datos, lo que forzará una sincronización del contexto. En la base de datos, el número de hijos de p1 pasará a 7, lo que debería confirmarse en la salida de la consola.
  • líneas 20-21: se crean dos personas, p3 y p4, con el mismo nombre. Sin embargo, el campo «nombre» de la @Entity «Persona» tiene el atributo «unique=true», lo que ha dado lugar a una restricción de unicidad en la columna NOM de la tabla [jpa01_personne].
  • líneas 23-24: las personas p3 y p4 se colocan en el contexto de persistencia.
  • línea 26: se confirma la transacción. A continuación, se realiza una segunda sincronización del contexto, ya que la primera tuvo lugar con el select. JPA emitirá dos órdenes SQL y insert para las personas p3 y p4. Se insertará p3. Para p4, SGBD generará una excepción, ya que p4 tiene el mismo nombre que p3. Por lo tanto, p4 no se inserta y el controlador JDBC devuelve una excepción al cliente.
  • línea 27: se maneja la excepción
  • líneas 29-31: se muestra la excepción y sus dos causas anteriores en la cadena de excepciones que nos han llevado hasta aquí.
  • Línea 34: se realiza un rollback de la transacción actualmente activa. Esta comenzó en la línea 9 del código Java. Desde entonces, se realizó una operación update para modificar el número de hijos de p1 y, posteriormente, una operación insert para la persona p3. Todo esto se anulará con el rollback.
  • línea 39: se vacía el contexto de persistencia
  • línea 42: se muestra la tabla [jpa01_personne]. Hay que verificar que p1 siga teniendo 6 hijos y que ni p3 ni p4 aparezcan en la tabla.

La salida en consola de la prueba 11 es la siguiente:


main : ----------- test11
[personnes]
[1,6,Martin,Paul,31/01/2000,false,7]
14:50:30,312 ERROR JDBCExceptionReporter:72 - Duplicate entry 'X' for key 2
Erreur dans transaction [javax.persistence.EntityExistsException,org.hibernate.exception.ConstraintViolationException: could not insert: [entites.Personne],org.hibernate.exception.ConstraintViolationException,could not insert: [entites.Personne],java.sql.SQLException,Duplicate entry 'X' for key 2]
[personnes]
[1,5,Martin,Paul,31/01/2000,false,6]
  • línea 3: el número de hijos de p1 pasó de 6 a 7 en la base de datos; la versión de p1 pasó a 6.
  • línea 4: la excepción capturada al confirmar la transacción. Si se lee con atención, se observa que la causa es una clave duplicada X (el nombre). Es la inserción de p4 la que provoca este error, ya que p3, que ya estaba insertado, también tiene el nombre X.
  • línea 7: la tabla después del rollback. p1 ha recuperado su versión 5 y su número de hijos 6; p3 y p4 no se han insertado.

2.1.13.13. Prueba 12

El código de la prueba 12 es el siguiente:


    // volvemos a hacer lo mismo, pero sin las transacciones
    // se obtiene el mismo resultado que antes con los SGBD: FIREBIRD, ORACLE XE, POSTGRES, MYSQL5
    //; con SQLSERVER, la tabla queda vacía. La conexión queda en un estado que impide la reejecución
    // del programa. Por lo tanto, es necesario reiniciar el servidor.
    // lo mismo ocurre con el Derby SGBD
    // HSQL inserta la primera persona; no hay reversión

    public static void test12() throws ParseException {
        // se vuelve a vincular a p1
        p1 = em.find(Personne.class, p1.getId());
        // se incrementa el número de hijos de p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // Visualización de personas: el número de hijos de p1 debe haber cambiado
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // creación de 2 personas con el mismo nombre, lo cual está prohibido por la DDL
        Personne p3 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p4 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        // Persistencia de las personas
        em.persist(p3);
        em.persist(p4);
        // volcado que provocará la sincronización del contexto em con el BD
        try {
            dump();
        } catch (RuntimeException e3) {
            System.out.format("Erreur dans dump [%s,%s,%s,%s]%n", e3.getClass().getName(), e3.getMessage(), e3.getCause().getClass().getName(), e3
                    .getCause().getMessage());
        }
        // se cierra el contexto actual
        em.close();
        // volcado
        dump();
}
  • La prueba 12 repite lo mismo que la prueba 11, pero fuera de la transacción. Queremos ver qué sucede en este caso.
  • Líneas 1-6: muestran los resultados de las pruebas con diversos SGBD:
  • con ciertos valores de SGBD (Firebird, Oracle, MySQL5, Postgres) se obtiene el mismo resultado que con la prueba 11. Lo cual sugiere que estos SGBD iniciaron por sí mismos una transacción que abarcaba todas las órdenes SQL recibidas hasta la que provocó el error, y que ellos mismos iniciaron un rollback.
  • Con otros SGBD (SQL Servidor, Apache Derby) se produce un fallo de la aplicación y/o del SGBD.
  • Con el SGBD y el HSQLDB, parece que la transacción abierta por el SGBD está en modo autocommit: la modificación del número de hijos de p1 y la inserción de p3 se hacen permanentes. Solo falla la inserción de p4.

Por lo tanto, obtenemos un resultado que depende de SGBD, lo que hace que la aplicación no sea portátil. Cabe destacar que las operaciones en el contexto de persistencia siempre deben realizarse dentro de una transacción.

2.1.14. Cambiar a SGBD

Volvamos a la arquitectura de prueba de nuestro proyecto actual:

La aplicación cliente [3] solo ve la interfaz JPA [5]. No ve ni la implementación real de esta, ni el SGBD de destino. Por lo tanto, debemos poder cambiar estos dos elementos de la cadena sin realizar modificaciones en el cliente [3]. Eso es lo que intentamos comprobar ahora, comenzando por cambiar el SGBD. Hasta ahora habíamos utilizado el MySQL5. Presentamos otros seis descritos en los anexos (párrafo 5), con la esperanza de que entre ellos se encuentre el SGBD favorito del lector.

En cualquier caso, la modificación que hay que realizar en el proyecto de Eclipse es sencilla (véase más abajo): reemplazar el archivo de configuración de la capa persistence.xml [1] por uno de los que se encuentran en la carpeta conf [2] del proyecto. Los controladores JDBC y SGBD ya se encuentran en la biblioteca [jpa-divers], [3] y [4].

2.1.14.1. Oracle 10g Express

Oracle 10g Express se presenta en los Anexos, en el párrafo 5.7. El archivo persistence.xml de Oracle es el siguiente:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  proveedor -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <!-- Clases persistentes -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- registros SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- conexión JDBC -->
            <property name="hibernate.connection.driver_class" value="oracle.jdbc.OracleDriver" />
            <property name="hibernate.connection.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
            <!-- creación automática del esquema -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- Dialecto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect" />
            <!-- propiedades DataSource c3p0 -->
            <property name="hibernate.c3p0.min_size" value="5" />
            <property name="hibernate.c3p0.max_size" value="20" />
            <property name="hibernate.c3p0.timeout" value="300" />
            <property name="hibernate.c3p0.max_statements" value="50" />
            <property name="hibernate.c3p0.idle_test_period" value="3000" />
        </properties>
    </persistence-unit>
</persistence>

Esta configuración es idéntica a la realizada para SGBD y MySQL5, salvo por los siguientes detalles:

  • líneas 15-18, que configuran la conexión JDBC con la base de datos
  • línea 22: que establece el dialecto SQL que se utilizará

En los ejemplos siguientes, solo especificaremos las líneas que cambian. Para obtener una explicación de la configuración, consulte el anexo dedicado al SGBD utilizado. En él se presenta un ejemplo de uso de la conexión JDBC en cada caso, en el contexto del complemento [SQL Explorer]. Con la información del anexo, el lector podrá repetir la operación de verificación del resultado de la aplicación [InitDB] realizada en el párrafo 2.1.10.2.

Procedemos como se indica en el párrafo mencionado anteriormente:

  • ejecutar el SGBD de Oracle
  • colocar conf/oracle/persistence.xml en META-INF/persistence.xml
  • ejecutar la aplicación [InitDB]

Se obtienen los siguientes resultados en la consola:

A partir de ahora, ya no mostraremos esta captura de pantalla, ya que siempre es la misma. Más interesante es la vista SQL Explorer de la conexión entre JDBC y SGBD. Seguiremos el procedimiento explicado en el párrafo 2.1.8.

  • en [1]: la conexión con Oracle
  • en [2]: el árbol de la conexión tras ejecutar [InitDB]
  • en [3]: la estructura de la tabla [jpa01_personne]
  • en [4]: su contenido.

Una vez hecho esto, se invita al lector a ejecutar la aplicación [Main] y luego detener SGBD.

2.1.14.2. PostgreSQL 8.2

PostgreSQL 8.2 se presenta en los Anexos, en el párrafo 5.6. Su archivo persistence.xml es el siguiente:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!-- inicio de sesión JDBC -->
            <property name="hibernate.connection.driver_class" value="org.postgresql.Driver" />
            <property name="hibernate.connection.url" value="jdbc:postgresql:jpa" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
...
            <!-- Dialecto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect" />
...
    </persistence-unit>
</persistence>

Para ejecutar [InitDB]:

  • ejecutar el SGBD PostgreSQL
  • colocar conf/postgres/persistence.xml en META-INF/persistence.xml
  • ejecutar la aplicación [InitDB]

La vista de SQL Explorer de la conexión entre JDBC y SGBD es la siguiente:

  • en [1]: la conexión con PostgreSQL
  • en [2]: el árbol de la conexión tras la ejecución de [InitDB]
  • en [3]: la estructura de la tabla [jpa01_personne]
  • en [4]: su contenido.

Una vez hecho esto, se invita al lector a ejecutar la aplicación [Main] y luego detener SGBD

2.1.14.3. SQL Server Express 2005

SQL Server Express 2005 se presenta en los anexos, en el párrafo 5.8, página 270. Su archivo persistence.xml es el siguiente:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!-- conexión JDBC -->
            <property name="hibernate.connection.driver_class" value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />
            <property name="hibernate.connection.url" value="jdbc:sqlserver://localhost\\SQLEXPRESS:1433;databaseName=jpa" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
...
            <!-- Dialecto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />
...
    </persistence-unit>
</persistence>

Para ejecutar [InitDB]:

  • ejecute el SGBD SQL Server
  • coloca conf/sqlserver/persistence.xml en META-INF/persistence.xml
  • ejecutar la aplicación [InitDB]

La vista de SQL Explorer de la conexión entre JDBC y SGBD es la siguiente:

  • en [1]: la conexión con el servidor SQL
  • en [2]: el árbol de la conexión tras la ejecución de [InitDB]
  • en [3]: la estructura de la tabla [jpa01_personne]
  • en [4]: su contenido.

Una vez hecho esto, se invita al lector a ejecutar la aplicación [Main] y luego detener SGBD

2.1.14.4. Firebird 2.0

Firebird 2.0 se presenta en los Anexos, en el párrafo 5.4. Su archivo persistence.xml es el siguiente:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!-- conexión JDBC -->
            <property name="hibernate.connection.driver_class" value="org.firebirdsql.jdbc.FBDriver" />
            <property name="hibernate.connection.url" value="jdbc:firebirdsql:localhost/3050:C:\data\2006-2007\eclipse\dvp-jpa\annexes\firebird\jpa.fdb" />
            <property name="hibernate.connection.username" value="sysdba" />
            <property name="hibernate.connection.password" value="masterkey" />
...
            <!-- Dialecto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.FirebirdDialect" />
...
    </persistence-unit>
</persistence>

Para ejecutar [InitDB]:

  • ejecuta el archivo SGBD de Firebird
  • colocar conf/firebird/persistence.xml en META-INF/persistence.xml
  • ejecutar la aplicación [InitDB]

La perspectiva SQL Explorer de la conexión entre JDBC y SGBD es la siguiente:

  • en [1]: la conexión con Firebird
  • en [2]: el árbol de la conexión después de ejecutar [InitDB]
  • en [3]: la estructura de la tabla [jpa01_personne]
  • en [4]: su contenido.

Una vez hecho esto, se invita al lector a ejecutar la aplicación [Main] y luego detener SGBD.

2.1.14.5. Apache Derby

Apache Derby se presenta en los Anexos, en el párrafo 5.10. Su archivo persistence.xml es el siguiente:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!-- conexión JDBC -->
            <property name="hibernate.connection.driver_class" value="org.apache.derby.jdbc.ClientDriver" />
            <property name="hibernate.connection.url" value="jdbc:derby://localhost:1527//data/2006-2007/eclipse/dvp-jpa/annexes/derby/jpa;create=true" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
...
            <!-- Dialecto -->
...
    </persistence-unit>
</persistence>

Para ejecutar [InitDB]:

  • ejecuta el archivo SGBD de Apache Derby
  • colocar conf/derby/persistence.xml en META-INF/persistence.xml
  • ejecutar la aplicación [InitDB]

La perspectiva SQL Explorer de la conexión entre JDBC y SGBD es la siguiente:

  • en [1]: la conexión con Apache Derby
  • en [2]: el árbol de la conexión tras la ejecución de [InitDB]. Cabe destacar la tabla [HIBERNATE_UNIQUE_KEY] creada por JPA / Hibernate para generar automáticamente los valores sucesivos de la clave primaria ID. Ya hemos señalado que este mecanismo suele ser propio del proveedor. Aquí se ve claramente. Gracias a JPA, el desarrollador no tiene que ocuparse de estos detalles de SGBD.
  • en [3]: la estructura de la tabla [jpa01_personne]
  • en [4]: su contenido.

Una vez hecho esto, se invita al lector a ejecutar la aplicación [Main] y luego detener SGBD.

2.1.14.6. HSQLDB

HSQLDB se presenta en los anexos, en el párrafo 5.9. Su archivo persistence.xml es el siguiente:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!-- conexión JDBC -->
            <property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver" />
            <property name="hibernate.connection.url" value="jdbc:hsqldb:hsql://localhost" />
            <property name="hibernate.connection.username" value="sa" />
            <!-- 
                <property name="hibernate.connection.password" value="" />
            -->
...
            <!-- Dialecto -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
...
        </properties>
    </persistence-unit>
</persistence>

Para ejecutar [InitDB]:

  • ejecuta el archivo SGBD HSQL
  • colocar conf/hsql/persistence.xml en META-INF/persistence.xml
  • ejecutar la aplicación [InitDB]

La perspectiva SQL Explorer de la conexión entre JDBC y SGBD es la siguiente:

  • en [1]: la conexión con HSQL
  • en [2]: el árbol de la conexión tras la ejecución de [InitDB].
  • en [3]: la estructura de la tabla [jpa01_personne]
  • en [4]: su contenido.

Una vez hecho esto, se invita al lector a ejecutar la aplicación [Main] y luego a detener la aplicación SGBD.

2.1.15. Cambiar la implementación a JPA

Volvamos a la arquitectura de prueba de nuestro proyecto actual:

El estudio anterior demostró que pudimos cambiar de SGBD a [7] sin modificar el código del cliente [3]. Ahora modificamos la implementación JPA [6] y demostramos, una vez más, que esto se realiza de manera transparente para el código del cliente [3]. Tomamos una implementación TopLink y [http://www.oracle.com/technology/products/ias/toplink/jpa/index.html]:

2.1.15.1. El proyecto de Eclipse

Con motivo del cambio de implementación a JPA, creamos un nuevo proyecto de Eclipse para no alterar el proyecto existente. De hecho, el nuevo proyecto utiliza bibliotecas de persistencia que pueden entrar en conflicto con las de Hibernate:

  • en [1]: la carpeta [<exemples>/toplink/direct/personnes-entites] contiene el proyecto de Eclipse. Importa este proyecto.
  • en [2]: el proyecto [toplink-personnes-entites] importado. Es idéntico (se obtuvo mediante copia) al proyecto [hibernate-personne-entites], salvo por dos detalles:
    • el archivo [META-INF/persistence.xml] [3] ahora configura una capa JPA / Toplink
    • la biblioteca [jpa-hibernate] ha sido reemplazada por la biblioteca [jpa-toplink], [4] y [5] (véase el párrafo 1.5).
  • En [6]: la carpeta [conf] contiene una versión del archivo [persistence.xml] para cada SGBD.
  • en [7]: la carpeta [ddl], que contendrá los scripts SQL para generar el esquema de la base de datos.

Sabemos que la capa JPA está configurada por el archivo [META-INF/persistence.xml]. Este, a su vez, configura ahora una implementación JPA / Toplink. Su contenido para una capa JPA interconectada con SGBD y MySQL5 es el siguiente:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  proveedor -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- clases persistentes -->
        <class>entites.Personne</class>
        <!-- propiedades de la unidad de persistencia -->
        <properties>
            <!-- conexión JDBC -->
            <property name="toplink.jdbc.driver" value="com.mysql.jdbc.Driver" />
            <property name="toplink.jdbc.url" value="jdbc:mysql://localhost:3306/jpa" />
            <property name="toplink.jdbc.user" value="jpa" />
            <property name="toplink.jdbc.password" value="jpa" />
            <property name="toplink.jdbc.read-connections.max" value="3" />
            <property name="toplink.jdbc.read-connections.min" value="1" />
            <property name="toplink.jdbc.write-connections.max" value="5" />
            <property name="toplink.jdbc.write-connections.min" value="2" />
            <!-- SGBD -->
            <property name="toplink.target-database" value="MySQL4" />
            <!--  servidor de aplicaciones -->
            <property name="toplink.target-server" value="None" />
            <!--  generación de esquema -->
            <property name="toplink.ddl-generation" value="drop-and-create-tables" />
            <property name="toplink.application-location" value="ddl/mysql5" />
            <property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
            <property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
            <property name="toplink.ddl-generation.output-mode" value="both" />
            <!-- registros -->
            <property name="toplink.logging.level" value="OFF" />
        </properties>
    </persistence-unit>
</persistence>
  • línea 3: no ha cambiado
  • línea 5: el proveedor ahora es Toplink. La clase que se menciona aquí se encuentra en la biblioteca [jpa-toplink] ([1] a continuación):
  • línea 7: la etiqueta <class> sirve para nombrar todas las clases @Entity del proyecto; aquí, solo la clase Personne. Hibernate tenía una opción de configuración que nos evitaba nombrar estas clases. Exploraba el archivo classpath del proyecto para encontrar las clases @Entity.
  • línea 9: la etiqueta <properties> introduce propiedades específicas de la implementación JPA utilizada, en este caso Toplink.
  • líneas 11-14: configuración de la conexión JDBC con el SGBD MySQL5
  • líneas 15-18: configuración del grupo de conexiones JDBC gestionado de forma nativa por Toplink:
  • líneas 15 y 16: número máximo y mínimo de conexiones en el grupo de conexiones de lectura. Valor predeterminado (2,2)
  • líneas 17 y 18: número máximo y mínimo de conexiones en el grupo de conexiones de escritura. Valor predeterminado (10,2)
  • línea 20: el SGBD de destino. La lista de SGBD utilizables está disponible en el paquete [oracle.toplink.essentials.platform.database] (véase [2] más arriba). El SGBD MySQL5 no está presente en la lista [2], por lo que se ha elegido MySQL4. Toplink es compatible con un poco menos de SGBD que Hibernate. Por lo tanto, de los siete SGBD utilizados en nuestros ejemplos, Firebird no es compatible. Tampoco se encuentra Oracle en la lista. De hecho, se encuentra en otro paquete ([3], mencionado anteriormente). Si en estos dos paquetes, el SGBD de destino se designa mediante la clase <Sgbd>Platform.class, la etiqueta se escribirá así:

            <property name="toplink.target-database" value="<Sgbd>" />
  • línea 22: establece el servidor de aplicaciones si la aplicación se ejecuta en uno de estos servidores. Los valores posibles actuales son (None, OC4J_10_1_3, SunAS9). Valor predeterminado (None).
  • líneas 24-28: cuando se inicialice la capa JPA, se le solicita que borre la base de datos definida por la conexión JDBC de las líneas 11-14. De esta manera, se comenzará con una base de datos vacía.
    • línea 24: se le pide a Toplink que realice un drop seguido de un create de las tablas del esquema de la base de datos
    • línea 25: se le pedirá a Toplink que genere los scripts SQL de las operaciones drop y create. application-location establece la carpeta en la que se generarán estos scripts. Por defecto: (carpeta actual).
    • línea 26: nombre del script SQL de las operaciones create.. Por defecto: createDDL.jdbc.
    • línea 27: nombre del script SQL de las operaciones drop.. Por defecto: dropDDL.jdbc.
    • línea 28: modo de generación del esquema (por defecto: both):
      • both: scripts y base de datos
      • database: solo base de datos
      • sql-script: solo scripts
  • línea 30: se desactivan (OFF) los registros de Toplink. Los distintos niveles de inicio de sesión disponibles son los siguientes: OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST. Por defecto: INFO.

Consulte la URL [http://www.oracle.com/technology/products/ias/toplink/JPA/essentials/toplink-jpa-extensions.html] para obtener una definición exhaustiva de las etiquetas <property> que se pueden utilizar con Toplink.

2.1.15.3. Prueba [InitDB]

No hay nada más que hacer. Estamos listos para ejecutar la primera prueba [InitDB]:

  • ejecutar el SGBD, aquí MySQL5
  • ejecutar [InitDB]
  • en [1]: la pantalla de la consola. Encontramos los resultados ya obtenidos con JPA / Hibernate.
  • en [3]: abrimos la perspectiva [SQL Explorer] y luego abrimos la conexión [mysql5-jpa]
  • En [4]: el árbol de la base de datos jpa. Se observa que la ejecución de [InitDB] ha creado dos tablas: [jpa01_personne], que era la esperada, y la tabla [sequence], que no se esperaba.
  • en [5]: la estructura de la tabla [jpa01_personne] y en [6] su contenido
  • en [7]: la estructura de la tabla [sequence] y en [8] su contenido.

El archivo de configuración [persistence.xml] solicitaba la generación de los scripts de DDL:


            <!--  generación de esquema -->
            <property name="toplink.ddl-generation" value="drop-and-create-tables" />
            <property name="toplink.application-location" value="ddl/mysql5" />
            <property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
            <property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
<property name="toplink.ddl-generation.output-mode" value="both" />

Veamos qué se generó en la carpeta [ddl/mysql5]:

 

create.sql


CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
  • Línea 1: la entrada DDL de la tabla [jpa01_personne]. Se observa que Toplink no ha utilizado el atributo autoincrement para la clave primaria ID. Por lo tanto, no se produce un incremento automático de esta clave al insertar filas.
  • línea 2: la tabla DDL de la tabla [sequence]. Su nombre parece indicar que Toplink utiliza esta tabla para generar los valores de la clave primaria ID.
  • línea 3: inserción de una sola línea en [SEQUENCE]

drop.sql


DROP TABLE jpa01_personne
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
  • línea 1: eliminación de la tabla [jpa01_personne]
  • línea 2: eliminación de una línea específica de la tabla [SEQUENCE]. Ni la tabla en sí ni las demás líneas que pudiera contener se eliminan.

Para obtener más información sobre la función de la tabla [SEQUENCE], se activan en [persistence.xml] los registros de Toplink en el nivel FINE, un nivel que registra las órdenes SQL emitidas por Toplink:


            <!-- registros -->
<property name="toplink.logging.level" value="FINE" />

Se vuelve a ejecutar InitDB. A continuación, solo se ha conservado una vista parcial de la pantalla de la consola:


...
[TopLink Config]: 2007.05.28 12:07:52.796--ServerSession(12910198)--Conexión(30708295)--Hilo(Thread[main,5,main])--Conectado: jdbc:mysql://localhost:3306/jpa
    User: jpa@localhost
    Database: MySQL  Version: 5.0.37-community-nt
    Driver: MySQL-AB JDBC Driver  Version: mysql-connector-java-3.1.9 ( $Date: 2005/05/19 15:52:23 $, $Revision: 1.1.2.2 $ )
...
[TopLink Fine]: 2007.05.28 12:07:53.093--ServerSession(12910198)--Conexión(19255406)--Hilo(Thread[main,5,main])--DROP TABLE jpa01_personne
[TopLink Fine]: 2007.05.28 12:07:53.265--ServerSession(12910198)--Conexión(30708295)--Hilo(Thread[main,5,main])--CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT(1) por defecto 0 NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
[TopLink Fine]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Conexión(19255406)--Hilo(Thread[main,5,main])--CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
[TopLink Warning]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Hilo(Hilo[main,5,main])--Excepción [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (Compilación b41-beta2 (30/03/2007))): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: Table 'sequence' already exists
Error Code: 1050
Call: CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
Query: DataModifyQuery()
[TopLink Fine]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Conexión(30708295)--Hilo(Thread[main,5,main])--DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
[TopLink Fine]: 2007.05.28 12:07:53.609--ServerSession(12910198)--Conexión(19255406)--Hilo(Thread[main,5,main])--SELECT * FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
[TopLink Fine]: 2007.05.28 12:07:53.609--ServerSession(12910198)--Conexión(30708295)--Hilo(Thread[main,5,main])--INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) valores ('SEQ_GEN', 1)
[TopLink Fine]: 2007.05.28 12:07:53.734--ClientSession(15308417)--Conexión(14069849)--Hilo (Thread[main,5,main])--eliminar de jpa01_personne
[TopLink Fine]: 2007.05.28 12:07:53.750--ClientSession(15308417)--Conexión(14069849)--Hilo(Hilo[main,5,main])--UPDATE SEQUENCE SET SEQ_COUNT = SEQ_COUNT + ? WHERE SEQ_NAME = ?
    bind => [50, SEQ_GEN]
[TopLink Fine]: 2007.05.28 12:07:53.750--ClientSession(15308417)--Conexión(14069849)--Hilo(Thread[main,5,main])--SELECT SEQ_COUNT FROM SEQUENCE WHERE SEQ_NAME = ?
    bind => [SEQ_GEN]
[personnes]
[TopLink Fine]: 2007.05.28 12:07:53.906--ClientSession(15308417)--Conexión(14069849)--Hilo(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
    bind => [3, Sylvie, 2001-07-05, Durant, false, 1, 0]
[TopLink Fine]: 2007.05.28 12:07:53.921--ClientSession(15308417)--Conexión(14069849)--Hilo(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
    bind => [2, Paul, 2000-01-31, Martin, true, 1, 2]
[TopLink Fine]: 2007.05.28 12:07:53.937--ClientSession(15308417)--Conexión(14069849)--Hilo(Thread[main,5,main])--SELECT ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS FROM jpa01_personne ORDER BY NOM ASC
[3,1,Durant,Sylvie,05/07/2001,false,0]
[2,1,Martin,Paul,31/01/2000,true,2]
[TopLink Config]: 2007.05.28 12:07:54.062--ServerSession(12910198)--Conexión(30708295)--Hilo(Thread[main,5,main])--desconexión
[TopLink Info]: 2007.05.28 12:07:54.062--ServerSession(12910198)--Hilo(Thread[main,5,main])--archivo:/C:/data/2006-2007/eclipse/dvp-jpa/toplink/direct/personas-entidades/bin/-jpa cierre de sesión exitoso
...
terminé ...
  • líneas 2-5: una conexión a SGBD con sus parámetros. De hecho, los registros muestran que, en realidad, Toplink crea tres conexiones con SGBD. Habría que verificar si este número está relacionado con alguno de los valores de configuración utilizados para el grupo de conexiones JDBC:

            <property name="toplink.jdbc.read-connections.max" value="3" />
            <property name="toplink.jdbc.read-connections.min" value="1" />
            <property name="toplink.jdbc.write-connections.max" value="5" />
<property name="toplink.jdbc.write-connections.min" value="2" />
  • línea 7: eliminación de la tabla [jpa01_personne]. Es normal, ya que el archivo [persistence.xml] solicita la limpieza de la base de datos JPA.
  • línea 8: creación de la tabla [jpa01_personne]. Se observa que la clave primaria ID no tiene el atributo autoincrement.
  • línea 9: creación de la tabla [SEQUENCE], que ya existe, ya que se creó en la ejecución anterior.
  • líneas 10-13: Toplink reporta un error al intentar crear la tabla [SEQUENCE].
  • líneas 15-18: Toplink limpia la tabla [SEQUENCE]. Al finalizar esta limpieza, la tabla [SEQUENCE] tiene una fila (SEQ_NAME, SEQ_COUNT) con los valores («SEQ_GEN», 1).
  • línea 18: la tabla [jpa01_personne] se vacía.
  • líneas 19-20: Toplink pasa la única línea en la que SEQ_NAME = 'SEQ_GEN' de la tabla [SEQUENCE], del valor ('SEQ_GEN', 1) al valor ('SEQ_GEN', 51)
  • línea 21: Toplink recupera el valor 51 de la fila ('SEQ_GEN', 51) de la tabla [SEQUENCE].
  • líneas 24-27: Toplink inserta en la tabla [jpa01_personne] a las dos personas «Martin» y «Durant». Aquí hay un misterio: las claves primarias de estas dos líneas reciben los valores 2 y 3 sin que se sepa cómo se obtuvieron dichos valores. No se sabe si el valor SEQ_COUNT (51) obtenido en la línea 21 sirvió para algo. Cabe señalar que el valor de la versión de las filas es 1, mientras que Hibernate comenzaba en 0.
  • línea 28: Toplink genera el valor SELECT para obtener todas las filas de la tabla [jpa01_personne]
  • líneas 29-30: líneas mostradas por el cliente Java
  • líneas 31-32: Toplink cierra una conexión. Repetirá la operación para cada una de las conexiones abiertas inicialmente.

Al final, no se conoce exactamente la función de la tabla [SEQUENCE], pero parece que desempeña un papel en la generación de los valores de la clave primaria ID. Al seleccionar el nivel de registro más detallado, FINEST, se obtiene un poco más de información sobre la función de la tabla [SEQUENCE].


            <!-- registros -->
            <property name="toplink.logging.level" value="FINEST" />

A continuación, solo hemos incluido los registros relacionados con la inserción de las dos personas en la tabla. Aquí es donde se observa el mecanismo de generación de los valores de la clave primaria:

[TopLink Finest]: 2007.05.28 03:05:04.046--ClientSession(30617157)--Hilo(Hilo[main,5,main])--Ejecutar consulta ValueReadQuery()
[TopLink Fine]: 2007.05.28 03:05:04.046--ClientSession(30617157)--Conexión(13301441)--Hilo(Hilo[main,5,main])--SELECT SEQ_COUNT FROM SEQUENCE WHERE SEQ_NAME = ?
    bind => [SEQ_GEN]
[TopLink Finest]: 2007.05.28 03:05:04.062--ClientSession(30617157)--Conexión(13301441)--Hilo(Thread[main,5,main])--preasignación de secuenciación local para SEQ_GEN: objetos: 50, primero: 2, último: 51
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--Hilo (Hilo[main,5,main])--asignar secuencia al objeto (2 -> [null,0,Martin,Paul,31/01/2000,true,2])
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--Hilo(Hilo[main,5,main])--Ejecutar consulta DoesExistQuery()
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--Hilo(Hilo[main,5,main])--PERSIST operación llamada en: [null,0,Durant,Sylvie,05/07/2001,false,0].
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--Hilo (Hilo[main,5,main])--asignar secuencia al objeto (3 -> [null,0,Durant,Sylvie,05/07/2001,false,0])
[personnes]
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--Hilo(Hilo[main,5,main])--Ejecutar consulta InsertObjectQuery([3,0,Durant,Sylvie,05/07/2001,false,0])
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--Hilo(Thread[main,5,main])--Asignar fila de retorno DatabaseRecord(
    jpa01_personne.VERSION => 1)
[TopLink Fine]: 2007.05.28 03:05:04.203--ClientSession(30617157)--Conexión(13301441)--Hilo(Hilo[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
    bind => [3, Sylvie, 2001-07-05, Durant, false, 1, 0]
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--Hilo(Hilo[main,5,main])--Ejecutar consulta InsertObjectQuery([2,0,Martin,Paul,31/01/2000,true,2])
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--Hilo(Thread[main,5,main])--Asignar fila de retorno DatabaseRecord(
    jpa01_personne.VERSION => 1)
[TopLink Fine]: 2007.05.28 03:05:04.203--ClientSession(30617157)--Conexión(13301441)--Hilo(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
bind => [2, Paul, 2000-01-31, Martin, true, 1, 2]
  • línea 4: se observa que el número 51 obtenido de la tabla [SEQUENCE] en la línea 2 sirve para delimitar un intervalo de valores para la clave primaria: [2,51]
  • línea 5: a la primera persona se le asigna el valor 2 como clave primaria
  • línea 8: a la segunda persona se le asigna el valor 3 como clave primaria
  • línea 12: muestra la gestión de versiones de la primera persona
  • línea 17: lo mismo para la segunda persona

El nivel de registros [FINEST] también muestra los límites de las transacciones emitidas por Toplink. El análisis de estos registros revela lo que hace Toplink y es una excelente forma de comprender el puente entre objetos y relaciones.

De lo anterior, cabe destacar lo siguiente:

  • que diferentes implementaciones de JPA generarán esquemas de bases de datos distintos. En este ejemplo, Hibernate y Toplink no generaron los mismos esquemas.
  • que los niveles de registro FINE, FINER y FINEST de Toplink deben utilizarse siempre que se desee obtener aclaraciones sobre lo que hace exactamente Toplink.

2.1.15.4. Prueba [Main]

Ahora ejecutamos la prueba [Main]:

  • en [1]: todas las pruebas pasan, excepto la prueba 11, [2]
  • en [3]: línea 376, la línea de código donde se produjo la excepción

El código que genera la excepción es el siguiente:


} catch (RuntimeException e1) {
            // tuvimos un problema
            System.out.format("Erreur dans transaction [%s,%s,%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(),
                    e1.getCause().getClass().getName(), e1.getCause().getMessage(), e1.getCause().getCause().getClass().getName(), e1.getCause().getCause()
                            .getMessage());
            try {
            ...
  • línea [3]: la línea de la excepción. Tenemos un NullPointerException, lo que sugiere que uno de los métodos getCause de las líneas 4 y 5 devolvió un puntero null. Una expresión como [e1.getCause().getCause()] supone que la cadena de excepciones tiene tres elementos [e1.getCause().getCause(), e1.getCause(), e1]. Si solo tiene dos, la primera expresión provocará una excepción.

Modificamos el código anterior para que muestre solo las dos últimas excepciones de la cadena de excepciones:


        } catch (RuntimeException e1) {
            // tuvimos un problema
            System.out.format("Erreur dans transaction [%s,%s,%s,%s,]%n", e1.getClass().getName(), e1.getMessage(),
                    e1.getCause().getClass().getName(), e1.getCause().getMessage());
            try {
...

Al ejecutarlo, obtenemos el siguiente resultado:


...
[personnes]
[2,5,Martin,Paul,31/01/2000,false,6]
main : ----------- prueba11
[personnes]
Erreur dans transaction [javax.persistence.OptimisticLockException,Exception [TOPLINK-5006] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (03/30/2007))): oracle.toplink.essentials.exceptions.OptimisticLockException
Exception Description: The object [[2,6,Martin,Paul,31/01/2000,false,7]] cannot be updated because it has changed or been deleted since it was last read. 
Class> entites.Personne Primary Key> [2],oracle.toplink.essentials.exceptions.OptimisticLockException,
Exception Description: The object [[2,6,Martin,Paul,31/01/2000,false,7]] cannot be updated because it has changed or been deleted since it was last read. 
Class> entites.Personne Primary Key> [2],]
[personnes]
[2,5,Martin,Paul,31/01/2000,false,6]

Esta vez, la prueba 11 pasa. Las visualizaciones de la excepción (líneas 6-10) fueron solicitadas por el código Java (línea 3 del código anterior). Recordemos que la prueba 11 encadenaba, en una misma transacción, varias operaciones SQL, una de las cuales fallaba y debía provocar un rollback de la transacción. Los estados de la tabla [jpa01_personne] antes (línea 3) y después de la prueba (línea 12) son idénticos, lo que demuestra que se llevó a cabo la reversión.

Cabe destacar aquí un punto importante: las implementaciones JPA / Hibernate y JPA / Toplink no son 100 % intercambiables. En este ejemplo, debemos modificar el código del cliente JPA para evitar un NullPointerException. Volveremos a encontrarnos con este problema más adelante, nuevamente en el contexto de una excepción.

Volvamos a la arquitectura de pruebas de nuestro proyecto actual:

Anteriormente, el SGBD utilizado en [7] era MySQL5. Mostramos con Oracle cómo cambiar de SGBD. En cualquier caso, la modificación que hay que realizar en el proyecto de Eclipse es sencilla (véase más abajo): reemplazar el archivo de configuración de la capa JPA, persistence.xml [1], por uno de los que se encuentran en la carpeta conf ([2] y [3]) del proyecto.

2.1.16.1. Oracle 10g Express

Oracle 10g Express se presenta en los Anexos, en el párrafo 5.7. El archivo persistence.xml de Oracle para Toplink es el siguiente:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  proveedor -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- clases persistentes -->
        <class>entites.Personne</class>
        <!-- propiedades de la unidad de persistencia -->
        <properties>
            <!-- conexión JDBC -->
            <property name="toplink.jdbc.driver" value="oracle.jdbc.OracleDriver" />
            <property name="toplink.jdbc.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
            <property name="toplink.jdbc.user" value="jpa" />
            <property name="toplink.jdbc.password" value="jpa" />
            <property name="toplink.jdbc.read-connections.max" value="3" />
            <property name="toplink.jdbc.read-connections.min" value="1" />
            <property name="toplink.jdbc.write-connections.max" value="5" />
            <property name="toplink.jdbc.write-connections.min" value="2" />
            <!-- SGBD -->
            <property name="toplink.target-database" value="Oracle" />
            <!--  servidor de aplicaciones -->
            <property name="toplink.target-server" value="None" />
            <!--  generación de esquema -->
            <property name="toplink.ddl-generation" value="drop-and-create-tables" />
            <property name="toplink.application-location" value="ddl/oracle" />
            <property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
            <property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
            <property name="toplink.ddl-generation.output-mode" value="both" />
            <!-- registros -->
            <property name="toplink.logging.level" value="OFF" />
        </properties>
    </persistence-unit>
</persistence>

Esta configuración es idéntica a la realizada para SGBD y MySQL5, salvo por los siguientes detalles:

  • las líneas 11 a 14, que configuran la conexión JDBC con la base de datos
  • línea 20: que establece el destino SGBD
  • línea 25: que establece la carpeta de generación de scripts SQL de DDL

Para ejecutar la prueba [InitDB]:

  • ejecuta el SGBD de Oracle
  • colocar conf/oracle/persistence.xml en META-INF/persistence.xml
  • ejecutar la aplicación [InitDB]

Se obtienen los siguientes resultados en la consola y en la perspectiva [SQL Explorer]:

  • [1]: la visualización de la consola
  • [2]: la conexión [oracle-jpa] en SQL Explorer
  • [3]: la base de datos jpa
  • [4]: InitDB ha creado dos tablas: JPA01_PERSONNE y SEQUENCE, al igual que con MySQL5. A veces, en [4], aparecen tablas [BIN*]. Estas corresponden a tablas que han sido eliminadas. Para observar este fenómeno, basta con volver a ejecutar [InitDB]. La fase de inicialización de la capa JPA incluye una limpieza de la base de datos jpa durante la cual se elimina la tabla [JPA01_PERSONNE]:

En [A], aparece una tabla [BIN]. Oracle no elimina definitivamente una tabla que ha sido sometida a un drop, sino que la coloca en una papelera [Recycle Bin]. Esta papelera es visible ([B]) con la herramienta SQL Developer descrita en el párrafo 5.7.4. En [B], se puede purgar la tabla [JPA01_PERSONNE] que se encuentra en la papelera. Esto vacía la papelera [C]. Si en SQL Explorer se actualizan (clic derecho / Refresh) las tablas, se observa que la tabla BIN ya no está presente ([D]).

  • [5, 6]: la estructura y el contenido de la tabla [JPA01_PERSONNE]
  • [7, 8]: la estructura y el contenido de la tabla [SEQUENCE]

¡Listo! Ahora se invita al lector a ejecutar la aplicación [Main] en Oracle.

2.1.16.2. Las demás SGBD

No nos detendremos mucho en los demás SGBD. Basta con repetir el procedimiento seguido para Oracle. Cabe destacar los siguientes puntos:

  • independientemente del SGBD, Toplink siempre utiliza la misma técnica para generar los valores de la clave primaria ID de la tabla [JPA01_PERSONNE]: utiliza la tabla [SEQUENCE] detallada anteriormente.
  • Toplink no reconoce el SGBD de Firebird. Existe una base de datos genérica para estos casos:
                <property name="toplink.target-database" value="Auto" />

Con esta base genérica llamada [Auto], las pruebas con Firebird fallan debido a errores de sintaxis en SQL. Toplink utiliza para la clave primaria ID un tipo SQL Number(10) que Firebird no reconoce. Por lo tanto, hay que elegir un SGBD que tenga los mismos tipos SQL que Firebird (en este ejemplo). Este es el caso de Apache Derby:


            <!-- conexión JDBC -->
            <property name="toplink.jdbc.driver" value="org.firebirdsql.jdbc.FBDriver" />
...
            <!-- SGBD -->
            <!-- 
            TopLink ne reconnaît pas Firebird pour l'instant (05/07). Derby convient pour remplacer.
            -->
            <property name="toplink.target-database" value="Derby" />
...
  • Toplink no puede generar el esquema original de la base de datos para el SGBD HSQLDB. Es decir, la directiva:

            <!--  generación de esquema -->
<property name="toplink.ddl-generation" value="drop-and-create-tables" />

falla para HSQLDB. La causa es un error de sintaxis al crear la tabla [jpa01_personne]:


[TopLink Fine]: 2007.05.29 09:44:18.515--ServerSession(12910198)--Conexión(29775659)--Hilo(Thread[main,5,main])--DROP TABLE jpa01_personne
[TopLink Fine]: 2007.05.29 09:44:18.531--ServerSession(12910198)--Conexión(29775659)--Hilo(Hilo[main,5,main])--CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
[TopLink Warning]: 2007.05.29 09:44:18.531--ServerSession(12910198)--Hilo(Hilo[main,5,main])--Excepción [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (Compilación b41-beta2 (30/03/2007))): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: Unexpected token: UNIQUE in statement [CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE]

Línea 4, la sintaxis NOM VARCHAR(30) UNIQUE NOT NULL no es aceptada por HSQL. Hibernate había utilizado la sintaxis: NOM VARCHAR(30) NOT NULL, UNIQUE(NOM).

En general, Hibernate ha sido más eficaz que Toplink a la hora de reconocer los SGBD con los que se realizaron las pruebas de este documento.

2.1.17. Conclusión

El estudio de la @Entity [Personne] termina aquí. Desde un punto de vista conceptual, se ha hecho relativamente poco: hemos estudiado el puente objeto-relacional en el caso más simple: un objeto @Entity <--> una tabla. Sin embargo, su análisis nos ha permitido presentar las herramientas que utilizaremos a lo largo de todo el documento. Esto nos permitirá avanzar un poco más rápido de ahora en adelante en el estudio de los otros casos del puente objeto-relacional que vamos a analizar:

  • al @Entity [Personne] anterior, le agregaremos un campo adresse modelado por una clase [Adresse]. En cuanto a la base de datos, veremos dos implementaciones posibles. Los objetos [Personne] y [Adresse] dan lugar a
  • una única tabla [personne] que incluye la dirección
  • dos tablas, [personne] y [adresse], vinculadas por una relación de clave externa de tipo uno a uno.
  • un ejemplo de relación uno-a-varios, en la que una tabla [article] está vinculada a una tabla [categorie] mediante una clave externa
  • Un ejemplo de relación «varios a varios», en la que las tablas [personne] y [activite] están conectadas mediante una tabla de unión [personne_activite].

2.2. Ejemplo 2: relación uno a uno mediante una inclusión

2.2.1. El esquema de la base de datos

 
1
2

    drop table if exists jpa02_personne;

    create table jpa02_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
) ENGINE=InnoDB;

  • en [1]: la base de datos (plugin Azurri Clay)
  • en [2]: la tabla DDL generada por Hibernate para MySQL5

La tabla [jpa02_personne] es la tabla [jpa01_personne] analizada anteriormente, a la que se le ha agregado una dirección (líneas 12-18 de la DDL).

2.2.2. Los objetos @Entity que representan la base de datos

La dirección de una persona estará representada por la siguiente clase [Adresse]:


package entites;

...
@SuppressWarnings("serial")
@Embeddable
public class Adresse implements Serializable {

    // campos
    @Column(length = 30, nullable = false)
    private String adr1;

    @Column(length = 30)
    private String adr2;

    @Column(length = 30)
    private String adr3;

    @Column(length = 5, nullable = false)
    private String codePostal;

    @Column(length = 20, nullable = false)
    private String ville;

    @Column(length = 3)
    private String cedex;

    @Column(length = 20, nullable = false)
    private String pays;

    // constructores
    public Adresse() {

    }

    public Adresse(String adr1, String adr2, String adr3, String codePostal, String ville, String cedex, String pays) {
...
    }

    // getters y setters
...

    // toString
    public String toString() {
        return String.format("A[%s,%s,%s,%s,%s,%s,%s]", getAdr1(), getAdr2(), getAdr3(), getCodePostal(), getVille(), getCedex(), getPays());
    }
}
  • La principal novedad radica en la anotación @Embeddable de la línea 5. La clase [Adresse] no está destinada a generar una tabla, por lo que no cuenta con la anotación @Entity. La anotación @Embeddable indica que la clase está destinada a integrarse en un objeto @Entity y, por lo tanto, en la tabla asociada a este. Por eso, en el esquema de la base de datos, la clase [Adresse] no aparece como una tabla independiente, sino como parte de la tabla asociada a la @Entity [Personne].

La @Entity [Personne] presenta pocos cambios con respecto a su versión anterior: simplemente se le agrega un campo adresse:


package entites;

...
@Entity
@Table(name = "jpa02_hb_personne")
public class Personne implements Serializable{

    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false, unique = true)
    private String nom;

    @Column(length = 30, nullable = false)
    private String prenom;

    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(nullable = false)
    private boolean marie;

    @Column(nullable = false)
    private int nbenfants;

    @Embedded
    private Adresse adresse;

    // constructores
    public Personne() {
    }
...
}
  • La modificación se realiza en las líneas 33-34. El objeto [Personne] ahora tiene un campo adresse de tipo Adresse. Esto es para el POJO. La anotación @Embedded está destinada al puente objeto-relacional. Indica que el campo [Adresse adresse] deberá encapsularse en la misma tabla que el objeto [Personne].

2.2.3. El entorno de pruebas

Realizaremos pruebas muy similares a las que hemos visto anteriormente. Se llevarán a cabo en el siguiente contexto:

La implementación utilizada es JPA / Hibernate [6]. El proyecto de Eclipse para las pruebas es el siguiente:

El proyecto de Eclipse [1] solo se diferencia del anterior por sus códigos Java [2]. El entorno (bibliotecas – persistence.xml – SGBD – carpetas de configuración, DDL – script Ant) es el que ya se analizó anteriormente, en particular en el párrafo 2.1.5. Este seguirá siendo el caso para los próximos proyectos de Hibernate y, salvo excepciones, no volveremos a abordar este entorno. En particular, los archivos persistence.xml que configuran la capa JPA/Hibernate para diferentes SGBD son los que ya se analizaron y que se encuentran en la carpeta <conf>.

Si tiene alguna duda sobre los procedimientos a seguir, se invita al lector a consultar los que se siguieron en el estudio anterior.

El proyecto Eclipse se encuentra en la carpeta de ejemplos. Lo importaremos.

2.2.4. Generación de la DDL de la base de datos

Siguiendo las instrucciones del párrafo 2.1.7, el archivo DDL obtenido para el SGBD MySQL5 es el siguiente:


    drop table if exists jpa02_hb_personne;

    create table jpa02_hb_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
) ENGINE=InnoDB;

Hibernate reconoció correctamente que la dirección de la persona debía incluirse en la tabla asociada a la @Entity Personne (líneas 11-17).

2.2.5. InitDB

El código de [InitDB] es el siguiente:


package tests;
...

public class InitDB {

    // constantes
    private final static String TABLE_NAME = "jpa02_hb_personne";

    public static void main(String[] args) throws ParseException {

        // Contexto de persistencia
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // se recupera un EntityManager a partir del EntityManagerFactory anterior
        em = emf.createEntityManager();
        // Inicio de la transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // solicitud
        Query sql1;
        // eliminar los elementos de la tabla PERSONNE
        sql1 = em.createNativeQuery("delete from " + TABLE_NAME);
        sql1.executeUpdate();
        // creación de personas
        Personne p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // creación de direcciones
        Adresse a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        Adresse a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        // asociaciones persona <--> dirección
        p1.setAdresse(a1);
        p2.setAdresse(a2);
        // persistencia de personas
        em.persist(p1);
        em.persist(p2);
        // visualización de personas
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // fin de la transacción
        tx.commit();
        // fin de EntityManager
        em.close();
        // fin de EntityManagerFactory
        emf.close();
        // registro
        System.out.println("terminé...");

    }
}

No hay nada nuevo en este código. Ya se ha visto todo. La ejecución de [InitDB] junto con MySQL5 arroja los siguientes resultados:

  • [1]: la salida de la consola
  • [2]: la tabla [jpa02_hb_personne] en la perspectiva SQL Explorer
  • [3] y [4]: su estructura y contenido.

2.2.6. Inicio

La clase [Main] es la siguiente:


package tests;

...
import entites.Adresse;
import entites.Personne;

@SuppressWarnings( { "unused", "unchecked" })
public class Main {

    // constantes
    private final static String TABLE_NAME = "jpa02_hb_personne";

    // Contexto de persistencia
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");

    private static EntityManager em = null;

    // objetos compartidos
    private static Personne p1, p2, newp1;

    private static Adresse a1, a2, a3, a4, newa1, newa4;

    public static void main(String[] args) throws Exception {
        // se recupera un EntityManager a partir del EntityManagerFactory
        em = emf.createEntityManager();

        // limpieza de la base
        log("clean");clean();

        // volcado de tabla
        dumpPersonne();

        // prueba1
        log("test1"); test1();

        // prueba2
        log("test2"); test2();

        // prueba3
        log("test3"); test3();

        // prueba4
        log("test4"); test4();

        // prueba5
        log("test5");test5();

        // fin del contexto de persistencia
        if (em != null && em.isOpen())
            em.close();

        // cierre de EntityManagerFactory
        emf.close();
    }

    // recuperar el EntityManager actual
    private static EntityManager getEntityManager() {
...
    }

    // obtener un nuevo EntityManager
    private static EntityManager getNewEntityManager() {
...
    }

    // mostrar el contenido de la tabla «Persona»
    private static void dumpPersonne() {
...
    }

    // borrar BD
    private static void clean() {
    ...
    }

    // registros
    private static void log(String message) {
...
    }

    // creación de objetos
    public static void test1() throws ParseException {
        // contexto de persistencia
        EntityManager em = getEntityManager();
        // creación de personas
        p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // creación de direcciones
        a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        // asociaciones persona <--> dirección
        p1.setAdresse(a1);
        p2.setAdresse(a2);
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // persistencia de personas
        em.persist(p1);
        em.persist(p2);
        // fin de transacción
        tx.commit();
        // volcado
        dumpPersonne();
    }

    // modificar un objeto del contexto
    public static void test2() {
        // contexto de persistencia
        EntityManager em = getEntityManager();
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // se incrementa el número de hijos de p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // se modifica su estado civil
        p1.setMarie(false);
        // el objeto p1 se guarda automáticamente (verificación de cambios)
        // durante la próxima sincronización (commit o select)
        // fin de la transacción
        tx.commit();
        // se muestra la nueva tabla
        dumpPersonne();
    }

    // eliminar un objeto que pertenece al contexto de persistencia
    public static void test4() {
        // contexto de persistencia
        EntityManager em = getEntityManager();
        // inicio de la transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // se elimina el objeto adjunto p2
        em.remove(p2);
        // fin de la transacción
        tx.commit();
        // se muestra la nueva tabla
        dumpPersonne();
    }

    // desvincular, volver a vincular y modificar
    public static void test5() {
        // nuevo contexto de persistencia
        EntityManager em = getNewEntityManager();
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // se vuelve a vincular p1 al nuevo contexto
        p1 = em.find(Personne.class, p1.getId());
        // fin de transacción
        tx.commit();
        // se cambia la dirección de p1
        p1.getAdresse().setVille("Paris");
        // se muestra la nueva tabla
        dumpPersonne();
    }

}

Una vez más, nada que no hayamos visto antes. La salida de la consola es la siguiente:

main : ----------- clean
[personnes]
main : ----------- test1
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,A[Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]]
P[1,0,Martin,Paul,31/01/2000,true,2,A[8 rue Boileau,null,null,49000,Angers,null,France]]
main : ----------- test2
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,A[Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]]
P[1,1,Martin,Paul,31/01/2000,false,3,A[8 rue Boileau,null,null,49000,Angers,null,France]]
main : ----------- test4
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,A[8 rue Boileau,null,null,49000,Angers,null,France]]
main : ----------- test5
[personnes]
P[1,2,Martin,Paul,31/01/2000,false,3,A[8 rue Boileau,null,null,49000,Paris,null,France]]

Se invita al lector a establecer la relación entre los resultados y el código.

Ahora utilizamos una implementación JPA / Toplink:

El nuevo proyecto de pruebas en Eclipse es el siguiente:

Los códigos Java son idénticos a los del proyecto Hibernate anterior. El entorno (bibliotecas – persistence.xml – SGBD – carpetas de configuración, DDL – script Ant) es el que ya se analizó en el párrafo 2.1.15.2. Este seguirá siendo el caso para los futuros proyectos de Toplink y, salvo excepciones, no volveremos a abordar este entorno. En particular, los archivos persistence.xml que configuran la capa JPA/Toplink para diferentes SGBD son los que ya se analizaron y que se encuentran en la carpeta <conf>.

Si tiene alguna duda sobre los procedimientos a seguir, se invita al lector a consultar los que se siguieron en el estudio anterior.

El proyecto Eclipse se encuentra en [3] dentro de la carpeta de ejemplos [4]. Lo importaremos.

Al ejecutar [InitDB] junto con SGBD y MySQL5, se obtienen los siguientes resultados:

  • [1]: la salida de la consola
  • [2]: las tablas [jpa02_tl_personne] y [SEQENCE] en la perspectiva SQL Explorer
  • [3] y [4]: la estructura y el contenido de [jpa02_tl_personne].

Los scripts SQL generados en ddl/mysql5 [5] son los siguientes:

create.sql


CREATE TABLE jpa02_tl_personne (ID BIGINT NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, VERSION INTEGER NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NBENFANTS INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, CODEPOSTAL VARCHAR(5) NOT NULL, ADR1 VARCHAR(30) NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR3 VARCHAR(30), CEDEX VARCHAR(3), ADR2 VARCHAR(30), PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

drop.sql


DROP TABLE jpa02_tl_personne
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'

2.3. Ejemplo 3: relación uno a uno mediante una clave externa

2.3.1. e del esquema de la base de datos

1
2

    alter table jpa03_hb_personne 
        drop 
        foreign key FKFBBBFDD05FE379D0;

    drop table if exists jpa03_hb_adresse;

    drop table if exists jpa03_hb_personne;

    create table jpa03_hb_adresse (
        id bigint not null auto_increment,
        version integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa03_hb_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adresse_id bigint not null unique,
        primary key (id)
    ) ENGINE=InnoDB;

    alter table jpa03_hb_personne 
        add index FKFBBBFDD05FE379D0 (adresse_id), 
        add constraint FKFBBBFDD05FE379D0 
        foreign key (adresse_id) 
references jpa03_hb_adresse (id);
  • en [1]: la base de datos. En esta ocasión, la dirección de la persona se almacena en una tabla propia, [adresse]. La tabla [personne] está vinculada a esta tabla mediante una clave externa.
  • en [2]: la tabla DDL generada por Hibernate para MySQL5:
    • líneas 9-20: la tabla [adresse] que se vinculará a la clase [Adresse], la cual se ha convertido en un objeto @Entity.
    • línea 10: la clave primaria de la tabla [adresse]
    • línea 30: en lugar de una dirección completa, ahora se encuentra en la tabla [personne] el identificador [adresse_id] de dicha dirección.
    • líneas 34-38: persona (adresse_id) es una clave externa sobre dirección (id).

2.3.2. Los objetos @Entity que representan la base de datos

Una persona con dirección ahora se representa mediante la siguiente clase [Personne]:


package entites;
...
@Entity
@Table(name = "jpa03_hb_personne")
public class Personne implements Serializable{

    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false, unique = true)
    private String nom;

    @Column(length = 30, nullable = false)
    private String prenom;

    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(nullable = false)
    private boolean marie;

    @Column(nullable = false)
    private int nbenfants;

    @OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
    private Adresse adresse;
...
}
  • líneas 32-34: la dirección de la persona
    • línea 32: la anotación @OneToOne indica una relación uno a uno: una persona tiene al menos una y como máximo una dirección. El atributo cascade = CascadeType.ALL significa que cualquier operación (persist, merge, remove) sobre la @Entity [Personne] debe propagarse a la @Entity [Adresse]. Desde el punto de vista del contexto de persistencia em, esto significa lo siguiente. Si p es una persona y tiene su dirección:
      • una operación explícita em.persist(p) provocará una operación implícita em.persist(a)
      • una operación explícita em.merge(p) dará lugar a una operación implícita em.merge(a)
      • Una operación explícita em.remove(p) dará lugar a una operación implícita em.remove(a)

La experiencia demuestra que estas cascadas implícitas no son la solución definitiva. El desarrollador termina olvidando lo que hacen. Es posible que se prefieran operaciones explícitas en el código. Existen diferentes tipos de cascadas. La anotación @OneToOne podría haberse escrito de la siguiente manera:


//@OneToOne(cascada = CascadeType.ALL, recuperación = FetchType.LAZY)
@OneToOne(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.REMOVE}, fetch=FetchType.LAZY)

El atributo cascade admite aquí como valor una matriz de constantes que especifican los tipos de cascadas deseadas.

El atributo fetch=FetchType.LAZY le indica a Hibernate que cargue la dependencia en el último momento. Cuando se agrega una lista de personas al contexto de persistencia, no necesariamente se quiere incluir sus direcciones. Por ejemplo, es posible que solo se quiera esa dirección para una persona específica seleccionada por un usuario a través de una interfaz web. El atributo fetch=FetchType.EAGER, por su parte, solicita la carga inmediata de las dependencias.

  • (continuación)
    • línea 33: la anotación @JoinColumn define la clave externa que tiene la tabla de la @Entity [Personne] sobre la tabla de la @Entity [Adresse]. El atributo name define el nombre de la columna que sirve como clave externa. El atributo unique=true impone una relación uno a uno: no puede haber dos veces el mismo valor en la columna [adresse_id]. El atributo nullable=false obliga a que una persona tenga una dirección.

La dirección de una persona ahora está representada por la siguiente @Entity [Adresse]:


package entites;

...
@Entity
@Table(name = "jpa03_hb_adresse")
public class Adresse implements Serializable {

    // campos
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false)
    private String adr1;

    @Column(length = 30)
    private String adr2;

    @Column(length = 30)
    private String adr3;

    @Column(length = 5, nullable = false)
    private String codePostal;

    @Column(length = 20, nullable = false)
    private String ville;

    @Column(length = 3)
    private String cedex;

    @Column(length = 20, nullable = false)
    private String pays;

    @OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
    private Personne personne;

    // constructores
    public Adresse() {

    }
...
}
  • línea 4: la clase [Adresse] se convierte en un objeto @Entity. Por lo tanto, será el objeto de una tabla en la base de datos.
  • líneas 9-12: al igual que cualquier objeto @Entity, [Adresse] tiene una clave primaria. Se le ha asignado el nombre Id y presenta las mismas anotaciones (estándar) que la clave primaria Id de la @Entity [Personne].
  • líneas 39-40: la relación uno a uno con la @Entity [Personne]. Aquí hay varios detalles a tener en cuenta:
    • en primer lugar, el campo personne no es obligatorio. Nos permite, a partir de una dirección, identificar a la única persona que tiene esa dirección. Si no hubiéramos querido esta facilidad, el campo personne no existiría y todo seguiría funcionando igual.
    • La relación uno a uno que vincula las dos entidades [Personne] y [Adresse] ya se ha configurado en la @Entity [Personne]:

    @OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
private Adresse adresse;

Para que las dos configuraciones uno-a-uno no entren en conflicto entre sí, una se considera como principale y la otra como inverse. Es la relación denominada principale la que es administrada por el puente objeto-relacional. La otra relación, denominada inverse, no se administra directamente, sino que se administra indirectamente a través de la relación principale. En @Entity [Adresse]:


@OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
private Personne personne;

es el atributo mappedBy el que establece la relación uno a uno anterior, la relación inverse de la relación uno a uno principale, definida por el campo adresse de @Entity [Personne].

2.3.3. El proyecto Eclipse / Hibernate 1

La implementación JPA utilizada aquí es la de Hibernate. El proyecto de Eclipse para las pruebas es el siguiente:

El proyecto [3] se encuentra en la carpeta de ejemplos [4]. Lo importaremos.

2.3.4. Generación del archivo DDL de la base de datos

Siguiendo las instrucciones del párrafo 2.1.7, el archivo DDL obtenido para el SGBD y el MySQL5 es el que se muestra al inicio de este párrafo.

2.3.5. InitDB

El código de [InitDB] es el siguiente:


package tests;
...
import entites.Adresse;
import entites.Personne;

public class InitDB {

    // constantes
    private final static String TABLE_PERSONNE = "jpa03_hb_personne";

    private final static String TABLE_ADRESSE = "jpa03_hb_adresse";

    public static void main(String[] args) throws ParseException {
        // Contexto de persistencia
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // se recupera un EntityManager a partir del EntityManagerFactory anterior
        em = emf.createEntityManager();
        // Inicio de la transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // solicitud
        Query sql1;
        // eliminar los elementos de la tabla PERSONNE
        sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE);
        sql1.executeUpdate();
        // eliminar los elementos de la tabla ADRESSE
        sql1 = em.createNativeQuery("delete from " + TABLE_ADRESSE);
        sql1.executeUpdate();
        // creación de personas
        Personne p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // crear direcciones
        Adresse a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        Adresse a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        Adresse a3 = new Adresse("x", "x", "x", "x", "x", "x", "x");
        Adresse a4 = new Adresse("y", "y", "y", "y", "y", "y", "y");
        // asociaciones persona <--> dirección
        p1.setAdresse(a1);
        a1.setPersonne(p1);
        p2.setAdresse(a2);
        a2.setPersonne(p2);
        // persistencia de las personas y, en cascada, de sus direcciones
        em.persist(p1);
        em.persist(p2);
        // y de las direcciones a3 y a4 no vinculadas a personas
        em.persist(a3);
        em.persist(a4);
        // visualización de personas
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // visualización de direcciones
        System.out.println("[adresses]");
        for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
            System.out.println(a);
        }

        // fin de la transacción
        tx.commit();
        // fin EntityManager
        em.close();
        // fin de EntityManagerFactory
        emf.close();
        // registro
        System.out.println("terminé...");

    }
}

Solo comentaremos lo que presenta un interés nuevo respecto a lo que ya se ha estudiado:

  • líneas 31-32: se crean dos personas
  • líneas 34-37: se crean cuatro direcciones
  • líneas 39-42: se asocian las personas (p1, p2) a las direcciones (a1, a2). Las direcciones (a3, a4) quedan huérfanas. Ninguna persona las referencia. El código DDL lo permite. Si bien una persona tiene necesariamente una dirección, lo contrario no es cierto.
  • líneas 44-45: se conservan las personas (p1, p2). Como hemos asignado un atributo cascade = CascadeType.ALL a la relación uno-a-uno que vincula a una persona con su dirección, las direcciones (a1, a2) de estas dos personas también deberían recibir un persist. Eso es lo que queremos verificar. Para las direcciones huérfanas (a3, a4), nos vemos obligados a hacerlo de manera explícita (líneas 47-48).
  • líneas 51-53: visualización de la tabla de personas
  • líneas 56-57: visualización de la tabla de direcciones

La ejecución de [InitDB] junto con MySQL5 arroja los siguientes resultados:

  • [1]: la salida de la consola
  • [2]: las tablas de [jpa03_hb_*] en la perspectiva de SQL Explorer
  • [3]: la tabla de personas
  • [4]: la tabla de direcciones. Todas están ahí. También cabe destacar la relación que existe entre la columna [adresse_id] en [3] y la columna [id] en [4] (clave externa).

2.3.6. Inicio

La clase [Main] encadena seis pruebas que vamos a revisar.

2.3.6.1. Prueba 1

Esta prueba es la siguiente:


// creación de objetos
    public static void test1() throws ParseException {
        // contexto de persistencia
        EntityManager em = getEntityManager();
        // creación de personas
        p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // creación de direcciones
        a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        a3 = new Adresse("x", "x", "x", "x", "x", "x", "x");
        a4 = new Adresse("y", "y", "y", "y", "y", "y", "y");
        // asociaciones persona <--> dirección
        p1.setAdresse(a1);
        a1.setPersonne(p1);
        p2.setAdresse(a2);
        a2.setPersonne(p2);
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // persistencia de personas
        em.persist(p1);
        em.persist(p2);
        // y de las direcciones a3 y a4 no vinculadas a personas
        em.persist(a3);
        em.persist(a4);
        // fin de la transacción
        tx.commit();
        // se muestran las tablas
        dumpPersonne();
        dumpAdresse();
    }

Este código se ha tomado de [InitDB]. Su resultado es el siguiente:

1
2
3
4
5
6
7
8
9
main : ----------- test1
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,0,Martin,Paul,31/01/2000,true,2,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[2,0,Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]

Se han completado ambas tablas.

2.3.6.2. Prueba 2

Esta prueba es la siguiente:


    // modificar un objeto del contexto
    public static void test2() {
        // contexto de persistencia
        EntityManager em = getEntityManager();
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // se incrementa el número de hijos de p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // se modifica su estado civil
        p1.setMarie(false);
        // el objeto p1 se guarda automáticamente (verificación de cambios)
        // durante la próxima sincronización (commit o select)
        // fin de la transacción
        tx.commit();
        // se muestra la nueva tabla
        dumpPersonne();
}

Su resultado es el siguiente:

1
2
3
4
main : ----------- test2
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
  • línea 4: la persona p1 vio aumentar su número de hijos en 1, y su versión pasó de 0 a 1

2.3.6.3. Prueba 4

Esta prueba es la siguiente:


    // eliminar un objeto que pertenece al contexto de persistencia
    public static void test4() {
        // contexto de persistencia
        EntityManager em = getEntityManager();
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // se elimina el objeto adjunto p2
        em.remove(p2);
        // fin de transacción
        tx.commit();
        // se muestran las nuevas tablas
        dumpPersonne();
        dumpAdresse();
}
  • línea 9: se elimina a la persona p2. Esta tiene una relación en cascada con la dirección a2. Por lo tanto, la dirección a2 también debería eliminarse.

El resultado de la prueba 4 es el siguiente:

main : ----------- test1
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,0,Martin,Paul,31/01/2000,true,2,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[2,0,Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test2
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
main : ----------- test4
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
  • La persona p2, presente en la línea 3 de la prueba 1, ya no aparece en la prueba 4
  • Lo mismo ocurre con su dirección a2, que aparece en la línea 7 de la prueba 1 y no aparece en la prueba 4.

2.3.6.4. Prueba 5

Esta prueba es la siguiente:


// desvincular, volver a vincular y modificar
    public static void test5() {
        // nuevo contexto de persistencia
        EntityManager em = getNewEntityManager();
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // se vuelve a vincular p1 al nuevo contexto
        p1 = em.find(Personne.class, p1.getId());
        // se cambia la dirección de p1
        p1.getAdresse().setVille("Paris");
        // fin de la transacción
        tx.commit();
        // se muestran las nuevas tablas
        dumpPersonne();
        dumpAdresse();
    }
  • Línea 4: tenemos un contexto de persistencia nuevo, por lo tanto, vacío.
  • Línea 9: se inserta la persona p1 en él. Se busca p1 en la base de datos porque no se encuentra en el contexto. Los elementos que dependen de p1 (su dirección), por su parte, no se recuperan de la base de datos porque se escribió:

    @OneToOne(..., fetch=FetchType.LAZY)

Este es el concepto de «lazy loading» o «carga diferida»: las dependencias de un objeto persistente solo se cargan en memoria cuando se necesitan.

  • línea 11: se modifica el campo «ciudad» de la dirección de p1. Debido a getAdresse, y si la dirección de p1 aún no se encontraba en el contexto de persistencia, se cargará allí mediante una lectura de la base de datos.
  • línea 13: se confirma la transacción, lo que provocará la sincronización del contexto de persistencia con la base de datos. Este detectará que la dirección de la persona p1 ha sido modificada y la guardará.

La ejecución de test5 arroja los siguientes resultados:

main : ----------- test4
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test5
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
  • La persona p1 (línea 3 de test4, línea 10 de test5) vio cómo su ciudad pasó de ser Angers (línea 5 de test4) a París (línea 12 de test5).

2.3.6.5. Prueba 6

Esta prueba es la siguiente:


// eliminar un objeto Dirección
    public static void test6() {
        EntityTransaction tx = null;
        // nuevo contexto de persistencia
        EntityManager em = getNewEntityManager();
        // inicio de transacción
        tx = em.getTransaction();
        tx.begin();
        // se vuelve a vincular la dirección a3 al nuevo contexto
        a3 = em.find(Adresse.class, a3.getId());
        System.out.println(a3);
        // se elimina
        em.remove(a3);
        // fin de transacción
        tx.commit();
        // volcado de la tabla Dirección
        dumpAdresse();
    }
  • línea 5: nos encontramos en un contexto de persistencia nuevo, por lo tanto, vacío.
  • línea 10: se ingresa la dirección a3 en el contexto de persistencia
  • línea 13: la eliminamos. Era una dirección huérfana (no vinculada a una persona). Por lo tanto, la eliminación es posible.

El resultado de la ejecución es el siguiente:

main : ----------- test5
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
  • la dirección a3 de la prueba 5 (línea 6) ha desaparecido de las direcciones de la prueba 6 (líneas 11-12)

2.3.6.6. Prueba 7

Esta prueba es la siguiente:


// revertir
    public static void test7() {
        EntityTransaction tx = null;
        try {
            // nuevo contexto de persistencia
            EntityManager em = getNewEntityManager();
            // inicio de transacción
            tx = em.getTransaction();
            tx.begin();
            // se vuelve a vincular la dirección a1 al nuevo contexto
            newa1 = em.find(Adresse.class, a1.getId());
            // se vuelve a vincular la dirección a4 al nuevo contexto
            newa4 = em.find(Adresse.class, a4.getId());
            // se intenta eliminarlas; debería lanzar una excepción, ya que no se puede eliminar una dirección vinculada a una persona, lo cual es el caso de newa1
            em.remove(newa4);
            em.remove(newa1);
            // fin de la transacción
            tx.commit();
        } catch (RuntimeException e1) {
            // Hubo un problema
            System.out.format("Erreur dans transaction [%s%n%s%n%s%n%s]%n", e1.getClass().getName(), e1.getMessage(), e1.getCause(), e1.getCause()
                    .getCause());
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s]%n", e2.getMessage());
            }
            // Se abandona el contexto actual
            em.clear();
        }
        // volcado: la tabla Dirección no debe haber cambiado debido a la reversión
        dumpAdresse();
    }
  • test7: se prueba la reversión de una transacción
    • Línea 6: nos encontramos en un contexto de persistencia nuevo, por lo tanto, vacío.
    • línea 11: se agrega la dirección a1 al contexto de persistencia, bajo la referencia newa1
    • línea 13: se agrega la dirección a4 al contexto de persistencia, bajo la referencia newa4
    • líneas 15-16: se eliminan las dos direcciones newa1 y newa4. newa1 es la dirección de la persona p1 y, por lo tanto, en la base de datos p1 hace referencia a newa1 mediante una clave externa. Por lo tanto, la eliminación de newa1 fallará y generará una excepción al sincronizar el contexto de persistencia al confirmar la transacción (línea 18). Esta sufrirá un rollback (línea 25) y, por lo tanto, las dos operaciones de la transacción serán revertidas. Por lo tanto, deberíamos observar que la dirección newa4, que podría haberse eliminado legalmente, no lo ha sido.

La ejecución arroja el siguiente resultado:


main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test7
Erreur dans transaction [javax.persistence.RollbackException
Error while commiting the transaction
org.hibernate.ObjectDeletedException: deleted entity passed to persist: [entites.Adresse#<null>]
null]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
  • la tabla de direcciones de la prueba 7 (líneas 12-13) es idéntica a la de la prueba 6 (líneas 4-5). Parece que la reversión se ha llevado a cabo. Dicho esto, el mensaje de error de la línea 9 es un enigma y merece ser investigado. Al parecer, la excepción que se produjo no es la esperada. Hay que pasar los registros de Hibernate en log4j.properties al modo DEBUG para entenderlo mejor:

# Opción de registro de raíz
log4j.rootLogger=ERROR, stdout

# Opciones de registro de Hibernate (INFO solo muestra mensajes de inicio)
log4j.logger.org.hibernate=DEBUG

Se observa entonces que, cuando se colocó la dirección a1 en el contexto de persistencia, Hibernate también colocó allí a la persona p1, probablemente debido a la relación uno a uno de la @Entity [Adresse]:


    @OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
private Personne personne;

Aunque aquí se solicitó el «LazyLoading», la dependencia [Personne] se carga de inmediato. Esto probablemente significa que el atributo fetch=FetchType.LAZY no tiene sentido aquí. Luego se observa que, al confirmar la transacción, Hibernate ha preparado la eliminación de las direcciones a1 y a4, pero también el almacenamiento de la persona p1. Y ahí es donde se produce la excepción: debido a que la persona p1 tiene una cascada en su dirección, Hibernate quiere persistir también la dirección a1, a pesar de que acaba de ser eliminada. Es Hibernate quien lanza la excepción y no el controlador JDBC. De ahí el mensaje de la línea 9 anterior. Por otra parte, se puede observar que el comando rollback de la línea 25 nunca se ejecuta porque la transacción se ha vuelto inactiva. La prueba de la línea 24 impide, por lo tanto, el comando rollback.

Por lo tanto, no se ha alcanzado el objetivo deseado: mostrar una reversión. De hecho, no se emitió ninguna orden SQL en la base de datos. Cabe destacar algunos puntos:

  • la importancia de activar registros detallados para comprender qué hace el ORM
  • si bien un ORM puede facilitarle la vida al desarrollador, también puede complicársela al ocultar comportamientos que el desarrollador necesitaría conocer. En este caso, el modo de carga de las dependencias de una @Entity.

2.3.7. Proyecto Eclipse / Hibernate 2

Copiamos y pegamos el proyecto Eclipse / Hibernate para modificar ligeramente la configuración de los objetos @Entity:

El proyecto se encuentra como [3] en la carpeta de ejemplos [4]. Lo importaremos.

Modificamos únicamente la @Entity [Adresse] para que ya no tenga una relación inversa uno a uno con la @Entity [Personne]:


package entites;
...
@Entity
@Table(name = "jpa04_hb_adresse")
public class Adresse implements Serializable {

    // campos
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false)
    private String adr1;

    ...

    @Column(length = 20, nullable = false)
    private String pays;

//     @OneToOne(mappedBy = "dirección", fetch=FetchType.LAZY)
//     private Persona persona;

    // constructores
    public Adresse() {

    }
  • líneas 25-26: se elimina la relación inversa @OneToOne. Es importante entender que una relación inversa nunca es indispensable. Solo la relación principal lo es. La relación inversa puede utilizarse por conveniencia. En este caso, permitía obtener de manera sencilla el propietario de una dirección. Una relación inversa siempre puede sustituirse por una consulta JPQL. Eso es lo que vamos a mostrar en el siguiente ejemplo.

Los programas de prueba se mantienen tal cual. El que nos interesa es únicamente la prueba 7, aquella en la que vimos la relación inversa uno a uno en acción. Además, agregamos una prueba 8 para mostrar cómo, sin la relación inversa Dirección -> Persona, se puede, no obstante, recuperar la persona que tiene dicha dirección.

La prueba 7 no cambia. Su ejecución ahora arroja los siguientes resultados (registros desactivados):


main : ----------- prueba6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
main : ----------- prueba7
Erreur dans transaction [javax.persistence.RollbackException
Error while commiting the transaction
org.hibernate.exception.ConstraintViolationException: could not delete: [entites.Adresse#1]
java.sql.SQLException: Cannot delete or update a parent row: a foreign key constraint fails (`jpa/jpa04_hb_personne`, CONSTRAINT `FKEA3F04515FE379D0` FOREIGN KEY (`adresse_id`) REFERENCES `jpa04_hb_adresse` (`id`))]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
  • Esta vez sí se produce la excepción esperada: la lanzada por el controlador JDBC porque intentamos eliminar de la tabla [adresse] una fila a la que hace referencia una clave externa de una fila de la tabla [personne]. La fila [10] explica claramente la causa del error.
  • La reversión se llevó a cabo correctamente: al finalizar la prueba 7, la tabla [adresse] (líneas 12-13) es la misma que teníamos al finalizar la prueba 6 (líneas 4-5).

¿Cuál es la diferencia con la prueba 7 del proyecto Eclipse anterior? ¿Por qué aquí se produce una excepción Jdbc que no se había presentado en la prueba anterior? Debido a que la @Entity [Adresse] ya no tiene una relación inversa uno a uno con la @Entity [Personne], Hibernate la gestiona de manera aislada. Cuando la dirección newa1 se incorporó al contexto de persistencia, Hibernate no incluyó también en ese contexto a la persona p1 que tiene esa dirección. Por lo tanto, la eliminación de las direcciones newa1 y newa4 se realizó sin que la entidad Personne estuviera presente en el contexto.

Ahora bien, ¿cómo es posible que, a partir de la dirección newa1, se pueda llegar a la persona p1 que tiene esa dirección? Es una pregunta legítima. La prueba 8 que sigue responde a ella:


// relación inversa uno a uno
    // realizada mediante una consulta JPQL
    public static void test8() {
        EntityTransaction tx = null;
        // nuevo contexto de persistencia
        EntityManager em = getNewEntityManager();
        // inicio de transacción
        tx = em.getTransaction();
        tx.begin();
        // se vuelve a vincular la dirección a1 al nuevo contexto
        newa1 = em.find(Adresse.class, a1.getId());
        // se recupera la persona propietaria de esta dirección
        Personne p1 = (Personne) em.createQuery("select p from Personne p join p.adresse a where a.id=:adresseId").setParameter("adresseId", newa1.getId())
                .getSingleResult();
        // se muestran
        System.out.println("adresse=" + newa1);
        System.out.println("personne=" + p1);
        // fin de la transacción
        tx.commit();
    }
  • línea 6: nuevo contexto de persistencia vacío
  • líneas 8-9: inicio de transacción
  • línea 11: la dirección a1 se incorpora al contexto de persistencia y es referenciada por newa1.
  • línea 13: se recupera la persona p1, que tiene la dirección newa1, mediante una consulta JPQL. Sabemos que [Personne] y [Adresse] están vinculadas por una relación de clave externa. En la clase [Personne], es el campo [adresse], que tiene la anotación @OneToOne, el que materializa esta relación. La instrucción JPQL «select p from Personne p join p.adresse a» realiza una unión entre las tablas [personne] y [adresse]. El equivalente SQL generado en una consola de Hibernate (véanse los ejemplos del párrafo 2.1.12) es el siguiente:
SQL #0 tipos: entites.Personne
-----------------
select
  personne0_.id as id1_,
  personne0_.version as version1_,
  personne0_.nom as nom1_,
  personne0_.prenom as prenom1_,
  personne0_.datenaissance as datenais5_1_,
  personne0_.marie as marie1_,
  personne0_.nbenfants as nbenfants1_,
  personne0_.adresse_id as adresse8_1_ 
 from
  jpa04_hb_personne personne0_ 
 inner join
  jpa04_hb_adresse adresse1_ 
on personne0_.adresse_id=adresse1_.id

Se observa claramente la unión de las dos tablas. Cada persona está ahora vinculada a su dirección. Queda por precisar que solo nos interesa la dirección newa1. La consulta queda así: «select p from Personne p join p.adresse a where a.id=:adresseId». Cabe destacar el uso de los alias p y a. Las consultas JPQL utilizan los alias de manera intensiva. Así, la expresión «from Persona p join p.adresse a» hace que una persona quede representada por el alias p y su dirección (p.adresse) por el alias a. La operación de restricción «where a.id=:adresseId» limita las filas solicitadas únicamente a las personas p que tienen el valor:adresseId como identificador de su dirección a. :adresseId se denomina parámetro, y la orden JPQL es una orden parametrizada. Al ejecutarse, este parámetro debe recibir un valor. Este es el método

Query setParameter(String nomParamètre, Object valeurParamètre)

el que permite asignar un valor a un parámetro identificado por su nombre. Cabe señalar que setParameter devuelve un objeto Query, al igual que el método createQuery. De manera que se pueden encadenar las llamadas a los métodos [em.createQuery(...).setParameter(...).getSingleResult(...)], ya que los métodos [setParameter, getSingleResult] forman parte de la interfaz Query. El método [getSingleResult] se utiliza para las consultas Select que solo devuelven un único resultado. Este es el caso aquí.

  • líneas 16-17: se muestra la dirección newa1 y la persona p1 que tiene esa dirección, para verificar.

El resultado obtenido es el siguiente:

1
2
3
main : ----------- test8
adresse=A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
personne=P[1,1,Martin,Paul,31/01/2000,false,3,1]

Es correcto. De este ejemplo se desprende que la relación inversa uno a uno de la @entity [Adresse] hacia la @entity [Personne] no era indispensable. La experiencia ha demostrado aquí que su eliminación dio lugar a un comportamiento más predecible del código. Esto suele ser así.

2.3.8. Consola de Hibernate

En la prueba 8 anterior se utilizó un comando JPQL para realizar una unión entre las entidades Personne y Adresse. Aunque son similares al lenguaje SQL, los lenguajes JPQL, JPA o HQL de Hibernate requieren un proceso de aprendizaje, y la consola de Hibernate es excelente para ello. Ya la hemos utilizado en el apartado 2.1.12 para trabajar con una sola tabla. Ahora lo repetimos aquí para trabajar con dos tablas vinculadas por una relación de clave externa.

Creemos una consola de Hibernate para nuestro proyecto actual de Eclipse:

  • [1]: cambiamos a la perspectiva [Hibernate Console] (Ventana / Abrir perspectiva / Otra)
  • [2]: creamos una nueva configuración
  • con el botón [4]; seleccionamos el proyecto Java para el que se creará la configuración de Hibernate. Su nombre aparece en [3].
  • En [5], le damos el nombre que queramos a esta configuración. Aquí, hemos tomado el nombre del proyecto Java.
  • En [6], indicamos que utilizamos una configuración JPA para que la herramienta sepa que debe procesar el archivo [META-INF/persistence.xml]
  • en [7]: en este archivo [META-INF/persistence.xml] indicamos que se debe utilizar la unidad de persistencia llamada jpa.
  • En [8], validamos la configuración.

A continuación, se debe ejecutar el SGBD. Aquí se trata del MySQL5.

  • en [1]: la configuración creada presenta un árbol de tres ramas
  • en [2]: la rama [Configuration] enumera los objetos que la consola utilizó para configurarse: en este caso, las @Entity Personne y Adresse.
  • En [3]: la Session Factory es un concepto de Hibernate similar al de EntityManager de JPA. Establece el puente entre objetos y relaciones gracias a los objetos de la rama [Configuration]. En [3] se presentan los objetos del contexto de persistencia, en este caso nuevamente las @Entity Personne y Adresse.
  • En [4]: la base de datos a la que se accede mediante la configuración que se encuentra en [persistence.xml]. Aquí se encuentran las tablas [jpa04_hb_*] generadas por nuestro proyecto actual de Eclipse.
  • en [1], se crea un editor HQL
  • en el editor HQL,
    • en [2], se elige la configuración de Hibernate que se va a utilizar si hay varias (como es el caso aquí)
    • en [3], se escribe el comando JPQL que se desea ejecutar; en este caso, el comando JPQL de la prueba 8
    • en [4], lo ejecutamos
    • en [5], se obtienen los resultados de la consulta en la ventana [Hibernate Query Result].
    • En [6], la ventana [Hibernate Dynamic SQL preview] permite ver la consulta SQL que se ha ejecutado.

Otra forma de obtener el mismo resultado:

  • en [1]: el comando JPQL realiza la unión de las entidades Personne y Adresse. [ref1] denomina a esta forma «unión theta».
  • en [2]: el equivalente SQL
  • en [3]: el resultado

Una tercera forma aceptada únicamente por Hibernate (HQL):

  • en [1]: el comando HQL. JPQL no acepta la notación p.adresse.id. Solo acepta un nivel de indirección.
  • en [2]: el equivalente SQL. Se observa que evita la unión entre tablas.
  • en [3]: el resultado

Aquí hay otros ejemplos:

  • en [1]: la lista de personas con sus direcciones
  • en [2]: el equivalente SQL.
  • en [3]: el resultado
  • en [1]: la lista de direcciones con su propietario, si lo hay, o ninguno si no lo hay (unión externa derecha: la entidad Adresse, que proporcionará las filas sin relación con Personne, se encuentra a la derecha de la palabra clave join).
  • en [2]: el equivalente a SQL.
  • en [3]: el resultado

Cabe señalar que solo la entidad Personne tiene una relación con la entidad Adresse. Lo contrario ya no es cierto desde que se eliminó la relación inversa uno a uno denominada personne en la entidad Adresse. Si esta relación inversa existiera, se podría haber escrito:

  • en [1]: la lista de direcciones con su propietario, si lo hay, o ninguna si no lo hay (unión externa izquierda: la entidad Adresse, que proporcionará las filas sin relación con Personne, se encuentra a la izquierda de la palabra clave join).
  • en [2]: el equivalente a SQL.
  • en [3]: el resultado

Recomendamos encarecidamente al lector que practique con el lenguaje JPQL utilizando la consola de Hibernate.

Ahora utilizamos una implementación JPA / Toplink:

El nuevo proyecto de pruebas en Eclipse es el siguiente:

Los códigos Java son idénticos a los del proyecto Hibernate anterior. El entorno (bibliotecas – persistence.xml – SGBD – carpetas conf, ddl – script ant) es el que se analizó en el párrafo 2.1.15.2. El proyecto de Eclipse [3] se encuentra en la carpeta de ejemplos [4]. Lo importaremos.

El archivo <persistence.xml> se modifica en un punto, el de las entidades declaradas:


    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  proveedor -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- clases persistentes -->
        <class>entites.Personne</class>
        <class>entites.Adresse</class>
        <!-- propiedades de la unidad de persistencia -->
...
  • líneas 5 y 6: las dos entidades gestionadas

La ejecución de [InitDB] junto con SGBD y MySQL5 arroja los siguientes resultados:

En [1], la salida de la consola; en [2], las dos tablas [jpa04_tl] generadas; y en [3], los scripts SQL generados. Su contenido es el siguiente:

create.sql


CREATE TABLE jpa04_tl_personne (ID BIGINT NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, VERSION INTEGER NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NBENFANTS INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa04_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, ADR1 VARCHAR(30) NOT NULL, VILLE VARCHAR(20) NOT NULL, VERSION INTEGER NOT NULL, CEDEX VARCHAR(3), ADR2 VARCHAR(30), PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa04_tl_personne ADD CONSTRAINT FK_jpa04_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa04_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

drop.sql


ALTER TABLE jpa04_tl_personne DROP FOREIGN KEY FK_jpa04_tl_personne_adresse_id
DROP TABLE jpa04_tl_personne
DROP TABLE jpa04_tl_adresse
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'

2.4. Ejemplo 4: relación uno a varios

2.4.1. El esquema de la base de datos « »

1
2

    alter table jpa06_article 
        drop 
        foreign key FKFFBDD9D8ECCE8750;

    drop table if exists jpa06_article;

    drop table if exists jpa06_categorie;

    create table jpa06_article (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        categorie_id bigint not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa06_categorie (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        primary key (id)
    ) ENGINE=InnoDB;

    alter table jpa06_article 
        add index FKFFBDD9D8ECCE8750 (categorie_id), 
        add constraint FKFFBDD9D8ECCE8750 
        foreign key (categorie_id) 
references jpa06_categorie (id);
  • en [1], la base de datos, y en [2], su DDL (MySQL5)

Un artículo A(id, versión, nombre) pertenece exactamente a una categoría C(id, versión, nombre). Una categoría C puede contener 0, 1 o varios artículos. Existe una relación uno-a-varios (Categoría -> Artículo) y la relación inversa varios-a-uno (Artículo -> Categoría). Esta relación se materializa mediante la clave externa que posee la tabla [article] sobre la tabla [categorie] (líneas 24-28 de la DDL).

2.4.2. Los objetos @Entity que representan la base de datos

Un artículo se representa mediante la siguiente @Entity [Article]:


package entites;

...
@Entity
@Table(name="jpa05_hb_article")
public class Article implements Serializable {

    // campos
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @SuppressWarnings("unused")
    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    // relación principal Artículo (muchos) -> Categoría (uno)
    // implementada mediante una clave externa (categorie_id) en Artículo
    // 1 Artículo tiene necesariamente 1 Categoría (nullable=false)
    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = "categorie_id", nullable = false)
    private Categorie categorie;

    // constructores
    public Article() {
    }

    // getters y setters
    ...
    // toString
    public String toString() {
        return String.format("Article[%d,%d,%s,%d]", id, version, nom, categorie.getId());
    }

}
  • líneas 9-11: clave primaria de la @Entity
  • líneas 13-15: su número de versión
  • líneas 17-18: nombre del artículo
  • líneas 20-25: relación «muchos a uno» que vincula la @Entity Article con la @Entity Categorie:
    • línea 23: la anotación ManyToOne. El «Many» se refiere a la @Entity Article en la que nos encontramos y el «One» a la @Entity Categorie (línea 25). Una categoría (One) puede tener varios artículos (Many).
    • línea 24: la anotación ManyToOne define la columna de clave externa en la tabla [article]. Se llamará (name) categorie_id y cada fila deberá tener un valor en esta columna (nullable=false).
    • línea 25: la categoría a la que pertenece el artículo. Cuando un artículo se coloque en el contexto de persistencia, se solicita que su categoría no se incluya allí de inmediato (fetch=FetchType.LAZY, línea 23). No sabemos si esta solicitud tiene sentido. Ya lo veremos.

Una categoría está representada por la siguiente @Entity [Categorie]:


package entites;
...
@Entity
@Table(name="jpa05_hb_categorie")
public class Categorie implements Serializable {

    // campos
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @SuppressWarnings("unused")
    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    // relación inversa Categoría (uno) -> Artículo (muchos) de la relación Artículo (muchos) -> Categoría (uno)
    // inserción en cascada de Categoría -> inserción de Artículos
    // cascada de actualización de Categoría -> actualización de Artículos
    // eliminación en cascada de Categoría -> eliminación de Artículos
    @OneToMany(mappedBy = "categorie", cascade = { CascadeType.ALL })
    private Set<Article> articles = new HashSet<Article>();

    // constructores
    public Categorie() {
    }

    // getters y setters
...
    // toString
    public String toString() {
        return String.format("Categorie[%d,%d,%s]", id, version, nom);
    }

    // asociación bidireccional Categoría <--> Artículo
    public void addArticle(Article article) {
        // el artículo se agrega a la colección de artículos de la categoría
        articles.add(article);
        // El artículo cambia de categoría
        article.setCategorie(this);
    }
}
  • líneas 8-11: la clave primaria de la @Entity
  • líneas 12-14: su versión
  • líneas 16-17: el nombre de la categoría
  • líneas 19-24: el conjunto (set) de artículos de la categoría
    • línea 23: la anotación @OneToMany indica una relación uno a varios. El «One» se refiere a la @Entity [Categorie] en la que nos encontramos, y el «Many» al tipo [Article] de la línea 24: una (One) categoría tiene varios (Many) artículos.
    • línea 23: la anotación es la inversa (mappedBy) de la anotación ManyToOne colocada en el campo categorie de la @Entity Article: mappedBy=categoría. La relación ManyToOne, definida en el campo categorie de la @Entity Article, es la relación principal. Es indispensable. Esta relación representa la relación de clave externa que vincula la @Entity Article con la @Entity Categorie. La relación OneToMany, definida sobre el campo articles de la @Entity Categorie, es la relación inversa. No es indispensable. Es una facilidad para obtener los artículos de una categoría. Sin esta facilidad, dichos artículos se obtendrían mediante una consulta JPQL.
    • línea 23: cascadeType.ALL solicita que las operaciones (persist, merge, remove) realizadas en una @Entity Categorie se apliquen en cascada a sus artículos.
    • línea 24: los artículos de una categoría se colocarán en un objeto de tipo Set<Article>. El tipo Set no admite duplicados. Por lo tanto, no se puede incluir dos veces el mismo artículo en el objeto Set<Article>. ¿Qué significa «el mismo artículo»? Para indicar que el artículo a es el mismo que el artículo b, Java utiliza la expresión a.equals(b). En la clase Object, clase padre de todas las clases, a.equals(b) es verdadera si a==b, c.a.d. si los objetos a y b tienen la misma ubicación en memoria. Podríamos querer decir que los artículos a y b son los mismos si tienen el mismo nombre. En este caso, el desarrollador debe redefinir dos métodos en la clase [Article]:
      • equals: que debe devolver verdadero si los dos artículos tienen el mismo nombre
      • hashCode: debe devolver un valor entero idéntico para dos objetos [Article] que el método equals considere iguales. En este caso, el valor se generará a partir del nombre del artículo. El valor devuelto por hashCode puede ser cualquier número entero. Se utiliza en diferentes contenedores de objetos, en particular en los diccionarios (Hashtable).

La relación OneToMany puede utilizar otros tipos además de Set para almacenar el Many, como objetos de tipo List, por ejemplo. No abordaremos estos casos en este documento. El lector los encontrará en [ref1].

  • línea 38: el método [addArticle] nos permite agregar un artículo a una categoría. El método se encarga de actualizar ambos extremos de la relación OneToMany que vincula a [Categorie] con [Article].

2.4.3. El proyecto Eclipse / Hibernate 1

La implementación JPA utilizada aquí es la de Hibernate. El proyecto de Eclipse para las pruebas es el siguiente:

El proyecto [3] se encuentra en la carpeta de ejemplos [4]. Lo importaremos.

2.4.4. Generación del archivo DDL de la base de datos

Siguiendo las instrucciones del párrafo 2.1.7, el archivo DDL obtenido para el SGBD MySQL5 es el que se muestra al inicio de este ejemplo, en el párrafo 2.4.1.

2.4.5. InitDB

El código de [InitDB] es el siguiente:


package tests;

...
public class InitDB {

    // constantes
    private final static String TABLE_ARTICLE = "jpa05_hb_article";

    private final static String TABLE_CATEGORIE = "jpa05_hb_categorie";

    public static void main(String[] args) {
        // Contexto de persistencia
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // se recupera un EntityManager a partir del EntityManagerFactory anterior
        em = emf.createEntityManager();
        // Inicio de la transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // solicitud
        Query sql1;
        // eliminar los elementos de la tabla ARTICLE
        sql1 = em.createNativeQuery("delete from " + TABLE_ARTICLE);
        sql1.executeUpdate();
        // eliminar los elementos de la tabla CATEGORIE
        sql1 = em.createNativeQuery("delete from " + TABLE_CATEGORIE);
        sql1.executeUpdate();
        // crear tres categorías
        Categorie categorieA = new Categorie();
        categorieA.setNom("A");
        Categorie categorieB = new Categorie();
        categorieB.setNom("B");
        Categorie categorieC = new Categorie();
        categorieC.setNom("C");
        // crear 3 artículos
        Article articleA1 = new Article();
        articleA1.setNom("A1");
        Article articleA2 = new Article();
        articleA2.setNom("A2");
        Article articleB1 = new Article();
        articleB1.setNom("B1");
        // vincularlos a su categoría
        categorieA.addArticle(articleA1);
        categorieA.addArticle(articleA2);
        categorieB.addArticle(articleB1);
        // guardar las categorías y, de forma cascada (inserción), los artículos
        em.persist(categorieA);
        em.persist(categorieB);
        em.persist(categorieC);
        // mostrar categorías
        System.out.println("[categories]");
        for (Object p : em.createQuery("select c from Categorie c order by c.nom asc").getResultList()) {
            System.out.println(p);
        }
        // mostrar artículos
        System.out.println("[articles]");
        for (Object p : em.createQuery("select a from Article a order by a.nom asc").getResultList()) {
            System.out.println(p);
        }
        // fin de la transacción
        tx.commit();
        // fin de EntityManager
        em.close();
        // fin de EntityMangerFactory
        emf.close();
        // registro
        System.out.println("terminé...");

    }
}
  • líneas 22-27: las tablas [article] y [categorie] se vacían. Cabe señalar que es obligatorio comenzar por la que tiene la clave externa. Si se comenzara por la tabla [categorie], se eliminarían categorías a las que hacen referencia las líneas de la tabla [article], y esto lo rechazaría la tabla SGBD.
  • líneas 29-34: se crean tres categorías A, B, C
  • líneas 36-41: se crean tres artículos: A1, A2 y B1 (la letra indica la categoría)
  • líneas 43-45: los tres artículos se colocan en sus respectivas categorías
  • líneas 47-49: las tres categorías se colocan en el contexto de persistencia. Debido a la cascada Categoría -> Artículo, sus artículos también se colocarán allí. Por lo tanto, todos los objetos creados se encuentran ahora en el contexto de persistencia.
  • líneas 50-59: se solicita el contexto de persistencia para obtener la lista de categorías y artículos. Sabemos que esto provocará una sincronización del contexto con la base de datos. Es en ese momento cuando las categorías y los artículos se guardarán en sus respectivas tablas.

La ejecución de [InitDB] junto con MySQL5 arroja los siguientes resultados:

  • [1]: la salida de la consola
  • [2]: las tablas de [jpa05_hb_*] en la perspectiva de SQL Explorer
  • [3]: la tabla de categorías
  • [4]: la tabla de artículos. Cabe destacar el enlace de [categorie_id] en [4] con [id] en [3] (clave externa).

2.4.6. Inicio

La clase [Main] encadena una serie de pruebas que revisamos, excepto las pruebas 1 y 2, las cuales retoman el código de [InitDB] para inicializar la base de datos.

2.4.6.1. Prueba 3

Esta prueba es la siguiente:


    // buscar un elemento específico
    public static void test3() {
        // nuevo contexto de persistencia
        EntityManager em = getNewEntityManager();
        // transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // carga de categoría
        Categorie categorie = em.find(Categorie.class, categorieA.getId());
        // visualización de la categoría y sus artículos asociados
        System.out.format("Articles de la catégorie %s :%n", categorie);
        for (Article a : categorie.getArticles()) {
            System.out.println(a);
        }
        // fin de transacción
        tx.commit();
}
  • línea 4: tenemos un contexto de persistencia nuevo, por lo que está vacío
  • líneas 6-7: inicio de transacción
  • línea 9: la categoría A se obtiene de la base de datos y se coloca en el contexto de persistencia
  • línea 11: se muestra la categoría A
  • líneas 12-14: se muestran los artículos de la categoría A. Aquí se pone de manifiesto la utilidad de la relación inversa OneToMany, que enlaza con los artículos de la @Entity Categorie. Su presencia nos evita tener que realizar una consulta JPQL para solicitar los artículos de la categoría A. Para obtenerlos, se utiliza el método get del campo articles.

Los resultados son los siguientes:

main : ----------- test1
[categories]
Categorie[1,0,A]
Categorie[2,0,B]
Categorie[3,0,C]
[articles]
Article[1,0,A1,1]
Article[2,0,A2,1]
Article[3,0,B1,2]
main : ----------- test2
3 categorie(s) trouvée(s) :
A
B
C
3 article(s) trouvé(s) :
A1
A2
B1
main : ----------- test3
Articles de la catégorie Categorie[1,0,A] :
Article[2,0,A2,1]
Article[1,0,A1,1]
  • línea 20: la categoría A
  • líneas 21-22: los dos artículos de la categoría A

2.4.6.2. Prueba4

Esta prueba es la siguiente:


    // eliminar un artículo
    @SuppressWarnings("unchecked")
    public static void test4() {
        // nuevo contexto de persistencia
        EntityManager em = getNewEntityManager();
        // transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // carga de artículo A1
        Article newarticle1 = em.find(Article.class, articleA1.getId());
        // eliminación del artículo A1 (actualmente no hay ninguna categoría cargada)
        em.remove(newarticle1);
        // toplink: el artículo debe eliminarse de su categoría; de lo contrario, la prueba 6 falla
        // hibernate: no es necesario
        newarticle1.getCategorie().getArticles().remove(newarticle1);
        // fin de transacción
        tx.commit();
        // volcado de artículos
        dumpArticles();
}
  • La prueba 4 elimina el artículo A1
  • línea 5: se parte de un contexto nuevo y vacío
  • línea 10: el artículo A1 se incorpora al contexto de persistencia. Allí se referenciará como newarticle1.
  • línea 12: se elimina del contexto
  • línea 15: las categorías A, B y C, y los artículos A1, A2 y B1, aunque ya no sean persistentes, siguen estando en memoria. Simplemente se han separado del contexto de persistencia. El artículo A1, que forma parte de los artículos de la categoría A, se retira de esta. Esto permitirá posteriormente volver a vincular la categoría A al contexto de persistencia. Si no se hace esto, la categoría A se vinculará con un conjunto de artículos de los cuales uno ha sido eliminado. Esto no parece afectar a Hibernate, pero hace que Toplink se bloquee.
  • Línea 19: se muestran todos los artículos para verificar que A1 haya desaparecido.

Los resultados son los siguientes:

1
2
3
4
main : ----------- test4
[articles]
Article[2,0,A2,1]
Article[3,0,B1,2]

El artículo A1 ha desaparecido correctamente.

2.4.6.3. Prueba 5

Esta prueba consiste en lo siguiente:


// modificación de 1 artículo
    public static void test5() {
        // nuevo contexto de persistencia
        EntityManager em = getNewEntityManager();
        // transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // modificación de articleA2
        articleA2.setNom(articleA2.getNom() + "-");
        // articleA2 se vuelve a colocar en el contexto de persistencia
        em.merge(articleA2);
        // fin de transacción
        tx.commit();
        // volcado de artículos
        dumpArticles();
    }
  • La prueba 5 cambia el nombre del artículo A2
  • línea 4: se parte de un contexto nuevo y vacío
  • línea 9: se cambia el nombre del artículo separado A2, que pasará a ser «A2-».
  • línea 11: el artículo separado A2 se vuelve a vincular al contexto de persistencia. Cabe señalar que A2 sigue siendo un objeto separado. El objeto em.merge (articleA2) es el que ahora forma parte del contexto de persistencia. Este objeto no se ha almacenado aquí en una variable, como es habitual. Por lo tanto, no es accesible.
  • Línea 13: sincronización del contexto de persistencia con la base de datos. El artículo A2 se modificará en la base de datos y su número de versión pasará de N a N+1. La versión en memoria separada articleA2 ya no es válida. Lo mismo ocurre con el objeto separado que representa la categoría A, ya que este contiene a articleA2 entre sus artículos.
  • línea 15: se muestran todos los artículos para verificar el cambio de nombre del artículo A2

Los resultados son los siguientes:

1
2
3
4
main : ----------- test5
[articles]
Article[2,1,A2-,1]
Article[3,0,B1,2]

El artículo A2 sí ha cambiado de nombre.

2.4.6.4. Prueba 6

Esta prueba consiste en lo siguiente:


// modificación de una categoría y sus artículos
    public static void test6() {
        // nuevo contexto de persistencia
        EntityManager em = getNewEntityManager();
        // transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // carga de categoría
        categorieA = em.find(Categorie.class, categorieA.getId());
        // lista de artículos de la categoría A
        for (Article a : categorieA.getArticles()) {
            a.setNom(a.getNom() + "-");
        }
        // modificación del nombre de la categoría
        categorieA.setNom(categorieA.getNom() + "-");
        // fin de la transacción
        tx.commit();
        // volcado de categorías y artículos
        dumpCategories();
        dumpArticles();
}
  • La prueba 6 cambia el nombre de la categoría A y de todos sus artículos
  • línea 4: se parte de un contexto nuevo y vacío
  • línea 9: buscamos la categoría A en la base de datos. No se realiza un merge del objeto separado categorieA porque sabemos que tiene una referencia al artículo A2, que ya quedó obsoleto. Por lo tanto, volvemos a empezar desde cero.
  • líneas 11-12: se cambia el nombre de todos los artículos de la categoría A. De nuevo se utiliza la relación inversa OneToMany mediante el método getArticles.
  • línea 15: también se modifica el nombre de la categoría
  • línea 17: fin de la transacción. Se realiza una sincronización del contexto con la base de datos. Todos los objetos del contexto que se han modificado se actualizarán en la base de datos.
  • líneas 21-22: se muestran los artículos y las categorías para su verificación

Los resultados son los siguientes:

1
2
3
4
5
6
7
8
main : ----------- test6
[categories]
Categorie[1,2,A-]
Categorie[2,0,B]
Categorie[3,0,C]
[articles]
Article[2,2,A2--,1]
Article[3,0,B1,2]

El artículo A2 ha cambiado de nombre una vez más, al igual que la categoría A.

2.4.6.5. Prueba7

Esta prueba es la siguiente:


// eliminación de una categoría
    public static void test7() {
        // nuevo contexto de persistencia
        EntityManager em = getNewEntityManager();
        // transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // persistencia catégorieB y eliminación en cascada (merge) de los artículos asociados
        Categorie mergedcategorieB = em.merge(categorieB);
        // eliminación de categoría y, en cascada (delete), los artículos asociados
        em.remove(mergedcategorieB);
        // fin de transacción
        tx.commit();
        // volcado de categorías y artículos
        dumpCategories();
        dumpArticles();
    }
  • La prueba 7 elimina la categoría B y, en consecuencia, sus artículos
  • línea 4: se parte de un contexto nuevo y vacío
  • línea 9: la categoría B existe en memoria como un objeto separado del contexto de persistencia. Se reincorpora (merge) al contexto de persistencia. En consecuencia, sus artículos (el artículo B1) se someterán a un merge y, por lo tanto, volverán a integrarse en el contexto de persistencia.
  • línea 11: ahora que la categoría B está en el contexto, se puede eliminar (remove). Por efecto en cadena, sus artículos también se someterán a un remove. Esta operación es posible precisamente porque la operación merge de la línea 9 los reintegró al contexto de persistencia.
  • línea 13: fin de la transacción. Se sincronizará el contexto. Los objetos del contexto que hayan sido sometidos a una operación remove se eliminarán de la base de datos.
  • líneas 15-16: se muestran los artículos y las categorías para su verificación

Los resultados son los siguientes:

1
2
3
4
5
6
main : ----------- test7
[categories]
Categorie[1,2,A-]
Categorie[3,0,C]
[articles]
Article[1,2,A2--,1]

La categoría B y el artículo B1 han desaparecido correctamente.

2.4.6.6. Prueba 8

Esta prueba es la siguiente:


// consultas
    @SuppressWarnings("unchecked")
    public static void test8() {
        // nuevo contexto de persistencia
        EntityManager em = getNewEntityManager();
        // transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // lista de artículos de la categoría A
        List articles = em
                .createQuery(
                        "select a from Categorie c join c.articles a where c.nom like 'A%' order by a.nom asc")
                .getResultList();
        // visualizaciones de artículos
        System.out.println("Articles de la catégorie A");
        for (Object a : articles) {
            System.out.println(a);
        }
        // fin de transacción
        tx.commit();
    }
  • La prueba 7 muestra cómo recuperar los artículos de una categoría sin pasar por la relación inversa. Esto demuestra que dicha relación no es, por lo tanto, indispensable.
  • línea 4: se parte de un contexto nuevo y vacío
  • línea 10: una consulta JPQL que solicita todos los artículos de una categoría cuyo nombre comience con A
  • líneas 15-17: visualización del resultado de la consulta.

Los resultados son los siguientes:

1
2
3
main : ----------- test8
Articles de la catégorie A
Article[2,2,A2--,1]

2.4.7. Proyecto Eclipse / Hibernate 2

Copiamos y pegamos el proyecto Eclipse / Hibernate para aclarar un punto sobre el concepto de relación principal / relación inversa que hemos creado en torno a la anotación @ManyToOne (principal) de la @Entity [Article] y la relación inversa @OneToMany (inversa) de la @Entity [Categorie]. Queremos demostrar que, si esta última relación no se declara como inversa a la otra, el esquema generado para la base de datos es completamente diferente al generado anteriormente.

En [1], el nuevo proyecto de Eclipse. En [2] está el código Java; en [3] está el script ant que generará el esquema SQL de la base de datos. El proyecto se encuentra en [4] dentro de la carpeta de ejemplos [5]. Lo importaremos.

Modificaremos únicamente la @Entity [Categorie] para que su relación @OneToMany con la @entidad [Article] ya no se declare como inversa a la relación @ManyToOne que tiene la @Entity [Article] con la @Entity [Categorie]:


...
@Entity
@Table(name="jpa05_hb_categorie")
public class Categorie implements Serializable {

    // campos
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @SuppressWarnings("unused")
    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    // relación OneToMany no inversa (sin mappedby) Categoría (uno) -> Artículo (muchos)
    // implementada mediante una tabla de unión Categorie_Article para que, a partir de una categoría
    // se pueda acceder a los artículos de esa categoría
    @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY)
    private Set<Article> articles = new HashSet<Article>();

    // fabricantes
...
  • líneas 18-22: queremos conservar la posibilidad de encontrar los artículos de una categoría determinada mediante la relación @OneToMany de la línea 21. Pero queremos conocer la influencia del atributo mappedBy, que convierte una relación en la inversa de una relación principal definida en otro lugar, en otra @Entity. Aquí, se ha eliminado el mappedBy.

Ejecutamos la tarea ant-DLL (véase el párrafo 2.1.7) con el SGBD y el MySQL5. El esquema obtenido es el siguiente:

Cabe destacar los siguientes puntos:

  • se ha creado una nueva tabla [categorie_article] [1]. Anteriormente no existía.
  • Se trata de una tabla de unión entre las tablas [categorie], [2] y [article], [3]. Si los objetos Artículo a1 y a2 forman parte de la categoría c1, en la tabla de unión se encontrarán las filas:
[c1,a1]
[c1,a2]

donde c1, a1 y a2 son las claves primarias de los objetos correspondientes.

  • Hibernate creó la tabla de unión [categorie_article] [1] para que, a partir de un objeto Categoría c, se puedan recuperar los objetos Artículo a que pertenecen a c. Fue la relación @OneToMany la que obligó a crear esta tabla. Como no se declaró como inversa de la relación principal @ManyToOne de la @Entity Artículo, Hibernate no sabía que podía utilizar esta relación principal para recuperar los artículos de una categoría c. Por lo tanto, se las arregló de otra manera.
  • Con este ejemplo, se comprenden mejor los conceptos de las relaciones principale y inverse. Una (la inversa) utiliza las propiedades de la otra (la principal).

El esquema SQL de esta base de datos para MySQL5 es el siguiente:


    alter table jpa05_hb_categorie_jpa06_hb_article 
        drop 
        foreign key FK79D4BA1D26D17756;

    alter table jpa05_hb_categorie_jpa06_hb_article 
        drop 
        foreign key FK79D4BA1D424C61C9;

    alter table jpa06_hb_article 
        drop 
        foreign key FK4547168FECCE8750;

    drop table if exists jpa05_hb_categorie;

    drop table if exists jpa05_hb_categorie_jpa06_hb_article;

    drop table if exists jpa06_hb_article;

    create table jpa05_hb_categorie (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa05_hb_categorie_jpa06_hb_article (
        jpa05_hb_categorie_id bigint not null,
        articles_id bigint not null,
        primary key (jpa05_hb_categorie_id, articles_id),
        unique (articles_id)
    ) ENGINE=InnoDB;

    create table jpa06_hb_article (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        categorie_id bigint not null,
        primary key (id)
    ) ENGINE=InnoDB;

    alter table jpa05_hb_categorie_jpa06_hb_article 
        add index FK79D4BA1D26D17756 (jpa05_hb_categorie_id), 
        add constraint FK79D4BA1D26D17756 
        foreign key (jpa05_hb_categorie_id) 
        references jpa05_hb_categorie (id);

    alter table jpa05_hb_categorie_jpa06_hb_article 
        add index FK79D4BA1D424C61C9 (articles_id), 
        add constraint FK79D4BA1D424C61C9 
        foreign key (articles_id) 
        references jpa06_hb_article (id);

    alter table jpa06_hb_article 
        add index FK4547168FECCE8750 (categorie_id), 
        add constraint FK4547168FECCE8750 
        foreign key (categorie_id) 
references jpa05_hb_categorie (id);
  • líneas 19-24, creación de la tabla [categorie] y líneas 33-39, creación de la tabla [article]. Cabe señalar que son idénticas a las del ejemplo anterior.
  • Líneas 26-31: creación de la tabla de unión [categorie_article] debido a la presencia de la relación no inversa @OneToMany de la @Entity Categorie. Las entradas de esta tabla son del tipo [c,a], donde c es la clave primaria de una categoría c y a es la clave primaria deun artículo a que pertenece a la categoría c. La clave primaria de esta tabla de unión está formada por las dos claves primarias [c,a] concatenadas (fila 29).
  • líneas 41-45: la restricción de clave externa de la tabla [categorie_article] hacia la tabla [categorie]
  • líneas 47-51: la restricción de clave externa de la tabla [categorie_article] hacia la tabla [article]
  • líneas 53-57: la restricción de clave externa de la tabla [article] hacia la tabla [categorie]

Se invita al lector a ejecutar las pruebas [InitDB] y [Main]. Estas arrojan los mismos resultados que antes. Sin embargo, el esquema de la base de datos es redundante y el rendimiento se verá afectado en comparación con la versión anterior. Sin duda, habría que profundizar en esta cuestión de las relaciones inversas y principales para ver si la nueva configuración no genera, además, conflictos debido al hecho de que se tienen dos relaciones independientes para representar lo mismo: la relación muchos-a-uno que tiene la tabla [article] con la tabla [categorie].

Ahora utilizamos una implementación JPA / Toplink:

El proyecto de Eclipse con Toplink es una copia del proyecto de Eclipse con Hibernate, versión 1:

Los códigos Java son idénticos a los del proyecto Hibernate —versión 1— anterior. El entorno (bibliotecas – persistence.xml – SGBD – carpetas conf, ddl – script ant) es el que se analizó en el párrafo 2.1.15.2. El proyecto de Eclipse, [3], se encuentra en la carpeta de ejemplos [4]. Lo importaremos.

El archivo <persistence.xml> [2] se modifica en un punto, el de las entidades declaradas:


        ...
        <!-- clases persistentes -->
        <class>entites.Categorie</class>
        <class>entites.Article</class>
...
  • líneas 3 y 4: las dos entidades gestionadas

La ejecución de [InitDB] junto con SGBD y MySQL5 arroja los siguientes resultados:

En [1], la salida de la consola; en [2], las dos tablas [jpa05_tl] generadas; en [3], los scripts SQL generados. Su contenido es el siguiente:

create.sql


CREATE TABLE jpa05_tl_article (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), categorie_id BIGINT NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa05_tl_categorie (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), PRIMARY KEY (ID))
ALTER TABLE jpa05_tl_article ADD CONSTRAINT FK_jpa05_tl_article_categorie_id FOREIGN KEY (categorie_id) REFERENCES jpa05_tl_categorie (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

drop.sql


ALTER TABLE jpa05_tl_article DROP FOREIGN KEY FK_jpa05_tl_article_categorie_id
DROP TABLE jpa05_tl_article
DROP TABLE jpa05_tl_categorie
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'

La ejecución de [Main] se realiza sin errores.

Este proyecto de Eclipse se creó a partir del anterior mediante copia. Como se realizó con Hibernate, se elimina el atributo mappedBy de la relación @OneToMany de la @Entity Categorie.


@Entity
@Table(name = "jpa06_tl_categorie")
public class Categorie implements Serializable {

    // campos
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    // relación OneToMany no inversa (sin mappedby) Categoría (uno) ->
    // Artículo (muchos)
    // implementada mediante una tabla de unión Categorie_Article para que, a partir
    // una categoría
    // se pueda acceder a varios artículos
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<Article> articles = new HashSet<Article>();

El esquema SQL generado para MySQL5 es entonces el siguiente:

create.sql


CREATE TABLE jpa06_tl_categorie (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), PRIMARY KEY (ID))
CREATE TABLE jpa06_tl_categorie_jpa06_tl_article (Categorie_ID BIGINT NOT NULL, articles_ID BIGINT NOT NULL, PRIMARY KEY (Categorie_ID, articles_ID))
CREATE TABLE jpa06_tl_article (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), categorie_id BIGINT NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa06_tl_categorie_jpa06_tl_article ADD CONSTRAINT FK_jpa06_tl_categorie_jpa06_tl_article_articles_ID FOREIGN KEY (articles_ID) REFERENCES jpa06_tl_article (ID)
ALTER TABLE jpa06_tl_categorie_jpa06_tl_article ADD CONSTRAINT jpa06_tl_categorie_jpa06_tl_article_Categorie_ID FOREIGN KEY (Categorie_ID) REFERENCES jpa06_tl_categorie (ID)
ALTER TABLE jpa06_tl_article ADD CONSTRAINT FK_jpa06_tl_article_categorie_id FOREIGN KEY (categorie_id) REFERENCES jpa06_tl_categorie (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
  • línea 2: la tabla de unión que materializa la relación @OneToMany no inversa anterior.

La ejecución de [InitDB] se realiza sin errores, pero la de [Main] falla en la prueba 7 con los registros (FINEST) siguientes:

main : ----------- test7
[TopLink Finer]: 2007.06.01 01:41:48.734--ServerSession(15290002)--Hilo(Hilo[main,5,main])--cliente adquirido
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Hilo(Hilo[main,5,main])--Fusionar clon con referencias Categoría[5,1,B]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Hilo(Hilo[main,5,main])--Registrar el objeto existente Artículo[6,1,B1]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Hilo(Hilo[main,5,main])--Registrar el objeto existente Categorie[5,1,B]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Hilo(Hilo[main,5,main])--Se ha realizado la operación de eliminación en: Categorie[5,1,B]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Hilo(Hilo[main,5,main])--Se ha realizado la operación de eliminación en: Artículo[6,1,B1]
[TopLink Finer]: 2007.06.01 01:41:48.750--UnitOfWork(26285048)--Hilo(Hilo[main,5,main])--inicio de la confirmación de la unidad de trabajo
[TopLink Finer]: 2007.06.01 01:41:48.750--ClientSession(15014700)--Conexión(6330655)--Hilo(Hilo[main,5,main])--inicio de transacción
[TopLink Finest]: 2007.06.01 01:41:48.750--UnitOfWork(26285048)--Hilo(Hilo[main,5,main])--Ejecutar consulta DeleteObjectQuery(Artículo[6,1,B1])
[TopLink Fine]: 2007.06.01 01:41:48.750--ClientSession(15014700)--Conexión(6330655)--Hilo(Hilo[main,5,main])--DELETE FROM jpa06_tl_article WHERE ((ID = ?) AND (VERSION = ?))
    bind => [6, 1]
[TopLink Warning]: 2007.06.01 01:41:48.750--UnitOfWork(26285048)--Hilo (Thread[main,5,main])--Pila de excepciones locales: 
Exception [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (03/30/2007))): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (`jpa/jpa06_tl_categorie_jpa06_tl_article`, CONSTRAINT `FK_jpa06_tl_categorie_jpa06_tl_article_articles_ID` FOREIGN KEY (`articles_ID`) REFERENCES `jpa06_tl_article` (`ID`))
Error Code: 1451
Call: DELETE FROM jpa06_tl_article WHERE ((ID = ?) AND (VERSION = ?))
    bind => [6, 1]
  • línea 3: el merge en la categoría B
  • línea 4: el artículo dependiente B1 se coloca en el contexto
  • línea 5: lo mismo ocurre con la categoría B misma
  • línea 6: el remove en la categoría B
  • línea 7: el remove en el artículo B1 (por cascada)
  • línea 8: el código Java solicita el commit de la transacción
  • línea 9: se inicia una transacción; por lo tanto, aparentemente aún no había comenzado.
  • línea 10: el artículo B1 será eliminado por una operación DELETE en la tabla [article]. Ahí es donde está el problema. La tabla de unión [categorie_article] tiene una referencia a la fila B1 de la tabla [article]. La eliminación de B1 en [article] infringirá una restricción de clave externa.
  • líneas 13 y siguientes: se produce la excepción

¿Qué podemos concluir?

  • Una vez más, tenemos un problema de portabilidad entre Hibernate y Toplink: Hibernate había superado esta prueba
  • Toplink no maneja bien el caso en que, cuando dos relaciones son en realidad inversas entre sí, una de ellas no se declare como principal y la otra como inversa. Esto se puede aceptar, ya que este caso representa, de hecho, un error de configuración. En nuestro ejemplo, la tabla [article] no tiene relación con la tabla de unión [categorie_article]. Por lo tanto, resulta natural que, al realizar una operación en la tabla [article], Toplink no intente trabajar con la tabla [categorie_article].

2.5. Ejemplo 5: relación muchos-a-muchos con una tabla de unión explícita

2.5.1. El esquema de la base de datos

  • en [1], la base de datos MySQL5

Ya conocemos las tablas [personne], [2] y [adresse], [3]. Estas se analizaron en el apartado 2.3.1. Tomamos la versión en la que la dirección de la persona se encuentra en una tabla propia: [adresse] y [3]. En la tabla [personne], la relación que vincula a una persona con su dirección se materializa mediante una restricción de clave externa.

Una persona realiza actividades. Estas se encuentran en las tablas [activite] y [4]. Una persona puede realizar varias actividades y una actividad puede ser realizada por varias personas. Por lo tanto, una relación de varios a varios vincula las tablas [personne] y [activite]. Esta relación se materializa mediante la tabla de unión [personne_activite] [5].

2.5.2. Los objetos @Entity que representan la base de datos

Las tablas anteriores serán representadas por las siguientes @Entity:

  • la @Entity Personne representará la tabla [personne]
  • el @Entity Adresse representará la tabla [adresse]
  • la @Entity Activite representará la tabla [activite]
  • la @Entity PersonneActivite representará la tabla [personne_activite]

Las relaciones entre estas entidades son las siguientes:

  • una relación uno a uno vincula la entidad Personne con la entidad Adresse: una persona p tiene una dirección a. La entidad Personne, que posee la clave externa, tendrá la relación principal, mientras que la entidad Adresse tendrá la relación inversa.
  • Una relación de varios a varios conecta las entidades Personne y Activite: una persona tiene varias actividades y una actividad es realizada por varias personas. Esta relación podría implementarse directamente mediante una anotación @ManyToMany en cada una de las dos entidades, declarando una como inversa de la otra. Esta solución se analizará más adelante. Aquí, implementamos la relación muchos-a-muchos mediante dos relaciones uno-a-muchos:
    • una relación uno-a-varios que vincula la entidad Personne con la entidad PersonneActivite: una fila (One) de la tabla [personne] es referenciada por varias (Many) filas de la tabla [personne_activite]. La tabla [personne_activite], que contiene la clave externa, tendrá la relación principal @ManyToOne, y la entidad Personne tendrá la relación inversa @OneToMany.
    • una relación uno-a-varios que vincula la entidad Activite con la entidad PersonneActivite: una fila (One) de la tabla [activite] es referenciada por varias (Many) filas de la tabla [personne_activite]. La tabla [personne_activite], que contiene la clave externa, tendrá la relación principal @ManyToOne, y la entidad Activite tendrá la relación inversa @OneToMany.

La @Entity Personne es la siguiente:


@Entity
@Table(name = "jpa07_hb_personne")
public class Personne implements Serializable {

    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false, unique = true)
    private String nom;

    @Column(length = 30, nullable = false)
    private String prenom;

    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(nullable = false)
    private boolean marie;

    @Column(nullable = false)
    private int nbenfants;

    // relación principal Persona (una) -> Dirección (una)
    // implementada por la clave externa Persona (adresse_id) -> Dirección
    // inserción en cascada de Persona -> inserción de Dirección
    // cascada de actualización de Persona -> actualización de Dirección
    // eliminación en cascada de Persona -> eliminación de Dirección
    // una persona debe tener una dirección (nullable=false)
    // 1 dirección pertenece solo a 1 persona (única=verdadero)
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
    private Adresse adresse;

    // relación Persona (uno) -> PersonneActivite (muchos)
    // inversión de la relación existente PersonneActivite (muchos) -> Persona (uno)
    // eliminación en cascada de Persona -> eliminación de PersonneActivite
    @OneToMany(mappedBy = "personne", cascade = { CascadeType.REMOVE })
    private Set<PersonneActivite> activites = new HashSet<PersonneActivite>();

    // constructores

Esta @Entity ya es conocida. Solo comentamos las relaciones que tiene con las demás entidades:

  • líneas 30-39: una relación uno a uno @OneToOne con la @Entity Adresse, materializada mediante una clave externa [adresse_id] (línea 38) que tendrá la tabla [personne] sobre la tabla [adresse].
  • líneas 41-45: una relación uno-a-varios @OneToMany con la @Entity PersonneActivite. Una persona (One) es referenciada por varias (Many) filas de la tabla de unión [personne_activite] representada por la @Entity PersonneActivite. Estos objetos PersonneActivite se colocarán en un tipo Set<PersonneActivite>, donde PersonneActivite es un tipo que definiremos más adelante.
  • Línea 44: la relación uno-a-varios definida aquí es la relación inversa de una relación principal definida en el campo personne de la @Entity PersonneActivite (palabra clave mappedBy). Tenemos una cascada Persona -> Actividad en las eliminaciones: la eliminación de una persona p provocará la eliminación de los elementos persistentes de tipo PersonneActivite que se encuentren en el conjunto p.activites.

La @Entity Adresse es la siguiente:


@Entity
@Table(name = "jpa07_hb_adresse")
public class Adresse implements Serializable {

    // campos
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false)
    private String adr1;
    @Column(length = 30)
    private String adr2;
    @Column(length = 30)
    private String adr3;
    @Column(length = 5, nullable = false)
    private String codePostal;
    @Column(length = 20, nullable = false)
    private String ville;
    @Column(length = 3)
    private String cedex;
    @Column(length = 20, nullable = false)
    private String pays;
    @OneToOne(mappedBy = "adresse")
    private Personne personne;

  • líneas 28-29: la relación @OneToOne es la inversa de la relación @OneToOne que apunta a la @Entity Personne (líneas 37-38 de Personne).

La @Entity Activite es la siguiente


@Entity
@Table(name = "jpa07_hb_activite")
public class Activite implements Serializable {

    // campos
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false, unique = true)
    private String nom;

    // relación Actividad (uno) -> PersonneActivite (muchos)
    // inversión de la relación existente PersonneActivite (muchos) -> Actividad (uno)
    // eliminación en cascada de Actividad -> eliminación de PersonneActivite
    @OneToMany(mappedBy = "activite", cascade = { CascadeType.REMOVE })
    private Set<PersonneActivite> personnes = new HashSet<PersonneActivite>();

  • líneas 6-9: la clave primaria de la actividad
  • líneas 11-13: el número de versión de la actividad
  • líneas 15-16: el nombre de la actividad
  • líneas 18-22: la relación uno-a-varios que vincula la @Entity Activite con la @Entity PersonneActivite: una actividad (One) es referenciada por varias (Many) filas de la tabla de unión [personne_activite] representada por la @Entity PersonneActivite. Estos objetos PersonneActivite se colocarán en un tipo Set<PersonneActivite>.
  • línea 22: la relación uno-a-varios definida aquí es la relación inversa de una relación principal definida en el campo activite de la @Entity PersonneActivite (palabra clave mappedBy). Existe una cascada de «Actividad» → PersonneActivite para las eliminaciones: la eliminación de la tabla [activite] deuna actividad a provocará la eliminación de la tabla de unión [personne_activite] de los elementos persistentes de tipo PersonneActivite que se encuentren en el conjunto a.personnes.

La @Entity PersonneActivite es la siguiente:


@Entity
// tabla de unión
@Table(name = "jpa07_hb_personne_activite")
public class PersonneActivite {

    @Embeddable
    public static class Id implements Serializable {
        // componentes de la clave compuesta
        // apunta a una persona
        @Column(name = "PERSONNE_ID")
        private Long personneId;

        // apunta a una Actividad
        @Column(name = "ACTIVITE_ID")
        private Long activiteId;

        // constructores
...

        // getters y setters
...
        // toString
        public String toString() {
            return String.format("[%d,%d]", getPersonneId(), getActiviteId());
        }
    }

    // campos de la clase Personne_Activite
    // clave compuesta
    @EmbeddedId
    private Id id = new Id();

    // relación principal PersonneActivite (muchos) -> Persona (uno)
    // implementada por la clave externa: personneId (PersonneActivite (muchos) -> Persona (uno)
    // personneId es al mismo tiempo parte de la clave primaria compuesta
    // JPA no debe gestionar esta clave externa (insertable = false, actualizable = false) ya que esto lo hace la propia aplicación en su constructor
    @ManyToOne
    @JoinColumn(name = "PERSONNE_ID", insertable = false, updatable = false)
    private Personne personne;

    // relación principal PersonneActivite -> Actividad
    // implementada por la clave externa: activiteId (PersonneActivite (muchos) -> Actividad (uno)
    // activiteId es al mismo tiempo parte de la clave primaria compuesta
    // JPA no debe gestionar esta clave externa (insertable = false, actualizable = false) ya que esto lo hace la propia aplicación en su constructor
    @ManyToOne()
    @JoinColumn(name = "ACTIVITE_ID", insertable = false, updatable = false)
    private Activite activite;

    // constructores
    public PersonneActivite() {

    }

    public PersonneActivite(Personne p, Activite a) {
        // las claves externas las establece la aplicación
        getId().setPersonneId(p.getId());
        getId().setActiviteId(a.getId());
        // asociaciones bidireccionales
        this.setPersonne(p);
        this.setActivite(a);
        p.getActivites().add(this);
        a.getPersonnes().add(this);
    }

    // getters y setters
...
    // toString
    public String toString() {
        return String.format("[%s,%s,%s]", getId(), getPersonne().getNom(), getActivite().getNom());
    }
}

Esta clase es más compleja que las anteriores.

  • La tabla [personne_activite] contiene filas con el formato [p,a], donde p es la clave primaria de una persona y a es la clave primaria de una actividad. Toda tabla debe tener una clave primaria y [personne_activite] no es la excepción. Hasta ahora, habíamos definido claves primarias generadas dinámicamente por el SGBD. También podríamos hacerlo aquí. Vamos a utilizar otra técnica, aquella en la que la propia aplicación define los valores de la clave primaria de una tabla. Aquí, una fila [p1,a1] indica que una persona p1 practica la actividad a1. No es posible encontrar esta misma fila por segunda vez en la tabla. Por lo tanto, el par (p, a) es un buen candidato para ser clave primaria. A esto se le llama clave primaria compuesta.
  • líneas 30-31: la clave primaria compuesta. La anotación @EmbeddedId (por lo general era @Id) es análoga a la notación @Embedded aplicada al campo Adresse de una persona. En este último caso, significaba que el campo Adresse formaba parte de una clase externa, pero debía insertarse en la misma tabla que la persona. Aquí el significado es el mismo, salvo que, para indicar que se trata de la clave primaria, la notación pasa a ser @EmbeddedId.
  • línea 31: se crea un objeto vacío que representa la clave primaria id al crear el objeto [PersonneActivite]. La clase que representa la clave primaria se define en las líneas 7-26 como una clase pública, estática e interna de la clase [PersonneActivite]. El hecho de que sea pública y estática lo impone Hibernate. Si se reemplaza «public static» por «private,», se produce una excepción y, en el mensaje de error asociado, se observa que Hibernate intentó ejecutar la instrucción «new PersonneActivite$Id». Por lo tanto, la clase «Id» debe ser tanto estática como pública.
  • Línea 6: la clase Id de la clave primaria se declara como @Embeddable. Recordemos que la clave primaria id de la línea 31 se declaró como @EmbeddedId. Por lo tanto, la clase correspondiente debe tener la anotación @Embeddable.
  • Hemos dicho que la clave primaria de la tabla [personne_activite] está compuesta por el par (p, a), donde p es la clave primaria de una persona y a la clave primaria de una actividad. Los dos elementos (p,a) de la clave compuesta se encuentran en la línea 11 (personneId) y en la línea 15 (activiteId). Las columnas asociadas a estos dos campos se denominan: PERSONNE_ID para la persona y ACTIVITE_ID para la actividad.
  • línea 31: se ha definido la clave primaria con sus dos columnas (PERSONNE_ID, ACTIVITE_ID). No hay otras columnas en la tabla [personne_activite]. Ahora solo queda definir las relaciones que existen entre la @Entity PersonneActivite que estamos describiendo y las demás @Entity del esquema relacional. Estas relaciones reflejan las restricciones de claves externas que tiene la tabla [personne_activite] con las demás tablas.
  • líneas 33-39: definen la clave externa que tiene la tabla [personne_activite] sobre la tabla [personne]
  • línea 37: la relación es de tipo @ManyToOne: una fila (One) de la tabla [personne] es referenciada por varias (Many) filas de la tabla [personne_activite].
  • línea 38: se nombra la columna de clave externa. Se utiliza el mismo nombre que el asignado al componente «persona» de la clave externa (línea 10). Los atributos insertable=false, updatable=false sirven para impedir que Hibernate administre la clave externa. De hecho, esta es el componente de una clave primaria calculada por la aplicación y Hibernate no debe intervenir.
  • Líneas 41-47: definen la clave externa que tiene la tabla [personne_activite] sobre la tabla [activite]. Las explicaciones son las mismas que las dadas anteriormente.
  • líneas 54-63: constructor de un objeto PersonneActivite a partir de una persona p y una actividad a. Recordemos que, al crear un objeto PersonneActivite, la clave primaria id de la línea 31 apuntaba a un objeto Id vacío. Las líneas 56 y 57 asignan un valor a cada uno de los campos (personneId, activiteId) del objeto Id. Estos valores son, respectivamente, las claves primarias de la persona p y de la actividad a, que se pasan como parámetros al constructor. Por lo tanto, la clave primaria id (línea 31) ahora tiene un valor.
  • línea 59: el campo personne de la línea 39 recibe el valor p
  • línea 60: el campo activite de la línea 47 recibe el valor a
  • Se ha creado e inicializado un objeto [PersonneActivite]. Se actualizan las relaciones inversas que tienen las @Entity Personne (línea 61) y Activite (línea 62) con la @Entity PersonneActivite que acaba de crearse.

Hemos terminado la descripción de las entidades de la base de datos. Nos encontramos en una situación compleja, pero lamentablemente frecuente. Veremos que existe otra configuración posible de la capa JPA que oculta parte de esta complejidad: la tabla de unión se vuelve implícita, construida y administrada por la capa JPA. Aquí hemos elegido la solución más compleja, pero que permite que el esquema relacional evolucione. De este modo, permite agregar columnas a la tabla de unión, algo que no permite la configuración en la que la tabla de unión no es una @Entity explícita. [ref1] recomienda la solución que estamos analizando. En [ref1] se encontró la información que permitió desarrollar esta solución.

2.5.3. El proyecto Eclipse / Hibernate

La implementación JPA utilizada aquí es la de Hibernate. El proyecto de Eclipse para las pruebas es el siguiente:

 

Image

En [1] se encuentra el proyecto de Eclipse, y en [2], los códigos Java. El proyecto se encuentra en [3], dentro de la carpeta de ejemplos [4]. Lo importaremos.

2.5.4. Generación del archivo DDL de la base de datos

Siguiendo las instrucciones del párrafo 2.1.7, el archivo DDL obtenido para el SGBD MySQL5 es el siguiente:


alter table jpa07_hb_personne 
        drop 
        foreign key FKB5C817D45FE379D0;

    alter table jpa07_hb_personne_activite 
        drop 
        foreign key FKD3E49B06CD852024;

    alter table jpa07_hb_personne_activite 
        drop 
        foreign key FKD3E49B0668C7A284;

    drop table if exists jpa07_hb_activite;

    drop table if exists jpa07_hb_adresse;

    drop table if exists jpa07_hb_personne;

    drop table if exists jpa07_hb_personne_activite;

    create table jpa07_hb_activite (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa07_hb_adresse (
        id bigint not null auto_increment,
        version integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa07_hb_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adresse_id bigint not null unique,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa07_hb_personne_activite (
        PERSONNE_ID bigint not null,
        ACTIVITE_ID bigint not null,
        primary key (PERSONNE_ID, ACTIVITE_ID)
    ) ENGINE=InnoDB;

    alter table jpa07_hb_personne 
        add index FKB5C817D45FE379D0 (adresse_id), 
        add constraint FKB5C817D45FE379D0 
        foreign key (adresse_id) 
        references jpa07_hb_adresse (id);

    alter table jpa07_hb_personne_activite 
        add index FKD3E49B06CD852024 (ACTIVITE_ID), 
        add constraint FKD3E49B06CD852024 
        foreign key (ACTIVITE_ID) 
        references jpa07_hb_activite (id);

    alter table jpa07_hb_personne_activite 
        add index FKD3E49B0668C7A284 (PERSONNE_ID), 
        add constraint FKD3E49B0668C7A284 
        foreign key (PERSONNE_ID) 
        references jpa07_hb_personne (id);
  • líneas 21-26: la tabla [activite]
  • líneas 28-39: la tabla [adresse]
  • líneas 41-51: la tabla [personne]
  • líneas 53-57: la tabla de unión [personne_activite]. Cabe destacar la clave compuesta (línea 56)
  • líneas 59-63: la clave externa de la tabla [personne] hacia la tabla [adresse]
  • líneas 65-69: la clave externa de la tabla [personne_activite] hacia la tabla [activite]
  • líneas 71-75: la clave externa de la tabla [personne_activite] hacia la tabla [personne]

2.5.5. InitDB

El código de [InitDB] es el siguiente:


package tests;

...
public class InitDB {

    // constantes
    private final static String TABLE_PERSONNE_ACTIVITE = "jpa07_hb_personne_activite";

    private final static String TABLE_PERSONNE = "jpa07_hb_personne";

    private final static String TABLE_ACTIVITE = "jpa07_hb_activite";

    private final static String TABLE_ADRESSE = "jpa07_hb_adresse";

    public static void main(String[] args) throws ParseException {
        // Contexto de persistencia
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // se recupera un EntityManager a partir del EntityManagerFactory
        // anterior
        em = emf.createEntityManager();
        // inicio de la transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // solicitud
        Query sql1;
        // eliminar los elementos de la tabla PERSONNE_ACTIVITE
        sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE_ACTIVITE);
        sql1.executeUpdate();
        // eliminar los elementos de la tabla PERSONNE
        sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE);
        sql1.executeUpdate();
        // eliminar los elementos de la tabla ACTIVITE
        sql1 = em.createNativeQuery("delete from " + TABLE_ACTIVITE);
        sql1.executeUpdate();
        // eliminar los elementos de la tabla ADRESSE
        sql1 = em.createNativeQuery("delete from " + TABLE_ADRESSE);
        sql1.executeUpdate();
        // creación de actividades
        Activite act1 = new Activite();
        act1.setNom("act1");
        Activite act2 = new Activite();
        act2.setNom("act2");
        Activite act3 = new Activite();
        act3.setNom("act3");
        // persistencia de actividades
        em.persist(act1);
        em.persist(act2);
        em.persist(act3);
        // creación de personas
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        Personne p3 = new Personne("p3", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // creación de direcciones
        Adresse adr1 = new Adresse("adr1", null, null, "49000", "Angers", null, "France");
        Adresse adr2 = new Adresse("adr2", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        Adresse adr3 = new Adresse("adr3", "x", "x", "x", "x", "x", "x");
        Adresse adr4 = new Adresse("adr4", "y", "y", "y", "y", "y", "y");
        // asociaciones entre personas y direcciones
        p1.setAdresse(adr1);
        adr1.setPersonne(p1);
        p2.setAdresse(adr2);
        adr2.setPersonne(p2);
        p3.setAdresse(adr3);
        adr3.setPersonne(p3);
        // persistencia de las personas y, por lo tanto, de las direcciones asociadas
        em.persist(p1);
        em.persist(p2);
        em.persist(p3);
        // persistencia de la dirección a4 no vinculada a una persona
        em.persist(adr4);
        // visualización de personas
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // visualización de direcciones
        System.out.println("[adresses]");
        for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
            System.out.println(a);
        }
        System.out.println("[activites]");
        for (Object a : em.createQuery("select a from Activite a").getResultList()) {
            System.out.println(a);
        }
        // asociaciones persona <--> actividad
        PersonneActivite p1act1 = new PersonneActivite(p1, act1);
        PersonneActivite p1act2 = new PersonneActivite(p1, act2);
        PersonneActivite p2act1 = new PersonneActivite(p2, act1);
        PersonneActivite p2act3 = new PersonneActivite(p2, act3);
        // Persistencia de las asociaciones persona <--> actividad
        em.persist(p1act1);
        em.persist(p1act2);
        em.persist(p2act1);
        em.persist(p2act3);
        // visualización de personas
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // visualización de direcciones
        System.out.println("[adresses]");
        for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
            System.out.println(a);
        }
        System.out.println("[activites]");
        for (Object a : em.createQuery("select a from Activite a").getResultList()) {
            System.out.println(a);
        }
        System.out.println("[personnes/activites]");
        for (Object pa : em.createQuery("select pa from PersonneActivite pa").getResultList()) {
            System.out.println(pa);
        }
        // fin de la transacción
        tx.commit();
        // fin de EntityManager
        em.close();
        // fin de EntityManagerFactory
        emf.close();
        // registro
        System.out.println("terminé...");

    }
}
  • líneas 27-38: se vacían las tablas [personne_activite], [personne], [adresse] y [activite]. Cabe señalar que es obligatorio comenzar por las tablas que contienen claves externas.
  • líneas 40-45: se crean tres actividades: act1, act2 y act3
  • líneas 47-49: se colocan en el contexto de persistencia.
  • líneas 51-53: se crean tres personas: p1, p2 y p3.
  • líneas 55-58: se crean cuatro direcciones, de adr1 a adr4.
  • líneas 60-65: las direcciones adri se asocian a las personas pi. En cada caso hay que realizar dos operaciones, ya que la relación Persona <-> Dirección es bidireccional.
  • líneas 67-69: las personas p1 a p3 se colocan en el contexto de persistencia. Debido a la cascada Persona -> Dirección, esto también se aplicará a las direcciones adr1 a adr3.
  • línea 71: la cuarta dirección adr4, que no está asociada a una persona, se coloca explícitamente en el contexto de persistencia.
  • Líneas 73-85: se consulta el contexto de persistencia para obtener la lista de entidades de tipo [Personne], [Adresse] y [Activite]. Sabemos que estas consultas provocarán la sincronización del contexto con la base de datos: las entidades creadas se insertarán en la base de datos y obtendrán su clave primaria. Es importante comprender esto para lo que sigue.
  • Líneas 87-90: se crean 4 asociaciones Persona <-> Actividad. Su nombre indica qué persona está vinculada a qué actividad. Quizás recordemos que la clave primaria de una entidad PersonneActivite es una clave compuesta formada por la clave primaria de una persona y la de una actividad. Por lo tanto, esta operación es posible precisamente porque las entidades Personne y Activite obtuvieron sus claves primarias durante una sincronización anterior.
  • líneas 92-95: estas 4 asociaciones se colocan en el contexto de persistencia.
  • líneas 87-86: se solicita al contexto de persistencia la lista de entidades de tipo [Personne], [Adresse], [Activite] y [PersonneActivite]. Sabemos que estas consultas provocarán la sincronización del contexto con la base de datos: las entidades PersonneActivite creadas se insertarán en la base de datos.

Al ejecutar [InitDB] junto con MySQL5, se muestra lo siguiente en la consola:

[personnes]
P[1,0,p1,Paul,31/01/2000,true,2,1]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[activites]
Ac[1,0,act1]
Ac[2,0,act2]
Ac[3,0,act3]
[personnes]
P[1,1,p1,Paul,31/01/2000,true,2,1]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[personnes/activites]
[[1,1],p1,act1]
[[2,1],p2,act1]
[[1,2],p1,act2]
[[2,3],p2,act3]
terminé...

Puede resultar sorprendente ver que, en las líneas 15 y 16, las personas p1 y p2 tienen su número de versión en 1, y que lo mismo ocurre, en las líneas 24 a 26, con las tres actividades. Intentemos entenderlo.

En las líneas 2 a 4, los números de versión de las personas están en 0, y en las líneas 11 a 13, los números de versión de las actividades están en 0. Las visualizaciones anteriores ocurren antes de la creación de las relaciones Persona <-> Actividad. En las líneas 87-90 del código Java, se crean relaciones entre las personas p1 y p2 y las actividades act1, act2, act3. Estas se realizan mediante el constructor de la @Entity PersonneActivite (véase el párrafo 2.5.2). Al leer el código de este constructor, se observa que cuando una persona p está vinculada a una actividad a:

  • la actividad a se agrega al conjunto p.activites
  • la persona p se agrega al conjunto a.personnes

Así, cuando se escribe new PersonneActivite(p,a), la persona p y la actividad a sufren una modificación en memoria. Al llegar a las líneas 97-113 de [InitDB], el contexto de persistencia se sincroniza con la base de datos, JPA / Hibernate detecta que los elementos persistentes p1, p2, act1, act2 y act3 han sido modificados. Estos cambios deben realizarse en la base de datos. De hecho, se registran en la tabla de unión [personne_activite], pero JPA / Hibernate incrementa de todos modos el número de versión de cada uno de los elementos persistentes modificados.

En la vista de SQL Explorer, los resultados son los siguientes:

  • [2]: las tablas [jpa07_hb_*]
  • [3]: la tabla de personas
  • [4]: la tabla de direcciones.
  • [5]: la tabla de actividades
  • [6]: la tabla de unión persona <-> actividad

2.5.6. Inicio

La clase [Main] encadena una serie de pruebas que revisamos, excepto la prueba 1, que retoma el código de [InitDB] para inicializar la base de datos.

2.5.6.1. Prueba 2

Esta prueba es la siguiente:


// eliminación de persona p1
    public static void test2() {
        // contexto de persistencia
        EntityManager em = getEntityManager();
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // eliminación de dependencias en p1: no es necesario para Hibernate, pero
        // es indispensable para TopLink
        act1.getPersonnes().remove(p1act1);
        act2.getPersonnes().remove(p1act2);
        // eliminación de persona p1
        em.remove(p1);
        // fin de la transacción
        tx.commit();
        // se muestran las nuevas tablas
        dumpPersonne();
        dumpActivite();
        dumpAdresse();
        dumpPersonne_Activite();
    }
  • línea 4: se utiliza el contexto de persistencia de test1, donde la persona p1 es un objeto del contexto.
  • línea 13: eliminación de la persona p1. Debido al atributo:
    • cascadeType.ALL sobre Adresse, se eliminará la dirección de la persona p1
    • cascadeType.REMOVE sobre PersonneActivite, se eliminarán las actividades de la persona p1.
  • líneas 10-11: se eliminan las dependencias que tienen las otras entidades sobre la persona p1, la cual será eliminada en la línea 13. Las actividades act1 y act2 son realizadas por la persona p1. Los vínculos fueron creados por el generador de la entidad PersonneActivite, cuyo código es el siguiente:

    public PersonneActivite(Personne p, Activite a) {
        // las claves externas las establece la aplicación
        getId().setPersonneId(p.getId());
        getId().setActiviteId(a.getId());
        // asociaciones bidireccionales
        setPersonne(p);
        setActivite(a);
        p.getActivites().add(this);
        a.getPersonnes().add(this);
}

En la línea 9, la actividad a recibe un elemento adicional de tipo PersonneActivite en su conjunto personnes. Este elemento es de tipo (p,a) para indicar que la persona p realiza la actividad a. En test1 de [Main], se crearon así dos enlaces: (p1,act1) y (p1,act2). Las líneas 10 y 11 de test2 eliminan estas dependencias. Cabe señalar que Hibernate funciona sin la eliminación de estas dependencias en el registro p1, pero Toplink no.

  • Líneas 17-20: se muestran todas las tablas

Los resultados son los siguientes:

main : ----------- test1
[personnes]
P[1,1,p1,Paul,31/01/2000,true,2,1]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[1,1],p1,act1]
[[2,1],p2,act1]
[[1,2],p1,act2]
[[2,3],p2,act3]
main : ----------- test2
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[2,1],p2,act1]
[[2,3],p2,act3]
  • La persona p1, que aparece en test1 (línea 3), ya no aparece al final de test2 (líneas 22-23)
  • la dirección adr1 de la persona p1, que aparece en test1 (línea 11) ya no lo es al finalizar test2 (líneas 29-31)
  • las actividades (p1,act1) (línea 16) y (p1,act2) (línea 18) de la persona p1, presentes en test1, ya no lo son al finalizar test2 (líneas 33-34)

2.5.6.2. Prueba 3

Esta prueba es la siguiente:


// eliminación de la actividad act1
    public static void test3() {
        // contexto de persistencia
        EntityManager em = getEntityManager();
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // eliminación de dependencias en act1: no es necesario para Hibernate, pero
        // es indispensable para TopLink
        p2.getActivites().remove(p2act1);
        // eliminación de la actividad act1
        em.remove(act1);
        // fin de la transacción
        tx.commit();
        // se muestran las nuevas tablas
        dumpPersonne();
        dumpActivite();
        dumpAdresse();
        dumpPersonne_Activite();
    }
  • línea 4: se utiliza el contexto de persistencia de test2
  • línea 12: se elimina la actividad act1. Debido al atributo:
    • cascadeType.REMOVE sobre PersonneActivite, se eliminarán las filas (p, act1) de la tabla [personne_activite].
  • línea 10: antes de sacar act1 del contexto de persistencia, se eliminan las dependencias que otras entidades puedan tener sobre este objeto persistente. Tras la eliminación de la persona p1 en la prueba anterior, solo la persona p2 realiza la actividad act1.
  • líneas 13-16: se muestran todas las tablas

Los resultados son los siguientes:

main : ----------- test2
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[2,1],p2,act1]
[[2,3],p2,act3]
main : ----------- test3
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[2,3],p2,act3]
  • en test2, la actividad act1 existe (línea 6). En test3, ya no existe (líneas 21-22)
  • En test2, existe el enlace p2,act1) (línea 14). En test3, ya no existe (línea 28)

2.5.6.3. Test4

Esta prueba es la siguiente:


// recuperación de actividades de una persona
    public static void test4() {
        // contexto de persistencia
        EntityManager em = getNewEntityManager();
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // se recupera la persona p2
        p2 = em.find(Personne.class, p2.getId());
        System.out.format("1 - Activités de la personne p2 (JPQL) :%n");
        // se revisan sus actividades
        for (Object pa : em.createQuery("select a.nom from Activite a join a.personnes pa where pa.personne.nom='p2'").getResultList()) {
            System.out.println(pa);
        }
        // se recorre la relación inversa de p2
        p2 = em.find(Personne.class, p2.getId());
        System.out.format("2 - Activités de la personne p2 (relation inverse) :%n");
        // se analizan sus actividades
        for (PersonneActivite pa : p2.getActivites()) {
            System.out.println(pa.getActivite().getNom());
        }
        // fin de la transacción
        tx.commit();
    }
  • La prueba 4 muestra las actividades de la persona p2.
  • línea 4: se parte de un contexto nuevo y vacío
  • Líneas 12-14: se muestran los nombres de las actividades realizadas por la persona p2 mediante una consulta JPQL.
    • Se realiza una unión entre Activite (a) y PersonneActivite (pa) (join a.personnes)
    • en las filas de esta unión (a,pa), se muestra el nombre de la actividad (a.nom) para la persona p2 (pa.personne.nom='p2').
  • líneas 16-21: se hace lo mismo que anteriormente, pero con la ayuda de la relación OneToMany p2.activites de la persona p2. La consulta JPQL será generada por JPA. Aquí se ve la ventaja de la relación inversa OneToMany: evita una consulta JPQL.

Los resultados son los siguientes:

1
2
3
4
5
main : ----------- test4
1 - Activités de la personne p2 (JPQL) :
act3
2 - Activités de la personne p2 (relation inverse) :
act3

2.5.6.4. Prueba5

Esta prueba es la siguiente:


// se recuperan las personas que realizan una actividad determinada
    public static void test5() {
        // contexto de persistencia
        EntityManager em = getNewEntityManager();
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        System.out.format("1 - Personnes pratiquant l'activité act3 (JPQL) :%n");
        // se solicitan las actividades de p2
        for (Object pa : em.createQuery("select p.nom from Personne p join p.activites pa where pa.activite.nom='act3'").getResultList()) {
            System.out.println(pa);
        }
        // se pasa por la relación inversa de act3
        System.out.format("2 - Personnes pratiquant l'activité act3 (relation inverse) :%n");
        act3 = em.find(Activite.class, act3.getId());
        for (PersonneActivite pa : act3.getPersonnes()) {
            System.out.println(pa.getPersonne().getNom());
        }
        // fin de la transacción
        tx.commit();
    }
  • La prueba 6 muestra a las personas que realizan la actividad act3. El procedimiento es similar al de la prueba 6. Dejamos al lector la tarea de establecer la relación entre los dos códigos.

Los resultados son los siguientes:

1
2
3
4
5
main : ----------- test5
1 - Personnes pratiquant l'activité act3 (JPQL) :
p2
2 - Personnes pratiquant l'activité act3 (relation inverse) :
p2

Las pruebas 4 y 5 tenían como objetivo demostrar nuevamente que una relación inversa nunca es indispensable y siempre puede ser reemplazada por una consulta JPQL.

Ahora utilizamos una implementación JPA / Toplink:

El proyecto de Eclipse con Toplink es una copia del proyecto de Eclipse con Hibernate:

Los códigos Java son idénticos a los del proyecto anterior con Hibernate, salvo por algunos detalles que mencionaremos. El entorno (bibliotecas – persistence.xml – SGBD – carpetas conf, ddl – script ant) es el que se analizó en el párrafo 2.1.15.2. El proyecto de Eclipse se encuentra en [3] dentro de la carpeta de ejemplos [4]. Lo importaremos.

El archivo <persistence.xml> [2] se modifica en un punto, el de las entidades declaradas:


        <!-- clases persistentes -->
        <class>entites.Activite</class>
        <class>entites.Adresse</class>
        <class>entites.Personne</class>
<class>entites.PersonneActivite</class>
  • líneas 2-5: las cuatro entidades administradas

La ejecución de [InitDB] junto con SGBD y MySQL5 arroja los siguientes resultados:

En [1], la salida de la consola; en [2], las tablas generadas [jpa07_tl]; en [3], los scripts generados SQL. Su contenido es el siguiente:

create.sql


CREATE TABLE jpa07_tl_activite (ID BIGINT NOT NULL, VERSION INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa07_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR2 VARCHAR(30), CEDEX VARCHAR(3), ADR1 VARCHAR(30) NOT NULL, PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa07_tl_personne_activite (PERSONNE_ID BIGINT NOT NULL, ACTIVITE_ID BIGINT NOT NULL, PRIMARY KEY (PERSONNE_ID, ACTIVITE_ID))
CREATE TABLE jpa07_tl_personne (ID BIGINT NOT NULL, DATENAISSANCE DATE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, NBENFANTS INTEGER NOT NULL, VERSION INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa07_tl_personne_activite ADD CONSTRAINT FK_jpa07_tl_personne_activite_ACTIVITE_ID FOREIGN KEY (ACTIVITE_ID) REFERENCES jpa07_tl_activite (ID)
ALTER TABLE jpa07_tl_personne_activite ADD CONSTRAINT FK_jpa07_tl_personne_activite_PERSONNE_ID FOREIGN KEY (PERSONNE_ID) REFERENCES jpa07_tl_personne (ID)
ALTER TABLE jpa07_tl_personne ADD CONSTRAINT FK_jpa07_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa07_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

La ejecución de [InitDB] y de [Main] se realiza sin errores.

2.6. Ejemplo 6: relación muchos-a-muchos con una tabla de unión implícita

Retomamos el ejemplo 4, pero ahora lo procesamos con una tabla de unión implícita generada por la propia capa JPA.

2.6.1. El esquema de la base de datos

  • en [1], la base de datos MySQL5 —en [2]—: la tabla [personne] – a [3]; la tabla [adresse] asociada – a [4]; la tabla [activite] de actividades – en [5]: la tabla de unión [personne_activite] que vincula a las personas con las actividades.

2.6.2. Los objetos @Entity que representan la base de datos

Las tablas anteriores serán representadas por las siguientes @Entity:

  • la @Entity Personne representará la tabla [personne]
  • el @Entity Adresse representará la tabla [adresse]
  • la @Entity Activite representará la tabla [activite]
  • la tabla [personne_activite] ya no está representada por una @Entity

Las relaciones entre estas entidades son las siguientes:

  • una relación uno a uno vincula la entidad Personne con la entidad Adresse: una persona p tiene una dirección a. La entidad Personne, que posee la clave externa, tendrá la relación principal, mientras que la entidad Adresse tendrá la relación inversa.
  • Una relación de varios a varios conecta las entidades Personne y Activite: una persona tiene varias actividades y una actividad es realizada por varias personas. Esta relación se materializará mediante una anotación @ManyToMany en cada una de las dos entidades, declarándose una como inversa de la otra.

La @Entity Personne es la siguiente:


@Entity
@Table(name = "jpa08_hb_personne")
public class Personne implements Serializable {

    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    // toplink sqlserver: @GeneratedValue (estrategia = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false, unique = true)
    private String nom;

    @Column(length = 30, nullable = false)
    private String prenom;

    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(nullable = false)
    private boolean marie;

    @Column(nullable = false)
    private int nbenfants;

    // relación principal Persona (one) -> Dirección (one)
    // implementada mediante la clave externa Persona (adresse_id) -> Dirección
    // inserción en cascada de Persona -> inserción de Dirección
    // cascada de actualización de Persona -> actualización de Dirección
    // eliminación en cascada de Persona -> eliminación de Dirección
    // una persona debe tener una dirección (nullable=false)
    // 1 dirección pertenece solo a 1 persona (única=verdadero)
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
    private Adresse adresse;

    // relación Persona (muchas) -> Actividad (muchas) a través de una tabla de unión personne_activite
    // personne_activite(PERSONNE_ID) es una clave foránea en Persona(id)
    // personne_activite(ACTIVITE_ID) es una clave foránea en Actividad (id)
    // cascada=CascadeType.PERSIST: la persistencia de una persona implica la de sus actividades
    @ManyToMany(cascade={CascadeType.PERSIST})
    @JoinTable(name="jpa08_hb_personne_activite",joinColumns = @JoinColumn(name = "PERSONNE_ID"), inverseJoinColumns = @JoinColumn(name = "ACTIVITE_ID"))
    private Set<Activite> activites = new HashSet<Activite>();

    // constructores
    public Personne() {
    }

Solo comentamos la relación @ManyToMany de las líneas 46 a 48, que vincula la @Entity Personne con la @Entity Activite:

  • línea 48: una persona tiene actividades. El campo «actividades» representará dichas actividades. En la versión anterior, el tipo de los elementos del conjunto activites era PersonneActivite. Aquí es Activite. Por lo tanto, se accede directamente a las actividades de una persona, mientras que en la versión anterior era necesario pasar por la entidad intermedia PersonneActivite.
  • línea 46: la relación que vincula la @Entity Personne que estamos analizando con la @Entity Activite del conjunto activites de la línea 48 es de tipo muchos-a-muchos (ManyToMany):
    • una persona (One) tiene varias actividades (Many)
    • una actividad (One) es realizada por varias personas (Many)
    • Al final, las @Entity Personne y Activite están vinculadas por una relación ManyToMany. Al igual que en la relación OneToOne, existe simetría entre las entidades en esta relación. Se puede elegir libremente qué @Entity tendrá la relación principal y cuál tendrá la relación inversa. En este caso, decidimos que la @Entity Personne tendrá la relación principal.
    • Como vimos en el ejemplo anterior, la relación @ManyToMany requiere una tabla de unión. Mientras que anteriormente la habíamos definido mediante una @Entity, aquí la tabla de unión se define mediante la anotación @JoinTable en la línea 47.
      • El atributo name le da un nombre a la tabla.
      • La tabla de unión está compuesta por las claves externas de las tablas que une. Aquí hay dos claves externas: una en la tabla [personne] y la otra en la tabla [activite]. Estas columnas de clave externa se definen mediante los atributos joinColumns y inverseJoinColumns.
      • La anotación @JoinColumn del atributo joinColumns define la clave externa en la tabla de la @Entity que contiene la relación principal @ManyToMany, en este caso la tabla [personne]. Esta columna de clave externa se llamará PERSONNE_ID.
      • La anotación @JoinColumn del atributo inverseJoinColumns define la clave externa en la tabla de la @Entity que contiene la relación inversa @ManyToMany, en este caso la tabla [activite]. Esta columna de clave externa se llamará ACTIVITE_ID.

La @Entity Adresse es la siguiente:


@Entity
@Table(name = "jpa07_hb_adresse")
public class Adresse implements Serializable {

    // campos
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false)
    private String adr1;
    @Column(length = 30)
    private String adr2;
    @Column(length = 30)
    private String adr3;
    @Column(length = 5, nullable = false)
    private String codePostal;
    @Column(length = 20, nullable = false)
    private String ville;
    @Column(length = 3)
    private String cedex;
    @Column(length = 20, nullable = false)
    private String pays;
    @OneToOne(mappedBy = "adresse")
    private Personne personne;

  • líneas 28-29: la relación @OneToOne es la inversa de la relación @OneToOne, que apunta a la @Entity Personne (líneas 37-38 de Personne).

La @Entity Activite es la siguiente


@Entity
@Table(name = "jpa08_hb_activite")
public class Activite implements Serializable {

    // campos
    @Id()
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    // toplink sqlserver: @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false, unique = true)
    private String nom;

    // relación inversa Actividad -> Persona
    @ManyToMany(mappedBy = "activites")
    private Set<Personne> personnes = new HashSet<Personne>();
...
  • líneas 20-21: la relación muchos-a-muchos que vincula la @Entity Activite con la @Entity Personne. Esta relación ya se definió en la @Entity Personne. Por lo tanto, aquí solo se indica que la relación es inversa (mappedBy) a la relación @ManyToMany existente en el campo «actividades» (mappedBy = «actividades») de la @Entidad Personne.
  • Recordemos que una relación inversa siempre es opcional. En este caso, la utilizamos para obtener las personas que realizan la actividad actual. Es el conjunto Set<Persona> personas el que permitirá obtenerlas. No se especifica el modo de carga de las dependencias Personne de la @Entity Activite. Tampoco lo habíamos especificado en el ejemplo anterior. Por defecto, este modo es fetch=FetchType.LAZY.

Hemos terminado la descripción de las entidades de la base de datos. Ha sido más sencilla que en el caso en que la tabla de unión [personne_activite] es una tabla explícita. Esta solución más sencilla puede presentar inconvenientes con el paso del tiempo: no permite agregar columnas a la tabla de unión. Sin embargo, esto puede resultar necesario para satisfacer nuevas necesidades, por ejemplo, agregar a la tabla [personne_activite] una columna que indique la fecha de inscripción de la persona en la actividad.

2.6.3. El proyecto Eclipse / Hibernate

La implementación JPA utilizada aquí es la de Hibernate. El proyecto de Eclipse para las pruebas es el siguiente:

En [1], el proyecto de Eclipse; en [2], los códigos Java. El proyecto se encuentra en [3], dentro de la carpeta de ejemplos [4]. Lo importaremos.

2.6.4. Generación del archivo DDL de la base de datos

Siguiendo las instrucciones del párrafo 2.1.7, el archivo DDL obtenido para el SGBD MySQL5 es el siguiente:


alter table jpa08_hb_personne 
        drop 
        foreign key FKA44B1E555FE379D0;

    alter table jpa08_hb_personne_activite 
        drop 
        foreign key FK5A6A55A5CD852024;

    alter table jpa08_hb_personne_activite 
        drop 
        foreign key FK5A6A55A568C7A284;

    drop table if exists jpa08_hb_activite;

    drop table if exists jpa08_hb_adresse;

    drop table if exists jpa08_hb_personne;

    drop table if exists jpa08_hb_personne_activite;

    create table jpa08_hb_activite (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa08_hb_adresse (
        id bigint not null auto_increment,
        version integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa08_hb_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adresse_id bigint not null unique,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa08_hb_personne_activite (
        PERSONNE_ID bigint not null,
        ACTIVITE_ID bigint not null,
        primary key (PERSONNE_ID, ACTIVITE_ID)
    ) ENGINE=InnoDB;

    alter table jpa08_hb_personne 
        add index FKA44B1E555FE379D0 (adresse_id), 
        add constraint FKA44B1E555FE379D0 
        foreign key (adresse_id) 
        references jpa08_hb_adresse (id);

    alter table jpa08_hb_personne_activite 
        add index FK5A6A55A5CD852024 (ACTIVITE_ID), 
        add constraint FK5A6A55A5CD852024 
        foreign key (ACTIVITE_ID) 
        references jpa08_hb_activite (id);

    alter table jpa08_hb_personne_activite 
        add index FK5A6A55A568C7A284 (PERSONNE_ID), 
        add constraint FK5A6A55A568C7A284 
        foreign key (PERSONNE_ID) 
        references jpa08_hb_personne (id);

Este DDL es análogo al obtenido con la tabla de unión explícita y corresponde al esquema ya presentado:

2.6.5. InitDB

No nos detendremos mucho en la clase [InitDB], ya que es idéntica a su versión anterior y arroja los mismos resultados. Simplemente, analicemos el siguiente código que muestra la unión entre Personne y Activite:


        // visualización de personas/actividades
        System.out.println("[personnes/activites]");
        Iterator iterator = em.createQuery("select p.id,a.id from Personne p join p.activites a").getResultList().iterator();
        while (iterator.hasNext()) {
            Object[] row = (Object[]) iterator.next();
            System.out.format("[%d,%d]%n", (Long) row[0], (Long) row[1]);
}
  • línea 3: la orden JPQL que realiza la unión. El resultado de select devuelve los identificadores de las entidades Personne y Activite, vinculadas entre sí por la tabla de unión. La lista devuelta por select está formada por filas que contienen dos objetos de tipo Long. Para recorrer esta lista, la fila 3 solicita un objeto Iterator de la lista.
  • líneas 4-7: con ayuda del objeto de tipo Iterator anterior, se recorre la lista.
    • línea 5: cada elemento de la lista es una matriz que contiene una línea resultante del select
    • línea 6: se recuperan los elementos de la línea actual resultante de select realizando los cambios de tipo adecuados.

El resultado de [InitDB] es el siguiente:

[personnes]
P[1,0,p1,Paul,31/01/2000,true,2,1]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[personnes/activites]
[1,1]
[1,2]
[2,1]
[2,3]
terminé...

2.6.6. Principal

La clase [Main] encadena varias pruebas, de las cuales revisaremos algunas.

2.6.6.1. Prueba 3

Esta prueba es la siguiente:


// eliminación de la actividad act1
    public static void test3() {
        // contexto de persistencia
        EntityManager em = getEntityManager();
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // eliminación de la actividad act1 de p2
        p2.getActivites().remove(act1);
        // se retira act1 del contexto de persistencia
        em.remove(act1);
        // fin de las transacciones
        tx.commit();
        // se muestran las nuevas tablas
        dumpPersonne();
        dumpActivite();
        dumpAdresse();
        dumpPersonne_Activite();
    }
  • línea 11: la actividad act1 se retira del contexto de persistencia
  • línea 9: la actividad act1 forma parte de las actividades de la única persona que queda en el contexto, la persona p2. La línea 9 retira la actividad act1 de las actividades de la persona p2. Hacemos esto para mantener la coherencia del contexto de persistencia, ya que lo conservamos para lo que sigue.

Los resultados son los siguientes:

main : ----------- test1
[personnes]
P[1,0,p1,Paul,31/01/2000,true,2,1]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,0,act1]
Ac[2,0,act2]
Ac[3,0,act3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[1,1]
[1,2]
[2,1]
[2,3]
main : ----------- test2
[personnes]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,0,act1]
Ac[2,0,act2]
Ac[3,0,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[2,1]
[2,3]
main : ----------- test3
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[2,0,act2]
Ac[3,0,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[2,3]
  • la actividad act1, que aparece en la línea 26 de test2, ha desaparecido de las actividades de test3 (líneas 40-41)
  • La persona p2 tenía en test2 la actividad act1 (línea 33). Al finalizar test3, ya no la tiene (línea 47)

2.6.6.2. Prueba 6

Esta prueba es la siguiente:


// modificación de las actividades de una persona
    public static void test6() {
        // contexto de persistencia
        EntityManager em = getNewEntityManager();
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // se recupera la persona p2
        p2 = em.find(Personne.class, p2.getId());
        // se recupera la actividad act2
        act2 = em.find(Activite.class, act2.getId());
        // p2 ya solo realiza la actividad act2
        p2.getActivites().clear();
        p2.getActivites().add(act2);
        // fin de la transacción
        tx.commit();
        // se muestran las nuevas tablas
        dumpPersonne();
        dumpActivite();
        dumpPersonne_Activite();
    }
  • línea 4: se utiliza un contexto de persistencia nuevo y vacío
  • línea 9: la persona p2 se transfiere de la base de datos al contexto de persistencia
  • línea 11: la actividad act2 se transfiere de la base de datos al contexto de persistencia
  • línea 13: las actividades de la persona p2 (act3) se transfieren de la base de datos al contexto (fetchType.LAZY). Es la llamada [getActivites] la que provoca esta carga. Se eliminan las actividades de p2. No se trata de una eliminación real de actividades (remove), sino de una modificación del estado de la persona p2. Ya no realiza ninguna actividad.
  • línea 14: se agrega a la persona p2 la actividad act2. Al final, el conjunto de nuevas actividades de la persona p2 es el conjunto {act2}.
  • línea 16: fin de la transacción. La sincronización revisará los objetos del contexto (p2, act2, act3) y detectará que el estado de p2 ha cambiado. Se ejecutarán las órdenes SQL que reflejan este cambio en la base de datos.
  • líneas 18-20: se muestran todas las tablas

Los resultados son los siguientes:

main : ----------- test4
1 - Activités de la personne p2 (JPQL) :
act3
2 - Activités de la personne p2 (relation principale) :
act3
main : ----------- test5
1 - Personnes pratiquant l'activité act3 (JPQL) :
p2
2 - Personnes pratiquant l'activité act3 (relation inverse) :
p2
main : ----------- test6
[personnes]
P[2,2,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[2,0,act2]
Ac[3,0,act3]
[personnes/activites]
[2,2]
  • Al finalizar la prueba 4, la persona p2 realizaba la actividad act3 (línea 3).
  • Al finalizar la prueba 6 (línea 19), la persona p2 ya no realiza la actividad act3 (línea 3) y realiza la actividad act2.

Ahora utilizamos una implementación JPA / Toplink:

El proyecto de Eclipse con Toplink es una copia del proyecto de Eclipse con Hibernate:

El archivo <persistence.xml> [2] se modifica en un punto, el de las entidades declaradas:


        <!--  proveedor -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- clases persistentes -->
        <class>entites.Activite</class>
        <class>entites.Adresse</class>
        <class>entites.Personne</class>
...
  • líneas 4-6: las entidades administradas

La ejecución de [InitDB] junto con SGBD y MySQL5 arroja los siguientes resultados:

En [1], la salida de la consola; en [2], las tablas generadas [jpa07_tl]; y en [3], los scripts generados SQL. Su contenido es el siguiente:

create.sql


CREATE TABLE jpa08_tl_personne_activite (PERSONNE_ID BIGINT NOT NULL, ACTIVITE_ID BIGINT NOT NULL, PRIMARY KEY (PERSONNE_ID, ACTIVITE_ID))
CREATE TABLE jpa08_tl_activite (ID BIGINT NOT NULL, VERSION INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa08_tl_personne (ID BIGINT NOT NULL, DATENAISSANCE DATE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, NBENFANTS INTEGER NOT NULL, VERSION INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa08_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR2 VARCHAR(30), CEDEX VARCHAR(3), ADR1 VARCHAR(30) NOT NULL, PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa08_tl_personne_activite ADD CONSTRAINT FK_jpa08_tl_personne_activite_ACTIVITE_ID FOREIGN KEY (ACTIVITE_ID) REFERENCES jpa08_tl_activite (ID)
ALTER TABLE jpa08_tl_personne_activite ADD CONSTRAINT FK_jpa08_tl_personne_activite_PERSONNE_ID FOREIGN KEY (PERSONNE_ID) REFERENCES jpa08_tl_personne (ID)
ALTER TABLE jpa08_tl_personne ADD CONSTRAINT FK_jpa08_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa08_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

La ejecución de [InitDB] y la de [Main] se realizan sin errores.

2.6.8. El proyecto Eclipse / Hibernate 2

Creamos un proyecto de Eclipse a partir del anterior mediante copia:

En [1], el proyecto de Eclipse; en [2], los códigos Java. El proyecto se encuentra en [3] dentro de la carpeta de ejemplos [4]. Lo importaremos.

Modificamos la relación que vincula Personne con Activité de la siguiente manera:

Persona


    // relación Persona (muchas) -> Actividad (muchas) a través de una tabla de unión personne_activite
    // personne_activite(PERSONNE_ID) es una clave foránea en Persona(id)
    // personne_activite(ACTIVITE_ID) es una clave foránea en Actividad (id)
    // sin cascada en las actividades
    // @ManyToMany(cascada={CascadeType.PERSIST})
    @ManyToMany()
    @JoinTable(name = "jpa09_hb_personne_activite", joinColumns = @JoinColumn(name = "PERSONNE_ID"), inverseJoinColumns = @JoinColumn(name = "ACTIVITE_ID"))
private Set<Activite> activites = new HashSet<Activite>();
  • línea 6: la relación principal @ManyToMany ya no tiene una cascada de persistencia Persona -> Actividad (véase la versión anterior, línea 5)

Actividad


    // ya no hay relación inversa con Persona
    // @ManyToMany(mappedBy = "actividades")
// private Set<Persona> personas = new HashSet<Persona>();
  • líneas 2-3: se elimina la relación inversa @ManyToMany Actividad -> Persona

Buscamos demostrar que los atributos eliminados (cascada y relación inversa) no son imprescindibles. El primer cambio que trae esta nueva configuración se encuentra en [InitDB]:


        // asociaciones personas <--> actividades
        p1.getActivites().add(act1);
        p1.getActivites().add(act2);
        p2.getActivites().add(act1);
        p2.getActivites().add(act3);
        // persistencia de las actividades
        em.persist(act1);
        em.persist(act2);
        em.persist(act3);
        // persistencia de las personas
        em.persist(p1);
        em.persist(p2);
        em.persist(p3);
        // y de la dirección a4 no vinculada a una persona
em.persist(adr4);
  • líneas 7-9: nos vemos obligados a incluir explícitamente las actividades de act1 a act3 en el contexto de persistencia. Cuando existía la cascada de persistencia Persona -> Actividad existía, las líneas 11-13 persistían tanto las personas de p1 a p3 como las actividades de esas personas de act1 a act3.

Se observa un segundo cambio en [Main]:


    // búsqueda de personas que realizan una actividad determinada
    public static void test5() {
        // contexto de persistencia
        EntityManager em = getNewEntityManager();
        // inicio de la transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        System.out.format("1 - Personnes pratiquant l'activité act3 (JPQL) :%n");
        // se solicitan las actividades de p2
        for (Object pa : em.createQuery("select p.nom from Personne p join p.activites a where a.nom='act3'").getResultList()) {
            System.out.println(pa);
        }
        // fin de transacción
        tx.commit();
}
  • líneas 9-12: la consulta JPQL que obtiene las personas que realizan la actividad act3
  • en la versión anterior, el mismo resultado también se había obtenido a través de la relación inversa Actividad -> Persona, ahora eliminada:

        // se pasa por la relación inversa de act3
        System.out.format("2 - Personnes pratiquant l'activité act3 (relation inverse) :%n");
        act3 = em.find(Activite.class, act3.getId());
        for (Personne p : act3.getPersonnes()) {
            System.out.println(p.getNom());
}

Estamos creando un proyecto de Eclipse a partir del proyecto anterior Eclipse / Toplink mediante copia:

En [1], el proyecto Eclipse; en [2], los códigos Java. El proyecto se encuentra en [3] dentro de la carpeta de ejemplos [4]. Lo importaremos.

Los códigos Java son idénticos a los de la versión de Hibernate.

2.7. Ejemplo 7: uso de consultas con nombre

Terminamos esta extensa presentación de las entidades JPA, iniciada en el párrafo 2, con un último ejemplo que muestra el uso de consultas JPQL externalizadas en un archivo de configuración. Este ejemplo proviene de la siguiente fuente:

[ref2]: «Getting started With JPA in Spring 2.0», de Mark Fisher, en la URL

[http://blog.springframework.com/markf/archives/2006/05/30/getting-started-with-jpa-in-spring-20/].

2.7.1. La base de datos de ejemplo

La base de datos es la siguiente:

  • en [1]: una lista de restaurantes con su nombre y dirección
  • en [2]: la tabla de direcciones de los restaurantes, limitada al número de la calle y al nombre de la calle. Existe una relación uno a uno entre las tablas restaurant y adresse: un restaurante tiene una dirección y solo una.
  • en [3]: una tabla de platillos con su nombre y un indicador verdadero/falso para indicar si el platillo es vegetariano o no
  • en [4]: la tabla de unión entre restaurantes y platillos: un restaurante sirve varios platillos y un mismo platillo puede ser servido por varios restaurantes. Existe una relación de varios a varios entre las tablas restaurant y plat.

2.7.2. Los objetos @Entity que representan la base de datos

Las tablas anteriores serán representadas por las siguientes @Entity:

  • la @Entity Restaurant representará la tabla [restaurant]
  • el @Entity Adresse representará la tabla [adresse]
  • la @Entity Plat representará la tabla [plat]

Las relaciones entre estas entidades son las siguientes:

  • una relación uno a uno vincula la entidad Restaurant con la entidad Adresse: un restaurante r tiene una dirección a. La entidad Restaurant, que contiene la clave externa, tendrá la relación principal. La entidad Adresse no tendrá una relación inversa.
  • Una relación de varios a varios conecta las entidades Restaurant y Plat: un restaurante sirve varios platillos y un mismo platillo puede ser servido por varios restaurantes. Esta relación se materializará mediante una anotación @ManyToMany en la entidad Restaurant. La entidad Plat no tendrá una relación inversa.

La @Entity Restaurant es la siguiente:


package entites;

...
@Entity
@Table(name = "jpa10_hb_restaurant")
public class Restaurant implements java.io.Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Column(unique = true, length = 30, nullable = false)
    private String nom;

    @OneToOne(cascade = CascadeType.ALL)
    private Adresse adresse;

    @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
    @JoinTable(name = "jpa10_hb_restaurant_plat", inverseJoinColumns = @JoinColumn(name = "plat_id"))
    private Set<Plat> plats = new HashSet<Plat>();

    // constructores
    public Restaurant() {

    }

    public Restaurant(String name, Adresse address, Set<Plat> entrees) {
...
    }

    // getters y setters
...

    // toString
    public String toString() {
        String signature = "R[" + getNom() + "," + getAdresse();
        for (Plat e : getPlats()) {
            signature += "," + e;
        }
        return signature + "]";
    }
}
  • línea 17: la relación uno a uno que tiene la entidad Restaurant con la entidad Adresse. Todas las operaciones de persistencia sobre un restaurante se propagan a su dirección.
  • línea 20: la relación que vincula a la @Entity Restaurant con la @Entity Plat del conjunto plats de la línea 22 es de tipo varios a varios (ManyToMany):
    • un restaurante (One) tiene varios platillos (Many)
    • un platillo (One) puede ser servido por varios restaurantes (Many)
    • al final, las @Entity Restaurant y Plat están vinculadas por una relación ManyToMany. Decidimos que la @Entity Restaurant tendrá la relación principal y que la @Entity Plat no tendrá una relación inversa.
    • La relación @ManyToMany requiere una tabla de unión. Esta se define mediante la anotación @JoinTable en la línea 47.
      • El atributo name le da un nombre a la tabla.
      • La tabla de unión está compuesta por las claves externas de las tablas que une. Aquí hay dos claves externas: una en la tabla [restaurant] y la otra en la tabla [plat]. Estas columnas de clave externa se definen mediante los atributos joinColumns y inverseJoinColumns.
      • El atributo joinColumns define la clave externa en la tabla de la @Entity que contiene la relación principal @ManyToMany, en este caso la tabla [restaurant]. El atributo joinColumns no aparece aquí. JPA tiene un valor por defecto en este caso: [table]_[clé_primaire_de_table], en este caso [jpa10_hb_restaurant_id].
      • La anotación @JoinColumn del atributo inverseJoinColumns define la clave externa en la tabla de la @Entity que contiene la relación inversa @ManyToMany, en este caso la tabla [plat]. Esta columna de clave externa se llamará plat_id.

La @Entity Adresse es la siguiente:


package entites;

...
@Entity
@Table(name="jpa10_hb_adresse")
public class Adresse implements java.io.Serializable {
  
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;
  
  @Column(name = "NUMERO_RUE")
  private int numeroRue;
  
  @Column(name = "NOM_RUE", length=30, nullable=false)
  private String nomRue;
  
  // getters y setters
 ...
 
  // constructores
  public Adresse(int streetNumber, String streetName){
...
  }
  
  public Adresse(){
    
  }
  
  // toString
  public String toString(){
    return "A["+getNumeroRue()+","+getNomRue()+"]";
  }
}
  • La @Entity Dirección es una entidad sin relación directa con las demás entidades. Solo se puede persistir a través de una entidad Restaurant.
  • Una dirección se define por un nombre de calle (línea 16) y un número en la calle (línea 13).

La @Entity Plat es la siguiente


package entites;
...
@Entity
@Table(name="jpa10_hb_plat")
public class Plat implements java.io.Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Column(unique=true, length=50, nullable=false)
    private String nom;

    private boolean vegetarien;

    // constructores
    public Plat() {

    }

    public Plat(String name, boolean vegetarian) {
...
    }

    // getters y setters
...

    // toString
    public String toString() {
        return "E[" + getNom() + "," + isVegetarien() + "]";
    }

}
  • La @Entity Plat es una entidad sin relación directa con las demás entidades. Solo se puede persistir a través de una entidad Restaurant.
  • Un plat se define por un nombre (línea 12) y un tipo (vegetariano o no) (línea 14).

2.7.3. El proyecto Eclipse / Hibernate

La implementación JPA utilizada aquí es la de Hibernate. El proyecto de Eclipse para las pruebas es el siguiente:

En [1] se encuentra el proyecto de Eclipse; en [2], los códigos Java; y en JPA, la configuración de la capa. Cabe destacar la presencia de un archivo [orm.xml] que aún no hemos visto. El proyecto se encuentra en [3] dentro de la carpeta de ejemplos [4]. Lo importaremos.

2.7.4. Generación del archivo DDL de la base de datos

Siguiendo las instrucciones del párrafo 2.1.7, el archivo DDL obtenido para los archivos SGBD y MySQL5 es el siguiente:


alter table jpa10_hb_restaurant 
        drop 
        foreign key FK3E8E4F5D5FE379D0;

    alter table jpa10_hb_restaurant_plat 
        drop 
        foreign key FK1D2D06D11F0F78A4;

    alter table jpa10_hb_restaurant_plat 
        drop 
        foreign key FK1D2D06D1AFAC3E44;

    drop table if exists jpa10_hb_adresse;

    drop table if exists jpa10_hb_plat;

    drop table if exists jpa10_hb_restaurant;

    drop table if exists jpa10_hb_restaurant_plat;

    create table jpa10_hb_adresse (
        id bigint not null auto_increment,
        NUMERO_RUE integer,
        NOM_RUE varchar(30) not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa10_hb_plat (
        id bigint not null auto_increment,
        nom varchar(50) not null unique,
        vegetarien bit not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa10_hb_restaurant (
        id bigint not null auto_increment,
        nom varchar(30) not null unique,
        adresse_id bigint,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa10_hb_restaurant_plat (
        jpa10_hb_restaurant_id bigint not null,
        plat_id bigint not null,
        primary key (jpa10_hb_restaurant_id, plat_id)
    ) ENGINE=InnoDB;

    alter table jpa10_hb_restaurant 
        add index FK3E8E4F5D5FE379D0 (adresse_id), 
        add constraint FK3E8E4F5D5FE379D0 
        foreign key (adresse_id) 
        references jpa10_hb_adresse (id);

    alter table jpa10_hb_restaurant_plat 
        add index FK1D2D06D11F0F78A4 (plat_id), 
        add constraint FK1D2D06D11F0F78A4 
        foreign key (plat_id) 
        references jpa10_hb_plat (id);

    alter table jpa10_hb_restaurant_plat 
        add index FK1D2D06D1AFAC3E44 (jpa10_hb_restaurant_id), 
        add constraint FK1D2D06D1AFAC3E44 
        foreign key (jpa10_hb_restaurant_id) 
        references jpa10_hb_restaurant (id);
  • líneas 21-26: la tabla [adresse]
  • líneas 28-33: la tabla [plat]
  • líneas 35-40: la tabla [restaurant]
  • líneas 42-46: la tabla de unión [restaurant_plat]. Cabe destacar la clave compuesta (línea 45)
  • líneas 48-52: la clave externa de la tabla [restaurant] hacia la tabla [adresse]
  • líneas 54-58: la clave externa de la tabla [restaurant_plat] hacia la tabla [plat]
  • líneas 60-64: la clave externa de la tabla [restaurant_plat] hacia la tabla [restaurant]

Esta tabla DDL corresponde al esquema ya presentado:

En la perspectiva SQL Explorer, la base de datos se presenta de la siguiente manera:

  • en [1]: las 4 tablas de la base
  • en [2]: las direcciones
  • en [3]: los platillos
  • en [4]: los restaurantes. [adresse_id] hace referencia a las direcciones de [2].
  • en [5]: la tabla de unión [restaurant,plat]. [jpa10_hb_restaurant_id] hace referencia a los restaurantes de [4] y [plat_id] a los platillos de [3]. Así, [1,1] significa que el restaurante «Burger Barn» sirve el platillo «CheeseBurger».

Para obtener los datos anteriores, se ejecutó el programa [QueryDB] del proyecto Eclipse.

2.7.5. Consultas JPQL con una consola de Hibernate

Creamos una consola de Hibernate vinculada al proyecto Eclipse anterior. Seguiremos el procedimiento ya explicado en dos ocasiones, en particular en el párrafo 2.1.12.

  • en [1] y [2]: la configuración de la consola de Hibernate
  • en [3]: una consulta JPQL y en [4] el resultado.
  • en [5]: la orden equivalente SQL

Ahora presentamos una serie de consultas JPQL. Invitamos al lector a ejecutarlas y descubrir la orden SQL generada por Hibernate para ejecutarlas.

Obtener todos los restaurantes con sus platillos:

Obtener los restaurantes que sirven al menos un platillo vegetariano:

Obtener los nombres de los restaurantes que solo sirven platillos vegetarianos:

Obtener los restaurantes que sirven hamburguesas:

2.7.6. QueryDB

Ahora nos enfocamos en el programa [QueryDB] del proyecto Eclipse, que:

  • llena la base
  • emite sobre ella una serie de consultas JPQL. Estas se registran en el archivo [META-INF/orm.xml] del proyecto Eclipse:

El archivo [orm.xml] se puede utilizar para configurar la capa JPA en lugar de las anotaciones de Java. Esto aporta flexibilidad a la configuración de la capa JPA. Se puede modificar sin necesidad de recompilar el código Java. Se pueden utilizar ambos métodos simultáneamente: anotaciones Java y el archivo [orm.xml]. La configuración JPA se realiza primero con las anotaciones Java y luego con el archivo [orm.xml]. Por lo tanto, si se desea modificar una configuración realizada mediante una anotación de Java sin recompilar, basta con incluir esa configuración en [orm.xml]. Será esta la que prevalecerá.

En nuestro ejemplo, el archivo [orm.xml] se utiliza para guardar los textos de las consultas de JPQL. Su contenido es el siguiente:


<?xml version="1.0" encoding="UTF-8" ?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd" version="1.0">
    <description>Restaurants</description>
    <named-query name="supprimer le contenu de la table restaurant">
        <query>delete from Restaurant</query>
    </named-query>
    <named-query name="supprimer le contenu de la table plat">
        <query>delete from Plat</query>
    </named-query>
    <named-query name="obtenir tous les restaurants">
        <query>select r from Restaurant r order by r.nom asc</query>
    </named-query>
    <named-query name="obtenir toutes les adresses">
        <query>select a from Adresse a order by a.nomRue asc</query>
    </named-query>
    <named-query name="obtenir tous les plats">
        <query>select p from Plat p order by p.nom asc</query>
    </named-query>
    <named-query name="obtenir tous les restaurants avec leurs plats">
        <query>select r.nom,p.nom from Restaurant r join r.plats p</query>
    </named-query>
    <named-query name="obtenir les restaurants ayant au moins un plat vegetarien">
        <query>select distinct r from Restaurant r join r.plats p where p.vegetarien=true</query>
    </named-query>
    <named-query name="obtenir les restaurants avec uniquement des plats vegetariens">
        <query>
            select distinct r1.nom from Restaurant r1 where not exists (select p1 from Restaurant r2 join r2.plats p1 where r2.id=r1.id and
            p1.vegetarien=false)
        </query>
    </named-query>
    <named-query name="obtenir les restaurants d'une certaine rue">
        <query>select r from Restaurant r where r.adresse.nomRue=:nomRue</query>
    </named-query>
    <named-query name="obtenir les restaurants qui servent des burgers">
        <query>select r.nom,r.adresse.numeroRue, r.adresse.nomRue, p.nom from Restaurant r join r.plats p where p.nom like '%burger'</query>
    </named-query>
    <named-query name="obtenir les plats du restaurant untel">
        <query>select p.nom from Restaurant r join r.plats p where r.nom=:nomRestaurant</query>
    </named-query>
</entity-mappings>
  • La raíz del archivo [orm.xml] es <entity-mappings> (línea 2).
  • Líneas 5-7: las consultas JPQL con nombre están dentro de las etiquetas <named-query name= "... ">texto</namedquery>.
    • El atributo name de la etiqueta es el nombre de la consulta.
    • El contenido texte de la etiqueta es el texto de la consulta.

QueryDB ejecutará las consultas anteriores. Su código es el siguiente:


package tests;

...
public class QueryDB {

    // Contexto de persistencia
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");

    private static EntityManager em = emf.createEntityManager();

    public static void main(String[] args) {
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // eliminar elementos de la tabla [restaurant]
        em.createNamedQuery("supprimer le contenu de la table restaurant").executeUpdate();
        // Eliminar elementos de la tabla [plat]
        em.createNamedQuery("supprimer le contenu de la table plat").executeUpdate();
        // creación de objetos Address
        Adresse adr1 = new Adresse(10, "Main Street");
        Adresse adr2 = new Adresse(20, "Main Street");
        Adresse adr3 = new Adresse(123, "Dover Street");
        // creación de objetos Entrada
        Plat ent1 = new Plat("Hamburger", false);
        Plat ent2 = new Plat("Cheeseburger", false);
        Plat ent3 = new Plat("Tofu Stir Fry", true);
        Plat ent4 = new Plat("Vegetable Soup", true);
        // creación de objetos «Restaurant»
        Restaurant restaurant1 = new Restaurant();
        restaurant1.setNom("Burger Barn");
        restaurant1.setAdresse(adr1);
        restaurant1.getPlats().add(ent1);
        restaurant1.getPlats().add(ent2);
        Restaurant restaurant2 = new Restaurant();
        restaurant2.setNom("Veggie Village");
        restaurant2.setAdresse(adr2);
        restaurant2.getPlats().add(ent3);
        restaurant2.getPlats().add(ent4);
        Restaurant restaurant3 = new Restaurant();
        restaurant3.setNom("Dover Diner");
        restaurant3.setAdresse(adr3);
        restaurant3.getPlats().add(ent1);
        restaurant3.getPlats().add(ent2);
        restaurant3.getPlats().add(ent4);
        // persistencia de los objetos «Restaurant» (y de los demás objetos en cascada)
        em.persist(restaurant1);
        em.persist(restaurant2);
        em.persist(restaurant3);
        // fin de transacción
        tx.commit();
        // volcado de la base de datos
        dumpDataBase();
        // fin EntityManager
        em.close();
        // fin de EntityManagerFactory
        emf.close();
    }

    // visualización del contenido de la base
    @SuppressWarnings("unchecked")
    private static void dumpDataBase() {
        // prueba2
        log("données de la base");
        // inicio de transacción
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // visualizaciones de restaurantes
        log("[restaurants]");
        for (Object restaurant : em.createNamedQuery("obtenir tous les restaurants").getResultList()) {
            System.out.println(restaurant);
        }
        // visualizaciones de direcciones
        log("[adresses]");
        for (Object adresse : em.createNamedQuery("obtenir toutes les adresses").getResultList()) {
            System.out.println(adresse);
        }
        // visualizaciones de platillos
        log("[plats]");
        for (Object plat : em.createNamedQuery("obtenir tous les plats").getResultList()) {
            System.out.println(plat);
        }
        // visualizaciones de enlaces entre restaurantes y platillos
        log("[restaurants/plats]");
        Iterator record = em.createNamedQuery("obtenir tous les restaurants avec leurs plats").getResultList().iterator();
        while (record.hasNext()) {
            Object[] currentRecord = (Object[]) record.next();
            System.out.format("[%s,%s]%n", currentRecord[0], currentRecord[1]);
        }
        log("[Liste des restaurants avec au moins un plat végétarien]");
        for (Object r : em.createNamedQuery("obtenir les restaurants ayant au moins un plat vegetarien").getResultList()) {
            System.out.println(r);
        }
        // consulta
        log("[Liste des restaurants avec seulement des plats végétariens]");
        for (Object r : em.createNamedQuery("obtenir les restaurants avec uniquement des plats vegetariens").getResultList()) {
            System.out.println(r);
        }
        // consulta
        log("[Liste des restaurants dans Dover Street]");
        for (Object r : em.createNamedQuery("obtenir les restaurants d'une certaine rue").setParameter("nomRue", "Dover Street").getResultList()) {
            System.out.println(r);
        }
        // consulta
        log("[Liste des restaurants ayant un plat de type burger]");
        record = em.createNamedQuery("obtenir les restaurants qui servent des burgers").getResultList().iterator();
        while (record.hasNext()) {
            Object[] currentRecord = (Object[]) record.next();
            System.out.format("[%s,%d,%s,%s]%n", currentRecord[0], currentRecord[1], currentRecord[2], currentRecord[3]);
        }
        // consulta
        log("[Plats de Veggie Village]");
        for (Object r : em.createNamedQuery("obtenir les plats du restaurant untel").setParameter("nomRestaurant", "Veggie Village").getResultList()) {
            System.out.println(r);
        }
        // fin de transacción
        tx.commit();
    }

    // registros
    private static void log(String message) {
        System.out.println(" -----------" + message);
    }

}

El resultado de la ejecución de [QueryDB] es el siguiente:

-----------données de la base
 -----------[restaurants]
R[Burger Barn,A[10,Main Street],E[Cheeseburger,false],E[Hamburger,false]]
R[Dover Diner,A[123,Dover Street],E[Cheeseburger,false],E[Hamburger,false],E[Vegetable Soup,true]]
R[Veggie Village,A[20,Main Street],E[Tofu Stir Fry,true],E[Vegetable Soup,true]]
 -----------[adresses]
A[123,Dover Street]
A[10,Main Street]
A[20,Main Street]
 -----------[plats]
E[Cheeseburger,false]
E[Hamburger,false]
E[Tofu Stir Fry,true]
E[Vegetable Soup,true]
 -----------[restaurants/plats]
[Burger Barn,Cheeseburger]
[Burger Barn,Hamburger]
[Dover Diner,Cheeseburger]
[Dover Diner,Hamburger]
[Dover Diner,Vegetable Soup]
[Veggie Village,Tofu Stir Fry]
[Veggie Village,Vegetable Soup]
 -----------[Liste des restaurants avec au moins un plat végétarien]
R[Veggie Village,A[20,Main Street],E[Tofu Stir Fry,true],E[Vegetable Soup,true]]
R[Dover Diner,A[123,Dover Street],E[Cheeseburger,false],E[Hamburger,false],E[Vegetable Soup,true]]
 -----------[Liste des restaurants avec seulement des plats végétariens]
Veggie Village
 -----------[Liste des restaurants dans Dover Street]
R[Dover Diner,A[123,Dover Street],E[Cheeseburger,false],E[Hamburger,false],E[Vegetable Soup,true]]
 -----------[Liste des restaurants ayant un plat de type burger]
[Burger Barn,10,Main Street,Cheeseburger]
[Burger Barn,10,Main Street,Hamburger]
[Dover Diner,123,Dover Street,Cheeseburger]
[Dover Diner,123,Dover Street,Hamburger]
 -----------[Plats de Veggie Village]
Tofu Stir Fry
Vegetable Soup

Dejamos al lector la tarea de establecer la relación entre el código y los resultados. Para ello, le recomendamos ejecutar la consulta JPQL en la consola de Hibernate y examinar el código SQL que la acompaña.

El lector interesado encontrará en los ejemplos descargables con este tutorial el proyecto anterior implementado con Toplink:

El proyecto de Eclipse con Toplink es una copia del proyecto de Eclipse con Hibernate:

El archivo <persistence.xml> [2] declara las entidades administradas:


        <!--  proveedor -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
            <!-- clases persistentes -->
        <class>entites.Restaurant</class>
        <class>entites.Adresse</class>
        <class>entites.Plat</class>

...
  • líneas 4-6: las entidades administradas

Las consultas JPQL registradas en [orm.xml] se ejecutan correctamente mediante Toplink. Para ello, en el proyecto anterior nos aseguramos de no utilizar consultas HQL (Hibernate Query Language), que de hecho es un superconjunto de JPQL y cuya sintaxis, en algunos casos, no es aceptada por JPQL.

2.8. Conclusion

Con esto concluimos nuestro estudio de las entidades JPA. Ha sido un proceso extenso y, sin embargo, aún quedan aspectos importantes (para el desarrollador avanzado) que no se han abordado. Una vez más, se recomienda leer un libro de referencia como el que se utilizó para este tutorial:

[ref1]: Java Persistence with Hibernate, de Christian Bauer y Gavin King, publicado por Manning.