Skip to content

5. Versione 1: Architettura Spring / JPA

Proponiamo di scrivere un'applicazione console e un'applicazione grafica per generare le buste paga degli operatori dell'infanzia impiegati presso la "Maison de la petite enfance" in un comune. L'applicazione avrà la seguente architettura:

5.1. DB Il database

I dati statici necessari per generare la busta paga saranno memorizzati in un database che chiameremo dbpam. Questo database potrebbe contenere le seguenti tabelle:

Tabella EMPLOYEES: contiene informazioni sui vari fornitori di servizi di assistenza all'infanzia

Struttura:

ID
chiave primaria
VERSIONE
numero di versione – aumenta ad ogni modifica della riga
SS
Numero di previdenza sociale del dipendente – univoco
NOME
Cognome del dipendente
nome
nome
INDIRIZZO
il loro indirizzo
CITTÀ
la sua città
CAP
il loro codice postale
INDEMNITE_ID
Chiave esterna sul campo [ID] della tabella [INDEMNITES]

Il suo contenuto potrebbe essere il seguente:

Image

Tabella COTISATIONS: contiene le percentuali necessarie per il calcolo dei contributi previdenziali

Struttura:

ID
chiave primaria
VERSIONE
numero di versione – aumenta ad ogni modifica della riga
CSGRDS
Percentuale: Contributo sociale generale + Contributo al rimborso del debito sociale
CSGD
percentuale: contributo sociale generale deducibile
SECU
percentuale: previdenza sociale, vedovanza, vecchiaia
PENSIONE
percentuale: pensione integrativa + assicurazione contro la disoccupazione

Il contenuto potrebbe essere il seguente:

Image

Le aliquote dei contributi previdenziali sono indipendenti dal dipendente. La tabella precedente ha una sola riga.

Tabella ALLOWANCES: contiene gli elementi utilizzati per calcolare lo stipendio da corrispondere.
ID
chiave primaria
  
VERSIONE
numero di versione – aumenta ad ogni modifica della riga
  
INDICE
Indice di elaborazione – univoco
  
TARIFFA ORARIA
Prezzo netto in euro per un'ora di reperibilità
  
MANUTENZIONE GIORNALIERA
indennità giornaliera in euro per ogni giorno di assistenza
  
PASTO AL GIORNO
Indennità per i pasti in euro per giorno di assistenza
  
INDENNITÀ DI FERIE
Indennità per ferie pagate. Si tratta di una percentuale applicata allo stipendio base.
  
   

Il contenuto potrebbe essere il seguente:

Image

Si noti che le indennità possono variare da un operatore all’altro. Esse sono collegate a un operatore specifico tramite la sua categoria retributiva. Pertanto, la sig.ra Marie Jouveinal, che ha una categoria retributiva 2 (tabella EMPLOYEES), ha una retribuzione oraria di 2,1 euro (tabella INDEMNITES).

5.2. Metodo di calcolo dello stipendio di un assistente all'infanzia

Presenteremo ora il metodo di calcolo dello stipendio mensile di un assistente all'infanzia. Questo non intende essere il metodo effettivamente utilizzato nella pratica. Come esempio, useremo lo stipendio della signora Marie Jouveinal, che ha lavorato 150 ore in 20 giorni durante il periodo di paga.

Si tiene conto dei seguenti fattori:

[TOTALHOURS]: totale delle ore
lavorate durante il mese

[TOTALDAYS]: totale dei giorni lavorati
nel mese
[TOTALHOURS]=150
[TOTALDAYS]= 20
Lo stipendio base dell'assistente all'infanzia
è dato dalla seguente formula:
[STIPENDIOBASE]=([TOTALEORE]
*[TARIFFA ORARIA])*(1+
[INDENNITÀ CP]/100)
[STIPENDIOBASE]=
(150*[2,1])*(1+0,15)= 362,25
Da questo stipendio base
deve essere detratto da questo stipendio base
:

Contributo sociale generale e
contributo per il rimborso del
debito sociale:
 [STIPENDIOBASE]*[CSGRDS/100]

Contributo sociale generale deducibile:
 [STIPENDIO BASE]*[CSGD/100]

Previdenza sociale, prestazioni di reversibilità e di vecchiaia:
 [STIPENDIO BASE]*[SECU/100]

Pensione integrativa + AGPF +
Assicurazione contro la disoccupazione:
 [STIPENDIO BASE]*[PENSIONE/100]
CSGRDS: 12,64
CSGD: 22,28
Previdenza sociale: 34,02
Pensione: 28,55
Totale contributi previdenziali:
[CONTRIBUTI SOCIALI] =
[STIPENDIO BASE]*(CSGRDS+CSGD
+SECU+PENSION)/100
[CONTRIBUTI SOCIALI]=97,48
Inoltre, l’assistente all’infanzia ha diritto a un’indennità di soggiorno e a un’indennità di vitto per ogni giorno lavorato. Pertanto, riceve le seguenti indennità:
[Indennità] = [TOTALE GIORNI]
*(INDENNITÀ GIORNALIERA + INDENNITÀ GIORNALIERA PER I PASTI)
[INDENNITÀ]=104
Alla fine, lo stipendio netto da versare all'assistente all'infanzia è il seguente:
[STIPENDIO BASE] - [CONTRIBUTI PREVIDENZIALI] + [INDENNITÀ]
[STIPENDIO NETTO]=368,77

5.3. Come funziona l'applicazione da console

Ecco un esempio di esecuzione dell'applicazione console in una finestra 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

