Skip to content

5. Version 1 : Architecture Spring / JPA

On se propose d’écrire une application console ainsi qu'une application graphique permettant d’établir le bulletin de salaire des assistantes maternelles employées par la "Maison de la petite enfance" d'une commune. Cette application aura l'architecture suivante :

5.1. BDLa base de données

Les données statiques utiles pour construire la fiche de paie seront placées dans une base de données que nous désignerons par la suite dbpam. Cette base de données pourrait avoir les tables suivantes :

Table EMPLOYES : rassemble des informations sur les différentes assistantes maternelles

Structure :

ID

clé primaire

VERSION

n° de version – augmente à chaque modification de la ligne

SS

numéro de sécurité sociale de l'employé - unique

NOM

nom de l'employé

prenom

son prénom

ADRESSE

son adresse

VILLE

sa ville

CODEPOSTAL

son code postal

INDEMNITE_ID

clé étrangère sur le champ [ID] de la table [INDEMNITES]

Son contenu pourrait être le suivant :

Image

Table COTISATIONS : rassemble des pourcentages nécessaires au calcul des cotisations sociales

Structure :

ID

clé primaire

VERSION

n° de version – augmente à chaque modification de la ligne

CSGRDS

pourcentage : contribution sociale généralisée + contribution au remboursement de la dette sociale

CSGD

pourcentage : contribution sociale généralisée déductible

SECU

pourcentage : sécurité sociale, veuvage, vieillesse

RETRAITE

pourcentage : retraite complémentaire + assurance chômage

Son contenu pourrait être le suivant :

Image

Les taux des cotisations sociales sont indépendants du salarié. La table précédente n'a qu'une ligne.

Table INDEMNITES : rassemble les éléments permettant le calcul du salaire à payer.

ID

clé primaire

VERSION

n° de version – augmente à chaque modification de la ligne

INDICE

indice de traitement - unique

BASEHEURE

prix net en euro d’une heure de garde

ENTRETIENJOUR

indemnité d’entretien en euro par jour de garde

REPASJOUR

indemnité de repas en euro par jour de garde

INDEMNITESCP

indemnité de congés payés. C'est un pourcentage à appliquer au salaire de base.

 

Son contenu pourrait être le suivant :

Image

On notera que les indemnités peuvent varier d'une assistante maternelle à une autre. Elles sont en effet associées à une assistante maternelle précise via l'indice de traitement de celle-ci. Ainsi Mme Marie Jouveinal qui a un indice de traitement de 2 (table EMPLOYES) a un salaire horaire de 2,1 euro (table INDEMNITES).

5.2. Mode de calcul du salaire d'une assistante maternelle

Nous présentons maintenant le mode de calcul du salaire mensuel d'une assistante maternelle. Il ne prétend pas être celui utilisé dans la réalité. Nous prenons pour exemple, le salaire de Mme Marie Jouveinal qui a travaillé 150 h sur 20 jours pendant le mois à payer.

Les éléments suivants sont pris en compte :


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

[TOTALJOURS]: total des jours travaillés
dans le mois
[TOTALHEURES]=150
[TOTALJOURS]= 20

Le salaire de base de l'assistante maternelle

est donné par la formule suivante :

[SALAIREBASE]=([TOTALHEURES]
*[BASEHEURE])*(1+
[INDEMNITESCP]/100)
[SALAIREBASE]=
(150*[2.1])*(1+0.15)= 362,25

Un certain nombre de cotisations sociales

doivent être prélevées sur ce salaire de

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 des cotisations sociales :

[COTISATIONSSOCIALES]=
[SALAIREBASE]*(CSGRDS+CSGD
+SECU+RETRAITE)/100
[COTISATIONSSOCIALES]=97,48

Par ailleurs, l'assistante maternelle a droit, chaque jour travaillé, à une indemnité d'entretien ainsi qu'à une indemnité de repas. A ce titre elle reçoit les indemnités suivantes :


[Indemnités]=[TOTALJOURS]
*(ENTRETIENJOUR+REPASJOUR)
[INDEMNITES]=104

Au final, le salaire net à payer à l'assistante maternelle est le suivant :

[SALAIREBASE]-[COTISATIONSSOCIALES]+[INDEMNITÉS]
[salaire NET]=368,77

5.3. Fonctionnement de l'application console

Voici un exemple d'exécution de l'application console dans une fenêtre 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

