Skip to content

5. Versión 1: Arquitectura Spring / JPA

Nos proponemos escribir una aplicación de consola y una aplicación gráfica que permitan elaborar la nómina de las cuidadoras infantiles empleadas por la «Casa de la Primera Infancia» de un municipio. Esta aplicación tendrá la siguiente arquitectura:

5.1. BD La base de datos

Los datos estáticos necesarios para elaborar la nómina se almacenarán en una base de datos que, en lo sucesivo, denominaremos dbpam. Esta base de datos podría contener las siguientes tablas:

Tabla EMPLOYES: recopila información sobre las diferentes cuidadoras infantiles

Estructura:

ID
clave primaria
VERSION
N.º de versión: aumenta con cada modificación de la línea
SS
Número de la Seguridad Social del empleado: único
NOM
nombre del empleado
prenom
su nombre
ADRESSE
su dirección
VILLE
su ciudad
CODEPOSTAL
su código postal
INDEMNITE_ID
clave externa en el campo [ID] de la tabla [INDEMNITES]

Su contenido podría ser el siguiente:

Image

Tabla COTISATIONS: recoge los porcentajes necesarios para el cálculo de las cotizaciones sociales

Estructura:

ID
clave primaria
VERSION
N.º de versión: aumenta con cada modificación de la línea
CSGRDS
porcentaje: cotización social generalizada + cotización para la amortización de la deuda social
CSGD
porcentaje: contribución social generalizada deducible
SECU
porcentaje: Seguridad Social, viudedad, vejez
RETRAITE
porcentaje: pensión complementaria + seguro de desempleo

Su contenido podría ser el siguiente:

Image

Los tipos de las cotizaciones sociales son independientes del empleado. La tabla anterior solo tiene una línea.

Tabla INDEMNITES: recoge los datos necesarios para calcular el salario a pagar.
ID
clave primaria
 
VERSION
N.º de versión: aumenta con cada modificación de la línea
 
INDICE
Índice de procesamiento: único
 
BASEHEURE
Precio neto en euros por hora de guardia
 
ENTRETIENJOUR
Indemnización por manutención en euros por día de custodia
 
REPASJOUR
Indemnización por comida en euros por día de cuidado
 
INDEMNITESCP
Indemnización por vacaciones pagadas. Se trata de un porcentaje que se aplica al salario base.
 
  

Su contenido podría ser el siguiente:

Image

Cabe señalar que las indemnizaciones pueden variar de una cuidadora a otra. De hecho, están asociadas a una cuidadora concreta a través de su índice salarial. Así, la Sra. Marie Jouveinal, que tiene un índice salarial de 2 (tabla EMPLOYES), tiene un salario por hora de 2,1 euros (tabla INDEMNITES).

5.2. Método de cálculo del salario de una cuidadora infantil

A continuación, presentamos el método de cálculo del salario mensual de una cuidadora infantil. No pretende ser el que se utiliza en la realidad. Tomamos como ejemplo el salario de la Sra. Marie Jouveinal, que ha trabajado 150 horas durante 20 días en el mes a pagar.

Se tienen en cuenta los siguientes elementos:

[TOTALHEURES]: total des heures
travaillées dans le mois

[TOTALJOURS]: total des jours travaillés
dans le mois
[TOTALHEURES]=150
[TOTALJOURS]= 20
El salario base de la cuidadora infantil
se calcula mediante la siguiente fórmula:
[SALAIREBASE]=([TOTALHEURES]
*[BASEHEURE])*(1+
[INDEMNITESCP]/100)
[SALAIREBASE]=
(150*[2.1])*(1+0.15)= 362,25
De este salario deben deducirse una serie de cotizaciones sociales
deben deducirse de este salario
base:

Contribution sociale généralisée et
contribution au remboursement de la
dette sociale :
 [SALAIREBASE]*[CSGRDS/100]

Contribution sociale généralisée déductible :
 [SALAIREBASE]*[CSGD/100]

Sécurité sociale, veuvage, vieillesse :
 [SALAIREBASE]*[SECU/100]

Retraite Complémentaire + AGPF +
Assurance Chômage :
 [SALAIREBASE]*[RETRAITE/100]
CSGRDS : 12,64
CSGD : 22,28
Sécurité sociale : 34,02
Retraite : 28,55
Total de cotizaciones sociales:
[COTISATIONSSOCIALES]=
[SALAIREBASE]*(CSGRDS+CSGD
+SECU+RETRAITE)/100
[COTISATIONSSOCIALES]=97,48
Además, la cuidadora infantil tiene derecho, por cada día trabajado, a una indemnización por gastos de manutención y a una indemnización por comida. En este concepto, percibe las siguientes indemnizaciones:

[Indemnités]=[TOTALJOURS]
*(ENTRETIENJOUR+REPASJOUR)
[INDEMNITES]=104
En definitiva, el salario neto que se debe abonar a la cuidadora infantil es el siguiente:
[SALAIREBASE]-[COTISATIONSSOCIALES]+[INDEMNITÉS]
[salaire NET]=368,77

5.3. Funcionamiento de la aplicación de consola

A continuación se muestra un ejemplo de ejecución de la aplicación de consola en una ventana de DOS:

dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar 254104940426058 150 20

Valeurs saisies :
N° de sécurité sociale de l'employé : 254104940426058
Nombre d'heures travaillées : 150
Nombre de jours travaillés : 20

Informations Employé :
Nom : Jouveinal
Prénom : Marie
Adresse : 5 rue des Oiseaux
Ville : St Corentin
Code Postal : 49203
Indice : 2

Informations Cotisations :
CSGRDS : 3.49 %
CSGD : 6.15 %
Retraite : 7.88 %
Sécurité sociale : 9.39 %

Informations Indemnités :
Salaire horaire : 2.1 euro
Entretien/jour : 2.1 euro
Repas/jour : 3.1 euro
Congés Payés : 15.0 %

Informations Salaire :
Salaire de base : 362.25 euro
Cotisations sociales : 97.48 euro
Indemnités d'entretien : 42.0 euro
Indemnités de repas : 62.0 euro
Salaire net : 368.77 euro

Escribiremos un programa que reciba la siguiente información:

  1. número de la Seguridad Social de la cuidadora (254104940426058 en el ejemplo —línea 1—)
  2. número total de horas trabajadas (150 en el ejemplo —línea 1—)
  3. número total de días trabajados (20 en el ejemplo —línea 1)

Se observa que:

  • líneas 9-14: muestran la información relativa a la empleada cuyo número de la Seguridad Social se ha facilitado
  • líneas 17-20: muestran los tipos de las diferentes cotizaciones
  • líneas 23-26: muestran las indemnizaciones asociadas al índice salarial del empleado (en este caso, el índice 2)
  • líneas 29-33: muestran los componentes del salario a pagar

La aplicación señala los posibles errores:

Llamada sin parámetros:


dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar
Syntaxe : pg num_securite_sociale nb_heures_travaillées nb_jours_travaillés

Llamada con datos erróneos:


dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar  254104940426058 150x 20x
Le nombre d'heures travaillées [150x] est erroné
Le nombre de jours travaillés [20x] est erroné

Llamada con un número de la Seguridad Social erróneo:


dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar  xx 150 20
L'erreur suivante s'est produite : L'employé de n°[xx] est introuvable

5.4. Funcionamiento de la aplicación gráfica

La aplicación gráfica permite calcular los salarios de las cuidadoras infantiles mediante un formulario Swing:

  • La información que antes se pasaba como parámetros al programa de consola ahora se introduce mediante los campos de entrada [1, 2, 3].
  • El botón [4] inicia el cálculo del salario
  • El formulario muestra los distintos componentes del salario hasta llegar al salario neto a pagar [5]

La lista desplegable [1, 6] no muestra los números de identificación SS de los empleados, sino sus nombres y apellidos. Se parte de la hipótesis de que no hay dos empleados con el mismo nombre y apellidos.

5.5. Creación de la base de datos

Ejecutamos WampServer y utilizamos la herramienta PhpMyAdmin [1]:

  • en [2], seleccionamos la opción [Bases de données],
  • en [3], se crea una base de datos [dbpam_hibernate],
  • en [4], la base de datos creada. La seleccionamos,
  • en [5], queremos importar un script SQL,
  • en [6], se utiliza el botón [Parcourir] para seleccionar el archivo,
  • en [7,8], seleccionamos el script SQL,
  • en [9], se ejecuta,
  • en [10], se han creado las tablas. Su contenido es el siguiente:

tabla EMPLOYES

Image

tabla

tabla INDEMNITES

Image

tabla

tabla COTISATIONS

Image

5.6. Implementación JPA

5.6.1. Capa JPA / Hibernate

Vamos a configurar la capa JPA en el siguiente entorno:

Un programa de consola trabajará con la base de datos. Para ello, es necesario:

  • disponer de una base de datos,
  • disponer del controlador JDBC del SGBD, en este caso MySQL,
  • implementar la capa JPA con Hibernate,
  • escribir el programa de consola.

Creamos el proyecto Maven [mv-pam-jpa-hibernate] [1]:

En la arquitectura de nuestra aplicación necesitamos los siguientes elementos:

  • la base de datos,
  • el controlador JDBC de SGBD MySQL,
  • la capa JPA / Hibernate (entidades y configuración),
  • el programa de consola de pruebas.

5.6.1.1. La base de datos

En primer lugar, creemos la base de datos vacía. Ejecutamos WampServer y utilizamos la herramienta PhpMyAdmin [1]:

  • en [2], seleccionamos la opción [Bases de données],
  • en [3], se crea una base de datos [dbpam_hibernate],
  • en [4], la base de datos creada.

5.6.1.2. Configuración de la capa JPA

La conexión entre la capa JDBC y la base de datos se establece en el archivo [persistence.xml], que configura la capa JPA. Este archivo se puede crear con NetBeans:

  • en la pestaña [services] [1], se establece la conexión con la base de datos mediante el controlador JDBC de MySQL [2],
  • en [3], el nombre de la base de datos a la que se desea conectarse.
  • en [4], el nombre de la base de datos,
  • en [5], nos conectamos como root sin contraseña,
  • en [6], se puede comprobar la conexión,
  • en [7], la conexión se ha establecido correctamente.
  • la conexión aparece en [8] y en [9],
  • en [10], se añade un nuevo elemento al proyecto,
  • en [11] se selecciona la categoría [Persistence] y en [12] el elemento [Persistence Unit],
  • en [13], se le da un nombre a esta unidad de persistencia,
  • en [14], se selecciona una implementación de Hibernate,
  • en [15], se indica la conexión que acabamos de crear con la base de datos MySQL,
  • en [16], se indica que, al instanciar la capa JPA, esta debe crear las tablas correspondientes a las entidades JPA del proyecto.

Al finalizar el asistente, se genera el archivo [persistence.xml]:

  • el archivo aparece en una nueva rama del proyecto, en una carpeta [META-INF] [1],
  • que corresponde a la carpeta [src/main/resources] del proyecto [2,3].

Su contenido es el siguiente:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="mv-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <properties>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbpam_hibernate"/>
      <property name="javax.persistence.jdbc.password" value=""/>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.user" value="root"/>
      <property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
      <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
    </properties>
  </persistence-unit>
</persistence>
  • línea 3: el nombre de la unidad de persistencia y el tipo de transacciones. RESOURCE_LOCAL indica que el propio proyecto gestiona las transacciones. En este caso, será el programa de consola el que deberá hacerlo,
  • línea 4: la implementación JPA utilizada es Hibernate,
  • líneas 6-9: las características JDBC de la conexión a la base de datos,
  • línea 11: solicita la creación de las tablas correspondientes a las entidades JPA. De hecho, NetBeans genera aquí una configuración errónea. La configuración debe ser la siguiente:

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

Con la opción «create», Hibernate, al instanciar la capa JPA, elimina y, a continuación, crea las tablas correspondientes a las entidades JPA. La opción «create-drop» hace lo mismo, pero al final del ciclo de vida de la capa JPA, elimina todas las tablas. Existe otra opción:


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

Esta opción crea las tablas si no existen, pero no las elimina si ya existen.

Añadiremos otras tres propiedades a la configuración de Hibernate:


      <property name="hibernate.show_sql" value="true"/>
      <property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>

Estas propiedades indican a Hibernate que muestre las órdenes SQL que envía a la base de datos. El archivo completo queda, por tanto, así:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="mv-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <properties>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbpam_hibernate"/>
      <property name="javax.persistence.jdbc.password" value=""/>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.user" value="root"/>
      <property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
      <property name="hibernate.hbm2ddl.auto" value="create"/>
      <property name="hibernate.show_sql" value="true"/>
      <property name="hibernate.format_sql" value="true"/>
      <property name="use_sql_comments" value="true"/>
    </properties>
  </persistence-unit>
</persistence>

5.6.1.3. Las dependencias

Volvamos a la arquitectura del proyecto:

Hemos configurado la capa JPA mediante el archivo [persistence.xml]. La implementación elegida ha sido Hibernate. Esto ha introducido algunas dependencias en el proyecto:

  

Estas dependencias se deben a la inclusión de Hibernate en el proyecto. Tenemos que añadir otra dependencia, la del controlador JDBC de MySQL, que implementa la capa JDBC de la arquitectura. Modificamos el archivo [pom.xml] de la siguiente manera:


<dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
    </dependency>    
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>4.1.2</version>
    </dependency>
    ...
    <dependency>
      <groupId>org.hibernate.common</groupId>
      <artifactId>hibernate-commons-annotations</artifactId>
      <version>4.0.1.Final</version>
    </dependency>
  </dependencies>

Las líneas 8-12 añaden la dependencia del controlador JDBC de MySQL.

5.6.1.4. Las entidades JPA


Pregunta: Siguiendo los pasos del ejemplo del apartado 4.4, genere las entidades [Cotisation, Indemnite, Employe].


Notas:

  • las entidades formarán parte de un paquete denominado [jpa],
  • cada entidad tendrá un número de versión,
  • si dos entidades están vinculadas por una relación, solo se creará la relación principal @ManyToOne. La relación inversa @OneToMany no se creará.

5.6.1.5. El código de la clase principal

Incluimos en el proyecto las entidades JPA y [1] desarrolladas anteriormente:

y, a continuación, añadimos [2], la siguiente clase [main.Main]:


package main;

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

public class Main {