Scriveremo un programma che riceverà le seguenti informazioni:

  1. il numero di previdenza sociale dell'assistente all'infanzia (254104940426058 nell'esempio - riga 1)
  2. Numero totale di ore lavorate (150 nell'esempio - riga 1)
  3. Numero totale di giorni lavorati (20 nell'esempio - riga 1)

Si osserva che:

  • righe 9–14: mostrano le informazioni sul dipendente il cui numero di previdenza sociale è stato fornito
  • righe 17–20: visualizzano le aliquote dei vari contributi
  • righe 23–26: visualizzano le indennità associate al livello retributivo del dipendente (in questo caso, livello 2)
  • righe 29–33: visualizzano le componenti dello stipendio da corrispondere

L'applicazione segnala eventuali errori:

Chiamata senza parametri:


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

Chiamata con dati errati:


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é

Chiamata con un numero di previdenza sociale errato:


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. Come funziona l'applicazione grafica

L'applicazione grafica calcola gli stipendi degli operatori dell'assistenza all'infanzia utilizzando un modulo Swing:

  • Le informazioni passate come parametri al programma da console vengono ora inserite utilizzando i campi di immissione [1, 2, 3].
  • Il pulsante [4] avvia il calcolo dello stipendio
  • Il modulo visualizza le varie componenti salariali fino allo stipendio netto da corrispondere [5]

L'elenco a discesa [1, 6] non mostra i numeri di previdenza sociale dei dipendenti, ma i loro nomi e cognomi. Si presume qui che non vi siano due dipendenti con lo stesso nome e cognome.

5.5. Creazione del database

Avviamo WampServer e utilizziamo lo strumento PhpMyAdmin [1]:

  • in [2], selezioniamo l'opzione [Database],
  • in [3], creare un database [dbpam_hibernate],
  • in [4], il database è stato creato. Selezionarlo,
  • in [5], vogliamo importare uno script SQL,
  • in [6], utilizzare il pulsante [Sfoglia] per selezionare il file,
  • In [7,8], selezionare lo script SQL,
  • in [9], lo eseguiamo,
  • In [10] sono state create le tabelle. Il loro contenuto è il seguente:

Tabella EMPLOYEES

Image

tabella INDENNITÀ

Image

tabella CONTRIBUTI

Image

5.6. Implementazione JPA

5.6.1. Livello JPA / Hibernate

Configureremo il livello JPA nel seguente ambiente:

Un programma da console interagirà con il database. Per farlo, è necessario:

  • disporre di un database,
  • disporre del driver JDBC per il DBMS, in questo caso MySQL,
  • implementare il livello JPA utilizzando Hibernate,
  • scrivere il programma da console.

Creiamo il progetto Maven [mv-pam-jpa-hibernate] [1]:

Nella nostra architettura applicativa, abbiamo bisogno dei seguenti elementi:

  • il database,
  • il driver JDBC per il DBMS MySQL,
  • il livello JPA/Hibernate (entità e configurazione),
  • il programma della console di test.

5.6.1.1. Il database

Per prima cosa, creiamo un database vuoto. Avviamo WampServer e utilizziamo lo strumento PhpMyAdmin [1]:

  • in [2], selezioniamo l'opzione [Database],
  • in [3], creare un database denominato [dbpam_hibernate],
  • in [4], il database è stato creato.

5.6.1.2. Configurazione del livello JPA

La connessione tra il livello JDBC e il database viene configurata nel file [persistence.xml], che configura il livello JPA. Questo file può essere creato utilizzando NetBeans:

  • nella scheda [Services] [1], connettersi al database utilizzando il driver JDBC di MySQL [2],
  • in [3], il nome del database a cui si desidera connettersi.
  • in [4], l'URL JDBC del database,
  • in [5], accedi come root senza password,
  • in [6], è possibile testare la connessione,
  • in [7], la connessione è andata a buon fine.
  • Il collegamento compare in [8] e [9],
  • in [10], aggiungi un nuovo elemento al progetto,
  • in [11] selezionare la categoria [Persistence] e in [12] l'elemento [Persistence Unit],
  • in [13], assegnare un nome a questa unità di persistenza,
  • in [14], selezioniamo un'implementazione di Hibernate,
  • in [15], specificare la connessione appena creata al database MySQL,
  • In [16], specificare che quando il livello JPA viene istanziato, deve creare le tabelle corrispondenti alle entità JPA del progetto.

Al termine della procedura guidata viene generato il file [persistence.xml]:

  • il file appare in un nuovo ramo del progetto, nella cartella [META-INF] [1],
  • che corrisponde alla cartella [src/main/resources] del progetto [2,3].

Il suo contenuto è il seguente:


<?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>
  • Riga 3: il nome dell'unità di persistenza e il tipo di transazione. RESOURCE_LOCAL indica che il progetto gestisce autonomamente le transazioni. In questo caso, sarà il programma console a occuparsene,
  • riga 4: l'implementazione JPA utilizzata è Hibernate,
  • righe 6–9: le proprietà JDBC per la connessione al database,
  • riga 11: richiede la creazione delle tabelle corrispondenti alle entità JPA. In realtà, NetBeans genera qui una configurazione errata. La configurazione dovrebbe essere la seguente:

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

Con l'opzione create, Hibernate, al momento dell'istanziazione del livello JPA, elimina e poi crea le tabelle corrispondenti alle entità JPA. L'opzione create-drop fa la stessa cosa ma, alla fine del ciclo di vita del livello JPA, elimina tutte le tabelle. Esiste un'altra opzione:


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

Questa opzione crea le tabelle se non esistono, ma non le elimina se sono già presenti.

Aggiungeremo altre tre proprietà alla configurazione di Hibernate:


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

Queste impostazioni indicano a Hibernate di visualizzare le istruzioni SQL che invia al database. Il file completo è quindi il seguente:


<?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. Dipendenze

Torniamo all'architettura del progetto:

Abbiamo configurato il livello JPA tramite il file [persistence.xml]. L'implementazione scelta è stata Hibernate. Ciò ha introdotto delle dipendenze nel progetto:

  

Queste dipendenze sono dovute all'inclusione di Hibernate nel progetto. Dobbiamo aggiungere un'altra dipendenza: il driver JDBC di MySQL, che implementa il livello JDBC dell'architettura. Aggiorniamo il file [pom.xml] come segue:


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

Le righe 8–12 aggiungono la dipendenza per il driver JDBC di MySQL.

5.6.1.4. Entità JPA


Domanda: Seguendo l'approccio illustrato nell'esempio della Sezione 4.4, generare le entità [Cotisation, Indemnite, Employe].


Note:

  • Le entità faranno parte di un pacchetto denominato [jpa],
  • ogni entità avrà un numero di versione,
  • se due entità sono collegate da una relazione, verrà creata solo la relazione primaria @ManyToOne. La relazione inversa @OneToMany non verrà creata.

5.6.1.5. Il codice per la classe principale

Includiamo nel progetto le entità JPA sviluppate in precedenza [1]:

quindi aggiungiamo [2], la seguente classe [main.Main]:


package main;
 
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
 
public class Main {
 
  public static void main(String[] args) {
    // creating the Entity Manager is enough to build the JPA layer
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("mv-pam-jpa-hibernatePU");
    EntityManager em=emf.createEntityManager();
    // resource release
    em.close();
    emf.close();
  }
}
  • Riga 10: creiamo l'EntityManagerFactory per l'unità di persistenza denominata [mv-pam-jpa-hibernatePU]. Questo nome deriva dal file [persistence.xml]:

  <persistence-unit name="mv-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
    ...
  </persistence-unit>
  • Riga 12: Viene creato l'EntityManager. Questo crea il livello JPA. Verrà utilizzato il file [persistence.xml] e quindi verranno create le tabelle del database.
  • Righe 14–15: le risorse vengono rilasciate.

5.6.1.6. Test

Torniamo all'architettura del nostro progetto:

Tutti i livelli sono stati implementati. Eseguiamo il progetto [2].

L'output della console è il seguente:

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

La console contiene solo i log di Hibernate, poiché il programma eseguito non fa altro che istanziare il livello JPA. Si notino i seguenti punti:

  • riga 43: Hibernate tenta di eliminare la chiave esterna dalla tabella [EMPLOYEES],
  • righe 51–55: eliminazione delle tre tabelle,
  • riga 57: creazione della tabella [COTISATIONS],
  • riga 67: creazione della tabella [EMPLOYEES],
  • riga 80: creazione della tabella [INDEMNITIES],
  • riga 91: creazione della chiave esterna per la tabella [EMPLOYEES].

In NetBeans, è possibile visualizzare le tabelle nella connessione creata in precedenza:

Le tabelle create dipendono sia dall'implementazione del livello JPA utilizzata sia dal DBMS utilizzato. Pertanto, un'implementazione JPA/EclipseLink con lo stesso database può generare tabelle diverse. È proprio questo che vedremo ora.

Creeremo un nuovo progetto Maven nel seguente ambiente:

Seguiremo i passaggi descritti nella sezione precedente:

  1. creare un database MySQL [dbpam_eclipselink]. Useremo lo script [dbpam_eclipselink.sql] per generarlo,
  2. creare il file [persistence.xml] del progetto. Utilizzare l'implementazione EclipseLink JPA 2.0,
  3. aggiungere la dipendenza del driver JDBC di MySQL alle dipendenze generate,
  4. aggiungere le entità JPA e il programma console,
  5. eseguire i test.

Il file [persistence.xml] sarà il seguente:


<?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>
  • Le proprietà da 9 a 13 sono state generate dalla procedura guidata di NetBeans,
  • Riga 14: questa proprietà ci permette di impostare il livello di log di EclipseLink. Il livello FINE ci permette di vedere le istruzioni SQL che EclipseLink eseguirà sul database,
  • Riga 15: Quando il livello JPA/EclipseLink viene istanziato, le tabelle delle entità JPA verranno eliminate e poi create.

L'output della console è il seguente:

------------------------------------------------------------------------
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
  • righe 26-30: connessione al database MySQL,
  • righe 31-34: conferma che la connessione è andata a buon fine,
  • riga 36: eliminazione della chiave esterna dalla tabella [EMPLOYEES],
  • riga 37: eliminazione della tabella [COTISATIONS],
  • riga 38: creazione della tabella [CONTRIBUTIONS]. Vale la pena notare che l'ID della chiave primaria non ha l'attributo auto_increment di MySQL. Ciò significa che MySQL non genera i valori della chiave primaria,
  • riga 39: eliminazione della tabella [EMPLOYEES],
  • riga 40: creazione della tabella [EMPLOYEES]. La sua chiave primaria ID non ha l'attributo auto_increment di MySQL,
  • riga 41: eliminazione della tabella [INDEMNITIES],
  • riga 42: creazione della tabella [INDEMNITIES]. La sua chiave primaria ID non ha l'attributo auto_increment di MySQL,
  • riga 43: creazione di una chiave esterna dalla tabella [EMPLOYEES] alla tabella [BENEFITS],
  • riga 44: creazione di una tabella [SEQUENCE]. Sarà utilizzata per generare le chiavi primarie per le tre tabelle precedenti,
  • riga 47: si verifica un'eccezione perché questa tabella esisteva già,
  • righe 51–53: inizializzazione della tabella [SEQUENCE].

L'esistenza delle tabelle generate può essere verificata in NetBeans [1]:

Pertanto, basandosi sulle stesse entità JPA, le implementazioni JPA di Hibernate ed EclipseLink non generano le stesse tabelle. Nel resto di questo documento, quando l'implementazione JPA utilizzata è:

  • Hibernate, useremo il database [dbpam_hibernate],
  • EclipseLink, useremo il database [dbpam_eclipselink].

5.6.3. Lavoro da svolgere

Seguendo la stessa procedura di prima,

  1. crea e testa un progetto [mv-pam-jpa-hibernate-oracle] utilizzando un'implementazione JPA di Hibernate e un DBMS Oracle,
  2. crea e testa un progetto [mv-pam-jpa-hibernate-mssql] utilizzando un'implementazione JPA di Hibernate e un DBMS SQL Server,
  3. crea e testa un progetto [mv-pam-jpa-eclipselink-oracle] utilizzando un'implementazione JPA di EclipseLink e un DBMS Oracle,
  4. creare e testare un progetto [mv-pam-jpa-eclipselink-mssql] utilizzando un'implementazione JPA di EclipseLink e un DBMS SQL Server,

5.6.4. Lazy o Eager?

Torniamo a una possibile definizione dell'entità [Employee]:


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

Le righe 27–29 definiscono la chiave esterna dalla tabella [EMPLOYEES] alla tabella [INDEMNITIES]. L'attributo fetch alla riga 27 definisce la strategia di recupero per il campo indemnity alla riga 29. Esistono due modalità:

  • FetchType.LAZY: quando viene interrogato un dipendente, l'indennità corrispondente non viene recuperata. Verrà recuperata quando il campo [Employee].indemnity viene referenziato per la prima volta.
  • FetchType.EAGER: quando si cerca un dipendente, viene recuperata l'indennità corrispondente. Questa è la modalità predefinita quando non viene specificata alcuna modalità.

Per comprendere il vantaggio dell'opzione FetchType.LAZY, si consideri il seguente esempio. In una pagina web viene visualizzato un elenco di dipendenti senza la relativa retribuzione, con un link [Dettagli]. Facendo clic su questo link viene visualizzata la retribuzione del dipendente selezionato. Si osserva che:

  • Per visualizzare la prima pagina, non è necessario includere i dipendenti e i relativi benefici. È quindi opportuno utilizzare la modalità FetchType.LAZY;
  • per visualizzare la seconda pagina con i dettagli, è necessario effettuare una query aggiuntiva al database per recuperare i benefici del dipendente selezionato.

La modalità FetchType.LAZY impedisce il recupero di una quantità eccessiva di dati di cui l'applicazione non ha bisogno immediatamente. Vediamo un esempio.

Il progetto [mv-pam-jpa-hibernate] viene duplicato:

  • in [1] il progetto viene copiato,
  • in [2] specifichiamo la cartella per la copia e in [3] il suo nome,
  • in [4], il nuovo progetto ha lo stesso nome di quello vecchio. Modifichiamo questo:
  • in [1], rinominiamo il progetto,
  • in [2], rinominiamo il progetto e il suo artifactId,
  • in [3], il nuovo progetto.

Modifichiamo il programma [Main.java] come segue:


package main;
 
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import jpa.Employe;
 
public class Main {
 
  // the JPQL query below brings back an employee
  // the foreign key [Employe].indemnite is in FetchType.LAZY
  public static void main(String[] args) {
    // creating the Entity Manager is enough to build the JPA layer
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("pam-jpa-hibernatePU");
    // first attempt
    EntityManager em = emf.createEntityManager();
    Employe employe = (Employe) em.createQuery("select e from Employe e where e.nom=:nom").setParameter("nom", "Jouveinal").getSingleResult();
    em.close();
    // we display the employee
    try {
      System.out.println(employe);
    } catch (Exception ex) {
      System.out.println(ex);
    }
    // second test
    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();
    // free up resources
    em.close();
    // we display the employee
    try {
      System.out.println(employe);
    } catch (Exception ex) {
      System.out.println(ex);
    }
    // resource release
    emf.close();
  }
}
  • riga 15: creiamo l'EntityManagerFactory per il livello JPA,
  • riga 17: otteniamo l'EntityManager, che ci permette di interagire con il livello JPA,
  • riga 18: recuperiamo il dipendente di nome Jouveinal,
  • riga 19: chiudiamo l'EntityManager. Questo chiude il contesto di persistenza.
  • riga 22: visualizziamo il dipendente recuperato.

La classe [Employee] è la seguente:


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()
    +"]";
  }
  ...
}
  • riga 27: il campo indemnite viene recuperato in modalità LAZY,
  • riga 47: utilizza il campo indemnite. Se il metodo toString viene chiamato mentre il campo indemnite non è ancora stato recuperato, verrà recuperato in quel momento. A meno che il contesto di persistenza non sia stato chiuso, come nell'esempio.

Torniamo al codice [Main]:

  • righe 21–25: dovremmo ottenere un'eccezione. Questo perché verrà chiamato il metodo toString. Esso utilizzerà il campo indemnite. Questo campo verrà cercato. Poiché il contesto di persistenza è stato chiuso, l'entità [Employee] recuperata non esiste più, da qui l'eccezione.
  • riga 27: creiamo un nuovo EntityManager,
  • riga 28: recuperiamo il dipendente Jouveinal richiedendo esplicitamente l'indennità associata nella query JPQL. Questa richiesta esplicita è necessaria perché la modalità di recupero per questa indennità è LAZY,
  • Riga 30: chiudiamo l'EntityManager,
  • Righe 32–36: visualizziamo nuovamente il dipendente. Non dovrebbe verificarsi alcuna eccezione.

Per eseguire il progetto, è necessario disporre di un database popolato. È possibile crearlo seguendo i passaggi descritti nella sezione 5.5. Inoltre, è necessario modificare il file [persistence.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="mv-pam-jpa-hibernatePU" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <class>jpa.Cotisation</class>
    <class>jpa.Employe</class>
    <class>jpa.Indemnite</class>
    <properties>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dbpam_hibernate"/>
      <property name="javax.persistence.jdbc.password" value=""/>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.user" value="root"/>
      <property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
    </properties>
  </persistence-unit>
</persistence>
  • Abbiamo rimosso l'opzione che creava le tabelle. Il database qui esiste già ed è popolato,
  • abbiamo rimosso le opzioni che facevano sì che Hibernate registrasse le istruzioni SQL inviate al database.

L'esecuzione del progetto genera i seguenti due messaggi nella 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]
  • Riga 1: L'eccezione verificatasi durante il tentativo di recuperare l'indennità mancante mentre la sessione era chiusa. Si può notare che l'indennità non è stata recuperata a causa della modalità LAZY,
  • riga 2: il dipendente con la sua indennità recuperata tramite una query che ha bypassato la modalità LAZY.

5.6.5. Lavoro da svolgere

Seguendo una procedura simile a quella appena descritta, creare un progetto [mv-pam-pa-eclipselink-lazy] che dimostri il comportamento di EclipseLink con la modalità LAZY.

Si ottengono i seguenti risultati:

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]

In modalità LAZY, entrambe le query hanno restituito la retribuzione insieme al dipendente. Ricercando questa anomalia online, scopriamo che l'annotazione [FetchType.LAZY] (riga 1):


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

non è un requisito ma un suggerimento. L'implementatore JPA non è obbligato a seguirlo. Possiamo quindi notare che il codice a volte diventa dipendente dall'implementazione JPA utilizzata. È possibile configurare EclipseLink affinché si comporti come previsto in modalità LAZY.

5.6.6. Andando avanti

L'architettura dell'applicazione da realizzare è la seguente:

Nel resto di questo documento, duplicheremo il progetto Maven [mv-pam-jpa-hibernate] nel progetto [mv-pam-spring-hibernate] [1, 2, 3]:

  • quindi rinomineremo il nuovo progetto [4, 5, 6].

Modificheremo le dipendenze del nuovo progetto. Il file [pom.xml] assumerà il seguente aspetto:


<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>
  • righe 25–31: la dipendenza per i test JUnit,
  • righe 32–41: dipendenze per il pool di connessioni Apache DBCP,
  • righe 42–65: dipendenze per il framework Spring,
  • righe 67–71: dipendenze per l'implementazione JPA/Hibernate,
  • righe 72–76: la dipendenza per il driver JDBC di MySQL,
  • righe 77–81: la dipendenza per l'interfaccia Swing. Questa viene aggiunta automaticamente da NetBeans quando si aggiunge un'interfaccia Swing al progetto.

Inoltre, genereremo i due database MySQL:

  • [dbpam_hibernate] dallo script [dbpam_hibernate.sql],
  • [dbpam_eclipselink] dallo script [dbpam_eclipselink.sql],

5.7. L'interfaccia im i per i livelli [business] e [DAO]

Torniamo all'architettura dell'applicazione:

Nell'architettura sopra riportata, quale interfaccia dovrebbe fornire il livello [DAO] al livello [business] e quale interfaccia dovrebbe fornire il livello [business] al livello [UI]? Un primo approccio alla definizione delle interfacce dei diversi livelli consiste nell'esaminare i vari casi d'uso dell'applicazione. Qui ne abbiamo due, a seconda dell'interfaccia utente scelta: console o modulo grafico.

Esaminiamo come viene utilizzata l'applicazione da 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'app riceve tre informazioni dall'utente (vedi riga 1 sopra)

  • il numero di previdenza sociale dell’assistente all’infanzia
  • il numero di ore lavorate nel mese
  • il numero di giorni lavorati nel mese

Sulla base di queste informazioni e di altri dati memorizzati nei file di configurazione, l'applicazione visualizza le seguenti informazioni:

  • righe 4–6: i valori inseriti
  • righe 8–10: informazioni relative al dipendente di cui è stato fornito il numero di previdenza sociale
  • righe 12–14: le aliquote dei vari contributi previdenziali
  • righe 16–17: le varie indennità corrisposte all’assistente all’infanzia
  • righe 19–24: voci presenti sulla busta paga del fornitore di servizi di assistenza all’infanzia

Il livello [aziendale] deve fornire una serie di informazioni al livello [UI]:

  1. informazioni relative a un fornitore di servizi di assistenza all'infanzia identificato tramite il proprio numero di previdenza sociale. Queste informazioni si trovano nella tabella [EMPLOYEES]. Ciò consente la visualizzazione delle righe 6–8.
  2. gli importi delle varie aliquote contributive previdenziali da detrarre dallo stipendio lordo. Queste informazioni si trovano nella tabella [COTISATIONS]. Ciò consente la visualizzazione delle righe 10–12.
  3. gli importi delle varie indennità relative al ruolo di assistente all'infanzia. Queste informazioni si trovano nella tabella [INDEMNITES]. Ciò consente di visualizzare le righe 14-15.
  4. le componenti dello stipendio visualizzate nelle righe 18-22.

Da ciò, potremmo decidere un'implementazione iniziale dell'interfaccia [IMetier] presentata dal livello [metier] al livello [ui]:

1
2
3
4
5
6
package metier;

public interface IMetier {
   // get your payslip
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
}
  • riga 1: gli elementi del livello [business] sono collocati nel pacchetto [business]
  • riga 5: il metodo [calculatePaystub] accetta come parametri le tre informazioni ottenute dal livello [ui] e restituisce un oggetto di tipo [Paystub] contenente le informazioni che il livello [ui] visualizzerà sulla console. La classe [ Paystub] potrebbe essere la seguente:
package metier;

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

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

  ...
}
  • riga 9: il dipendente a cui si riferisce la busta paga - informazione n. 1 visualizzata dal livello [ui]
  • riga 10: le varie aliquote contributive - informazione n. 2 visualizzata dal livello [ui]
  • riga 11: le varie indennità collegate all'indice del dipendente - informazione n. 3 visualizzata dal livello [ui]
  • riga 12: le componenti del loro stipendio - informazione n. 4 visualizzata dal livello [ui]