On écrira un programme qui recevra les informations suivantes :

  1. n° de sécurité sociale de l'assistante maternelle ( 254104940426058 dans l'exemple - ligne 1)
  2. nombre total d'heures travaillées (150 dans l'exemple - ligne 1)
  3. nombre total de jours travaillés (20 dans l'exemple - ligne 1)

On voit que :

  • lignes 9-14 : affichent les informations concernant l'employé dont on a donné le n° de sécurité sociale
  • lignes 17-20 : affichent les taux des différentes cotisations
  • lignes 23-26 : affichent les indemnités associées à l'indice de traitement de l'employé (ici l'indice 2)
  • lignes 29-33 : affichent les éléments constitutifs du salaire à payer

L'application signale les erreurs éventuelles :

Appel sans paramètres :


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

Appel avec des données erronées :


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é

Appel avec un n° de sécurité sociale erroné :


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. Fonctionnement de l'application graphique

L'application graphique permet le calcul des salaires des assistantes maternelles au travers d'un formulaire Swing :

  • les informations passées en paramètres au programme console, sont maintenant saisies au moyen des champs de saisie [1, 2, 3].
  • le bouton [4] demande le calcul du salaire
  • le formulaire affiche les différents éléments du salaire jusqu'au salaire net à payer [5]

La liste déroulante [1, 6] ne présente pas les n°s SS des employés mais les noms et prénoms de ceux-ci. On fait ici l'hypothèse qu'il n'y a pas deux employés de mêmes nom et prénom.

5.5. Création de la base de données

Nous lançons WampServer et utilisons l'outil PhpMyAdmin [1] :

  • en [2], on prend l'option [Bases de données],
  • en [3], on crée une base de données [dbpam_hibernate],
  • en [4], la base créée. On la sélectionne,
  • en [5], on veut importer un script SQL,
  • en [6], on utilise le bouton [Parcourir] pour désigner le fichier,
  • en [7,8], on sélectionne le script SQL,
  • en [9], on l'exécute,
  • en [10], les tables ont été créées. Leur contenu est le suivant :

table EMPLOYES

Image

table INDEMNITES

Image

table COTISATIONS

Image

5.6. Implémentation JPA

5.6.1. Couche JPA / Hibernate

Nous allons configurer la couche JPA dans l'environnement suivant :

Un programme console travaillera avec la base de données. Pour cela, il faut :

  • avoir une base de données,
  • avoir le pilote JDBC du SGBD, ici MySQL,
  • implémenter la couche JPA avec Hibernate,
  • écrire le programme console.

Nous créons le projet Maven [mv-pam-jpa-hibernate] [1] :

Dans l'architecture de notre application il nous faut les éléments suivants :

  • la base de données,
  • le pilote JDBC du SGBD MySQL,
  • la couche JPA / Hibernate (entités et configuration),
  • le programme console de test.

5.6.1.1. La base de données

Créons tout d'abord la base de données vide. Nous lançons WampServer et utilisons l'outil PhpMyAdmin [1] :

  • en [2], on prend l'option [Bases de données],
  • en [3], on crée une base de données [dbpam_hibernate],
  • en [4], la base créée.

5.6.1.2. Configuration de la couche JPA

La liaison entre la couche JDBC et la base de données se fait dans le fichier [persistence.xml] qui configure la couche JPA. Ce fichier peut être construit avec Netbeans :

  • dans l'onglet [services] [1], on se connecte à la base de données avec le pilote JDBC de MySQL [2],
  • en [3], le nom de la base de données à laquelle on veut se connecter.
  • en [4], l'URL JDBC de la base,
  • en [5], on se connecte en tant que root sans mot de passe,
  • en [6], on peut tester la connexion,
  • en [7], la connexion a réussi.
  • la connexion apparaît en [8] et en [9],
  • en [10], on ajoute un nouvel élément au projet,
  • en [11] on choisit la catégorie [Persistence] et en [12] l'élément [Persistence Unit],
  • en [13], on donne un nom à cette unité de persistance,
  • en [14], on choisit une implémentation Hibernate,
  • en [15], on désigne la connexion que nous venons de créer vers la base MySQL,
  • en [16], on indique qu'à l'instanciation de la couche JPA, celle-ci doit construire (create) les tables correspondant aux entités JPA du projet.

La fin de l'assistant génère le fichier [persistence.xml] :

  • le fichier apparaît dans une nouvelle branche du projet, dans un dossier [META-INF] [1],
  • qui correspond au dossier [src/main/resources] du projet [2,3] .

Son contenu est le suivant :


<?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>
  • ligne 3 : le nom de l'unité de persistance et le type de transactions. RESOURCE_LOCAL indique que le projet gère lui-même les transactions. C'est ici le programme console qui devra le faire,
  • ligne 4 : l'implémentation JPA utilisée est Hibernate,
  • lignes 6-9 : les caractéristiques JDBC de la connexion à la base de données,
  • ligne 11 : demande la création des tables correspondant aux entités JPA. En fait, Netbeans génère ici une configuration erreonée. La configuration doit être la suivante :

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

Avec l'option create, Hibernate, à l'instanciation de la couche JPA, supprime puis crée les tables correspondant aux entités JPA. L'option create-drop fait la même chose mais à la fin de vie de la couche JPA, elle supprime toutes les tables. Il existe une autre option :


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

Cette option crée les tables si elles n'existent pas mais elle ne les détruit pas si elles existent déjà.

Nous ajouterons trois autres propriétés à la configuration d'Hibernate :


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

Elles demandent à Hibernate d'afficher les ordres SQL qu'il envoie à la base de données. Le fichier complet est donc le suivant :


<?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. Les dépendances

Revenons à l'architecture du projet :

Nous avons configuré la couche JPA via le fichier [persistence.xml]. L'implémentation choisie a été Hibernate. Cela a amené des dépendances dans le projet :

  

Ces dépendances sont dues à l'inclusion d'Hibernate dans le projet. Il nous faut ajouter une autre dépendance, celle du pilote JDBC de MySQL qui implémente la couche JDBC de l'architecture. Nous faisons évoluer le fichier [pom.xml] de la façon suivante :


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

Les lignes 8-12 ajoutent la dépendance du pilote JDBC de MySQL.

5.6.1.4. Les entités JPA


Question : En suivant la démarche de l'exemple du paragraphe 4.4, générer les entités [Cotisation, Indemnite, Employe].


Notes :

  • les entités feront partie d'un paquetage nommé [jpa],
  • chaque entité aura un n° de version,
  • si deux entités sont liées par une relation, seule la relation principale @ManyToOne sera construite. La relation inverse @OneToMany ne le sera pas.

5.6.1.5. Le code de la classe principale

Nous incluons dans le projet les entités JPA développées précédemment [1] :

puis nous rajoutons [2], la classe [main.Main] suivante :


package main;

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

public class Main {

  public static void main(String[] args) {
    // créer l'Entity Manager suffit à construire la couche JPA
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("mv-pam-jpa-hibernatePU");
    EntityManager em=emf.createEntityManager();
    // libération ressources
    em.close();
    emf.close();
  }
}
  • ligne 10 : on crée l'EntityManagerFactory de l'unité de persistance nommée [mv-pam-jpa-hibernatePU]. Ce nom vient du fichier [persistence.xml] :

  <persistence-unit name="mv-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
    ...
  </persistence-unit>
  • ligne 12 : on crée l'EntityManager. Cette création crée la couche JPA. Le fichier [persistence.xml] va être exploité et donc les tables de la base de données vont être créées,
  • lignes 14-15 : on libère les ressources.

5.6.1.6. Tests

Revenons à l'architecture de notre projet :

Toutes les couches ont été implémentées. On exécute le projet [2].

Les résultats console sont les suivants :

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

On trouve dans la console uniquement des logs d'Hibernate puisque le programme exécuté ne fait rien en-dehors d'instancier la couche JPA. On notera les points suivants :

  • ligne 43 : Hibernate essaie de supprimer la clé étrangère de la table [EMPLOYES],
  • lignes 51-55 : suppression des trois tables,
  • ligne 57 : création de la table [COTISATIONS],
  • ligne 67 : création de la table [EMPLOYES],
  • ligne 80 : création de la table [INDEMNITES],
  • ligne 91 : création de la clé étrangère de la table [EMPLOYES].

Dans Netbeans, on peut voir les tables dans la connexion qui a été créée précédemment :

Les tables créées dépendent à la fois de l'implémentation de la couche JPA utilisée et du SGBD utilisé. Ainsi une implémentation JPA / EclipseLink avec la même base de données peut générer des tables différentes. C'est ce que nous allons voir maintenant.

Nous allons construire un nouveau projet Maven dans l'environnement suivant :

On suivra la démarche du paragraphe précédent :

  1. créer une base MySQL [dbpam_eclipselink]. On utilisera le script [dbpam_eclipselink.sql] pour la générer,
  2. créer le fichier [persistence.xml] du projet. Prendre l'implémentation JPA 2.0 EclipseLink,
  3. ajouter dans les dépendances générées la dépendance du pilote JDBC de MySQL,
  4. ajouter les entités JPA et le programme console,
  5. faire les tests.

Le fichier [persistence.xml] sera le suivant :


<?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>
  • les propriétés 9-13 ont été générées par l'assistant Netbeans,
  • ligne 14 : cette propriété nous permet de fixer le niveu de logs d'EclipseLink. Le niveau FINE nous permet de connaître les ordres SQL qu'EclipseLink va émettre sur la base de données,
  • ligne 15 : à l'instanciation de la couche JPA / EclipseLink, les tables des entités JPA seront détruites puis créées.

Les résultats console obtenus sont les suivants :

------------------------------------------------------------------------
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)--Thread(Thread[main,5,main])--The access type for the persistent class [class jpa.Cotisation] is set to [FIELD].
[EL Config]: 2012-06-22 14:35:01.884--ServerSession(730572764)--Thread(Thread[main,5,main])--The access type for the persistent class [class jpa.Employe] is set to [FIELD].
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Thread(Thread[main,5,main])--The target entity (reference) class for the many to one mapping element [field indemnite] is being defaulted to: class jpa.Indemnite.
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Thread(Thread[main,5,main])--The access type for the persistent class [class jpa.Indemnite] is set to [FIELD].
[EL Config]: 2012-06-22 14:35:01.899--ServerSession(730572764)--Thread(Thread[main,5,main])--The alias name for the entity class [class jpa.Cotisation] is being defaulted to: Cotisation.
[EL Config]: 2012-06-22 14:35:01.915--ServerSession(730572764)--Thread(Thread[main,5,main])--The column name for element [id] is being defaulted to: ID.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Thread(Thread[main,5,main])--The alias name for the entity class [class jpa.Employe] is being defaulted to: Employe.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Thread(Thread[main,5,main])--The column name for element [id] is being defaulted to: ID.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Thread(Thread[main,5,main])--The alias name for the entity class [class jpa.Indemnite] is being defaulted to: Indemnite.
[EL Config]: 2012-06-22 14:35:01.93--ServerSession(730572764)--Thread(Thread[main,5,main])--The column name for element [id] is being defaulted to: ID.
[EL Config]: 2012-06-22 14:35:01.962--ServerSession(730572764)--Thread(Thread[main,5,main])--The primary key column name for the mapping element [field indemnite] is being defaulted to: ID.
[EL Info]: 2012-06-22 14:35:02.558--ServerSession(730572764)--Thread(Thread[main,5,main])--EclipseLink, version: Eclipse Persistence Services - 2.3.0.v20110604-r9504
[EL Config]: 2012-06-22 14:35:02.568--ServerSession(730572764)--Connection(1543921451)--Thread(Thread[main,5,main])--connecting(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)--Connection(1296716340)--Thread(Thread[main,5,main])--Connected: 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)--Thread(Thread[main,5,main])--file:/D:/data/istia-1112/netbeans/glassfish/mv-pam/05/mv-pam-jpa-eclipselink/target/classes/_pam-jpa-eclipselinkPU login successful
[EL Fine]: 2012-06-22 14:35:02.818--ServerSession(730572764)--Connection(1296716340)--Thread(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)--Connection(1296716340)--Thread(Thread[main,5,main])--DROP TABLE COTISATIONS
[EL Fine]: 2012-06-22 14:35:03.118--ServerSession(730572764)--Connection(1296716340)--Thread(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)--Connection(1296716340)--Thread(Thread[main,5,main])--DROP TABLE EMPLOYES
[EL Fine]: 2012-06-22 14:35:03.238--ServerSession(730572764)--Connection(1296716340)--Thread(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)--Connection(1296716340)--Thread(Thread[main,5,main])--DROP TABLE INDEMNITES
[EL Fine]: 2012-06-22 14:35:03.338--ServerSession(730572764)--Connection(1296716340)--Thread(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)--Connection(1296716340)--Thread(Thread[main,5,main])--ALTER TABLE EMPLOYES ADD CONSTRAINT FK_EMPLOYES_INDEMNITE_ID FOREIGN KEY (INDEMNITE_ID) REFERENCES INDEMNITES (ID)
[EL Fine]: 2012-06-22 14:35:03.568--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
[EL Fine]: 2012-06-22 14:35:03.578--ServerSession(730572764)--Thread(Thread[main,5,main])--SELECT 1
[EL Warning]: 2012-06-22 14:35:03.578--ServerSession(730572764)--Thread(Thread[main,5,main])--Exception [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)--Connection(1296716340)--Thread(Thread[main,5,main])--DELETE FROM SEQUENCE WHERE SEQ_NAME = SEQ_GEN
[EL Fine]: 2012-06-22 14:35:03.638--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--SELECT * FROM SEQUENCE WHERE SEQ_NAME = SEQ_GEN
[EL Fine]: 2012-06-22 14:35:03.638--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values (SEQ_GEN, 0)
[EL Config]: 2012-06-22 14:35:03.748--ServerSession(730572764)--Connection(1296716340)--Thread(Thread[main,5,main])--disconnect
[EL Info]: 2012-06-22 14:35:03.748--ServerSession(730572764)--Thread(Thread[main,5,main])--file:/D:/data/istia-1112/netbeans/glassfish/mv-pam/05/mv-pam-jpa-eclipselink/target/classes/_pam-jpa-eclipselinkPU logout successful
[EL Config]: 2012-06-22 14:35:03.748--ServerSession(730572764)--Connection(1543921451)--Thread(Thread[main,5,main])--disconnect
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 3.503s
Finished at: Fri Jun 22 14:35:03 CEST 2012
Final Memory: 8M/153M
  • lignes 26-30 : connexion à la base de données MySQL,
  • lignes 31-34 : confirmation que la connexion a réussi,
  • ligne 36 : suppression de la clé étrangère de la table [EMPLOYES],
  • ligne 37 : suppression de la table [COTISATIONS],
  • ligne 38 : création de la table [COTISATIONS]. On notera avec intérêt, que la clé primaire ID n'a pas l'attribut MySQL auto_increment. Cela veut dire que ce n'est pas MySQL qui génère les valeurs de la clé primaire,
  • ligne 39 : suppression de la table [EMPLOYES],
  • ligne 40 : création de la table [EMPLOYES]. Sa clé primaire ID n'a pas l'attribut MySQL auto_increment,
  • ligne 41 : suppression de la table [INDEMNITES],
  • ligne 42 : création de la table [INDEMNITES]. Sa clé primaire ID n'a pas l'attribut MySQL auto_increment,
  • ligne 43 : création de la clé étrangère de la table [EMPLOYES] vers la table [INDEMNITES],
  • ligne 44 : création d'une table [SEQUENCE]. Elle sera utilisée pour générer les clés primaires des trois tables précédentes,
  • ligne 47 : on a une exception car cette table existait déjà,
  • lignes 51-53 : initialisation de la table [SEQUENCE].

L'existence des tables générées peut être vérifiée dans Netbeans [1] :

Donc, à partir des mêmes entités JPA, les implémentations JPA Hibernate et EclipseLink ne génèrent pas les mêmes tables. Dans la suite du document, lorsque l'implémentation JPA utilisée est :

  • Hibernate, on utilisera la base de données [dbpam_hibernate],
  • EclipseLink, on utilisera la base de données [dbpam_eclipselink].

5.6.3. Travail à faire

En suivant la même démarche que précédemment,

  1. créer et tester un projet [mv-pam-jpa-hibernate-oracle] utilisant une implémentation JPA Hibernate et un SGBD Oracle,
  2. créer et tester un projet [mv-pam-jpa-hibernate-mssql] utilisant une implémentation JPA Hibernate et un SGBD SQL server,
  3. créer et tester un projet [mv-pam-jpa-eclipselink-oracle] utilisant une implémentation JPA EclipseLink et un SGBD Oracle,
  4. créer et tester un projet [mv-pam-jpa-eclipselink-mssql] utilisant une implémentation JPA EclipseLink et un SGBD SQL server,

