Skip to content

5. Versión 1: Arquitectura Spring / JPA

Se propone desarrollar una aplicación de consola y una aplicación gráfica que permitan generar 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 a la que en adelante nos referiremos como 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 fila
SS
número de seguro 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: recopila 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: contribución social generalizada + contribución al pago 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

Las tasas de las cotizaciones sociales son independientes del empleado. La tabla anterior solo tiene una fila.

Tabla INDEMNITES: reúne los elementos que permiten 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 una hora de guardia
 
ENTRETIENJOUR
indemnización de 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. Es 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 vinculadas a una cuidadora específica 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, quien trabajó 150 horas durante 20 días en el mes a pagar.

Se toman 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 niñera
se calcula mediante la siguiente fórmula:
[SALAIREBASE]=([TOTALHEURES]
*[BASEHEURE])*(1+
[INDEMNITESCP]/100)
[SALAIREBASE]=
(150*[2.1])*(1+0.15)= 362,25
Se deben deducir ciertas 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 contribuciones 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. Por este concepto, recibe las siguientes indemnizaciones:

[Indemnités]=[TOTALJOURS]
*(ENTRETIENJOUR+REPASJOUR)
[INDEMNITES]=104
En definitiva, el salario neto que se le debe pagar 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 cómo ejecutar 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 recibirá la siguiente información:

  1. número de seguro 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 sobre la empleada cuyo número de seguro social se ha proporcionado
  • líneas 17-20: muestran las tasas de las diferentes cotizaciones
  • líneas 23-26: muestran las prestaciones 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 seguro social incorrecto:


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 ingresa 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. Aquí se parte de la hipótesis de que no hay dos empleados con el mismo nombre y apellido.

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], usamos 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 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:

  • tener una base de datos,
  • tener el 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 prueba.

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 realiza en el archivo [persistence.xml], que configura la capa JPA. Este archivo se puede crear con NetBeans:

  • en la pestaña [services] [1], nos conectamos a la base de datos con 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 probar la conexión,
  • en [7], la conexión se ha realizado con éxito.
  • la conexión aparece en [8] y en [9],
  • en [10], se agrega 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 elige una implementación de Hibernate,
  • en [15], se designa 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 proyecto gestiona las transacciones por sí mismo. 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 luego 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.

Agregaremos 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 le indican a Hibernate que muestre las órdenes SQL que envía a la base de datos. El archivo completo es, por lo tanto, 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>
    <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 fue Hibernate. Esto generó algunas dependencias en el proyecto:

  

Estas dependencias se deben a la inclusión de Hibernate en el proyecto. Necesitamos agregar otra dependencia: el 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 a 12 agregan la dependencia del controlador JDBC de MySQL.

5.6.1.4. Las entidades JPA


Pregunta: Siguiendo el procedimiento del ejemplo del párrafo 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 luego agregamos [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 proviene 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 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 en 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 se encuentran registros de Hibernate, ya que el programa ejecutado no hace nada más que instanciar la capa JPA. Cabe destacar los siguientes puntos:

  • línea 43: Hibernate intenta eliminar la clave externa 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 que se creó anteriormente:

Las tablas creadas dependen tanto de la implementación de la capa JPA utilizada como de la de SGBD. Por lo tanto, una implementación de JPA / EclipseLink con la misma base de datos puede generar tablas diferentes. Eso es lo que veremos 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]. Usaremos el script [dbpam_eclipselink.sql] para generarla,
  2. crearemos el archivo [persistence.xml] del proyecto. Tomaremos la implementación JPA 2.0 EclipseLink,
  3. agregar en las dependencias generadas la dependencia del controlador JDBC de MySQL,
  4. agregar las entidades JPA y el programa de consola,
  5. realizar las pruebas.

El archivo [persistence.xml] quedará 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="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 fueron 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 emitirá a la base de datos,
  • línea 15: al instanciar la capa JPA / EclipseLink, las tablas de las entidades JPA se eliminarán y luego 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 para la clase persistente [class jpa.Cotisation] está configurado como [FIELD].