Un secondo caso d'uso per il livello [business] si presenta con l'interfaccia grafica:

Come mostrato sopra, l'elenco a discesa [1, 2] visualizza tutti i dipendenti. Questo elenco deve essere richiesto al livello [business]. L' ace di interfaccia per questo livello si evolve quindi come segue:

package metier;

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

public interface IMetier {
   // get your payslip
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
   // list of employees
  public List<Employe> findAllEmployes();
}
  • riga [10]: il metodo che consentirà al livello [UI] di richiedere l'elenco di tutti i dipendenti al livello [Business].

Il livello [business] può inizializzare i campi [Dipendente, Contributo, Indennità] dell'oggetto [Libro paga] sopra indicato solo interrogando il livello [DAO], poiché tali informazioni sono memorizzate nelle tabelle del database. Lo stesso vale per il recupero dell'elenco di tutti i dipendenti. Potremmo creare un'unica interfaccia [DAO] per gestire l'accesso alle tre entità [Employee, Contribution, Allowance]. Tuttavia, in questo caso abbiamo deciso di creare un'interfaccia [DAO] per ogni entità.

L'interfaccia [DAO] per l'accesso alle entità [Contribution] nella tabella [CONTRIBUTIONS] sarà la seguente:

package dao;

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

public interface ICotisationDao {
       // create a new contribution
  public Cotisation create(Cotisation cotisation);
       // modify an existing contribution
  public Cotisation edit(Cotisation cotisation);
       // delete an existing contribution
  public void destroy(Cotisation cotisation);
       // search for a specific contribution
  public Cotisation find(Long id);
       // get all objects Contribution
  public List<Cotisation> findAll();

}
  • Riga 6: L'interfaccia [ICotisationDao] gestisce l'accesso all'entità [Cotisation] e quindi alla tabella [COTISATIONS] nel database. La nostra applicazione necessita solo del metodo [findAll] alla riga 16, che recupera tutti i contenuti della tabella [COTISATIONS]. In questo caso, abbiamo voluto affrontare un caso più generale in cui tutte le operazioni CRUD (Create, Read, Update, Delete) vengono eseguite sull'entità.
  • Riga 8: Il metodo [create] crea una nuova entità [Cotisation]
  • Riga 10: Il metodo [edit] modifica un'entità [Cotisation] esistente
  • Riga 12: il metodo [destroy] elimina un'entità [Cotisation] esistente
  • Riga 14: il metodo [find] recupera un'entità [Cotisation] esistente utilizzando il suo id
  • Riga 16: Il metodo [findAll] restituisce un elenco di tutte le entità [Membership] esistenti