5.6.4. Lazy ou Eager ?

Revenons à une définition possible de l'entité [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;
  ...
}

Les lignes 27-29 définissent la clé étrangère de la table [EMPLOYES] vers la table [INDEMNITES]. L'attribut fetch de la ligne 27 définit la stratégie de recherche du champ indemnite de la ligne 29. Il y a deux modes :

  • FetchType.LAZY : lorsqu'un employé est cherché, l'indemnité qui lui correspond n'est pas ramenée. Elle le sera lorsque le champ [Employe].indemnite sera référencé pour la première fois.
  • FetchType.EAGER : lorsqu'un employé est cherché, l'indemnité qui lui correspond est ramenée. C'est le mode par défaut lorsqu'aucun mode n'est précisé.

Pour comprendre l'intérêt de l'option FetchType.LAZY, on peut prendre l'exemple suivant. Une liste d'employés sans les indemnités est présentée dans une page web avec un lien [Details]. Un clic sur ce lien présente alors les indemnités de l'employé sélectionné. On voit que :

  • pour afficher la première page on n'a pas besoin des employés avec leurs indemnités. Le mode FetchType.LAZY convient alors,
  • pour afficher la seconde page avec les détails, une requête supplémentaire doit être faite à la base de données pour avoir les indemnités de l'employé sélectionné.

Le mode FetchType.LAZY évite de ramener trop de données dont l'application n'a pas besoin tout de suite. Voyons un exemple.

Le projet [mv-pam-jpa-hibernate] est dupliqué :

  • en [1], on copie le projet,
  • en [2], on indique le dossier de la copie et en [3] son nom,
  • en [4], le nouveau projet porte le même nom que l'ancien. Nous changeons cela :
  • en [1], on renomme le projet,
  • en [2], on renomme le projet et son artifactId,
  • en [3], le nouveau projet.

Nous modifions le programme [Main.java] de la façon suivante :


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 requête JPQL ci-dessous ramène un employé
  // la clé étrangère [Employe].indemnite est en FetchType.LAZY
  public static void main(String[] args) {
    // créer l'Entity Manager suffit à construire la couche JPA
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("pam-jpa-hibernatePU");
    // premier essai
    EntityManager em = emf.createEntityManager();
    Employe employe = (Employe) em.createQuery("select e from Employe e where e.nom=:nom").setParameter("nom", "Jouveinal").getSingleResult();
    em.close();
    // on affiche l'employé
    try {
      System.out.println(employe);
    } catch (Exception ex) {
      System.out.println(ex);
    }
    // deuxième essai
    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();
    // libérer les ressources
    em.close();
    // on affiche l'employé
    try {
      System.out.println(employe);
    } catch (Exception ex) {
      System.out.println(ex);
    }
    // libération ressources
    emf.close();
  }
}
  • ligne 15 : on crée l'EntityManagerFactory de la couche JPA,
  • ligne 17 : on obtient l'EntityManager qui nous permet de dialoguer avec la couche JPA,
  • ligne 18 : on demande l'employé de nom Jouveinal,
  • ligne 19 : on ferme l'EntityManager. Cela a pour effet de fermer le contexte de persistence.
  • ligne 22 : on affiche l'employé reçu.

La classe [Employe] est la suivante :


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()
    +"]";
  }
  ...
}
  • ligne 27 : le champ indemnite est ramené en mode LAZY,
  • ligne 47 : utilise le champ indemnite. Si la méthode toString est appelée alors que le champ indemnite n'a pas été encore ramené, il le sera à ce moment là. Sauf si le contexte de persistance a été fermé comme dans l'exemple.

Revenons au code du [Main] :

  • lignes 21-25 : on devrait avoir une exception. En effet, la méthode toString va être appelée. Elle va utiliser le champ indemnite. Celui-ci va être cherché. Comme le contexte de persistance a été fermé, l'entité [Employe] ramenée n'existe plus d'où l'exception.
  • ligne 27 : on crée un nouveau EntityManager,
  • ligne 28 : on demande l'employé Jouveinal en demandant explicitement dans la requête JPQL l'indemnité qui va avec. Cette demande explicite est nécessaire parce que le mode de recherche de cette indemnité est LAZY,
  • ligne 30 : on ferme l'EntityManager,
  • lignes 32-36 : on réaffiche l'employé. Il ne devrait pas y avoir d'exception.

Pour exécuter le projet, on a besoin d'une base de données remplie. On la créera en suivant la démarche du paragraphe 5.5. Par ailleurs, le fichier [persistence.xml] doit être modifié :


<?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>
  • on a enlevé l'option qui créait les tables. La base de données ici existe déjà et est remplie,
  • on a enlevé les options qui faisaient qu'Hibernate loguait les ordres SQL qu'il émettait vers la base de données.

L'exécution du projet donne les deux affichages suivants dans la console :

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]
  • ligne 1 : l'exception qui s'est produite lorsqu'il a fallu chercher l'indemnité qui manquait alors que la session était fermée. On voit que l'indemnité n'avait pas été ramenée à cause du mode LAZY,
  • ligne 2 : l'employé avec son indemnité obtenue par une requête qui a contourné le mode LAZY.

5.6.5. Travail à faire

En suivant une démarche analogue à celle qui vient d'être suivie, créez un projet [mv-pam-pa-eclipselink-lazy] qui montre le comportement d'EclipseLink face au mode LAZY.

On obtient les résultats suivants :

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 mode LAZY, les deux requêtes ont ramené l'indemnité avec l'employé. Lorsqu'on se renseigne sur le net sur cette aberration, on découvre que l'annotation [FetchType.LAZY] (ligne 1) :


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

n'est pas un ordre mais un souhait. L'implémenteur JPA n'est pas obligé de le suivre. On voit donc que le code devient parfois dépendant de l'implémentation JPA utilisée. Il est possible de donner par configuration à EclipseLink le comportement attendu pour le mode LAZY.

5.6.6. Pour la suite

L'architecture de l'application à construire est la suivante :

Pour la suite du document, on dupliquera le projet Maven [mv-pam-jpa-hibernate] dans le projet [mv-pam-spring-hibernate] [1, 2, 3] :

  • puis on renomme le nouveau projet [4, 5, 6].

On changera les dépendances du nouveau projet. Le fichier [pom.xml] devient le suivant :


<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>
  • lignes 25-31 : la dépendance pour les tests JUnit,
  • lignes 32-41 : les dépendances pour le pool de connexions Apache DBCP,
  • lignes 42-65 : les dépendances pour le framework Spring,
  • lignes 67-71 : les dépendances pour l'implémentation JPA / Hibernate,
  • lignes 72-76 : la dépendance pour le pilote JDBC de MySQL,
  • lignes 77-81 : la dépendance pour l'interface Swing. Celle-ci est automatiquement ajoutée par Netbeans lorsqu'on ajoute une interface Swing au projet.

Par ailleurs, on génèrera les deux bases MySQL :

  • [dbpam_hibernate] à partir du script [dbpam_hibernate.sql],
  • [dbpam_eclipselink] à partir du script [dbpam_eclipselink.sql],

5.7. Les interfaces des couches [metier] et [DAO]

Revenons à l'architecture de l'application :

Dans l'architecture ci-dessus, quelle interface doit offrir la couche [DAO] à la couche [metier] et quelle interface doit offrir la couche [metier] à la couche [ui] ? Une première approche pour définir les interfaces des différentes couches est d'examiner les différents cas d'usage (use cases) de l'application. Ici nous en avons deux, selon l'interface utilisateur choisie : console ou formulaire graphique.

Examinons le mode d'utilisation de l'application console :

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

L'application reçoit trois informations de l'utilisateur (cf ligne 1 ci-dessus)

  • le n° de sécurité sociale de l'assistante maternelle
  • le nombre d'heures travaillées dans le mois
  • le nombre de jours travaillés dans le mois

A partir de ces information et d'autres enregistrées dans des fichiers de configuration, l'application affiche les informations suivantes :

  • lignes 4-6 : les valeurs saisies
  • lignes 8-10 : les informations liées à l'employé dont on a donné le n° de sécurité sociale
  • lignes 12-14 : les taux des différentes cotisations sociales
  • lignes 16-17 : les différentes indemnités versées à l'assistante maternelle
  • lignes 19-24 : les éléments de la feuille de salaire de l'assistante maternelle

Un certain nombre d'informations doivent être fournies par la couche [metier] à la couche [ui] :

  1. les informations liées à une assistante maternelle identifiée par son n° de sécurité sociale. On trouve ces informations dans la table [EMPLOYES]. Cela permet d'afficher les lignes 6-8.
  2. les montants des divers taux de cotisations sociales à prélever sur le salaire brut. On trouve ces informations dans la table [COTISATIONS]. Cela permet d'afficher les lignes 10-12.
  3. les montants des diverses indemnités liées à la fonction d'assistante maternelle. On trouve ces informations dans la table [INDEMNITES]. Cela permet d'afficher les lignes 14-15.
  4. les éléments constitutifs du salaire affichés lignes 18-22.

De ceci, on pourrait décider d'une première écriture de l'interface [IMetier] présentée par la couche [metier] à la couche [ui] :

1
2
3
4
5
6
package metier;

public interface IMetier {
  // obtenir la feuille de salaire
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
}
  • ligne 1 : les éléments de la couche [metier] sont mis dans le paquetage [metier]
  • ligne 5 : la méthode [ calculerFeuilleSalaire ] prend pour paramètres les trois informations acquises par la couche [ui] et rend un objet de type [FeuilleSalaire] contenant les informations que la couche [ui] affichera sur la console. La classe [FeuilleSalaire] pourrait être la suivante :
package metier;

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

public class FeuilleSalaire {
  // champs privés
  private Employe employe;
  private Cotisation cotisation;
  private ElementsSalaire elementsSalaire;

  ...
}
  • ligne 9 : l'employé concerné par la feuille de salaire - information n° 1 affichée par la couche [ui]
  • ligne 10 : les différents taux de cotisation - information n° 2 affichée par la couche [ui]
  • ligne 11 : les différentes indemnités liées à l'indice de l'employé - information n° 3 affichée par la couche [ui]
  • ligne 12 : les éléments constitutifs de son salaire - information n° 4 affichée par la couche [ui]

Un second cas d'usage de la couche [métier] apparaît avec l'interface graphique :

On voit ci-dessus, que la liste déroulante [1, 2] présente tous les employés. Cette liste doit être demandée à la couche [métier]. L'interface de celle-ci évolue alors de la façon suivante :

