Skip to content

3. JPA en una arquitectura de múltiples capas

Para estudiar el API JPA, utilizamos la siguiente arquitectura de prueba:

Nuestros programas de prueba eran aplicaciones de consola que consultaban directamente la capa JPA. En esta ocasión, descubrimos los principales métodos de la capa JPA. Nos encontrábamos en un entorno denominado «Java SE» (Edición Estándar). JPA funciona tanto en un entorno Java SE como en uno Java EE5 (Edición Empresarial).

Ahora que ya dominamos tanto la configuración del puente relacional/objeto como el uso de los métodos de la capa JPA, volvemos a una arquitectura multicapa más clásica:

Se accederá a la capa [JPA] a través de una arquitectura de dos capas: [metier] y [dao]. Se utilizará el marco Spring [7], y luego el contenedor EJB3 de JBoss y [8] para vincular estas capas entre sí.

Como mencionamos anteriormente, JPA está disponible en los entornos SE y EE5. El entorno Java EE5 ofrece numerosos servicios en el ámbito del acceso a datos persistentes, en particular grupos de conexiones, administradores de transacciones, etc. Puede ser interesante para un desarrollador aprovechar estos servicios. El entorno Java EE5 aún no está muy extendido (mayo de 2007). Actualmente se encuentra en el servidor de aplicaciones Sun Application Server 9.x (Glassfish). Un servidor de aplicaciones es, esencialmente, un servidor de aplicaciones web. Si se desarrolla una aplicación gráfica independiente de tipo Swing, no se puede disponer del entorno EE ni de los servicios que ofrece. Esto representa un problema. Se están empezando a ver entornos EE «independientes», c.a.d. que pueden utilizarse fuera de un servidor de aplicaciones. Este es el caso de JBoss y EJB3, que utilizaremos en este documento.

En un entorno EE5, las capas se implementan mediante objetos llamados EJB (Enterprise Java Bean). En versiones anteriores de EE, los EJB (EJB y 2.x) tenían fama de ser difíciles de implementar y de probar, y en ocasiones ofrecían un rendimiento deficiente. Se distingue entre los EJB2.x «entity» y los EJB2.x «session». En resumen, un EJB2.x «entidad» es la representación de una fila de una tabla de base de datos y un EJB2.x «sesión» es un objeto utilizado para implementar las capas [metier], [dao] de una arquitectura multicapa. Una de las principales críticas a las capas implementadas con EJB es que solo se pueden utilizar dentro de contenedores EJB, un servicio proporcionado por el entorno EE. Esto dificulta las pruebas unitarias. Así, en el esquema anterior, las pruebas unitarias de las capas [metier] y [dao], construidas con EJB, requerirían la configuración de un servidor de aplicaciones, una operación bastante engorrosa que no motiva realmente al desarrollador a realizar pruebas con frecuencia.

El marco Spring surgió como respuesta a la complejidad de los EJB2. Spring proporciona, en un entorno SE, una gran cantidad de los servicios que suelen ofrecer los entornos EE. Así, en la sección de «Persistencia de datos» que nos interesa aquí, Spring proporciona los grupos de conexiones y los administradores de transacciones que necesitan las aplicaciones. La aparición de Spring ha fomentado la cultura de las pruebas unitarias, que de repente se volvieron mucho más fáciles de implementar. Spring permite implementar las capas de una aplicación mediante objetos Java clásicos (POJO, Plain Old/Ordinary Java Object), lo que permite reutilizarlos en otro contexto. Por último, integra numerosas herramientas de terceros de manera bastante transparente, en particular herramientas de persistencia como Hibernate, iBatis, etc.

Java EE5 fue diseñado para corregir las deficiencias de la especificación anterior EE. Los EJB y 2.x se han convertido en los EJB3. Estos son POJOs etiquetados con anotaciones que los convierten en objetos especiales cuando se encuentran dentro de un contenedor EJB3. Dentro de este, el EJB3 podrá beneficiarse de los servicios del contenedor (pool de conexiones, administrador de transacciones, etc.). Fuera del contenedor EJB3, el EJB3 se convierte en un objeto Java normal. Sus anotaciones EJB se ignoran.

Anteriormente, hemos representado a Spring y a JBoss EJB3 como una posible infraestructura (framework) de nuestra arquitectura multicapa. Es esta infraestructura la que proporcionará los servicios que necesitamos: un grupo de conexiones y un administrador de transacciones.

  • Con Spring, las capas se implementarán mediante POJOs. Estos tendrán acceso a los servicios de Spring (pool de conexiones, administrador de transacciones) mediante la inyección de dependencias en estos POJOs: al crearlos, Spring les inyecta referencias a los servicios que van a necesitar.
  • JBoss EJB3 es un contenedor EJB que puede funcionar fuera de un servidor de aplicaciones. Su principio de funcionamiento (para el desarrollador) es similar al descrito para Spring. Encontraremos pocas diferencias.

Terminaremos el documento con un ejemplo de aplicación web de tres capas, básico pero representativo:

3.1. Ejemplo 1: Spring / JPA con la entidad Persona

Tomamos la entidad Personne analizada en el párrafo 2.1 y la integramos en una arquitectura de múltiples capas, donde la integración de las capas se realiza con Spring y la capa de persistencia se implementa mediante Hibernate.

Se supone que el lector cuenta con conocimientos básicos sobre Spring. De no ser así, puede consultar el siguiente documento, que explica el concepto de inyección de dependencias, que es el núcleo de Spring:

[ref3]: Spring IoC (Inversión de control) [http://tahe.developpez.com/java/springioc].

3.1.1. El proyecto Eclipse / Spring / Hibernate

El proyecto de Eclipse es el siguiente:

  • en [1]: el proyecto Eclipse. Se encuentra en [6], dentro de los ejemplos del tutorial [5]. Lo importaremos.
  • en [2]: los códigos Java de las capas presentados en paquetes:
    • [entites]: el paquete de entidades JPA
    • [dao]: la capa de acceso a los datos —se basa en la capa JPA
    • [service]: una capa de servicios más que de negocio. En ella se utilizará el servicio de transacciones de los contenedores.
    • [tests]: agrupa los programas de pruebas.
  • en [3]: la biblioteca [jpa-spring] agrupa los archivos JAR necesarios para Spring (véanse también [7] y [8]).
  • en [4]: la carpeta [conf] reúne los archivos de configuración de Spring para cada uno de los SGBD utilizados en este tutorial.

3.1.2. Las entidades JPA

Aquí solo se administra una entidad, la entidad Personne analizada en el párrafo 2.1, cuya configuración se recuerda a continuación:


package entites;

...
@Entity
@Table(name="jpa01_hb_personne")
public class Personne {

    @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
...
}

3.1.3. La capa [dao]

La capa [dao] presenta la siguiente interfaz IDao:


package dao;

import java.util.List;

import entites.Personne;

public interface IDao {
    // obtener una persona mediante su identificador
    public Personne getOne(Integer id);

    // obtener a todas las personas
    public List<Personne> getAll();

    // guardar un contacto
    public Personne saveOne(Personne personne);

    // actualizar un contacto
    public Personne updateOne(Personne personne);

    // eliminar una persona por su identificador
    public void deleteOne(Integer id);

    // obtener las personas cuyo nombre coincida con un patrón
    public List<Personne> getAllLike(String modele);

}

La implementación [Dao] de esta interfaz es la siguiente:


package dao;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import entites.Personne;

public class Dao implements IDao {

    @PersistenceContext
    private EntityManager em;

    // eliminar a una persona mediante su identificador
    public void deleteOne(Integer id) {
        Personne personne = em.find(Personne.class, id);
        if (personne == null) {
            throw new DaoException(2);
        }
        em.remove(personne);
    }

    @SuppressWarnings("unchecked")
    // obtener todas las personas
    public List<Personne> getAll() {
        return em.createQuery("select p from Personne p").getResultList();
    }

    @SuppressWarnings("unchecked")
    // obtener las personas cuyo nombre coincida con un patrón
    public List<Personne> getAllLike(String modele) {
        return em.createQuery("select p from Personne p where p.nom like :modele")
                .setParameter("modele", modele).getResultList();
    }

    // obtener una persona mediante su identificador
    public Personne getOne(Integer id) {
        return em.find(Personne.class, id);
    }

    // guardar un usuario
    public Personne saveOne(Personne personne) {
        em.persist(personne);
        return personne;
    }

    // actualizar un usuario
    public Personne updateOne(Personne personne) {
        return em.merge(personne);
    }

}
  • En primer lugar, cabe destacar la simplicidad de la implementación [Dao]. Esto se debe al uso de la capa JPA, que realiza la mayor parte del trabajo de acceso a los datos.
  • Línea 10: la clase [Dao] implementa la interfaz [IDao]
  • línea 13: el objeto de tipo [EntityManager] que se utilizará para manipular el contexto de persistencia JPA. Por conveniencia, a veces lo confundiremos con el contexto de persistencia mismo. El contexto de persistencia contendrá entidades Personne.
  • línea 12: en ninguna parte del código se inicializa el campo [EntityManager em]. Spring lo inicializará al iniciar la aplicación. Es la anotación JPA @PersistenceContext de la línea 12 la que le pide a Spring que inyecte en em un administrador de contexto de persistencia.
  • líneas 26-28: la lista de todas las personas se obtiene mediante una consulta JPQL.
  • líneas 32-35: la lista de todas las personas cuyo nombre corresponde a un determinado modelo se obtiene mediante una consulta JPQL.
  • líneas 38-40: la persona con dicho identificador se obtiene mediante el método find de API JPA. Devuelve un puntero null si la persona no existe.
  • líneas 43-46: una persona se hace persistente mediante el método `persist` de API JPA. El método hace que la persona sea persistente.
  • líneas 49-51: la actualización de una persona se realiza mediante el método merge de API JPA. Este método solo tiene sentido si la persona así actualizada estaba previamente desvinculada. El método hace que la persona así creada sea persistente.
  • líneas 16-22: la eliminación de la persona cuyo identificador se nos pasa como parámetro se realiza en dos pasos:
    • línea 17: se busca en el contexto de persistencia
    • líneas 18-20: si no se encuentra, se lanza una excepción con el código de error 2
    • línea 21: si se ha encontrado, se elimina del contexto de persistencia con el método remove de API JPA.
  • Lo que no se ve por el momento es que cada método se ejecutará dentro de una transacción iniciada por la capa [service].

La aplicación tiene su propio tipo de excepción llamado [DaoException]:


package dao;

@SuppressWarnings("serial")
public class DaoException extends RuntimeException {

    // código de error
    private int code;

    public DaoException(int code) {
        super();
        this.code = code;
    }

    public DaoException(String message, int code) {
        super(message);
        this.code = code;
    }

    public DaoException(Throwable cause, int code) {
        super(cause);
        this.code = code;
    }

    public DaoException(String message, Throwable cause, int code) {
        super(message, cause);
        this.code = code;
    }

    // getter y setter

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

}
  • línea 4: [DaoException] deriva de [RuntimeException]. Por lo tanto, se trata de un tipo de excepción que el compilador no nos obliga a manejar mediante un try/catch ni a incluir en la firma de los métodos. Por esta razón, [DaoException] no aparece en la firma del método [deleteOne] de la interfaz [IDao]. Esto permite que esta interfaz sea implementada por una clase que lance otro tipo de excepciones, siempre y cuando dicha clase también derive de [RuntimeException].
  • Para diferenciar los errores que pueden ocurrir, se utiliza el código de error de la línea 7. Los tres constructores de las líneas 14, 19 y 24 son los de la clase padre [RuntimeException], a los que se les ha agregado un parámetro: el del código de error que se desea asignar a la excepción.

3.1.4. La capa [metier / service]

La capa [service] presenta la siguiente interfaz [IService]:


package service;

import java.util.List;

import entites.Personne;

public interface IService {
    // obtener un usuario mediante su identificador
    public Personne getOne(Integer id);

    // obtener todas las personas
    public List<Personne> getAll();

    // guardar un perfil
    public Personne saveOne(Personne personne);

    // Actualizar un usuario
    public Personne updateOne(Personne personne);

    // eliminar a una persona mediante su identificador
    public void deleteOne(Integer id);

    // obtener las personas cuyo nombre coincida con un patrón
    public List<Personne> getAllLike(String modele);

    // eliminar varias personas a la vez
    public void deleteArray(Personne[] personnes);

    // guardar varias personas a la vez
    public Personne[] saveArray(Personne[] personnes);

    // actualizar varias personas a la vez
    public Personne[] updateArray(Personne[] personnes);

}
  • líneas 8-24: la interfaz [IService] incluye los métodos de la interfaz [IDao]
  • línea 27: el método [deleteArray] permite eliminar un conjunto de personas dentro de una transacción: se eliminan todas las personas o ninguna.
  • líneas 30 y 33: métodos análogos a [deleteArray] para guardar (línea 30) o actualizar (línea 33) un conjunto de personas dentro de una transacción.

La implementación [Service] de la interfaz [IService] es la siguiente:


package service;

...

// Todos los métodos de la clase se ejecutan en una transacción
@Transactional
public class Service implements IService {

    // capa [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

    // eliminar varias personas a la vez
    public void deleteArray(Personne[] personnes) {
        for (Personne p : personnes) {
            dao.deleteOne(p.getId());
        }
    }

    // eliminar una persona mediante su identificador
    public void deleteOne(Integer id) {
        dao.deleteOne(id);
    }

    // obtener todas las personas
    public List<Personne> getAll() {
        return dao.getAll();
    }

    // obtener las personas cuyo nombre coincida con un patrón
    public List<Personne> getAllLike(String modele) {
        return dao.getAllLike(modele);
    }

    // obtener una persona mediante su identificador
    public Personne getOne(Integer id) {
        return dao.getOne(id);
    }

    // guardar varias personas a la vez
    public Personne[] saveArray(Personne[] personnes) {
        Personne[] personnes2 = new Personne[personnes.length];
        for (int i = 0; i < personnes.length; i++) {
            personnes2[i] = dao.saveOne(personnes[i]);
        }
        return personnes2;
    }

    // guardar una persona
    public Personne saveOne(Personne personne) {
        return dao.saveOne(personne);
    }

    // actualizar varias personas a la vez
    public Personne[] updateArray(Personne[] personnes) {
        Personne[] personnes2 = new Personne[personnes.length];
        for (int i = 0; i < personnes.length; i++) {
            personnes2[i] = dao.updateOne(personnes[i]);
        }
        return personnes2;
    }

    // actualizar un usuario
    public Personne updateOne(Personne personne) {
        return dao.updateOne(personne);
    }

}
  • línea 6: la anotación de Spring @Transactional indica que todos los métodos de la clase deben ejecutarse dentro de una transacción. Se iniciará una transacción antes de que comience la ejecución del método y se cerrará después de la ejecución. Si se produce una excepción de tipo [RuntimeException] o derivada durante la ejecución del método, un rollback automático cancela toda la transacción; de lo contrario, un commit automático la valida. Cabe destacar que el código Java no necesita ocuparse de las transacciones. Estas son administradas por Spring.
  • línea 10: una referencia a la capa [dao]. Veremos más adelante que Spring inicializa esta referencia al iniciar la aplicación.
  • Los métodos de [Service] se limitan a llamar a los métodos de la interfaz [IDao dao] de la línea 10. Dejamos que el lector revise el código. No presenta dificultades particulares.
  • Anteriormente mencionamos que cada método de [Service] se ejecuta en una transacción. Esta transacción está vinculada al hilo de ejecución del método. En este hilo se ejecutan los métodos de la capa [dao]. Estos se vincularán automáticamente a la transacción del hilo de ejecución. El método [deleteArray] (línea 21), por ejemplo, debe ejecutar N veces el método [deleteOne] de la capa [dao]. Estas N ejecuciones se realizarán dentro del hilo de ejecución del método [deleteArray], es decir, dentro de la misma transacción. Por lo tanto, todas se validarán (commit) si todo sale bien, o se anularán (rollback) si se produce una excepción en cualquiera de las N ejecuciones del método [deleteOne] de la capa [dao].

3.1.5. Configuración de las capas

La configuración de las capas [service], [dao] y [JPA] se realiza mediante los dos archivos mencionados anteriormente: [META-INF/persistence.xml] y [spring-config.xml]. Ambos archivos deben estar en el directorio classpath de la aplicación, lo que explica por qué se encuentran en la carpeta [src] del proyecto de Eclipse. El nombre del archivo [spring-config.xml] puede elegirse libremente.

persistence.xml


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL" />
</persistence>
  • línea 4: el archivo declara una unidad de persistencia llamada jpa que utiliza transacciones «locales», c.a.d, no proporcionadas por un contenedor EJB3. Estas transacciones son creadas y administradas por Spring y se configuran en el archivo [spring-config.xml].

spring-config.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

    <!-- capas de aplicación -->
    <bean id="dao" class="dao.Dao" />
    <bean id="service" class="service.Service">
        <property name="dao" ref="dao" />
    </bean>

    <!-- capa de persistencia JPA -->
    <bean id="entityManagerFactory"
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean
                class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform"
                    value="org.hibernate.dialect.MySQL5InnoDBDialect" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean
                class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
    </bean>

    <!-- la fuente de datos DBCP -->
    <bean id="dataSource"
        class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/jpa" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>

    <!-- el administrador de transacciones -->
    <tx:annotation-driven transaction-manager="txManager" />
    <bean id="txManager"
        class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory"
            ref="entityManagerFactory" />
    </bean>

    <!-- traducción de excepciones -->
    <bean
        class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />

    <!-- anotaciones de persistencia -->
    <bean
        class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />

</beans>
  • líneas 2-5: la etiqueta raíz <beans> del archivo de configuración. No comentaremos los diversos atributos de esta etiqueta. Hay que tener cuidado al copiar y pegar, ya que un error en cualquiera de estos atributos provoca errores que a veces son difíciles de entender.
  • línea 8: el bean «dao» es una referencia a una instancia de la clase [dao.Dao]. Se creará una única instancia (singleton) que implementará la capa [dao] de la aplicación.
  • Líneas 9-11: instanciación de la capa [service]. El bean «service» es una referencia a una instancia de la clase [service.Service]. Se creará una única instancia (singleton) que implementará la capa [service] de la aplicación. Hemos visto que la clase [service.Service] tiene un campo privado [IDao dao]. Este campo se inicializa en la línea 10 mediante el bean «dao» definido en la línea 8.
  • Al final, las líneas 8 a 11 han configurado las capas [dao] y [service]. Más adelante veremos cuándo y cómo se instanciarán.
  • Líneas 35-42: se define una fuente de datos. Ya hemos visto el concepto de fuente de datos al estudiar las entidades JPA con Hibernate:

En el ejemplo anterior, [c3p0], denominado «pool de conexiones», podría haberse llamado «fuente de datos». Una fuente de datos proporciona el servicio de «pool de conexiones». Con Spring, utilizaremos una fuente de datos distinta a [c3p0]. Se trata de [DBCP] del proyecto Apache Commons DBCP [http://jakarta.apache.org/commons/dbcp/]. Los archivos de [DBCP] se han colocado en la biblioteca de usuario [jpa-spring]:

 
  • líneas 38-41: para establecer conexiones con la base de datos de destino, la fuente de datos necesita conocer el controlador JDBC utilizado (línea 38), la URL de la base de datos (línea 39), el usuario de la conexión y su contraseña (líneas 40-41).
  • líneas 14-32: configuran la capa JPA
  • líneas 14-15: definen un bean de tipo [EntityManagerFactory] capaz de crear objetos de tipo [EntityManager] para gestionar los contextos de persistencia. La clase instanciada [LocalContainerEntityManagerFactoryBean] es proporcionada por Spring. Requiere una serie de parámetros para su instanciación, definidos en las líneas 16-31.
  • línea 16: la fuente de datos que se utilizará para obtener conexiones con SGBD. Se trata de la fuente [DBCP] definida en las líneas 35 a 42.
  • líneas 17 a 27: la implementación JPA que se utilizará
  • líneas 18-26: definen a Hibernate (línea 19) como la implementación JPA que se debe utilizar
  • líneas 23-24: el dialecto SQL que Hibernate debe utilizar con el SGBD de destino, en este caso MySQL5.
  • línea 25: solicita que, al iniciar la aplicación, se genere la base de datos (drop y create).
  • Líneas 28-31: definen un «cargador de clases». No sabría explicar con claridad la función de este bean utilizado por el EntityManagerFactory de la capa JPA. De todos modos, implica pasarle a JVM —que ejecuta la aplicación— el nombre de un archivo cuyo contenido se encargará de cargar las clases al iniciar la aplicación. En este caso, ese archivo es [spring-agent.jar], ubicado en la biblioteca de usuario [jpa-spring] (véase más arriba). Veremos que Hibernate no necesita este agente, pero que Toplink sí lo requiere.
  • líneas 45-50: definen el administrador de transacciones que se utilizará
  • línea 45: indica que las transacciones se gestionan con anotaciones de Java (también podrían haberse declarado en spring-config.xml). En concreto, se trata de la anotación @Transactional que aparece en la clase [Service] (línea 6).
  • líneas 46-50: el administrador de transacciones
  • línea 47: el administrador de transacciones es una clase proporcionada por Spring
  • líneas 48-49: el administrador de transacciones de Spring necesita conocer la clase EntityManagerFactory que administra la capa JPA. Es la que se define en las líneas 14-32.
  • líneas 57-58: definen la clase que maneja las anotaciones de persistencia de Spring que se encuentran en el código Java, como la anotación @PersistenceContext de la clase [dao.Dao] (línea 12).
  • líneas 53-54: definen la clase de Spring que gestiona, entre otras cosas, la anotación @Repository, la cual hace que una clase así anotada sea elegible para la conversión de las excepciones nativas del controlador JDBC de SGBD en excepciones genéricas de Spring del tipo [DataAccessException]. Esta conversión encapsula la excepción nativa de Jdbc en un tipo [DataAccessException] que cuenta con varias subclases:

Image

Esta conversión permite que el programa cliente maneje las excepciones de manera genérica, independientemente del SGBD de destino. No hemos utilizado la anotación @Repository en nuestro código Java. Por lo tanto, las líneas 53 y 54 son innecesarias. Las hemos dejado solo a modo informativo.

Hemos terminado con el archivo de configuración de Spring. Es complejo y muchas cosas siguen sin estar claras. Se ha extraído de la documentación de Spring. Afortunadamente, su adaptación a diversas situaciones a menudo se reduce a dos modificaciones:

  • la de la base de datos de destino: líneas 38-41. Daremos un ejemplo con Oracle.
  • la de la implementación JPA: líneas 14-32. Daremos un ejemplo con Toplink.

3.1.6. Programa cliente [InitDB]

Nos dedicamos a escribir un primer cliente de la arquitectura descrita anteriormente:

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


package tests;

...
public class InitDB {

    // capa de servicio
    private static IService service;

    // constructor
    public static void main(String[] args) throws ParseException {
        // configuración de la aplicación
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // capa de servicio
        service = (IService) ctx.getBean("service");
        // se vacía la base de datos
        clean();
        // se llena
        fill();
        // se verifica visualmente
        dumpPersonnes();
    }

    // visualización del contenido de la tabla
    private static void dumpPersonnes() {
        System.out.format("[personnes]%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }

    // llenar la tabla
    public static void fill() throws ParseException {
        // 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);
        // que se guarda
        service.saveArray(new Personne[] { p1, p2 });
    }

    // eliminación de elementos de la tabla
    public static void clean() {
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }
}
  • línea 12: se utiliza el archivo [spring-config.xml] para crear un objeto [ApplicationContext ctx], que es una imagen en memoria del archivo. Los beans definidos en [spring-config.xml] se instancian en este momento.
  • línea 14: se solicita al contexto de aplicación ctx una referencia a la capa [service]. Se sabe que esta está representada por un bean llamado «service».
  • línea 16: se vacía la base de datos mediante el método clean de las líneas 41-45:
    • líneas 42-44: se solicita la lista de todas las personas al contexto de persistencia y se recorre la lista para eliminarlas una por una. Quizás recordemos que [spring-config.xml] especifica que la base de datos debe generarse al iniciar la aplicación. Por lo tanto, en nuestro caso, la llamada al método clean es innecesaria, ya que partimos de una base vacía.
  • línea 18: el método fill llena la base de datos. Esta se define en las líneas 32-38:
    • líneas 34-35: se crean dos personas
    • línea 37: se le pide a la capa [service] que las haga persistentes.
  • línea 20: el método dumpPersonnes muestra las personas persistentes. Se define en las líneas 24 a 29
    • líneas 26-28: se solicita la lista de todas las personas persistentes a la capa [service] y se muestran en la consola.

La ejecución de [InitDB] arroja el siguiente resultado:

1
2
3
[personnes]
[72,0,p1,Paul,31/01/2000,true,2]
[73,0,p2,Sylvie,05/07/2001,false,0]

3.1.7. Pruebas unitarias [TestNG]

La instalación del complemento [TestNG] se describe en el párrafo 5.2.4. El código del programa [TestNG] es el siguiente:


package tests;

....
public class TestNG {

    // capa de servicio
    private IService service;

    @BeforeClass
    public void init() {
        // registro
        log("init");
        // configuración de la aplicación
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // capa de servicio
        service = (IService) ctx.getBean("service");
    }

    @BeforeMethod
    public void setUp() throws ParseException {
        // se vacía la base de datos
        clean();
        // se llena
        fill();
    }

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

    // visualización del contenido de la tabla
    private void dump() {
        log("dump");
        System.out.format("[personnes]%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }

    // llenado de la tabla
    public void fill() throws ParseException {
        log("fill");
        // 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);
        // que se guardan
        service.saveArray(new Personne[] { p1, p2 });
    }

    // eliminación de elementos de la tabla
    public void clean() {
        log("clean");
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }

    @Test()
    public void test01() {
...
    }
...
}
  • línea 9: la anotación @BeforeClass indica el método que se debe ejecutar para inicializar la configuración necesaria para las pruebas. Se ejecuta antes de que se ejecute la primera prueba. La anotación @AfterClass, que no se utiliza aquí, indica el método que se debe ejecutar una vez que se hayan ejecutado todas las pruebas.
  • Líneas 10-17: el método `init`, anotado con @BeforeClass, utiliza el archivo de configuración de Spring para instanciar las diferentes capas de la aplicación y obtener una referencia a la capa `[service]`. Posteriormente, todas las pruebas utilizan esta referencia.
  • línea 19: la anotación @BeforeMethod indica el método que se debe ejecutar antes de cada prueba. La anotación @AfterMethod, que no se utiliza aquí, indica el método que se debe ejecutar después de cada prueba.
  • líneas 20-25: el método setUp, anotado con @BeforeMethod, vacía la base de datos (clean, líneas 52-56) y luego la llena con dos personas (fill, líneas 42-49).
  • línea 59: la anotación @Test indica un método de prueba que se debe ejecutar. A continuación describimos estas pruebas.

@Test()
    public void test01() {
        log("test1");
        dump();
        // lista de personas
        List<Personne> personnes = service.getAll();
        assert 2 == personnes.size();
    }

    @Test()
    public void test02() {
        log("test2");
        // búsqueda de personas por su nombre
        List<Personne> personnes = service.getAllLike("p1%");
        assert 1 == personnes.size();
        Personne p1 = personnes.get(0);
        assert "Paul".equals(p1.getPrenom());
    }

    @Test()
    public void test03() throws ParseException {
        log("test3");
        // creación de una nueva persona
        Personne p3 = new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // se guarda
        service.saveOne(p3);
        // se vuelve a solicitar
        Personne loadedp3 = service.getOne(p3.getId());
        // se muestra
        System.out.println(loadedp3);
        // verificación
        assert "p3".equals(loadedp3.getNom());
    }
  • líneas 2-8: la prueba 01. Hay que recordar que, al inicio de cada prueba, la base de datos contiene dos personas llamadas p1 y p2, respectivamente.
  • línea 6: se solicita la lista de personas
  • línea 7: se verifica que el número de personas en la lista obtenida sea 2
  • línea 14: se solicita la lista de personas cuyo nombre comience por p1
  • se verifica que la lista obtenida tenga solo un elemento (línea 15) y que el nombre de la única persona obtenida sea «Paul» (línea 17)
  • línea 24: se crea una persona llamada p3
  • línea 25: se guarda en el sistema
  • línea 28: se vuelve a solicitar al contexto de persistencia para verificar
  • línea 32: se verifica que la persona obtenida tenga efectivamente el nombre p3.

@Test()
    public void test04() throws ParseException {
        log("test4");
        // se carga el usuario p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // se muestra
        System.out.println(p1);
        // se verifica
        assert "p1".equals(p1.getNom());
        int version1 = p1.getVersion();
        // se modifica el nombre
        p1.setPrenom("x");
        // se guarda
        service.updateOne(p1);
        // se vuelve a cargar
        p1 = service.getOne(p1.getId());
        // se muestra
        System.out.println(p1);
        // se verifica que la versión se haya incrementado
        assert (version1 + 1) == p1.getVersion();

    }
  • línea 5: se solicita la persona p1
  • línea 10: se verifica su nombre
  • línea 11: se anota su número de versión
  • línea 13: se modifica su nombre
  • línea 15: se guarda el cambio
  • línea 17: se vuelve a solicitar la persona p1
  • línea 21: se verifica que su número de versión haya aumentado en 1

@Test()
    public void test05() {
        log("test5");
        // se carga la persona p2
        List<Personne> personnes = service.getAllLike("p2%");
        Personne p2 = personnes.get(0);
        // se muestra
        System.out.println(p2);
        // se verifica
        assert "p2".equals(p2.getNom());
        // se elimina a la persona p2
        service.deleteOne(p2.getId());
        // se vuelve a cargar
        p2 = service.getOne(p2.getId());
        // se verifica que se haya obtenido un puntero nulo
        assert null == p2;
        // se muestra la tabla
        dump();
    }
  • línea 5: se solicita la persona p2
  • línea 10: se verifica su nombre
  • línea 12: se elimina
  • línea 14: se vuelve a buscar
  • línea 16: se verifica que no se haya encontrado

@Test()
    public void test06() throws ParseException {
        log("test6");
        // se crea una tabla con 2 personas con el mismo nombre (lo que viola la regla de unicidad del nombre)
        Personne[] personnes = { new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2),
                new Personne("p4", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2),
                new Personne("p4", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2)};
        // se guarda esta tabla; debe producirse una excepción y una reversión
        boolean erreur = false;
        try {
            service.saveArray(personnes);
        } catch (RuntimeException e) {
            erreur = true;
        }
        // volcado
        dump();
        // verificaciones
        assert erreur;
        // búsqueda de persona con nombre p3
        List<Personne> personnesp3 = service.getAllLike("p3%");
        assert 0 == personnesp3.size();
        // volcado
        dump();
    }
  • línea 5: se crea una tabla con tres personas, dos de las cuales tienen el mismo nombre «p4». Esto viola la regla de unicidad del nombre de la @Entity Personne:

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
private String nom;
  • línea 11: el array de las tres personas se coloca en el contexto de persistencia. La adición de la segunda persona, p4, debería fallar. Como el método [saveArray] se ejecuta en una transacción, todas las inserciones que se hayan podido realizar anteriormente se revertirán. Al final, no se realizará ninguna adición.
  • línea 18: se verifica que [saveArray] haya lanzado una excepción
  • líneas 20-21: se verifica que la persona p3, que podría haberse agregado, no lo haya sido.

@Test()
    public void test07() {
        log("test7");
        // prueba de bloqueo optimista
        // se carga la persona p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // se muestra
        System.out.println(p1);
        // se aumenta su número de hijos
        int nbEnfants1 = p1.getNbenfants();
        p1.setNbenfants(nbEnfants1 + 1);
        // se guarda p1
        Personne newp1 = service.updateOne(p1);
        assert (nbEnfants1 + 1) == newp1.getNbenfants();
        System.out.println(newp1);
        // se guarda por segunda vez; debe producirse una excepción porque p1 ya no tiene la versión correcta
        // es newp1 quien lo tiene
        boolean erreur = false;
        try {
            service.updateOne(p1);
        } catch (RuntimeException e) {
            erreur = true;
        }
        // verificación
        assert erreur;
        // se aumenta el número de hijos de newp1
        int nbEnfants2 = newp1.getNbenfants();
        newp1.setNbenfants(nbEnfants2 + 1);
        // guardamos newp1
        service.updateOne(newp1);
        // se vuelve a cargar
        p1 = service.getOne(p1.getId());
        // se verifica
        assert (nbEnfants1 + 2) == p1.getNbenfants();
        System.out.println(p1);
    }
  • línea 6: se solicita la persona p1
  • línea 12: se incrementa en 1 su número de hijos
  • línea 14: se actualiza la persona p1 en el contexto de persistencia. El método [updateOne] hace que la nueva versión newp1 de p1 sea persistente. Se diferencia de p1 por su número de versión, que debe haberse incrementado.
  • línea 15: se verifica el número de hijos de newp1.
  • línea 21: se solicita nuevamente una actualización de la persona p1 a partir de la versión anterior p1. Debe producirse una excepción, ya que p1 no es la última versión de la persona p1. Esta última versión es newp1.
  • línea 23: se verifica que el error se haya producido efectivamente
  • líneas 27-35: se verifica que, si se realiza una actualización a partir de la última versión newp1, todo funciona correctamente.

@Test()
    public void test08() {
        log("test8");
        // prueba de reversión en updateArray
        // se carga la persona p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // se muestra
        System.out.println(p1);
        // se aumenta su número de hijos
        int nbEnfants1 = p1.getNbenfants();
        p1.setNbenfants(nbEnfants1 + 1);
        // se guardan 2 modificaciones, de las cuales la segunda debe fallar (persona mal inicializada)
        // debido a la transacción, ambas deben ser canceladas
        boolean erreur = false;
        try {
            service.updateArray(new Personne[] { p1, new Personne() });
        } catch (RuntimeException e) {
            erreur = true;
        }
        // verificaciones
        assert erreur;
        // se vuelve a cargar la persona p1
        personnes = service.getAllLike("p1%");
        p1 = personnes.get(0);
        // su número de hijos no debería haber cambiado
        assert nbEnfants1 == p1.getNbenfants();
    }
  • La prueba 8 es similar a la prueba 6: verifica la función rollback sobre una función updateArray que opera sobre una tabla de dos personas, en la que la segunda no se ha inicializado correctamente. Desde la perspectiva de JPA, la operación de fusión sobre la segunda persona, que noexiste ya generará una orden SQL insert que fallará debido a las restricciones nullable=false que existen en algunos de los campos de la entidad Personne.

@Test()
    public void test09() {
        log("test9");
        // prueba de reversión en deleteArray
        // volcado
        dump();
        // se carga la persona p1
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // se muestra
        System.out.println(p1);
        // se realizan 2 eliminaciones, de las cuales la segunda debe fallar (persona desconocida)
        // debido a la transacción, ambas deben cancelarse
        boolean erreur = false;
        try {
            service.deleteArray(new Personne[] { p1, new Personne() });
        } catch (RuntimeException e) {
            erreur = true;
        }
        // verificaciones
        assert erreur;
        // se recarga a la persona p1
        personnes = service.getAllLike("p1%");
        // verificación
        assert 1 == personnes.size();
        // volcado
        dump();
    }
  • La prueba 9 es similar a la anterior: verifica el rollback en un deleteArray que opera sobre una tabla de dos personas, en la que la segunda no existe. Sin embargo, en este caso, el método [deleteOne] de la capa [dao] genera una excepción.

// bloqueo optimista - acceso multihilo
    @Test()
    public void test10() throws Exception {
        // se agrega una persona
        Personne p3 = new Personne("X", "X", new SimpleDateFormat("dd/MM/yyyy").parse("01/02/2006"), true, 0);
        service.saveOne(p3);
        int id3 = p3.getId();
        // creación de N subprocesos para actualizar el número de hijos
        final int N = 20;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadMajEnfants("thread n° " + i, service, id3);
            taches[i].start();
        }
        // se espera a que terminen los hilos
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
        // se recupera la persona
        p3 = service.getOne(id3);
        // debe tener N hijos
        assert N == p3.getNbenfants();
        // eliminación de la persona p3
        service.deleteOne(p3.getId());
        // verificación
        p3 = service.getOne(p3.getId());
        // debe haber un puntero nulo
        assert p3 == null;
    }
  • La idea de la prueba 10 es iniciar N subprocesos (línea 9) para incrementar en paralelo el número de hijos de una persona. Se quiere verificar que el sistema de números de versión resista bien este caso. Fue creado precisamente para eso.
  • Líneas 5-6: se crea una persona llamada p3 y luego se guarda. Al inicio tiene 0 hijos.
  • línea 7: se anota su identificador.
  • líneas 9-14: se inician N subprocesos en paralelo, todos encargados de incrementar en 1 el número de hijos de p3.
  • líneas 16-18: se espera a que terminen todos los hilos
  • línea 20: se solicita ver a la persona p3
  • línea 22: se verifica que ahora tenga N hijos
  • línea 24: se elimina a la persona p3.

El hilo [ThreadMajEnfants] es el siguiente:


package tests;

...
public class ThreadMajEnfants extends Thread {
    // nombre del hilo
    private String name;

    // referencia en la capa [service]
    private IService service;

    // el ID de la persona en la que vamos a trabajar
    private int idPersonne;

    // constructor
    public ThreadMajEnfants(String name, IService service, int idPersonne) {
        this.name = name;
        this.service = service;
        this.idPersonne = idPersonne;
    }

    // núcleo del hilo
    public void run() {
        // seguimiento
        suivi("lancé");
        // se repite el ciclo hasta que se logre incrementar en 1
        // el número de hijos de la persona idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
            // se recupera una copia de la persona de idPersonne
            Personne personne = service.getOne(idPersonne);
            nbEnfants = personne.getNbenfants();
            // seguimiento
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
            // incrementa en 1 el número de hijos de la persona
            personne.setNbenfants(nbEnfants + 1);
            // espera de 10 ms para liberar el procesador
            try {
                // seguimiento
                suivi("début attente");
                // se interrumpe para liberar el procesador
                Thread.sleep(10);
                // seguimiento
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
            // espera finalizada: se intenta validar la copia
            // Mientras tanto, otros hilos pudieron haber modificado el original
            try {
                // se intenta modificar el original
                service.updateOne(personne);
                // Ya pasó: el original ha sido modificado
                fini = true;
            } catch (javax.persistence.OptimisticLockException e) {
                // versión incorrecta del objeto: se ignora la excepción para volver a intentarlo
            } catch (org.springframework.transaction.UnexpectedRollbackException e2) {
                // excepción de Spring que surge de vez en cuando
            } catch (RuntimeException e3) {
                // Otro tipo de excepción: se reporta
                throw e3;
            }
        }
        // seguimiento
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

    // seguimiento
    private void suivi(String message) {
        System.out.println(name + " [" + new Date().getTime() + "] : " + message);
    }
}
  • líneas 15-19: el constructor almacena la información que necesita para funcionar: su nombre (línea 16), la referencia en la capa [service] que debe utilizar (línea 17) y el identificador de la persona p cuyo número de hijos debe incrementar (línea 18).
  • líneas 22-66: el método [run] ejecutado por todos los hilos en paralelo.
  • línea 29: el hilo intenta repetidamente incrementar el número de hijos de la persona p. Solo se detiene cuando lo logra.
  • línea 31: se consulta a la persona p
  • línea 36: se incrementa en memoria su número de hijos
  • líneas 38-47: se hace una pausa de 10 ms. Esto permitirá que otros hilos obtengan la misma versión de la persona p. Por lo tanto, en ese mismo momento habrá varios hilos que tengan la misma versión de la persona p y quieran modificarla. Eso es lo que se busca.
  • línea 52: una vez finalizada la pausa, el hilo solicita a la capa [service] que guarde el cambio. Sabemos que de vez en cuando habrá excepciones, por lo que hemos rodeado la operación con un try / catch.
  • línea 55: las pruebas muestran que se producen excepciones de tipo [javax.persistence.OptimisticLockException]. Esto es normal: es la excepción lanzada por la capa JPA cuando un hilo intenta modificar la persona p sin tener su última versión. Esta excepción se ignora para permitir que el hilo vuelva a intentar la operación hasta que lo logre.
  • línea 57: las pruebas muestran que también se producen excepciones del tipo [org.springframework.transaction.UnexpectedRollbackException]. Esto es molesto e inesperado. No tengo ninguna explicación que dar. Ahora dependemos de Spring, cuando hubiéramos querido evitarlo. Esto significa que si ejecutamos nuestra aplicación en JBoss Ejb3, por ejemplo, habrá que modificar el código del hilo. La excepción de Spring también se ignora aquí para permitir que el hilo vuelva a intentar la operación de incremento.
  • línea 59: los demás tipos de excepción se transmiten a la aplicación.

Al ejecutar [TestNG] se obtienen los siguientes resultados:

Image

Las 10 pruebas se han superado con éxito.

La prueba 10 merece una explicación adicional porque el hecho de que haya tenido éxito tiene algo de mágico. Volvamos primero a la configuración de la capa [dao]:


public class Dao implements IDao {

    @PersistenceContext
    private EntityManager em;

  • línea 4: se inyecta un objeto [EntityManager] en el campo em mediante la anotación JPA @PersistenceContext. La capa [dao] se instancia una sola vez. Se trata de un singleton utilizado por todos los hilos que emplean la capa JPA. Por lo tanto, el objeto EntityManager «em» es común a todos los hilos. Esto se puede verificar mostrando el valor de em en el método [updateOne] utilizado por los hilos [ThreadMajEnfants]: se observa el mismo valor para todos los hilos.

Por lo tanto, cabe preguntarse si los objetos persistentes de los distintos hilos, manipulados por el em de EntityManager —que es el mismo para todos los hilos—, no se mezclarán y crearán conflictos entre sí. Un ejemplo de lo que podría suceder se encuentra en [ThreadMajEnfants]:


        while (!fini) {
            // se recupera una copia de la persona de idPersonne
            Personne personne = service.getOne(idPersonne);
            nbEnfants = personne.getNbenfants();
            // seguimiento
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
            // incrementa en 1 el número de hijos de la persona
            personne.setNbenfants(nbEnfants + 1);
            // espera de 10 ms para liberar el procesador
            try {
                // seguimiento
                suivi("début attente");
                // se interrumpe para liberar el procesador
                Thread.sleep(10);
                // seguimiento
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
}
  • línea 3: un hilo T1 recupera a la persona p
  • línea 8: incrementa el número de hijos de p
  • línea 14: el hilo T1 hace una pausa

Un hilo T2 toma el control y también ejecuta la línea 3: solicita la misma persona p que T1. Si el contexto de persistencia de los hilos fuera el mismo, la persona p, que ya se encuentra en el contexto gracias a T1, debería devolverse a T2. De hecho, el método [getOne] utiliza el método [EntityManager].Este método busca en API y JPA, y solo accede a la base de datos si el objeto solicitado no forma parte del contexto de persistencia; de lo contrario, devuelve el objeto del contexto de persistencia. Si ese fuera el caso, T1 y T2 tendrían la misma persona p. T2 incrementaría entonces el número de hijos de p en 1 nuevamente (línea 8). Si uno de los hilos logra actualizar el dato después de la pausa, entonces el número de hijos de p se habrá incrementado en 2 y no en 1 como se esperaba. Se podría esperar entonces que los N hilos aumentaran el número de hijos no a N, sino a un número mayor. Sin embargo, este no es el caso. Por lo tanto, podemos concluir que T1 y T2 no comparten la misma referencia a p. Lo verificamos haciendo que los hilos muestren la dirección de p: es diferente para cada uno de ellos.

Por lo tanto, parecería que los hilos:

  • compartan el mismo administrador de contexto de persistencia (EntityManager)
  • pero cada uno tiene su propio contexto de persistencia.

Estas son solo suposiciones y sería útil contar con la opinión de un experto en este caso.

3.1.8. Cambiar de SGBD

Para cambiar de SGBD, solo hay que reemplazar el archivo [src/spring-config.xml] [2] por el archivo [spring-config.xml] del SGBD correspondiente de la carpeta [conf] [1].

El archivo [spring-config.xml] de Oracle es, por ejemplo, el siguiente:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

...
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform" value="org.hibernate.dialect.OracleDialect" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
    </bean>

    <!-- la fuente de datos DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.OracleDriver" />
        <property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>
...
</beans>

Solo cambian algunas líneas con respecto al mismo archivo utilizado anteriormente para MySQL5:

  • línea 14: el dialecto SQL que Hibernate debe utilizar
  • líneas 25-28: las características de la conexión JDBC con el SGBD

Se invita al lector a repetir las pruebas descritas para MySQL5 con otros SGBD.

3.1.9. Cambiar la implementación de JPA

Volvamos a la arquitectura de las pruebas anteriores:

Reemplazamos la implementación JPA / Hibernate por una implementación JPA / Toplink. Dado que Toplink no utiliza las mismas bibliotecas que Hibernate, utilizamos un nuevo proyecto de Eclipse:

  • en [1]: el proyecto de Eclipse. Es idéntico al anterior. Lo único que cambia es el archivo de configuración [spring-config.xml] [2] y la biblioteca [jpa-toplink], que reemplaza a la biblioteca [jpa-hibernate].
  • En [3]: la carpeta de ejemplos de este tutorial. En [4], el proyecto de Eclipse que se debe importar.

El archivo de configuración [spring-config.xml] para Toplink queda así:


<?xml version="1.0" encoding="UTF-8"?>

<!-- el JVM debe ejecutarse con el argumento -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar 
    (à remplacer par le chemin exact de spring-agent.jar)-->

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

    <!-- capas de aplicación -->
    <bean id="dao" class="dao.Dao" />
    <bean id="service" class="service.Service">
        <property name="dao" ref="dao" />
    </bean>

    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform" value="oracle.toplink.essentials.platform.database.MySQL4Platform" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
    </bean>

    <!-- la fuente de datos DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/jpa" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>

    <!-- el administrador de transacciones -->
    <tx:annotation-driven transaction-manager="txManager" />
    <bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
    </bean>

    <!-- traducción de excepciones -->
    <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />

    <!-- persistencia -->
    <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />

</beans>

Solo hay que modificar unas pocas líneas para pasar de Hibernate a Toplink:

  • línea 19: la implementación JPA ahora la realiza Toplink
  • línea 23: la propiedad [databasePlatform] tiene un valor diferente al de Hibernate: el nombre de una clase propia de Toplink. En el párrafo 2.1.15.2 se explica dónde encontrar este nombre.

Eso es todo. Cabe destacar la facilidad con la que se puede cambiar de SGBD o de la implementación JPA con Spring.

Sin embargo, aún no hemos terminado del todo. Al ejecutar [InitDB], por ejemplo, aparece una excepción que no es fácil de entender:


Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [spring-config.xml]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Must start with Java agent to use InstrumentationLoadTimeWeaver. See Spring documentation.
Caused by: java.lang.IllegalStateException: Must start with Java agent to use 

El mensaje de error de la línea 1 nos lleva a consultar la documentación de Spring. Allí descubrimos un poco más sobre el papel que desempeña una declaración poco clara del archivo [spring-config.xml]:


    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform" value="org.hibernate.dialect.OracleDialect" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
</bean>

La línea 1 de la excepción hace referencia a una clase llamada [InstrumentationLoadTimeWeaver], clase que se encuentra en la línea 13 del archivo de configuración de Spring. La documentación de Spring explica que esta clase es necesaria en ciertos casos para cargar las clases de la aplicación y que, para que funcione, la clase JVM debe ejecutarse con un agente. Este agente lo proporciona Spring y se llama [spring-agent]:

  • El archivo [spring-agent.jar] se encuentra en la carpeta <ejemplos>/lib [1]. Se incluye con la distribución de Spring 2.x (véase el párrafo 5.11).
  • En [3], se crea una configuración de ejecución [Run/Run...]
  • en [4], se crea una configuración de ejecución Java (hay varios tipos de configuraciones de ejecución)
  • En [5], se selecciona la pestaña [Main]
  • en [6], se le da un nombre a la configuración
  • en [7], se nombra el proyecto de Eclipse al que corresponde esta configuración (utilizar el botón «Browse»)
  • en [8], se nombra la clase Java que contiene el método [main] (usa el botón «Browse»)
  • en [9], se pasa a la pestaña [Arguments]. En esta se pueden especificar dos tipos de argumentos:
    • en [9], los que se pasan al método [main]
    • en [10], los que se pasan a JVM, que ejecutará el código. El agente Spring se define mediante el parámetro -javaagent:valor de JVM. El valor es la ruta del archivo [spring-agent.jar].
  • en [11]: se valida la configuración
  • en [12]: se crea la configuración
  • en [13]: se ejecuta

Una vez hecho esto, se ejecuta [InitDB] y arroja los mismos resultados que con Hibernate. Para [TestNG], hay que proceder de la misma manera:

  • en [1], se crea una configuración de ejecución [Run/Run...]
  • en [2], se crea una configuración de ejecución TestNG
  • en [3], se selecciona la pestaña [Test]
  • en [4], se le da un nombre a la configuración
  • en [5], se nombra el proyecto de Eclipse al que corresponde esta configuración (utilizar el botón «Browse»)
  • en [6], se nombra la clase de pruebas (utilice el botón «Browse»)
  • en [7], se pasa a la pestaña [Arguments].
  • En [8]: se establece el argumento -javaagent de JVM.
  • en [9]: se valida la configuración
  • en [10]: se crea la configuración
  • en [11]: se ejecuta

Una vez hecho esto, se ejecuta [TestNG] y arroja los mismos resultados que con Hibernate.

3.2. Ejemplo 2: JBoss EJB3 / JPA con la entidad Persona

Retomamos el mismo ejemplo que antes, pero lo ejecutamos en un contenedor EJB3, el de JBoss:

Normalmente, un contenedor Ejb3 está integrado en un servidor de aplicaciones. JBoss proporciona un contenedor Ejb3 «autónomo» que se puede utilizar fuera de un servidor de aplicaciones. Veremos que ofrece servicios similares a los que ofrece Spring. Intentaremos determinar cuál de estos contenedores resulta más práctico.

La instalación del contenedor JBoss EJB3 se describe en el párrafo 5.12.

3.2.1. El proyecto Eclipse / JBoss EJB3 / Hibernate

El proyecto de Eclipse es el siguiente:

  • en [1]: el proyecto Eclipse. Se encuentra en [6], dentro de los ejemplos del tutorial [5]. Lo importaremos.
  • en [2]: los códigos Java de las capas presentados en paquetes:
    • [entites]: el paquete de entidades JPA
    • [dao]: la capa de acceso a datos —se basa en la capa JPA
    • [service]: una capa de servicios más que de negocio. En ella se utilizará el servicio de transacciones del contenedor EJB3.
    • [tests]: agrupa los programas de prueba.
  • en [3]: la biblioteca [jpa-jbossejb3] reúne los archivos JAR necesarios para JBoss EJB3 (véanse también [7] y [8]).
  • en [4]: la carpeta [conf] reúne los archivos de configuración para cada uno de los SGBD utilizados en este tutorial. Hay dos en cada caso: [persistence.xml], que configura la capa JPA, y [jboss-config.xml], que configura el contenedor Ejb3.

3.2.2. Las entidades JPA

Aquí solo se gestiona una entidad: la entidad Personne, analizada anteriormente en el párrafo 3.1.2.

3.2.3. La capa [dao]

La capa [dao] presenta la interfaz [IDao] descrita anteriormente en el párrafo 3.1.3.

La implementación [Dao] de esta interfaz es la siguiente:


package dao;

...
@Stateless
public class Dao implements IDao {

    @PersistenceContext
    private EntityManager em;

    // eliminar a una persona mediante su identificador
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void deleteOne(Integer id) {
        Personne personne = em.find(Personne.class, id);
        if (personne == null) {
            throw new DaoException(2);
        }
        em.remove(personne);
    }

    // obtener todas las personas
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public List<Personne> getAll() {
        return em.createQuery("select p from Personne p").getResultList();
    }

    // obtener las personas cuyo nombre coincida con un patrón
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public List<Personne> getAllLike(String modele) {
        return em.createQuery("select p from Personne p where p.nom like :modele")
                .setParameter("modele", modele).getResultList();
    }

    // obtener una persona mediante su identificador
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne getOne(Integer id) {
        return em.find(Personne.class, id);
    }

    // guardar un usuario
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne saveOne(Personne personne) {
        em.persist(personne);
        return personne;
    }

    // actualizar una persona
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne updateOne(Personne personne) {
        return em.merge(personne);
    }

}
  • Este código es idéntico en todos los aspectos al que teníamos con Spring. Solo cambian las anotaciones de Java, y eso es lo que comentamos.
  • Línea 4: la anotación @Stateless convierte a la clase [Dao] en un EJB sin estado. La anotación @Stateful convierte a una clase en un EJB con estado. Un EJB con estado tiene campos privados cuyos valores deben conservarse a lo largo del tiempo. Un ejemplo clásico es el de una clase que contiene información relacionada con el usuario web de una aplicación. Una instancia de esta clase está vinculada a un usuario específico y, cuando finaliza el hilo de ejecución de una solicitud de ese usuario, la instancia debe conservarse para que esté disponible en la próxima solicitud del mismo cliente. Un EJB @Stateless no tiene estado. Si retomamos el mismo ejemplo, al finalizar el hilo de ejecución de una solicitud de un usuario, el EJB @Stateless se incorporará a un grupo de EJB @Stateless y quedará disponible para el hilo de ejecución de una solicitud de otro usuario.
  • Para el desarrollador, el concepto de EJB3 @Stateless es similar al del singleton de Spring. Lo utilizará en los mismos casos.
  • Línea 7: la anotación @PersistenceContext es la misma que la que se encuentra en la versión Spring de la capa [dao]. Indica el campo que recibirá el EntityManager, lo cual permitirá a la capa [dao] manejar el contexto de persistencia.
  • línea 11: la anotación @TransactionAttribute aplicada a un método sirve para configurar la transacción en la que se ejecutará el método. Estos son algunos valores posibles de esta anotación:
    • TransactionAttributeType.REQUIRED: el método debe ejecutarse dentro de una transacción. Si ya se ha iniciado una transacción, las operaciones de persistencia del método se realizan dentro de ella. De lo contrario, se crea y se inicia una transacción.
    • TransactionAttributeType.REQUIRES_NEW: el método debe ejecutarse en una transacción nueva. Esta se crea y se inicia.
    • TransactionAttributeType.MANDATORY: el método debe ejecutarse en una transacción existente. Si esta no existe, se lanza una excepción.
    • TransactionAttributeType.NEVER: el método nunca se ejecuta en una transacción.
    • ...

La anotación podría haberse colocado en la propia clase:


@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Dao implements IDao {

De esta manera, el atributo se aplica a todos los métodos de la clase.

3.2.4. La capa [metier / service]

La capa [service] presenta la interfaz [IService] analizada anteriormente en el párrafo 3.1.4. La implementación [Service] de la interfaz [IService] es idéntica a la implementación analizada anteriormente en el párrafo 3.1.4, salvo por tres detalles:



@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Service implements IService {

    // capa [dao]
    @EJB
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

  • línea 2: la clase [Service] es un EJB sin estado
  • línea 3: todos los métodos de la clase [Service] deben ejecutarse dentro de una transacción
  • líneas 7-8: el contenedor EJB inyectará una referencia al EJB de la capa [dao] en el campo [IDao dao] de la línea 8. Es la anotación @EJB de la línea 7 la que solicita esta inyección. El objeto inyectado debe ser un EJB. Esta es una diferencia importante con respecto a Spring, donde se puede inyectar cualquier tipo de objeto en otro objeto.

3.2.5. Configuración de las capas

La configuración de las capas [service], [dao] y [JPA] se realiza mediante los siguientes archivos:

  • [META-INF/persistence.xml] configura la capa JPA
  • [jboss-config.xml] configura el contenedor Ejb3. Este, a su vez, utiliza los archivos [default.persistence.properties, ejb3-interceptors-aop.xml, embedded-jboss-beans.xml, jndi.properties]. Estos últimos archivos se incluyen con JBoss Ejb3 y proporcionan una configuración predeterminada que normalmente no se modifica. Al desarrollador solo le interesa el archivo [jboss-config.xml]

Analicemos los dos archivos de configuración:

persistence.xml


<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
    http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

    <persistence-unit name="jpa">

        <!-- el proveedor JPA es Hibernate -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>

        <!-- la DataSource JTA es administrada por el entorno Java EE5 -->
        <jta-data-source>java:/datasource</jta-data-source>

        <properties>
            <!-- búsqueda de entidades de la capa JBA -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />

            <!-- registros de Hibernate SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->

            <!-- el tipo de SGBD administrado -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect" />

            <!-- recreación de todas las tablas (drop+create) al implementar la unidad de persistencia -->
            <property name="hibernate.hbm2ddl.auto" value="create" />

        </properties>
    </persistence-unit>

</persistence>

Este archivo se parece a los que ya hemos visto al analizar las entidades JPA. Configura una capa JPA de Hibernate. Las novedades son las siguientes:

  • línea 5: la unidad de persistencia jpa no tiene el atributo transaction-type que siempre habíamos tenido hasta ahora:

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

Al no haber un valor, el atributo transaction-type toma el valor por defecto «JTA» (para Java Transaction API), lo que indica que el administrador de transacciones es proporcionado por un contenedor EJB3. Un administrador «JTA» puede hacer más que un administrador «RESOURCE_LOCAL»: puede gestionar transacciones que abarquen varias conexiones. Con JTA, se puede abrir una transacción t1 en una conexión c1 sobre un SGBD 1, una transacción t2 en una conexión c2 con una SGBD 2, y poder considerar (t1,t2) como una única transacción en la que o bien todas las operaciones se completan con éxito (commit) o bien ninguna (rollback).

Aquí trabajamos con el administrador JTA del contenedor JBoss EJB3.

  • Línea 11: declara la fuente de datos que debe utilizar el administrador JTA. Esta se indica en forma de un nombre JNDI (Java Naming and Directory Interface). Esta fuente de datos se define en [jboss-config.xml].

jboss-config.xml


<?xml version="1.0" encoding="UTF-8"?>

<deployment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:jboss:bean-deployer bean-deployer_1_0.xsd"
    xmlns="urn:jboss:bean-deployer:2.0">

    <!-- fábrica de DataSource -->
    <bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
        <!-- nombre JNDI de la DataSource -->
        <property name="jndiName">java:/datasource</property>

        <!-- base de datos administrada -->
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="connectionURL">jdbc:mysql://localhost:3306/jpa</property>
        <property name="userName">jpa</property>
        <property name="password">jpa</property>

        <!-- propiedades del grupo de conexiones -->
        <property name="minSize">0</property>
        <property name="maxSize">10</property>
        <property name="blockingTimeout">1000</property>
        <property name="idleTimeout">100000</property>

        <!-- administrador de transacciones, en este caso JTA -->
        <property name="transactionManager">
            <inject bean="TransactionManager" />
        </property>
        <!-- administrador de caché de Hibernate -->
        <property name="cachedConnectionManager">
            <inject bean="CachedConnectionManager" />
        </property>
        <!-- ¿Propiedades de instanciación de JNDI? -->
        <property name="initialContextProperties">
            <inject bean="InitialContextProperties" />
        </property>
    </bean>

    <!-- se solicita DataSource a una fábrica -->
    <bean name="datasource" class="java.lang.Object">
        <constructor factoryMethod="getDatasource">
            <factory bean="datasourceFactory" />
        </constructor>
    </bean>

</deployment>
  • línea 3: la etiqueta raíz del archivo es <deployment>. Este archivo de implementación tiene como objetivo principal configurar la fuente de datos java:/datasource que se declaró en persistence.xml.
  • La fuente de datos se define mediante el bean «datasource» de la línea 38. Se observa que la fuente de datos se obtiene (línea 40) de una «fábrica» definida por el bean «datasourceFactory» de la línea 7. Para obtener la fuente de datos de la aplicación, el cliente deberá llamar al método [getDatasource] de factory (línea 39).
  • línea 7: la clase factory, que proporciona la fuente de datos, es una clase de JBoss.
  • línea 9: el nombre JNDI de la fuente de datos. Debe ser el mismo nombre que el declarado en la etiqueta <jta-data-source> del archivo persistence.xml. De hecho, la capa JPA utilizará este nombre, JNDI, para solicitar la fuente de datos.
  • líneas 12-15: algo más convencional: las características JDBC de la conexión a SGBD
  • líneas 18-21: configuración del grupo de conexiones interno del contenedor JBoss EJB3.
  • líneas 24-26: el administrador JTA. La clase [TransactionManager] inyectada en la línea 25 se define en el archivo [embedded-jboss-beans.xml].
  • líneas 28-30: la caché de Hibernate, un concepto que no hemos abordado. La clase [CachedConnectionManager], inyectada en la línea 29, se define en el archivo [embedded-jboss-beans.xml]. Cabe señalar que la configuración ahora depende de Hibernate, lo que nos planteará un problema cuando queramos migrar a Toplink.
  • Líneas 32-34: configuración del servicio JNDI.

Hemos terminado con el archivo de configuración de JBoss EJB3. Es complejo y muchas cosas siguen sin estar claras. Se ha extraído de [ref1]. Sin embargo, podremos adaptarlo a otro SGBD (líneas 12-15 de jboss-config.xml, línea 24 de persistence.xml). La migración a Toplink no fue posible por falta de ejemplos.

3.2.6. Programa cliente [InitDB]

Nos dispusimos a escribir un primer cliente de la arquitectura descrita anteriormente:

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


package tests;

...
public class InitDB {

    // capa de servicio
    private static IService service;

    // constructor
    public static void main(String[] args) throws ParseException, NamingException {
        // se inicia el contenedor EJB3 JBoss
        // se utilizan los archivos de configuración ejb3-interceptors-aop.xml y embedded-jboss-beans.xml
        EJB3StandaloneBootstrap.boot(null);

        // Creación de los beans específicos de la aplicación
        EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");

        // Implementar todos los EJBs encontrados en la ruta de clases (lento, revisa todos)
        // EJB3StandaloneBootstrap.scanClasspath();

        // Se implementan todos los EJB encontrados en la ruta de clases de la aplicación
        EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));

        // Se inicializa el contexto JNDI. Se procesa el archivo jndi.properties
        InitialContext initialContext = new InitialContext();

        // instanciación de la capa de servicio
        service = (IService) initialContext.lookup("Service/local");
        // Se vacía la base de datos
        clean();
        // se llena
        fill();
        // se verifica visualmente
        dumpPersonnes();
        // se detiene el contenedor EJB
        EJB3StandaloneBootstrap.shutdown();

    }

    // visualización del contenido de la tabla
    private static void dumpPersonnes() {
        System.out.format("[personnes]-------------------------------------------------------------------%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }

    // llenar la tabla
    public static void fill() throws ParseException {
        // 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);
        // que se guardan
        service.saveArray(new Personne[] { p1, p2 });
    }

    // eliminación de elementos de la tabla
    public static void clean() {
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }
}
  • La forma de iniciar el contenedor JBoss EJB3 se encontró en [ref1].
  • Línea 13: se inicia el contenedor. [EJB3StandaloneBootstrap] es una clase del contenedor.
  • Línea 16: la unidad de implementación configurada por [jboss-config.xml] se implementa en el contenedor: se configuran el administrador JTA, la fuente de datos, el grupo de conexiones, la caché de Hibernate y el servicio JNDI.
  • línea 22: se le solicita al contenedor que escanee la carpeta bin del proyecto Eclipse para encontrar los EJB. Los EJB de las capas [service] y [dao] serán encontrados y administrados por el contenedor.
  • línea 25: se inicializa un contexto JNDI. Nos servirá para localizar los EJB.
  • línea 28: se solicita al servicio JNDI el EJB correspondiente a la clase [Service] de la capa [service]. Se puede acceder a un EJB de manera local o a través de la red. Aquí, el nombre «Service/local» del EJB buscado hace referencia a la clase [Service] de la capa [service] para un acceso local.
  • Ahora, la aplicación está implementada y tenemos una referencia a la capa [service]. Nos encontramos en la misma situación que después de la línea 11 a continuación del código [InitDB] de la versión Spring. Por lo tanto, encontramos el mismo código en ambas versiones.

public class InitDB {

    // capa de servicio
    private static IService service;

    // constructor
    public static void main(String[] args) throws ParseException {
        // configuración de la aplicación
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // capa de servicio
        service = (IService) ctx.getBean("service");
        // se vacía la base de datos
        clean();
        // se llena
        fill();
        // se verifica visualmente
        dumpPersonnes();
    }
...
  • línea 36 (JBoss EJB3): se detiene el contenedor EJB3.

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

16:07:00,781  INFO LocalTxDataSource:117 - Bound datasource to JNDI name 'java:/datasource'
...
16:07:01,171  INFO Version:94 - Hibernate EntityManager 3.2.0.CR1
...
16:07:01,296  INFO Ejb3Configuration:94 - Processing PersistenceUnitInfo [
    name: jpa
    ...]
16:07:01,312  INFO Ejb3Configuration:94 - found EJB3 Entity bean: entites.Personne
...
16:07:01,375  INFO Configuration:94 - Reading mappings from resource: META-INF/orm.xml
16:07:01,375  INFO Ejb3Configuration:94 - [PersistenceUnit: jpa] no META-INF/orm.xml found
16:07:01,421  INFO AnnotationBinder:94 - Binding entity from annotated class: entites.Personne
16:07:01,468  INFO EntityBinder:94 - Bind entity entites.Personne on table jpa01_hb_personne
...
16:07:01,859  INFO SettingsFactory:94 - RDBMS: MySQL, version: 5.0.41-community-nt
16:07:01,859  INFO SettingsFactory:94 - JDBC driver: MySQL-AB JDBC Driver, version: mysql-connector-java-5.0.5 ( $Date: 2007-03-01 00:01:06 +0100 (Thu, 01 Mar 2007) $, $Revision: 6329 $ )
16:07:01,890  INFO Dialect:94 - Using dialect: org.hibernate.dialect.MySQLInnoDBDialect
16:07:01,890  INFO TransactionFactoryFactory:94 - Transaction strategy: org.hibernate.ejb.transaction.JoinableCMTTransactionFactory
...
16:07:02,234  INFO SchemaExport:94 - Running hbm2ddl schema export
16:07:02,234  INFO SchemaExport:94 - exporting generated schema to database
16:07:02,343  INFO SchemaExport:94 - schema export complete
...
16:07:02,562  INFO EJBContainer:479 - STARTED EJB: dao.Dao ejbName: Dao
...
16:07:02,593  INFO EJBContainer:479 - STARTED EJB: service.Service ejbName: Service
...
[personnes]-------------------------------------------------------------------
[1,0,p1,Paul,31/01/2000,true,2]
[2,0,p2,Sylvie,05/07/2001,false,0]

Se invita al lector a revisar estos registros. En ellos se encuentra información interesante sobre el funcionamiento del contenedor EJB3.

3.2.7. Pruebas unitarias [TestNG]

El código del programa [TestNG] es el siguiente:


package tests;

...
public class TestNG {

    // capa de servicio
    private IService service = null;

    @BeforeClass
    public void init() throws NamingException, ParseException {
        // registro
        log("init");
        // se inicia el contenedor EJB3 JBoss
        // se utilizan los archivos de configuración ejb3-interceptors-aop.xml y embedded-jboss-beans.xml
        EJB3StandaloneBootstrap.boot(null);

        // Creación de los beans específicos de la aplicación
        EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");

        // Implementar todos los EJBs encontrados en la ruta de clases (lento, revisa todos)
        // EJB3StandaloneBootstrap.scanClasspath();

        // Se implementan todos los EJB encontrados en la ruta de clases de la aplicación
        EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));

        // Se inicializa el contexto JNDI. Se procesa el archivo jndi.properties
        InitialContext initialContext = new InitialContext();

        // instanciación de la capa de servicio
        service = (IService) initialContext.lookup("Service/local");
        // Se vacía la base de datos
        clean();
        // se llena
        fill();
        // se verifica visualmente
        dumpPersonnes();
    }

    @AfterClass
    public void terminate() {
        // registro
        log("terminate");
        // Apagado del contenedor EJB
        EJB3StandaloneBootstrap.shutdown();
    }

    @BeforeMethod
    public void setUp() throws ParseException {
...
    }

...
}
  • El método init (líneas 10-37), que sirve para configurar el entorno necesario para las pruebas, retoma el código explicado anteriormente en [InitDB].
  • El método `terminate` (líneas 40-45), que se ejecuta al final de las pruebas (presencia de la anotación @AfterClass), detiene el contenedor EJB3 (línea 44).
  • Todo lo demás es idéntico a lo que era en la versión de Spring.

Las pruebas se ejecutan correctamente:

Image

3.2.8. Cambiar a SGBD

Para cambiar a SGBD, solo hay que reemplazar el contenido de la carpeta [META-INF] [2] por el de la carpeta SGBD dentro de la carpeta [conf] [1]. Tomemos como ejemplo el servidor SQL:

El archivo [persistence.xml] es el siguiente:


<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
    http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

    <persistence-unit name="jpa">

        <!-- el proveedor JPA es Hibernate -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>

        <!-- la DataSource JTA es administrada por el entorno Java EE5 -->
        <jta-data-source>java:/datasource</jta-data-source>

        <properties>
            <!-- búsqueda de entidades de la capa JBA -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />

            <!-- registros de SQL Hibernate
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->

            <!-- el tipo de SGBD administrado -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />

            <!-- recreación de todas las tablas (drop+create) al implementar la unidad de persistencia -->
            <property name="hibernate.hbm2ddl.auto" value="create" />

        </properties>
    </persistence-unit>

</persistence>

Solo ha cambiado una línea:

  • línea 24: el dialecto SQL que Hibernate debe utilizar

El archivo [jboss-config.xml] del servidor SQL es, a su vez, el siguiente:


<?xml version="1.0" encoding="UTF-8"?>

<deployment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:jboss:bean-deployer bean-deployer_1_0.xsd"
    xmlns="urn:jboss:bean-deployer:2.0">

    <!-- fábrica de DataSource -->
    <bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
        <!-- nombre JNDI de la DataSource -->
        <property name="jndiName">java:/datasource</property>

        <!-- base de datos administrada -->
        <property name="driverClass">com.microsoft.sqlserver.jdbc.SQLServerDriver</property>
        <property name="connectionURL">jdbc:sqlserver://localhost\\SQLEXPRESS:1246;databaseName=jpa</property>
        <property name="userName">jpa</property>
        <property name="password">jpa</property>

        <!-- propiedades del grupo de conexiones -->
    ...
    </bean>

</deployment>

Solo se han modificado las líneas 12 a 15: estas indican las características de la nueva conexión JDBC.

Se invita al lector a repetir con otros archivos SGBD las pruebas descritas para MySQL5.

3.2.9. Cambiar la implementación de JPA

Como se indicó anteriormente, no hemos encontrado ningún ejemplo de uso del contenedor JBoss EJB3 con TopLink. Hasta la fecha (junio de 2007), sigo sin saber si esta configuración es posible.

3.3. Otros ejemplos

Resumamos lo que se ha hecho con la entidad Personne. Hemos construido tres arquitecturas para realizar las mismas pruebas:

1 - una implementación con Spring y Hibernate

2 - una implementación con Spring y Toplink

3 - una implementación con JBoss EJB3 y Hibernate

Los ejemplos del tutorial incluyen estas tres arquitecturas junto con otras entidades analizadas en la primera parte del tutorial:

Categoría - Artículo

  • en [1]: la versión Spring / Hibernate
  • en [2]: la versión Spring / Toplink
  • en [3]: la versión Jboss Ejb3 / Hibernate

Persona - Dirección - Actividad

  • en [1]: la versión Spring / Hibernate
  • en [2]: la versión Spring / Toplink
  • en [3]: la versión JBoss EJB3 / Hibernate

Estos ejemplos no aportan novedades en cuanto a la arquitectura. Simplemente se ubican en un contexto en el que hay varias entidades que gestionar con relaciones uno a varios o varios a varios entre ellas, algo que no tenían los ejemplos con la entidad Personne.

3.4. Ejemplo 3: Spring / JPA en una aplicación web

3.4.1. Presentación

Aquí retomamos una aplicación presentada en el siguiente documento:

[ref4]: Fundamentos del desarrollo web MVC en Java [http://tahe.developpez.com/java/baseswebmvc/].

Este documento presenta los fundamentos del desarrollo web MVC en Java. Para comprender el ejemplo que sigue, el lector debe contar con estos fundamentos. La aplicación web utilizará el servidor Tomcat. Su instalación y uso dentro de Eclipse se describen en el párrafo 5.3.

La aplicación se había desarrollado con una capa [dao] basada en la herramienta Ibatis / SqlMap [http://ibatis.apache.org/], que se encargaba del puente entre el modelo relacional y el modelo de objetos. Nos limitamos a sustituir Ibatis por JPA. La arquitectura de la aplicación será la siguiente:

La aplicación web que vamos a desarrollar permitirá administrar un grupo de personas mediante cuatro operaciones:

  • lista de personas del grupo
  • agregar una persona al grupo
  • modificar a una persona del grupo
  • eliminar a una persona del grupo

Estas cuatro operaciones son las básicas que se realizan en una tabla de base de datos. Las siguientes capturas de pantalla muestran las páginas que la aplicación muestra al usuario.

 

3.4.2. El proyecto Eclipse

El proyecto Eclipse de la aplicación es el siguiente:

  • en [1]: el proyecto web. Es un proyecto de Eclipse del tipo [Dynamic Web Project] [2]. Se encuentra en [4] dentro de la carpeta [3] de los ejemplos del tutorial. Lo importaremos.
  • en [5]: las fuentes y la configuración de las capas [service, dao, jpa]. Conservamos los logros de [dao, entites, service] del proyecto Eclipse [hibernate-spring-personnes-metier-dao] analizado en el párrafo 3.1.1. Solo desarrollamos la capa [web], representada aquí por el paquete [web]. Por otra parte, conservamos los archivos de configuración [persistence.xml, spring-config.xml] de este proyecto, con la única diferencia de que utilizaremos el Postgres SGBD, lo que se traduce en las siguientes modificaciones en [spring-config.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
...
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
...
                <property name="databasePlatform" value="org.hibernate.dialect.PostgreSQLDialect" />
...
        </property>
    ...
    </bean>

    <!-- la fuente de datos DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="org.postgresql.Driver" />
        <property name="url" value="jdbc:postgresql:jpa" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>
....
</beans>

Las líneas 8 y 16-19 se han adaptado a Postgres.

  • En [6]: la carpeta [WebContent] contiene las páginas JSP del proyecto, así como las bibliotecas necesarias. Estas últimas se presentan en [8]
  • La aplicación se puede utilizar con diversos SGBD. Solo hay que modificar el archivo [spring-config.xml]. La carpeta [conf] [7] contiene el archivo [spring-config.xml] adaptado a diversos SGBD.

3.4.3. La capa [web]

Nuestra aplicación tiene la siguiente arquitectura de múltiples capas:

La capa [web] mostrará pantallas al usuario para permitirle administrar el grupo de personas:

  • lista de personas del grupo
  • agregar una persona al grupo
  • modificar a una persona del grupo
  • eliminar a una persona del grupo

Para ello, se basará en la capa [service], la cual a su vez recurrirá a la capa [dao]. Ya hemos presentado las pantallas gestionadas por la capa [web] (párrafo 3.4.1). Para describir la capa web, presentaremos sucesivamente:

  • su configuración
  • sus vistas
  • su controlador
  • algunas pruebas

3.4.3.1. Configuración de la aplicación web

Repasemos la arquitectura del proyecto Eclipse:

 
  • En el paquete [web] se encuentra el controlador de la aplicación web: la clase [Application].
  • Las páginas JSP / JSTL de la aplicación se encuentran en [WEB-INF/vues].
  • La carpeta [WEB-INF/lib] contiene los archivos de terceros necesarios para la aplicación. Estos se pueden ver en la carpeta [Web App Libraries].

[web.xml]


El archivo [web.xml] es el archivo que utiliza el servidor web para cargar la aplicación. Su contenido es el siguiente:


<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <display-name>spring-jpa-hibernate-personnes-crud</display-name>
    <!--  ServletPersonne -->
    <servlet>
        <servlet-name>personnes</servlet-name>
        <servlet-class>web.Application</servlet-class>
        <init-param>
            <param-name>urlEdit</param-name>
            <param-value>/WEB-INF/vues/edit.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlErreurs</param-name>
            <param-value>/WEB-INF/vues/erreurs.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlList</param-name>
            <param-value>/WEB-INF/vues/list.jsp</param-value>
        </init-param>
    </servlet>
    <!--  Asignación ServletPersonne-->
    <servlet-mapping>
        <servlet-name>personnes</servlet-name>
        <url-pattern>/do/*</patrón-url>
    </servlet-mapping>
    <!--  archivos de inicio -->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    <!--  Página de error inesperado -->
    <error-page>
        <exception-type>java.lang.Exception</exception-type>
        <location>/WEB-INF/vues/exception.jsp</location>
    </error-page>
</web-app>
  • líneas 23-26: las URL [/do/*] serán procesadas por el servlet [personnes]
  • líneas 7-8: el servlet [personnes] es una instancia de la clase [Application], una clase que vamos a crear.
  • líneas 9-20: definen tres parámetros [urlList, urlEdit, urlErreurs] que identifican las URL de las páginas JSP de las vistas [list, edit, erreurs].
  • líneas 28-30: la aplicación tiene una página de inicio predeterminada [index.jsp] que se encuentra en la raíz de la carpeta de la aplicación web.
  • líneas 32-35: la aplicación cuenta con una página de errores predeterminada que se muestra cuando el servidor web detecta una excepción no manejada por la aplicación.
    • línea 37: la etiqueta <exception-type> indica el tipo de excepción que maneja la directiva <error-page>; en este caso, el tipo [java.lang.Exception] y sus derivados, es decir, todas las excepciones.
    • línea 38: la etiqueta <location> indica la página JSP que se debe mostrar cuando ocurre una excepción del tipo definido por <exception-type>. La excepción que ocurrió está disponible en esta página en un objeto llamado exception si la página tiene la directiva:

<%@ page isErrorPage="true" %>
  • (continuación)
    • si <exception-type> especifica un tipo T1 y una excepción de tipo T2, que no deriva de T1, se remite al servidor web, este envía al cliente una página de excepción propia que, por lo general, no es muy intuitiva. De ahí la importancia de la etiqueta <error-page> en el archivo [web.xml].

[index.jsp]


Esta página se muestra si un usuario solicita directamente el contexto de la aplicación sin especificar una URL, c.a.d. Aquí [/spring-jpa-hibernate-personnes-crud]. Su contenido es el siguiente:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/do/list"/>

[index.jsp] redirige (línea 4) al cliente a la URL [/do/list]. Esta URL muestra la lista de personas del grupo.

3.4.3.2. Las páginas JSP / JSTL de la aplicación


La vista [list.jsp]


Sirve para mostrar la lista de personas:

Image

Su código es el siguiente:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
            <c:if test="${erreurs!=null}">
                <h3>Les erreurs suivantes se sont produites :</h3>
                <ul>
                    <c:forEach items="${erreurs}" var="erreur">
                        <li><c:out value="${erreur}"/></li>
                    </c:forEach>
                </ul>
            <hr>
        </c:if>
        <h2>Liste des personnes</h2>
        <table border="1">
            <tr>
                <th>Id</th>
                <th>Version</th>
                <th>Pr&eacute;nom</th>
                <th>Nom</th>
                <th>Date de naissance</th>
                <th>Mari&eacute;</th>
                <th>Nombre d'enfants</th>
                <th></th>
            </tr>
            <c:forEach var="personne" items="${personnes}">
                <tr>
                    <td><c:out value="${personne.id}"/></td>
                    <td><c:out value="${personne.version}"/></td>
                    <td><c:out value="${personne.prenom}"/></td>
                    <td><c:out value="${personne.nom}"/></td>
                    <td><dt:format pattern="dd/MM/yyyy">${personne.datenaissance.time}</dt:format></td>
                    <td><c:out value="${personne.marie}"/></td>
                    <td><c:out value="${personne.nbenfants}"/></td>
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>
  • Esta vista recibe dos elementos en su modelo:
    • el elemento [personnes] asociado a un objeto de tipo [List] de objetos de tipo [Personne]: una lista de personas.
    • el elemento opcional [erreurs] asociado a un objeto de tipo [List] de objetos de tipo [String]: una lista de mensajes de error.
  • líneas 31-43: se recorre la lista ${personas} para mostrar una tabla HTML que contiene a las personas del grupo.
  • línea 40: la URL a la que apunta el enlace [Modifier] se configura mediante el campo [id] de la persona actual, para que el controlador asociado a la URL [/do/edit] sepa qué persona se debe modificar.
  • línea 41: se hace lo mismo con el enlace [Supprimer].
  • línea 37: para mostrar la fecha de nacimiento de la persona en el formato JJ/MM/AAAA, se utiliza la etiqueta <dt> de la biblioteca de etiquetas [DateTime] del proyecto Apache [Jakarta Taglibs]:

Image

El archivo de descripción de esta biblioteca de etiquetas se define en la línea 3.

  • Línea 46: el enlace [Ajout] para agregar una nueva persona apunta a la URL [/do/edit], al igual que el enlace [Modifier] de la línea 40. Es el valor -1 del parámetro [id] el que indica que se trata de una adición y no de una modificación.
  • líneas 10-18: si el elemento ${errores} está en la plantilla, entonces se muestran los mensajes de error que contiene.

La vista [edit.jsp]


Sirve para mostrar el formulario para agregar una persona nueva o modificar una persona existente:

El código de la vista [edit.jsp] es el siguiente:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="../ressources/standard.jpg">
        <h2>Ajout/Modification d'une personne</h2>
        <c:if test="${erreurEdit!=''}">
            <h3>Echec de la mise à jour :</h3>
          L'erreur suivante s'est produite : ${erreurEdit}
            <hr>
        </c:if>
        <form method="post" action="<c:url value="/do/validate"/>">
            <table border="1">
                <tr>
                    <td>Id</td>
                    <td>${id}</td>
                </tr>
                <tr>
                    <td>Version</td>
                    <td>${version}</td>
                </tr>
                <tr>
                    <td>Pr&eacute;nom</td>
                    <td>
                        <input type="text" value="${prenom}" name="prenom" size="20">
                    </td>
                    <td>${erreurPrenom}</td>
                </tr>
                <tr>
                    <td>Nom</td>
                    <td>
                        <input type="text" value="${nom}" name="nom" size="20">
                    </td>
                    <td>${erreurNom}</td>
                </tr>
                <tr>
                <td>Date de naissance (JJ/MM/AAAA)</td>
                    <td>
                        <input type="text" value="${datenaissance}" name="datenaissance">
                    </td>
                    <td>${erreurDateNaissance}</td>
                </tr>
                <tr>
                    <td>Mari&eacute;</td>
                    <td>
                        <c:choose>
                            <c:when test="${marie}">
                                <input type="radio" name="marie" value="true" checked>Oui
                                <input type="radio" name="marie" value="false">Non
                            </c:when>
                            <c:otherwise>
                                <input type="radio" name="marie" value="true">Oui
                                <input type="radio" name="marie" value="false" checked>Non
                            </c:otherwise>
                        </c:choose>
                    </td>
                </tr>
                <tr>
                    <td>Nombre d'enfants</td>
                    <td>
                        <input type="text" value="${nbenfants}" name="nbenfants">
                    </td>
                    <td>${erreurNbEnfants}</td>
                </tr>
            </table>
            <br>
            <input type="hidden" value="${id}" name="id">
      <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
            <a href="<c:url value="/do/list"/>">Annuler</a>
        </form>
    </body>
</html>

Esta vista presenta un formulario para agregar una persona nueva o actualizar una ya existente. De aquí en adelante, y para simplificar la redacción, utilizaremos únicamente el término [mise à jour]. El botón [Valider] (línea 73) activa el POST del formulario en la URL [/do/validate] (línea 16). Si el POST falla, se vuelve a mostrar la vista [edit.jsp] con el error o los errores que se hayan producido; de lo contrario, se muestra la vista [list.jsp].

  • La vista [edit.jsp], que se muestra tanto en un GET como en un POST que falla, recibe los siguientes elementos en su modelo:
atributo
GET
POST
id
identificador de la persona
actualizada
ídem
version
su versión
ídem
prenom
su nombre
nombre ingresado
nom
su apellido
apellido ingresado
datenaissance
su fecha de nacimiento
fecha de nacimiento ingresada
marie
su estado civil
estado civil ingresado
nbenfants
su número de hijos
número de hijos ingresado
erreurEdit
vacío
un mensaje de error que indica que no se pudo agregar
o la modificación en el momento de la ejecución de POST, provocada
por el botón [Envoyer]. Vacío si no hay error.
erreurPrenom
vacío
indica un nombre incorrecto; de lo contrario, está vacío
erreurNom
vacío
indica un apellido incorrecto; si no, está vacío
erreurDateNaissance
vacío
indica una fecha de nacimiento incorrecta; si no, está vacío
erreurNbEnfants
vacío
indica un número de hijos incorrecto; si no, queda en blanco
  • líneas 11-15: si el POST del formulario falla, aparecerá [erreurEdit!=''] y se mostrará un mensaje de error.
  • línea 16: el formulario se enviará a la URL [/do/validate]
  • línea 20: se muestra el elemento [id] de la plantilla
  • línea 24: se muestra el elemento [version] de la plantilla
  • líneas 26-32: ingreso del nombre de la persona:
    • al mostrar el formulario por primera vez (GET), ${nombre} muestra el valor actual del campo [prenom] del objeto [Personne] actualizado y ${erreurPrenom} está vacío.
    • En caso de error después del POST, se vuelve a mostrar el valor ingresado ${prenom}, así como el posible mensaje de error ${erreurPrenom}
  • líneas 33-39: ingreso del apellido de la persona
  • líneas 40-46: ingreso de la fecha de nacimiento de la persona
  • líneas 47-61: ingreso del estado civil de la persona mediante un botón de opción. Se utiliza el valor del campo [marie] del objeto [Personne] para determinar cuál de los dos botones de opción debe marcarse.
  • líneas 62-68: ingreso del número de hijos de la persona
  • línea 71: un campo oculto HTML denominado [id], cuyo valor corresponde al campo [id] de la persona que se está actualizando; -1 para un agregado, cualquier otro valor para una modificación.
  • línea 72: un campo oculto HTML llamado [version], cuyo valor es el campo [id] de la persona que se está actualizando.
  • línea 73: el botón [Valider], de tipo [Submit], del formulario
  • línea 74: un enlace que permite regresar a la lista de personas. Se ha denominado [Annuler] porque permite salir del formulario sin validarlo.

La vista [exception.jsp]


Sirve para mostrar una página que indica que se ha producido una excepción no manejada por la aplicación y que se ha reportado al servidor web.

Por ejemplo, eliminemos una persona que no existe en el grupo:

El código de la vista [exception.jsp] es el siguiente:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ page isErrorPage="true" %>

<%
  response.setStatus(200);
%>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
        <h2>MVC - personnes</h2>
        L'exception suivante s'est produite :
        <%= exception.getMessage()%>
        <br><br>
        <a href="<c:url value="/do/list"/>">Retour &agrave; la liste</a>
    </body>
</html>
  • Esta vista recibe una clave en su modelo: el elemento [exception], que es la excepción que fue interceptada por el servidor web. Para que el servidor web incluya este elemento en el modelo de la página JSP, es necesario que la página tenga definida la etiqueta de la línea 3.
  • línea 6: se establece en 200 el código de estado HTTP de la respuesta. Se trata del primer encabezado HTTP de la respuesta. El código 200 le indica al cliente que su solicitud ha sido atendida. Por lo general, se ha incluido un documento HTML en la respuesta del servidor. Este es el caso aquí. Si no se establece en 200 el código de estado HTTP de la respuesta, aquí tendrá el valor 500, lo que significa que se ha producido un error. De hecho, al haber interceptado una excepción no manejada, el servidor web considera que esta situación es anómala y la señala con el código 500. La reacción ante el código HTTP 500 varía según el navegador: Firefox muestra el documento HTML que puede acompañar a esta respuesta, mientras que IE ignora este documento y muestra su propia página. Por esta razón, hemos reemplazado el código 500 por el código 200.
  • línea 16: se muestra el texto de la excepción
  • línea 18: se ofrece al usuario un enlace para regresar a la lista de personas

La vista [erreurs.jsp]


Sirve para mostrar una página que informa sobre los errores de inicialización de la aplicación, c.a.d, y los errores detectados durante la ejecución del método [init] del servlet del controlador. Esto puede deberse, por ejemplo, a la falta de un parámetro en el archivo [web.xml], como se muestra en el siguiente ejemplo:

Image

El código de la página [erreurs.jsp] es el siguiente:


<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<html>
    <head>
      <title>MVC - Personnes</title>
  </head>
  <body>
      <h2>Les erreurs suivantes se sont produites</h2>
    <ul>
            <c:forEach var="erreur" items="${erreurs}">
                <li>${erreur}</li>
            </c:forEach>
    </ul>
  </body>
</html>

La página recibe en su plantilla un elemento [erreurs], que es un objeto de tipo [ArrayList] de objetos [String]; estos últimos son mensajes de error. Se muestran mediante el bucle de las líneas 13 a 15.

3.4.3.3. El controlador de la aplicación

El controlador [Application] se define en el paquete [web]:

Image


Estruct tura e inicialización del controlador


El esqueleto del controlador [Application] es el siguiente:


package web;

...


@SuppressWarnings("serial")
public class Application extends HttpServlet {
    // parámetros de instancia
    private String urlErreurs = null;
    private ArrayList erreursInitialisation = new ArrayList<String>();
    private String[] paramètres = { "urlList", "urlEdit", "urlErreurs" };
    private Map params = new HashMap<String, String>();

    // servicio
    private IService service = null;

    // inicialización
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
        // se recuperan los parámetros de inicialización del servlet
        ServletConfig config = getServletConfig();
        // se procesan los demás parámetros de inicialización
        String valeur = null;
        for (int i = 0; i < paramètres.length; i++) {
            // valor del parámetro
            valeur = config.getInitParameter(paramètres[i]);
            // ¿Existe el parámetro?
            if (valeur == null) {
                // se registra el error
                erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
            } else {
                // se almacena el valor del parámetro
                params.put(paramètres[i], valeur);
            }
        }
        // la URL de la vista [erreurs] tiene un tratamiento especial
        urlErreurs = config.getInitParameter("urlErreurs");
        if (urlErreurs == null)
            throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
        // configuración de la aplicación
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // capa de servicio
        service = (IService) ctx.getBean("service");
        // se vacía la base de datos
        clean();
        // se llena
        try {
            fill();
        } catch (ParseException e) {
            throw new ServletException(e);
        }
    }

    // llenado de la tabla
    public void fill() throws ParseException {
        // 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);
        // que se guardan
        service.saveArray(new Personne[] { p1, p2 });
    }

    // eliminación de elementos de la tabla
    public void clean() {
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }

    // GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
...
    }

    // visualización de la lista de personas
    private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // modificación / agregación de una persona
    private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // eliminación de una persona
    private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // confirmación de la modificación o adición de una persona
    public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // visualización del formulario prellenado
    private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
    ...
    }

    // enviar
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        // se pasa el control a GET
        doGet(request, response);
    }

}
  • líneas 21-34: se recuperan los parámetros esperados del archivo [web.xml].
  • líneas 37-39: el parámetro [urlErreurs] debe estar presente obligatoriamente, ya que indica la URL de la vista [erreurs], capaz de mostrar los posibles errores de inicialización. Si no existe, se interrumpe la aplicación al ejecutar una [ServletException] (línea 39). Esta excepción se transmitirá al servidor web y será gestionada por la etiqueta <error-page> del archivo [web.xml]. Por lo tanto, se muestra la vista [exception.jsp]:

Image

El enlace [Retour à la liste] anterior no funciona. Al utilizarlo, se obtiene la misma respuesta mientras la aplicación no se haya modificado y recargado. Es útil para otros tipos de excepciones, como ya lo hemos visto.

  • Líneas 40-43: utilizan el archivo de configuración de Spring para obtener una referencia a la capa [service]. Tras la inicialización del controlador, sus métodos disponen de una referencia [service] en la capa [service] (línea 15), la cual utilizarán para ejecutar las acciones solicitadas por el usuario. Estas serán interceptadas por el método [doGet], que las hará procesar por un método específico del controlador:
Url
Método HTTP
método del controlador
/do/list
GET
doListPersonnes
/do/edit
GET
doEditPersonne
/do/validate
POST
doValidatePersonne
/do/delete
GET
doDeletePersonne

El método [doGet]


El objetivo de este método es dirigir el procesamiento de las acciones solicitadas por el usuario hacia el método correcto. Su código es el siguiente:


// GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {

        
// se verifica cómo se realizó la inicialización del servlet
        if (erreursInitialisation.size() != 0) {
            // se pasa el control a la página de errores
            request.setAttribute("erreurs", erreursInitialisation);
            getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
            // fin
            return;
        }
        // se recupera el método de envío de la solicitud
        String méthode = request.getMethod().toLowerCase();
        // se recupera la acción a ejecutar
        String action = request.getPathInfo();
        // ¿acción?
        if (action == null) {
            action = "/list";
        }
        // ejecución de la acción
        if (méthode.equals("get") && action.equals("/list")) {
            // lista de personas
            doListPersonnes(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/delete")) {
            // eliminación de una persona
            doDeletePersonne(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/edit")) {
            // presentación del formulario para agregar o modificar una persona
            doEditPersonne(request, response);
            return;
        }
        if (méthode.equals("post") && action.equals("/validate")) {
            // validación del formulario para agregar o modificar una persona
            doValidatePersonne(request, response);
            return;
        }
        // otros casos
        doListPersonnes(request, response);
    }
  • líneas 7-13: se verifica que la lista de errores de inicialización esté vacía. Si no es así, se muestra la vista [erreurs(erreurs)], que indicará el error o los errores.
  • línea 15: se recupera el método [get] o [post] que el cliente utilizó para realizar su solicitud.
  • línea 17: se recupera el valor del parámetro [action] de la consulta.
  • líneas 23-27: procesamiento de la solicitud [GET /do/list] que solicita la lista de personas.
  • líneas 28-32: procesamiento de la solicitud [GET /do/delete], que solicita la eliminación de una persona.
  • líneas 33-37: procesamiento de la solicitud [GET /do/edit], que solicita el formulario para actualizar una persona.
  • líneas 38-42: procesamiento de la solicitud [POST /do/validate], que solicita la validación de la persona actualizada.
  • línea 44: si la acción solicitada no es ninguna de las cinco anteriores, entonces se procede como si fuera [GET /do/list].

El método [doListPersonnes]


Este método procesa la solicitud [GET /do/list], que solicita la lista de personas:

Image

Su código es el siguiente:


    // visualización de la lista de personas
    private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // la plantilla de la vista [list]
        request.setAttribute("personnes", service.getAll());
        // Visualización de la vista [list]
        getServletContext().getRequestDispatcher((String) params.get("urlList")).forward(request, response);
}
  • línea 4: se solicita a la capa [service] la lista de personas del grupo y se inserta esta en la plantilla bajo la clave «personas».
  • línea 6: se muestra la vista [list.jsp] descrita en el párrafo 3.4.3.2.

El método [doDeletePersonne]


Este método procesa la solicitud [GET /do/delete?id=XX], que solicita la eliminación de la persona con id=XX. La URL [/do/delete?id=XX] es la de los enlaces [Supprimer] de la vista [list.jsp]:

Image

cuyo código es el siguiente:


...
<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

En la línea 12, se ve la URL [/do/delete?id=XX] del enlace [Supprimer]. El método [doDeletePersonne], que debe procesar esta URL, debe eliminar a la persona con id=XX y luego mostrar la nueva lista de personas del grupo. Su código es el siguiente:


// eliminación de una persona
    private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // se recupera el ID de la persona
        int id = Integer.parseInt(request.getParameter("id"));
        // se elimina a la persona
        service.deleteOne(id);
        // redirecciona a la lista de personas
        response.sendRedirect("list");
    }
  • línea 4: la URL procesada tiene el formato [/do/delete?id=XX]. Se recupera el valor [XX] del parámetro [id].
  • línea 6: se le solicita a la capa [service] que elimine a la persona con el ID obtenido. No realizamos ninguna verificación. Si la persona que se busca eliminar no existe, la capa [dao] lanza una excepción que la capa [service] deja pasar. Tampoco la manejamos aquí, en el controlador. Por lo tanto, se propagará hasta el servidor web, el cual, según su configuración, mostrará la página [exception.jsp], descrita en el párrafo 3.4.3.2:

Image

  • línea 9: si se ha llevado a cabo la eliminación (sin excepciones), se le pide al cliente que se redirija a la URL relativa [list]. Como la que acaba de procesarse es [/do/delete], la URL de redirección será [/do/list]. Por lo tanto, el navegador se verá obligado a realizar una [GET /do/list], lo que provocará que se muestre la lista de personas.

El método [doEditPersonne]


Este método procesa la solicitud [GET /do/edit?id=XX], que solicita el formulario de actualización de la persona con id=XX. La URL [/do/edit?id=XX] es la de los enlaces [Modifier] y la del enlace [Ajout] de la vista [list.jsp]:

Image

cuyo código es el siguiente:


...
<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

En la línea 11, se ve la URL [/do/edit?id=XX] del enlace [Modifier] y, en la línea 17, la URL [/do/edit?id=-1] del enlace [Ajout]. El método [doEditPersonne] debe mostrar el formulario de edición de la persona con id=XX o, si se trata de un nuevo registro, presentar un formulario en blanco.

  • En [1], mencionado anteriormente, se muestra el formulario de alta, y en [2], el formulario de modificación.

El código del método [doEditPersonne] es el siguiente:


// modificación o agregación de una persona
    private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // se recupera el ID de la persona
        int id = Integer.parseInt(request.getParameter("id"));
        // ¿Agregar o modificar?
        Personne personne = null;
        if (id != -1) {
            // modificación: se recupera la persona a modificar
            personne = service.getOne(id);
            request.setAttribute("id", personne.getId());
            request.setAttribute("version", personne.getVersion());
        } else {
            // agregación: se crea una persona vacía
            personne = new Personne();
            request.setAttribute("id", -1);
            request.setAttribute("version", -1);
        }
        // se coloca el objeto [Personne] en la sesión del usuario
        request.getSession().setAttribute("personne", personne);
        // y en la plantilla de la vista [edit]
        request.setAttribute("erreurEdit", "");
        request.setAttribute("prenom", personne.getPrenom());
        request.setAttribute("nom", personne.getNom());
        Date dateNaissance = personne.getDatenaissance();
        if (dateNaissance != null) {
            request.setAttribute("datenaissance", new SimpleDateFormat("dd/MM/yyyy").format(dateNaissance));
        } else {
            request.setAttribute("datenaissance", "");
        }
        request.setAttribute("marie", personne.isMarie());
        request.setAttribute("nbenfants", personne.getNbenfants());
        // se muestra la vista [edit]
        getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • el GET tiene como destino una URL del tipo [/do/edit?id=XX]. En la línea 4, recuperamos el valor de [id]. A continuación, hay dos casos:
    1. El id es distinto de -1. En ese caso, se trata de una modificación y hay que mostrar un formulario prellenado con la información de la persona que se va a modificar. En la línea 9, se solicita esta persona a la capa [service].
    2. Si id es igual a -1, se trata de una adición y hay que mostrar un formulario vacío. Para ello, se crea una persona vacía en la línea 14.
    3. En ambos casos, se inicializan los elementos [id, version] de la plantilla de la página [edit.jsp] descrita en el párrafo 3.4.3.2.
  • El objeto [Personne] obtenido se coloca en la plantilla de la página [edit.jsp]. Esta plantilla incluye los siguientes elementos: [erreurEdit, id, version, prenom, erreurPrenom, nom, erreurNom, datenaissance, erreurDateNaissance, marie, nbenfants, erreurNbEnfants]. Estos elementos se inicializan en las líneas 19 a 31, excepto aquellos cuyo valor es la cadena vacía [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants]. Se sabe que, si no están presentes en la plantilla, la biblioteca JSTL mostrará una cadena vacía como su valor. Aunque el elemento [erreurEdit] también tiene como valor una cadena vacía, se inicializa de todos modos porque se realiza una comprobación de su valor en la página [edit.jsp].
  • Una vez que el modelo está listo, el control pasa a la página [edit.jsp], línea 33, que generará la vista [edit].

El método [doValidatePersonne]


Este método procesa la solicitud [POST /do/validate] que valida el formulario de actualización. Esta solicitud POST se activa mediante el botón [Valider]:

Image

Recordemos los campos de entrada del formulario HTML de la vista anterior:


<form method="post" action="<c:url value="/do/validate"/>">
...
                        <input type="text" value="${nom}" name="nom" size="20">
...
                        <input type="text" value="${datenaissance}" name="datenaissance">
...
                        <c:choose>
                            <c:when test="${marie}">
                                <input type="radio" name="marie" value="true" checked>Oui
                                <input type="radio" name="marie" value="false">Non
                            </c:when>
                            <c:otherwise>
                                <input type="radio" name="marie" value="true">Oui
                                <input type="radio" name="marie" value="false" checked>Non
                            </c:otherwise>
                        </c:choose>
...
                        <input type="text" value="${nbenfants}" name="nbenfants">
...
            <input type="hidden" value="${id}" name="id">
      <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
            <a href="<c:url value="/do/list"/>">Annuler</a>
        </form>

La solicitud POST contiene los parámetros [prenom, nom, datenaissance, marie, nbenfants, id] y se envía a la URL [/do/validate] (línea 1). Es procesada por el siguiente método [doValidatePersonne]:


// validación de la modificación o adición de una persona
    public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // se recuperan los elementos enviados
        boolean formulaireErroné = false;
        boolean erreur;
        // el nombre
        String prenom = request.getParameter("prenom").trim();
        // ¿El nombre es válido?
        if (prenom.length() == 0) {
            // se registra el error
            request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
            formulaireErroné = true;
        }
        // el apellido
        String nom = request.getParameter("nom").trim();
        // ¿Nombre válido?
        if (nom.length() == 0) {
            // se registra el error
            request.setAttribute("erreurNom", "Le nom est obligatoire");
            formulaireErroné = true;
        }
        // la fecha de nacimiento
        Date datenaissance = null;
        try {
            datenaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request.getParameter("datenaissance").trim());
        } catch (ParseException e) {
            // se registra el error
            request.setAttribute("erreurDateNaissance", "Date incorrecte");
            formulaireErroné = true;
        }
        // estado civil
        boolean marie = Boolean.parseBoolean(request.getParameter("marie").trim());
        // número de hijos
        int nbenfants = 0;
        erreur = false;
        try {
            nbenfants = Integer.parseInt(request.getParameter("nbenfants").trim());
            if (nbenfants < 0) {
                erreur = true;
            }
        } catch (NumberFormatException ex) {
            // se observa el error
            erreur = true;
        }
        // ¿Número de hijos incorrecto?
        if (erreur) {
            // se reporta el error
            request.setAttribute("erreurNbEnfants", "Nombre d'enfants incorrect");
            formulaireErroné = true;
        }
        // identificación de la persona
        int id = Integer.parseInt(request.getParameter("id"));
        // ¿El formulario tiene errores?
        if (formulaireErroné) {
            // Se vuelve a mostrar el formulario con los mensajes de error
            showFormulaire(request, response, "");
            // listo
            return;
        }
        // El formulario es correcto; se actualiza la persona que se ha guardado en la sesión
        // con la información enviada por el cliente
        Personne personne = (Personne)request.getSession().getAttribute("personne");
        personne.setDatenaissance(datenaissance);
        personne.setMarie(marie);
        personne.setNbenfants(nbenfants);
        personne.setNom(nom);
        personne.setPrenom(prenom);
        // persistencia
        try {
            if (id == -1) {
                // creación
                service.saveOne(personne);
            } else {
                // actualización
                service.updateOne(personne);
            }
        } catch (DaoException ex) {
            // se vuelve a mostrar el formulario con el mensaje del error que ocurrió
            showFormulaire(request, response, ex.getMessage());
            // fin
            return;
        }
        // se redirige a la lista de personas
        response.sendRedirect("list");
    }

    // se muestra el formulario prellenado
    private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
        // se prepara la plantilla de la vista [edit]
        request.setAttribute("erreurEdit", erreurEdit);
        request.setAttribute("id", request.getParameter("id"));
        request.setAttribute("version", request.getParameter("version"));
        request.setAttribute("prenom", request.getParameter("prenom").trim());
        request.setAttribute("nom", request.getParameter("nom").trim());
        request.setAttribute("datenaissance", request.getParameter("datenaissance").trim());
        request.setAttribute("marie", request.getParameter("marie"));
        request.setAttribute("nbenfants", request.getParameter("nbenfants").trim());
        // se muestra la vista [edit]
        getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • líneas 7-13: se recupera el parámetro [prenom] de la solicitud POST y se verifica su validez. Si resulta incorrecto, el elemento [erreurPrenom] se inicializa con un mensaje de error y se coloca en los atributos de la consulta.
  • líneas 15-21: se procede de manera similar para el parámetro [nom]
  • líneas 23-30: se procede de manera similar para el parámetro [datenaissance]
  • línea 32: se recupera el parámetro [marie]. No se verifica su validez porque, a priori, proviene del valor de un botón de opción. Dicho esto, nada impide que un programa genere un [POST /.../do/validate] acompañado de un parámetro [marie] inventado. Por lo tanto, deberíamos verificar la validez de este parámetro. Aquí nos basamos en nuestro manejo de excepciones, que provoca que se muestre la página [exception.jsp] si el controlador no las maneja por sí mismo. Así pues, si la conversión del parámetro [marie] a un valor booleano falla en la línea 32, se generará una excepción que dará lugar al envío de la página [exception.jsp] al cliente. Este funcionamiento nos conviene.
  • líneas 34-50: se recupera el parámetro [nbenfants] y se verifica su valor.
  • línea 52: se recupera el parámetro [id] sin verificar su valor
  • líneas 54-59: si el formulario contiene errores, se vuelve a mostrar con los mensajes de error generados anteriormente
  • líneas 62-67: si es válido, se crea un nuevo objeto [Personne] con los elementos del formulario
  • líneas 69-82: se guarda el usuario. El proceso de guardado puede fallar. En un entorno multiusuario, es posible que el usuario que se va a modificar haya sido eliminado o ya haya sido modificado por otra persona. En ese caso, la capa [dao] lanzará una excepción que se maneja aquí.
  • línea 84: si no se ha producido ninguna excepción, se redirige al cliente a la URL [/do/list] para mostrarle el nuevo estado del grupo.
  • línea 79: si se produjo una excepción durante el guardado, se solicita nuevamente que se vuelva a mostrar el formulario inicial, pasándole el mensaje de error de la excepción (tercer parámetro).

El método [showFormulaire] (líneas 88-97) genera la plantilla necesaria para la página [edit.jsp] con los valores ingresados (request.getParameter(" ... ")). Recordemos que los mensajes de error ya han sido insertados en la plantilla mediante el método [doValidatePersonne]. La página [edit.jsp] se muestra en la línea 99.

3.4.4. Las pruebas de la aplicación web

En el párrafo 3.4.1 se presentaron varias pruebas. Invitamos al lector a reproducirlas. Aquí mostramos otras capturas de pantalla que ilustran los casos de conflictos de acceso a los datos en un entorno multiusuario:

[Firefox] será el navegador del usuario U1. Este solicita la URL [http://localhost:8080/spring-jpa-hibernate-personnes-crud/do/list]:

Image

[IE7] será el navegador del usuario U2. Este solicita la misma URL:

Image

El usuario U1 ingresa para modificar el perfil de la persona [p2]:

Image

El usuario U2 hace lo mismo:

Image

El usuario U1 realiza modificaciones y las valida:

El usuario U2 hace lo mismo:

El usuario U2 regresa a la lista de personas mediante el enlace [Retour à la liste] del formulario:

Image

Encuentra a la persona [Lemarchand] tal como la modificó U1 (casada, 2 hijos). El número de versión de p2 ha cambiado. Ahora U2 elimina a [p2]:

U1 sigue teniendo su propia lista y quiere modificar [p2] nuevamente:

U1 usa el enlace [Retour à la liste] para ver de qué se trata:

Image

Descubre que, efectivamente, [p2] ya no forma parte de la lista...

3.4.5. Versión 2

Modificamos ligeramente la versión anterior para utilizar los archivos de las capas [service, dao, jpa] en lugar de sus códigos fuente:

  • en [1]: el nuevo proyecto de Eclipse. Cabe destacar la desaparición de los paquetes [service, dao, entites]. Estos se han encapsulado en el archivo [service-dao-jpa-personne.jar] [2], ubicado dentro de [WEB-INF/lib].
  • La carpeta del proyecto se encuentra en [4]. La importaremos.

No hay nada más que hacer. Cuando se inicia la nueva aplicación web y se solicita la lista de personas, se recibe la siguiente respuesta:

 

Hibernate no encuentra la entidad [Personne]. Para resolver este problema, debemos declarar explícitamente en [persistence.xml] las entidades gestionadas:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0"
    xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <class>entites.Personne</class>
    </persistence-unit>
</persistence>
  • línea 7: se declara la entidad Personne.

Una vez hecho esto, la excepción desaparece:

 

3.4.6. Cambiar la implementación JPA

  • por [1]: el nuevo proyecto de Eclipse
  • en [2]: las bibliotecas Toplink han reemplazado a las bibliotecas Hibernate
  • La carpeta del proyecto es [4]. La importaremos.

Cambiar la implementación JPA solo implica algunos cambios en el archivo [spring-config.xml]. Nada más cambia. Los cambios realizados en el archivo [spring-config.xml] se explicaron en el párrafo 3.1.9:


<?xml version="1.0" encoding="UTF-8"?>

<!-- JVM debe ejecutarse con el argumento -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar 
    (à remplacer par le chemin exact de spring-agent.jar)-->

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
...
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
...    
            <property name="databasePlatform" value="oracle.toplink.essentials.platform.database.MySQL4Platform" />
...
    </bean>
...
</beans>

Solo hay que modificar unas pocas líneas para pasar de Hibernate a Toplink:

  • línea 11: la implementación JPA ahora la realiza Toplink
  • línea 13: la propiedad [databasePlatform] tiene un valor diferente al de Hibernate: el nombre de una clase propia de Toplink. En el párrafo 2.1.15.2 se explica dónde encontrar ese nombre.

Eso es todo. Cabe destacar la facilidad con la que se puede cambiar de SGBD o de la implementación JPA con Spring. Sin embargo, aún no hemos terminado del todo. Al ejecutar la aplicación, se produce una excepción:

 

Aquí se reconoce un problema que ya se mencionó y describió en el párrafo 3.1.9. Se resuelve ejecutando JVM con un agente de Spring. Para ello, se modifica la configuración de inicio de Tomcat:

  • a [1]: se ha utilizado la opción [Run / Run...] para modificar la configuración de Tomcat
  • a [2]: se seleccionó la pestaña [Arguments]
  • en [3]: se agregó el parámetro -javaagent tal como se describe en el párrafo 3.1.9.

Una vez hecho esto, se puede solicitar la lista de personas:

Image

3.5. Otros ejemplos

Nos hubiera gustado mostrar un ejemplo web en el que el contenedor Spring fuera reemplazado por el contenedor Jboss Ejb3 analizado en el párrafo 3.2:

  • en [1]: el proyecto de Eclipse
  • en [3]: su ubicación en la carpeta de ejemplos. Lo importaremos.

Hemos retomado la configuración [jboss-config.xml, persistence.xml] descrita en el párrafo 3.2 y, a continuación, hemos modificado el método [init] del controlador [Application.java] de la siguiente manera:


// init
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
        try {
            // se recuperan los parámetros de inicialización del servlet
            ServletConfig config = getServletConfig();
            // se procesan los demás parámetros de inicialización
            String valeur = null;
            for (int i = 0; i < paramètres.length; i++) {
                // valor del parámetro
                valeur = config.getInitParameter(paramètres[i]);
                // ¿Existe el parámetro?
                if (valeur == null) {
                    // se registra el error
                    erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
                } else {
                    // se almacena el valor del parámetro
                    params.put(paramètres[i], valeur);
                }
            }
            // la URL de la vista [erreurs] tiene un tratamiento especial
            urlErreurs = config.getInitParameter("urlErreurs");
            if (urlErreurs == null)
                throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
            // Configuración de la aplicación
            // se inicia el contenedor EJB3 JBoss
            // se utilizan los archivos de configuración ejb3-interceptors-aop.xml y embedded-jboss-beans.xml
            EJB3StandaloneBootstrap.boot(null);

            // Creación de los beans específicos de la aplicación
            EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");

            // Se implementan todos los EJB encontrados en la ruta de clases de la aplicación
            //EJB3StandaloneBootstrap.scanClasspath("WEB-INF/classes".replace("/", File.separator));
            EJB3StandaloneBootstrap.scanClasspath();

            // Se inicializa el contexto JNDI. Se utiliza el archivo jndi.properties
            InitialContext initialContext = new InitialContext();

            // instanciación de la capa de servicio
            service = (IService) initialContext.lookup("Service/local");
            // se vacía la base de datos
            clean();
            // se llena la base
            fill();
        } catch (Exception e) {
            throw new ServletException(e);
        }
    }
  • líneas 28-38: se inicia el contenedor Ejb3. Este reemplaza al contenedor Spring.
  • línea 41: se solicita una referencia a la capa [service] de la aplicación.

A primera vista, estas son las únicas modificaciones que hay que realizar. Al ejecutarlo, aparece el siguiente error:

 

No he podido entender cuál era exactamente el problema. La excepción reportada por Tomcat parece indicar que se solicitó el objeto denominado «TransactionManager» al servicio JNDI y que este no lo reconocía. Dejo a los lectores la tarea de encontrar una solución a este problema. Si se encuentra una solución, se incorporará al documento.