Diamo un'occhiata più da vicino alla firma del metodo [create]:

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

Il metodo create ha un parametro cotisation di tipo Cotisation. Il parametro cotisation deve essere persistito, ovvero memorizzato nella tabella [COTISATIONS]. Prima di questa persistenza, il parametro cotisation ha un identificatore id senza valore. Dopo la persistenza, il campo id ha un valore che è la chiave primaria del record aggiunto alla tabella [COTISATIONS]. Il parametro cotisation è quindi un parametro di input/output del metodo create. Non sembra necessario che il metodo create restituisca inoltre il parametro cotisation come risultato. Poiché il metodo chiamante detiene un riferimento all'oggetto [Cotisation cotisation], se questo viene modificato, il metodo chiamante ha accesso all'oggetto modificato poiché ne detiene un riferimento. Può quindi conoscere il valore che il metodo create ha assegnato al campo id dell'oggetto [Cotisation cotisation]. La firma del metodo potrebbe quindi essere semplificata in:

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

Quando si scrive un'interfaccia, è importante ricordare che può essere utilizzata in due contesti diversi: locale e remoto . Nel contesto locale, il metodo chiamante e il metodo chiamato vengono eseguiti nella stessa JVM:

Se il livello [business] chiama il metodo create del livello [DAO], dispone effettivamente di un riferimento al parametro [Membership membership] che passa al metodo.

Nel contesto remoto, il metodo chiamante e il metodo chiamato vengono eseguiti in JVM diverse:

Nell'esempio sopra riportato, il livello [business] viene eseguito nella JVM 1 e il livello [DAO] nella JVM 2 su due macchine diverse. I due livelli non comunicano direttamente. Tra di essi si trova un livello che chiameremo livello di comunicazione [1]. Questo è costituito da un livello di trasmissione [2] e da un livello di ricezione [3]. Lo sviluppatore generalmente non deve scrivere questi livelli di comunicazione. Essi vengono generati automaticamente da strumenti software. Il livello [business] viene scritto come se fosse in esecuzione nella stessa JVM del livello [DAO]. Pertanto, non sono necessarie modifiche al codice.

Il meccanismo di comunicazione tra il livello [business] e il livello [DAO] è il seguente:

  • il livello [business] chiama il metodo create del livello [DAO], passando il parametro [Contribution contribution1]
  • questo parametro viene effettivamente passato al livello di trasmissione [2]. Questo livello trasmetterà il valore del parametro cotisation1 sulla rete, non il suo riferimento. La forma esatta di questo valore dipende dal protocollo di comunicazione utilizzato.
  • Il livello di ricezione [3] recupera questo valore e lo utilizza per ricostruire un oggetto [Cotisation cotisation2] che rispecchia il parametro iniziale inviato dal livello [business]. Ora abbiamo due oggetti identici (in termini di contenuto) in due JVM diverse: cotisation1 e cotisation2.
  • Il livello di presentazione passerà l'oggetto `contribution2` al metodo `create` del livello [DAO], che lo renderà persistente nel database. Dopo questa operazione, il campo `id` dell'oggetto `contribution2` è stato inizializzato con la chiave primaria del record aggiunto alla tabella [COTISATIONS]. Questo non è il caso dell'oggetto `contribution1`, a cui il livello [business] fa riferimento. Se vogliamo che il livello [business] abbia un riferimento all'oggetto cotisation2, dobbiamo passarlo al livello. Pertanto, dobbiamo modificare la firma del metodo create nel livello [DAO]:
      // créer une nouvelle cotisation
Cotisation create(Cotisation cotisation);
  • Con questa nuova firma, il metodo create restituirà l'oggetto persistito contribution2. Questo risultato viene restituito al livello ricevente [3] che ha chiamato il livello [DAO]. Il livello [DAO] restituirà il valore (non il riferimento) di contribution2 al livello mittente [2].
  • Il livello mittente [2] recupererà questo valore e lo utilizzerà per ricostruire un oggetto [Membership membership3] che rispecchia il risultato restituito dal metodo create del livello [DAO].
  • L'oggetto [Contribution contribution3] viene restituito al metodo nel livello [business] la cui chiamata al metodo create del livello [DAO] aveva avviato l'intero meccanismo. Il livello [business] può quindi determinare il valore della chiave primaria assegnato all'oggetto [Contribution contribution1] per il quale aveva richiesto la persistenza: questo è il valore del campo id in contribution3.

L'architettura precedente non è la più comune. Più frequentemente, i livelli [business] e [DAO] si trovano nella stessa JVM:

In questa architettura, sono i metodi del livello [business] che devono restituire i risultati, non quelli del livello [DAO]. Tuttavia, la seguente firma del metodo create del livello [DAO]:

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

ci permette di evitare di fare ipotesi sull'effettiva architettura in uso. Usare firme che funzionano indipendentemente dall'architettura scelta — sia essa locale o remota — significa che se un metodo chiamato modifica alcuni dei suoi parametri:

  • anche questi devono far parte del risultato del metodo chiamato
  • il metodo chiamante deve utilizzare il risultato del metodo chiamato e non i riferimenti ai parametri modificati che ha passato al metodo chiamato.

Questo ci permette di passare da un'architettura locale a un'architettura remota senza modificare il codice. Rivediamo l'interfaccia [ICotisationDao] in questa ottica:

package dao;

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

public interface ICotisationDao {
       // create a new contribution
  public Cotisation create(Cotisation cotisation);
       // modify an existing contribution
  public Cotisation edit(Cotisation cotisation);
       // delete an existing contribution
  public void destroy(Cotisation cotisation);
       // search for a specific contribution
  public Cotisation find(Long id);
       // get all objects Contribution
  public List<Cotisation> findAll();

}
  • riga 8: il caso del metodo create è stato gestito
  • Riga 10: il metodo edit utilizza il suo parametro [Cotisation cotisation1] per aggiornare il record nella tabella [COTISATIONS] che ha la stessa chiave primaria dell'oggetto cotisation. Restituisce l'oggetto cotisation2, che è una rappresentazione del record modificato. Il parametro contribution1 non viene modificato. Il metodo deve restituire contribution2 come risultato, sia in un'architettura remota che locale.
  • Riga 12: Il metodo destroy elimina il record dalla tabella [COTISATIONS] che ha la stessa chiave primaria dell'oggetto contribution passato come parametro. L'oggetto contribution non viene modificato. Pertanto, non è necessario restituirlo.
  • Riga 14: Il parametro id del metodo find non viene modificato dal metodo. Non è necessario includerlo nel risultato.
  • Riga 16: Il metodo findAll non ha parametri. Non è quindi necessario esaminarlo.

In definitiva, solo la firma del metodo create deve essere adattata per essere utilizzabile all'interno di un'architettura remota. Il ragionamento di cui sopra si applica alle altre interfacce [DAO]. Non lo ripeteremo qui e useremo invece firme utilizzabili sia in architetture remote che locali.

L'interfaccia [DAO] per l'accesso alle entità [Indemnite] nella tabella [INDEMNITES] sarà la seguente:

package dao;

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

public interface IIndemniteDao {
     // create an Indemnity entity
  public Indemnite create(Indemnite indemnite);
     // modify an Indemnite entity
  public Indemnite edit(Indemnite indemnite);
     // delete an Indemnity entity
  public void destroy(Indemnite indemnite);
     // search for an Indemnite entity via its identifier
  public Indemnite find(Long id);
     // get all Indemnite entities
  public List<Indemnite> findAll();

}
  • Riga 6: L'interfaccia [IIndemniteDao] gestisce l'accesso all'entità [Indemnite] e quindi alla tabella [INDEMNITES] nel database. La nostra applicazione necessita solo del metodo [findAll] alla riga 16, che recupera l'intero contenuto della tabella [INDEMNITES]. In questo caso, abbiamo voluto affrontare un caso più generale in cui tutte le operazioni CRUD (Create, Read, Update, Delete) vengono eseguite sull'entità.
  • Riga 8: Il metodo [create] crea una nuova entità [Indemnite]
  • Riga 10: Il metodo [edit] modifica un'entità [Indemnite] esistente
  • Riga 12: il metodo [destroy] elimina un'entità [Indemnite] esistente
  • Riga 14: il metodo [find] recupera un'entità [Indemnite] esistente utilizzando il suo id
  • Riga 16: Il metodo [findAll] restituisce un elenco di tutte le entità [Indemnite] esistenti

L'interfaccia [DAO] per accedere alle entità [Employe] nella tabella [EMPLOYES] sarà la seguente:

package dao;

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