package metier;

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

public interface IMetier {
  // obtenir la feuille de salaire
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
  // liste des employés
  public List<Employe> findAllEmployes();
}
  • ligne [10] : la méthode qui va permettre à la couche [ui] de demander la liste de tous les employés à la couche [métier].

La couche [metier] ne peut initialiser les champs [Employe, Cotisation, Indemnite] de l'objet [FeuilleSalaire] ci-dessus qu'en questionnant la couche [DAO] car ces informations sont dans les tables de la base de données. Il en est de même pour obtenir la liste de tous les employés. On peut créer une interface [DAO] unique gérant l'accès aux trois entités [Employe, Cotisation, Indemnite]. Nous décidons plutôt ici de créer une interface [DAO] par entité.

L'interface [DAO] pour les accès aux entités [Cotisation] de la table [COTISATIONS] sera la suivante :

package dao;

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

public interface ICotisationDao {
      // créer une nouvelle cotisation
  public Cotisation create(Cotisation cotisation);
      // modifier une cotisation existante
  public Cotisation edit(Cotisation cotisation);
      // supprimer une cotisation existante
  public void destroy(Cotisation cotisation);
      // chercher une cotisation particulière
  public Cotisation find(Long id);
      // obtenir tous les objets Cotisation
  public List<Cotisation> findAll();

}
  • ligne 6, l'interface [ICotisationDao] gère les accès à l'entité [Cotisation] et donc à la table [COTISATIONS] de la base de données. Notre application n'a besoin que de la méthode [findAll] de la ligne 16 qui permet de retrouver tout le contenu de la table [COTISATIONS]. On a voulu ici se mettre dans un cas plus général où toutes les opérations CRUD (Create, Read, Update, Delete) sont effectuées sur l'entité.
  • ligne 8 : la méthode [create] crée une nouvelle entité [Cotisation]
  • ligne 10 : la méthode [edit] modifie une entité [Cotisation] existante
  • ligne 12 : la méthode [destroy] supprime une entité [Cotisation] existante
  • ligne 14 : la méthode [find] permet de retrouver une entité [Cotisation] existante via son identifiant id
  • ligne 16 : la méthode [findAll] rend dans une liste toutes les entités [Cotisation] existantes

Attardons-nous sur la signature de la méthode [create] :

      // créer une nouvelle cotisation
Cotisation create(Cotisation cotisation);

La méthode create a un paramètre cotisation de type Cotisation. Le paramètre cotisation doit être persisté, c.a.d. ici mis dans la table [COTISATIONS]. Avant cette persistance, le paramètre cotisation a un identifiant id sans valeur. Après la persistance, le champ id a une valeur qui est la clé primaire de l'enregistrement ajouté à la table [COTISATIONS]. Le paramètre cotisation est donc un paramètre d'entrée / sortie de la méthode create. Il ne semble pas nécessaire que méthode create rende de plus le paramètre cotisation comme résultat. La méthode appelante détenant une référence sur l'objet [Cotisation cotisation], si celui-ci est modifié, elle a accès à l'objet modifié puisqu'elle a une référence dessus. Elle peut donc connaître la valeur que la méthode create a donné au champ id de l'objet [Cotisation cotisation]. La signature de la méthode pourrait donc être plus simplement :

      // créer une nouvelle cotisation
void create(Cotisation cotisation);

Lorsqu'on écrit une interface, il est bon de se rappeler qu'elle peut être utilisée dans deux contextes différents : local et distant. Dans le contexte local, la méthode appelante et la méthode appelée sont exécutées dans la même JVM :

Si la couche [metier] fait appel à la méthode create de la couche [DAO], elle a bien une référence sur le paramètre [Cotisation cotisation] qu'elle passe à la méthode.

Dans le contexte distant, la méthode appelante et la méthode appelée sont exécutées dans des JVM différentes :

Ci-dessus, la couche [metier] s'exécute dans la JVM 1 et la couche [DAO] dans la JVM 2 sur deux machines différentes. Les deux couches ne communiquent pas directement. Entre-elles s'intercale une couche qu'on appellera couche de communication [1]. Celle-ci est composée d'une couche d'émission [2] et d'une couche de réception [3]. Le développeur n'a en général pas à écrire ces couches de communication. Elles sont générées automatiquement par des outils logiciels. La couche [metier] est écrite comme si elle s'exécutait dans la même JVM que la couche [DAO]. Il n'y a donc aucune modification de code.

Le mécanisme de communication entre la couche [metier] et la couche [DAO] est le suivant :

  • la couche [metier] fait appel à la méthode create de la couche [DAO] en lui passant le paramètre [Cotisation cotisation1]
  • ce paramètre est en fait passé à la couche d'émission [2]. Celle-ci va transmettre sur le réseau, la valeur du paramètre cotisation1 et non sa référence. La forme exacte de cette valeur dépend du protocole de communication utilisé.
  • la couche de réception [3] va récupérer cette valeur et reconstruire à partir d'elle un objet [Cotisation cotisation2] image du paramètre initial envoyé par la couche [metier]. On a maintenant deux objets identiques (au sens de contenu) dans deux JVM différentes : cotisation1 et cotisation2.
  • la couche de réception va passer l'objet cotisation2 à la méthode create de la couche [DAO] qui va le persister en base de données. Après cette opération, le champ id de l'objet cotisation2 a été initialisé par la clé primaire de l'enregistrement ajouté à la table [COTISATIONS]. Ce n'est pas le cas de l'objet cotisation1 sur lequel la couche [metier] a une référence. Si on veut que la couche [metier] ait une référence sur l'objet cotisation2, il faut le lui envoyer. Aussi est-on amenés à changer la signature de la méthode create de la couche [DAO] :
      // créer une nouvelle cotisation
Cotisation create(Cotisation cotisation);
  • avec cette nouvelle signature, la méthode create va rendre comme résultat l'objet persisté cotisation2. Ce résultat est rendu à la couche de réception [3] qui avait appelé la couche [DAO]. Celle-ci va rendre la valeur (et non la référence) de cotisation2 à la couche d'émission [2].
  • la couche d'émission [2] va récupérer cette valeur et reconstruire à partir d'elle un objet [Cotisation cotisation3] image du résultat rendu par la méthode create de la couche [DAO].
  • l'objet [Cotisation cotisation3] est rendu à la méthode de la couche [metier] dont l'appel à la méthode create de la couche [DAO] avait initié tout ce mécanisme. La couche [metier] peut donc connaître la valeur de clé primaire donné à l'objet [Cotisation cotisation1] dont elle avait demandé la persistance : c'est la valeur du champ id de cotisation3.

L'architecture précédente n'est pas la plus courante. On trouve plus fréquemment les couches [metier] et [DAO] dans la même JVM :

Dans cette architecture, ce sont les méthodes de la couche [metier] qui doivent rendre des résultats et non celles de la couche [DAO]. Néanmoins la signature suivante de la méthode create de la couche [DAO] :

      // créer une nouvelle cotisation
Cotisation create(Cotisation cotisation);

nous permet de ne pas faire d'hypothèses sur l'architecture réellement mise en place. Utiliser des signatures qui fonctionneront quelque soit l'architecture retenue, locale ou distante, implique que dans le cas où une méthode appelée modifie certains de ses paramètres :

  • ceux-ci doivent faire également partie du résultat de la méthode appelée
  • la méthode appelante doit utiliser le résultat de la méthode appelée et non les références des paramètres modifiés qu'elle a transmis à la méthode appelée.

On se laisse ainsi la possibilité de passer une couche d'une architecture locale à une architecture distante sans modification de code. Réexaminons, à cette lumière, l'interface [ICotisationDao] :

package dao;

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

public interface ICotisationDao {
      // créer une nouvelle cotisation
  public Cotisation create(Cotisation cotisation);
      // modifier une cotisation existante
  public Cotisation edit(Cotisation cotisation);
      // supprimer une cotisation existante
  public void destroy(Cotisation cotisation);
      // chercher une cotisation particulière
  public Cotisation find(Long id);
      // obtenir tous les objets Cotisation
  public List<Cotisation> findAll();

}
  • ligne 8 : le cas de la méthode create a été traité
  • ligne 10 : la méthode edit utilise son paramètre [Cotisation cotisation1] pour mettre à jour l'enregistrement de la table [COTISATIONS] ayant la même clé primaire que l'objet cotisation. Elle rend comme résultat l'objet cotisation2 image de l'enregistrement modifié. Le paramètre cotisation1 n'est lui pas modifié. La méthode doit rendre cotisation2 comme résultat qu'on soit dans le cadre d'une architecture distante ou locale.
  • ligne 12 : la méthode destroy supprime l'enregistrement de la table [COTISATIONS] ayant la même clé primaire que l'objet cotisation passé en paramètre. Celui-ci n'est pas modifié. Il n'a donc pas à être rendu.
  • ligne 14 : le paramètre id de la méthode find n'est pas modifié par la méthode. Il n'a pas à faire partie du résultat.
  • ligne 16 : la méthode findAll n'a pas de paramètres. On n'a donc pas à l'étudier.

Au final, seule la signature de la méthode create doit être adaptée pour être utilisable dans le cadre d'une architecture distante. Les raisonnements précédents seront valables pour les autres interfaces [DAO]. Nous ne les répèterons pas et utiliserons directement des signatures utilisables aussi bien dans le cadre d'une architecture distante que locale.

L'interface [DAO] pour les accès aux entités [Indemnite] de la table [INDEMNITES] sera la suivante :

package dao;

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

public interface IIndemniteDao {
    // créer une entité Indemnite
  public Indemnite create(Indemnite indemnite);
    // modifier une entité Indemnite
  public Indemnite edit(Indemnite indemnite);
    // supprimer une entité Indemnite
  public void destroy(Indemnite indemnite);
    // rechercher une entité Indemnite via son identifiant
  public Indemnite find(Long id);
    // obtenir toutes les entités Indemnite
  public List<Indemnite> findAll();

}
  • ligne 6, l'interface [IIndemniteDao] gère les accès à l'entité [Indemnite] et donc à la table [INDEMNITES] de la base de données. Notre application n'a besoin que de la méthode [findAll] de la ligne 16 qui permet de retrouver tout le contenu de la table [INDEMNITES]. On a voulu ici se mettre dans un cas plus général où toutes les opérations CRUD (Create, Read, Update, Delete) sont effectuées sur l'entité.
  • ligne 8 : la méthode [create] crée une nouvelle entité [Indemnite]
  • ligne 10 : la méthode [edit] modifie une entité [Indemnite] existante
  • ligne 12 : la méthode [destroy] supprime une entité [Indemnite] existante
  • ligne 14 : la méthode [find] permet de retrouver une entité [Indemnite] existante via son identifiant id
  • ligne 16 : la méthode [findAll] rend dans une liste toutes les entités [Indemnite] existantes