[EL Config]: 2012-06-22 14:35:01.884--ServerSession(730572764)--Hilo(Hilo[main,5,main])--El tipo de acceso para la clase persistente [class jpa.Employe] está configurado como [FIELD].
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Hilo(Hilo[main,5,main])--La clase de entidad de destino (referencia) para el elemento de mapeo de 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 para la clase persistente [class jpa.Indemnite] está configurado como [FIELD].
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Hilo(Hilo[main,5,main])--El nombre de alias para 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 para el elemento [id] se está estableciendo por defecto como: ID.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Hilo(Hilo[main,5,main])--El nombre de alias para la clase de entidad [class jpa.Employe] se está estableciendo por defecto como: Empleado.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Hilo(Thread[main,5,main])--El nombre de la columna para el elemento [id] se está estableciendo por defecto como: ID.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Hilo(Hilo[main,5,main])--El nombre de alias para 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 la columna para el elemento [id] se está estableciendo por defecto como: ID.
[EL Config]: 2012-06-22 14:35:01.962--ServerSession(730572764)--Hilo(Hilo[main,5,main])--El nombre de la columna de clave primaria para el elemento de mapeo [field indemnite] se está estableciendo por defecto como: 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 exitoso
[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(Hilo[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(Hilo[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(Hilo[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 exitoso
[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 realizó con éxito,
  • 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 verificar 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 de 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 a 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 predeterminado cuando no se especifica ningún modo.

Para entender la utilidad de la opción FetchType.LAZY, podemos tomar el siguiente ejemplo. En una página web se presenta 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 adecuado;
  • para mostrar la segunda página con los detalles, se debe realizar una consulta adicional a la base de datos para obtener las prestaciones del empleado seleccionado.

El modo FetchType.LAZY evita traer 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. Modificamos esto:
  • en [1], renombramos el proyecto,
  • a [2], renombramos el proyecto y su artifactId,
  • a [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 se muestra 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);
    }
    // segundo intento
    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: obtenemos el EntityManager, que nos permite interactuar con la capa JPA,
  • línea 18: se solicita el empleado con 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 ha sido restablecido, se restablecerá en ese momento. A menos 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 llamará al método toString. Este utilizará el campo indemnite. Se buscará dicho campo. Como el contexto de persistencia se ha cerrado, 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 esta indemnización es LAZY,
  • línea 30: se cierra el EntityManager,
  • líneas 32-36: se vuelve a mostrar al empleado. No debería haber ninguna excepción.

Para ejecutar el proyecto, se necesita una base de datos completa. Se creará siguiendo los pasos del párrafo 5.5. Además, se debe 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,
  • 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 siguientes dos mensajes 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 en comparación con el 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 devolvieron 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 a veces depende de la implementación JPA utilizada. Es posible configurar EclipseLink para que se comporte como se espera en el modo LAZY.

5.6.6. Para continuar

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

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

  • luego renombraremos el nuevo proyecto como [4, 5, 6].

Modificaremos las dependencias del nuevo proyecto. El archivo [pom.xml] queda 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 para el grupo de conexiones de Apache DBCP,
  • líneas 42-65: las dependencias para el marco 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 agrega 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 más arriba)

  • el número de seguro 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 esta información y de otros datos almacenados en archivos de configuración, la aplicación muestra la siguiente información:

  • líneas 4-6: los valores ingresados
  • líneas 8-10: la información relacionada con el empleado cuyo número de seguro social se ha ingresado
  • líneas 12-14: las tasas de las diferentes cotizaciones sociales
  • líneas 16-17: las diferentes indemnizaciones pagadas a la cuidadora infantil
  • líneas 19-24: los datos de la nómina de la cuidadora infantil

La capa [metier] debe proporcionar cierta información a la capa [ui]:

  1. la información relacionada con una cuidadora infantil identificada por su número de seguro social. Esta información se encuentra en la tabla [EMPLOYES]. Esto permite mostrar las líneas 6-8.
  2. los montos de las diversas tasas de cotizaciones sociales que se deben deducir del salario bruto. Esta información se encuentra en la tabla [COTISATIONS]. Esto permite mostrar las líneas 10 a 12.
  3. los montos de las diversas indemnizaciones 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 a 22.

A partir de esto, se podría decidir realizar un primer registro 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 colocan 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: las diferentes tasas de cotización —información n.º 2 mostrada por la capa [ui]
  • línea 11: las diferentes indemnizaciones relacionadas con el í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]

Un segundo caso de uso de la capa [métier] aparece 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 capa 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] mencionado anteriormente consultando la capa [DAO], ya que esta información se encuentra en las tablas de la base de datos. Lo mismo ocurre para obtener la lista de todos los empleados. Se podría crear una única interfaz [DAO] que administre el acceso a las tres entidades [Employe, Cotisation, Indemnite]. Sin embargo, en este caso decidimos 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 específica
  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]. Aquí hemos querido plantearnos un caso más general en el que todas las operaciones CRUD (Crear, Leer, Actualizar, Eliminar) 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] 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 almacena aquí en la tabla [COTISATIONS]. Antes de esta persistencia, el parámetro cotisation tiene un identificador id sin valor. Después de la persistencia, el campo id tiene un valor que es la clave primaria del registro agregado 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, tiene acceso al objeto modificado, ya que cuenta con una referencia sobre él. Por lo tanto, puede conocer el valor que el método create le ha asignado al campo id del objeto [Cotisation cotisation]. La firma del método podría ser, por lo tanto, más sencilla:

       // crear una nueva cotización
void create(Cotisation cotisation);

Al escribir una interfaz, es importante recordar que puede utilizarse en dos contextos diferentes: local y distant. En el contexto local, tanto el método que llama como el método al que se llama se ejecutan en el mismo JVM:

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

En el contexto distant, el método que llama 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 a la que llamaremos capa de comunicación [1]. Esta está compuesta por una capa de transmisión [2] y una capa de recepción [3]. Por lo general, el desarrollador no tiene que escribir estas capas de comunicación. Son generadas automáticamente por 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 en el 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 es 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 agregado a la tabla [COTISATIONS]. Este no es el caso del objeto cotisation1, al que la capa [metier] hace referencia. Si queremos que la capa [metier] tenga una referencia al objeto cotisation2, debemos enviársela. Por lo tanto, debemos modificar la firma del método create de la capa [DAO]:
       // crear una nueva cotización
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: es el valor del campo id de cotisation3.

La arquitectura anterior no es la más común. 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]. Sin embargo, la siguiente firma del método create de la capa [DAO]:

       // crear una nueva cotización
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 que realiza la llamada debe utilizar el resultado del método invocado y no las referencias de los parámetros modificados que transmitió al método invocado.

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

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 específica
  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 representa el registro modificado. El parámetro cotisation1, por su parte, no se modifica. El método debe devolver cotisation2 como resultado, independientemente de si se trata de una arquitectura distante o 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 debe 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 sea utilizable 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 de 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» por su identificador
  public Indemnite find(Long id);
     // obtener todas las entidades Indemnite
  public List<Indemnite> findAll();

}
  • En la línea 6, la interfaz [IIndemniteDao] administra el acceso 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]. Aquí hemos querido plantearnos un caso más general en el que todas las operaciones CRUD (Crear, Leer, Actualizar, Eliminar) 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] 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]. Aquí hemos querido plantearnos un caso más general en el que todas las operaciones CRUD (Crear, Leer, Actualizar, Eliminar) 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] existente
  • línea 12: el método [destroy] elimina una entidad [Employe] existente
  • línea 14: el método [find] permite buscar 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 vimos que este método era necesario para la aplicación gráfica.