public interface IEmployeDao {
     // create a new Employ entity
  public Employe create(Employe employe);
     // modify an existing Employe entity
  public Employe edit(Employe employe);
     // delete an Employ entity
  public void destroy(Employe employe);
     // search for an Employe entity via its id identifier
  public Employe find(Long id);
     // search for an Employe entity via its SS number
  public Employe find(String SS);
     // get all Employe entities
  public List<Employe> findAll();
}
  • Riga 6: L'interfaccia [IEmployeDao] gestisce l'accesso all'entità [Employee] e quindi alla tabella [EMPLOYEES] nel database. La nostra applicazione necessita solo del metodo [findAll] alla riga 16, che recupera tutti i contenuti della tabella [EMPLOYEES]. In questo caso, volevamo affrontare un caso più generale in cui tutte le operazioni CRUD (Create, Read, Update, Delete) vengono eseguite sull'entità.
  • Riga 8: Il metodo [create] crea una nuova entità [Employee]
  • Riga 10: Il metodo [edit] modifica un'entità [Employee] esistente
  • Riga 12: il metodo [destroy] elimina un'entità [Employee] esistente
  • Riga 14: il metodo [find] recupera un'entità [Employee] esistente tramite il suo ID
  • Riga 16: Il metodo [find(String SS)] recupera un'entità [Employee] esistente utilizzando il suo numero SS. Abbiamo visto che questo metodo era necessario per l'applicazione console.
  • Riga 18: il metodo [findAll] restituisce un elenco di tutte le entità [Employee] esistenti. Abbiamo visto che questo metodo era necessario per l'applicazione grafica.

5.8. La classe [PamException]

Il livello [DAO] funzionerà con l'API JDBC di Java. Questa API genera eccezioni [SQLException] controllate, che presentano due svantaggi:

  • appesantiscono il codice, che deve gestire queste eccezioni utilizzando blocchi try/catch.
  • devono essere dichiarate nelle firme dei metodi dell'interfaccia [IDao] utilizzando "throws SQLException". Ciò impedisce l'implementazione di questa interfaccia da parte di classi che genererebbero un'eccezione controllata di tipo diverso da [SQLException].

Per risolvere questo problema, il livello [DAO] "propagherà" solo eccezioni non controllate di tipo [PamException].

  • Il livello [JDBC] genera eccezioni di tipo [SQLException]
  • Il livello [JPA] genera eccezioni specifiche dell'implementazione JPA in uso
  • il livello [DAO] genera eccezioni non intercettate di tipo [PamException]

Ciò comporta due conseguenze:

  • Il livello [business] non sarà tenuto a gestire le eccezioni provenienti dal livello [DAO] utilizzando blocchi try/catch. Può semplicemente lasciarle propagare fino al livello [UI].
  • I metodi dell'interfaccia [IDao] non devono specificare la natura della [PamException] nelle loro firme, il che lascia aperta la possibilità di implementare questa interfaccia con classi che genererebbero un altro tipo di eccezione non intercettata.

La classe [PamException] verrà inserita nel pacchetto [exception] del progetto NetBeans:

Il suo codice è il seguente:

package exception;

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

   // error code
  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 and setter

  public int getCode() {
    return code;
  }

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

}
  • Riga 4: [PamException] deriva da [RuntimeException]. Si tratta quindi di un tipo di eccezione che il compilatore non richiede di gestire con un blocco try/catch né di includere nelle firme dei metodi. Per questo motivo, [PamException] non è inclusa nelle firme dei metodi dell'interfaccia [IDao]. Ciò consente all'interfaccia di essere implementata da una classe che genera un diverso tipo di eccezione, a condizione che derivi anch'essa da [RuntimeException].
  • Per distinguere tra gli errori che possono verificarsi, utilizziamo il codice di errore nella riga 7. I tre costruttori nelle righe 14, 19 e 24 sono quelli della classe padre [RuntimeException], a cui abbiamo aggiunto un parametro: il codice di errore che vogliamo assegnare all'eccezione.

Il comportamento dell'applicazione, dal punto di vista delle eccezioni, sarà il seguente:

  • Il livello [DAO] incapsulerà qualsiasi eccezione incontrata all'interno di una [PamException] e la rilancerà al livello [business].
  • Il livello [business] consentirà alle eccezioni lanciate dal livello [DAO] di propagarsi verso l'alto. Incapsulerà qualsiasi eccezione che si verifichi nel livello [business] in una [PamException] e la rilancerà al livello [UI].
  • Il livello [UI] intercetta tutte le eccezioni propagate dai livelli [business] e [DAO]. Si limiterà a visualizzare l'eccezione sulla console o sull'interfaccia utente grafica.

Esaminiamo ora l'implementazione dei livelli [DAO] e [business] uno per uno.

5.9. Il livello [DAO] dell'applicazione [PAM]

Stiamo lavorando all'interno della seguente architettura:

5.9.1. Implementazione

Lettura consigliata: Sezione 3.1.3 di [rif1]


Domanda: Utilizzando l'integrazione Spring/JPA, scrivi le classi [CotisationDao, IndemniteDao, EmployeDao] per implementare le interfacce [ICotisationDao, IIndemniteDao, IEmployeDao]. Ogni metodo di classe intercetterà qualsiasi eccezione e la racchiuderà in un oggetto [PamException] con un codice di errore specifico per l'eccezione intercettata.


Le classi di implementazione faranno parte del pacchetto [dao]:

  

5.9.2. Configurazione

Lettura consigliata: Sezione 3.1.5 di [rif. 1]

L'integrazione DAO/JPA è configurata dal file Spring [spring-config-dao.xml] e dal file JPA [persistence.xml]:


Domanda: Scrivi il contenuto di questi due file. Si suppone che il database utilizzato sia il database MySQL5 [dbpam_hibernate] generato dallo script SQL [dbpam_hibernate.sql]. Il file Spring definirà i seguenti tre bean: employeDao di tipo EmployeDao, indemniteDao di tipo IndemniteDao e cotisationDao di tipo CotisationDao. Inoltre, l'implementazione JPA utilizzata sarà Hibernate.


5.9.3. Test

Lettura consigliata: sezioni 3.1.6 e 3.1.7 di [rif1]

Ora che il livello [DAO] è stato scritto e configurato, possiamo testarlo. L'architettura di test sarà la seguente:

5.9.4. InitDB

Creeremo due programmi di test per il livello [DAO]. Questi saranno inseriti nel pacchetto [dao] [2] del ramo [Test Packages] [1] del progetto NetBeans. Questo ramo non è incluso nel progetto generato dall'opzione [Build project], il che garantisce che i programmi di test che inseriamo lì non saranno inclusi nel file .jar finale del progetto.

Le classi collocate nel ramo [Test Packages] hanno accesso alle classi nel ramo [Source Packages] e alle librerie di classi del progetto. Se i test richiedono librerie diverse da quelle presenti nel progetto, queste devono essere dichiarate nel ramo [Test Libraries] [2].

Le classi di test utilizzano lo strumento di test unitari JUnit:

  • [JUnitInitDB] non esegue alcun test. Popola il database con alcuni record e poi li visualizza sulla console.
  • [JUnitDao] esegue una serie di test e ne verifica i risultati.

Lo scheletro della classe [JUnitInitDB] è il seguente:

package dao;

...

public class JUnitInitDB {

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

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

  @Test
  public void initDB(){
     // fill the base
...
     // displays the contents of the database
...
  }

  @Before()
  public void clean(){
     // empty the base
...
  }
}
  • Il metodo [init] viene eseguito prima dell'avvio della suite di test (annotazione @BeforeClass). Esso istanzia il livello [DAO].
  • Il metodo [clean] viene eseguito prima di ogni test (annotazione @Before). Cancella il database.
  • Il metodo [initDB] è un test (annotazione @Test). È l'unico. Un test deve contenere istruzioni di asserzione Assert.assertCondition. Qui non ce ne saranno. Il metodo è quindi un test fittizio. Il suo scopo è quello di popolare il database con alcune righe e quindi visualizzare il contenuto del database sulla console. Qui vengono utilizzati i metodi create e findAll dei livelli [DAO].

Domanda: Completa il codice per la classe [JUnitInitDB]. Usa l'esempio della Sezione 3.1.6 di [ref1] come guida. Il codice genererà l'output mostrato nella Sezione 5.1.


5.9.5. Implementazione del test

Ora siamo pronti per eseguire [InitDB]. Descriviamo la procedura utilizzando il DBMS MySQL5:

  • le classi [1], i file di configurazione [2] e le classi di test del livello [DAO] [3] sono stati configurati,
  • il progetto viene compilato [4]
  • viene eseguita la classe [JUnitInitDB] [5]. Il DBMS MySQL5 viene avviato con un database [dbpam_hibernate] esistente,
  • la finestra [Test Results] [6] indica che i test hanno avuto esito positivo. Questo messaggio non è rilevante in questo contesto, poiché il programma [JUnitInitDB] non contiene istruzioni di asserzione Assert.assertCondition che potrebbero causare il fallimento del test. Tuttavia, dimostra che non si sono verificate eccezioni durante l'esecuzione del test.

La finestra [Output] contiene i log di esecuzione, inclusi quelli di Spring e del test stesso. L'output generato dalla classe [JUnitInitDB] è il seguente:

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

Le tabelle [EMPLOYEES, ALLOWANCES, CONTRIBUTIONS] sono state popolate. È possibile verificarlo collegando NetBeans al database [dbpam_hibernate].

  • In [1], nella scheda [services], è possibile visualizzare i dati della tabella [employees] della connessione [dbpam_hibernate] [2],
  • in [3] il risultato.

5.9.6. JUnitD ao

Ora esamineremo una seconda classe di test [JUnitDao]:

Lo scheletro della classe sarà il seguente:

package dao;

import exception.PamException;
...

public class JUnitDao {

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