L'interface [DAO] pour les accès aux entités [Employe] de la table [EMPLOYES] sera la suivante :

package dao;

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

public interface IEmployeDao {
    // créer une nouvelle entité Employe
  public Employe create(Employe employe);
    // modifier une entité Employe existante
  public Employe edit(Employe employe);
    // supprimer une entité Employe
  public void destroy(Employe employe);
    // chercher une entité Employe via son identifiant id
  public Employe find(Long id);
    // chercher une entité Employe via son n° SS
  public Employe find(String SS);
    // obtenir toutes les entités Employe
  public List<Employe> findAll();
}
  • ligne 6, l'interface [IEmployeDao] gère les accès à l'entité [Employe] et donc à la table [EMPLOYES] de la base de données. Notre application n'a besoin que de la méthode [findAll] de la ligne 16 qui permet de retrouver tout le contenu de la table [EMPLOYES]. On a voulu ici se mettre dans un cas plus général où toutes les opérations CRUD (Create, Read, Update, Delete) sont effectuées sur l'entité.
  • ligne 8 : la méthode [create] crée une nouvelle entité [Employe]
  • ligne 10 : la méthode [edit] modifie une entité [Employe] existante
  • ligne 12 : la méthode [destroy] supprime une entité [Employe] existante
  • ligne 14 : la méthode [find] permet de retrouver une entité [Employe] existante via son identifiant id
  • ligne 16 : la méthode [find(String SS)] permet de retrouver une entité [Employe] existante via son n° SS. Nous avons vu que cette méthode était nécessaire à l'application console.
  • ligne 18 : la méthode [findAll] rend dans une liste toutes les entités [Employe] existantes. Nous avons vu que cette méthode était nécessaire à l'application graphique.

5.8. La classe [PamException]

La couche [DAO] va travailler avec l'API JDBC de Java. Cette API lance des exceptions contrôlées de type [SQLException] qui présentent deux inconvénients :

  • elles alourdissent le code qui doit obligatoirement gérer ces exceptions avec des try / catch.
  • elles doivent être déclarées dans la signature des méthodes de l'interface [IDao] par un "throws SQLException". Ceci a pour conséquence d'empêcher l'implémentation de cette interface par des classes qui lanceraient une exception contrôlée d'un type différent de [SQLException].

Pour remédier à ce problème, la couche [DAO] ne "remontera" que des exceptions non contrôlées de type [PamException].

  • la couche [JDBC] lance des exceptions de type [SQLException]
  • la couche [JPA] lance des exceptions propres à l'implémentation JPA utilisée
  • la couche [DAO] lance des exceptions de type [PamException] non contrôlées

Ceci a deux conséquences :

  • la couche [metier] n'aura pas l'obligation de gérer les exceptions de la couche [DAO] avec des try / catch. Elle pourra simplement les laisser remonter jusqu'à la couche [ui].
  • les méthodes de l'interface [IDao] n'ont pas à mettre dans leur signature la nature de l'exception [PamException], ce qui laisse la possiblité d'implémenter cette interface avec des classes qui lanceraient un autre type d'exception non contrôlée.

La classe [PamException] sera placée dans le paquetage [exception] du projet Netbeans :

Son code est le suivant :

package exception;

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

  // code d'erreur
  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 et setter

  public int getCode() {
    return code;
  }

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

}
  • ligne 4 : [PamException] dérive de [RuntimeException]. C'est donc un type d'exceptions que le compilateur ne nous oblige pas à gérer par un try / catch ou à mettre dans la signature des méthodes. C'est pour cette raison, que [PamException] n'est pas dans la signature des méthodes de l'interface [IDao]. Cela permet à cette interface d'être implémentée par une classe lançant un autre type d'exceptions, pourvu que celui-ci dérive également de [RuntimeException].
  • pour différencier les erreurs qui peuvent se produire, on utilise le code erreur de la ligne 7. Les trois constructeurs des lignes 14, 19 et 24 sont ceux de la classe parente [RuntimeException] auxquels on a rajouté un paramètre : celui du code d'erreur qu'on veut donner à l'exception.

Le fonctionnement de l'application, du point de vue des exceptions, sera le suivant :

  • la couche [DAO] encapsulera toute exception rencontrée, dans une exception de type [PamException], et relancera cette dernière pour la couche [métier].
  • la couche [métier] laissera remonter les exceptions lancées par la couche [DAO]. Elle encapsulera toute exception survenant dans la couche [métier], dans une exception de type [PamException] et relancera cette dernière pour la couche [ui].
  • la couche [ui] intercepte toutes les exceptions qui remontent des couches [métier] et [DAO]. Elle se contentera d'afficher l'exception sur la console ou l'interface graphique.

Examinons maintenant successivement l'implémentation des couches [DAO] et [metier].

5.9. La couche [DAO] de l'application [PAM]

Nous nous plaçons dans le cadre de l'architecture suivante :

5.9.1. Implémentation

Lectures conseillées : paragraphe 3.1.3 de [ref1]


Question : En utilisant l'intégration Spring / JPA, écrire les classes [CotisationDao, IndemniteDao, EmployeDao] d'implémentation des interfaces [ICotisationDao, IIndemniteDao, IEmployeDao]. Chaque méthode de classe interceptera une éventuelle exception et l'encapsulera dans une exception de type [PamException] avec un code d'erreur propre à l'exception interceptée.


Les classes d'implémentation feront partie du paquetage [dao] :

  

5.9.2. Configuration

Lectures conseillées : paragraphe 3.1.5 de [ref1]

L'intégration DAO / JPA est configurée par le fichier Spring [spring-config-dao.xml] et le fichier JPA [persistence.xml] :


Question : écrire le contenu de ces deux fichiers. On supposera que la base de données utilisée est la base MySQL5 [dbpam_hibernate] générée par le script SQL [dbpam_hibernate.sql]. Le fichier Spring définira les trois beans suivants : employeDao de type EmployeDao, indemniteDao de type IndemniteDao, cotisationDao de type CotisationDao. Par ailleurs, l'implémentation JPA utilisée sera Hibernate.


5.9.3. Tests

Lectures conseillées : paragraphes 3.1.6 et 3.1.7 de [ref1]

Maintenant que la couche [DAO] est écrite et configurée, nous pouvons la tester. L'architecture des tests sera la suivante :

5.9.4. InitDB

Nous allons créer deux programmes de tests de la couche [DAO]. Ceux-ci seront placés dans le paquetage [dao] [2] de la branche [Test Packages] [1] du projet Netbeans. Cette branche n'est pas incluse dans le projet généré par l'option [Build project], ce qui nous assure que les programmes de tests que nous y plaçons ne seront pas inclus dans le .jar final du projet.

Les classes placées dans la branche [Test Packages] ont connaissance des classes présentes dans la branche [Source Packages] ainsi que des bibliothèques de classes du projet. Si les tests ont besoin de bibliothèques autres que celles du projet, celles-ci doivent être déclarées dans la branche [Test Libraries] [2].

Les classes de tests utilisent l'outil de tests unitaires JUnit :

  • [JUnitInitDB] ne fait aucun test. Elle remplit la base de données avec quelques enregistrements et affiche ensuite ceux-ci sur la console.
  • [JUnitDao] fait une série de tests dont elle vérifie le résultat.

Le squelette de la classe [JUnitInitDB] est le suivant :

package dao;

...

public class JUnitInitDB {

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

  @BeforeClass
  public void init(){
    // configuration de l'application
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-dao.xml");
    // couches DAO
    employeDao = (IEmployeDao) ctx.getBean("employeDao");
    cotisationDao = (ICotisationDao) ctx.getBean("cotisationDao");
    indemniteDao = (IIndemniteDao) ctx.getBean("indemniteDao");
  }

  @Test
  public void initDB(){
    // on remplit la base
...
    // on affiche le contenu de la base
...
  }

  @Before()
  public void clean(){
    // on vide la base
...
  }
}
  • la méthode [init] est exécutée avant le début de la série des tests (annotation @BeforeClass). Elle instancie la couche [DAO].
  • la méthode [clean] est exécutée avant chaque test (annotation @Before). Elle vide la base de données.
  • la méthode [initDB] est un test (annotation @Test). C'est le seul. Un test doit contenir des instructions d'assertion Assert.assertCondition. Ici il n'y en aura aucune. La méthode est donc un faux test. Elle a pour rôle de remplir la base de données avec quelques lignes puis d'afficher le contenu de la base sur la console. Ce sont les méthodes create et findAll des couches [DAO] qui sont ici utilisées.

Question : compléter le code de la classe [JUnitInitDB]. On s'aidera de l'exemple du paragraphe 3.1.6 de [ref1]. Le code génèrera le contenu présenté au paragraphe 5.1.


5.9.5. Mise en oeuvre des tests

Nous sommes désormais prêts pour exécuter [InitDB]. Nous décrivons la procédure avec le SGBD MySQL5 :

  • les classes [1], les fichiers de configuration [2] et les classes de test de la couche [DAO] [3]sont mis en place,
  • le projet est construit [4]
  • la classe [JUnitInitDB] est exécutée [5]. Le SGBD MySQL5 est lancé avec une base [dbpam_hibernate] existante,
  • la fenêtre [Test Results] [6] dit que les tests ont été réussis. Ce message n'est pas significatif ici, car le programme [JUnitInitDB] ne contient aucune instruction d'assertion Assert.assertCondition, qui pourrait provoquer l'échec du test. Néanmoins, cela montre qu'il n'y a pas eu d'exception à l'exécution du test.

La fenêtre [Output] contient les logs de l'exécution, ceux de Spring et ceux du test lui-même. Les affichages faits par la classe [JUnitInitDB] sont les suivants :

------------- 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]
------------- ---------------- ---------------

Les tables [EMPLOYES, INDEMNITES, COTISATIONS] ont été remplies. On peut le vérifier avec une connexion Netbeans à la base [dbpam_hibernate].

  • en [1], dans l'onglet [services], on visualise les données de la table [employes] de la connexion [dbpam_hibernate] [2],
  • en [3] le résultat.

5.9.6. JUnitDao

Nous nous intéressons maintenant à une seconde classe de tests [JUnitDao] :

Le squelette de la classe sera le suivant :

package dao;

import exception.PamException;
...

public class JUnitDao {

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