5.8. La clase [PamException]

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

  • aumentan la complejidad del código, que debe manejar obligatoriamente estas excepciones con 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 a [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] genera excepciones de tipo [PamException] no controladas

Esto tiene dos consecuencias:

  • la capa [metier] no tendrá la obligación de manejar las excepciones de la capa [DAO] con try / catch. Simplemente podrá dejar que se propaguen hasta la capa [ui].
  • los métodos de la interfaz [IDao] no tienen que incluir en su firma la naturaleza de la excepción [PamException], lo que deja abierta la posibilidad de 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 manejar mediante un try/catch ni a incluir en la firma de los métodos. Es por esta razón que [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 y cuando esta también derive de [RuntimeException].
  • Para diferenciar los errores que pueden ocurrir, se utiliza el código de error de la línea 7. Los tres constructores de las líneas 14, 19 y 24 son los de la clase padre [RuntimeException], a los que se les ha agregado un parámetro: el del código de error que se desea asignar a la excepción.

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 presente 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 surja 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.

Ahora analicemos sucesivamente la implementación de las capas [DAO] y [metier].

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

Nos situamos en el marco de la siguiente arquitectura:

5.9.1. Implementación

Lecturas recomendadas: párrafo 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: párrafo 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 siguientes tres beans: employeDao de tipo EmployeDao, indemniteDao de tipo IndemniteDao, cotisationDao de tipo CotisationDao. Además, la implementación JPA que se utilizará será Hibernate.


5.9.3. Pruebas

Lecturas recomendadas: párrafos 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 prueba 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 asegura 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. Llena la base de datos con algunos registros y luego los muestra en la consola.
  • [JUnitDao] realiza una serie de pruebas y verifica los resultados.

El esqueleto de la clase [JUnitInitDB] es el 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 llena la base de datos
...
     // se muestra el contenido de la base de datos
...
  }

  @Before()
  public void clean(){
     // se vacía 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. Aquí no habrá ninguna. Por lo tanto, el método es una prueba falsa. Su función es llenar la base de datos con algunas filas y luego mostrar el contenido de la base en la consola. Aquí se utilizan los métodos create y findAll de las capas [DAO].

Pregunta: complete el código de la clase [JUnitInitDB]. Utilice como referencia el ejemplo del párrafo 3.1.6 de [ref1]. El código generará el contenido presentado en el párrafo 5.1.


5.9.5. Implementació ación de las pruebas

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

  • se han implementado 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 existente [dbpam_hibernate],
  • la ventana de [Test Results] y [6] indica que las pruebas se han realizado con éxito. 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 fracaso de la prueba. No obstante, esto demuestra que no se produjo 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 prueba en sí. 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 llenado. Esto se puede verificar con 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] se muestra el resultado.