  @BeforeClass
  public static void init() {
     // log
    log("init");
     // application configuration
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-dao.xml");
     // layers 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");
     // list of contributions
    List<Cotisation> cotisations = cotisationDao.findAll();
    int nbCotisations = cotisations.size();
     // we add a contribution
    Cotisation cotisation = cotisationDao.create(new Cotisation(3.49, 6.15, 9.39, 7.88));
     // on demand
    cotisation = cotisationDao.find(cotisation.getId());
     // check
    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);
     // we modify it
    cotisation.setCsgrds(-1);
    cotisation.setCsgd(-1);
    cotisation.setRetraite(-1);
    cotisation.setSecu(-1);
    Cotisation cotisation2 = cotisationDao.edit(cotisation);
     // checks
    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);
     // the modified element is requested
    Cotisation cotisation3 = cotisationDao.find(cotisation2.getId());
     // checks
    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);
     // we delete the
    cotisationDao.destroy(cotisation3);
     // checks
    Cotisation cotisation4 = cotisationDao.find(cotisation3.getId());
    Assert.assertNull(cotisation4);
    cotisations = cotisationDao.findAll();
    Assert.assertEquals(nbCotisations, cotisations.size());
  }


  @Test
  public void test02(){
    log("test02");
     // we ask for the list of allowances
...
     // we add an Indemnite indemnite
..
     // fetch indemnity from base - recover indemnity1
..
     // we check that indemnity1 = indemnity
...
     // modify the indemnity obtained and persist the modification in BD. The result is indemnity2
 ...
     // check the indemnite2 version
    ...
     // search for indemnity2 in base - obtain indemnity3
    ...
     // check that compensation3 = compensation2
    ...
     // delete indemnite3 image in base
    ...
     // we'll search for indemnite3 in base
    ...
     // check that a null reference has been obtained
 ...
  }

  @Test
  public void test03(){
    log("test03");
     // we repeat a test analogous to the previous ones for Employe
 ...
  }

  @Test
  public void test04(){
    log("test04");
     // test method [IEmployeDao].find(String SS)
     // first with an existing employee
     // then with a non-existent employee
...
  }

  @Test
  public void test05(){
    log("test05");
     // we create two allowances with the same index
     // violates index uniqueness constraint
     // check for the occurrence of a PamException exception
     // and has the expected error no
...
  }

  @Test
  public void test06(){
    log("test06");
     // create two employees with the same SS number
     // violates the uniqueness constraint on n° SS
     // check for the occurrence of a PamException exception
     // and has the expected error no
...

  }

  @Test
  public void test07(){
    log("test07");
     // create two employees with the same SS number, the 1st with create, the 2nd with edit
     // violates the uniqueness constraint on n° SS
     // check for the occurrence of a PamException exception
     // and has the expected error no
...
  }

  @Test
  public void test08(){
    log("test08");
     // deleting a non-existent employee does not trigger an exception
     // it is added and then destroyed - it is checked
...
  }

  @Test
  public void test09(){
    log("test09");
     // modifying an employee without having the correct version should trigger an exception
     // we check it
...
  }

  @Test
  public void test10(){
    log("test10");
     // deleting an employee without having the correct version should trigger an exception
     // we check it
...

  }

   // getters and setters
  ...
}

Nella classe di test precedente, il database viene svuotato prima di ogni test.


Domanda: Scrivere i seguenti metodi:

1 - test02: basato su test01

2 - test03: un dipendente ha un campo di tipo Indennità. Pertanto, creare un'entità Indennità e un'entità Dipendente

3 - test04.


Procedendo allo stesso modo della classe di test [JUnitInitDB], otteniamo i seguenti risultati:

  • In [1], eseguiamo la classe di test
  • in [2], i risultati del test nella finestra [Test Results]

Proviamo a generare un errore per vedere come viene segnalato nella pagina dei risultati:

  @Test
  public void test01() {
    log("test01");
     // list of contributions
    List<Cotisation> cotisations = cotisationDao.findAll();
    int nbCotisations = cotisations.size();
     // we add a contribution
    Cotisation cotisation = cotisationDao.create(new Cotisation(3.49, 6.15, 9.39, 7.88));
     // on demand
    cotisation = cotisationDao.find(cotisation.getId());
     // check
    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);
     // we modify it
....
}

Riga 13: L'asserzione genererà un errore, poiché il valore di Csgrds è 3,49 (riga 8). L'esecuzione della classe di test produce i seguenti risultati:

  • La pagina dei risultati [1] ora mostra che alcuni test hanno dato esito negativo.
  • In [2], un riepilogo dell'eccezione che ha causato il fallimento del test. Include il numero di riga nel codice Java in cui si è verificata l'eccezione.

5.10. Il livello [business] dell'applicazione [PAM]

Ora che il livello [DAO] è stato scritto, passiamo allo studio del livello business [2]:

5.10.1. L'interfaccia Java [IMetier]

È stata descritta nella sezione 5.7. La riportiamo di seguito:

package metier;

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

public interface IMetier {
   // get your payslip
  public FeuilleSalaire calculerFeuilleSalaire(String SS, double nbHeuresTravaillées, int nbJoursTravaillés );
   // list of employees
  public List<Employe> findAllEmployes();
}

L'implementazione del livello [business] verrà effettuata in un pacchetto [business]:

 

Il pacchetto [Business] includerà, oltre all'interfaccia [IMetier] e alla sua implementazione [Metier], altre due classi: [Payroll] e [PayrollItems]. La classe [Payroll] è stata brevemente introdotta nella Sezione 5.7. Ora la rivedremo.

5.10.2. La classe [Payroll]

Il metodo [calculatePayStub] dell'interfaccia [IMetier] restituisce un oggetto di tipo [PayStub] che rappresenta i vari elementi di una busta paga. La sua definizione è la seguente:

package metier;

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

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

   // manufacturers
  public FeuilleSalaire() {

  }

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

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

   // accessors
...  
}
  • riga 7: la classe implementa l'interfaccia Serializable poiché le sue istanze possono essere scambiate in rete.
  • riga 9: il dipendente a cui si riferisce la busta paga
  • riga 10: le varie aliquote contributive
  • riga 11: le varie indennità legate all'indice del dipendente
  • riga 12: le componenti del loro stipendio
  • righe 14–22: i due costruttori della classe
  • righe 25–27: metodo [toString] che identifica uno specifico oggetto [PayStub]
  • Righe 29 e successive: accessori pubblici ai campi privati della classe

La classe [ElementsSalaire] a cui si fa riferimento alla riga 11 della classe [FeuilleSalaire] sopra riportata contiene gli elementi che compongono una busta paga. La sua definizione è la seguente:

package metier;

public class ElementsSalaire implements Serializable{

   // private fields
  private double salaireBase;
  private double cotisationsSociales;
  private double indemnitesEntretien;
  private double indemnitesRepas;
  private double salaireNet;

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

   // public accessors
...  
}
  • riga 3: la classe implementa l'interfaccia Serializable poiché è un componente della PayrollClass, che deve essere serializzabile.
  • riga 6: lo stipendio base
  • riga 7: contributi previdenziali versati su questo stipendio base
  • riga 8: assegni giornalieri per il mantenimento dei figli
  • riga 9: le indennità giornaliere per i pasti dei bambini
  • riga 10: lo stipendio netto da versare all'asilo nido
  • righe 12–24: costruttori di classe
  • righe 27–31: metodo [toString] che identifica uno specifico oggetto [ElementsSalaire]
  • Righe 34 e successive: accessori pubblici ai campi privati della classe

5.10.3. La classe di implementazione [Metier] del livello [business]

La classe di implementazione [Metier] del livello [business] potrebbe essere la seguente:

package metier;

...

@Transactional
public class Metier implements IMetier {

   // reference on the [DAO] layer
  private ICotisationDao cotisationDao = null;
  private IEmployeDao employeDao=null;


   // get your payslip
  public FeuilleSalaire calculerFeuilleSalaire(String SS,
    double nbHeuresTravaillées, int nbJoursTravaillés) {
...
  }

   // list of employees
   public List<Employe> findAllEmployes() {
     ...
  }

   // getters and setters
...
 }
  • riga 5: L'annotazione @Transactional di Spring garantisce che ogni metodo della classe venga eseguito all'interno di una transazione.
  • righe 9-10: riferimenti ai livelli [DAO] delle entità [Cotisation, Employe, Indemnite]
  • righe 14–17: il metodo [calculatePayroll]
  • righe 20–22: il metodo [findAllEmployees]
  • Riga 24 e seguenti: gli accessori pubblici per i campi privati della classe

Domanda: Scrivi il codice per il metodo [findAllEmployees].



Domanda: Scrivi il codice per il metodo [calculatePayroll].


Tieni presente quanto segue:

  • Il metodo per il calcolo dello stipendio è stato spiegato nella sezione 5.2.
  • Se il parametro [SS] non corrisponde a nessun dipendente (il livello [DAO] ha restituito un puntatore nullo), il metodo genererà un'eccezione [PamException] con un codice di errore appropriato.

5.10.4. Test del livello [business]

Creiamo due programmi di test:

Le classi di test [3] vengono create nel pacchetto [metier] [2] all'interno del ramo [Test Packages] [1] del progetto.

La classe [JUnitMetier_1] potrebbe avere questo aspetto:

package metier;

...

public class JUnitMetier_1 {

// business layer
  private IMetier metier;

  @BeforeClass
  public void init(){
     // log
    log("init");
     // application configuration
     // instantiation layer [metier]
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-metier-dao.xml");
    metier = (IMetier) ctx.getBean("metier");
     // layers DAO
    IEmployeDao employeDao=(IEmployeDao) ctx.getBean("employeDao");
    ICotisationDao cotisationDao=(ICotisationDao) ctx.getBean("cotisationDao");
    IIndemniteDao indemniteDao=(IIndemniteDao) ctx.getBean("indemniteDao");
     // empty the 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);
    }
     // fill it
    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(){
     // wage sheet calculation
    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()));
    }
  }
}