  @BeforeClass
  public static void init() {
    // log
    log("init");
    // configuration de l'application
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-dao.xml");
    // couches 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() {
...
  }

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

  // tests
  @Test
  public void test01() {
    log("test01");
    // liste des cotisations
    List<Cotisation> cotisations = cotisationDao.findAll();
    int nbCotisations = cotisations.size();
    // on ajoute une cotisation
    Cotisation cotisation = cotisationDao.create(new Cotisation(3.49, 6.15, 9.39, 7.88));
    // on la demande
    cotisation = cotisationDao.find(cotisation.getId());
    // vérification
    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);
    // on la modifie
    cotisation.setCsgrds(-1);
    cotisation.setCsgd(-1);
    cotisation.setRetraite(-1);
    cotisation.setSecu(-1);
    Cotisation cotisation2 = cotisationDao.edit(cotisation);
    // vérifications
    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);
    // on demande l'élément modifié
    Cotisation cotisation3 = cotisationDao.find(cotisation2.getId());
    // vérifications
    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);
    // on supprime l'élément
    cotisationDao.destroy(cotisation3);
    // vérifications
    Cotisation cotisation4 = cotisationDao.find(cotisation3.getId());
    Assert.assertNull(cotisation4);
    cotisations = cotisationDao.findAll();
    Assert.assertEquals(nbCotisations, cotisations.size());
  }


  @Test
  public void test02(){
    log("test02");
    // on demande la liste des indemnités
...
    // on ajoute une Indemnite indemnite
..
    // on va chercher indemnite en base – on récupère indemnite1
..
    // on vérifie que indemnite1 = indemnite
...
    // on modifie l'indemnité obtenue et on persiste la modification en BD. On obtient indemnite2
 ...
    // on vérifie la version de indemnite2
    ...
    // on va chercher indemnite2 en base – on obtient indemnite3
    ...
    // on vérifie que indemnite3 = indemnite2
    ...
    // on supprime en base l'image de indemnite3
    ...
    // on va chercher indemnite3 en base
    ...
    // on vérifie qu'on a obtenu une référence null
 ...
  }

  @Test
  public void test03(){
    log("test03");
    // on répète un test analogue aux précédents pour Employe
 ...
  }

  @Test
  public void test04(){
    log("test04");
    // on teste la méthode [IEmployeDao].find(String SS)
    // d'abord avec un employé existant
    // puis avec un employé inexistant
...
  }

  @Test
  public void test05(){
    log("test05");
    // on crée deux indemnités avec le même indice
    // enfreint la contrainte d'unicité de l'indice
    // on vérifie qu'une exception de type PamException se produit
    // et qu'elle a le n° d'erreur attendu
...
  }

  @Test
  public void test06(){
    log("test06");
    // on crée deux employés avec le même n° SS
    // enfreint la contrainte d'unicité sur le n° SS
    // on vérifie qu'une exception de type PamException se produit
    // et qu'elle a le n° d'erreur attendu
...

  }

  @Test
  public void test07(){
    log("test07");
    // on crée deux employés avec le même n° SS, le 1er avec create, le 2ème avec edit
    // enfreint la contrainte d'unicité sur le n° SS
    // on vérifie qu'une exception de type PamException se produit
    // et qu'elle a le n° d'erreur attendu
...
  }

  @Test
  public void test08(){
    log("test08");
    // supprimer un employé qui n'existe pas ne provoque pas d'exception
    // il est ajouté puis détruit – on le vérifie
...
  }

  @Test
  public void test09(){
    log("test09");
    // modifier un employé sans avoir la bonne version doit provoquer une exception
    // on le vérifie
...
  }

  @Test
  public void test10(){
    log("test10");
    // supprimer un employé sans avoir la bonne version doit provoquer une exception
    // on le vérifie
...

  }

  // getters et setters
  ...
}

Dans la classe de tests précédente, la base est vidée avant chaque test.


Question : écrire les méthodes suivantes :

1 - test02 : on s'inspirera de test01

2 - test03 : un employé a un champ de type Indemnite. Il faut donc créer une entité Indemnite et une entité Employe

3 - test04.


En procédant de la même façon que pour la classe de tests [JUnitInitDB], on obtient les résultats suivants :

  • en [1], on exécute la classe de tests
  • en [2], les résultats des tests dans la fenêtre [Test Results]

Provoquons une erreur pour voir comment cela est signalé dans la page des résultats :

  @Test
  public void test01() {
    log("test01");
    // liste des cotisations
    List<Cotisation> cotisations = cotisationDao.findAll();
    int nbCotisations = cotisations.size();
    // on ajoute une cotisation
    Cotisation cotisation = cotisationDao.create(new Cotisation(3.49, 6.15, 9.39, 7.88));
    // on la demande
    cotisation = cotisationDao.find(cotisation.getId());
    // vérification
    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);
    // on la modifie
....
}

Ligne 13, l'assertion va provoquer une erreur, la valeur de Csgrds étant 3.49 (ligne 8). L'exécution de la classe de tests donne les résultats suivants :

  • la page des résultats [1] montre maintenant qu'il y a eu des tests non réussis.
  • en [2], un résumé de l'exception qui a fait échouer le test. On y trouve le n° de la ligne du code Java où s'est produite l'exception.

5.10. La couche [metier] de l'application [PAM]

Maintenant que la couche [DAO] a été écrite, nous passons à l'étude de la couche métier [2] :

5.10.1. L'interface Java [IMetier]

Celle-ci a été décrite au paragraphe 5.7. Nous la rappelons ci-dessous :

package metier;

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

public interface IMetier {
  // obtenir la feuille de salaire
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
  // liste des employés
  public List<Employe> findAllEmployes();
}

L'implémentation de la couche [metier] sera faite dans un paquetage [metier] :

 

Le paquetage [metier] comprendra, outre l'interface [IMetier] et son implémentation [Metier], deux autres classes [FeuilleSalaire] et [ElementsSalaire]. La classe [FeuilleSalaire] a été brièvement présentée au paragraphe 5.7. Nous revenons dessus maintenant.

5.10.2. La classe [FeuilleSalaire]

La méthode [calculerFeuilleSalaire] de l'interface [IMetier] rend un objet de type [FeuilleSalaire] qui représente les différents éléments d'une feuille de salaire. Sa définition est la suivante :

package metier;

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

public class FeuilleSalaire implements Serializable{
  // champs privés
  private Employe employe;
  private Cotisation cotisation;
  private ElementsSalaire elementsSalaire;

  // constructeurs
  public FeuilleSalaire() {

  }

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

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

  // accesseurs
...  
}
  • ligne 7 : la classe implémente l'interface Serializable parce que ses instances sont susceptibles d'être échangées sur le réseau.
  • ligne 9 : l'employé concerné par la feuille de salaire
  • ligne 10 : les différents taux de cotisation
  • ligne 11 : les différentes indemnités liées à l'indice de l'employé
  • ligne 12 : les éléments constitutifs de son salaire
  • lignes 14-22 : les deux constructeurs de la classe
  • lignes 25-27 : méthode [toString] identifiant un objet [FeuilleSalaire] particulier
  • lignes 29 et au-delà : les accesseurs publics aux champs privés de la classe

La classe [ElementsSalaire] référencée ligne 11 de la classe [FeuilleSalaire] ci-dessus, rassemble les éléments constituant une fiche de paie. Sa définition est la suivante :

package metier;

public class ElementsSalaire implements Serializable{

  // champs privés
  private double salaireBase;
  private double cotisationsSociales;
  private double indemnitesEntretien;
  private double indemnitesRepas;
  private double salaireNet;

  // constructeurs
  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 + "]";
  }

  // accesseurs publics
...  
}
  • ligne 3 : la classe implémente l'interface Serializable parce qu'elle est un composant de la classe FeuilleSalaire qui doit être sérialisable.
  • ligne 6 : le salaire de base
  • ligne 7 : les cotisations sociales payées sur ce salaire de base
  • ligne 8 : les indemnités journalières d'entretien de l'enfant
  • ligne 9 : les indemnités journalières de repas de l'enfant
  • ligne 10 : le salaire net à payer à l'assistante maternelle
  • lignes 12-24 : les constructeurs de la classe
  • lignes 27-31 : méthode [toString] identifiant un objet [ElementsSalaire] particulier
  • lignes 34 et au-delà : les accesseurs publics aux champs privés de la classe

5.10.3. La classe d'implémentation [Metier] de la couche [metier]

La classe d'implémentation [Metier] de la couche [metier] pourrait être la suivante :

package metier;

...

@Transactional
public class Metier implements IMetier {

  // référence sur la couche [DAO]
  private ICotisationDao cotisationDao = null;
  private IEmployeDao employeDao=null;


  // obtenir la feuille de salaire
  public FeuilleSalaire calculerFeuilleSalaire(String SS,
    double nbHeuresTravaillées, int nbJoursTravaillés) {
...
  }

  // liste des employés
   public List<Employe> findAllEmployes() {
     ...
  }

  // getters et setters
...
 }
  • ligne 5 : l'annotation Spring @Transactional fait que chaque méthode de la classe se déroulera au sein d'une transaction.
  • lignes 9-10 : les référence sur les couches [DAO] des entités [Cotisation, Employe, Indemnite]
  • lignes 14-17 : la méthode [calculerFeuilleSalaire]
  • lignes 20-22 : la méthode [findAllEmployes]
  • ligne 24 et au-delà : les accesseurs publics des champs privés de la classe

Question : écrire le code de la méthode [findAllEmployes].



Question : écrire le code de la méthode [calculerFeuilleSalaire].


On notera les points suivants :

  • le mode de calcul du salaire a été expliqué au paragraphe 5.2.
  • si le paramètre [SS] ne correspond à aucun employé (la couche [DAO] a renvoyé un pointeur null), la méthode lancera une exception de type [PamException] avec un code d'erreur approprié.

5.10.4. Tests de la couche [metier]

Nous créons deux programmes de test :

Les classes de tests [3] sont créés dans un paquetage [metier] [2] de la branche [Test Packages] [1] du projet.

La classe [JUnitMetier_1] pourrait être la suivante :

package metier;

...

public class JUnitMetier_1 {

// couche métier
  private IMetier metier;

  @BeforeClass
  public void init(){
    // log
    log("init");
    // configuration de l'application
    // instanciation couche [metier]
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-metier-dao.xml");
    metier = (IMetier) ctx.getBean("metier");
    // couches DAO
    IEmployeDao employeDao=(IEmployeDao) ctx.getBean("employeDao");
    ICotisationDao cotisationDao=(ICotisationDao) ctx.getBean("cotisationDao");
    IIndemniteDao indemniteDao=(IIndemniteDao) ctx.getBean("indemniteDao");
    // on vide 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);
    }
    // on la remplit
    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));
  }

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

  // test
  @Test
  public void test01(){
    // calcul de feuilles de salaire
    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()));
    }
  }
}