  public static void main(String[] args) {
    // basta con crear el Entity Manager para construir la capa JPA
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("mv-pam-jpa-hibernatePU");
    EntityManager em=emf.createEntityManager();
    // liberación de recursos
    em.close();
    emf.close();
  }
}
  • línea 10: creamos la clase EntityManagerFactory de la unidad de persistencia denominada [mv-pam-jpa-hibernatePU]. Este nombre procede del archivo [persistence.xml]:

  <persistence-unit name="mv-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
    ...
  </persistence-unit>
  • línea 12: se crea el archivo EntityManager. Esta creación genera la capa JPA. Se va a procesar el archivo [persistence.xml] y, por lo tanto, se crearán las tablas de la base de datos,
  • líneas 14-15: se liberan los recursos.

5.6.1.6. Tests

Volvamos a la arquitectura de nuestro proyecto:

Se han implementado todas las capas. Ejecutamos el proyecto [2].

Los resultados de la consola son los siguientes:

------------------------------------------------------------------------
Building mv-pam-jpa-hibernate 1.0-SNAPSHOT
------------------------------------------------------------------------

[resources:resources]
[debug] execute contextualize
Using 'UTF-8' encoding to copy filtered resources.
Copying 1 resource

[compiler:compile]
Nothing to compile - all classes are up to date