Nella classe non sono presenti asserzioni Assert.assertCondition. Stiamo semplicemente cercando di calcolare alcuni stipendi in modo da poterli poi verificare manualmente. L'output sullo schermo ottenuto eseguendo la classe precedente è il seguente:

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
  • Riga 4: busta paga di Justine Laverti
  • riga 5: busta paga di Marie Jouveinal
  • riga 6: l'eccezione dovuta al fatto che il dipendente con numero di previdenza sociale 'xx' non esiste.

Domanda: La riga 17 di [JUnitMetier_1] utilizza il bean Spring denominato metier. Fornisci la definizione di questo bean nel file [spring-config-metier-dao.xml].


La classe [JUnitMetier_2] potrebbe essere la seguente:

package metier;

...
public class JUnitMetier_2 {

// business layer
  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] è una copia della classe [JUnitMetier_1], con la differenza che in questo caso le asserzioni sono state inserite nel metodo test01.


Domanda: Scrivi il metodo test01.


Quando si esegue la classe [JUnitMetier_2], se tutto va bene si ottengono i seguenti risultati:

Image

5.11. Il livello [ui] dell'applicazione [PAM] – versione della console

Ora che il livello [business] è stato scritto, dobbiamo ancora scrivere il livello [ui] [1]:

Creeremo due diverse implementazioni del livello [ui]: una versione console e una versione GUI Swing:

5.11.1. La classe [ ui.console.Main]

Ci concentreremo innanzitutto sull'applicazione console implementata dalla classe [ui.console.Main] sopra riportata. Il suo funzionamento è stato descritto nella Sezione 5.3. Lo scheletro della classe [Main] potrebbe essere il seguente:

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) {
     // local data
    final String syntaxe = "pg num_securite_sociale nb_heures_travaillées nb_jours_travaillés";
     // check the number of parameters
...
     // error list
    ArrayList erreurs = new ArrayList();
     // the second parameter must be a real number >0
...
     // mistake?
    if (...) {
      erreurs.add("Le nombre d'heures travaillées [" + args[1]
        + "] est erroné");
    }
     // the third parameter must be an integer >0
...
     // mistake?
    if (...) {
      erreurs.add("Le nombre de jours travaillés [" + args[2]
        + "] est erroné");
    }
     // mistakes?
    if (erreurs.size() != 0) {
      for (int i = 0; i < erreurs.size(); i++) {
        System.err.println(erreurs.get(i));
      }
      return;
    }
     // it's OK - we can ask for the payslip
    FeuilleSalaire feuilleSalaire = null;
    try {
       // instantiation layer [metier]
      ...
       // wage sheet calculation
      ...
    } 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;
    }

     // detailed display
    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";
  }
}

Domanda: Completa il codice sopra riportato.


5.11.2. Esecuzione

Per eseguire la classe [ui.console.Main], procedere come segue:

  • In [1], selezionare le proprietà del progetto,
  • in [2], selezionare la proprietà [Esegui] del progetto,
  • utilizzare il pulsante [3] per specificare la classe (nota come classe principale) da eseguire,
  • selezionare la classe [4],
  • la classe è riportata in [5]. Per essere eseguita, questa classe richiede tre argomenti (numero di previdenza sociale, numero di ore lavorate, numero di giorni lavorati). Questi argomenti vengono inseriti in [6],
  • una volta fatto ciò, il progetto può essere eseguito [7]. La configurazione precedente implica che verrà eseguita la classe [ui.console.Main].

I risultati dell'esecuzione vengono visualizzati nella finestra [output]:

5.12. Il livello [ui] dell'applicazione [PAM] – versione grafica

Ora implementeremo il livello [ui] con un'interfaccia utente grafica:

  • in [1], la classe [PamJFrame] dell'interfaccia grafica
  • in [2]: l'interfaccia utente grafica

5.12.1. Un breve tutorial

Per creare l'interfaccia grafica utente, procedere come segue:

  • [1]: Creare un nuovo file utilizzando il pulsante [1] [Nuovo file...]
  • [2]: Seleziona la categoria di file [Moduli GUI Swing], ovvero i moduli grafici
  • [3]: Seleziona il tipo [Modulo JFrame], un tipo di modulo vuoto
  • [5]: Assegnare un nome al modulo; questo sarà anche il nome della classe
  • [6]: Inserisci il modulo in un pacchetto
  • [8]: Il modulo viene aggiunto alla struttura del progetto
  • [9]: È possibile accedere al modulo tramite due viste: [Design] [9], che consente di progettare i vari componenti del modulo, e [Source] [10 sotto], che fornisce l'accesso al codice Java del modulo. In definitiva, un modulo è una classe Java come qualsiasi altra. La vista [Design] è uno strumento per la progettazione del modulo. Ogni volta che viene aggiunto un componente in modalità [Design], il codice Java viene aggiunto nella vista [Source] per tenerne conto.
  • [11]: L'elenco dei componenti Swing disponibili per un modulo si trova nella finestra [Tavolozza].
  • [12]: La finestra [Inspector] mostra la struttura ad albero dei componenti del modulo. I componenti con una rappresentazione visiva si trovano nel ramo [JFrame]; gli altri nel ramo [Other Components].
  • In [13], selezioniamo un componente [JLabel] con un singolo clic
  • In [14], lo trasciniamo sul modulo in modalità [Design]
  • In [15], definiamo le proprietà di JLabel (testo, font).
  • In [16], il risultato.
  • In [17], richiediamo un'anteprima del modulo
  • In [18], il risultato
  • In [19], l'etichetta [JLabel1] è stata aggiunta all'albero dei componenti nella finestra [Inspector]
  • In [20] e [21]: nella vista [Source] del modulo è stato aggiunto del codice Java per gestire il JLabel aggiunto.

Un tutorial sulla creazione di moduli con NetBeans è disponibile all'URL [http://www.netbeans.org/kb/trails/matisse.html].

5.12.2. L'interfaccia grafica [PamJFrame]

Realizzeremo la seguente interfaccia grafica utente:

  • in [1], l'interfaccia grafica
  • in [2], la struttura ad albero dei suoi componenti: un JLabel e sei contenitori JPanel

JLabel1

JPanel1

JPanel2

JPanel3

JPanel4

JPanel5


Esercizio pratico: Realizza l'interfaccia grafica vista in precedenza seguendo il tutorial [http://www.netbeans.org/kb/trails/matisse.html].


5.12.3. Eventi dell'interfaccia grafica utente

Lettura consigliata: il capitolo [Interfacce grafiche utente] in [rif2].

Gestiremo il clic sul pulsante [jButtonSalaire]. Per creare il metodo che gestisce questo evento, possiamo procedere come segue:

Viene generato il gestore per il clic sul pulsante [JButtonSalaire]:

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

Viene generato anche il codice Java che associa il metodo precedente al clic sul pulsante [JButtonSalaire]:

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

Le righe 2–5 specificano che il clic (evt di tipo ActionPerformed) sul pulsante [jButtonSalaire] (riga 2) deve essere gestito dal metodo [jButtonSalaireActionPerformed] (riga 4).

Gestiremo anche l'evento [caretUpdate] (spostamento del cursore) sul campo di immissione [jTextFieldHT]. Per creare il gestore di questo evento, procediamo come in precedenza:

Viene generato il gestore per l'evento [caretUpdate] sul campo di immissione [jTextFieldHT]:

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

Viene generato anche il codice Java che associa il metodo precedente all'evento [caretUpdate] sul campo di testo [jTextFieldHT]:

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

Le righe 1–4 indicano che l'evento [caretUpdate] (riga 2) sul pulsante [jTextFieldHT] (riga 1) deve essere gestito dal metodo [jTextFieldHTCaretUpdate] (riga 3).

5.12.4. Inizializzazione della GUI

Torniamo all'architettura della nostra applicazione:

Il livello [ui] necessita di un riferimento al livello [business]. Ricordiamo come veniva ottenuto questo riferimento nell'applicazione console:

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

Il metodo è lo stesso nell'applicazione GUI. All'avvio dell'applicazione GUI, deve essere inizializzato anche il riferimento [IMetier metier] della riga 3 sopra riportata. Il codice generato per la GUI è attualmente il seguente:

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

}
  • Righe 29–35: il metodo statico [main] che avvia l'applicazione
  • riga 32: viene creata e resa visibile un'istanza della GUI [PamJFrame].
  • righe 7-9: il costruttore della GUI.
  • riga 8: chiamata al metodo [initComponents] definito alla riga 17. Questo metodo viene generato automaticamente in base al lavoro svolto in modalità [Design]. Non modificarlo.
  • riga 21: il metodo che gestirà lo spostamento del cursore di input nel campo [jTextFieldHT]
  • Riga 25: il metodo che gestirà il clic sul pulsante [jButtonSalaire]

Per aggiungere le nostre inizializzazioni al codice precedente, possiamo procedere come segue:

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

...

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

   // proprietary initializations
  public void doMyInit(){
     // init context
    try{
       // instantiation layer [metier]
...
     // list of employees
...
    }catch (PamException ex){
     // the exception message is placed in [jTextAreaStatus]
...
     // return
      return;
    }
     // salary button disabled
...
     // jScrollPane1 hidden
...
     // spinner days worked
    jSpinnerJT.setModel(new SpinnerNumberModel(0,0,31,1));
     // combobox employees
    employesCombo=new String[employes.size()];
    int i=0;
    for(Employe employe : employes){
      employesCombo[i++]=employe.getPrenom()+" "+employe.getNom();
    }
    jComboBoxEmployes.setModel(new DefaultComboBoxModel(employesCombo));
}
  • Riga 4: chiamiamo un metodo personalizzato per eseguire le nostre inizializzazioni. Queste sono definite dal codice nelle righe 10–42

Domanda: Utilizzando i commenti come guida, completa il codice per la procedura [doMyInit].


5.12.5. Gestori di eventi


Domanda: Scrivi il metodo [jTextFieldHTCaretUpdate]. Questo metodo deve garantire che, se i dati nel campo [jTextFieldHT] non sono un numero reale >=0, il pulsante [jButtonSalaire] venga disabilitato.