Il n'y a pas d'assertion Assert.assertCondition dans la classe. On cherche simplement à calculer quelques salaires afin de les vérifier ensuite à la main. L'affichage écran obtenu par l'exécution de la classe précédente est le suivant :

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
  • ligne 4 : la feuille de salaire de Justine Laverti
  • ligne 5 : la feuille de salaire de Marie Jouveinal
  • ligne 6 : l'exception due au fait que l'employé de n° SS 'xx' n'existe pas.

Question : la ligne 17 de [JUnitMetier_1] utilise le bean Spring nommé metier. Donner la définition de ce bean dans le fichier [spring-config-metier-dao.xml].


La classe [JUnitMetier_2] pourrait être la suivante :

package metier;

...
public class JUnitMetier_2 {

// couche métier
  private IMetier metier;

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

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

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

La classe [JUnitMetier_2] est une copie de la classe [JUnitMetier_1] où cette fois, des assertions ont été placées dans la méthode test01.


Question : écrire la méthode test01.


Lors de l'exécution de la classe [JUnitMetier_2], on obtient les résultats suivants si tout va bien :

Image

5.11. La couche [ui] de l'application [PAM] – version console

Maintenant que la couche [metier] a été écrite, il nous reste à écrire la couche [ui] [1] :

Nous créerons deux implémentations différentes de la couche [ui] : une version console et une version graphique swing :

5.11.1. La classe [ui.console.Main]

Nous nous intéressons tout d'abord à l'application console implémentée par la classe [ui.console.Main] ci-dessus. Son fonctionnement a été décrit au paragraphe 5.3. Le squelette de la classe [Main] pourrait être le suivant :

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) {
    // données locales
    final String syntaxe = "pg num_securite_sociale nb_heures_travaillées nb_jours_travaillés";
    // on vérifie le nombre de paramètres
...
    // liste des erreurs
    ArrayList erreurs = new ArrayList();
    // le second paramètre doit être un nombre réel >0
...
    // erreur ?
    if (...) {
      erreurs.add("Le nombre d'heures travaillées [" + args[1]
        + "] est erroné");
    }
    // le troisième paramètre doit être un nombre entier >0
...
    // erreur ?
    if (...) {
      erreurs.add("Le nombre de jours travaillés [" + args[2]
        + "] est erroné");
    }
    // des erreurs ?
    if (erreurs.size() != 0) {
      for (int i = 0; i < erreurs.size(); i++) {
        System.err.println(erreurs.get(i));
      }
      return;
    }
    // c'est bon - on peut demander la feuille de salaire
    FeuilleSalaire feuilleSalaire = null;
    try {
      // instanciation couche [metier]
      ...
      // calcul de la feuille de salaire
      ...
    } 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;
    }

    // affichage détaillé
    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";
  }
}

Question : compléter le code ci-dessus.


5.11.2. Exécution

Pour exécuter la classe [ui.console.Main], on procèdera de la façon suivante :

  • en [1], sélectionner les propriétés du projet,
  • en [2], sélectionner la propriété [Run] du projet,
  • utiliser le bouton [3] pour désigner la classe (dite classe principale) à exécuter,
  • sélectionner la classe [4],
  • la classe apparaît en [5]. Celle-ci a besoin de trois arguments pour s'exécuter (n° SS, nombre d'heures travaillées, nombre de jours travaillés). Ces arguments sont placés en [6],
  • ceci fait, on peut exécuter le projet [7]. La configuration précédente fait que c'est la classe [ui.console.Main] qui va être exécutée.

Les résultats de l'exécution sont obtenus dans la fenêtre [output] :

5.12. La couche [ui] de l'application [PAM] – version graphique

Nous implémentons maintenant la couche [ui] avec une interface graphique :

  • en [1], la classe [PamJFrame] de l'interface graphique
  • en[2] : l'interface graphique

5.12.1. Un rapide tutoriel

Pour créer l'interface graphique, on pourra procéder de la façon suivante :

  • [1] : on crée un nouveau fichier avec le bouton [1] [New File...]
  • [2] : on choisit la catégorie du fichier [Swing GUI Forms], c.a.d. formulaires graphiques
  • [3] : on choisit le type [JFrame Form], un type de formulaire vide
  • [5] : on donne un nom au formulaire qui sera aussi une classe
  • [6] : on place le formulaire dans un paquetage
  • [8] : le formulaire est ajouté à l'arborescence du projet
  • [9] : le formulaire est accessible selon deux perspectives : [Design] [9] qui permet de dessiner les différents composants du formulaire, [Source] [10 ci-dessous] qui donne accès au code Java du formulaire. Au final, un formulaire est une classe Java comme une autre. La perspective [Design] est une facilité pour dessiner le formulaire. A chaque ajout de composant en mode [Design], du code Java est ajouté dans la perspective [Source] pour le prendre en compte.
  • [11] : la liste des composants Swing disponibles pour un formulaire est trouvée dans la fenêtre [Palette].
  • [12] : la fenêtre [Inspector] présente l'arborescence des composants du formulaire. Les composants ayant une représentation visuelle se retrouveront dans la branche [JFrame], les autres dans la branche [Other Components].
  • en [13], nous sélectionnons un composant [JLabel] par un clic simple
  • en [14], nous le déposons sur le formulaire en mode [Design]
  • en [15], nous définissons les propriétés du JLabel (text, font).
  • en [16], le résultat obtenu.
  • en [17], on demande la prévisualisation du formulaire
  • en [18], le résultat
  • en [19], le label [JLabel1] a été ajouté à l'arborescence des composants dans la fenêtre [Inspector]
  • en [20] et [21] : dans la perspective [Source] du formulaire, du code Java a été ajouté pour gérer le JLabel ajouté.

Un tutoriel sur la construction de formulaires avec Netbeans est disponible à l'url [http://www.netbeans.org/kb/trails/matisse.html].

5.12.2. L'interface graphique [PamJFrame]

On construira l'interface graphique suivante :

  • en [1], l'interface graphique
  • en [2], l'arborescence de ses composants : un JLabel et six conteneurs JPanel

JLabel1

JPanel1

JPanel2

JPanel3

JPanel4

JPanel5


Travail pratique : construire l'interface graphique précédente en s'aidant du tutoriel [http://www.netbeans.org/kb/trails/matisse.html].


5.12.3. Les événements de l'interface graphique

Lectures conseillées : chapitre [Interfaces graphiques] de [ref2].

Nous gèrerons le clic sur le bouton [jButtonSalaire]. Pour créer la méthode de gestion de cet événement, on pourra procéder comme suit :

Le gestionnaire du clic sur le bouton [JButtonSalaire] est généré :

1
2
3
    private void jButtonSalaireActionPerformed(java.awt.event.ActionEvent evt) {
      // TODO add your handling code here:
}

Le code Java qui associe la méthode précédente au clic sur le bouton [JButtonSalaire] est lui aussi généré :

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);
      }
});

Ce sont les lignes 2-5 qui indiquent que le clic (evt de type ActionPerformed) sur le bouton [jButtonSalaire] (ligne 2) doit être géré par la méthode [jButtonSalaireActionPerformed] (ligne 4).

Nous gèrerons également, l'événement [caretUpdate] (déplacement du curseur de saisie) sur le champ de saisie [jTextFieldHT]. Pour créer le gestionnaire de cet événement, nous procédons comme précédemment :

Le gestionnaire de l'événement [caretUpdate] sur le champ de saisie [jTextFieldHT] est généré :

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

Le code Java qui associe la méthode précédente à l'événement [caretUpdate] sur le champ de saisie [jTextFieldHT] est lui aussi généré :

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

Les lignes 1-4 indiquent que l'événement [caretUpdate] (ligne 2) sur le bouton [jTextFieldHT] (ligne 1) doit être géré par la méthode [ jTextFieldHTCaretUpdate] (ligne 3).

5.12.4. Initialisation de l'interface graphique

Revenons à l'architecture de notre application :

La couche [ui] a besoin d'une référence sur la couche [metier]. Rappelons comment cette référence avait été obtenue dans l'application console :

1
2
3
   // instanciation couche [metier]
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-metier-dao.xml");
IMetier metier = (IMetier) ctx.getBean("metier");

La méthode est la même dans l'application graphique. Il faut que lorsque celle-ci s'initialise, la référence [IMetier metier] de la ligne 3 ci-dessus soit également initialisée. Le code généré pour l'interface graphique est pour l'instant le suivant :

package ui.swing;

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

  /** Creates new form PamJFrame */
  public PamJFrame() {
    initComponents();
  }

  /** This method is called from within the constructor to
   * 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=" Generated Code ">
  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);
      }
    });
  }

  // Variables declaration - do not modify
  private javax.swing.JButton jButtonSalaire;
...
  // End of variables declaration

}
  • lignes 29-35 : la méthode statique [main] qui lance l'application
  • ligne 32 : une instance de l'interface graphique [PamJFrame] est créée et rendue visible.
  • lignes 7-9 : le constructeur de l'interface graphique.
  • ligne 8 : appel à la méthode [initComponents] définie ligne 17. Cette méthode est auto-générée à partir du travail fait en mode [Design]. On ne doit pas y toucher.
  • ligne 21 : la méthode qui va gérer le déplacement du curseur de saisie dans le champ [jTextFieldHT]
  • ligne 25 : la méthode qui va gérer le clic sur le bouton [jButtonSalaire]

Pour ajouter au code précédent nos propres initialisations, nous pouvons procéder comme suit :

 /** Creates new form PamJFrame */
  public PamJFrame() {
    initComponents();
    doMyInit();
  }

...

  // variables d'instance
  private IMetier metier=null;
  private List<Employe> employes=null;
  private String[] employesCombo=null;
  private double heuresTravaillées=0;

  // initialisations propriétaires
  public void doMyInit(){
    // init contexte
    try{
      // instanciation couche [metier]
...
    // liste des employés
...
    }catch (PamException ex){
    // le message de l'exception est placé dans [jTextAreaStatus]
...
    // retour
      return;
    }
    // bouton salaire inhibé
...
    // jScrollPane1 caché
...
    // spinner jours travaillés
    jSpinnerJT.setModel(new SpinnerNumberModel(0,0,31,1));
    // combobox employés
    employesCombo=new String[employes.size()];
    int i=0;
    for(Employe employe : employes){
      employesCombo[i++]=employe.getPrenom()+" "+employe.getNom();
    }
    jComboBoxEmployes.setModel(new DefaultComboBoxModel(employesCombo));
}
  • ligne 4 : on appelle une méthode propriétaire pour faire nos propres initialisations. Celles-ci sont définies par le code des lignes 10-42

