Skip to content

17. Applicazione web MVC in un’architettura a 3 livelli – Esempio 3 – DBMS Firebird

17.1. Il database Firebird

In questa nuova versione, installeremo l’elenco delle persone in una tabella del database Firebird. Nel documento [http://tahe.developpez.com/divers/sql-firebird/] sono disponibili le informazioni per installare e gestire questo SGBD. Di seguito, le schermate provengono da IBExpert, un client di amministrazione per i database Interbase e Firebird.

Il database si chiama [dbpersonnes.gdb]. Contiene una tabella denominata [PERSONNES]:

Image

La tabella [PERSONNES] conterrà l’elenco delle persone gestite dall’applicazione web. È stata creata con i seguenti comandi SQL:

CREATE TABLE PERSONNES (
    ID             INTEGER NOT NULL,
    "VERSION"      INTEGER NOT NULL,
    NOM            VARCHAR(30) NOT NULL,
    PRENOM         VARCHAR(30) NOT NULL,
    DATENAISSANCE  DATE NOT NULL,
    MARIE          SMALLINT NOT NULL,
    NBENFANTS      SMALLINT NOT NULL
);


ALTER TABLE PERSONNES ADD CONSTRAINT CHK_PRENOM_PERSONNES check (PRENOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_MARIE_PERSONNES check (MARIE=0 OR MARIE=1);
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_NOM_PERSONNES check (NOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_ENFANTS_PERSONNES check (NBENFANTS>=0);


ALTER TABLE PERSONNES ADD CONSTRAINT PK_PERSONNES PRIMARY KEY (ID);
  • righe 2-10: la struttura della tabella [PERSONNES], destinata a memorizzare oggetti di tipo [Personne], rispecchia la struttura di tale oggetto. Poiché il tipo booleano non esiste in Firebird, il campo [MARIE] (riga 8) è stato dichiarato di tipo [SMALLINT], un intero. Il suo valore sarà 0 (non sposato) o 1 (sposato).
  • righe 13-16: vincoli di integrità che rispecchiano quelli del validatore di dati [ValidatePersonne].
  • riga 19: il campo ID è la chiave primaria della tabella [PERSONNES]

La tabella [PERSONNES] potrebbe avere il seguente contenuto:

Image

Il database [dbpersonnes.gdb] contiene, oltre alla tabella [PERSONNES], un oggetto denominato generatore e denominato [GEN_PERSONNES_ID]. Questo generatore produce numeri interi consecutivi che useremo per assegnare un valore alla chiave primaria [ID] della classe [PERSONNES]. Facciamo un esempio per illustrarne il funzionamento:

Si può notare che il valore del generatore [GEN_PERSONNES_ID] è cambiato (fare doppio clic su di esso + F5 per aggiornare):

 

L'ordine SQL

SELECT GEN_ID ( GEN_PERSONNES_ID,1 ) FROM RDB$DATABASE

consente quindi di ottenere il seguente valore del generatore [GEN_PERSONNES_ID]. GEN_ID è una funzione interna di Firebird e [RDB$DATABASE] è una tabella di sistema di SGBD.

17.2. Il progetto Eclipse dei livelli [dao] e [service]

Per sviluppare i livelli [dao] e [service] della nostra applicazione con database, utilizzeremo il seguente progetto Eclipse [mvc-personnes-03]:

Image

Il progetto è un semplice progetto Java, non un progetto web Tomcat. Ricordiamo che la versione 2 della nostra applicazione utilizzerà il livello [web] della versione 1. Questo livello non deve quindi essere riscritto.


Cartella [src]


Questa cartella contiene i codici sorgente dei livelli [dao] e [service]:

Image

Al suo interno sono presenti diversi pacchetti:

  • [istia.st.mvc.personnes.dao]: contiene il livello [dao]
  • [istia.st.mvc.personnes.entites]: contiene la classe [Personne]
  • [istia.st.mvc.personnes.service]: contiene la classe [service]
  • [istia.st.mvc.personnes.tests]: contiene i test JUnit dei livelli [dao] e [service]

nonché i file di configurazione che devono trovarsi nella cartella ClassPath dell’applicazione.


Cartella [database]


Questa cartella contiene il database Firebird delle persone:

Image

  • [dbpersonnes.gdb] è il database.
  • [dbpersonnes.sql] è lo script SQL per la generazione del database:
/******************************************************************************/
/***           Generato da IBExpert 07/03/2006 27/04/2006 1         0:27:11 ***/
/******************************************************************************/

SET SQL DIALECT 3;

SET NAMES NONE;

CREATE DATABASE 'C:\data\2005-2006\webjava\dvp-spring-mvc\mvc-38\database\DBPERSONNES.GDB'
USER 'SYSDBA' PASSWORD 'masterkey'
PAGE_SIZE 16384
DEFAULT CHARACTER SET NONE;



/******************************************************************************/
/***                                                               Generatori ***/
/******************************************************************************/

CREATE GENERATOR GEN_PERSONNES_ID;
SET GENERATOR GEN_PERSONNES_ID TO 787;



/******************************************************************************/
/***                                                                   Tabelle ***/
/******************************************************************************/



CREATE TABLE PERSONNES (
    ID             INTEGER NOT NULL,
    "VERSION"      INTEGER NOT NULL,
    NOM            VARCHAR(30) NOT NULL,
    PRENOM         VARCHAR(30) NOT NULL,
    DATENAISSANCE  DATE NOT NULL,
    MARIE          SMALLINT NOT NULL,
    NBENFANTS      SMALLINT NOT NULL
);

INSERT INTO PERSONNES (ID, "VERSION", NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) VALUES (1, 1, 'Major', 'Joachim', '1984-11-13', 1, 2);
INSERT INTO PERSONNES (ID, "VERSION", NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) VALUES (2, 1, 'Humbort', 'Mélanie', '1985-02-12', 0, 1);
INSERT INTO PERSONNES (ID, "VERSION", NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) VALUES (3, 1, 'Lemarchand', 'Charles', '1986-03-01', 0, 0);

COMMIT WORK;



/* Verifica definizione dei vincoli */

ALTER TABLE PERSONNES ADD CONSTRAINT CHK_PRENOM_PERSONNES check (PRENOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_NOM_PERSONNES check (NOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_MARIE_PERSONNES check (MARIE=0 OR MARIE=1);
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_ENFANTS_PERSONNES check (NBENFANTS>=0);


/******************************************************************************/
/***                                                             Chiavi primarie ***/
/******************************************************************************/

ALTER TABLE PERSONNES ADD CONSTRAINT PK_PERSONNES PRIMARY KEY (ID);

Cartella [lib]


Questa cartella contiene gli archivi necessari all’applicazione:

Si noti la presenza del driver JDBC [firebirdsql-full.jar] del Firebird SGBD, nonché di una serie di archivi [spring-*.jar]. Avremmo potuto utilizzare l'unico archivio [spring.jar] presente nella cartella [dist] della distribuzione, che contiene tutte le classi di Spring. È anche possibile utilizzare solo gli archivi necessari al progetto. È ciò che abbiamo fatto in questo caso, lasciandoci guidare dagli errori relativi alle classi mancanti segnalati da Eclipse e dai nomi degli archivi parziali di Spring. Tutti questi archivi della cartella [lib] sono stati inseriti nell’archivio Classpath del progetto.


Cartella [dist]


Questa cartella conterrà gli archivi risultanti dalla compilazione delle classi dell’applicazione:

Image

  • [personnes-dao.jar]: archivio del livello [dao]
  • [personnes-service.jar]: archivio del livello [service]

17.3. Il livello [dao]

17.3.1. I componenti del livello [dao]

Il livello [dao] è costituito dalle seguenti classi e interfacce:

Image

  • [IDao] è l’interfaccia presentata dal livello [dao]
  • [DaoImplCommon] è un'implementazione di quest'ultima in cui il gruppo di persone si trova in una tabella del database. [DaoImplCommon] raggruppa funzionalità indipendenti da SGBD.
  • [DaoImplFirebird] è una classe derivata da [DaoImplCommon] per gestire specificamente un database Firebird.
  • [DaoException] è il tipo delle eccezioni non controllate, generate dal livello [dao]. Questa classe appartiene alla versione 1.

L'interfaccia [IDao] è la seguente:

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;

import java.util.Collection;

public interface IDao {
     // Elenco di tutte le persone
    Collection getAll();
     // recupera una persona specifica
    Personne getOne(int id);
     // Aggiungere/modificare una persona
    void saveOne(Personne personne);
     // eliminare una persona
    void deleteOne(int id);
}
  • L'interfaccia presenta gli stessi quattro metodi della versione precedente.

La classe [DaoImplCommon] che implementa questa interfaccia sarà la seguente:

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;
import org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;

import java.util.Collection;

public class DaoImplCommon extends SqlMapClientDaoSupport implements
        IDao {

     // elenco delle persone
    public Collection getAll() {
...
    }

     // recuperare una persona specifica
    public Personne getOne(int id) {
...
    }

     // eliminazione di una persona
    public void deleteOne(int id) {
...
    }

     // aggiungere o modificare una persona
    public void saveOne(Personne personne) {
         // Il parametro "persona" è valido?
        check(personne);
         // Aggiunta o modifica?
        if (personne.getId() == -1) {
             // Aggiunta
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }

     // Aggiungi una persona
    protected void insertPersonne(Personne personne) {
...
    }

     // Modifica di una persona
    protected void updatePersonne(Personne personne) {
...
    }

     // Verifica della validità di una persona
    private void check(Personne p) {
...
    }

...
}
  • righe 8-9: la classe [DaoImpl] implementa l’interfaccia [IDao] e quindi i quattro metodi [getAll, getOne, saveOne, deleteOne].
  • righe 27-37: il metodo [saveOne] utilizza due metodi interni, [insertPersonne] e [updatePersonne], a seconda che si debba aggiungere o modificare una persona.
  • riga 50: il metodo privato [check] è quello della versione precedente. Non ci soffermeremo su di esso.
  • riga 8: per implementare l’interfaccia [IDao], la classe [DaoImpl] deriva dalla classe Spring [SqlMapClientDaoSupport].

17.3.2. Il livello di accesso ai dati [iBATIS]

La classe Spring [SqlMapClientDaoSupport] utilizza un framework di terze parti [Ibatis SqlMap] disponibile all’URL [http://ibatis.apache.org/]:

Image

[iBATIS] è un progetto Apache che facilita la creazione di livelli [dao] basati su database. Con [iBATIS], l’architettura del livello di accesso ai dati è la seguente:

[iBATIS] si inserisce tra il livello [dao] dell'applicazione e il driver JDBC del database. Esistono alternative a [iBATIS], come ad esempio l’alternativa [Hibernate]:

Image

L'utilizzo del framework [iBATIS] richiede due archivi [ibatis-common, ibatis-sqlmap], entrambi inseriti nella cartella [lib] del progetto:

La classe [SqlMapClientDaoSupport] incapsula la parte generica dell’utilizzo del framework [iBATIS], c.a.d. delle parti di codice presenti in tutti i livelli [dao] che utilizzano lo strumento [iBATIS]. Per scrivere la parte non generica del codice, ovvero quella specifica del livello [dao], è sufficiente derivare la classe [SqlMapClientDaoSupport]. È proprio ciò che facciamo qui.

La classe [SqlMapClientDaoSupport] è definita come segue:

Image

Tra i metodi di questa classe, uno consente di configurare il client [iBATIS] con cui si utilizzerà il database:

Image

L'oggetto [SqlMapClient sqlMapClient] è l'oggetto [IBATIS] utilizzato per accedere a un database. Da solo, implementa il livello [iBATIS] della nostra architettura:

Una sequenza tipica di azioni con questo oggetto è la seguente:

  1. richiedere una connessione a un pool di connessioni
  2. aprire una transazione
  3. eseguire una serie di comandi SQL memorizzati in un file di configurazione
  4. chiudere la transazione
  5. restituire la connessione al pool

Se la nostra implementazione [DaoImplCommon] operasse direttamente con [iBATIS], dovrebbe eseguire questa sequenza ripetutamente. Solo l’operazione 3 è specifica del livello [dao], mentre le altre operazioni sono generiche. La classe Spring [SqlMapClientDaoSupport] si occuperà autonomamente delle operazioni 1, 2, 4 e 5, delegando l’operazione 3 alla sua classe derivata, in questo caso la classe [DaoImplCommon].

Per poter funzionare, la classe [SqlMapClientDaoSupport] necessita di un riferimento all’oggetto iBATIS [SqlMapClient sqlMapClient] che garantirà l’interazione con il database. Questo oggetto necessita di due elementi per funzionare:

  • un oggetto [DataSource] connesso al database, al quale richiederà le connessioni
  • uno o più file di configurazione in cui sono esternalizzati i comandi SQL da eseguire. Infatti, questi non sono presenti nel codice Java. Sono identificati da un codice in un file di configurazione e l’oggetto [SqlMapClient sqlMapClient] utilizza tale codice per far eseguire un comando SQL specifico.

Una bozza di configurazione del nostro livello [dao] che rispecchi l’architettura sopra descritta sarebbe la seguente:


    <!-- classi di accesso al livello [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
</bean>

Qui viene inizializzata la proprietà [sqlMapClient] (riga 3) della classe [DaoImplCommon] (riga 2). L’inizializzazione avviene tramite il metodo [setSqlMapClient] della classe [DaoImpl]. Questa classe non possiede tale metodo. È la sua classe padre [SqlMapClientDaoSupport] a possederlo. È quindi quest’ultima che viene effettivamente inizializzata in questo punto.

Ora, alla riga 4, si fa riferimento a un oggetto denominato "sqlMapClient" che deve ancora essere creato. Come già detto, questo è di tipo [SqlMapClient], un tipo [iBATIS]:

Image

[SqlMapClient] è un'interfaccia. Spring mette a disposizione la classe [SqlMapClientFactoryBean] per ottenere un oggetto che implementi tale interfaccia:

Image

Ricordiamo che stiamo cercando di istanziare un oggetto che implementi l’interfaccia [SqlMapClient]. A quanto pare, la classe [SqlMapClientFactoryBean] non soddisfa questo requisito. Essa implementa infatti l’interfaccia [FactoryBean] (vedi sopra). Quest’ultima dispone del seguente metodo [getObject()]:

Image

Quando si richiede a Spring un’istanza di un oggetto che implementi l’interfaccia [FactoryBean], esso:

  • crea un'istanza [I] della classe - in questo caso crea un'istanza di tipo [SqlMapClientFactoryBean].
  • restituisce al metodo chiamante il risultato del metodo [I].getObject() - il metodo [SqlMapClientFactoryBean].getObject() restituirà qui un oggetto che implementa l’interfaccia [SqlMapClient].

Per poter restituire un oggetto che implementa l’interfaccia [SqlMapClient], la classe [SqlMapClientFactoryBean] necessita di due informazioni indispensabili per tale oggetto:

  • un oggetto [DataSource] connesso al database, al quale richiederà le connessioni
  • uno o più file di configurazione in cui sono esternalizzati i comandi SQL da eseguire

La classe [SqlMapClientFactoryBean] dispone dei metodi set per inizializzare queste due proprietà:

Image

Stiamo facendo progressi... Il nostro file di configurazione si sta definendo e diventa:


<!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- le classi di accesso al livello [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
  • righe 2-3: il bean "sqlMapClient" è di tipo [SqlMapClientFactoryBean]. Da quanto appena spiegato, sappiamo che quando richiediamo a Spring un’istanza di questo bean, otteniamo un oggetto che implementa l’interfaccia iBATIS [SqlMapClient]. È proprio quest’ultimo oggetto che verrà quindi ottenuto alla riga 14.
  • righe 7-9: indichiamo che il file di configurazione necessario per l’oggetto iBATIS [SqlMapClient] si chiama "sql-map-config-firebird.xml" e che deve essere cercato nel ClassPath dell’applicazione. Qui viene utilizzato il metodo [SqlMapClientFactoryBean].setConfigLocation.
  • Righe 4-6: inizializziamo la proprietà [dataSource] di [SqlMapClientFactoryBean] con il suo metodo [setDataSource].

Riga 5: facciamo riferimento a un bean denominato "dataSource" che deve ancora essere creato. Se osserviamo il parametro richiesto dal metodo [setDataSource] di [SqlMapClientFactoryBean], vediamo che è di tipo [DataSource]:

Image

Ci troviamo nuovamente di fronte a un’interfaccia per la quale dobbiamo trovare una classe di implementazione. Il ruolo di tale classe è quello di fornire a un’applicazione, in modo efficiente, connessioni a un database specifico. Un SGBD non può mantenere aperte contemporaneamente un gran numero di connessioni. Per ridurre il numero di connessioni aperte in un dato momento, per ogni scambio con il database si è costretti a:

  • aprire una connessione
  • avviare una transazione
  • inviare comandi SQL
  • chiudere la transazione
  • chiudere la connessione

Aprire e chiudere ripetutamente le connessioni richiede molto tempo. Per risolvere questi due problemi (limitare sia il numero di connessioni aperte in un dato momento, sia il costo di apertura/chiusura delle stesse), le classi che implementano l’interfaccia [DataSource] procedono spesso nel modo seguente:

  • aprono, non appena vengono istanziate, N connessioni con il database di destinazione. N ha in genere un valore predefinito e nella maggior parte dei casi può essere definito in un file di configurazione. Queste N connessioni rimarranno sempre aperte e formano un pool di connessioni disponibili per i thread dell’applicazione.
  • Quando un thread dell’applicazione richiede l’apertura di una connessione, l’oggetto [DataSource] gli assegna una delle N connessioni aperte all’avvio, se ne rimangono disponibili. Quando l’applicazione chiude la connessione, questa in realtà non viene chiusa, ma semplicemente reinserita nel pool delle connessioni disponibili.

Esistono diverse implementazioni dell’interfaccia [DataSource] disponibili gratuitamente. In questa sede utilizzeremo l’implementazione [commons DBCP] disponibile all’URL [http://jakarta.apache.org/commons/dbcp/]:

Image

L’utilizzo dello strumento [commons DBCP] richiede due archivi [commons-dbcp, commons-pool], entrambi inseriti nella cartella [lib] del progetto:

La classe [BasicDataSource] di [commons DBCP] fornisce l'implementazione [DataSource] di cui abbiamo bisogno:

Image

Questa classe ci fornirà un pool di connessioni per accedere al database Firebird [dbpersonnes.gdb] della nostra applicazione. A tal fine, è necessario fornirle le informazioni necessarie per creare le connessioni del pool:

  1. il nome del driver JDBC da utilizzare – inizializzato con [setDriverClassName]
  2. il nome dell’URL del database da utilizzare – inizializzato con [setUrl]
  3. l’ID dell’utente proprietario della connessione – inizializzato con [setUsername] (e non con setUserName come ci si sarebbe potuto aspettare)
  4. la sua password - inizializzata con [setPassword]

Il file di configurazione del nostro livello [dao] potrebbe essere il seguente:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- la fonte dei dati DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- attenzione: non lasciare spazi tra i due tag <value> dell’URL -->
        <property name="url">
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- la classe di accesso al livello [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>
  • righe 7-9: il nome del driver JDBC di Firebird SGBD
  • righe 11-13: l’URL del database Firebird [dbpersonnes.gdb]. Presteremo particolare attenzione alla sua scrittura. Non devono esserci spazi tra i tag <value> e l’URL.
  • righe 14-16: il proprietario della connessione – in questo caso, [sysdba], che è l’amministratore predefinito delle distribuzioni Firebird
  • righe 17-19: la sua password [masterkey] – anch’essa il valore predefinito

Abbiamo fatto molti progressi, ma rimangono ancora alcuni aspetti di configurazione da chiarire: la riga 28 fa riferimento al file [sql-map-config-firebird.xml], che dovrebbe configurare il client [SqlMapClient] di iBATIS. Prima di esaminarne il contenuto, vediamo dove si trovano questi file di configurazione nel nostro progetto Eclipse:

Image

  • [spring-config-test-dao-firebird.xml] è il file di configurazione del livello [dao] che abbiamo appena esaminato
  • [sql-map-config-firebird.xml] è referenziato da [spring-config-test-dao-firebird.xml]. Lo esamineremo.
  • [personnes-firebird.xml] è referenziato da [sql-map-config-firebird.xml]. Lo esamineremo.

I tre file precedenti si trovano nella cartella [src]. In Eclipse, ciò significa che al momento dell’esecuzione saranno presenti nella cartella [bin] del progetto (non rappresentata sopra). Questa cartella fa parte del ClassPath dell’applicazione. In definitiva, i tre file precedenti saranno quindi presenti nel ClassPath dell’applicazione. Ciò è necessario.

Il file [sql-map-config-firebird.xml] è il seguente:


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
    PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-config-2.dtd">

<sqlMapConfig>
    <sqlMap resource="personnes-firebird.xml"/>
</sqlMapConfig>
  • questo file deve avere <sqlMapConfig> come tag radice (righe 6 e 8)
  • riga 7: il tag <sqlMap> serve a indicare i file che contengono i comandi SQL da eseguire. Spesso, ma non è obbligatorio, c'è un file per ogni tabella. Ciò consente di raggruppare i comandi SQL relativi a una determinata tabella in un unico file. Tuttavia, si incontrano frequentemente comandi SQL che coinvolgono più tabelle. In questo caso, la suddivisione precedente non è valida. È sufficiente ricordare che tutti i file contrassegnati dai tag <sqlMap> verranno uniti. Questi file vengono ricercati nel file ClassPath dell’applicazione.

Il file [personnes-firebird.xml] descrive i comandi SQL che verranno emessi sulla tabella [PERSONNES] del database Firebird [dbpersonnes.gdb]. Il suo contenuto è il seguente:


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

<!DOCTYPE sqlMap
    PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-2.dtd">

<sqlMap>
    <!-- alias della classe [Personne] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- tabella di mappatura [PERSONNES] - oggetto [Personne] -->
    <resultMap id="Personne.map" 
        class="Personne.classe">
        <result property="id" column="ID" />
        <result property="version" column="VERSION" />
        <result property="nom" column="NOM"/>
        <result property="prenom" column="PRENOM"/>
        <result property="dateNaissance" column="DATENAISSANCE"/>
        <result property="marie" column="MARIE"/>
        <result property="nbEnfants" column="NBENFANTS"/>
    </resultMap>
    <!-- elenco di tutte le persone -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
    <!-- recupera una persona specifica -->
        <select id="Personne.getOne" resultMap="Personne.map" >select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES WHERE ID=#valore#</select>
    <!-- aggiungere una persona -->
    <insert id="Personne.insertOne" parameterClass="Personne.classe">
        <selectKey keyProperty="id">
            SELECT GEN_ID(GEN_PERSONNES_ID,1) as "value" FROM RDB$$DATABASE
        </selectKey>         
        insert into 
        PERSONNES(ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
        VALUES(#id#, #versione#, #cognome#, #nome#, #dateNaissance#, #sposata#, 
        #nbEnfants#) </insert>
    <!-- aggiornare una persona -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#versione#+1, NOM=#cognome#, PRENOM=#nome#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE ID=#id# e 
        VERSION=#versione#</aggiornamento>
    <!-- eliminare una persona -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#valore# </delete>
</sqlMap>
  • il file deve avere <sqlMap> come tag radice (righe 7 e 45)
  • righe 9-10: per facilitare la scrittura del file, si assegna l’alias (sinonimo) [Personne.classe] alla classe [istia.st.springmvc.personnes.entites.Personne].
  • righe 12-21: definiscono le corrispondenze tra le colonne della tabella [PERSONNES] e i campi dell’oggetto [Personne].
  • righe 23-24: l’ordine SQL [select] per ottenere tutte le persone dalla tabella [PERSONNES]
  • righe 26-27: il comando SQL [select] per ottenere una persona specifica dalla tabella [PERSONNES]
  • righe 29-36: il comando SQL [insert] che inserisce una persona nella tabella [PERSONNES]
  • righe 38-41: il comando SQL [update] che aggiorna una persona nella tabella [PERSONNES]
  • righe 42-44: l'ordine SQL [delete] che elimina una persona dalla tabella [PERSONNES]

Il ruolo e il significato del contenuto del file [personnes-firebird.xml] saranno spiegati attraverso l’analisi della classe [DaoImplCommon] che implementa il livello [dao].

17.3.3. La classe [DaoImplCommon]

Torniamo all’architettura di accesso ai dati:

La classe [DaoImplCommon] è la seguente:

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;
import org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;

import java.util.Collection;

public class DaoImplCommon extends SqlMapClientDaoSupport implements
        IDao {

     // elenco delle persone
    public Collection getAll() {
...
    }

     // recuperare una persona specifica
    public Personne getOne(int id) {
...
    }

     // eliminazione di una persona
    public void deleteOne(int id) {
...
    }

     // aggiungere o modificare una persona
    public void saveOne(Personne personne) {
         // il parametro persona è valido?
        check(personne);
         // Aggiunta o modifica?
        if (personne.getId() == -1) {
             // aggiunta
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }

     // Aggiungi una persona
    protected void insertPersonne(Personne personne) {
...
    }

     // modifica di una persona
    protected void updatePersonne(Personne personne) {
...
    }

     // Verifica della validità di una persona
    private void check(Personne p) {
...
    }

...
}

Esamineremo i metodi uno dopo l’altro.


getAll


Questo metodo consente di ottenere tutte le persone presenti nell’elenco. Il suo codice è il seguente:

1
2
3
4
     // elenco delle persone
    public Collection getAll() {
        return getSqlMapClientTemplate().queryForList("Personne.getAll", null);
}

Ricordiamo innanzitutto che la classe [DaoImplCommon] deriva dalla classe Spring [SqlMapClientDaoSupport]. È questa classe che contiene il metodo [getSqlMapClientTemplate()] utilizzato nella riga 3 sopra riportata. Questo metodo ha la seguente firma:

Image

Il tipo [SqlMapClientTemplate] incapsula l’oggetto [SqlMapClient] del livello [iBATIS]. È tramite di esso che si avrà accesso al database. Il tipo [iBATIS] SqlMapClient potrebbe essere utilizzato direttamente poiché la classe [SqlMapClientDaoSupport] vi ha accesso:

Image

Lo svantaggio della classe [iBATIS] SqlMapClient è che genera eccezioni di tipo [SQLException], un tipo di eccezione controllata, c.a.d. che deve essere gestita tramite un try/catch o dichiarata nella firma dei metodi che la generano. Ricordiamo però che il livello [dao] implementa un’interfaccia [IDao] i cui metodi non contengono eccezioni nelle loro firme. Pertanto, nemmeno i metodi delle classi di implementazione dell’interfaccia [IDao] possono avere eccezioni nelle loro firme. Dobbiamo quindi intercettare ogni eccezione [SQLException] generata dal livello [iBATIS] e incapsularla in un’eccezione non controllata. Il tipo [DaoException] del nostro progetto sarebbe adatto a tale incapsulamento.

Anziché gestire noi stessi queste eccezioni, le affideremo al tipo Spring [SqlMapClientTemplate] che incapsula l’oggetto [SqlMapClient] del livello [iBATIS]. Infatti, [SqlMapClientTemplate] è stato progettato per intercettare le eccezioni [SQLException] generate dal livello [SqlMapClient] e incapsularle in un tipo [DataAccessException] non controllato. Questo comportamento ci soddisfa. Basterà semplicemente tenere presente che il livello [dao] è ora in grado di generare due tipi di eccezioni non controllate:

  • il nostro tipo proprietario [DaoException]
  • il tipo Spring [DataAccessException]

Il tipo [SqlMapClientTemplate] è definito come segue:

Image

Implementa la seguente interfaccia [SqlMapClientOperations]:

Image

Questa interfaccia definisce metodi in grado di gestire il contenuto del file [personnes-firebird.xml]:

[queryForList]

Image

Questo metodo consente di inviare un ordine [SELECT] e di recuperarne il risultato sotto forma di un elenco di oggetti:

  • [statementName]: l’identificativo (id) dell’ordine [select] nel file di configurazione
  • [parameterObject]: l’oggetto "parametro" per un [select] configurato. L’oggetto "parametro" può assumere due forme:
    • un oggetto conforme allo standard Javabean: i parametri dell’ordine [select] sono quindi i nomi dei campi del Javabean. All’esecuzione dell’ordine [select], essi vengono sostituiti dai valori di tali campi.
    • un dizionario: i parametri dell’ordine [select] sono quindi le chiavi del dizionario. All’esecuzione dell’ordine [select], queste vengono sostituite dai valori ad esse associati nel dizionario.
  • Se [SELECT] non restituisce alcuna riga, il risultato [List] è un oggetto privo di elementi, ma non null (da verificare).

[queryForObject]

Image

Questo metodo è identico nella logica a quello precedente, ma restituisce un solo oggetto. Se [SELECT] non restituisce alcuna riga, il risultato è il puntatore null.

[insert]

Image

Questo metodo consente di eseguire un comando SQL [insert] configurato dal secondo parametro. L’oggetto restituito è la chiave primaria della riga che è stata inserita. Non è obbligatorio utilizzare questo risultato.

[update]

Image

Questo metodo consente di eseguire un comando SQL [update] configurato dal secondo parametro. Il risultato è il numero di righe modificate dal comando SQL [update].

[delete]

Image

Questo metodo consente di eseguire un comando SQL [delete] configurato dal secondo parametro. Il risultato è il numero di righe eliminate dal comando SQL [delete].

Torniamo al metodo [getAll] della classe [DaoImplCommon]:

1
2
3
4
     // elenco delle persone
    public Collection getAll() {
        return getSqlMapClientTemplate().queryForList("Personne.getAll", null);
}
  • riga 4: viene eseguito l’ordine [select] denominato "Personne.getAll". Non è configurato e quindi l’oggetto "parametro" è null.

In [personnes-firebird.xml], il comando [select] denominato "Personne.getAll" è il seguente:


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

<!DOCTYPE sqlMap
    PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-2.dtd">

<sqlMap>
    <!-- alias classe [Personne] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- tabella di mappatura [PERSONNES] - oggetto [Personne] -->
    <resultMap id="Personne.map" 
        class="Personne.classe">
        <result property="id" column="ID" />
        <result property="version" column="VERSION" />
        <result property="nom" column="NOM"/>
        <result property="prenom" column="PRENOM"/>
        <result property="dateNaissance" column="DATENAISSANCE"/>
        <result property="marie" column="MARIE"/>
        <result property="nbEnfants" column="NBENFANTS"/>
    </resultMap>
    <!-- elenco di tutte le persone -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
...
</sqlMap>
  • riga 23: il comando SQL «Personne.getAll» non è parametrizzato (assenza di parametri nel testo della richiesta).
  • la riga 3 del metodo [getAll] richiede l’esecuzione della query [select] denominata "Personne.getAll". Quest’ultima verrà eseguita. [iBATIS] si basa su JDBC. Si sa quindi che il risultato della query sarà ottenuto sotto forma di un oggetto [ResultSet]. Riga 23: l’attributo [resultMap] del tag <select> indica a [iBATIS] quale " resultMap " deve utilizzare per trasformare ogni riga del [ResultSet] ottenuto in oggetto. È il " resultMap " [Personne.map] definito nelle righe 12-21 che indica come passare da una riga della tabella [PERSONNES] a un oggetto di tipo [Personne]. [iBATIS] utilizzerà queste corrispondenze per fornire un elenco di oggetti [Personne] a partire dalle righe dell’oggetto [ResultSet].
  • La riga 3 del metodo [getAll] restituisce quindi una collezione di oggetti [Personne]
  • il metodo [queryForList] può generare un'eccezione Spring [DataAccessException]. Lasciamo che venga propagata.

Spieghiamo più rapidamente gli altri metodi della classe [AbstractDaoImpl], poiché l’essenziale sull’utilizzo di [iBATIS] è già stato illustrato nell’analisi del metodo [getAll].


getOne


Questo metodo consente di ottenere una persona identificata tramite il suo [id]. Il suo codice è il seguente:

         // recuperare una persona specifica
    public Personne getOne(int id) {
         // la si recupera dalla BD
        Personne personne = (Personne) getSqlMapClientTemplate()
                .queryForObject("Personne.getOne", new Integer(id));
         // è stato recuperato qualcosa?
        if (personne == null) {
             // viene generata un'eccezione
            throw new DaoException(
                    "La personne d'id [" + id + "] n'existe pas", 2);
        }
         // si restituisce la persona
        return personne;
    }
  • riga 4: richiede l’esecuzione del comando [select] denominato "Personne.getOne". Questo è il seguente nel file [personnes-firebird.xml]:

<!-- ottenere una persona in particolare -->
        <select id="Personne.getOne" resultMap="Personne.map" parameterClass="int">
            select ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM 
            PERSONNES WHERE ID=#valore#</select>

L'ordine SQL è configurato dal parametro #value# (riga 4). L’attributo #value# indica il valore del parametro passato al comando SQL, quando tale parametro è di tipo semplice: Integer, Double, String, ... Negli attributi del tag <select>, l’attributo [parameterClass] indica che il parametro è di tipo intero (riga 2). Alla riga 5 di [getOne], si vede che questo parametro è l’identificativo della persona ricercata sotto forma di un oggetto Integer. Questo cambio di tipo è obbligatorio poiché il secondo parametro di [queryForList] deve essere di tipo [Object].

Il risultato della query [select] dovrà essere convertito in oggetto tramite l’attributo [resultMap="Personne.map"] (riga 2). Si otterrà quindi un tipo [Personne].

  • righe 7-11: se la query [select] non ha restituito alcuna riga, si recupera il puntatore null alla riga 4. Ciò significa che la persona ricercata non è stata trovata. In questo caso, si lancia una [DaoException] con codice 2 (righe 9-10).
  • riga 13: se non si è verificata alcuna eccezione, viene restituito l’oggetto [Personne] richiesto.

deleteOne


Questo metodo consente di eliminare una persona identificata dal suo [id]. Il suo codice è il seguente:

     // eliminazione di una persona
    public void deleteOne(int id) {
         // si elimina la persona
        int n = getSqlMapClientTemplate().delete("Personne.deleteOne",
                new Integer(id));
         // l'operazione è andata a buon fine
        if (n == 0) {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        }
    }
  • righe 4-5: richiede l’esecuzione del comando [delete] denominato «Personne.deleteOne». Questo è il seguente nel file [personnes-firebird.xml]:

<!-- eliminare una persona -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#valore# </delete>

L'ordine SQL è configurato dal parametro #value# (riga 3) di tipo [parameterClass="int"] (riga 2). Questo sarà l'identificativo della persona ricercata (riga 5 di deleteOne)

  • riga 4: il risultato del metodo [SqlMapClientTemplate].delete è il numero di righe eliminate.
  • righe 7-8: se la query [delete] non ha eliminato alcuna riga, significa che la persona non esiste. Si esegue una [DaoException] con codice 2 (riga 8).

saveOne


Questo metodo consente di aggiungere una nuova persona o di modificare una persona esistente. Il suo codice è il seguente:

         // aggiungere o modificare una persona
    public void saveOne(Personne personne) {
         // il parametro "persona" è valido?
        check(personne);
         // Aggiunta o modifica?
        if (personne.getId() == -1) {
             // aggiunta
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }
...
  • riga 4: si verifica la validità della persona con il metodo [check]. Questo metodo era già presente nella versione precedente ed era stato allora commentato. Se la persona non è valida, genera un [DaoException]. Si lascia che questo venga riportato.
  • riga 6: se si arriva a questo punto, significa che non si è verificata alcuna eccezione. La persona è quindi valida.
  • righe 6-11: a seconda dell’ID della persona, si tratta di un’aggiunta (ID = -1) o di un aggiornamento (ID ≠ -1). In entrambi i casi, si ricorre a due metodi interni alla classe:
    • insertPersonne: per l’aggiunta
    • updatePersonne: per l’aggiornamento

insertPersonne


Questo metodo consente di aggiungere una nuova persona. Il suo codice è il seguente:

// aggiungere una persona
    protected void insertPersonne(Personne personne) {
         // Prima versione
        personne.setVersion(1);
         // si attende 10 ms - per i test impostare "true" invece di "false"
        if (true)
            wait(10);
         // si inserisce la nuova persona nella tabella di BD
        getSqlMapClientTemplate().insert("Personne.insertOne", personne);
    }
  • riga 4: si imposta a 1 il numero di versione della persona che si sta creando
  • riga 9: si esegue l’inserimento tramite la query denominata "Personne.insertOne", che è la seguente:

        <insert id="Personne.insertOne" parameterClass="Personne.classe">
            <selectKey keyProperty="id">
                SELECT GEN_ID(GEN_PERSONNES_ID,1) as "value" FROM RDB$$DATABASE
            </selectKey>         
        insert into 
        PERSONNES(ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
        VALUES(#id#, #versione#, #cognome#, #nome#, #dateNaissance#, #maria#, 
    #nbEnfants#) </insert>

Si tratta di una query parametrizzata e il parametro è di tipo [Personne] (parameterClass="Personne.classe", riga 1). I campi dell’oggetto [Personne] passati come parametro (riga 9 di insertPersonne) vengono utilizzati per compilare le colonne della riga che verrà inserita nella tabella [PERSONNES] (righe 5-8). C'è un problema da risolvere. Durante un inserimento, l'oggetto [Personne] da inserire ha un ID pari a -1. È necessario sostituire questo valore con una chiave primaria valida. A tal fine si utilizzano le righe 2-4 del tag <selectKey> sopra riportato. Esse indicano:

  • (continua)
    • la query SQL da eseguire per ottenere un valore di chiave primaria. Quella qui indicata è quella che abbiamo presentato al paragrafo 17.1. Due punti da notare:
      • «as "value"» è obbligatorio. Si può anche scrivere «as value», ma «value» è una parola chiave di Firebird che deve essere protetta dalle virgolette.
      • La tabella Firebird si chiama in realtà [RDB$DATABASE]. Tuttavia, il carattere $ viene interpretato come [iBATIS]. È stato protetto duplicandolo.
    • Il campo dell'oggetto [Personne] che deve essere inizializzato con il valore recuperato dal comando [SELECT], in questo caso il campo [id]. È l’attributo [keyProperty] della riga 2 che indica questo campo.
  • righe 6-7: ai fini dei test, dovremo attendere 10 ms prima di effettuare l’inserimento, per verificare se vi siano conflitti tra thread che intendono effettuare aggiunte contemporaneamente.

updatePersonne


Questo metodo consente di modificare una persona già presente nella tabella [PERSONNES]. Il suo codice è il seguente:

// modifica una persona
    protected void updatePersonne(Personne personne) {
         // si attende 10 ms - per i test impostare true al posto di false
        if (true)
            wait(10);
         // modifica
        int n = getSqlMapClientTemplate()
                .update("Personne.updateOne", personne);
        if (n == 0)
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] n'existe pas ou bien a été modifiée", 2);
    }
  • Un aggiornamento può fallire per almeno due motivi:
    1. il soggetto da aggiornare non esiste
    2. la persona da aggiornare esiste, ma il thread che intende modificarla non dispone della versione corretta
  • righe 7-8: viene eseguita la query SQL [update] denominata "Personne.updateOne". È la seguente:

    <!-- aggiornare una persona -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#versione#+1, NOM=#cognome#, PRENOM=#nome#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE ID=#id# e 
VERSION=#versione#</aggiornamento>
  • (continua)
    • riga 2: la richiesta è configurata e accetta come parametro un tipo [Personne] (parameterClass="Personne.classe"). Si tratta della persona da modificare (riga 8 – updatePersonne).
    • Si desidera modificare solo la persona della tabella [PERSONNES] che abbia lo stesso numero [id] e la stessa versione [version] del parametro. Per questo motivo, viene applicato il vincolo [WHERE ID=#id# and VERSION=#version#]. Se questa persona viene individuata, viene aggiornata con i dati della persona parametro e la sua versione viene incrementata di 1 (riga 3 sopra).
  • riga 9: si recupera il numero di righe aggiornate.
  • righe 10-11: se tale numero è pari a zero, si avvia un [DaoException] con codice 2, indicando che la persona da aggiornare non esiste oppure che nel frattempo ha cambiato versione.

17.4. Test del livello [dao]

17.4.1. Test dell’implementazione [DaoImplCommon]

Ora che abbiamo scritto il livello [dao], intendiamo testarlo con i test JUnit:

Image

Prima di eseguire test intensivi, possiamo iniziare con un semplice programma di tipo [main] che visualizzerà il contenuto della tabella [PERSONNES]. Si tratta della classe [MainTestDaoFirebird]:

package istia.st.mvc.personnes.tests;

import istia.st.mvc.personnes.dao.IDao;

import java.util.Collection;
import java.util.Iterator;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

public class MainTestDaoFirebird {
    public static void main(String[] args) {
        IDao dao = (IDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao-firebird.xml"))).getBean("dao");
         // elenco attuale
        Collection personnes = dao.getAll();
         // visualizzazione console
        Iterator iter = personnes.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
        }
    }
}

Il file di configurazione [spring-config-test-dao-firebird.xml] del livello [dao], utilizzato alle righe 13-14, è il seguente:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- la fonte dei dati DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- attenzione: non lasciare spazi tra i due tag <value> -->
        <property name="url">
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- classe di accesso al livello [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>

Questo file è quello analizzato nel paragrafo 17.3.2.

Per il test, viene avviato Firebird SGBD. Il contenuto della tabella [PERSONNES] è il seguente:

Image

L'esecuzione del programma [MainTestDaoFirebird] produce i seguenti risultati sullo schermo:

Image

Abbiamo ottenuto correttamente l’elenco delle persone. Possiamo passare al test JUnit.

Il test JUnit [TestDaoFirebird] è il seguente:

package istia.st.mvc.personnes.tests;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Iterator;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;
import junit.framework.TestCase;

public class TestDaoFirebird extends TestCase {

     // livello [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

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

     // costruttore
    public void setUp() {
        dao = (IDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao-firebird.xml"))).getBean("dao");
    }

     // elenco delle persone
    private void doListe(Collection personnes) {
...
    }

     // test1
    public void test1() throws ParseException {
...
    }

     // modifica-eliminazione di un elemento inesistente
    public void test2() throws ParseException {
..
    }

     // gestione delle versioni di una persona
    public void test3() throws ParseException, InterruptedException {
...
    }

     // blocco ottimistico - accesso multithread
    public void test4() throws Exception {
...
    }

     // test di validità di saveOne
    public void test5() throws ParseException {
....
    }

     // inserimenti multithread
    public void test6() throws ParseException, InterruptedException{
...
}
  • I test da [test1] a [test5] sono gli stessi della versione 1, tranne [test4] che ha subito una leggera modifica. Il test [test6] è invece nuovo. Commenteremo solo questi due test.

[test4]


[test4] ha lo scopo di testare il metodo [updatePersonne - DaoImplCommon]. Ricordiamo il codice di quest’ultimo:

// modifica di una persona
    protected void updatePersonne(Personne personne) {
         // attesa di 10 ms - per i test impostare true al posto di false
        if (true)
            wait(10);
         // modifica
        int n = getSqlMapClientTemplate()
                .update("Personne.updateOne", personne);
        if (n == 0)
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] n'existe pas ou bien a été modifiée", 2);
    }
  • righe 4-5: si attende 10 ms. In questo modo si costringe il thread che esegue [updatePersonne] a perdere il controllo del processore, il che può aumentare le nostre possibilità di osservare conflitti di accesso tra thread concorrenti.

[test4] avvia N=100 thread incaricati di incrementare, contemporaneamente, di 1 il numero di figli della stessa persona. Vogliamo osservare come vengono gestiti i conflitti di versione e i conflitti di accesso.

    public void test4() throws Exception {
         // aggiunta di una persona
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        dao.saveOne(p1);
        int id1 = p1.getId();
         // creazione di N thread per l'aggiornamento del numero di figli
        final int N = 100;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadDaoMajEnfants("thread n° " + i, dao, id1);
            taches[i].start();
        }
         // si attende la fine dei thread
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
         // si recupera la persona
        p1 = dao.getOne(id1);
         // deve avere N figli
        assertEquals(N, p1.getNbEnfants());
         // eliminazione della persona p1
        dao.deleteOne(p1.getId());
         // verifica
        boolean erreur = false;
        int codeErreur = 0;
        try {
            p1 = dao.getOne(p1.getId());
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // dovrebbe esserci un errore con codice 2
        assertTrue(erreur);
        assertEquals(2, codeErreur);
    }

I thread vengono creati alle righe 8-13. Ciascuno aumenterà di 1 il numero di figli della persona creata alle righe 3-5. I thread di aggiornamento [ThreadDaoMajEnfants ] sono i seguenti:

package istia.st.mvc.personnes.tests;

import java.util.Date;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;

public class ThreadDaoMajEnfants extends Thread {
     // nome del thread
    private String name;

     // riferimento al livello [dao]
    private IDao dao;

     // ID della persona su cui si lavorerà
    private int idPersonne;

     // costruttore
    public ThreadDaoMajEnfants(String name, IDao dao, int idPersonne) {
        this.name = name;
        this.dao = dao;
        this.idPersonne = idPersonne;
    }

     // nucleo del thread
    public void run() {
         // monitoraggio
        suivi("lancé");
         // si ripete il ciclo finché non si riesce ad incrementare di 1
         // il numero di figli della persona idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
             // si recupera una copia della persona da idPersonne
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
             // monitoraggio
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1)
                    + " pour la version " + personne.getVersion());
             // attesa di 10 ms per liberare il processore
            try {
                 // seguito
                suivi("début attente");
                 // si interrompe per lasciare spazio al processore
                Thread.sleep(10);
                 // monitoraggio
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
             // attesa terminata - si tenta di convalidare la copia
             // nel frattempo altri thread potrebbero aver modificato l'originale
            int codeErreur = 0;
            try {
                 // si incrementa di 1 il numero di figli di questa copia
                personne.setNbEnfants(nbEnfants + 1);
                 // si sta tentando di modificare l'originale
                dao.saveOne(personne);
                 // L'operazione è andata a buon fine: l'originale è stato modificato
                fini = true;
            } catch (DaoException ex) {
                 // si recupera il codice di errore
                codeErreur = ex.getCode();
                 // se si verifica un errore di ID o un codice di errore 2, si riprova l'aggiornamento
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                default:
                     // eccezione non gestita - si lascia che venga segnalata
                    throw ex;
                }
            }
        }
         // monitoraggio
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

     // monitoraggio
    private void suivi(String message) {
        System.out.println(name + " [" + new Date().getTime() + "] : "
                + message);
    }
}

Un aggiornamento di una persona può fallire perché la persona che si desidera modificare non esiste o è stata aggiornata in precedenza da un altro thread. Questi due casi sono gestiti qui alle righe 67-69. In entrambi i casi, infatti, il metodo [updatePersonne] avvia un [DaoException] con codice 2. Il thread verrà quindi riportato all’inizio per ricominciare la procedura di aggiornamento dall’inizio (ciclo while, riga 34).


[test6]


[test6] ha lo scopo di testare il metodo [insertPersonne - DaoImplCommon]. Ricordiamo il codice di quest’ultimo:

// aggiungere una persona
    protected void insertPersonne(Personne personne) {
         // prima versione
        personne.setVersion(1);
         // si attendono 10 ms - per i test impostare true al posto di false
        if (true)
            wait(10);
         // si inserisce la nuova persona nella tabella di BD
        getSqlMapClientTemplate().insert("Personne.insertOne", personne);
    }
  • righe 6-7: si attende 10 ms per costringere il thread che esegue [insertPersonne] a perdere il controllo del processore e aumentare così le probabilità che si verifichino conflitti dovuti a thread che effettuano inserimenti contemporaneamente.

Il codice di [test6] è il seguente:

     // inserimenti multithread
    public void test6() throws ParseException, InterruptedException{
         // creazione di una persona
        Personne p = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
         // che viene duplicata N volte in un array
        final int N = 100;
        Personne[] personnes=new Personne[N];
        for(int i=0;i<personnes.length;i++){
            personnes[i]=new Personne(p);
        }
         // creazione di N thread di inserimento - ogni thread inserisce 1 persona
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadDaoInsertPersonne("thread n° " + i, dao, personnes[i]);
            taches[i].start();
        }
         // si attende il completamento dei thread
        for (int i = 0; i < taches.length; i++) {
             // thread n. i
            taches[i].join();
             // eliminazione di una persona
            dao.deleteOne(personnes[i].getId());
        }
}

Si creano 100 thread che inseriranno contemporaneamente 100 persone diverse. Questi 100 thread otterranno tutti una chiave primaria per la persona che devono inserire, per poi essere interrotti per 10 ms (riga 10 – insertPersonne) prima di poter effettuare l’inserimento. Vogliamo verificare che tutto proceda correttamente e, in particolare, che ottengano effettivamente valori di chiave primaria diversi.

  • righe 7-11: viene creato un array di 100 persone. Queste persone sono tutte copie della persona p creata alle righe 4-5.
  • righe 14-17: vengono avviati i 100 thread di inserimento. Ciascuno di essi ha il compito di inserire una delle 100 persone create in precedenza.
  • righe 19-23: [test6] attende il completamento di ciascuno dei 100 thread che ha avviato. Quando rileva il completamento del thread n. i, elimina la persona che quel thread ha appena inserito.

Il thread di inserimento [ThreadDaoInsertPersonne] è il seguente:

package istia.st.mvc.personnes.tests;

import java.util.Date;

import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;

public class ThreadDaoInsertPersonne extends Thread {
     // nome del thread
    private String name;

     // riferimento sul livello [dao]
    private IDao dao;

     // ID della persona su cui si lavorerà
    private Personne personne;

     // costruttore
    public ThreadDaoInsertPersonne(String name, IDao dao, Personne personne) {
        this.name = name;
        this.dao = dao;
        this.personne = personne;
    }

     // nucleo del thread
    public void run() {
         // monitoraggio
        suivi("lancé");
         // inserimento
        dao.saveOne(personne);
         // monitoraggio
        suivi("a terminé");
    }

     // monitoraggio
    private void suivi(String message) {
        System.out.println(name + " [" + new Date().getTime() + "] : "
                + message);
    }
}
  • righe 19-22: il costruttore del thread memorizza la persona che deve inserire e il livello [dao] che deve utilizzare per effettuare tale inserimento.
  • riga 30: la persona viene inserita. Se si verifica un'eccezione, questa viene segnalata a [test6].

Test


Durante i test si ottengono i seguenti risultati:

Il test [test4] fallisce quindi. Il numero di figli è sceso a 69 invece dei 100 previsti. Cosa è successo? Esaminiamo i log di schermo. Mostrano l’esistenza di eccezioni generate da Firebird:


Exception in thread "Thread-62" org.springframework.jdbc.UncategorizedSQLException: SqlMapClient operation; uncategorized SQLException for SQL []; SQL state [HY000]; error code [335544336];   
--- Si è verificato un errore in personnes-firebird.xml.  
--- Si è verificato un errore durante l'applicazione di una mappa dei parametri.  
--- Controllare il file Personne.updateOne-InlineParameterMap.  
--- Controllare l'istruzione (aggiornamento non riuscito).  
--- Causa: org.firebirdsql.jdbc.FBSQLException: Eccezione GDS. 335544336. deadlock
update conflicts with concurrent update; nested exception is com.ibatis.common.jdbc.exception.NestedSQLException:   
--- Si è verificato un errore in personnes-firebird.xml.  
--- Si è verificato un errore durante l'applicazione di una mappa dei parametri.  
  • riga 1 – si è verificata un'eccezione Spring [org.springframework.jdbc.UncategorizedSQLException]. Si tratta di un'eccezione non controllata utilizzata per incapsulare un'eccezione generata dal driver JDBC di Firebird, descritta alla riga 6.
  • riga 6 – il driver JDBC di Firebird ha generato un'eccezione di tipo [org.firebirdsql.jdbc.FBSQLException] con codice di errore 335544336.
  • riga 7: indica che si è verificato un conflitto di accesso tra due thread che volevano aggiornare contemporaneamente la stessa riga della tabella [PERSONNES].

Non si tratta di un errore irreversibile. Il thread che intercetta questa eccezione può riprovare ad eseguire l'aggiornamento. A tal fine è necessario modificare il codice di [ThreadDaoMajEnfants]:

            try {
                 // incrementa di 1 il numero di figli di questa copia
                personne.setNbEnfants(nbEnfants + 1);
                 // si sta tentando di modificare l'originale
                dao.saveOne(personne);
                 // operazione completata - l'originale è stato modificato
                fini = true;
            } catch (DaoException ex) {
                 // si recupera il codice di errore
                codeErreur = ex.getCode();
                 // se si verifica un errore di ID o un errore di versione 2, si riprova l'aggiornamento
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                default:
                     // eccezione non gestita - si lascia che venga segnalata
                    throw ex;
                }
  • riga 8: si gestisce un'eccezione di tipo [DaoException]. In base a quanto detto, dovremmo gestire l'eccezione che si è verificata durante i test, ovvero il tipo [org.springframework.jdbc.UncategorizedSQLException]. Non ci si può tuttavia limitare a gestire questo tipo, che è un tipo generico di Spring destinato a incapsulare eccezioni che non conosce. Spring riconosce le eccezioni generate dai driver JDBC di alcuni SGBD quali Oracle, MySQL, Postgres, DB2, SQL Server, ... ma non Firebird. Pertanto, qualsiasi eccezione generata dal driver JDBC di Firebird viene incapsulata nel tipo Spring [org.springframework.jdbc.UncategorizedSQLException]:

Image

Come si vede sopra, la classe [UncategorizedSQLException] deriva dalla classe [DataAccessException] di cui abbiamo parlato nel paragrafo 17.3.3. È possibile identificare l’eccezione incapsulata in [UncategorizedSQLException] grazie al suo metodo [getSQLException]:

Image

Questa eccezione di tipo [SQLException] è quella generata dal livello [iBATIS], che a sua volta incapsula l’eccezione generata dal driver JDBC del database. La causa esatta dell’eccezione di tipo [SQLException] può essere ottenuta tramite il metodo:

Image

Si ottiene l’oggetto di tipo [Throwable] che è stato generato dal driver JDBC:

Image

Il tipo [Throwable] è la classe padre di [Exception].

In questo caso dovremo verificare che l’oggetto di tipo [Throwable], generato dal driver JDBC di Firebird e causa dell’eccezione [SQLException] generata dal livello [iBATIS] sia effettivamente un'eccezione di tipo [org.firebirdsql.gds.GDSException] con codice di errore 335544336. Per recuperare il codice di errore, possiamo utilizzare il metodo [getErrorCode()] della classe [org.firebirdsql.gds.GDSException].

Se nel codice di [ThreadDaoMajEnfants] utilizziamo l’eccezione [org.firebirdsql.gds.GDSException], allora questo thread potrà funzionare solo con Firebird SGBD. Lo stesso vale per il test [test4] che utilizza questo thread. Vogliamo evitare questa situazione. Infatti, desideriamo che i nostri test JUnit rimangano validi indipendentemente dal SGBD utilizzato. Per ottenere questo risultato, si decide che il livello [dao] avvierà un [DaoException] con codice 4 quando viene rilevata un’eccezione di tipo “conflitto di aggiornamento”, indipendentemente dal SGBD sottostante. Pertanto, il thread [ThreadDaoMajEnfants] potrà essere riscritto come segue:

package istia.st.mvc.personnes.tests;
...

public class ThreadDaoMajEnfants extends Thread {
...

     // nucleo del thread
    public void run() {
...
        while (!fini) {
             // si recupera una copia della persona da idPersonne
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
...
             // attesa terminata - si tenta di convalidare la copia
             // nel frattempo altri thread potrebbero aver modificato l'originale
            int codeErreur = 0;
            try {
                 // si incrementa di 1 il numero di figli di questa copia
                personne.setNbEnfants(nbEnfants + 1);
                 // si sta tentando di modificare l'originale
                dao.saveOne(personne);
                 // è stato aggiornato - l'originale è stato modificato
                fini = true;
            } catch (DaoException ex) {
                 // si recupera il codice di errore
                codeErreur = ex.getCode();
                 // se si verifica un errore di ID o di versione 2 o un deadlock 4, allora
                 // si riprova l'aggiornamento
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                case 4:
                    suivi("conflit de mise à jour");
                    break;
                default:
                     // eccezione non gestita - si lascia che venga segnalata
                    throw ex;
                }
            }
        }
         // monitoraggio
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }
...
}
  • righe 34-36: l’eccezione di tipo [DaoException] con codice 4 viene intercettata. Il thread [ThreadDaoMajEnfants] sarà costretto a ricominciare la procedura di aggiornamento dall’inizio (riga 10)

Il nostro livello [dao] deve quindi essere in grado di riconoscere un'eccezione di tipo «conflitto di aggiornamento». Questa viene generata da un driver JDBC ed è specifica di quest'ultimo. Tale eccezione deve essere gestita nel metodo [updatePersonne] della classe [DaoImplCommon]:

// modifica di una persona
    protected void updatePersonne(Personne personne) {
         // si attende 10 ms - per i test impostare true al posto di false
        if (true)
            wait(10);
         // modifica
        int n = getSqlMapClientTemplate()
                .update("Personne.updateOne", personne);
        if (n == 0)
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] n'existe pas ou bien a été modifiée", 2);
    }

Le righe 7-11 devono essere racchiuse in un blocco try/catch. Per il driver Firebird SGBD, occorre verificare che l’eccezione che ha causato il fallimento dell’aggiornamento sia di tipo [org.firebirdsql.gds.GDSException] e abbia come codice di errore 335544336. Se inseriamo questo tipo di controllo in [DaoImplCommon], collegheremo questa classe al Firebird SGBD, il che ovviamente non è auspicabile. Se vogliamo mantenere un carattere generico alla classe [DaoImplCommon], dobbiamo derivarla e gestire l’eccezione in una classe specifica per Firebird. È ciò che faremo ora.

17.4.2. La classe [DaoImplFirebird]

Il suo codice è il seguente:

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;

public class DaoImplFirebird extends DaoImplCommon {

     // modifica di una persona
    protected void updatePersonne(Personne personne) {
         // si attende 10 ms - per i test impostare "true" al posto di "false"
        if (true)
            wait(10);
         // modifica
        try {
             // si modifica la persona che ha la versione corretta
            int n = getSqlMapClientTemplate().update("Personne.updateOne",
                    personne);
            if (n == 0)
                throw new DaoException("La personne d'Id [" + personne.getId()
                        + "] n'existe pas ou bien a été modifiée", 2);
        } catch (org.springframework.jdbc.UncategorizedSQLException ex) {
            if (ex.getSQLException().getCause().getClass().isAssignableFrom(
                    org.firebirdsql.jdbc.FBSQLException.class)) {
                org.firebirdsql.jdbc.FBSQLException cause = (org.firebirdsql.jdbc.FBSQLException) ex
                        .getSQLException().getCause();
                if (cause.getErrorCode() == 335544336) {
                    throw new DaoException(
                            "Conflit d'accès au même enregistrement", 4);
                }
            } else {
                throw ex;
            }
        }
    }

     // attesa
    private void wait(int N) {
         // si attende N ms
        try {
            Thread.sleep(N);
        } catch (InterruptedException e) {
             // si visualizza la traccia dell'eccezione
            e.printStackTrace();
            return;
        }
    }

}
  • riga 5: la classe [DaoImplFirebird] deriva da [DaoImplCommon], la classe che abbiamo appena esaminato. Ridefinisce, alle righe 8-33, il metodo [updatePersonne] che ci crea problemi.
  • riga 20: intercettiamo l’eccezione Spring di tipo [UncategorizedSQLException]
  • righe 21-22: verifichiamo che l’eccezione sottostante di tipo [SQLException], generata dal livello [iBATIS], sia causata da un’eccezione di tipo [org.firebirdsql.jdbc.FBSQLException]
  • riga 25: si verifica inoltre che il codice di errore di questa eccezione Firebird sia 335544336, il codice di errore del "deadlock".
  • righe 26-27: se tutte queste condizioni sono soddisfatte, viene generata una [DaoException] con codice 4.
  • righe 36-44: il metodo [wait] consente di sospendere il thread corrente per N millisecondi. È utile solo a scopo di test.

Siamo pronti per i test del nuovo livello [dao].

17.4.3. Test dell’implementazione [DaoImplFirebird]

Il file di configurazione dei test [spring-config-test-dao-firebird.xml] viene modificato per utilizzare l’implementazione [DaoImplFirebird]:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- la fonte dei dati DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- attenzione: non lasciare spazi tra i due tag <value> -->
        <property name="url">
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- la classe di accesso al livello [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>
  • riga 32: la nuova implementazione [DaoImplFirebird] del livello [dao].

I risultati del test [test4], che in precedenza era fallito, sono i seguenti:

Image

[test4] è stato eseguito con successo. Le ultime righe dei log di schermo sono le seguenti:

1
2
3
4
5
6
7
thread n° 36 [1145977145984] : fin attente
thread n° 75 [1145977145984] : a terminé et passé le nombre d'enfants à 99
thread n° 36 [1145977146000] : version corrompue ou personne inexistante
thread n° 36 [1145977146000] : 99 -> 100 pour la version 100
thread n° 36 [1145977146000] : début attente
thread n° 36 [1145977146015] : fin attente
thread n° 36 [1145977146031] : a terminé et passé le nombre d'enfants à 100

L’ultima riga indica che è stato il thread n. 36 a terminare per ultimo. La riga 3 mostra un conflitto di versione che ha costretto il thread n. 36 a riprendere la procedura di aggiornamento della persona (riga 4). Altri log mostrano conflitti di accesso durante gli aggiornamenti:

1
2
3
thread n° 52 [1145977145765] : version corrompue ou personne inexistante
thread n° 75 [1145977145765] : conflit de mise à jour
thread n° 36 [1145977145765] : version corrompue ou personne inexistante

La riga 2 mostra che il thread n. 75 ha fallito durante l’aggiornamento a causa di un conflitto di aggiornamento: quando il comando SQL [update] è stato emesso sulla tabella [PERSONNES], la riga da aggiornare era bloccata da un altro thread. Questo conflitto di accesso costringerà il thread n. 75 a riprovare l’aggiornamento.

Per concludere con [test4], si nota una differenza significativa rispetto ai risultati dello stesso test nella versione 1, dove il test era fallito a causa di problemi di sincronizzazione. Poiché i metodi del livello [dao] della versione 1 non erano sincronizzati, si verificavano conflitti di accesso. In questo caso, non è stato necessario sincronizzare il livello [dao]. Abbiamo semplicemente gestito i conflitti di accesso segnalati da Firebird.

Eseguiamo ora l’intero test JUnit del livello [dao]:

Image

Sembra quindi che il livello [dao] sia valido. Per dichiararlo valido con un’elevata probabilità, dovremmo eseguire ulteriori test. Tuttavia, lo considereremo operativo.

17.5. Il livello [service]

17.5.1. Le componenti del livello [service]

Il livello [service] è costituito dalle seguenti classi e interfacce:

Image

  • [IService] è l'interfaccia fornita dal livello [service]
  • [ServiceImpl] ne costituisce un'implementazione

L’interfaccia [IService] è la seguente:

package istia.st.mvc.personnes.service;

import istia.st.mvc.personnes.entites.Personne;

import java.util.Collection;

public interface IService {
     // elenco di tutte le persone
    Collection getAll();

     // ricerca di una persona specifica
    Personne getOne(int id);

     // aggiungere/modificare una persona
    void saveOne(Personne personne);

     // eliminare una persona
    void deleteOne(int id);

     // salvare più persone
    void saveMany(Personne[] personnes);

     // eliminare più persone
    void deleteMany(int ids[]);
}
  • L'interfaccia presenta gli stessi quattro metodi della versione 1, ma ne ha due in più:
    • saveMany: consente di salvare più persone contemporaneamente in modo atomico. O vengono salvate tutte, oppure nessuna.
    • deleteMany: consente di eliminare più persone contemporaneamente in modo atomico. O vengono eliminate tutte, oppure nessuna.

Questi due metodi non saranno utilizzati dall’applicazione web. Li abbiamo aggiunti per illustrare il concetto di transazione in un database. Entrambi i metodi dovranno infatti essere eseguiti all’interno di una transazione per ottenere l’atomicità desiderata.

La classe [ServiceImpl] che implementa questa interfaccia sarà la seguente:

package istia.st.mvc.personnes.service;

import istia.st.mvc.personnes.entites.Personne;
import istia.st.mvc.personnes.dao.IDao;

import java.util.Collection;

public class ServiceImpl implements IService {

     // il livello [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

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

     // elenco delle persone
    public Collection getAll() {
        return dao.getAll();
    }

     // recuperare una persona specifica
    public Personne getOne(int id) {
        return dao.getOne(id);
    }

     // aggiungere o modificare una persona
    public void saveOne(Personne personne) {
        dao.saveOne(personne);
    }

     // eliminazione di una persona
    public void deleteOne(int id) {
        dao.deleteOne(id);
    }

     // salvare una raccolta di persone
    public void saveMany(Personne[] personnes) {
         // iterazione sull'array delle persone
        for (int i = 0; i < personnes.length; i++) {
            dao.saveOne(personnes[i]);
        }
    }

     // eliminare una raccolta di persone
    public void deleteMany(int[] ids) {
         // ids: gli ID delle persone da eliminare
        for (int i = 0; i < ids.length; i++) {
            dao.deleteOne(ids[i]);
        }
    }
}
  • I metodi [getAll, getOne, insertOne, saveOne] richiamano i metodi omonimi del livello [dao].
  • righe 42-47: il metodo [saveMany] salva, una per una, le persone presenti nell’array passato come parametro.
  • righe 50-55: il metodo [deleteMany] elimina, una alla volta, le persone il cui array è stato passato come parametro dal metodo id

Abbiamo detto che i metodi [saveMany] e [deleteMany] devono essere eseguiti all’interno di una transazione per garantire il carattere «tutto o niente» di questi metodi. Possiamo notare che il codice sopra riportato ignora completamente questo concetto di transazione. Quest’ultimo apparirà solo nel file di configurazione del livello [service].

17.5.2. Configurazione del livello [service]

Nella riga 11 sopra riportata, si nota che l’implementazione [ServiceImpl] contiene un riferimento al livello [dao]. Quest’ultimo, come nella versione 1, verrà inizializzato da Spring al momento dell’istanziazione del livello [service - ServiceImpl]. Il file di configurazione che consentirà l’istanziazione del livello [service] sarà il seguente:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- la fonte dei dati DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <property name="url">
            <!-- attenzione: non lasciare spazi tra i due tag <value> -->
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- la classe di accesso al livello [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
    <!-- gestore delle transazioni -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!-- le classi di accesso al livello [service] -->
    <bean id="service" 
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager">
            <ref local="transactionManager"/>
        </property>
        <property name="target">
            <bean class="istia.st.mvc.personnes.service.ServiceImpl">
                <property name="dao">
                    <ref local="dao"/>
                </property>
            </bean>
        </property>
        <property name="transactionAttributes">
            <props>
                <prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
                <prop key="save*">PROPAGATION_REQUIRED</prop>
                <prop key="delete*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>
</beans>
  • righe 1-36: configurazione del livello [dao]. Questa configurazione è stata spiegata durante l’analisi del livello [dao] al paragrafo 17.3.2.
  • righe 38-64: configurano il livello [service]

Alla riga 46 si può notare che l’implementazione del livello [service] è effettuata dal tipo [TransactionProxyFactoryBean]. Ci si aspettava di trovare il tipo [ServiceImpl]. [TransactionProxyFactoryBean] è un tipo predefinito di Spring. Com’è possibile che un tipo predefinito possa implementare l’interfaccia [IService], che è specifica della nostra applicazione?

Analizziamo innanzitutto la classe [TransactionProxyFactoryBean]:

Image

Notiamo che implementa l’interfaccia [FactoryBean]. Abbiamo già incontrato questa interfaccia. Sappiamo che quando un’applicazione richiede a Spring un’istanza di un tipo che implementi [FactoryBean], Spring non restituisce un’istanza [I] di quel tipo, ma l’oggetto restituito dal metodo [I].getObject():

Image

Nel nostro caso, il livello [service] verrà implementato dall’oggetto restituito da [TransactionProxyFactoryBean].getObject(). Qual è la natura di questo oggetto? Non entreremo nei dettagli perché sono complessi. Rientrano in ciò che viene chiamato Spring AOP (Aspect Oriented Programming). Cercheremo di chiarire le cose con alcuni semplici schemi. AOP consente quanto segue:

  • abbiamo due classi, C1 e C2, dove C1 utilizza l’interfaccia [I2] definita da C2:
  • grazie a AOP, è possibile inserire, in modo trasparente per entrambe le classi, un intercettatore tra le classi C1 e C2:

La classe [C1] è stata compilata per funzionare con l'interfaccia [I2] implementata da [C2]. Al momento dell’esecuzione, AOP inserisce la classe [intercepteur] tra [C1] e [C2]. Affinché ciò sia possibile, è ovviamente necessario che la classe [intercepteur] presenti a [C1] la stessa interfaccia [I2] di [C2].

A cosa può servire? La documentazione di Spring fornisce alcuni esempi. Si potrebbe voler, ad esempio, generare dei log in occasione delle chiamate a un particolare metodo M di [C2], per effettuare un audit di tale metodo. In [intercepteur], si scriverà quindi un metodo [M] che esegue tali registrazioni. La chiamata da [C1] a [C2].M avverrà in questo modo (cfr. schema sopra):

  1. [C1] chiama il metodo M di [C2]. In realtà verrà chiamato il metodo M di [intercepteur]. Ciò è possibile se [C1] si rivolge a un’interfaccia [I2] anziché a una specifica implementazione di [I2]. È quindi sufficiente che [intercepteur] implementi [I2].
  2. Il metodo M di [intercepteur] genera i log e chiama il metodo M di [C2], inizialmente previsto da [C1].
  3. Il metodo M di [C2] viene eseguito e restituisce il proprio risultato al metodo M di [intercepteur], che può eventualmente aggiungere qualcosa a quanto fatto al punto 2.
  4. il metodo M di [intercepteur] restituisce un risultato al metodo chiamante di [C1]

Si nota che il metodo M di [intercepteur] può eseguire operazioni prima e dopo la chiamata al metodo M di [C2]. Rispetto a [C1], essa arricchisce quindi il metodo M di [C2]. Si può quindi considerare la tecnologia AOP come un modo per arricchire l’interfaccia presentata da una classe.

Come si applica questo concetto al nostro livello [service]? Se implementiamo il livello [service] direttamente con un’istanza [ServiceImpl], la nostra applicazione web avrà la seguente architettura:

Se si implementa il livello [service] con un'istanza [TransactionProxyFactoryBean], si otterrà la seguente architettura:

Si può dire che il livello [service] sia istanziato con due oggetti:

  • l’oggetto che sopra chiamiamo [proxy transactionnel] e che è in realtà l’oggetto restituito dal metodo [getObject] di [TransactionProxyFactoryBean]. È questo oggetto che fungerà da interfaccia tra il livello [service] e il livello [web]. Esso implementa per definizione l’interfaccia [IService].
  • un'istanza [ServiceImpl] che a sua volta implementa l'interfaccia [IService]. Solo essa sa come interagire con il livello [dao], pertanto è necessaria.

Immaginiamo che il livello [web] chiami il metodo [saveMany] dell’interfaccia [IService]. Sappiamo che, dal punto di vista funzionale, le operazioni di inserimento/aggiornamento effettuate da questo metodo devono avvenire all’interno di una transazione. O vanno a buon fine tutte, oppure nessuna viene eseguita. Abbiamo presentato il metodo [saveMany] della classe [ServiceImpl] e abbiamo sottolineato che esso non prevedeva il concetto di transazione. Il metodo [saveMany] della classe [proxy transactionnel] arricchirà il metodo [saveMany] della classe [ServiceImpl] con questo concetto di transazione. Seguiamo lo schema riportato sopra:

  1. il livello [web] chiama il metodo [saveMany] dell’interfaccia [IService].
  2. Viene eseguito il metodo [saveMany] di [proxy transactionnel]. Esso avvia una transazione. Per farlo, deve disporre di informazioni sufficienti, in particolare di un oggetto [DataSource] per ottenere una connessione a SGBD. Successivamente, chiama il metodo [saveMany] di [ServiceImpl].
  3. Quest’ultimo viene eseguito. Esso richiama ripetutamente il livello [dao] per eseguire gli inserimenti o gli aggiornamenti. I comandi SQL eseguiti in questa occasione vengono eseguiti all’interno della transazione avviata al punto 2.
  4. Supponiamo che una di queste operazioni fallisca. Il livello [dao] farà risalire un'eccezione al livello [service], ovvero al metodo [saveMany] dell'istanza [ServiceImpl].
  5. Quest’ultima non esegue alcuna operazione e lascia che l’eccezione risalga fino al metodo [saveMany] di [proxy transactionnel].
  6. Al ricevimento dell’eccezione, il metodo [saveMany] di [proxy transactionnel], che è il proprietario della transazione, esegue un [rollback] su di essa per annullare tutti gli aggiornamenti, poi lascia che l’eccezione risalga fino al livello [web], che si occuperà di gestirla.

Nella fase 4, abbiamo ipotizzato che uno degli inserimenti o degli aggiornamenti non andasse a buon fine. Se così non fosse, in [5] non verrebbe segnalata alcuna eccezione. Lo stesso vale per [6]. In questo caso, il metodo [saveMany] di [proxy transactionnel] esegue un [commit] della transazione per convalidare tutti gli aggiornamenti.

Ora abbiamo un’idea più precisa dell’architettura implementata dal bean [TransactionProxyFactoryBean]. Torniamo alla sua configurazione:


    <!-- gestore delle transazioni -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!-- classe di accesso al livello [service] -->
    <bean id="service" 
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager">
            <ref local="transactionManager"/>
        </property>
        <property name="target">
            <bean class="istia.st.mvc.personnes.service.ServiceImpl">
                <property name="dao">
                    <ref local="dao"/>
                </property>
            </bean>
        </property>
        <property name="transactionAttributes">
            <props>
                <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
                <prop key="save*">PROPAGATION_REQUIRED</prop>
                <prop key="delete*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>

Esaminiamo questa configurazione alla luce dell’architettura configurata:

  • [proxy transactionnel] gestirà le transazioni. Spring offre diverse strategie per la loro gestione. [proxy transactionnel] necessita di un riferimento al gestore di transazioni scelto.
  • righe 11–13: definiscono l’attributo [transactionManager] del bean [TransactionProxyFactoryBean] con un riferimento a un gestore di transazioni. Quest’ultimo è definito nelle righe 2–7.
  • righe 2-7: il gestore delle transazioni è di tipo [DataSourceTransactionManager]:

Image

[DataSourceTransactionManager] è un gestore di transazioni adatto ai SGBD a cui si accede tramite un oggetto [DataSource]. È in grado di gestire solo transazioni su un unico SGBD. Non è in grado di gestire transazioni distribuite su più SGBD. In questo caso, abbiamo un solo SGBD. Pertanto, questo gestore di transazioni è adeguato. Quando [proxy transactionnel] avvierà una transazione, lo farà su una connessione associata al thread. È questa connessione che verrà utilizzata in tutti i livelli che conducono al database: [ServiceImpl, DaoImplCommon, SqlMapClientTemplate, JDBC].

La classe [DataSourceTransactionManager] deve conoscere la fonte dati alla quale deve richiedere una connessione per associarla al thread. Questa è definita alle righe 4-6: si tratta della stessa fonte dati utilizzata dal livello [dao] (cfr. paragrafo 17.5.2).

  • righe 14-19: l’attributo "target" indica la classe che deve essere intercettata, in questo caso la classe [ServiceImpl]. Questa informazione è necessaria per due motivi:
    • la classe [ServiceImpl] deve essere istanziata poiché è essa a garantire il dialogo con il livello [dao]
    • [TransactionProxyFactoryBean] deve generare un proxy che presenti al livello [web] la stessa interfaccia di [ServiceImpl].
  • righe 21-27: indicano quali metodi di [ServiceImpl] il proxy deve intercettare. L’attributo [transactionAttributes], riga 21, indica quali metodi di [ServiceImpl] richiedono una transazione e quali sono gli attributi di quest’ultima:
  • riga 23: i metodi il cui nome inizia con get [getOne, getAll] vengono eseguiti in una transazione con l’attributo [PROPAGATION_REQUIRED,readOnly]:
    • PROPAGATION_REQUIRED: il metodo viene eseguito all’interno di una transazione se ne esiste già una associata al thread; in caso contrario, ne viene creata una nuova e il metodo viene eseguito al suo interno.
    • readOnly: transazione in sola lettura

In questo caso, i metodi [getOne] e [getAll] di [ServiceImpl] verranno eseguiti all’interno di una transazione, mentre in realtà non è necessario. Si tratta in entrambi i casi di un’operazione costituita da un unico comando SELECT. Non si vede l’utilità di inserire questo SELECT in una transazione.

  • riga 24: i metodi il cui nome inizia con «save», [saveOne, saveMany], vengono eseguiti in una transazione con l’attributo [PROPAGATION_REQUIRED].
  • riga 25: i metodi [deleteOne] e [deleteMany] di [ServiceImpl] sono configurati in modo identico ai metodi [saveOne, saveMany].

Nel nostro livello [service], solo i metodi [saveMany] e [deleteMany] devono essere eseguiti in una transazione. La configurazione avrebbe potuto essere ridotta alle seguenti righe:


        <property name="transactionAttributes">
            <props>
                <prop key="saveMany">PROPAGATION_REQUIRED</prop>
                <prop key="deleteMany">PROPAGATION_REQUIRED</prop>
            </props>
</property>

17.6. Test del livello [service]

Ora che abbiamo scritto e configurato il livello [service], intendiamo testarlo con i test JUnit:

Image

Il file di configurazione [spring-config-test-service-firebird.xml] del livello [service] è quello descritto al paragrafo 17.5.2.

Il test JUnit [TestServiceFirebird] è il seguente:

package istia.st.mvc.personnes.tests;

...

public class TestServiceFirebird extends TestCase {

     // livello [service]
    private IService service;

    public IService getService() {
        return service;
    }

    public void setService(IService service) {
        this.service = service;
    }

     // configurazione
    public void setUp() {
        service = (IService) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-service-firebird.xml"))).getBean("service");
    }

     // elenco delle persone
    private void doListe(Collection personnes) {
...
    }

     // test1
    public void test1() throws ParseException {
...
    }

     // modifica-eliminazione di un elemento inesistente
    public void test2() throws ParseException {
...
    }

     // gestione delle versioni delle persone
    public void test3() throws ParseException, InterruptedException {
...
    }

     // blocco ottimistico - accesso multithread
    public void test4() throws Exception {
...
    }

     // test di validità di saveOne
    public void test5() throws ParseException {
...
    }

         // inserimenti multithread
    public void test6() throws ParseException, InterruptedException{
...
    }

     // test del metodo deleteMany
    public void test7() throws ParseException {
         // elenco attuale
        Collection personnes = service.getAll();
        int nbPersonnes1 = personnes.size();
         // visualizzazione
        doListe(personnes);
         // creazione di tre persone
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        Personne p2 = new Personne(-1, "Y", "Y", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/03/2006"), false, 0);
        Personne p3 = new Personne(-2, "Z", "Z", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/04/2006"), true, 2);
         // aggiunta delle 3 persone - la persona p3 con l'ID  -2 provocerà
         // un'eccezione
        boolean erreur = false;
        try {
            service.saveMany(new Personne[] { p1, p2, p3 });
        } catch (Exception ex) {
            erreur = true;
            System.out.println(ex.toString());
        }
         // verifica
        assertTrue(erreur);
         // nuovo elenco - il numero di elementi non dovrebbe essere cambiato
         // a causa del rollback automatico della transazione
        int nbPersonnes2 = service.getAll().size();
        assertEquals(nbPersonnes1, nbPersonnes2);
         // aggiunta delle due persone valide
         // si reimposta il loro ID a -1
        p1.setId(-1);
        p2.setId(-1);
        service.saveMany(new Personne[] { p1, p2 });
         // si recuperano i loro ID
        int id1 = p1.getId();
        int id2 = p2.getId();
         // verifiche
        p1 = service.getOne(id1);
        assertEquals(p1.getNom(), "X");
        p2 = service.getOne(id2);
        assertEquals(p2.getNom(), "Y");
         // nuovo elenco - devono esserci 2 elementi in più
        int nbPersonnes3 = service.getAll().size();
        assertEquals(nbPersonnes1 + 2, nbPersonnes3);
         // eliminazione di p1 e p2 e di una persona inesistente
         // deve verificarsi un'eccezione
        erreur = false;
        try {
            service.deleteMany(new int[] { id1, id2, -1 });
        } catch (Exception ex) {
            erreur = true;
            System.out.println(ex.toString());
        }
         // verifica
        assertTrue(erreur);
         // nuovo elenco
        personnes = service.getAll();
        int nbPersonnes4 = personnes.size();
         // non è stata eliminata alcuna persona (rollback
         // automatico della transazione)
        assertEquals(nbPersonnes4, nbPersonnes3);
         // si eliminano le due persone valide
        service.deleteMany(new int[] { id1, id2 });
         // verifiche
         // persona p1
        erreur = false;
        int codeErreur = 0;
        try {
            p1 = service.getOne(id1);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // deve esserci un errore con codice 2
        assertTrue(erreur);
        assertEquals(2, codeErreur);
         // persona p2
        erreur = false;
        codeErreur = 0;
        try {
            p1 = service.getOne(id2);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // dovrebbe esserci un errore di codice 2
        assertTrue(erreur);
        assertEquals(2, codeErreur);
         // nuovo elenco
        personnes = service.getAll();
        int nbPersonnes5 = personnes.size();
         // verifica - dovremmo essere tornati al punto di partenza
        assertEquals(nbPersonnes5, nbPersonnes1);
         // visualizzazione
        doListe(personnes);
    }

}
  • righe 19-22: il programma verifica i livelli [dao] e [service] configurati dal file [spring-config-test-service-firebird.xml], quello esaminato nella sezione precedente.
  • I test da [test1] a [test6] sono identici nella loro struttura alle controparti con lo stesso nome nella classe di test [TestDaoFirebird] del livello [dao]. L’unica differenza è che, per impostazione predefinita, i metodi [saveOne] e [deleteOne] vengono ora eseguiti all’interno di una transazione.
  • Il metodo [test7] ha lo scopo di testare i metodi [saveMany] e [deleteMany]. Si vuole verificare che vengano effettivamente eseguiti all’interno di una transazione. Commentiamo il codice di questo metodo:
  • righe 62-63: si conta il numero di persone [nbPersonnes1] attualmente presenti nell’elenco
  • righe 67-72: si creano tre persone
  • righe 73-83: questi tre soggetti vengono salvati tramite il metodo [saveMany] – riga 77. I primi due soggetti, p1 e p2, con un ID pari a -1, verranno aggiunti alla tabella [PERSONNES]. La persona p3 ha invece un ID pari a -2. Non si tratta quindi di un inserimento, ma di un aggiornamento. Quest’ultimo fallirà poiché nella tabella [PERSONNES] non è presente alcuna persona con un ID pari a -2. Il livello [dao] genererà quindi un'eccezione che verrà propagata fino al livello [service]. L'esistenza di questa eccezione viene verificata alla riga 83.
  • A causa dell’eccezione precedente, il livello [service] dovrebbe eseguire un [rollback] su tutti gli ordini SQL emessi durante l’esecuzione del metodo [saveMany], questo perché tale metodo viene eseguito all’interno di una transazione. Righe 86-87: si verifica che il numero di persone nell’elenco non sia cambiato e che quindi gli inserimenti di p1 e p2 non abbiano avuto luogo.
  • righe 88-103: si aggiungono solo le persone p1 e p2 e si verifica che, di conseguenza, nell’elenco siano presenti due persone in più.
  • righe 106-114: si elimina un gruppo di persone costituito dalle persone p1 e p2 appena aggiunte e da una persona inesistente (id = -1). A tal fine viene utilizzato il metodo [deleteMany], riga 108. Questo metodo fallirà poiché nella tabella [PERSONNES] non è presente alcuna persona con id pari a –1. Il livello [dao] genererà quindi un'eccezione che verrà propagata fino al livello [service]. La presenza di questa eccezione viene verificata alla riga 114.
  • A causa dell’eccezione precedente, il livello [service] dovrebbe eseguire un [rollback] su tutti gli ordini SQL emessi durante l’esecuzione del metodo [deleteMany], questo perché tale metodo viene eseguito all’interno di una transazione. Righe 116-117: si verifica che il numero di persone nell’elenco non sia cambiato e che quindi le eliminazioni di p1 e p2 non abbiano avuto luogo.
  • riga 122: si elimina un gruppo costituito esclusivamente dalle persone p1 e p2. L'operazione dovrebbe andare a buon fine. Il resto del metodo verifica che sia effettivamente così.

L’esecuzione dei test fornisce i seguenti risultati:

Image

I sette test sono stati superati con successo. Considereremo il nostro livello [service] operativo.

17.7. Il livello [web]

Ricordiamo l’architettura generale dell’applicazione web da realizzare:

Abbiamo appena realizzato i livelli [dao] e [service] che consentono di lavorare con un database Firebird. Abbiamo scritto una versione 1 di questa applicazione in cui i livelli [dao] e [service] operavano con un elenco di persone in memoria. Il livello [web] scritto in quell’occasione rimane valido. Infatti, era destinato a un livello [service] che implementava l’interfaccia [IService]. Poiché il nuovo livello [service] implementa la stessa interfaccia, il livello [web] non deve essere modificato.

Nell’articolo precedente, la versione 1 dell’applicazione era stata testata con il progetto Eclipse [mvc-personnes-02B], in cui i livelli [web, service, dao, entites] erano stati inseriti in archivi .jar:

La cartella [src] era vuota. Le classi dei livelli si trovavano negli archivi [personnes-*.jar ]:

Per testare la versione 2, in Eclipse duplichiamo la cartella Eclipse [mvc-personnes-02B] in [mvc-personnes-03B] (copia/incolla):

Image

Nel progetto [mvc-personnes-03], esportiamo i livelli [dao] e [service] rispettivamente negli archivi [personnes-dao.jar] e [personnes-service.jar] della cartella [dist] del progetto:

Image

Copiamo questi due file, quindi in Eclipse li incolliamo nella cartella [WEB-INF/lib] del progetto [mvc-personnes-03B], dove sostituiranno gli archivi con lo stesso nome della versione precedente.

Copiamo e incolliamo anche gli archivi [commons-dbcp-*.jar, commons-pool-*.jar, firebirdsql-full.jar, ibatis-common-2.jar, ibatis-sqlmap-2.jar] dalla cartella [lib] del progetto [mvc-personnes-03] nella cartella [WEB-INF/lib] del progetto [mvc-personnes-03B]. Questi archivi sono necessari per i nuovi livelli [dao] e [service].

Fatto ciò, includiamo i nuovi archivi nel Classpath del progetto: [clic droit sur projet -> Properties -> Java Build Path -> Add Jars].

La cartella [src] contiene i file di configurazione dei livelli [dao] e [service]:

Image

Il file [spring-config.xml] configura i livelli [dao] e [service] dell’applicazione web. Nella nuova versione, è identico al file [spring-config-test-service-firebird.xml] che è stato utilizzato per configurare il test del livello di servizio nel progetto [mvc-personnes-03]. Si esegue quindi un copia/incolla da uno all’altro:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- la fonte dei dati DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <property name="url">
            <!-- attenzione: non lasciare spazi tra i due tag <value> -->
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- le classi di accesso al livello [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
    <!-- gestore delle transazioni -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!-- le classi di accesso al livello [service] -->
    <bean id="service" 
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager">
            <ref local="transactionManager"/>
        </property>
        <property name="target">
            <bean class="istia.st.mvc.personnes.service.ServiceImpl">
                <property name="dao">
                    <ref local="dao"/>
                </property>
            </bean>
        </property>
        <property name="transactionAttributes">
            <props>
                <prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
                <prop key="save*">PROPAGATION_REQUIRED</prop>
                <prop key="delete*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>
</beans>
  • riga 12: l’URL del database Firebird. Continuiamo a utilizzare il database utilizzato per i test dei livelli [dao] e [service]

Distribuiamo il progetto web [mvc-personnes-03B] su Tomcat:

Siamo pronti per i test . Viene avviato Firebird SGBD. Il contenuto della tabella [PERSONNES] è quindi il seguente:

Image

A sua volta viene avviato Tomcat. Con un browser, richiediamo l'URL [http://localhost:8080/mvc-personnes-03B]:

Image

Aggiungiamo una nuova persona tramite il link [Ajout]:

Verifichiamo l’aggiunta nel database:

Image

L'utente è invitato a eseguire ulteriori test [modification, suppression].

Eseguiamo ora il test sui conflitti di versione che era stato effettuato nella versione 1. [Firefox] sarà il browser dell’utente U1. Quest’ultimo richiede l’URL [http://localhost:8080/mvc-personnes-03B]:

Image

[IE] sarà il browser dell’utente U2. Quest’ultimo richiede lo stesso URL:

Image

L’utente U1 accede alla modifica della persona [Perrichon]:

Image

L’utente U2 fa lo stesso:

Image

L'utente U1 apporta delle modifiche e conferma:

Anche l’utente U2 fa lo stesso:

L'utente U2 torna all'elenco delle persone tramite il link [Annuler] del modulo:

Image

Trova la persona [Perrichon] così come è stata modificata da U1 (nome convertito in maiuscolo).

E il database in tutto questo? Vediamo:

Image

Il nome della persona n. 899 è effettivamente in maiuscolo a seguito della modifica apportata da U1.

17.8. Conclusion

Ricordiamo cosa volevamo fare. Avevamo un’applicazione web con la seguente architettura a tre livelli:

in cui i livelli [dao] e [service] operavano con un elenco di dati in memoria che andava quindi perso all’arresto del server web. Questa era la versione 1. Nella versione 2, i livelli [service] e [dao] sono stati riscritti in modo che l’elenco delle persone fosse contenuto in una tabella del database. Ora è quindi persistente. Ora ci proponiamo di vedere quale impatto abbia sulla nostra applicazione la modifica apportata a SGBD. A tal fine, realizzeremo tre nuove versioni della nostra applicazione web:

  • versione 3: SGBD utilizza Postgres
  • versione 4: SGBD è MySQL
  • versione 5: SGBD è SQL Server Express 2005

Le modifiche vengono apportate nei seguenti punti:

  • la classe [DaoImplFirebird] implementa le funzionalità del livello [dao] relative a SGBD Firebird. Se tale esigenza dovesse persistere, essa verrà sostituita rispettivamente dalle classi [DaoImplPostgres], [DaoImplMySQL] e [DaoImplSqlExpress].
  • Il file di mappatura [personnes-firebird.xml] di iBATIS per SGBD Firebird verrà sostituito rispettivamente dai file di mappatura [personnes-postgres.xml], [personnes-mysql.xml] e [personnes-sqlexpress.xml].
  • La configurazione dell’oggetto [DataSource] del livello [dao] è specifica per un SGBD. Cambierà quindi ad ogni versione.
  • Anche il driver JDBC del SGBD cambia ad ogni versione

A parte questi aspetti, tutto il resto rimane invariato. Di seguito descriviamo queste nuove versioni concentrandoci esclusivamente sulle novità introdotte da ciascuna di esse.