Domanda: Scrivi il metodo [jButtonSalaireActionPerformed], che deve visualizzare la busta paga del dipendente selezionato in [jComboBoxEmployes].


5.12.6. Esecuzione della GUI

Per eseguire l'interfaccia grafica, modificare la configurazione [Run] del progetto:

  • In [1], inserire la classe dell'interfaccia grafica

Il progetto deve essere completo dei file di configurazione (persistence.xml, spring-config-metier-dao.xml) e della classe GUI. Avviare il DBMS di destinazione prima di eseguire il progetto.

Ci interessa la seguente architettura in cui il livello JPA è ora implementato da EclipseLink:

5.13.1. Il progetto NetBeans

Il nuovo progetto NetBeans viene creato copiando il progetto precedente:

  • in [1]: dopo aver cliccato con il tasto destro del mouse sul progetto Hibernate, selezionare Copia
  • utilizzando il pulsante [2], selezionare la cartella principale per il nuovo progetto. Il nome della cartella appare in [3].
  • in [4], assegnare un nome al nuovo progetto
  • in [5], il nome della cartella del progetto
  • in [1], il nuovo progetto è stato creato. Ha lo stesso nome dell'originale,
  • in [2] e [3], rinominarlo in [mv-pam-spring-eclipselink].

Il progetto deve essere modificato in due punti per adattarlo al nuovo livello JPA / EclipseLink:

  1. in [4], i file di configurazione di Spring devono essere modificati. È qui che si trova la configurazione del livello JPA.
  2. In [5], le librerie del progetto devono essere modificate: le librerie Hibernate devono essere sostituite con quelle di EclipseLink.

Cominciamo da quest'ultimo punto. Il file [pom.xml] per il nuovo progetto sarà il seguente:


<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>
  • righe 73–82: dipendenze per l'implementazione JPA di EclipseLink,
  • righe 19–24: il repository Maven per EclipseLink.

I file di configurazione Spring devono essere modificati per indicare che l'implementazione JPA è cambiata. In entrambi i file, cambia solo la sezione che configura il livello JPA. Ad esempio, in [spring-config-metier-dao.xml] abbiamo:

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

   <!-- application layers -->
   <!- - DAO -->
  <bean id="employeDao" class="dao.EmployeDao" />
  <bean id="indemniteDao" class="dao.IndemniteDao" />
  <bean id="cotisationDao" class="dao.CotisationDao" />
   <!-- business -->
  <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>

   <!-- data source 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>

Le righe 19–36 configurano il livello JPA. L'implementazione JPA utilizzata è Hibernate (riga 22). Inoltre, il database di destinazione è [dbpam_hibernate] (riga 41).

Per passare a un'implementazione JPA/EclipseLink, le righe 19–35 sopra riportate vengono sostituite dalle righe seguenti:

  <!-- 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>
  • Riga 5: L'implementazione JPA utilizzata è EclipseLink
  • riga 9: la proprietà databasePlatform imposta il DBMS di destinazione, in questo caso MySQL
  • riga 11: per generare le tabelle del database quando viene istanziato il livello JPA. Qui, la proprietà è commentata.
  • riga 7: per visualizzare sulla console le istruzioni SQL emesse dal livello JPA. Qui la proprietà è commentata.

Inoltre, il database di destinazione diventa [dbpam_eclipselink] (riga 4 sotto):

1
2
3
4
5
6
7
8
9
<!-- data source 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. Esecuzione dei test

Prima di testare l'intera applicazione, è consigliabile verificare che i test JUnit vengano superati con la nuova implementazione JPA. Prima di eseguirli, inizieremo eliminando le tabelle dal database. Per farlo, nella scheda [Runtime] di NetBeans, se necessario, crea una connessione al database dbpam_eclipselink / MySQL5. Una volta connesso al database dbpam_eclipselink / MySQL5, puoi procedere all'eliminazione delle tabelle come mostrato di seguito:

  • [1]: prima dell'eliminazione
  • [2]: dopo l'eliminazione

Una volta fatto ciò, è possibile eseguire il primo test sul livello [DAO]: InitDB, che popola il database. Per garantire che le tabelle precedentemente eliminate vengano ricreate dall'applicazione, assicurarsi che nella configurazione Spring JPA / EclipseLink sia presente la riga:

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

sia presente e non sia commentata.

Compiliamo il progetto e poi eseguiamo il test [JUnit InitDB]:

  • In [1], il test InitDB viene eseguito.
  • In [2], fallisce. L'eccezione viene generata da Spring e non da un test che ha fallito.

Causa: org.springframework.beans.factory.BeanCreationException: Errore durante la creazione del bean con nome 'entityManagerFactory' definito nella risorsa del percorso di classe [spring-config-DAO.xml]: Fallita l'invocazione del metodo init; l'eccezione annidata è java.lang.IllegalStateException: È necessario avviare l'agente Java per utilizzare InstrumentationLoadTimeWeaver. Vedere la documentazione di Spring.

Spring indica che c'è un problema di configurazione. Il messaggio non è chiaro. Il motivo dell'eccezione è stato spiegato nella sezione 3.1.9 di [rif1]. Affinché la configurazione Spring/EclipseLink funzioni, la JVM che esegue l'applicazione deve essere avviata con un parametro specifico, un agente Java. Il formato di questo parametro è il seguente:

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

[spring-agent.jar] è l'agente Java richiesto dalla JVM per gestire la configurazione di Spring/EclipseLink.

Quando si esegue un progetto, è possibile passare argomenti alla JVM:

  • In [1], è possibile accedere alle proprietà del progetto
  • In [2], le proprietà di esecuzione
  • In [3], passare il parametro -javaagent alla JVM

5.13.3. InitDB

Ora siamo pronti a testare nuovamente [InitDB]. Questa volta, i risultati sono i seguenti:

  • In [1], il test ha avuto esito positivo
  • In [2], nella scheda [Services], aggiorniamo la connessione di NetBeans al database [dbpam_eclipselink]
  • In [3], sono state create quattro tabelle
  • in [5], visualizziamo il contenuto della tabella [employees]
  • in [6], il risultato.

5.13.4. JUnitDao

L'esecuzione della classe di test [JUnitDao] potrebbe fallire, anche se ha avuto esito positivo con l'implementazione JPA/Hibernate. Per capire il motivo, analizziamo un esempio.

Il metodo sottoposto a test è il seguente metodo IndemniteDao.create:

package dao;

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

  @PersistenceContext
  private EntityManager em;

   // manufacturer
  public IndemniteDao() {
  }

   // create an allowance
  public Indemnite create(Indemnite indemnite) {
    try{
      em.persist(indemnite);
    }catch(Throwable th){
      throw new PamException(th,31);
    }
    return indemnite;
  }

...
}
  • righe 15–22: il metodo in fase di test

Il metodo di prova è il seguente:


package dao;
 
...
 
public class JUnitDao {
 
// layers DAO
  static private IEmployeDao employeDao;
  static private IIndemniteDao indemniteDao;
  static private ICotisationDao cotisationDao;
 
  @BeforeClass
  public static void init() {
    // log
    log("init");
    // application configuration
    ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config-DAO.xml");
    // layers DAO
    employeDao = (IEmployeDao) ctx.getBean("employeDao");
    indemniteDao = (IIndemniteDao) ctx.getBean("indemniteDao");
    cotisationDao = (ICotisationDao) ctx.getBean("cotisationDao");
  }
 
  @Before()
  public void clean() {
    // empty the 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");
    // we create two allowances with the same index
    // violates index uniqueness constraint
    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;
      // checks
      Assert.assertEquals(31, ex.getCode());
    } catch (Throwable th1) {
      th = th1;
    }
    // checks
    Assert.assertTrue(erreur);
    // exception chain
    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());
    }
    // the 1st allowance had to be continued
    Indemnite indemnite = indemniteDao.find(indemnite1.getId());
    // check
    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);
    // the second indemnity should not have persisted
    List<Indemnite> indemnites = indemniteDao.findAll();
    int nbIndemnites = indemnites.size();
    Assert.assertEquals(nbIndemnites, 1);
  }
 
...
}

Domanda: Spiega cosa fa il test test05 e indica i risultati attesi.


I risultati ottenuti utilizzando un livello JPA/Hibernate sono i seguenti:

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

Il test ha esito positivo, il che significa che le asserzioni sono state verificate e che il metodo di test non ha generato alcuna eccezione.


Domanda: Spiega cosa è successo.


I risultati ottenuti con un livello JPA/EclipseLink sono i seguenti:

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

Come in precedenza con Hibernate, il test viene superato, il che significa che le asserzioni sono state verificate e che dal metodo di test non viene generata alcuna eccezione.


Domanda: Spiega cosa è successo.



Domanda: Da questi due esempi, cosa possiamo concludere sull'intercambiabilità delle implementazioni JPA? È completa in questo caso?


5.13.5. Gli altri test

Una volta che il livello [DAO] è stato testato e ritenuto corretto, possiamo passare a testare il livello [business] e il progetto stesso nella sua versione da console o grafica. La modifica dell'implementazione JPA non ha alcun effetto sui livelli [business] e [UI]; pertanto, se questi livelli funzionavano con Hibernate, funzioneranno con EclipseLink con poche eccezioni: l'esempio precedente mostra che le eccezioni generate dai livelli [DAO] possono differire. Pertanto, nel caso del test, Spring / JPA / Hibernate genera una [PamException], un'eccezione specifica dell'applicazione [pam], mentre Spring / JPA / EclipseLink genera una [TransactionSystemException], un'eccezione del framework Spring. Se, nel caso di test, il livello [ui] si aspetta una [PamException] perché è stato costruito con Hibernate, non funzionerà più quando si passa a EclipseLink.

5.13.6. Lavoro da svolgere


Compito pratico: Riprovare le applicazioni console e Swing con diversi DBMS: MySQL5, Oracle XE, SQL Server.