Question : en vous aidant des commentaires, compléter le code de la procédure [doMyInit].


5.12.5. Gestionnaires d'événements


Question : écrire la méthode [jTextFieldHTCaretUpdate]. Cette méthode doit faire en sorte que si la donnée présente dans le champ [jTextFieldHT] n'est pas un nombre réel >=0, alors le bouton [jButtonSalaire] doit être inactif.



Question : écrire la méthode [jButtonSalaireActionPerformed] qui doit afficher la feuille de salaire de l'employé sélectionné dans [jComboBoxEmployes].


5.12.6. Exécution de l'interface graphique

Pour exécuter l'interface graphique, on modifiera la configuration [Run] du projet :

  • en [1], mettre la classe de l'interface graphique

Le projet doit être complet avec ses fichiers de configuration (persistence.xml, spring-config-metier-dao.xml) et la classe de l'interface graphique. On lancera Le SGBD cible avant d'exécuter le projet.

Nous nous intéressons à l'architecture suivante où la couche JPA est désormais implémentée par EclipseLink :

5.13.1. Le projet Netbeans

Le nouveau projet Netbeans est obtenu par recopie du projet précédent :

  • en [1] : après un clic droit sur le projet Hibernate, choisir Copy
  • à l'aide du bouton [2], choisir le dossier parent du nouveau projet. Le nom du dossier apparaît en [3].
  • en [4], donner un nom au nouveau projet
  • en [5], le nom du dossier du projet
  • en [1], le nouveau projet a été créé. Il porte le même nom que l'original,
  • en [2] et [3], on le renomme [mv-pam-spring-eclipselink].

Le projet doit être modifié en deux points pour l'adapter à la nouvelle couche JPA / EclipseLink :

  1. en [4], les fichiers de configuration de Spring doivent être modifiés. On y trouve en effet la configuration de la couche JPA.
  2. en [5], les bibliothèques du projet doivent être modifiées : celles d'Hibernate doivent être remplacées par celles de EclipseLink.

Commençons par ce dernier point. Le fichier [pom.xml] pour le nouveau projet sera celui-ci :


<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>
  • lignes 73-82 : les dépendances pour l'implémentation JPA EclipseLink,
  • lignes 19-24 : le dépôt Maven pour EclipseLink.

Les fichiers de configuration de Spring doivent être modifiés pour indiquer que l'implémentation JPA a changé. Dans les deux fichiers, seule la section configurant la couche JPA change. Par exemple dans [spring-config-metier-dao.xml] on a :

<?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">

  <!-- couches applicatives -->
  <!--  DAO -->
  <bean id="employeDao" class="dao.EmployeDao" />
  <bean id="indemniteDao" class="dao.IndemniteDao" />
  <bean id="cotisationDao" class="dao.CotisationDao" />
  <!-- métier -->
  <bean id="metier" class="metier.Metier">
    <property name="employeDao" ref="employeDao"/>
    <property name="indemniteDao" ref="indemniteDao"/>
    <property name="cotisationDao" ref="cotisationDao"/>  
  </bean>

  <!-- configuration 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 source de donnéees 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>

Les lignes 19-36 configurent la couche JPA. L'implémentation JPA utilisée est Hibernate (ligne 22). Par ailleurs, la base de données cible est [dbpam_hibernate] (ligne 41).

Pour passer à une implémentation JPA / EclipseLink, les lignes 19-35 ci-dessus sont remplacées par les lignes ci-dessous :

  <!-- configuration 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>
  • ligne 5 : l'implémentation JPA utilisée est EclipseLink
  • ligne 9 : la propriété databasePlatform fixe le SGBD cible, ici MySQL
  • ligne 11 : pour générer les tables de la base de données lorsque la couche JPA est instanciée. Ici, la propriété est en commentaires.
  • ligne 7 : pour visualiser sur la console les ordres SQL émis par la couche JPA. Ici, la propriété est en commentaires.

Par ailleurs, la base de données cible devient [dbpam_eclipselink] (ligne 4 ci-dessous) :

1
2
3
4
5
6
7
8
9
<!-- la source de donnéees 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. Mise en oeuvre des tests

Avant de tester l'application entière, il est bon de vérifier si les tests JUnit passent avec la nouvelle implémentation JPA. Avant de les faire, on commencera par supprimer les tables de la base de données. Pour cela, dans l'onglet [Runtime] de Netbeans, si besoin est, on créera une connexion sur la base dbpam_eclipselink / MySQL5. Une fois connecté à la base dbpam_eclipselink / MySQL5, on pourra procéder à la suppression des tables comme montré ci-dessous :

  • [1] : avant la suppression
  • [2] : après la suppression

Ceci fait, on peut exécuter le premier test sur la couche [DAO] : InitDB qui remplit la base. Pour que les tables détruites précédemment soient recréées par l'application, il faut s'assurer que dans la configuration JPA / EclipseLink de Spring la ligne :

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

existe et n'est pas mise en commentaires.

Nous construisons le projet (Build) puis nous exécutons le test [JUnitInitDB] :

  • en [1], le test InitDB est exécuté.
  • en [2], il échoue. L'exception est lancée par Spring et non par un test qui aurait échoué.

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

Spring indique qu'il y a un problème de configuration. Le message n'est pas clair. La raison de l'exception a été expliquée au paragraphe 3.1.9 de [ref1]. Pour que la configuration Spring / EclipseLink fonctionne, la JVM qui exécute l'application doit être lancée avec un paramètre particulier, un agent Java. La forme de ce paramètre est la suivante :

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

[spring-agent.jar] est l'agent Java dont a besoin la JVM pour gérer la configuration Spring / EclipseLink.

Lorsqu'on exécute un projet, il est possible de passer des arguments à la JVM :

  • en [1], on accède aux propriétés du projet
  • en [2], les propriété du Run
  • en [3], on passe le paramètre -javaagent à la JVM

5.13.3. InitDB

Maintenant, nous sommes prêts pour tester de nouveau [InitDB]. Cette fois-ci les résultats obtenus sont les suivants :

  • en [1], le test a été réussi
  • en [2], dans l'onglet [Services], on rafraîchit la connexion qu'a Netbeans avec la base [dbpam_eclipselink]
  • en [3], quatre tables ont été créées
  • en [5], on visualise le contenu de la table [employes]
  • en [6], le résultat.

5.13.4. JUnitDao

L'exécution de la classe de tests [JUnitDao] peut échouer, même si avec l'implémentation JPA / Hibernate, elle avait réussi. Pour comprendre pourquoi, analysons un exemple.

La méthode testée est la méthode IndemniteDao.create suivante :

package dao;

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

  @PersistenceContext
  private EntityManager em;

  // constructeur
  public IndemniteDao() {
  }

  // créer une indemnité
  public Indemnite create(Indemnite indemnite) {
    try{
      em.persist(indemnite);
    }catch(Throwable th){
      throw new PamException(th,31);
    }
    return indemnite;
  }

...
}
  • lignes 15-22 : la méthode testée

La méthode de test est la suivante :


package dao;

...

public class JUnitDao {

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

  @BeforeClass
  public static void init() {
    // log
    log("init");
    // configuration de l'application
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-DAO.xml");
    // couches DAO
    employeDao = (IEmployeDao) ctx.getBean("employeDao");
    indemniteDao = (IIndemniteDao) ctx.getBean("indemniteDao");
    cotisationDao = (ICotisationDao) ctx.getBean("cotisationDao");
  }

  @Before()
  public void clean() {
    // on vide 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);
    }
  }

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

  // tests
….
  @Test
  public void test05() {
    log("test05");
    // on crée deux indemnités avec le même indice
    // enfreint la contrainte d'unicité de l'indice
    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;
      // vérifications
      Assert.assertEquals(31, ex.getCode());
    } catch (Throwable th1) {
      th = th1;
    }
    // vérifications
    Assert.assertTrue(erreur);
    // chaîne des exceptions
    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 1ère indemnité a du être persistée
    Indemnite indemnite = indemniteDao.find(indemnite1.getId());
    // vérification
    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 seconde indemnité n'a pas du être persistée
    List<Indemnite> indemnites = indemniteDao.findAll();
    int nbIndemnites = indemnites.size();
    Assert.assertEquals(nbIndemnites, 1);
  }

...
}

Question : expliquer ce que fait le test test05 et indiquer les résultats attendus.


Les résultats obtenus avec une couche JPA / Hibernate sont les suivants :

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

Le test passe, c.a.d. que les assertions sont vérifiées et il n'y a pas d'exception qui sort de la méthode de test.


Question : expliquer ce qui s'est passé.


Les résultats obtenus avec une couche JPA / EclipseLink sont les suivants :

----------- test05
[EL Warning]: 2010-06-04 16:48:26.421--UnitOfWork(749304)--Exception [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

Comme précédemment avec Hibernate, le test passe, c.a.d. que les assertions sont vérifiées et il n'y a pas d'exception qui sort de la méthode de test.


Question : expliquer ce qui s'est passé.



Question : de ces deux exemples, que peut-on conclure de l'interchangeabilité des implémentations JPA ? Est-elle totale ici ?


5.13.5. Les autres tests

Une fois la couche [DAO] testée et considérée correcte, on pourra passer aux tests de la couche [metier] et à ceux du projet lui-même dans sa version console ou graphique. Le changement d'implémentation JPA n'influe en rien sur les couches [metier] et [ui] et donc si ces couches fonctionnaient avec Hibernate, elles fonctionneront avec EclipseLink à quelques exceptions près : l'exemple précédent montre en effet que les exceptions lancées par les couches [DAO] peuvent différer. Ainsi dans le cas d'utilisation du test, Spring / JPA / Hibernate lance une exception de type [PamException], une exception propre à l'application [pam] alors que Spring / JPA / EclipseLink lui, lance une exception de type [TransactionSystemException], une exception du framework Spring. Si dans le cas d'usage du test, la couche [ui] attend une exception de type [PamException] parce qu'elle a été construite avec Hibernate, elle ne fonctionnera plus lorsqu'on passera à EclipseLink.

5.13.6. Travail à faire


Travail pratique : refaire les tests des applications console et swing avec différents SGBD : MySQL5, Oracle XE, SQL Server.