5.9.6. JUnitDao

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

El esqueleto de la clase será el 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 agrega 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(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);
     // verificaciones
    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());
     // verificaciones
    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);
     // verificaciones
    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 agrega una indemnización
..
     // se busca la indemnización en la base de datos – se recupera indemnización1
..
     // se verifica que indemnización1 = indemnización
...
     // se modifica la indemnización obtenida y se guarda el cambio en BD. Se obtiene la indemnización2
 ...
     // se verifica la versión de indemnización2
    ...
     // se busca indemnización2 en la base de datos; se obtiene indemnización3
    ...
     // se verifica que indemnite3 = indemnite2
    ...
     // se elimina de la base de datos la imagen de indemnite3
    ...
     // se busca «indemnite3» en la base de datos
    ...
     // se verifica que se haya 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 luego con un empleado que no existe
...
  }

  @Test
  public void test05(){
    log("test05");
     // creamos dos indemnizaciones con el mismo índice
     // viola la restricción de unicidad del índice
     // se verifica que se produzca una excepción del 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 viola la restricción de unicidad sobre el número SS
     // se verifica que se produzca 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úmero SS; el primero con «create» y el segundo con «edit»
     // viola la restricción de unicidad sobre el número SS
     //: se verifica 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 una excepción
     // se agrega y luego se elimina; se verifica
...
  }

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

  @Test
  public void test10(){
    log("test10");
     // Eliminar un empleado sin tener la versión correcta debe generar una excepción
     // se verifica
...

  }

   // 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.


Al proceder de la misma manera 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 reporta 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 agrega 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);
     // modificarla
....
}

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 arroja los siguientes resultados:

  • La página de resultados [1] ahora muestra que hubo pruebas fallidas.
  • En [2], se muestra un resumen de la excepción que provocó el fallo de la prueba. Allí se encuentra el número de línea del código Java donde ocurrió 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 párrafo 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 párrafo 5.7. Ahora volveremos a ella.

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 diferentes indemnizaciones relacionadas con el í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] específico
  • 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 conforman 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, la cual debe ser serializable.
  • línea 6: el salario base
  • línea 7: las cotizaciones sociales pagadas sobre este salario base
  • línea 8: las indemnizaciones diarias por manutención del niño
  • línea 9: las indemnizaciones diarias por comida del niño
  • línea 10: el salario neto a pagar a la niñera
  • líneas 12-24: los constructores de la clase
  • líneas 27-31: método [toString] que identifica un objeto [ElementsSalaire] específico
  • 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 en las capas [DAO] a 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 destacar lo siguiente:

  • el modo de cálculo del salario se explicó en el párrafo 5.2.
  • Si el parámetro [SS] no corresponde a ningún empleado (la capa [DAO] devolvió un puntero null), el método lanzará una excepción de tipo [PamException] con un código de error apropiado.

5.10.4. Pruebas de la capa [metier]

Creamos dos programas de prueba:

Las clases de prueba [3] se crean en un 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
    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 llena
    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 busca 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 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 colocado aserciones en el método test01.


Pregunta: escribe el método test01.


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

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 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 enfocamos en la aplicación de consola implementada por la clase [ui.console.Main] mencionada anteriormente. Su funcionamiento se describió en el párrafo 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 verifica 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 está bien; podemos 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: complete el código anterior.