[exec:exec]
juin 21, 2012 4:22:47 PM org.hibernate.annotations.common.Version <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {4.0.1.Final}
juin 21, 2012 4:22:47 PM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {4.1.2}
juin 21, 2012 4:22:47 PM org.hibernate.cfg.Environment <clinit>
INFO: HHH000206: hibernate.properties not found
juin 21, 2012 4:22:47 PM org.hibernate.cfg.Environment buildBytecodeProvider
INFO: HHH000021: Bytecode provider name : javassist
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000402: Using Hibernate built-in connection pool (not for production use!)
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000115: Hibernate connection pool size: 20
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000006: Autocommit mode: true
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000401: using driver [com.mysql.jdbc.Driver] at URL [jdbc:mysql://localhost:3306/dbpam_hibernate]
juin 21, 2012 4:22:48 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000046: Connection properties: {user=root, autocommit=true, release_mode=auto}
juin 21, 2012 4:22:48 PM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
juin 21, 2012 4:22:48 PM org.hibernate.engine.jdbc.internal.LobCreatorBuilder useContextualLobCreation
INFO: HHH000423: Disabling contextual LOB creation as JDBC driver reported JDBC version [3] less than 4
juin 21, 2012 4:22:48 PM org.hibernate.engine.transaction.internal.TransactionFactoryInitiator initiateService
INFO: HHH000268: Transaction strategy: org.hibernate.engine.transaction.internal.jdbc.JdbcTransactionFactory
juin 21, 2012 4:22:48 PM org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory <init>
INFO: HHH000397: Using ASTQueryTranslatorFactory
juin 21, 2012 4:22:48 PM org.hibernate.tool.hbm2ddl.SchemaExport execute
INFO: HHH000227: Running hbm2ddl schema export
Hibernate: 
    alter table EMPLOYES 
        drop 
        foreign key FK75C8D6BC73F24A67
juin 21, 2012 4:22:48 PM org.hibernate.tool.hbm2ddl.SchemaExport perform
ERROR: HHH000389: Unsuccessful: alter table EMPLOYES drop foreign key FK75C8D6BC73F24A67
juin 21, 2012 4:22:48 PM org.hibernate.tool.hbm2ddl.SchemaExport perform
ERROR: Table 'dbpam_hibernate.employes' doesn't exist
Hibernate: 
    drop table if exists COTISATIONS
Hibernate: 
    drop table if exists EMPLOYES
Hibernate: 
    drop table if exists INDEMNITES
Hibernate: 
    create table COTISATIONS (
        id bigint not null auto_increment,
        CSGD double precision not null,
        CSGRDS double precision not null,
        RETRAITE double precision not null,
        SECU double precision not null,
        VERSION integer not null,
        primary key (id)
    )
Hibernate: 
    create table EMPLOYES (
        id bigint not null auto_increment,
        SS varchar(15) not null unique,
        ADRESSE varchar(50) not null,
        CP varchar(5) not null,
        NOM varchar(30) not null,
        PRENOM varchar(20) not null,
        VERSION integer not null,
        VILLE varchar(30) not null,
        INDEMNITE_ID bigint not null,
        primary key (id)
    )
Hibernate: 
    create table INDEMNITES (
        id bigint not null auto_increment,
        BASE_HEURE double precision not null,
        ENTRETIEN_JOUR double precision not null,
        INDEMNITES_CP double precision not null,
        INDICE integer not null unique,
        REPAS_JOUR double precision not null,
        VERSION integer not null,
        primary key (id)
    )
Hibernate: 
    alter table EMPLOYES 
        add index FK75C8D6BC73F24A67 (INDEMNITE_ID), 
        add constraint FK75C8D6BC73F24A67 
        foreign key (INDEMNITE_ID) 
        references INDEMNITES (id)
juin 21, 2012 4:22:49 PM org.hibernate.tool.hbm2ddl.SchemaExport execute
INFO: HHH000230: Schema export complete
juin 21, 2012 4:22:49 PM org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH000030: Cleaning up connection pool [jdbc:mysql://localhost:3306/dbpam_hibernate]
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 2.637s
Finished at: Thu Jun 21 16:22:49 CEST 2012
Final Memory: 8M/153M

En la consola solo aparecen registros de Hibernate, ya que el programa ejecutado no realiza ninguna otra acción aparte de instanciar la capa JPA. Cabe destacar los siguientes puntos:

  • línea 43: Hibernate intenta eliminar la clave foránea de la tabla [EMPLOYES],
  • líneas 51-55: eliminación de las tres tablas,
  • línea 57: creación de la tabla [COTISATIONS],
  • línea 67: creación de la tabla [EMPLOYES],
  • línea 80: creación de la tabla [INDEMNITES],
  • línea 91: creación de la clave externa de la tabla [EMPLOYES].

En NetBeans, se pueden ver las tablas en la conexión creada anteriormente:

Las tablas creadas dependen tanto de la implementación de la capa JPA utilizada como de la de SGBD. Así, una implementación de JPA / EclipseLink con la misma base de datos puede generar tablas diferentes. Esto es lo que vamos a ver ahora.

Vamos a crear un nuevo proyecto Maven en el siguiente entorno:

Seguiremos los pasos del párrafo anterior:

  1. crear una base de datos MySQL [dbpam_eclipselink]. Utilizaremos el script [dbpam_eclipselink.sql] para generarla,
  2. crearemos el archivo [persistence.xml] del proyecto. Tomaremos la implementación JPA 2.0 EclipseLink,
  3. añadir en las dependencias generadas la dependencia del controlador JDBC de MySQL,
  4. añadir las entidades JPA y el programa de consola,
  5. realizar las pruebas.

El archivo [persistence.xml] tendrá el siguiente contenido:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="pam-jpa-eclipselinkPU" transaction-type="RESOURCE_LOCAL">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <properties>
      <property name="eclipselink.target-database" value="MySQL"/>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbpam_eclipselink"/>
      <property name="javax.persistence.jdbc.password" value=""/>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.user" value="root"/>
      <property name="eclipselink.logging.level" value="FINE"/>
      <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
    </properties>
  </persistence-unit>
</persistence>
  • las propiedades 9-13 han sido generadas por el asistente de NetBeans,
  • línea 14: esta propiedad nos permite establecer el nivel de registro de EclipseLink. El nivel de FINE nos permite conocer las órdenes de SQL que EclipseLink va a enviar a la base de datos,
  • línea 15: al instanciar la capa JPA / EclipseLink, las tablas de las entidades JPA se eliminarán y, a continuación, se volverán a crear.

Los resultados obtenidos en la consola son los siguientes:

------------------------------------------------------------------------
Building mv-pam-jpa-eclipselink 1.0-SNAPSHOT
------------------------------------------------------------------------

[resources:resources]
[debug] execute contextualize
Using 'UTF-8' encoding to copy filtered resources.
Copying 1 resource

[compiler:compile]
Nothing to compile - all classes are up to date

[exec:exec]
[EL Config]: 2012-06-22 14:35:01.852--ServerSession(730572764)--Hilo(Thread[main,5,main])--El tipo de acceso de la clase persistente [class jpa.Cotisation] se ha establecido en [FIELD].
[EL Config]: 2012-06-22 14:35:01.884--ServerSession(730572764)--Hilo(Thread[main,5,main])--El tipo de acceso de la clase persistente [class jpa.Employe] se ha establecido en [FIELD].
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Hilo(Thread[main,5,main])--La clase de entidad de destino (referencia) para el elemento de asignación «muchos a uno» [field indemnite] se está estableciendo por defecto en: clase jpa.Indemnite.
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Hilo (Thread[main,5,main])--El tipo de acceso de la clase persistente [class jpa.Indemnite] está establecido en [FIELD].
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Hilo(Thread[main,5,main])--El nombre de alias de la clase de entidad [class jpa.Cotisation] se establece por defecto en: Cotización.
[EL Config]: 2012-06-22 14:35:01.915--ServerSession(730572764)--Hilo(Thread[main,5,main])--El nombre de la columna del elemento [id] se establece por defecto como: ID.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Hilo(Thread[main,5,main])--El nombre de alias de la clase de entidad [class jpa.Employe] se está estableciendo por defecto como: Employe.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Hilo(Thread[main,5,main])--El nombre de columna para el elemento [id] se establece por defecto en: ID.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Hilo(Thread[main,5,main])--El nombre de alias de la clase de entidad [class jpa.Indemnite] se está estableciendo por defecto como: Indemnite.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Hilo(Thread[main,5,main])--El nombre de columna para el elemento [id] se está estableciendo por defecto como: ID.
[EL Config]: 2012-06-22 14:35:01.962--ServerSession(730572764)--Hilo(Thread[main,5,main])--El nombre de la columna de clave principal para el elemento de mapeo [field indemnite] se está estableciendo por defecto en: ID.
[EL Info]: 2012-06-22 14:35:02.558--ServerSession(730572764)--Hilo(Thread[main,5,main])--EclipseLink, versión: Eclipse Persistence Services - 2.3.0.v20110604-r9504
[EL Config]: 2012-06-22 14:35:02.568--ServerSession(730572764)--Conexión(1543921451)--Hilo(Thread[main,5,main])--conectando(DatabaseLogin(
    platform=>MySQLPlatform
    user name=> "root"
    datasource URL=> "jdbc:mysql://localhost:3306/dbpam_eclipselink"
))
[EL Config]: 2012-06-22 14:35:02.738--ServerSession(730572764)--Conexión(1296716340)--Hilo(Thread[main,5,main])--Conectado: jdbc:mysql://localhost:3306/dbpam_eclipselink
    User: root@localhost
    Database: MySQL  Version: 5.5.20-log
    Driver: MySQL-AB JDBC Driver  Version: mysql-connector-java-5.1.6 ( Revision: ${svn.Revision} )
[EL Info]: 2012-06-22 14:35:02.798--ServerSession(730572764)--Hilo (Thread[main,5,main])--file:/D:/data/istia-1112/netbeans/glassfish/mv-pam/05/mv-pam-jpa-eclipselink/target/classes/_pam-jpa-eclipselinkPU inicio de sesión correcto
[EL Fine]: 2012-06-22 14:35:02.818--ServerSession(730572764)--Conexión(1296716340)--Hilo(Thread[main,5,main])--ALTER TABLE EMPLOYES DROP FOREIGN KEY FK_EMPLOYES_INDEMNITE_ID
[EL Fine]: 2012-06-22 14:35:03.088--ServerSession(730572764)--Conexión(1296716340)--Hilo(Thread[main,5,main])--DROP TABLE COTISATIONS
[EL Fine]: 2012-06-22 14:35:03.118--ServerSession(730572764)--Conexión(1296716340)--Hilo(Thread[main,5,main])--CREATE TABLE COTISATIONS (ID BIGINT NOT NULL, CSGD DOUBLE NOT NULL, CSGRDS DOUBLE NOT NULL, RETRAITE DOUBLE NOT NULL, SECU DOUBLE NOT NULL, VERSION INTEGER NOT NULL, PRIMARY KEY (ID))
[EL Fine]: 2012-06-22 14:35:03.198--ServerSession(730572764)--Conexión(1296716340)--Hilo(Thread[main,5,main])--DROP TABLE EMPLOYES
[EL Fine]: 2012-06-22 14:35:03.238--ServerSession(730572764)--Conexión(1296716340)--Hilo(Thread[main,5,main])--CREATE TABLE EMPLOYES (ID BIGINT NOT NULL, SS VARCHAR(15) NOT NULL UNIQUE, ADRESSE VARCHAR(50) NOT NULL, CP VARCHAR(5) NOT NULL, NOM VARCHAR(30) NOT NULL, PRENOM VARCHAR(20) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(30) NOT NULL, INDEMNITE_ID BIGINT NOT NULL, PRIMARY KEY (ID))
[EL Fine]: 2012-06-22 14:35:03.318--ServerSession(730572764)--Conexión(1296716340)--Hilo(Thread[main,5,main])--DROP TABLE INDEMNITES
[EL Fine]: 2012-06-22 14:35:03.338--ServerSession(730572764)--Conexión(1296716340)--Hilo(Thread[main,5,main])--CREATE TABLE INDEMNITES (ID BIGINT NOT NULL, BASE_HEURE DOUBLE NOT NULL, ENTRETIEN_JOUR DOUBLE NOT NULL, INDEMNITES_CP DOUBLE NOT NULL, INDICE INTEGER NOT NULL UNIQUE, REPAS_JOUR DOUBLE NOT NULL, VERSION INTEGER NOT NULL, PRIMARY KEY (ID))
[EL Fine]: 2012-06-22 14:35:03.418--ServerSession(730572764)--Conexión(1296716340)--Hilo(Thread[main,5,main])--ALTER TABLE EMPLOYES ADD CONSTRAINT FK_EMPLOYES_INDEMNITE_ID FOREIGN KEY (INDEMNITE_ID) REFERENCES INDEMNITES (ID)
[EL Fine]: 2012-06-22 14:35:03.568--ServerSession(730572764)--Conexión(1296716340)--Hilo(Thread[main,5,main])--CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
[EL Fine]: 2012-06-22 14:35:03.578--ServerSession(730572764)--Hilo(Hilo[main,5,main])--SELECT 1
[EL Warning]: 2012-06-22 14:35:03.578--ServerSession(730572764)--Hilo(Hilo[main,5,main])--Excepción [EclipseLink-4002] (Eclipse Persistence Services - 2.3.0.v20110604-r9504): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'sequence' already exists
Error Code: 1050
Call: CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
Query: DataModifyQuery(sql="CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))")
[EL Fine]: 2012-06-22 14:35:03.578--ServerSession(730572764)--Conexión(1296716340)--Hilo(Thread[main,5,main])--DELETE FROM SEQUENCE WHERE SEQ_NAME = SEQ_GEN
[EL Fine]: 2012-06-22 14:35:03.638--ServerSession(730572764)--Conexión(1296716340)--Hilo(Thread[main,5,main])--SELECT * FROM SEQUENCE WHERE SEQ_NAME = SEQ_GEN
[EL Fine]: 2012-06-22 14:35:03.638--ServerSession(730572764)--Conexión(1296716340)--Hilo(Thread[main,5,main])--INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) valores (SEQ_GEN, 0)
[EL Config]: 2012-06-22 14:35:03.748--ServerSession(730572764)--Conexión (1296716340)--Hilo (Thread[main,5,main])--desconexión
[EL Info]: 2012-06-22 14:35:03.748--ServerSession(730572764)--Hilo (Thread[main,5,main])--archivo:/D:/data/istia-1112/netbeans/glassfish/mv-pam/05/mv-pam-jpa-eclipselink/target/classes/_pam-jpa-eclipselinkPU Cierre de sesión realizado con éxito
[EL Config]: 2012-06-22 14:35:03.748--ServerSession(730572764)--Conexión(1543921451)--Hilo(Thread[main,5,main])--desconexión
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 3.503s
Finished at: Fri Jun 22 14:35:03 CEST 2012
Final Memory: 8M/153M
  • líneas 26-30: conexión a la base de datos MySQL,
  • líneas 31-34: confirmación de que la conexión se ha realizado correctamente,
  • línea 36: eliminación de la clave externa de la tabla [EMPLOYES],
  • línea 37: eliminación de la tabla [COTISATIONS],
  • línea 38: creación de la tabla [COTISATIONS]. Cabe destacar que la clave primaria ID no tiene el atributo MySQL auto_increment. Esto significa que no es MySQL la que genera los valores de la clave primaria,
  • línea 39: eliminación de la tabla [EMPLOYES],
  • línea 40: creación de la tabla [EMPLOYES]. Su clave primaria ID no tiene el atributo MySQL auto_increment,
  • línea 41: eliminación de la tabla [INDEMNITES],
  • línea 42: creación de la tabla [INDEMNITES]. Su clave primaria ID no tiene el atributo MySQL auto_increment,
  • línea 43: creación de la clave externa de la tabla [EMPLOYES] hacia la tabla [INDEMNITES],
  • línea 44: creación de una tabla [SEQUENCE]. Se utilizará para generar las claves primarias de las tres tablas anteriores,
  • línea 47: se produce una excepción porque esta tabla ya existía,
  • líneas 51-53: inicialización de la tabla [SEQUENCE].

La existencia de las tablas generadas se puede comprobar en NetBeans [1]:

Por lo tanto, a partir de las mismas entidades JPA, las implementaciones JPA, Hibernate y EclipseLink no generan las mismas tablas. En el resto del documento, cuando la implementación JPA utilizada sea:

  • Hibernate, se utilizará la base de datos [dbpam_hibernate];
  • EclipseLink, se utilizará la base de datos [dbpam_eclipselink].

5.6.3. Tarea a realizar

Siguiendo el mismo procedimiento que antes,

  1. crea y prueba un proyecto [mv-pam-jpa-hibernate-oracle] utilizando una implementación JPA de Hibernate y una SGBD de Oracle,
  2. crear y probar un proyecto [mv-pam-jpa-hibernate-mssql] utilizando una implementación JPA de Hibernate y un servidor SGBD SQL,
  3. crear y probar un proyecto [mv-pam-jpa-eclipselink-oracle] utilizando una implementación JPA EclipseLink y un servidor Oracle SGBD,
  4. crear y probar un proyecto [mv-pam-jpa-eclipselink-mssql] utilizando una implementación JPA, EclipseLink y un servidor SGBD, SQL,

5.6.4. ¿Lazy o Eager?

Volvamos a una posible definición de la entidad [Employe]:


package jpa;

...

@Entity
@Table(name="EMPLOYES")
public class Employe implements Serializable {
  
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;
  @Version
  @Column(name="VERSION",nullable=false)
  private int version;
  @Column(name="SS", nullable=false, unique=true, length=15)
  private String SS;
  @Column(name="NOM", nullable=false, length=30)
  private String nom;
  @Column(name="PRENOM", nullable=false, length=20)
  private String prenom;
  @Column(name="ADRESSE", nullable=false, length=50)
  private String adresse;
  @Column(name="VILLE", nullable=false, length=30)
  private String ville;
  @Column(name="CP", nullable=false, length=5)
  private String codePostal;
  @ManyToOne(fetch= FetchType.LAZY)
  @JoinColumn(name="INDEMNITE_ID",nullable=false)
  private Indemnite indemnite;
  ...
}

Las líneas 27-29 definen la clave externa de la tabla [EMPLOYES] hacia la tabla [INDEMNITES]. El atributo «fetch» de la línea 27 define la estrategia de búsqueda del campo indemnite de la línea 29. Existen dos modos:

  • FetchType.LAZY: cuando se busca a un empleado, no se recupera la indemnización que le corresponde. Esta se recuperará cuando se haga referencia por primera vez al campo [Employe].indemnite.
  • FetchType.EAGER: cuando se busca a un empleado, se muestra la indemnización que le corresponde. Este es el modo por defecto cuando no se especifica ninguno.

Para comprender la utilidad de la opción FetchType.LAZY, podemos tomar el siguiente ejemplo. En una página web se muestra una lista de empleados sin las indemnizaciones mediante un enlace [Details]. Al hacer clic en este enlace, se muestran las indemnizaciones del empleado seleccionado. Se observa que:

  • para mostrar la primera página no se necesita la información de los empleados con sus prestaciones. En ese caso, el modo FetchType.LAZY es el adecuado;
  • para mostrar la segunda página con los detalles, hay que realizar una consulta adicional a la base de datos para obtener las prestaciones del empleado seleccionado.

El modo FetchType.LAZY evita recuperar demasiados datos que la aplicación no necesita de inmediato. Veamos un ejemplo.

El proyecto [mv-pam-jpa-hibernate] se duplica:

  • en [1], se copia el proyecto,
  • en [2], se indica la carpeta de la copia y en [3], su nombre,
  • en [4], el nuevo proyecto tiene el mismo nombre que el anterior. Lo cambiamos así:
  • en [1], cambiamos el nombre del proyecto,
  • a [2], renombramos el proyecto y su artifactId,
  • en [3], el nuevo proyecto.

Modificamos el programa [Main.java] de la siguiente manera:


package main;

import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import jpa.Employe;

public class Main {

  // la consulta JPQL que aparece a continuación devuelve un empleado
  // la clave externa [Employe].indemnite se encuentra en FetchType.LAZY
  public static void main(String[] args) {
    // basta con crear el Entity Manager para construir la capa JPA
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("pam-jpa-hibernatePU");
    // primer intento
    EntityManager em = emf.createEntityManager();
    Employe employe = (Employe) em.createQuery("select e from Employe e where e.nom=:nom").setParameter("nom", "Jouveinal").getSingleResult();
    em.close();
    // se muestra el empleado
    try {
      System.out.println(employe);
    } catch (Exception ex) {
      System.out.println(ex);
    }
    // segunda prueba
    em = emf.createEntityManager();
    employe = (Employe) em.createQuery("select e from Employe e left join fetch e.indemnite where e.nom=:nom").setParameter("nom", "Jouveinal").getSingleResult();
    // liberar los recursos
    em.close();
    // se muestra el empleado
    try {
      System.out.println(employe);
    } catch (Exception ex) {
      System.out.println(ex);
    }
    // liberación de recursos
    emf.close();
  }
}
  • línea 15: creamos el EntityManagerFactory a partir de la capa JPA,
  • línea 17: se obtiene el EntityManager, que nos permite interactuar con la capa JPA,
  • línea 18: se solicita el empleado con el nombre Jouveinal,
  • línea 19: se cierra el EntityManager. Esto tiene como efecto cerrar el contexto de persistencia.
  • línea 22: se muestra el empleado recibido.

La clase [Employe] es la siguiente:


package jpa;

...

@Entity
@Table(name="EMPLOYES")
public class Employe implements Serializable {
  
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;
  @Version
  @Column(name="VERSION",nullable=false)
  private int version;
  @Column(name="SS", nullable=false, unique=true, length=15)
  private String SS;
  @Column(name="NOM", nullable=false, length=30)
  private String nom;
  @Column(name="PRENOM", nullable=false, length=20)
  private String prenom;
  @Column(name="ADRESSE", nullable=false, length=50)
  private String adresse;
  @Column(name="VILLE", nullable=false, length=30)
  private String ville;
  @Column(name="CP", nullable=false, length=5)
  private String codePostal;
  @ManyToOne(fetch= FetchType.LAZY)
  @JoinColumn(name="INDEMNITE_ID",nullable=false)
  private Indemnite indemnite;
  
  
  /**
   * Returns a string representation of the object.  This implementation constructs
   * that representation based on the id fields.
   * @return a string representation of the object.
   */
  @Override
  public String toString() {
    return "jpa.Employe[id=" + getId()
    + ",version="+getVersion()
    +",SS="+getSS()
    + ",nom="+getNom()
    + ",prenom="+getPrenom()
    + ",adresse="+getAdresse()
    +",ville="+getVille()
    +",code postal="+getCodePostal()
    +",indice="+getIndemnite().getIndice()
    +"]";
  }
  ...
}
  • línea 27: el campo indemnite se vuelve a poner en modo LAZY,
  • línea 47: utiliza el campo indemnite. Si se invoca el método toString cuando el campo indemnite aún no se ha restablecido, se restablecerá en ese momento. Salvo que el contexto de persistencia se haya cerrado, como en el ejemplo.

Volvamos al código de [Main]:

  • líneas 21-25: debería producirse una excepción. De hecho, se va a llamar al método toString. Este utilizará el campo indemnite. Se buscará dicho campo. Como se ha cerrado el contexto de persistencia, la entidad [Employe] recuperada ya no existe, de ahí la excepción.
  • línea 27: se crea un nuevo EntityManager,
  • línea 28: se solicita el empleado Jouveinal, indicando explícitamente en la consulta JPQL la indemnización correspondiente. Esta solicitud explícita es necesaria porque el modo de búsqueda de dicha indemnización es LAZY,
  • línea 30: se cierra el EntityManager,
  • líneas 32-36: se vuelve a mostrar al empleado. No debería producirse ninguna excepción.

Para ejecutar el proyecto, se necesita una base de datos completa. Se creará siguiendo los pasos del apartado 5.5. Por otra parte, hay que modificar el archivo [persistence.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="mv-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <properties>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbpam_hibernate"/>
      <property name="javax.persistence.jdbc.password" value=""/>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.user" value="root"/>
      <property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
    </properties>
  </persistence-unit>
</persistence>
  • se ha eliminado la opción que creaba las tablas. La base de datos ya existe y está completa,
  • y se han eliminado las opciones que hacían que Hibernate registrara las órdenes SQL que enviaba a la base de datos.

Al ejecutar el proyecto, aparecen los dos mensajes siguientes en la consola:

org.hibernate.LazyInitializationException: could not initialize proxy - no Session
jpa.Employe[id=31,version=0,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2]
  • línea 1: la excepción que se produjo al intentar buscar la indemnización que faltaba, ya que la sesión estaba cerrada. Se observa que la indemnización no se había recuperado debido al modo LAZY,
  • línea 2: el empleado con su indemnización obtenida mediante una consulta que eludió el modo LAZY.

5.6.5. Tarea a realizar

Siguiendo un procedimiento similar al que acabamos de seguir, crea un proyecto [mv-pam-pa-eclipselink-lazy] que muestre el comportamiento de EclipseLink frente al modo LAZY.

Se obtienen los siguientes resultados:

jpa.Employe[id=453,version=1,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2]
jpa.Employe[id=453,version=1,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2]

En el modo LAZY, ambas consultas han devuelto la indemnización junto con el empleado. Al buscar información en Internet sobre esta anomalía, se descubre que la anotación [FetchType.LAZY] (línea 1):


  @ManyToOne(fetch= FetchType.LAZY)
  @JoinColumn(name="INDEMNITE_ID",nullable=false)
private Indemnite indemnite;

no es una orden, sino una sugerencia. El implementador JPA no está obligado a seguirla. Así pues, se observa que el código depende en ocasiones de la implementación JPA utilizada. Es posible configurar EclipseLink para que se comporte según lo esperado en el modo LAZY.

5.6.6. A continuación

La arquitectura de la aplicación que se va a desarrollar es la siguiente:

En el resto del documento, se duplicará el proyecto Maven [mv-pam-jpa-hibernate] en el proyecto [mv-pam-spring-hibernate] [1, 2, 3]:

  • y, a continuación, renombraremos el nuevo proyecto como [4, 5, 6].

Se modificarán las dependencias del nuevo proyecto. El archivo [pom.xml] quedará así:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>istia.st</groupId>
  <artifactId>mv-pam-spring-hibernate</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>mv-pam-spring-hibernate</name>
  <url>http://maven.apache.org</url>
  <repositories>
    <repository>
      <url>http://repo1.maven.org/maven2/</url>
      <id>swing-layout</id>
      <layout>default</layout>
      <name>Repository for library Library[swing-layout]</name>
    </repository>
  </repositories>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
      <scope>test</scope>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>commons-dbcp</groupId>
      <artifactId>commons-dbcp</artifactId>
      <version>1.2.2</version>
    </dependency>
    <dependency>
      <groupId>commons-pool</groupId>
      <artifactId>commons-pool</artifactId>
      <version>1.6</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>4.1.2</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
    </dependency>
    <dependency>
      <groupId>org.swinglabs</groupId>
      <artifactId>swing-layout</artifactId>
      <version>1.0.3</version>
    </dependency>
  </dependencies>
</project>
  • líneas 25-31: la dependencia para las pruebas JUnit,
  • líneas 32-41: las dependencias del pool de conexiones de Apache DBCP,
  • líneas 42-65: las dependencias del framework Spring,
  • líneas 67-71: las dependencias para la implementación JPA / Hibernate,
  • líneas 72-76: la dependencia del controlador JDBC de MySQL,
  • líneas 77-81: la dependencia de la interfaz Swing. NetBeans la añade automáticamente al añadir una interfaz Swing al proyecto.

Además, se generarán las dos bases MySQL:

  • [dbpam_hibernate] a partir del script [dbpam_hibernate.sql],
  • [dbpam_eclipselink] a partir del script [dbpam_eclipselink.sql],

5.7. Las interfaces de « » de las capas [metier] y [DAO]

Volvamos a la arquitectura de la aplicación:

En la arquitectura anterior, ¿qué interfaz debe ofrecer la capa [DAO] a la capa [metier] y qué interfaz debe ofrecer la capa [metier] a la capa [ui]? Un primer enfoque para definir las interfaces de las diferentes capas consiste en examinar los distintos casos de uso (use cases) de la aplicación. En este caso tenemos dos, según la interfaz de usuario elegida: consola o formulario gráfico.

Analicemos el modo de uso de la aplicación de consola:

dos>java -jar pam-spring-ui-metier-dao-jpa-eclipselink.jar 254104940426058 150 20

Valeurs saisies :
N° de sécurité sociale de l'employé : 254104940426058
Nombre d'heures travaillées : 150
Nombre de jours travaillés : 20

Informations Employé :
Nom : Jouveinal
...

Informations Cotisations :
CSGRDS : 3.49 %
...

Informations Indemnités :
...

Informations Salaire :
Salaire de base : 362.25 euro
Cotisations sociales : 97.48 euro
Indemnités d'entretien : 42.0 euro
Indemnités de repas : 62.0 euro
Salaire net : 368.77 euro

La aplicación recibe tres datos del usuario (véase la línea 1 anterior)

  • el número de la Seguridad Social de la cuidadora
  • el número de horas trabajadas en el mes
  • el número de días trabajados en el mes

A partir de estos datos y de otros almacenados en archivos de configuración, la aplicación muestra la siguiente información:

  • líneas 4-6: los valores introducidos
  • líneas 8-10: la información relacionada con el empleado cuyo número de la Seguridad Social se ha facilitado
  • líneas 12-14: los tipos de las diferentes cotizaciones sociales
  • líneas 16-17: las diferentes prestaciones abonadas a la cuidadora infantil
  • líneas 19-24: los datos de la nómina de la cuidadora infantil

La capa [metier] debe proporcionar una serie de datos a la capa [ui]:

  1. la información relativa a una cuidadora infantil identificada por su número de la Seguridad Social. Esta información se encuentra en la tabla [EMPLOYES]. Esto permite mostrar las líneas 6-8.
  2. los importes de los distintos tipos de cotizaciones sociales que deben deducirse del salario bruto. Esta información se encuentra en la tabla [COTISATIONS]. Esto permite mostrar las líneas 10-12.
  3. los importes de las distintas prestaciones relacionadas con la función de cuidadora infantil. Esta información se encuentra en la tabla [INDEMNITES]. Esto permite mostrar las líneas 14 y 15.
  4. los componentes del salario que se muestran en las líneas 18-22.

A partir de esto, se podría decidir realizar un primer asiento de la interfaz [IMetier], presentada por la capa [metier] a la capa [ui]:

1
2
3
4
5
6
package metier;

public interface IMetier {
   // obtener la nómina
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
}
  • línea 1: los elementos de la capa [metier] se incluyen en el paquete [metier]
  • línea 5: el método [ calculerFeuilleSalaire ] toma como parámetros los tres datos obtenidos por la capa [ui] y devuelve un objeto de tipo [FeuilleSalaire] que contiene la información que la capa [ui] mostrará en la consola. La clase [FeuilleSalaire] podría ser la siguiente:
package metier;

import jpa.Cotisation;
import jpa.Employe;
import jpa.Indemnite;

public class FeuilleSalaire {
   // campos privados
  private Employe employe;
  private Cotisation cotisation;
  private ElementsSalaire elementsSalaire;

  ...
}
  • línea 9: el empleado al que se refiere la nómina —información n.º 1 mostrada por la capa [ui]
  • línea 10: los distintos tipos de cotización —información n.º 2 mostrada por la capa [ui]
  • línea 11: las diferentes indemnizaciones vinculadas al índice del empleado —información n.º 3 mostrada por la capa [ui]
  • línea 12: los componentes de su salario —información n.º 4 mostrada por la capa [ui]

Aparece un segundo caso de uso de la capa [métier] con la interfaz gráfica:

Como se ve arriba, la lista desplegable [1, 2] muestra a todos los empleados. Esta lista debe solicitarse a la capa [métier]. La inter ace de esta evoluciona entonces de la siguiente manera:

package metier;

import java.util.List;
import jpa.Employe;

public interface IMetier {
   // obtener la nómina
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
   // lista de empleados
  public List<Employe> findAllEmployes();
}
  • línea [10]: el método que permitirá a la capa [ui] solicitar la lista de todos los empleados a la capa [métier].

La capa [metier] solo puede inicializar los campos [Employe, Cotisation, Indemnite] del objeto [FeuilleSalaire] anterior consultando la capa [DAO], ya que esta información se encuentra en las tablas de la base de datos. Lo mismo ocurre a la hora de obtener la lista de todos los empleados. Se podría crear una única interfaz [DAO] que gestionara el acceso a las tres entidades [Employe, Cotisation, Indemnite]. Sin embargo, en este caso hemos decidido crear una interfaz [DAO] por cada entidad.

La interfaz [DAO] para acceder a las entidades [Cotisation] de la tabla [COTISATIONS] será la siguiente:

package dao;

import java.util.List;
import jpa.Cotisation;

public interface ICotisationDao {
       // crear una nueva cotización
  public Cotisation create(Cotisation cotisation);
       // modificar una cotización existente
  public Cotisation edit(Cotisation cotisation);
       // eliminar una cotización existente
  public void destroy(Cotisation cotisation);
       // buscar una cotización concreta
  public Cotisation find(Long id);
       // obtener todos los objetos de cotización
  public List<Cotisation> findAll();

}
  • En la línea 6, la interfaz [ICotisationDao] gestiona el acceso a la entidad [Cotisation] y, por lo tanto, a la tabla [COTISATIONS] de la base de datos. Nuestra aplicación solo necesita el método [findAll] de la línea 16, que permite recuperar todo el contenido de la tabla [COTISATIONS]. En este caso, hemos querido plantearnos un caso más general en el que todas las operaciones CRUD (Create, Read, Update, Delete) se realizan sobre la entidad.
  • línea 8: el método [create] crea una nueva entidad [Cotisation]
  • línea 10: el método [edit] modifica una entidad [Cotisation] ya existente
  • línea 12: el método [destroy] elimina una entidad [Cotisation] existente
  • línea 14: el método [find] permite recuperar una entidad [Cotisation] existente mediante su identificador id
  • línea 16: el método [findAll] devuelve en una lista todas las entidades [Cotisation] existentes

Analicemos la firma del método [create]:

       // crear una nueva cuota
Cotisation create(Cotisation cotisation);

El método create tiene un parámetro cotisation de tipo Cotisation. El parámetro cotisation debe persistir, c.a.d. que se introduce aquí en la tabla [COTISATIONS]. Antes de esta persistencia, el parámetro cotisation tiene un identificador id sin valor. Tras la persistencia, el campo id tiene un valor que es la clave primaria del registro añadido a la tabla [COTISATIONS]. Por lo tanto, el parámetro cotisation es un parámetro de entrada/salida del método create. No parece necesario que el método create devuelva además el parámetro cotisation como resultado. Dado que el método que realiza la llamada tiene una referencia al objeto [Cotisation cotisation], si este se modifica, tendrá acceso al objeto modificado, ya que tiene una referencia sobre él. Por lo tanto, puede conocer el valor que el método create ha asignado al campo id del objeto [Cotisation cotisation]. Así pues, la firma del método podría ser más sencilla:

       // crear una nueva cuota
void create(Cotisation cotisation);

Al escribir una interfaz, conviene recordar que puede utilizarse en dos contextos diferentes: local y distant. En el contexto local, el método llamante y el método llamado se ejecutan en el mismo JVM:

Si la capa [metier] invoca el método create de la capa [DAO], sí que tiene una referencia al parámetro [Cotisation cotisation] que pasa al método.

En el contexto distant, el método llamante y el método llamado se ejecutan en diferentes JVM:

En el ejemplo anterior, la capa [metier] se ejecuta en la JVM 1 y la capa [DAO] en la JVM 2, en dos máquinas diferentes. Las dos capas no se comunican directamente. Entre ellas se intercala una capa que denominaremos capa de comunicación [1]. Esta se compone de una capa de emisión [2] y una capa de recepción [3]. Por lo general, el desarrollador no tiene que escribir estas capas de comunicación. Se generan automáticamente mediante herramientas de software. La capa [metier] se escribe como si se ejecutara en la misma JVM que la capa [DAO]. Por lo tanto, no hay ninguna modificación del código.

El mecanismo de comunicación entre la capa [metier] y la capa [DAO] es el siguiente:

  • la capa [metier] invoca el método create de la capa [DAO] pasándole el parámetro [Cotisation cotisation1]
  • Este parámetro se pasa, de hecho, a la capa de emisión [2]. Esta transmitirá por la red el valor del parámetro cotisation1 y no su referencia. La forma exacta de este valor depende del protocolo de comunicación utilizado.
  • La capa de recepción [3] recuperará este valor y, a partir de él, reconstruirá un objeto [Cotisation cotisation2] que sea una réplica del parámetro inicial enviado por la capa [metier]. Ahora tenemos dos objetos idénticos (en cuanto al contenido) en dos JVM diferentes: cotisation1 y cotisation2.
  • La capa de recepción pasará el objeto cotisation2 al método create de la capa [DAO], que lo almacenará en la base de datos. Tras esta operación, el campo id del objeto cotisation2 se ha inicializado con la clave primaria del registro añadido a la tabla [COTISATIONS]. No es el caso del objeto cotisation1, al que hace referencia la capa [metier]. Si queremos que la capa [metier] tenga una referencia al objeto cotisation2, hay que enviársela. Por ello, hay que modificar la firma del método create de la capa [DAO]:
       // crear una nueva cuota
Cotisation create(Cotisation cotisation);
  • Con esta nueva firma, el método create devolverá como resultado el objeto persistente cotisation2. Este resultado se devuelve a la capa de recepción [3], que había llamado a la capa [DAO]. Esta, a su vez, devolverá el valor (y no la referencia) de cotisation2 a la capa de emisión [2].
  • La capa de emisión [2] recuperará este valor y, a partir de él, reconstruirá un objeto [Cotisation cotisation3] que representa el resultado devuelto por el método create de la capa [DAO].
  • El objeto [Cotisation cotisation3] se devuelve al método de la capa [metier], cuya llamada al método create de la capa [DAO] había iniciado todo este mecanismo. Por lo tanto, la capa [metier] puede conocer el valor de la clave primaria asignada al objeto [Cotisation cotisation1], cuya persistencia había solicitado: se trata del valor del campo id de cotisation3.

La arquitectura anterior no es la más habitual. Es más frecuente encontrar las capas [metier] y [DAO] dentro de la misma JVM:

En esta arquitectura, son los métodos de la capa [metier] los que deben devolver resultados, y no los de la capa [DAO]. No obstante, la siguiente firma del método create de la capa [DAO]:

       // crear una nueva cuota
Cotisation create(Cotisation cotisation);

nos permite no hacer suposiciones sobre la arquitectura realmente implementada. Utilizar firmas que funcionen independientemente de la arquitectura elegida, ya sea local o remota, implica que, en caso de que un método invocado modifique algunos de sus parámetros:

  • estos también deben formar parte del resultado del método invocado
  • el método llamante debe utilizar el resultado del método invocado y no las referencias de los parámetros modificados que haya transmitido al método invocado.

De este modo, nos reservamos la posibilidad de pasar de una arquitectura locale a una arquitectura distante sin necesidad de modificar el código. Reexaminemos, a la luz de esto, la interfaz [ICotisationDao]:

package dao;

import java.util.List;
import jpa.Cotisation;

public interface ICotisationDao {
       // crear una nueva cuota
  public Cotisation create(Cotisation cotisation);
       // modificar una cuota existente
  public Cotisation edit(Cotisation cotisation);
       // eliminar una cuota existente
  public void destroy(Cotisation cotisation);
       // buscar una cuota concreta
  public Cotisation find(Long id);
       // obtener todos los objetos de «Cotización»
  public List<Cotisation> findAll();

}
  • línea 8: se ha tratado el caso del método create
  • línea 10: el método edit utiliza su parámetro [Cotisation cotisation1] para actualizar el registro de la tabla [COTISATIONS] que tiene la misma clave primaria que el objeto cotisation. Devuelve como resultado el objeto cotisation2, que es una imagen del registro modificado. El parámetro cotisation1, por su parte, no se modifica. El método debe devolver cotisation2 como resultado, tanto si se trata de una arquitectura distante como de una locale.
  • Línea 12: el método destroy elimina el registro de la tabla [COTISATIONS] que tiene la misma clave primaria que el objeto cotisation pasado como parámetro. Este último no se modifica. Por lo tanto, no es necesario devolverlo.
  • línea 14: el parámetro id del método find no es modificado por el método. No tiene que formar parte del resultado.
  • línea 16: el método findAll no tiene parámetros. Por lo tanto, no es necesario analizarlo.

En definitiva, solo la firma del método create debe adaptarse para que pueda utilizarse en el marco de una arquitectura distante. Los razonamientos anteriores serán válidos para las demás interfaces [DAO]. No las repetiremos y utilizaremos directamente firmas que se puedan emplear tanto en el marco de una arquitectura distante como en el de una arquitectura locale.

La interfaz [DAO] para acceder a las entidades [Indemnite] de la tabla [INDEMNITES] será la siguiente:

package dao;

import java.util.List;
import jpa.Indemnite;

public interface IIndemniteDao {
     // crear una entidad «Indemnización»
  public Indemnite create(Indemnite indemnite);
     // modificar una entidad «Indemnización»
  public Indemnite edit(Indemnite indemnite);
     // eliminar una entidad «Indemnización»
  public void destroy(Indemnite indemnite);
     // buscar una entidad «Indemnización» mediante su identificador
  public Indemnite find(Long id);
     // obtener todas las entidades Indemnite
  public List<Indemnite> findAll();

}
  • En la línea 6, la interfaz [IIndemniteDao] gestiona los accesos a la entidad [Indemnite] y, por lo tanto, a la tabla [INDEMNITES] de la base de datos. Nuestra aplicación solo necesita el método [findAll] de la línea 16, que permite recuperar todo el contenido de la tabla [INDEMNITES]. En este caso, hemos querido plantearnos un caso más general en el que todas las operaciones CRUD (Create, Read, Update, Delete) se realizan sobre la entidad.
  • línea 8: el método [create] crea una nueva entidad [Indemnite]
  • línea 10: el método [edit] modifica una entidad [Indemnite] ya existente
  • línea 12: el método [destroy] elimina una entidad [Indemnite] existente
  • línea 14: el método [find] permite recuperar una entidad [Indemnite] existente mediante su identificador id
  • línea 16: el método [findAll] devuelve en una lista todas las entidades [Indemnite] existentes

La interfaz [DAO] para acceder a las entidades [Employe] de la tabla [EMPLOYES] será la siguiente:

package dao;

import java.util.List;
import jpa.Employe;

public interface IEmployeDao {
     // crear una nueva entidad «Empleado»
  public Employe create(Employe employe);
     // modificar una entidad «Empleado» existente
  public Employe edit(Employe employe);
     // eliminar una entidad «Empleado»
  public void destroy(Employe employe);
     // buscar una entidad «Empleado» por su identificador id
  public Employe find(Long id);
     // buscar una entidad «Empleado» mediante su n.º SS
  public Employe find(String SS);
     // obtener todas las entidades «Empleado»
  public List<Employe> findAll();
}
  • En la línea 6, la interfaz [IEmployeDao] gestiona el acceso a la entidad [Employe] y, por lo tanto, a la tabla [EMPLOYES] de la base de datos. Nuestra aplicación solo necesita el método [findAll] de la línea 16, que permite recuperar todo el contenido de la tabla [EMPLOYES]. En este caso, hemos querido plantearnos un caso más general en el que todas las operaciones CRUD (Create, Read, Update, Delete) se realizan sobre la entidad.
  • línea 8: el método [create] crea una nueva entidad [Employe]
  • línea 10: el método [edit] modifica una entidad [Employe] ya existente
  • línea 12: el método [destroy] elimina una entidad [Employe] existente
  • línea 14: el método [find] permite recuperar una entidad [Employe] existente mediante su identificador id
  • línea 16: el método [find(String SS)] permite recuperar una entidad [Employe] existente mediante su n.º SS. Hemos visto que este método era necesario para la aplicación de consola.
  • línea 18: el método [findAll] devuelve en una lista todas las entidades [Employe] existentes. Ya hemos visto que este método era necesario para la aplicación gráfica.

5.8. La clase [PamException]

La capa [DAO] va a trabajar con la clase API de Java. Esta clase API lanza excepciones controladas de tipo [SQLException] que presentan dos inconvenientes:

  • pesan el código, que debe gestionar obligatoriamente estas excepciones mediante try/catch.
  • deben declararse en la firma de los métodos de la interfaz [IDao] mediante un «throws SQLException». Esto tiene como consecuencia que no se pueda implementar esta interfaz mediante clases que lancen una excepción controlada de un tipo distinto de [SQLException].

Para solucionar este problema, la capa [DAO] solo «elevará» excepciones no controladas de tipo [PamException].

  • la capa [JDBC] lanza excepciones de tipo [SQLException]
  • la capa [JPA] lanza excepciones propias de la implementación JPA utilizada
  • la capa [DAO] lanza excepciones de tipo [PamException] no controladas

Esto tiene dos consecuencias:

  • la capa [metier] no tendrá la obligación de gestionar las excepciones de la capa [DAO] mediante try/catch. Podrá simplemente dejar que se propaguen hasta la capa [ui].
  • los métodos de la interfaz [IDao] no tienen que incluir en su firma el tipo de excepción [PamException], lo que permite implementar esta interfaz con clases que lanzarían otro tipo de excepción no controlada.

La clase [PamException] se colocará en el paquete [exception] del proyecto NetBeans:

Su código es el siguiente:

package exception;

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

   // código de error
  private int code;

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

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

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

  public PamException(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: [PamException] deriva de [RuntimeException]. Por lo tanto, se trata de un tipo de excepción que el compilador no nos obliga a gestionar mediante un try/catch ni a incluir en la firma de los métodos. Por este motivo, [PamException] no aparece en la firma de los métodos de la interfaz [IDao]. Esto permite que esta interfaz sea implementada por una clase que lance otro tipo de excepciones, siempre que esta también derive de [RuntimeException].
  • Para diferenciar los errores que pueden producirse, 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 ha añadido un parámetro: el del código de error que se quiere asignar a la excepción.

El funcionamiento de la aplicación, desde el punto de vista de las excepciones, será el siguiente:

  • la capa [DAO] encapsulará cualquier excepción que se produzca en una excepción de tipo [PamException] y la reenviará a la capa [métier].
  • La capa [métier] permitirá que se propaguen las excepciones lanzadas por la capa [DAO]. Encapsulará cualquier excepción que se produzca en la capa [métier] en una excepción de tipo [PamException] y la reenviará a la capa [ui].
  • La capa [ui] intercepta todas las excepciones que se transmiten desde las capas [métier] y [DAO]. Se limitará a mostrar la excepción en la consola o en la interfaz gráfica.

Veamos ahora sucesivamente la implementación de las capas [DAO] y [metier].

5.9. La capa [DAO] de la aplicación [PAM]

Nos situamos en el contexto de la siguiente arquitectura:

5.9.1. Implementación

Lecturas recomendadas: apartado 3.1.3 de [ref1]


Pregunta: Utilizando la integración Spring / JPA, escriba las clases [CotisationDao, IndemniteDao, EmployeDao] para implementar las interfaces [ICotisationDao, IIndemniteDao, IEmployeDao]. Cada método de clase interceptará cualquier excepción que se produzca y la encapsulará en una excepción de tipo [PamException] con un código de error específico para la excepción interceptada.


Las clases de implementación formarán parte del paquete [dao]:

  

5.9.2. Configuración

Lecturas recomendadas: apartado 3.1.5 de [ref1]

La integración DAO / JPA se configura mediante el archivo Spring [spring-config-dao.xml] y los archivos JPA y [persistence.xml]:


Pregunta: escribe el contenido de estos dos archivos. Se supone que la base de datos utilizada es la base MySQL5 [dbpam_hibernate] generada por el script SQL [dbpam_hibernate.sql]. El archivo Spring definirá los tres beans siguientes: employeDao de tipo EmployeDao, indemniteDao de tipo IndemniteDao, cotisationDao de tipo CotisationDao. Por otra parte, la implementación JPA utilizada será Hibernate.


5.9.3. Pruebas

Lecturas recomendadas: apartados 3.1.6 y 3.1.7 de [ref1]

Ahora que la capa [DAO] está escrita y configurada, podemos probarla. La arquitectura de las pruebas será la siguiente:

5.9.4. InitDB

Vamos a crear dos programas de pruebas para la capa [DAO]. Estos se colocarán en el paquete [dao] [2] de la rama [Test Packages] [1] del proyecto NetBeans. Esta rama no se incluye en el proyecto generado por la opción [Build project], lo que nos garantiza que los programas de prueba que coloquemos en ella no se incluirán en el archivo .jar final del proyecto.

Las clases colocadas en la rama [Test Packages] tienen acceso a las clases presentes en la rama [Source Packages], así como a las bibliotecas de clases del proyecto. Si las pruebas necesitan bibliotecas distintas a las del proyecto, estas deben declararse en la rama [Test Libraries] [2].

Las clases de prueba utilizan la herramienta de pruebas unitarias JUnit:

  • [JUnitInitDB] no realiza ninguna prueba. Rellena la base de datos con algunos registros y, a continuación, los muestra en la consola.
  • [JUnitDao] realiza una serie de pruebas y comprueba sus resultados.

La estructura básica de la clase [JUnitInitDB] es la siguiente:

package dao;

...

public class JUnitInitDB {

  private IEmployeDao employeDao = null;
  private ICotisationDao cotisationDao = null;
  private IIndemniteDao indemniteDao = null;

  @BeforeClass
  public void init(){
     // Configuración de la aplicación
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-dao.xml");
     // capas DAO
    employeDao = (IEmployeDao) ctx.getBean("employeDao");
    cotisationDao = (ICotisationDao) ctx.getBean("cotisationDao");
    indemniteDao = (IIndemniteDao) ctx.getBean("indemniteDao");
  }

  @Test
  public void initDB(){
     // se rellena la base de datos
...
     // se muestra el contenido de la base de datos
...
  }

  @Before()
  public void clean(){
     // vaciado de la base de datos
...
  }
}
  • El método [init] se ejecuta antes de que comience la serie de pruebas (anotación @BeforeClass). Instancia la capa [DAO].
  • El método [clean] se ejecuta antes de cada prueba (anotación @Before). Vacía la base de datos.
  • El método [initDB] es una prueba (anotación @Test). Es la única. Una prueba debe contener instrucciones de afirmación Assert.assertCondition. En este caso, no habrá ninguna. Por lo tanto, el método es una prueba ficticia. Su función es rellenar la base de datos con algunas líneas y, a continuación, mostrar el contenido de la base en la consola. Para ello se utilizan los métodos create y findAll de las capas [DAO].

Pregunta: completa el código de la clase [JUnitInitDB]. Utiliza como referencia el ejemplo del apartado 3.1.6 de [ref1]. El código generará el contenido que se muestra en el apartado 5.1.


5.9.5. Puesta en mar re de las pruebas

Ya estamos listos para ejecutar [InitDB]. Describimos el procedimiento con los programas SGBD y MySQL5:

  • se han configurado las clases [1], los archivos de configuración [2] y las clases de prueba de la capa [DAO] y [3],
  • se compila el proyecto [4]
  • se ejecuta la clase [JUnitInitDB] [5]. Se inicia SGBD MySQL5 con una base de datos [dbpam_hibernate] ya existente,
  • la ventana de [Test Results] y [6] indica que las pruebas se han superado. Este mensaje no es relevante en este caso, ya que el programa [JUnitInitDB] no contiene ninguna instrucción de afirmación Assert.assertCondition que pudiera provocar el fallo de la prueba. No obstante, esto demuestra que no se ha producido ninguna excepción durante la ejecución de la prueba.

La ventana [Output] contiene los registros de la ejecución, los de Spring y los de la propia prueba. Las salidas generadas por la clase [JUnitInitDB] son las siguientes:

------------- Standard Output ---------------
Employés ----------------------
jpa.Employe[id=5,version=0,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2]
jpa.Employe[id=6,version=0,SS=260124402111742,nom=Laverti,prenom=Justine,adresse=La brûlerie,ville=St Marcel,code postal=49014,indice=1]
Indemnités ----------------------
jpa.Indemnite[id=5,version=0,indice=1,base heure=1.93,entretien jour2.0,repas jour=3.0,indemnités CP=12.0]
jpa.Indemnite[id=6,version=0,indice=2,base heure=2.1,entretien jour2.1,repas jour=3.1,indemnités CP=15.0]
Cotisations ----------------------
jpa.Cotisation[id=3,version=0,csgrds=3.49,csgd=6.15,secu=9.39,retraite=7.88]
------------- ---------------- ---------------

Las tablas [EMPLOYES, INDEMNITES, COTISATIONS] se han rellenado. Esto se puede comprobar mediante una conexión de NetBeans a la base de datos [dbpam_hibernate].

  • en [1], en la pestaña [services], se visualizan los datos de la tabla [employes] de la conexión [dbpam_hibernate] [2],
  • en [3], el resultado.

5.9.6. JUnitDao

Ahora nos centramos en una segunda clase de pruebas, [JUnitDao]:

La estructura básica de la clase será la siguiente:

package dao;

import exception.PamException;
...

public class JUnitDao {

// capas DAO
  static private IEmployeDao employeDao;
  static private IIndemniteDao indemniteDao;
  static private ICotisationDao cotisationDao;

  @BeforeClass
  public static void init() {
     // registro
    log("init");
     // configuración de la aplicación
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-dao.xml");
     // capas DAO
    employeDao = (IEmployeDao) ctx.getBean("employeDao");
    indemniteDao = (IIndemniteDao) ctx.getBean("indemniteDao");
    cotisationDao = (ICotisationDao) ctx.getBean("cotisationDao");
  }

  @AfterClass
  public static void terminate() {
  }

  @Before()
  public void clean() {
...
  }

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

   // pruebas
  @Test
  public void test01() {
    log("test01");
     // lista de cuotas
    List<Cotisation> cotisations = cotisationDao.findAll();
    int nbCotisations = cotisations.size();
     // se añade una cuota
    Cotisation cotisation = cotisationDao.create(new Cotisation(3.49, 6.15, 9.39, 7.88));
     // se solicita
    cotisation = cotisationDao.find(cotisation.getId());
     // verificación
    Assert.assertNotNull(cotisation);
    Assert.assertEquals(3.49, cotisation.getCsgrds(), 1e-6);
    Assert.assertEquals(6.15, cotisation.getCsgd(), 1e-6);
    Assert.assertEquals(9.39, cotisation.getSecu(), 1e-6);
    Assert.assertEquals(7.88, cotisation.getRetraite(), 1e-6);
     // se modifica
    cotisation.setCsgrds(-1);
    cotisation.setCsgd(-1);
    cotisation.setRetraite(-1);
    cotisation.setSecu(-1);
    Cotisation cotisation2 = cotisationDao.edit(cotisation);
     // comprobaciones
    Assert.assertEquals(cotisation.getVersion() + 1, cotisation2.getVersion());
    Assert.assertEquals(-1, cotisation2.getCsgrds(), 1e-6);
    Assert.assertEquals(-1, cotisation2.getCsgd(), 1e-6);
    Assert.assertEquals(-1, cotisation2.getRetraite(), 1e-6);
    Assert.assertEquals(-1, cotisation2.getSecu(), 1e-6);
     // se solicita el elemento modificado
    Cotisation cotisation3 = cotisationDao.find(cotisation2.getId());
     // comprobaciones
    Assert.assertEquals(cotisation3.getVersion(), cotisation2.getVersion());
    Assert.assertEquals(-1, cotisation3.getCsgrds(), 1e-6);
    Assert.assertEquals(-1, cotisation3.getCsgd(), 1e-6);
    Assert.assertEquals(-1, cotisation3.getRetraite(), 1e-6);
    Assert.assertEquals(-1, cotisation3.getSecu(), 1e-6);
     // se elimina el elemento
    cotisationDao.destroy(cotisation3);
     // comprobaciones
    Cotisation cotisation4 = cotisationDao.find(cotisation3.getId());
    Assert.assertNull(cotisation4);
    cotisations = cotisationDao.findAll();
    Assert.assertEquals(nbCotisations, cotisations.size());
  }


  @Test
  public void test02(){
    log("test02");
     // se solicita la lista de indemnizaciones
...
     // se añade una indemnización
..
     // se busca la indemnización en la base de datos – se recupera la indemnización1
..
     // se comprueba que indemnización1 = indemnización
...
     // se modifica la indemnización obtenida y se guarda el cambio en BD. Se obtiene la indemnización2
 ...
     // se comprueba la versión de indemnización2
    ...
     // se recupera «indemnite2» de la base de datos; se obtiene «indemnite3»
    ...
     // se comprueba que indemnite3 = indemnite2
    ...
     // se elimina de la base de datos la imagen de indemnite3
    ...
     // se busca «indemnite3» en la base de datos
    ...
     // se comprueba que se ha obtenido una referencia nula
 ...
  }

  @Test
  public void test03(){
    log("test03");
     // se repite una prueba similar a las anteriores para «Empleado»
 ...
  }

  @Test
  public void test04(){
    log("test04");
     // se prueba el método [IEmployeDao].find(String SS)
     // primero con un empleado existente
     // y, a continuación, con un empleado que no existe
...
  }

  @Test
  public void test05(){
    log("test05");
     // creamos dos indemnizaciones con el mismo índice
     // incumple la restricción de unicidad del índice
     // se comprueba que se produce una excepción de tipo PamException
     // y que tiene el número de error esperado
...
  }

  @Test
  public void test06(){
    log("test06");
     // se crean dos empleados con el mismo número SS
     //, lo que incumple la restricción de unicidad del número SS
     // se comprueba que se produce una excepción de tipo PamException
     // y que tiene el número de error esperado
...

  }

  @Test
  public void test07(){
    log("test07");
     // se crean dos empleados con el mismo n.º SS, el primero con «create» y el segundo con «edit»
     // incumple la restricción de unicidad sobre el n.º SS
     //: se comprueba que se produce una excepción de tipo PamException
     // y que tiene el número de error esperado
...
  }

  @Test
  public void test08(){
    log("test08");
     // eliminar a un empleado que no existe no provoca ninguna excepción
     // se añade y luego se elimina; lo comprobamos
...
  }

  @Test
  public void test09(){
    log("test09");
     // modificar un empleado sin tener la versión correcta debe provocar una excepción
     // se comprueba
...
  }

  @Test
  public void test10(){
    log("test10");
     // Eliminar a un empleado sin tener la versión correcta debe provocar una excepción
     // lo comprobamos
...

  }

   // getter y setter
  ...
}

En la clase de pruebas anterior, la base de datos se vacía antes de cada prueba.


Pregunta: escribe los siguientes métodos:

1 - test02: nos basaremos en test01

2 - test03: un empleado tiene un campo de tipo Indemnite. Por lo tanto, hay que crear una entidad Indemnite y una entidad Employe

3 - test04.


Siguiendo el mismo procedimiento que para la clase de pruebas [JUnitInitDB], se obtienen los siguientes resultados:

  • en [1], se ejecuta la clase de pruebas
  • en [2], los resultados de las pruebas en la ventana [Test Results]

Provocamos un error para ver cómo se muestra en la página de resultados:

  @Test
  public void test01() {
    log("test01");
     // lista de cotizaciones
    List<Cotisation> cotisations = cotisationDao.findAll();
    int nbCotisations = cotisations.size();
     // Se añade una cotización
    Cotisation cotisation = cotisationDao.create(new Cotisation(3.49, 6.15, 9.39, 7.88));
     // se solicita
    cotisation = cotisationDao.find(cotisation.getId());
     // verificación
    Assert.assertNotNull(cotisation);
    Assert.assertEquals(0, cotisation.getCsgrds(), 1e-6);
    Assert.assertEquals(6.15, cotisation.getCsgd(), 1e-6);
    Assert.assertEquals(9.39, cotisation.getSecu(), 1e-6);
    Assert.assertEquals(7.88, cotisation.getRetraite(), 1e-6);
     // modificación
....
}

Línea 13: la afirmación provocará un error, ya que el valor de Csgrds es 3,49 (línea 8). La ejecución de la clase de pruebas ofrece los siguientes resultados:

  • La página de resultados [1] muestra ahora que ha habido pruebas fallidas.
  • En [2], aparece un resumen de la excepción que ha provocado el fallo de la prueba. En él se indica el número de línea del código Java en el que se produjo la excepción.

5.10. La capa [metier] de la aplicación [PAM]

Ahora que ya se ha escrito la capa [DAO], pasamos al análisis de la capa de negocio [2]:

5.10.1. La interfaz Java [IMetier]

Esta se describió en el apartado 5.7. A continuación la recordamos:

package metier;

import java.util.List;
import jpa.Employe;

public interface IMetier {
   // obtener la nómina
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
   // lista de empleados
  public List<Employe> findAllEmployes();
}

La implementación de la capa [metier] se realizará en un paquete [metier]:

 

El paquete [metier] incluirá, además de la interfaz [IMetier] y su implementación [Metier], otras dos clases: [FeuilleSalaire] y [ElementsSalaire]. La clase [FeuilleSalaire] se presentó brevemente en el apartado 5.7. Volvemos a ella ahora.

5.10.2. La clase [FeuilleSalaire]

El método [calculerFeuilleSalaire] de la interfaz [IMetier] devuelve un objeto de tipo [FeuilleSalaire] que representa los distintos elementos de una nómina. Su definición es la siguiente:

package metier;

import jpa.Cotisation;
import jpa.Employe;
import jpa.Indemnite;

public class FeuilleSalaire implements Serializable{
   // campos privados
  private Employe employe;
  private Cotisation cotisation;
  private ElementsSalaire elementsSalaire;

   // constructores
  public FeuilleSalaire() {

  }

  public FeuilleSalaire(Employe employe, Cotisation cotisation, ElementsSalaire elementsSalaire) {
    setEmploye(employe);
    setCotisation(cotisation);
    setElementsSalaire(elementsSalaire);
  }

   // toString
  public String toString() {
    return "[" + employe + "," + cotisation + "," + elementsSalaire + "]";
  }

   // accesores
...  
}
  • línea 7: la clase implementa la interfaz Serializable porque sus instancias pueden intercambiarse a través de la red.
  • línea 9: el empleado al que se refiere la nómina
  • línea 10: las diferentes tasas de cotización
  • línea 11: las distintas indemnizaciones vinculadas al índice del empleado
  • línea 12: los componentes de su salario
  • líneas 14-22: los dos constructores de la clase
  • líneas 25-27: método [toString] que identifica un objeto [FeuilleSalaire] concreto
  • líneas 29 y siguientes: los accesores públicos a los campos privados de la clase

La clase [ElementsSalaire], a la que se hace referencia en la línea 11 de la clase [FeuilleSalaire] anterior, reúne los elementos que componen una nómina. Su definición es la siguiente:

package metier;

public class ElementsSalaire implements Serializable{

   // campos privados
  private double salaireBase;
  private double cotisationsSociales;
  private double indemnitesEntretien;
  private double indemnitesRepas;
  private double salaireNet;

   // constructores
  public ElementsSalaire() {

  }

  public ElementsSalaire(double salaireBase, double cotisationsSociales,
    double indemnitesEntretien, double indemnitesRepas,
    double salaireNet) {
    setSalaireBase(salaireBase);
    setCotisationsSociales(cotisationsSociales);
    setIndemnitesEntretien(indemnitesEntretien);
    setIndemnitesRepas(indemnitesRepas);
  }

   // toString
  public String toString() {
    return "[salaire base=" + salaireBase + ",cotisations sociales=" + cotisationsSociales + ",indemnités d'entretien="
      + indemnitesEntretien + ",indemnités de repas=" + indemnitesRepas + ",salaire net="
      + salaireNet + "]";
  }

   // accesores públicos
...  
}
  • línea 3: la clase implementa la interfaz Serializable porque es un componente de la clase FeuilleSalaire, que debe ser serializable.
  • línea 6: el salario base
  • línea 7: las cotizaciones sociales pagadas sobre este salario base
  • línea 8: las prestaciones diarias por manutención del hijo
  • línea 9: las indemnizaciones diarias por comida del niño
  • línea 10: el salario neto a pagar a la cuidadora infantil
  • líneas 12-24: los constructores de la clase
  • líneas 27-31: método [toString] que identifica un objeto concreto [ElementsSalaire]
  • líneas 34 y siguientes: los accesores públicos a los campos privados de la clase

5.10.3. La clase de implementación [Metier] de la capa [metier]

La clase de implementación [Metier] de la capa [metier] podría ser la siguiente:

package metier;

...

@Transactional
public class Metier implements IMetier {

   // referencia en la capa [DAO]
  private ICotisationDao cotisationDao = null;
  private IEmployeDao employeDao=null;


   // obtener la nómina
  public FeuilleSalaire calculerFeuilleSalaire(String SS,
    double nbHeuresTravaillées, int nbJoursTravaillés) {
...
  }

   // lista de empleados
   public List<Employe> findAllEmployes() {
     ...
  }

   // getter y setter
...
 }
  • línea 5: la anotación de Spring @Transactional hace que cada método de la clase se ejecute dentro de una transacción.
  • líneas 9-10: las referencias a las capas [DAO] de las entidades [Cotisation, Employe, Indemnite]
  • líneas 14-17: el método [calculerFeuilleSalaire]
  • líneas 20-22: el método [findAllEmployes]
  • línea 24 y siguientes: los accesores públicos de los campos privados de la clase

Pregunta: escribe el código del método [findAllEmployes].



Pregunta: escribe el código del método [calculerFeuilleSalaire].


Cabe señalar lo siguiente:

  • el método de cálculo del salario se ha explicado en el apartado 5.2.
  • Si el parámetro [SS] no se corresponde con ningún empleado (la capa [DAO] ha devuelto un puntero null), el método lanzará una excepción de tipo [PamException] con el código de error correspondiente.

5.10.4. Pruebas de la capa [metier]

Creamos dos programas de prueba:

Las clases de prueba [3] se crean en el paquete [metier] [2] de la rama [Test Packages] [1] del proyecto.

La clase [JUnitMetier_1] podría ser la siguiente:

package metier;

...

public class JUnitMetier_1 {

// capa de negocio
  private IMetier metier;

  @BeforeClass
  public void init(){
     // registro
    log("init");
     // Configuración de la aplicación
     // instanciación de la capa [metier]
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-metier-dao.xml");
    metier = (IMetier) ctx.getBean("metier");
     // capas DAO
    IEmployeDao employeDao=(IEmployeDao) ctx.getBean("employeDao");
    ICotisationDao cotisationDao=(ICotisationDao) ctx.getBean("cotisationDao");
    IIndemniteDao indemniteDao=(IIndemniteDao) ctx.getBean("indemniteDao");
     // se vacía la base de datos
    for(Employe employe:employeDao.findAll()){
      employeDao.destroy(employe);
    }
    for(Cotisation cotisation:cotisationDao.findAll()){
      cotisationDao.destroy(cotisation);
    }
    for(Indemnite indemnite : indemniteDao.findAll()){
      indemniteDao.destroy(indemnite);
    }
     // se rellena
    Indemnite indemnite1=indemniteDao.create(new Indemnite(1,1.93,2,3,12));
    Indemnite indemnite2=indemniteDao.create(new Indemnite(2,2.1,2.1,3.1,15));
    Employe employe2=employeDao.create(new Employe("254104940426058","Jouveinal","Marie","5 rue des oiseaux","St Corentin","49203",indemnite2));
    Employe employe1=employeDao.create(new Employe("260124402111742","Laverti","Justine","La brûlerie","St Marcel","49014",indemnite1));
    Cotisation cotisation1=cotisationDao.create(new Cotisation(3.49,6.15,9.39,7.88));
  }

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

   // prueba
  @Test
  public void test01(){
     // cálculo de nóminas
    System.out.println(metier.calculerFeuilleSalaire("260124402111742",30, 5));
    System.out.println(metier.calculerFeuilleSalaire("254104940426058",150, 20));
    try {
      System.out.println(metier.calculerFeuilleSalaire("xx", 150, 20));
    } catch (PamException ex) {
      System.err.println(String.format("PamException[Code=%d, message=%s]",ex.getCode(), ex.getMessage()));
    }
  }
}

No hay ninguna afirmación Assert.assertCondition en la clase. Simplemente se trata de calcular algunos salarios para luego verificarlos manualmente. La salida en pantalla obtenida al ejecutar la clase anterior es la siguiente:

1
2
3
4
5
6
7
Testsuite: metier.JUnitMetier_1
----------- init
....
[jpa.Employe[id=22,version=0,SS=260124402111742,nom=Laverti,prenom=Justine,adresse=La brûlerie,ville=St Marcel,code postal=49014,indice=1],jpa.Cotisation[id=6,version=0,csgrds=3.49,csgd=6.15,secu=9.39,retraite=7.88],jpa.Indemnite[id=29,version=0,indice=1,base heure=1.93,entretien jour2.0,repas jour=3.0,indemnités CP=12.0],[salaire base=64.85,cotisations sociales=17.45,indemnités d'entretien=10.0,indemnités de repas=15.0,salaire net=72.4]]
[jpa.Employe[id=21,version=0,SS=254104940426058,nom=Jouveinal,prenom=Marie,adresse=5 rue des oiseaux,ville=St Corentin,code postal=49203,indice=2],jpa.Cotisation[id=6,version=0,csgrds=3.49,csgd=6.15,secu=9.39,retraite=7.88],jpa.Indemnite[id=30,version=0,indice=2,base heure=2.1,entretien jour2.1,repas jour=3.1,indemnités CP=15.0],[salaire base=362.25,cotisations sociales=97.48,indemnités d'entretien=42.0,indemnités de repas=62.0,salaire net=368.77]]
PamException[Code=101, message=L'employé de n°[xx] est introuvable]
Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 3,234 sec
  • línea 4: la nómina de Justine Laverti
  • línea 5: la nómina de Marie Jouveinal
  • línea 6: la excepción debida a que el empleado con el n.º SS «xx» no existe.

Pregunta: la línea 17 de [JUnitMetier_1] utiliza el bean de Spring denominado metier. Indique la definición de este bean en el archivo [spring-config-metier-dao.xml].


La clase [JUnitMetier_2] podría ser la siguiente:

package metier;

...
public class JUnitMetier_2 {

// capa de negocio
  private IMetier metier;

  @BeforeClass
  public void init(){
...
  }

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

   // prueba
  @Test
  public void test01(){
...
  }
}

La clase [JUnitMetier_2] es una copia de la clase [JUnitMetier_1], en la que, en esta ocasión, se han incluido aserciones en el método test01.


Pregunta: escribe el método test01.


Al ejecutar la clase [JUnitMetier_2], si todo va bien, se obtienen los siguientes resultados:

Image

5.11. La capa [ui] de la aplicación [PAM] – versión console

Ahora que ya se ha escrito la capa [metier], nos queda por escribir la capa [ui] [1]:

Crearemos dos implementaciones diferentes de la capa [ui]: una versión console y una versión gráfica swing:

5.11.1. La clase [ui.console.Main]

En primer lugar, nos centramos en la aplicación de consola implementada por la clase [ui.console.Main] mencionada anteriormente. Su funcionamiento se ha descrito en el apartado 5.3. El esqueleto de la clase [Main] podría ser el siguiente:

package ui.console;

import exception.PamException;
import metier.FeuilleSalaire;
import metier.IMetier;

import java.util.ArrayList;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

  /**
   * @param args
   */
  public static void main(String[] args) {
     // datos locales
    final String syntaxe = "pg num_securite_sociale nb_heures_travaillées nb_jours_travaillés";
     // se comprueba el número de parámetros
...
     // lista de errores
    ArrayList erreurs = new ArrayList();
     // el segundo parámetro debe ser un número real >0
...
     // ¿Error?
    if (...) {
      erreurs.add("Le nombre d'heures travaillées [" + args[1]
        + "] est erroné");
    }
     // el tercer parámetro debe ser un número entero mayor que 0
...
     // ¿Error?
    if (...) {
      erreurs.add("Le nombre de jours travaillés [" + args[2]
        + "] est erroné");
    }
     // ¿Hay errores?
    if (erreurs.size() != 0) {
      for (int i = 0; i < erreurs.size(); i++) {
        System.err.println(erreurs.get(i));
      }
      return;
    }
     // Todo correcto: ya se puede solicitar la nómina
    FeuilleSalaire feuilleSalaire = null;
    try {
       // instanciación de la capa [metier]
      ...
       // cálculo de la nómina
      ...
    } catch (PamException ex) {
      System.err.println("L'erreur suivante s'est produite : "+ ex.getMessage());
      return;
    } catch (Exception ex) {
      System.err.println("L'erreur suivante s'est produite : "+ ex.toString());
      return;
    }

     // visualización detallada
    String output = "Valeurs saisies :\n";
    output += ajouteInfo("N° de sécurité sociale de l'employé", args[0]);
    output += ajouteInfo("Nombre d'heures travaillées", args[1]);
    output += ajouteInfo("Nombre de jours travaillés", args[2]);
    output += ajouteInfo("\nInformations Employé", "");
    output += ajouteInfo("Nom", feuilleSalaire.getEmploye().getNom());
    output += ajouteInfo("Prénom", feuilleSalaire.getEmploye().getPrenom());
    output += ajouteInfo("Adresse", feuilleSalaire.getEmploye().getAdresse());
    output += ajouteInfo("Ville", feuilleSalaire.getEmploye().getVille());
    output += ajouteInfo("Code Postal", feuilleSalaire.getEmploye().getCodePostal());
    output += ajouteInfo("Indice", ""+ feuilleSalaire.getEmploye().getIndemnite().getIndice());
    output += ajouteInfo("\nInformations Cotisations", "");
    output += ajouteInfo("CSGRDS", ""+ feuilleSalaire.getCotisation().getCsgrds() + " %");
    output += ajouteInfo("CSGD", ""+ feuilleSalaire.getCotisation().getCsgd() + " %");
    output += ajouteInfo("Retraite", ""+ feuilleSalaire.getCotisation().getRetraite() + " %");
    output += ajouteInfo("Sécurité sociale", ""+ feuilleSalaire.getCotisation().getSecu() + " %");
    output += ajouteInfo("\nInformations Indemnités", "");
    output += ajouteInfo("Salaire horaire", ""+ feuilleSalaire.getEmploye().getIndemnite().getBaseHeure() + " euro");
    output += ajouteInfo("Entretien/jour", ""+ feuilleSalaire.getEmploye().getIndemnite().getEntretienJour() + " euro");
    output += ajouteInfo("Repas/jour", ""+ feuilleSalaire.getEmploye().getIndemnite().getRepasJour() + " euro");
    output += ajouteInfo("Congés Payés", ""+ feuilleSalaire.getEmploye().getIndemnite().getIndemnitesCP()+ " %");
    output += ajouteInfo("\nInformations Salaire", "");
    output += ajouteInfo("Salaire de base", ""+ feuilleSalaire.getElementsSalaire().getSalaireBase()+ " euro");
    output += ajouteInfo("Cotisations sociales", ""+ feuilleSalaire.getElementsSalaire().getCotisationsSociales()+ " euro");
    output += ajouteInfo("Indemnités d'entretien", ""+ feuilleSalaire.getElementsSalaire().getIndemnitesEntretien()+ " euro");
    output += ajouteInfo("Indemnités de repas", ""+ feuilleSalaire.getElementsSalaire().getIndemnitesRepas()+ " euro");
    output += ajouteInfo("Salaire net", ""+ feuilleSalaire.getElementsSalaire().getSalaireNet() + " euro");

    System.out.println(output);
  }

  static String ajouteInfo(String message, String valeur) {
    return message + " : " + valeur + "\n";
  }
}

Pregunta: completa el código anterior.


5.11.2. Ejecución

Para ejecutar la clase [ui.console.Main], se procederá de la siguiente manera:

  • en [1], seleccionar las propiedades del proyecto,
  • en [2], seleccionar la propiedad [Run] del proyecto,
  • utilizar el botón [3] para designar la clase (denominada clase principal) que se va a ejecutar,
  • seleccionar la clase [4],
  • la clase aparece en [5]. Esta necesita tres argumentos para ejecutarse (n.º SS, número de horas trabajadas, número de días trabajados). Estos argumentos se introducen en [6],
  • una vez hecho esto, se puede ejecutar el proyecto [7]. La configuración anterior hace que sea la clase [ui.console.Main] la que se ejecute.

Los resultados de la ejecución se obtienen en la ventana [output]:

5.12. La capa [ui] de la aplicación [PAM] – versión gráfica

Ahora implementamos la capa [ui] con una interfaz gráfica:

  • en [1], la clase [PamJFrame] de la interfaz gráfica
  • en [2]: la interfaz gráfica

5.12.1. Un tutorial rápido

Para crear la interfaz gráfica, se puede proceder de la siguiente manera:

  • [1]: se crea un nuevo archivo con el botón [1] [New File...]
  • [2]: se selecciona la categoría del archivo [Swing GUI Forms], c.a.d. Formularios gráficos
  • [3]: se selecciona el tipo [JFrame Form], un tipo de formulario vacío
  • [5]: se le da un nombre al formulario, que también será una clase
  • [6]: se coloca el formulario en un paquete
  • [8]: el formulario se añade al árbol del proyecto
  • [9]: se puede acceder al formulario desde dos perspectivas: [Design] y [9], que permiten diseñar los distintos componentes del formulario, y [Source] y [10 ci-dessous], que dan acceso al código Java del formulario. En definitiva, un formulario es una clase Java como cualquier otra. La perspectiva [Design] facilita el diseño del formulario. Cada vez que se añade un componente en el modo [Design], se añade código Java en la perspectiva [Source] para tenerlo en cuenta.
  • [11]: la lista de componentes Swing disponibles para un formulario se encuentra en la ventana [Palette].
  • [12]: la ventana [Inspector] muestra el árbol de componentes del formulario. Los componentes con representación visual se encuentran en la rama [JFrame], y los demás, en la rama [Other Components].
  • en [13], seleccionamos un componente [JLabel] con un simple clic
  • en [14], lo colocamos en el formulario en modo [Design]
  • en [15], definimos las propiedades del JLabel (texto, fuente).
  • en [16], el resultado obtenido.
  • En [17], se solicita la vista previa del formulario
  • en [18], el resultado
  • en [19], la etiqueta [JLabel1] se ha añadido al árbol de componentes de la ventana [Inspector]
  • en [20] y [21]: en la perspectiva [Source] del formulario, se ha añadido código Java para gestionar el JLabel añadido.

Hay disponible un tutorial sobre la creación de formularios con NetBeans en la URL [http://www.netbeans.org/kb/trails/matisse.html].

5.12.2. La interfaz gráfica [PamJFrame]

Crearemos la siguiente interfaz gráfica:

  • en [1], la interfaz gráfica
  • en [2], el árbol de sus componentes: un JLabel y seis contenedores JPanel

JLabel1

JPanel1

JPanel2

JPanel3

JPanel4

JPanel5


Ejercicio práctico: crea la interfaz gráfica anterior con la ayuda del tutorial [http://www.netbeans.org/kb/trails/matisse.html].


5.12.3. Los eventos de la interfaz gráfica

Lecturas recomendadas: capítulo [Interfaces graphiques] de [ref2].

Gestionaremos el clic en el botón [jButtonSalaire]. Para crear el método de gestión de este evento, podemos proceder de la siguiente manera:

Se genera el controlador del clic en el botón [JButtonSalaire]:

1
2
3
    private void jButtonSalaireActionPerformed(java.awt.event.ActionEvent evt) {
       // TODO añade aquí tu código de gestión:
}

También se genera el código Java que asocia el método anterior al clic en el botón [JButtonSalaire]:

1
2
3
4
5
6
    jButtonSalaire.setText("Salaire");
    jButtonSalaire.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButtonSalaireActionPerformed(evt);
      }
});

Las líneas 2 a 5 indican que el clic (evento de tipo ActionPerformed) sobre el botón [jButtonSalaire] (línea 2) debe ser gestionado por el método [jButtonSalaireActionPerformed] (línea 4).

También gestionaremos el evento [caretUpdate] (desplazamiento del cursor de entrada) en el campo de entrada [jTextFieldHT]. Para crear el gestor de este evento, procedemos como anteriormente:

Se genera el controlador del evento [caretUpdate] en el campo de entrada [jTextFieldHT]:

  private void jTextFieldHTCaretUpdate(javax.swing.event.CaretEvent evt) {                                         
 ...
  }

También se genera el código Java que asocia el método anterior al evento [caretUpdate] en el campo de entrada [jTextFieldHT]:

1
2
3
4
5
    jTextFieldHT.addCaretListener(new javax.swing.event.CaretListener() {
      public void caretUpdate(javax.swing.event.CaretEvent evt) {
        jTextFieldHTCaretUpdate(evt);
      }
});

Las líneas 1-4 indican que el evento [caretUpdate] (línea 2) del botón [jTextFieldHT] (línea 1) debe ser gestionado por el método [ jTextFieldHTCaretUpdate] (línea 3).

5.12.4. Inicialización de la interfaz gráfica

Volvamos a la arquitectura de nuestra aplicación:

La capa [ui] necesita una referencia a la capa [metier]. Recordemos cómo se obtuvo esta referencia en la aplicación console:

1
2
3
    // instanciación de la capa [metier]
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-metier-dao.xml");
IMetier metier = (IMetier) ctx.getBean("metier");

El método es el mismo en la aplicación gráfica. Es necesario que, cuando esta se inicialice, la referencia [IMetier metier] de la línea 3 anterior también se inicialice. El código generado para la interfaz gráfica es, por el momento, el siguiente:

package ui.swing;

...
public class PamJFrame extends javax.swing.JFrame {

   /** Crea un nuevo formulario PamJFrame */
  public PamJFrame() {
    initComponents();
  }

   /** Este método se invoca desde el constructor para
   * initialize the form.
   * WARNING: Do NOT modify this code. The content of this method is
   * always regenerated by the Form Editor.
   */
   // <editor-fold defaultstate="collapsed" desc="Código generado">
  private void initComponents() {
...
  }// </editor-fold>

  private void jTextFieldHTCaretUpdate(javax.swing.event.CaretEvent evt) {                                         
 ...
  }                                        

  private void jButtonSalaireActionPerformed(java.awt.event.ActionEvent evt) {                                               
...
  }                                              

  public static void main(String args[]) {
    java.awt.EventQueue.invokeLater(new Runnable() {
      public void run() {
        new PamJFrame().setVisible(true);
      }
    });
  }

   // Declaración de variables: no modificar
  private javax.swing.JButton jButtonSalaire;
...
   // Fin de la declaración de variables

}
  • líneas 29-35: el método estático [main] que inicia la aplicación
  • línea 32: se crea una instancia de la interfaz gráfica [PamJFrame] y se hace visible.
  • líneas 7-9: el constructor de la interfaz gráfica.
  • línea 8: llamada al método [initComponents] definido en la línea 17. Este método se genera automáticamente a partir del trabajo realizado en el modo [Design]. No se debe modificar.
  • línea 21: el método que gestionará el desplazamiento del cursor de entrada en el campo [jTextFieldHT]
  • línea 25: el método que gestionará el clic en el botón [jButtonSalaire]

Para añadir al código anterior nuestras propias inicializaciones, podemos proceder de la siguiente manera:

  /** Crea un nuevo formulario PamJFrame */
  public PamJFrame() {
    initComponents();
    doMyInit();
  }

...

   // Variables de instancia
  private IMetier metier=null;
  private List<Employe> employes=null;
  private String[] employesCombo=null;
  private double heuresTravaillées=0;

   // Inicializaciones propias
  public void doMyInit(){
     // Inicialización del contexto
    try{
       // instanciación de la capa [metier]
...
     // lista de empleados
...
    }catch (PamException ex){
     // el mensaje de la excepción se coloca en [jTextAreaStatus]
...
     // Volver
      return;
    }
     // botón de salario desactivado
...
     // jScrollPane1 oculto
...
     // selector de días trabajados
    jSpinnerJT.setModel(new SpinnerNumberModel(0,0,31,1));
     // lista desplegable de empleados
    employesCombo=new String[employes.size()];
    int i=0;
    for(Employe employe : employes){
      employesCombo[i++]=employe.getPrenom()+" "+employe.getNom();
    }
    jComboBoxEmployes.setModel(new DefaultComboBoxModel(employesCombo));
}
  • Línea 4: se llama a un método propio para realizar nuestras propias inicializaciones. Estas se definen en el código de las líneas 10-42

Pregunta: con la ayuda de los comentarios, completa el código del procedimiento [doMyInit].


5.12.5. Gestores de eventos


Pregunta: escribe el método [jTextFieldHTCaretUpdate]. Este método debe garantizar que, si el dato presente en el campo [jTextFieldHT] no es un número real >=0, el botón [jButtonSalaire] quede inactivo.



Pregunta: escriba el método [jButtonSalaireActionPerformed], que debe mostrar la nómina del empleado seleccionado en [jComboBoxEmployes].


5.12.6. Ejecución de la interfaz gráfica

Para ejecutar la interfaz gráfica, se modificará la configuración [Run] del proyecto:

  • por [1] y se debe introducir la clase de la interfaz gráfica

El proyecto debe estar completo con sus archivos de configuración (persistence.xml, spring-config-metier-dao.xml) y la clase de la interfaz gráfica. Se lanzará el objetivo SGBD antes de ejecutar el proyecto.

Nos interesa la siguiente arquitectura, en la que la capa JPA ahora está implementada por EclipseLink:

5.13.1. El proyecto NetBeans

El nuevo proyecto de NetBeans se obtiene copiando el proyecto anterior:

  • en [1]: tras hacer clic con el botón derecho del ratón sobre el proyecto Hibernate, selecciona Copy
  • con el botón [2], selecciona la carpeta principal del nuevo proyecto. El nombre de la carpeta aparece en [3].
  • En [4], asigna un nombre al nuevo proyecto
  • en [5], el nombre de la carpeta del proyecto
  • en [1], se ha creado el nuevo proyecto. Tiene el mismo nombre que el original,
  • en [2] y [3], se le cambia el nombre a [mv-pam-spring-eclipselink].

El proyecto debe modificarse en dos puntos para adaptarlo a la nueva capa JPA / EclipseLink:

  1. en [4], hay que modificar los archivos de configuración de Spring. En ellos se encuentra, de hecho, la configuración de la capa JPA.
  2. En [5], hay que modificar las bibliotecas del proyecto: las de Hibernate deben sustituirse por las de EclipseLink.

Empecemos por este último punto. El archivo [pom.xml] para el nuevo proyecto será este:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>istia.st</groupId>
  <artifactId>mv-pam-spring-eclipselink</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>mv-pam-spring-eclipselink</name>
  <url>http://maven.apache.org</url>
  <repositories>
    <repository>
      <url>http://repo1.maven.org/maven2/</url>
      <id>swing-layout</id>
      <layout>default</layout>
      <name>Repository for library Library[swing-layout]</name>
    </repository>
    <repository>
      <url>http://download.eclipse.org/rt/eclipselink/maven.repo/</url>
      <id>eclipselink</id>
      <layout>default</layout>
      <name>Repository for library Library[eclipselink]</name>
    </repository>    
  </repositories>
  
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
      <scope>test</scope>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>commons-dbcp</groupId>
      <artifactId>commons-dbcp</artifactId>
      <version>1.2.2</version>
    </dependency>
    <dependency>
      <groupId>commons-pool</groupId>
      <artifactId>commons-pool</artifactId>
      <version>1.6</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>3.1.1.RELEASE</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.eclipse.persistence</groupId>
      <artifactId>eclipselink</artifactId>
      <version>2.3.0</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.persistence</groupId>
      <artifactId>javax.persistence</artifactId>
      <version>2.0.3</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
    </dependency>
    <dependency>
      <groupId>org.swinglabs</groupId>
      <artifactId>swing-layout</artifactId>
      <version>1.0.3</version>
    </dependency>
  </dependencies>
</project>
  • líneas 73-82: las dependencias para la implementación JPA EclipseLink,
  • líneas 19-24: el repositorio de Maven para EclipseLink.

Los archivos de configuración de Spring deben modificarse para indicar que la implementación JPA ha cambiado. En ambos archivos, solo cambia la sección que configura la capa JPA. Por ejemplo, en [spring-config-metier-dao.xml] tenemos:

<?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 -->
   <!- - DAO -->
  <bean id="employeDao" class="dao.EmployeDao" />
  <bean id="indemniteDao" class="dao.IndemniteDao" />
  <bean id="cotisationDao" class="dao.CotisationDao" />
   <!-- negocio -->
  <bean id="metier" class="metier.Metier">
    <property name="employeDao" ref="employeDao"/>
    <property name="indemniteDao" ref="indemniteDao"/>
    <property name="cotisationDao" ref="cotisationDao"/>  
  </bean>

   <!-- configuración 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" />
   <!--
        <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/dbpam_hibernate" />
    <property name="username" value="root" />
<!--
    <property name="password" value="" />
-->
  </bean>
....  
</beans>

Las líneas 19-36 configuran la capa JPA. La implementación JPA utilizada es Hibernate (línea 22). Por otra parte, la base de datos de destino es [dbpam_hibernate] (línea 41).

Para cambiar a una implementación JPA / EclipseLink, las líneas 19-35 anteriores se sustituyen por las líneas siguientes:

   <!-- configuración 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.EclipseLinkJpaVendorAdapter">
        <!--
          <property name="showSql" value="true" />
  -->
        <property name="databasePlatform" value="org.eclipse.persistence.platform.database.MySQLPlatform" />
        <!--
        <property name="generateDdl" value="true" />
        -->
      </bean>
    </property>
    <property name="loadTimeWeaver">
      <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
    </property>
</bean>
  • línea 5: la implementación JPA utilizada es EclipseLink
  • línea 9: la propiedad databasePlatform establece el SGBD de destino, en este caso MySQL
  • línea 11: para generar las tablas de la base de datos cuando se instancia la capa JPA. En este caso, la propiedad aparece entre comentarios.
  • línea 7: para visualizar en la consola las órdenes SQL emitidas por la capa JPA. Aquí, la propiedad está comentada.

Por otra parte, la base de datos de destino pasa a ser [dbpam_eclipselink] (línea 4 a continuación):

1
2
3
4
5
6
7
8
9
<!-- 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/dbpam_eclipselink" />
    <property name="username" value="root" />
<!--
    <property name="password" value="" />
-->
  </bean>

5.13.2. Ejecución de las pruebas

Antes de probar la aplicación completa, conviene comprobar si las pruebas JUnit se superan con la nueva implementación JPA. Antes de realizarlas, comenzaremos por eliminar las tablas de la base de datos. Para ello, en la pestaña [Runtime] de NetBeans, si es necesario, crearemos una conexión a la base de datos dbpam_eclipselink / MySQL5. Una vez conectados a la base de datos dbpam_eclipselink / MySQL5, podremos proceder a la eliminación de las tablas tal y como se muestra a continuación:

  • [1]: antes de la eliminación
  • [2]: después de la eliminación

Una vez hecho esto, se puede ejecutar la primera prueba en la capa [DAO]: InitDB, que rellena la base de datos. Para que la aplicación vuelva a crear las tablas eliminadas anteriormente, hay que asegurarse de que, en la configuración de Spring para JPA / EclipseLink, la línea:

        <property name="generateDdl" value="true" />

exista y no esté comentada.

Compilamos el proyecto (Build) y, a continuación, ejecutamos la prueba [JUnitInitDB] :

  • en [1], se ejecuta la prueba InitDB.
  • En [2], falla. La excepción la lanza Spring y no una prueba que haya fallado.

Causado por: org.springframework.beans.factory.BeanCreationException: Error al crear el bean con el nombre «entityManagerFactory» definido en el recurso de ruta de clases [spring-config-DAO.xml]: Ha fallado la invocación del método init; la excepción anidada es java.lang.IllegalStateException: Para utilizar InstrumentationLoadTimeWeaver, es necesario iniciar el agente Java. Consulte la documentación de Spring.

Spring indica que hay un problema de configuración. El mensaje no es claro. El motivo de la excepción se ha explicado en el apartado 3.1.9 de [ref1]. Para que la configuración de Spring / EclipseLink funcione, JVM, que ejecuta la aplicación, debe iniciarse con un parámetro específico: un agente Java. La forma de este parámetro es la siguiente:

-javaagent:C:\...\spring-agent.jar

[spring-agent.jar] es el agente Java que necesita JVM para gestionar la configuración Spring / EclipseLink.

Al ejecutar un proyecto, es posible pasar argumentos a JVM:

  • en [1], se accede a las propiedades del proyecto
  • en [2], se acceden a las propiedades de Run
  • en [3], se pasa el parámetro -javaagent a JVM

5.13.3. InitDB

Ahora estamos listos para volver a probar [InitDB]. Esta vez, los resultados obtenidos son los siguientes:

  • en [1], la prueba se ha superado
  • en [2], en la pestaña [Services], se actualiza la conexión que tiene NetBeans con la base de datos [dbpam_eclipselink]
  • en [3], se han creado cuatro tablas
  • en [5], se visualiza el contenido de la tabla [employes]
  • en [6], el resultado.

5.13.4. JUnitDao

La ejecución de la clase de pruebas [JUnitDao] puede fallar, aunque con la implementación JPA / Hibernate hubiera tenido éxito. Para entender por qué, analicemos un ejemplo.

El método que se prueba es el siguiente: IndemniteDao.create:

package dao;

...
@Transactional(propagation=Propagation.REQUIRED)
public class IndemniteDao implements IIndemniteDao{

  @PersistenceContext
  private EntityManager em;

   // fabricante
  public IndemniteDao() {
  }

   // crear una indemnización
  public Indemnite create(Indemnite indemnite) {
    try{
      em.persist(indemnite);
    }catch(Throwable th){
      throw new PamException(th,31);
    }
    return indemnite;
  }

...
}
  • líneas 15-22: el método sometido a prueba

El método de prueba es el siguiente:


package dao;

...

public class JUnitDao {

// capas DAO
  static private IEmployeDao employeDao;
  static private IIndemniteDao indemniteDao;
  static private ICotisationDao cotisationDao;

  @BeforeClass
  public static void init() {
    // registro
    log("init");
    // configuración de la aplicación
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-DAO.xml");
    // capas DAO
    employeDao = (IEmployeDao) ctx.getBean("employeDao");
    indemniteDao = (IIndemniteDao) ctx.getBean("indemniteDao");
    cotisationDao = (ICotisationDao) ctx.getBean("cotisationDao");
  }

  @Before()
  public void clean() {
    // vaciado de la base de datos
    for (Employe employe : employeDao.findAll()) {
      employeDao.destroy(employe);
    }
    for (Cotisation cotisation : cotisationDao.findAll()) {
      cotisationDao.destroy(cotisation);
    }
    for (Indemnite indemnite : indemniteDao.findAll()) {
      indemniteDao.destroy(indemnite);
    }
  }

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

  // pruebas
….
  @Test
  public void test05() {
    log("test05");
    // se crean dos indemnizaciones con el mismo índice
    // incumple la restricción de unicidad del índice
    boolean erreur = true;
    Indemnite indemnite1 = null;
    Indemnite indemnite2 = null;
    Throwable th = null;
    try {
      indemnite1 = indemniteDao.create(new Indemnite(1, 1.93, 2, 3, 12));
      indemnite2 = indemniteDao.create(new Indemnite(1, 1.93, 2, 3, 12));
      erreur = false;
    } catch (PamException ex) {
      th = ex;
      // comprobaciones
      Assert.assertEquals(31, ex.getCode());
    } catch (Throwable th1) {
      th = th1;
    }
    // comprobaciones
    Assert.assertTrue(erreur);
    // cadena de excepciones
    System.out.println("Chaîne des exceptions --------------------------------------");
    System.out.println(th.getClass().getName());
    while (th.getCause() != null) {
      th = th.getCause();
      System.out.println(th.getClass().getName());
    }
    // la primera indemnización tuvo que conservarse
    Indemnite indemnite = indemniteDao.find(indemnite1.getId());
    // verificación
    Assert.assertNotNull(indemnite);
    Assert.assertEquals(1, indemnite.getIndice());
    Assert.assertEquals(1.93, indemnite.getBaseHeure(), 1e-6);
    Assert.assertEquals(2, indemnite.getEntretienJour(), 1e-6);
    Assert.assertEquals(3, indemnite.getRepasJour(), 1e-6);
    Assert.assertEquals(12, indemnite.getIndemnitesCP(), 1e-6);
    // la segunda indemnización no se ha conservado
    List<Indemnite> indemnites = indemniteDao.findAll();
    int nbIndemnites = indemnites.size();
    Assert.assertEquals(nbIndemnites, 1);
  }

...
}

Pregunta: explique qué hace la prueba test05 e indique los resultados esperados.


Los resultados obtenidos con una capa JPA / Hibernate son los siguientes:

----------- test05
4 juin 2010 16:45:43 org.hibernate.util.JDBCExceptionReporter logExceptions
ATTENTION: SQL Error: 1062, SQLState: 23000
4 juin 2010 16:45:43 org.hibernate.util.JDBCExceptionReporter logExceptions
GRAVE: Duplicate entry '1' for key 2
Chaîne des exceptions --------------------------------------
exception.PamException
javax.persistence.EntityExistsException
org.hibernate.exception.ConstraintViolationException
com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException

La prueba se supera, c.a.d. Se comprueban las aserciones y no se produce ninguna excepción en el método de prueba.


Pregunta: explica qué ha ocurrido.


Los resultados obtenidos con una capa JPA / EclipseLink son los siguientes:

----------- prueba05
[EL Warning]: 2010-06-04 16:48:26.421--UnitOfWork(749304)--Excepción [EclipseLink-4002] (Eclipse Persistence Services - 2.0.0.v20091127-r5931): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException: Duplicate entry '1' for key 2
Error Code: 1062
Call: INSERT INTO INDEMNITES (ID, ENTRETIEN_JOUR, REPAS_JOUR, INDICE, INDEMNITES_CP, BASE_HEURE, VERSION) VALUES (?, ?, ?, ?, ?, ?, ?)
        bind => [108, 2.0, 3.0, 1, 12.0, 1.93, 1]
Query: InsertObjectQuery(jpa.Indemnite[id=108,version=1,indice=1,base heure=1.93,entretien jour2.0,repas jour=3.0,indemnités CP=12.0])
Chaîne des exceptions --------------------------------------
org.springframework.transaction.TransactionSystemException
javax.persistence.RollbackException
org.eclipse.persistence.exceptions.DatabaseException
com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException

Al igual que anteriormente con Hibernate, la prueba se supera, c.a.d. Las aserciones se verifican y no se produce ninguna excepción que salga del método de prueba.


Pregunta: explica qué ha ocurrido.



Pregunta: a partir de estos dos ejemplos, ¿qué se puede concluir sobre la intercambiabilidad de las implementaciones JPA? ¿Es total en este caso?


5.13.5. Las demás pruebas

Una vez que la capa [DAO] haya sido probada y se considere correcta, se podrá pasar a las pruebas de la capa [metier] y a las del propio proyecto, tanto en su versión de consola como en la gráfica. El cambio de implementación JPA no afecta en absoluto a las capas [metier] y [ui], por lo que, si estas capas funcionaban con Hibernate, funcionarán con EclipseLink, salvo algunas excepciones: el ejemplo anterior muestra, de hecho, que las excepciones lanzadas por las capas [DAO] pueden diferir. Así, en el caso de la prueba, Spring / JPA / Hibernate lanza una excepción de tipo [PamException], una excepción propia de la aplicación [pam], mientras que Spring / JPA / EclipseLink lanza una excepción de tipo [TransactionSystemException], una excepción del marco Spring. Si, en el caso de uso de la prueba, la capa [ui] espera una excepción de tipo [PamException] porque se ha creado con Hibernate, dejará de funcionar cuando se cambie a EclipseLink.

5.13.6. Tarea a realizar


Tarea práctica: volver a realizar las pruebas de las aplicaciones console y swing con diferentes SGBD: MySQL5, Oracle XE y SQL Server.