5.11.2. Ejecución

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

  • en [1], seleccione 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 ejecutará,
  • seleccionar la clase [4],
  • la clase aparece en [5]. Esta requiere tres argumentos para ejecutarse (n.º SS, número de horas trabajadas, número de días trabajados). Estos argumentos se colocan 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 elige la categoría del archivo [Swing GUI Forms], c.a.d. formularios gráficos
  • [3]: se elige 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 agrega 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 de Java como cualquier otra. La perspectiva [Design] facilita el diseño del formulario. Cada vez que se agrega un componente en el modo [Design], se agrega código Java en la perspectiva [Source] para incorporarlo.
  • [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], solicitamos la vista previa del formulario
  • en [18], el resultado
  • en [19], la etiqueta [JLabel1] se ha agregado al árbol de componentes en la ventana [Inspector]
  • en [20] y [21]: en la perspectiva [Source] del formulario, se ha agregado código Java para gestionar el JLabel agregado.

Hay un tutorial sobre la creación de formularios con NetBeans disponible 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: construye 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 que maneje 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 agregue aquí su código de manejo:
}

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) en el botón [jButtonSalaire] (línea 2) debe ser manejado por el método [jButtonSalaireActionPerformed] (línea 4).

También manejaremos el evento [caretUpdate] (desplazamiento del cursor de entrada) en el campo de entrada [jTextFieldHT]. Para crear el manejador 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 a 4 indican que el evento [caretUpdate] (línea 2) en el botón [jTextFieldHT] (línea 1) debe ser manejado 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, al inicializarse esta, también se inicialice la referencia [IMetier metier] de la línea 3 anterior. 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 agregar nuestras propias inicializaciones al código anterior, 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]
...
     // regreso
      return;
    }
     // botón de salario desactivado
...
     // jScrollPane1 oculto
...
     // selector de días trabajados
    jSpinnerJT.setModel(new SpinnerNumberModel(0,0,31,1));
     // cuadro combinado 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 a 42

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


5.12.5. Manejadores 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, modificaremos la configuración [Run] del proyecto:

  • a [1], y se debe establecer 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 de NetBeans

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

  • en [1]: después de hacer clic con el botón derecho del ratón en el proyecto Hibernate, seleccione Copy
  • con el botón [2], elija la carpeta principal del nuevo proyecto. El nombre de la carpeta aparece en [3].
  • En [4], asigne 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], lo renombramos como [mv-pam-spring-eclipselink].

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

  1. En [4], se deben 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.

Comencemos 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). Además, la base de datos de destino es [dbpam_hibernate] (línea 41).

Para cambiar a una implementación JPA / EclipseLink, las líneas 19 a 35 anteriores se reemplazan por las 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. Aquí, la propiedad está comentada.
  • línea 7: para visualizar en la consola los comandos SQL emitidos por la capa JPA. Aquí, la propiedad está comentada.

Además, 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, es recomendable verificar si las pruebas JUnit se ejecutan correctamente 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 conectado a la base de datos dbpam_eclipselink / MySQL5, podremos proceder a eliminar las tablas 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 llena 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 luego ejecutamos la prueba [JUnitInitDB] :

  • en [1], se ejecuta la prueba InitDB.
  • En [2], falla. La excepción la genera 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]: Falló la invocación del método init; la excepción anidada es java.lang.IllegalStateException: Se debe iniciar el agente de Java para utilizar InstrumentationLoadTimeWeaver. Consulte la documentación de Spring.

Spring indica que hay un problema de configuración. El mensaje no es claro. La razón de la excepción se explicó en el párrafo 3.1.9 de [ref1]. Para que la configuración de Spring / EclipseLink funcione, el 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 de 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 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 realizó con éxito
  • 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 crearon 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 sí había 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 que se 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() {
    // 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);
    }
  }

  // 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
    // viola 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;
      // verificaciones
      Assert.assertEquals(31, ex.getCode());
    } catch (Throwable th1) {
      th = th1;
    }
    // verificaciones
    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 debió haberse guardado
    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 debió haberse guardado
    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 pasa, c.a.d. Las aserciones se verifican y no hay ninguna excepción que salga del método de prueba.


Pregunta: explique qué sucedió.


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 pasa, c.a.d. Las aserciones se verifican y no hay ninguna excepción que salga del método de prueba.


Pregunta: explique qué sucedió.



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 proyecto mismo, tanto en su versión de consola como en la gráfica. El cambio en la implementación JPA no afecta en absoluto a las capas [metier] y [ui]; por lo tanto, si estas capas funcionaban con Hibernate, funcionarán con EclipseLink con 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, por su parte, 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, SQL Server.