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, memorizzeremo l'elenco delle persone in una tabella del database Firebird. Le informazioni sull'installazione e la gestione di questo DBMS sono disponibili nel documento [http://tahe.developpez.com/divers/sql-firebird/]. Le schermate riportate di seguito provengono da IBExpert, un client di amministrazione per i DBMS 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 utilizzando le seguenti istruzioni 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], progettata per memorizzare oggetti di tipo [Person], riflette la struttura di tale oggetto. Poiché il tipo booleano non esiste in Firebird, il campo [MARRIED] (riga 8) è stato dichiarato come tipo [SMALLINT], un numero intero. Il suo valore sarà 0 (non sposato) o 1 (sposato).
  • Righe 13–16: vincoli di integrità che rispecchiano quelli del validatore di dati [ValidatePerson].
  • 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 chiamato generatore denominato [GEN_PERSONNES_ID]. Questo generatore produce numeri interi sequenziali che useremo per assegnare un valore alla chiave primaria [ID] della classe [PERSONNES]. Facciamo un esempio per illustrare come funziona:

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

 

L'istruzione SQL

SELECT GEN_ID ( GEN_PERSONNES_ID,1 ) FROM RDB$DATABASE

restituisce quindi il seguente valore per il generatore [GEN_PERSONNES_ID]. GEN_ID è una funzione interna di Firebird, mentre [RDB$DATABASE] è una tabella di sistema in questo DBMS.

17.2. Il progetto Eclipse per i livelli [dao] e [service]

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

Image

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


Cartella [src]


Questa cartella contiene il codice sorgente per i livelli [dao] e [service]:

Image

Contiene vari pacchetti:

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

oltre ai file di configurazione che devono trovarsi nel ClassPath dell'applicazione.


Cartella [database]


Questa cartella contiene il database Firebird per gli utenti:

Image

  • [dbpersonnes.gdb] è il database.
  • [dbpersonnes.sql] è lo script SQL per la generazione del database:
/******************************************************************************/
/*** Generated by IBExpert 2006.03.07 27/04/2006 10: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;



/******************************************************************************/
/*** Generators ***/
/******************************************************************************/

CREATE GENERATOR GEN_PERSONNES_ID;
SET GENERATOR GEN_PERSONNES_ID TO 787;



/******************************************************************************/
/*** Tables ***/
/******************************************************************************/



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;



/* Check constraints definition */

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


/******************************************************************************/
/*** Primary Keys ***/
/******************************************************************************/

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

Cartella [lib]


Questa cartella contiene i file richiesti dall'applicazione:

Si noti la presenza del driver JDBC [firebirdsql-full.jar] per il DBMS Firebird, oltre a una serie di file [spring-*.jar]. Avremmo potuto utilizzare il singolo file [spring.jar] presente nella cartella [dist] della distribuzione, che contiene tutte le classi di Spring. Possiamo anche utilizzare solo gli archivi necessari per il progetto. È ciò che abbiamo fatto in questo caso, guidati dagli errori di classe mancante segnalati da Eclipse e dai nomi degli archivi parziali di Spring. Tutti questi archivi dalla cartella [lib] sono stati inseriti nel 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. Componenti del livello [dao]

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

Image

  • [IDao] è l'interfaccia fornita dal livello [dao]
  • [DaoImplCommon] è un'implementazione di questa interfaccia in cui il gruppo di persone è memorizzato in una tabella del database. [DaoImplCommon] raggruppa funzionalità indipendenti dal DBMS.
  • [DaoImplFirebird] è una classe derivata da [DaoImplCommon] per gestire specificamente un database Firebird.
  • [DaoException] è il tipo di eccezioni non gestite generate dal livello [dao]. Questa classe è presente dalla 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 {
    // list of all persons
    Collection getAll();
    // find a specific person
    Personne getOne(int id);
    // add/modify a person
    void saveOne(Personne personne);
    // delete a person
    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 {

    // list of persons
    public Collection getAll() {
...
    }

    // get a specific person
    public Personne getOne(int id) {
...
    }

    // deleting a person
    public void deleteOne(int id) {
...
    }

    // add or modify a person
    public void saveOne(Personne personne) {
        // is the person parameter valid?
        check(personne);
        // addition or modification?
        if (personne.getId() == -1) {
            // add
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }

    // add a person
    protected void insertPersonne(Personne personne) {
...
    }

    // edit a person
    protected void updatePersonne(Personne personne) {
...
    }

    // person validity check
    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, [insertPerson] e [updatePerson], a seconda che sia necessario aggiungere o modificare una persona.
  • riga 50: il metodo privato [check] è lo stesso della versione precedente. Non lo riprenderemo qui.
  • Riga 8: Per implementare l'interfaccia [IDao], la classe [DaoImpl] estende la 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 colloca tra il livello [DAO] dell'applicazione e il driver JDBC del database. Esistono alternative a [iBATIS], come [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], ovvero i segmenti di codice presenti in tutti i livelli [DAO] che utilizzano lo strumento [iBATIS]. Per scrivere la parte non generica del codice, ovvero il codice specifico del livello [DAO] che stiamo scrivendo, è sufficiente derivare la classe [SqlMapClientDaoSupport]. È proprio quello che stiamo facendo qui.

La classe [SqlMapClientDaoSupport] è definita come segue:

Image

Tra i metodi di questa classe, uno ci permette di configurare il client [iBATIS] con cui gestiremo il database:

Image

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

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

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

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

Per funzionare, la classe [SqlMapClientDaoSupport] richiede un riferimento all'oggetto iBATIS [SqlMapClient sqlMapClient], che gestirà la comunicazione con il database. Questo oggetto richiede due cose per funzionare:

  • un oggetto [DataSource] connesso al database da cui richiederà le connessioni
  • uno (o più) file di configurazione in cui sono esternalizzate le istruzioni SQL da eseguire. Infatti, queste non sono nel codice Java. Sono identificate da un codice in un file di configurazione e l'oggetto [SqlMapClient sqlMapClient] utilizza questo codice per eseguire una specifica istruzione SQL.

Una configurazione preliminare del nostro livello [dao] che rifletta l'architettura sopra descritta sarebbe la seguente:


    <!-- the [dao] layer access classes -->
    <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 dispone di tale metodo; è la sua classe padre [SqlMapClientDaoSupport] a disporne. Quindi è in realtà questa classe che viene inizializzata qui.

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

Image

[SqlMapClient] è un'interfaccia. Spring fornisce la classe [SqlMapClientFactoryBean] per ottenere un oggetto che implementi questa interfaccia:

Image

Ricordiamo che stiamo cercando di istanziare un oggetto che implementi l'interfaccia [SqlMapClient]. Questo non sembra essere il caso della classe [SqlMapClientFactoryBean]. Questa classe implementa l'interfaccia [FactoryBean] (vedi sopra). Dispone del seguente metodo [getObject()]:

Image

Quando a Spring viene richiesta un'istanza di un oggetto che implementa l'interfaccia [FactoryBean], essa:

  • 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à un oggetto che implementa l'interfaccia [SqlMapClient].

Per restituire un oggetto che implementa l'interfaccia [SqlMapClient], la classe [SqlMapClientFactoryBean] necessita di due informazioni richieste per quell'oggetto:

  • un oggetto [DataSource] connesso al database da cui richiederà le connessioni
  • uno (o più) file di configurazione in cui sono memorizzate le istruzioni SQL da eseguire

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

Image

Stiamo facendo progressi... Il nostro file di configurazione sta prendendo forma 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>
    <!-- the [dao] layer access classes -->
    <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 chiediamo a Spring un'istanza di questo bean, otteniamo un oggetto che implementa l'interfaccia iBATIS [SqlMapClient]. È quindi questo oggetto che verrà ottenuto alla riga 14.
  • righe 7-9: specifichiamo che il file di configurazione richiesto dall'oggetto iBATIS [SqlMapClient] si chiama "sql-map-config-firebird.xml" e che deve trovarsi nel ClassPath dell'applicazione. Qui viene utilizzato il metodo [SqlMapClientFactoryBean].setConfigLocation.
  • Righe 4–6: inizializziamo la proprietà [dataSource] di [SqlMapClientFactoryBean] utilizzando il suo metodo [setDataSource].

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

Image

Ancora una volta, abbiamo a che fare con un'interfaccia per la quale dobbiamo trovare una classe di implementazione. Il ruolo di tale classe è quello di fornire in modo efficiente a un'applicazione le connessioni a un database specifico. Un DBMS non può mantenere aperte contemporaneamente un gran numero di connessioni. Per ridurre il numero di connessioni aperte in un dato momento, per ogni interazione con il database, dobbiamo:

  • aprire una connessione
  • avviare una transazione
  • eseguire istruzioni SQL
  • chiudere la transazione
  • chiudere la connessione

L'apertura e la chiusura ripetute delle connessioni richiedono molto tempo. Per risolvere questi due problemi — limitare il numero di connessioni aperte in un dato momento e ridurre il sovraccarico derivante dalla loro apertura e chiusura — le classi che implementano l'interfaccia [DataSource] spesso procedono come segue:

  • Al momento dell'istanziazione, aprono N connessioni al database di destinazione. N ha generalmente un valore predefinito e di solito può essere definito in un file di configurazione. Queste N connessioni rimangono aperte in ogni momento e formano un pool di connessioni disponibili per i thread dell'applicazione.
  • Quando un thread dell'applicazione richiede una connessione, l'oggetto [DataSource] gli fornisce una delle N connessioni aperte all'avvio, se ne rimangono disponibili. Quando l'applicazione chiude la connessione, questa non viene effettivamente chiusa, ma semplicemente restituita al pool di connessioni disponibili.

Esistono varie implementazioni disponibili gratuitamente dell'interfaccia [DataSource]. Qui useremo 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 di [DataSource] di cui abbiamo bisogno:

Image

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

  1. il nome del driver JDBC da utilizzare – inizializzato con [setDriverClassName]
  2. l'URL del database da utilizzare – inizializzato con [setUrl]
  3. il nome utente dell'utente titolare della connessione – inizializzato con [setUsername] (non setUserName come ci si potrebbe aspettare)
  4. la relativa password – inizializzata con [setPassword]

Il file di configurazione per il nostro livello [dao] potrebbe apparire così:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- data source DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- warning: do not leave spaces between the two <value> tags in the 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>
    <!-- the [dao] layer access classes -->
    <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 per il DBMS Firebird
  • righe 11-13: l'URL del database Firebird [dbpersonnes.gdb]. Presta molta attenzione a come è scritto. 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 per le distribuzioni Firebird
  • Righe 17–19: la relativa password [masterkey] — anch’essa valore predefinito

Abbiamo fatto progressi significativi, ma ci sono ancora alcuni punti di configurazione da chiarire: la riga 28 fa riferimento al file [sql-map-config-firebird.xml], che deve configurare l'iBATIS [SqlMapClient]. Prima di esaminarne il contenuto, mostriamo la posizione di questi file di configurazione nel nostro progetto Eclipse:

Image

  • [spring-config-test-dao-firebird.xml] è il file di configurazione per il 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 sopra menzionati si trovano nella cartella [src]. In Eclipse, ciò significa che in fase di esecuzione saranno presenti nella cartella [bin] del progetto (non mostrata sopra). Questa cartella fa parte del ClassPath dell'applicazione. In definitiva, i tre file sopra menzionati 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> viene utilizzato per specificare i file contenenti le istruzioni SQL da eseguire. Spesso, ma non necessariamente, c'è un file per ogni tabella. Ciò consente di raggruppare in un unico file le istruzioni SQL relative a una determinata tabella. Tuttavia, sono comuni le istruzioni SQL che coinvolgono più tabelle. In tali casi, la struttura precedente non è applicabile. È semplicemente importante ricordare che tutti i file designati dai tag <sqlMap> verranno uniti. Questi file vengono cercati nel ClassPath dell'applicazione.

Il file [personnes-firebird.xml] descrive le istruzioni SQL che verranno eseguite sulla tabella [PERSONNES] nel 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 class [Person] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- mapping table [PERSONNES] - object [Person] -->
    <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>
    <!-- list of all persons -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
    <!-- get a specific person -->
        <select id="Personne.getOne" resultMap="Personne.map" >select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES WHERE ID=#value#</select>
    <!-- add a person -->
    <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#, #version#, #nom#, #prenom#, #dateNaissance#, #marie#, 
        #nbEnfants#) </insert>
    <!-- update a person -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#version#+1, NOM=#nom#, PRENOM=#prenom#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE ID=#id# and 
        VERSION=#version#</update>
    <!-- delete a person -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#value# </delete>
</sqlMap>
  • Il file deve avere <sqlMap> come tag radice (righe 7 e 45)
  • Righe 9–10: Per facilitare la scrittura del file, assegniamo l'alias [Person.class] alla classe [istia.st.springmvc.personnes.entites.Person].
  • Righe 12-21: definiscono le mappature tra le colonne della tabella [PERSONNES] e i campi dell'oggetto [Personne].
  • Righe 23–24: L'istruzione SQL [SELECT] per recuperare tutte le persone dalla tabella [PERSONNES]
  • righe 26–27: l’istruzione SQL [select] per recuperare una persona specifica dalla tabella [PERSONNES]
  • Righe 29–36: l'istruzione SQL [insert] che inserisce una persona nella tabella [PERSONS]
  • righe 38-41: l'istruzione SQL [update] che aggiorna una persona nella tabella [PERSONS]
  • righe 42-44: il comando SQL [delete] che elimina una persona dalla tabella [PERSONES]

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

17.3.3. La classe [DaoImplCommon]

Rivediamo l'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 {

    // list of persons
    public Collection getAll() {
...
    }

    // get a specific person
    public Personne getOne(int id) {
...
    }

    // deleting a person
    public void deleteOne(int id) {
...
    }

    // add or modify a person
    public void saveOne(Personne personne) {
        // is the person parameter valid?
        check(personne);
        // addition or modification?
        if (personne.getId() == -1) {
            // add
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }

    // add a person
    protected void insertPersonne(Personne personne) {
...
    }

    // edit a person
    protected void updatePersonne(Personne personne) {
...
    }

    // person validity check
    private void check(Personne p) {
...
    }

...
}

Esamineremo i metodi uno per uno.


getAll


Questo metodo recupera tutte le persone presenti nell'elenco. Il codice è il seguente:

1
2
3
4
    // list of persons
    public Collection getAll() {
        return getSqlMapClientTemplate().queryForList("Personne.getAll", null);
}

Innanzitutto, ricordiamo che la classe [DaoImplCommon] deriva dalla classe [SqlMapClientDaoSupport] di Spring. È proprio questa classe a fornire 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]. È attraverso questo che accederemo al database. Il tipo SqlMapClient di [iBATIS] potrebbe essere utilizzato direttamente poiché la classe [SqlMapClientDaoSupport] vi ha accesso:

Image

Lo svantaggio della classe [iBATIS] SqlMapClient è che genera eccezioni [SQLException], un tipo di eccezione controllata, ovvero un'eccezione che deve essere gestita da un blocco try/catch o dichiarata nella firma dei metodi che la generano. Tuttavia, ricordiamo che il livello [dao] implementa un'interfaccia [IDao] i cui metodi non includono eccezioni nelle loro firme. Pertanto, nemmeno i metodi delle classi che implementano l'interfaccia [IDao] possono avere eccezioni nelle loro firme. Dobbiamo quindi intercettare ogni [SQLException] generata dal livello [iBATIS] e incapsularla in un'eccezione non controllata. Il tipo [DaoException] del nostro progetto sarebbe adatto a questa incapsulazione.

Piuttosto che gestire queste eccezioni noi stessi, 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 gestita. Questo comportamento ci va bene. Dobbiamo semplicemente ricordare che il livello [dao] è ora in grado di generare due tipi di eccezioni non gestite:

  • il nostro tipo personalizzato [DaoException]
  • il tipo [DataAccessException] di Spring

Il tipo [SqlMapClientTemplate] è definito come segue:

Image

Implementa la seguente interfaccia [SqlMapClientOperations]:

Image

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

[queryForList]

Image

Questo metodo consente di eseguire un'istruzione [SELECT] e recuperare il risultato come elenco di oggetti:

  • [statementName]: l'identificatore (id) dell'istruzione [select] nel file di configurazione
  • [parameterObject]: l'oggetto "parameter" per un [SELECT] parametrizzato. L'oggetto "parameter" può assumere due forme:
    • un oggetto conforme allo standard JavaBean: i parametri dell'istruzione [SELECT] sono quindi i nomi dei campi del JavaBean. Quando l'istruzione [SELECT] viene eseguita, essi vengono sostituiti dai valori di questi campi.
    • un dizionario: i parametri dell’istruzione [select] sono quindi le chiavi del dizionario. Quando l’istruzione [select] viene eseguita, queste vengono sostituite dai valori associati nel dizionario.
  • Se [SELECT] non restituisce righe, il risultato [List] è un oggetto vuoto ma non nullo (da verificare).

[queryForObject]

Image

Questo metodo è concettualmente identico al precedente ma restituisce solo un singolo oggetto. Se [SELECT] non restituisce righe, il risultato è il puntatore null.

[insert]

Image

Questo metodo esegue un'istruzione SQL [insert] configurata dal secondo parametro. L'oggetto restituito è la chiave primaria della riga che è stata inserita. Non è obbligatorio utilizzare questo risultato.

[update]

Image

Questo metodo esegue un'istruzione SQL [update] configurata dal secondo parametro. Il risultato è il numero di righe modificate dall'istruzione SQL [update].

[delete]

Image

Questo metodo esegue un'istruzione SQL [delete] configurata dal secondo parametro. Il risultato è il numero di righe eliminate dall'istruzione SQL [delete].

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

1
2
3
4
    // list of persons
    public Collection getAll() {
        return getSqlMapClientTemplate().queryForList("Personne.getAll", null);
}
  • Riga 4: Viene eseguita l'istruzione [select] denominata "Person.getAll". Non ha parametri, quindi l'oggetto "parameter" è nullo.

Nel file [people-firebird.xml], l'istruzione [select] denominata "Person.getAll" è la 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 class [Person] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- mapping table [PERSONNES] - object [Person] -->
    <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>
    <!-- list of all persons -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
...
</sqlMap>
  • Riga 23: L'istruzione SQL "Person.getAll" non è parametrizzata (non ci sono parametri nel testo della query).
  • La riga 3 del metodo [getAll] richiede l'esecuzione della query [select] denominata "Personne.getAll". Questa query verrà eseguita. [iBATIS] si basa su JDBC. Sappiamo quindi che il risultato della query verrà restituito come oggetto [ResultSet]. Riga 23: l'attributo [resultMap] del tag <select> indica a [iBATIS] quale "resultMap" utilizzare per convertire ogni riga del [ResultSet] ottenuto in un oggetto. È il "resultMap" [Person.map] definito nelle righe 12–21 che specifica come mappare una riga della tabella [PERSONNES] a un oggetto di tipo [Person]. [iBATIS] utilizzerà queste mappature per restituire un elenco di oggetti [Person] basato sulle righe nel [ResultSet].
  • La riga 3 del metodo [getAll] restituisce quindi una collezione di oggetti [Person]
  • Il metodo [queryForList] potrebbe generare un'eccezione Spring [DataAccessException]. Lasciamo che si propaghi.

Spiegheremo gli altri metodi della classe [AbstractDaoImpl] più brevemente, poiché gli elementi essenziali dell'utilizzo di [iBATIS] sono già stati trattati nella discussione sul metodo [getAll].


getOne


Questo metodo recupera una persona identificata dal proprio [id]. Il codice è il seguente:

        // get a specific person
    public Personne getOne(int id) {
        // it is retrieved from the BD
        Personne personne = (Personne) getSqlMapClientTemplate()
                .queryForObject("Personne.getOne", new Integer(id));
        // did we recover anything?
        if (personne == null) {
            // throw an exception
            throw new DaoException(
                    "La personne d'id [" + id + "] n'existe pas", 2);
        }
        // we return the person
        return personne;
    }
  • riga 4: richiede l'esecuzione dell'istruzione [select] denominata "Person.getOne". Nel file [people-firebird.xml] corrisponde a quanto segue:

<!-- get a specific person -->
        <select id="Personne.getOne" resultMap="Personne.map" parameterClass="int">
            select ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM 
            PERSONNES WHERE ID=#value#</select>

La query SQL è configurata dal parametro #value# (riga 4). L'attributo #value# specifica il valore del parametro passato alla query SQL quando tale parametro è di tipo semplice: Integer, Double, String, ecc. Negli attributi del tag <select>, l'attributo [parameterClass] indica che il parametro è di tipo Integer (riga 2). Nella riga 5 di [getOne], vediamo che questo parametro è l'ID della persona ricercata, sotto forma di oggetto Integer. Questa conversione di tipo è obbligatoria poiché il secondo parametro di [queryForList] deve essere di tipo [Object].

Il risultato della query [select] verrà convertito in un oggetto tramite l'attributo [resultMap="Personne.map"] (riga 2). Otterremo quindi un tipo [Personne].

  • Righe 7–11: Se la query [select] non ha restituito righe, recuperiamo il puntatore null dalla riga 4. Ciò significa che la persona ricercata non è stata trovata. In questo caso, generiamo un'eccezione [DaoException] con codice 2 (righe 9–10).
  • riga 13: se non si è verificata alcuna eccezione, viene restituito l'oggetto [Person] richiesto.

deleteOne


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

    // deleting a person
    public void deleteOne(int id) {
        // we delete the person
        int n = getSqlMapClientTemplate().delete("Personne.deleteOne",
                new Integer(id));
        // have we succeeded
        if (n == 0) {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        }
    }
  • righe 4-5: richiedono l'esecuzione del comando [delete] denominato "Person.deleteOne". Nel file [people-firebird.xml] corrisponde a quanto segue:

<!-- delete a person -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#value# </delete>

Il comando SQL è configurato dal parametro #value# (riga 3) di tipo [parameterClass="int"] (riga 2). Questo sarà l'ID 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. Viene generata un'eccezione [DaoException] con codice 2 (riga 8).

saveOne


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

        // add or modify a person
    public void saveOne(Personne personne) {
        // is the person parameter valid?
        check(personne);
        // addition or modification?
        if (personne.getId() == -1) {
            // add
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }
...
  • riga 4: verifichiamo la validità della persona utilizzando il metodo [check]. Questo metodo esisteva già nella versione precedente ed era stato commentato all'epoca. Genera un'eccezione [DaoException] se la persona non è valida. Lasciamo che questa eccezione si propaghi.
  • riga 6: se arriviamo 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, vengono chiamati due metodi interni della classe:
    • insertPersonne: per l'aggiunta
    • updatePersonne: per l'aggiornamento

insertPerson


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

// add a person
    protected void insertPersonne(Personne personne) {
        // 1st version
        personne.setVersion(1);
        // wait 10 ms - for tests set true instead of false
        if (true)
            wait(10);
        // insert the new person in the BD table
        getSqlMapClientTemplate().insert("Personne.insertOne", personne);
    }
  • Riga 4: Imposta il numero di versione della persona che si sta creando su 1
  • riga 9: inserisci il record utilizzando la query denominata "Person.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#, #version#, #nom#, #prenom#, #dateNaissance#, #marie#, 
    #nbEnfants#) </insert>

Si tratta di una query parametrizzata e il parametro è di tipo [Person] (parameterClass="Person.class", riga 1). I campi dell'oggetto [Person] passato come parametro (riga 9 di insertPersonne) vengono utilizzati per popolare le colonne della riga da inserire nella tabella [PERSONS] (righe 5–8). Abbiamo un problema da risolvere. Durante un inserimento, l'oggetto [Person] da inserire ha un ID pari a -1. Questo valore deve essere sostituito con una chiave primaria valida. Per farlo, utilizziamo le righe 2–4 del tag <selectKey> sopra riportato. Esse specificano:

  • (continua)
    • la query SQL da eseguire per ottenere un valore di chiave primaria. Quella mostrata qui è quella che abbiamo presentato nella Sezione 17.1. Due punti meritano di essere notati:
      • "as 'value'" è obbligatorio. È possibile scrivere anche "as value", ma "value" è una parola chiave di Firebird che deve essere racchiusa tra virgolette.
      • La tabella Firebird si chiama in realtà [RDB$DATABASE]. Tuttavia, il carattere $ viene interpretato da [iBATIS]. È stato sfuggito raddoppiandolo.
    • Il campo dell'oggetto [Person] che deve essere inizializzato con il valore recuperato dall'istruzione [SELECT], in questo caso il campo [id]. Questo campo è specificato dall'attributo [keyProperty] alla riga 2.
  • Righe 6-7: A scopo di test, aspetteremo 10 ms prima di eseguire l'inserimento per verificare la presenza di conflitti tra thread che tentano di effettuare aggiunte simultaneamente.

updatePerson


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

// edit a person
    protected void updatePersonne(Personne personne) {
        // wait 10 ms - for tests set true instead of false
        if (true)
            wait(10);
        // change
        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. la persona da aggiornare non esiste
    2. la persona da aggiornare esiste, ma il thread che tenta di modificarla non ha la versione corretta
  • righe 7-8: viene eseguita la query SQL [update] denominata "Person.updateOne". Il codice è il seguente:

    <!-- update a person -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#version#+1, NOM=#nom#, PRENOM=#prenom#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#marie#, NBENFANTS=#nbEnfants# WHERE ID=#id# and 
VERSION=#version#</update>
  • (continua)
    • Riga 2: La query è parametrizzata e accetta un tipo [Person] come parametro (parameterClass="Person.class"). Questa è la persona da modificare (riga 8 – updatePerson).
    • Vogliamo modificare solo la persona nella tabella [PERSONS] che ha lo stesso ID e la stessa versione del parametro. Ecco perché abbiamo il vincolo [WHERE ID=#id# and VERSION=#version#]. Se questa persona viene trovata, viene aggiornata con la persona del parametro e la sua versione viene incrementata di 1 (riga 3 sopra).
  • Riga 9: Recuperiamo il numero di righe aggiornate.
  • Righe 10–11: se questo numero è zero, viene generata una [DaoException] con codice 2, indicando che la persona da aggiornare non esiste o che la sua versione è cambiata nel frattempo.

17.4. Test per il livello [dao]

17.4.1. Test dell'implementazione [DaoImplCommon]

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

Image

Prima di eseguire test approfonditi, possiamo iniziare con un semplice programma [main] che visualizzerà il contenuto della tabella [PERSONNES]. Questa è la 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");
        // current list
        Collection personnes = dao.getAll();
        // console display
        Iterator iter = personnes.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
        }
    }
}

Il file di configurazione [spring-config-test-dao-firebird.xml] per il 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>
    <!-- data source DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- warning: do not leave spaces between the two <value> tags -->
        <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>
    <!-- the [dao] layer access classes -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>

Questo file è quello descritto nella Sezione 17.3.2.

A scopo di test, viene avviato il DBMS Firebird. Il contenuto della tabella [PERSONNES] è il seguente:

Image

L'esecuzione del programma [MainTestDaoFirebird] produce il seguente output sullo schermo:

Image

Abbiamo ottenuto con successo l'elenco delle persone. Ora possiamo procedere 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 {

    // layer [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

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

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

    // list of persons
    private void doListe(Collection personnes) {
...
    }

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

    // modification-deletion of a non-existent element
    public void test2() throws ParseException {
..
    }

    // person version management
    public void test3() throws ParseException, InterruptedException {
...
    }

    // optimistic locking - multi-threaded access
    public void test4() throws Exception {
...
    }

    // validity tests for saveOne
    public void test5() throws ParseException {
....
    }

    // multi-threaded insertions
    public void test6() throws ParseException, InterruptedException{
...
}
  • I test da [test1] a [test5] sono gli stessi della versione 1, ad eccezione di [test4], che è stato leggermente modificato. Il test [test6] è nuovo. Commenteremo solo questi due test.

[test4]


[test4] ha lo scopo di testare il metodo [updatePersonne - DaoImplCommon]. Ecco il codice di quel metodo:

// edit a person
    protected void updatePersonne(Personne personne) {
        // wait 10 ms - for tests set true instead of false
        if (true)
            wait(10);
        // change
        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: attendiamo 10 ms. Questo costringe il thread che esegue [updatePerson] a cedere la CPU, il che può aumentare le nostre possibilità di osservare conflitti di accesso tra thread concorrenti.

[test4] avvia N=100 thread con il compito di incrementare simultaneamente di 1 il numero di figli della stessa persona. Vogliamo vedere come vengono gestiti i conflitti di versione e i conflitti di accesso.

    public void test4() throws Exception {
        // add a person
        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();
        // creation of N child update threads
        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();
        }
        // we wait for the end of threads
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
        // we pick up the person
        p1 = dao.getOne(id1);
        // she must have N children
        assertEquals(N, p1.getNbEnfants());
        // delete person p1
        dao.deleteOne(p1.getId());
        // check
        boolean erreur = false;
        int codeErreur = 0;
        try {
            p1 = dao.getOne(p1.getId());
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
        // we must have a code 2 error
        assertTrue(erreur);
        assertEquals(2, codeErreur);
    }

I thread vengono creati nelle righe 8–13. Ciascuno di essi incrementerà di 1 il numero di figli della persona creata nelle 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 {
    // thread name
    private String name;

    // reference on the [dao] layer
    private IDao dao;

    // the id of the person we're going to work on
    private int idPersonne;

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

    // thread core
    public void run() {
        // follow-up
        suivi("lancé");
        // we loop until we have succeeded in incrementing by 1
        // person's number of children idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
            // a copy of the idPersonne person is retrieved
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
            // follow-up
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1)
                    + " pour la version " + personne.getVersion());
            // 10 ms wait to abandon processor
            try {
                // follow-up
                suivi("début attente");
                // we pause to let the processor
                Thread.sleep(10);
                // follow-up
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
            // waiting complete - try to validate the copy
            // in the meantime, other threads may have modified the original
            int codeErreur = 0;
            try {
                // increments by 1 the number of children in this copy
                personne.setNbEnfants(nbEnfants + 1);
                // we try to modify the original
                dao.saveOne(personne);
                // we passed - the original has been modified
                fini = true;
            } catch (DaoException ex) {
                // we retrieve the error code
                codeErreur = ex.getCode();
                // if a ID or error code 2 version error occurs, retry the update
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                default:
                    // unhandled exception - left to rise
                    throw ex;
                }
            }
        }
        // follow-up
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

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

L'aggiornamento di una persona potrebbe non andare a buon fine perché la persona che vogliamo modificare non esiste o perché è stata precedentemente aggiornata da un altro thread. Questi due casi vengono gestiti qui alle righe 67–69. In entrambi i casi, il metodo [updatePersonne] genera un'eccezione [DaoException] con codice 2. Il thread sarà quindi costretto a riavviare la procedura di aggiornamento dall'inizio (ciclo while, riga 34).


[test6]


[test6] ha lo scopo di testare il metodo [insertPersonne - DaoImplCommon]. Ecco il codice di tale metodo:

// add a person
    protected void insertPersonne(Personne personne) {
        // 1st version
        personne.setVersion(1);
        // wait 10 ms - for tests set true instead of false
        if (true)
            wait(10);
        // insert the new person in the BD table
        getSqlMapClientTemplate().insert("Personne.insertOne", personne);
    }
  • Righe 6-7: attendiamo 10 ms per costringere il thread che esegue [insertPerson] a cedere la CPU, aumentando così le nostre possibilità di rilevare conflitti causati da thread che eseguono inserimenti contemporaneamente.

Il codice per [test6] è il seguente:

    // multi-threaded insertions
    public void test6() throws ParseException, InterruptedException{
        // creation of a person
        Personne p = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        // duplicated N times in an array
        final int N = 100;
        Personne[] personnes=new Personne[N];
        for(int i=0;i<personnes.length;i++){
            personnes[i]=new Personne(p);
        }
        // creation of N insertion threads - each thread inserts 1 person
        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();
        }
        // we wait for the end of threads
        for (int i = 0; i < taches.length; i++) {
            // thread n° i
            taches[i].join();
            // supression personne
            dao.deleteOne(personnes[i].getId());
        }
}

Creiamo 100 thread che inseriranno simultaneamente 100 persone diverse. Questi 100 thread otterranno tutti una chiave primaria per la persona che devono inserire, quindi verranno messi in pausa per 10 ms (riga 10 – insertPerson) prima di poter eseguire l'inserimento. Vogliamo verificare che tutto proceda senza intoppi 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 nelle righe 4–5.
  • Righe 14–17: Vengono avviati i 100 thread di inserimento. Ciascuno è responsabile dell'inserimento di una delle 100 persone create in precedenza.
  • Righe 19–23: [test6] attende che ciascuno dei 100 thread avviati abbia terminato. Quando rileva che il thread numero i ha terminato, 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 {
    // thread name
    private String name;

    // reference on the [dao] layer
    private IDao dao;

    // the id of the person we're going to work on
    private Personne personne;

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

    // thread core
    public void run() {
        // follow-up
        suivi("lancé");
        // insertion
        dao.saveOne(personne);
        // follow-up
        suivi("a terminé");
    }

    // follow-up
    private void suivi(String message) {
        System.out.println(name + " [" + new Date().getTime() + "] : "
                + message);
    }
}
  • Righe 19–22: il costruttore del thread memorizza la persona da inserire e il livello [DAO] da utilizzare per l'inserimento.
  • riga 30: la persona viene inserita. Se si verifica un'eccezione, questa viene propagata a [test6].

Test


Durante i test, otteniamo 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 la presenza 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];   
--- The error occurred in personnes-firebird.xml.  
--- The error occurred while applying a parameter map.  
--- Check the Personne.updateOne-InlineParameterMap.  
--- Check the statement (update failed).  
--- Cause: org.firebirdsql.jdbc.FBSQLException: GDS Exception. 335544336. deadlock
update conflicts with concurrent update; nested exception is com.ibatis.common.jdbc.exception.NestedSQLException:   
--- The error occurred in personnes-firebird.xml.  
--- The error occurred while applying a parameter map.  
  • Riga 1 – Si è verificata un'eccezione Spring [org.springframework.jdbc.UncategorizedSQLException]. Si tratta di un'eccezione non gestita 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 concorrenza tra due thread che stavano tentando di aggiornare contemporaneamente la stessa riga nella tabella [PERSONNES].

Questo non è un errore fatale. Il thread che intercetta questa eccezione può riprovare l'aggiornamento. Per farlo, modificare il codice in [ThreadDaoMajEnfants]:

            try {
                // incrémente de 1 le nbre d'enfants de cette copie
                personne.setNbEnfants(nbEnfants + 1);
                // on essaie de modifier l'original
                dao.saveOne(personne);
                // on est passé - l'original a été modifié
                fini = true;
            } catch (DaoException ex) {
                // on récupère le code erreur
                codeErreur = ex.getCode();
                // si une erreur d'ID ou de version de code ereur 2, on réessaie la mise à jour
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                default:
                    // exception non gérée - on laisse remonter
                    throw ex;
                }
  • Riga 8: Gestiamo un'eccezione di tipo [DaoException]. In base a quanto detto, dovremmo gestire l'eccezione che è apparsa durante i test, il tipo [org.springframework.jdbc.UncategorizedSQLException]. Tuttavia, non possiamo semplicemente gestire questo tipo, che è un tipo generico di Spring destinato a incapsulare le eccezioni che non riconosce. Spring riconosce le eccezioni generate dai driver JDBC di numerosi DBMS 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 illustrato sopra, la classe [UncategorizedSQLException] deriva dalla classe [DataAccessException] menzionata nella sezione 17.3.3. È possibile determinare quale eccezione sia stata incapsulata in [UncategorizedSQLException] utilizzando il suo metodo [getSQLException]:

Image

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

Image

Otteniamo l'oggetto di tipo [Throwable] generato dal driver JDBC:

Image

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

Qui, dobbiamo verificare che l'oggetto [Throwable] generato dal driver JDBC di Firebird — che ha causato l'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 utilizziamo l'eccezione [org.firebirdsql.gds.GDSException] nel codice [ThreadDaoMajEnfants], allora questo thread funzionerà solo con il DBMS Firebird. Lo stesso vale per il test [test4] che utilizza questo thread. Vogliamo evitare questo. Infatti, vogliamo che i nostri test JUnit rimangano validi indipendentemente dal DBMS utilizzato. Per ottenere questo, decidiamo che il livello [dao] lancerà una [DaoException] con codice 4 ogni volta che viene rilevata un'eccezione di "conflitto di aggiornamento", indipendentemente dal DBMS sottostante. Pertanto, il thread [ThreadDaoMajEnfants] può essere riscritto come segue:

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

public class ThreadDaoMajEnfants extends Thread {
...

    // thread core
    public void run() {
...
        while (!fini) {
            // a copy of the idPersonne person is retrieved
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
...
            // waiting complete - try to validate the copy
            // in the meantime, other threads may have modified the original
            int codeErreur = 0;
            try {
                // increments by 1 the number of children in this copy
                personne.setNbEnfants(nbEnfants + 1);
                // we try to modify the original
                dao.saveOne(personne);
                // we passed - the original has been modified
                fini = true;
            } catch (DaoException ex) {
                // we retrieve the error code
                codeErreur = ex.getCode();
                // if a ID or version 2 error or a deadlock 4 occurs, we
                // try the update again
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                case 4:
                    suivi("conflit de mise à jour");
                    break;
                default:
                    // unhandled exception - left to rise
                    throw ex;
                }
            }
        }
        // follow-up
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }
...
}
  • righe 34-36: viene intercettata l'eccezione [DaoException] con codice 4. Il thread [ThreadDaoMajEnfants] sarà costretto a riavviare la procedura di aggiornamento dall'inizio (riga 10)

Il nostro livello [dao] deve quindi essere in grado di riconoscere un'eccezione di "conflitto di aggiornamento". Questa eccezione viene generata da un driver JDBC ed è specifica di esso. Questa eccezione deve essere gestita nel metodo [updatePerson] della classe [DaoImplCommon]:

// edit a person
    protected void updatePersonne(Personne personne) {
        // wait 10 ms - for tests set true instead of false
        if (true)
            wait(10);
        // change
        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 DBMS Firebird, dobbiamo verificare che l'eccezione che ha causato il fallimento dell'aggiornamento sia di tipo [org.firebirdsql.gds.GDSException] e abbia il codice di errore 335544336. Se inseriamo questo tipo di test in [DaoImplCommon], legheremo questa classe al DBMS Firebird, il che è ovviamente indesiderabile. Se vogliamo mantenere la classe [DaoImplCommon] generica, dobbiamo derivarla e gestire l'eccezione in una classe specifica per Firebird. Questo è ciò che stiamo facendo 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 {

    // edit a person
    protected void updatePersonne(Personne personne) {
        // wait 10 ms - for tests set true instead of false
        if (true)
            wait(10);
        // change
        try {
            // we modify the person who has the correct version
            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;
            }
        }
    }

    // waiting
    private void wait(int N) {
        // we wait for N ms
        try {
            Thread.sleep(N);
        } catch (InterruptedException e) {
            // the exception trace is displayed
            e.printStackTrace();
            return;
        }
    }

}
  • Riga 5: La classe [DaoImplFirebird] deriva da [DaoImplCommon], la classe che abbiamo appena studiato. Ridefinisce, alle righe 8–33, il metodo [updatePersonne] che ci sta causando 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: verifichiamo inoltre che il codice di errore per questa eccezione Firebird sia 335544336, il codice di errore "deadlock".
  • righe 26-27: se tutte queste condizioni sono soddisfatte, viene generata una [DaoException] con codice 4.
  • righe 36-44: il metodo [wait] mette in pausa il thread corrente per N millisecondi. È utile solo per i test.

Siamo pronti a testare il nuovo livello [dao].

17.4.3. Test dell'implementazione [DaoImplFirebird]

Il file di configurazione di 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>
    <!-- data source DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- warning: do not leave spaces between the two <value> tags -->
        <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>
    <!-- the [dao] layer access classes -->
    <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] superato. Le ultime righe dei log sullo 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 il thread n. 36 è stato l'ultimo a terminare. La riga 3 mostra un conflitto di versione che ha costretto il thread n. 36 a riavviare 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 è stato emesso il comando SQL [update] sulla tabella [PERSONNES], la riga che doveva essere aggiornata era bloccata da un altro thread. Questo conflitto di accesso costringerà il thread n. 75 a riprovare l'aggiornamento.

Per concludere con [test4], notiamo una differenza significativa rispetto ai risultati dello stesso test nella versione 1, dove falliva a causa di problemi di sincronizzazione. Poiché i metodi nel livello [dao] della versione 1 non erano sincronizzati, si verificavano conflitti di accesso. Qui, non abbiamo avuto bisogno di sincronizzare il livello [dao]. Abbiamo semplicemente gestito i conflitti di accesso segnalati da Firebird.

Eseguiamo ora l'intero test JUnit per il livello [dao]:

Image

Sembra quindi che abbiamo un livello [dao] valido. Per dichiararlo valido con un alto grado di certezza, dovremmo eseguire ulteriori test. Tuttavia, lo considereremo operativo.

17.5. Il livello [service]

17.5.1. I componenti del livello [service]

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

Image

  • [IService] è l'interfaccia presentata dal livello [service]
  • [ServiceImpl] è un'implementazione di questa interfaccia

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 {
    // list of all persons
    Collection getAll();

    // find a specific person
    Personne getOne(int id);

    // add/modify a person
    void saveOne(Personne personne);

    // delete a person
    void deleteOne(int id);

    // save multiple people
    void saveMany(Personne[] personnes);

    // delete several people
    void deleteMany(int ids[]);
}
  • L'interfaccia presenta gli stessi quattro metodi della versione 1, ma ne ha due aggiuntivi:
    • 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 di database. Entrambi i metodi devono 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 {

    // the [dao] layer
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

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

    // list of persons
    public Collection getAll() {
        return dao.getAll();
    }

    // get a specific person
    public Personne getOne(int id) {
        return dao.getOne(id);
    }

    // add or modify a person
    public void saveOne(Personne personne) {
        dao.saveOne(personne);
    }

    // deleting a person
    public void deleteOne(int id) {
        dao.deleteOne(id);
    }

    // save a collection of people
    public void saveMany(Personne[] personnes) {
        // we loop over the people table
        for (int i = 0; i < personnes.length; i++) {
            dao.saveOne(personnes[i]);
        }
    }

    // delete a collection of people
    public void deleteMany(int[] ids) {
        // ids: the ids of the people to be deleted
        for (int i = 0; i < ids.length; i++) {
            dao.deleteOne(ids[i]);
        }
    }
}
  • I metodi [getAll, getOne, insertOne, saveOne] richiamano i metodi del livello [dao] con gli stessi nomi.
  • Righe 42–47: Il metodo [saveMany] salva, uno per uno, le persone presenti nell'array passato come parametro.
  • Righe 50–55: Il metodo [deleteMany] elimina, una alla volta, le persone i cui ID sono passati come parametro array.

Abbiamo menzionato che i metodi [saveMany] e [deleteMany] devono essere eseguiti all'interno di una transazione per garantire la natura "tutto o niente" di questi metodi. Possiamo notare che il codice sopra riportato ignora completamente questo concetto di transazioni. Questo apparirà solo nel file di configurazione del livello [service].

17.5.2. Configurazione del livello [ ]

Sopra, alla riga 11, vediamo che l'implementazione [ServiceImpl] contiene un riferimento al livello [dao]. Questo, come nella versione 1, verrà inizializzato da Spring quando il livello [service - ServiceImpl] viene istanziato. Il file di configurazione che consentirà l'istanziazione del livello [service] è il seguente:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- data source 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">
            <!-- warning: do not leave spaces between the two <value> tags -->
            <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>
    <!-- the [dao] layer access class -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
    <!-- transaction manager -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!-- access classes to the [service] layer -->
    <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 quando si è discusso del livello [dao] nella sezione 17.3.2.
  • Righe 38–64: configurazione del livello [service]

Alla riga 46, possiamo vedere che il livello [service] è implementato dal tipo [TransactionProxyFactoryBean]. Ci aspettavamo di trovare il tipo [ServiceImpl]. [TransactionProxyFactoryBean] è un tipo Spring predefinito. Com'è possibile che un tipo predefinito implementi l'interfaccia [IService], che è specifica della nostra applicazione?

Diamo prima un'occhiata alla classe [TransactionProxyFactoryBean]:

Image

Vediamo che implementa l'interfaccia [FactoryBean]. Abbiamo già incontrato questa interfaccia. Sappiamo che quando un'applicazione richiede a Spring un'istanza di un tipo che implementa [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] sarà implementato dall'oggetto restituito dal metodo [TransactionProxyFactoryBean].getObject(). Qual è la natura di questo oggetto? Non entreremo nei dettagli perché sono complessi. Rientrano in quello che è noto come Spring AOP (programmazione orientata agli aspetti). Cercheremo di chiarire il concetto con alcuni semplici diagrammi. L'AOP consente quanto segue:

  • Abbiamo due classi, C1 e C2, dove C1 utilizza l'interfaccia [I2] fornita da C2:
  • Grazie all'AOP, possiamo inserire un intercettatore tra le classi C1 e C2 in modo trasparente per entrambe le classi:

La classe [C1] è stata compilata per funzionare con l'interfaccia [I2] implementata da [C2]. In fase di esecuzione, l'AOP inserisce la classe [interceptor] tra [C1] e [C2]. Affinché ciò sia possibile, la classe [interceptor] deve, ovviamente, presentare la stessa interfaccia [I2] a [C1] così come fa [C2].

A cosa serve? La documentazione di Spring fornisce alcuni esempi. Ad esempio, potresti voler registrare le chiamate a un metodo specifico M di [C2] per poterlo monitorare. In [interceptor], dovresti quindi scrivere un metodo [M] che esegua queste registrazioni. La chiamata da [C1] a [C2].M procederà come segue (vedi diagramma sopra):

  1. [C1] chiama il metodo M di [C2]. In realtà, sarà il metodo M di [interceptor] a essere chiamato. Ciò è possibile se [C1] si rivolge a un'interfaccia [I2] anziché a una specifica implementazione di [I2]. Tutto ciò che serve è che [interceptor] implementi [I2].
  2. Il metodo M di [interceptor] registra le informazioni e chiama il metodo M di [C2] che era inizialmente il bersaglio di [C1].
  3. Il metodo M di [C2] viene eseguito e restituisce il proprio risultato al metodo M di [interceptor], che può facoltativamente aggiungere qualcosa a quanto fatto nel passaggio 2.
  4. Il metodo M di [interceptor] restituisce un risultato al metodo chiamante di [C1]

Vediamo che il metodo M di [interceptor] può fare qualcosa prima e dopo la chiamata al metodo M di [C2]. Dal punto di vista di [C1], arricchisce quindi il metodo M di [C2]. Possiamo 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 implementiamo il livello [service] con un'istanza [TransactionProxyFactoryBean], avremo la seguente architettura:

Possiamo dire che il livello [service] viene istanziato con due oggetti:

  • l'oggetto a cui ci riferiamo sopra come [proxy transazionale], che in realtà è l'oggetto restituito dal metodo [getObject] di [TransactionProxyFactoryBean]. Questo oggetto funge da interfaccia tra il livello [service] e il livello [web]. Per come è progettato, implementa l'interfaccia [IService].
  • un'istanza di [ServiceImpl], che implementa anch'essa l'interfaccia [IService]. Solo questa sa come interagire con il livello [dao], quindi è necessaria.

Immaginiamo che il livello [web] chiami il metodo [saveMany] dell'interfaccia [IService]. Sappiamo che, dal punto di vista funzionale, gli inserimenti/aggiornamenti eseguiti da questo metodo devono avvenire all'interno di una transazione. O vanno tutti a buon fine, oppure nessuno viene eseguito. Abbiamo introdotto il metodo [saveMany] della classe [ServiceImpl] e abbiamo notato che mancava il concetto di transazione. Il metodo [saveMany] del [proxy transazionale] migliorerà il metodo [saveMany] della classe [ServiceImpl] con questo concetto di transazione. Seguiamo il diagramma sopra:

  1. Il livello [web] chiama il metodo [saveMany] dell'interfaccia [IService].
  2. Viene eseguito il metodo [saveMany] del [transactional proxy]. Questo avvia una transazione. Deve disporre di informazioni sufficienti per farlo, in particolare un oggetto [DataSource] per stabilire una connessione al DBMS. Quindi chiama il metodo [saveMany] di [ServiceImpl].
  3. Questo metodo viene eseguito. Chiama ripetutamente il livello [dao] per eseguire gli inserimenti o gli aggiornamenti. Le istruzioni SQL eseguite in questo momento vengono eseguite all'interno della transazione avviata al punto 2.
  4. Supponiamo che una di queste operazioni fallisca. Il livello [dao] propagherà un'eccezione fino al livello [service], in particolare al metodo [saveMany] dell'istanza [ServiceImpl].
  5. Questo metodo non fa nulla e permette all'eccezione di propagarsi fino al metodo [saveMany] di [transactional proxy].
  6. Una volta ricevuta l'eccezione, il metodo [saveMany] di [transactional proxy], che possiede la transazione, esegue un [rollback] per annullare tutti gli aggiornamenti, quindi permette all'eccezione di propagarsi fino al livello [web], che sarà responsabile della sua gestione.

Nel passaggio 4, abbiamo ipotizzato che uno degli inserimenti o degli aggiornamenti non sia andato a buon fine. Se così non fosse, in [5] non verrebbe propagata alcuna eccezione. Lo stesso vale in [6]. In questo caso, il metodo [saveMany] di [transactional proxy] esegue il commit della transazione per convalidare tutti gli aggiornamenti.

Ora abbiamo un quadro più chiaro dell'architettura implementata dal bean [TransactionProxyFactoryBean]. Rivediamo la sua configurazione:


    <!-- transaction manager -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!-- access classes to the [service] layer -->
    <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 che è stata impostata:

  • [transactional proxy] gestirà le transazioni. Spring offre diverse strategie di gestione delle transazioni. [transactional proxy] richiede un riferimento al gestore delle transazioni scelto.
  • Righe 11–13: definiscono l'attributo [transactionManager] del bean [TransactionProxyFactoryBean] con un riferimento a un gestore delle transazioni. Questo è definito nelle righe 2–7.
  • Righe 2–7: il gestore delle transazioni è di tipo [DataSourceTransactionManager]:

Image

[DataSourceTransactionManager] è un gestore di transazioni adatto ai DBMS a cui si accede tramite un oggetto [DataSource]. Può gestire solo transazioni su un singolo DBMS. Non può gestire transazioni distribuite su più DBMS. In questo caso, abbiamo un solo DBMS. Pertanto, questo gestore di transazioni è appropriato. Quando il [proxy transazionale] avvia una transazione, lo fa su una connessione collegata al thread. Questa connessione verrà utilizzata in tutti i livelli che conducono al database: [ServiceImpl, DaoImplCommon, SqlMapClientTemplate, JDBC].

La classe [DataSourceTransactionManager] deve conoscere l'origine dati da cui deve richiedere una connessione da associare al thread. Ciò è definito nelle righe 4–6: si tratta della stessa origine dati utilizzata dal livello [dao] (vedere la sezione 17.5.2).

  • Righe 14–19: L'attributo "target" specifica la classe da intercettare, in questo caso la classe [ServiceImpl]. Questa informazione è necessaria per due motivi:
    • la classe [ServiceImpl] deve essere istanziata poiché gestisce la comunicazione con il livello [dao]
    • [TransactionProxyFactoryBean] deve generare un proxy che presenti la stessa interfaccia di [ServiceImpl] al livello [web].
  • Righe 21–27: specificano quali metodi di [ServiceImpl] il proxy deve intercettare. L'attributo [transactionAttributes] alla riga 21 indica quali metodi di [ServiceImpl] richiedono una transazione e quali sono gli attributi della transazione:
  • riga 23: i metodi i cui nomi iniziano con get [getOne, getAll] vengono eseguiti all'interno di una transazione con gli attributi [PROPAGATION_REQUIRED, readOnly]:
    • PROPAGATION_REQUIRED: il metodo viene eseguito in una transazione se ne è già una associata al thread; in caso contrario, ne viene creata una nuova e il metodo viene eseguito al suo interno.
    • readOnly: transazione di sola lettura

Qui, i metodi [getOne] e [getAll] di [ServiceImpl] verranno eseguiti all'interno di una transazione, anche se in realtà non è necessario. Ogni operazione consiste in una singola istruzione SELECT. Non vediamo il motivo di inserire questa SELECT all'interno di una transazione.

  • Riga 24: I metodi i cui nomi iniziano con "save" — [saveOne] e [saveMany] — vengono eseguiti all'interno di una transazione con l'attributo [PROPAGATION_REQUIRED].
  • Riga 25: I metodi [deleteOne] e [deleteMany] di [ServiceImpl] sono configurati in modo identico ai metodi [saveOne] e [saveMany].

Nel nostro livello [service], solo i metodi [saveMany] e [deleteMany] devono essere eseguiti all'interno di 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], lo testeremo utilizzando i test JUnit:

Image

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

Il test JUnit [TestServiceFirebird] è il seguente:

package istia.st.mvc.personnes.tests;

...

public class TestServiceFirebird extends TestCase {

    // service] layer
    private IService service;

    public IService getService() {
        return service;
    }

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

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

    // list of persons
    private void doListe(Collection personnes) {
...
    }

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

    // modification-deletion of a non-existent element
    public void test2() throws ParseException {
...
    }

    // person version management
    public void test3() throws ParseException, InterruptedException {
...
    }

    // optimistic locking - multi-threaded access
    public void test4() throws Exception {
...
    }

    // validity tests for saveOne
    public void test5() throws ParseException {
...
    }

        // multi-threaded insertions
    public void test6() throws ParseException, InterruptedException{
...
    }

    // tests of the deleteMany method
    public void test7() throws ParseException {
        // current list
        Collection personnes = service.getAll();
        int nbPersonnes1 = personnes.size();
        // display
        doListe(personnes);
        // creation of three people
        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);
        // add 3 people - person p3 with id -2 will cause
        // an exception
        boolean erreur = false;
        try {
            service.saveMany(new Personne[] { p1, p2, p3 });
        } catch (Exception ex) {
            erreur = true;
            System.out.println(ex.toString());
        }
        // check
        assertTrue(erreur);
        // new list - the number of elements must not have changed
        // because of automatic transaction rollback
        int nbPersonnes2 = service.getAll().size();
        assertEquals(nbPersonnes1, nbPersonnes2);
        // addition of two able-bodied people
        // reset their id to -1
        p1.setId(-1);
        p2.setId(-1);
        service.saveMany(new Personne[] { p1, p2 });
        // we retrieve their id
        int id1 = p1.getId();
        int id2 = p2.getId();
        // checks
        p1 = service.getOne(id1);
        assertEquals(p1.getNom(), "X");
        p2 = service.getOne(id2);
        assertEquals(p2.getNom(), "Y");
        // new list - must have 2 + elements
        int nbPersonnes3 = service.getAll().size();
        assertEquals(nbPersonnes1 + 2, nbPersonnes3);
        // deletion of p1 and p2 and a non-existent person
        // an exception must occur
        erreur = false;
        try {
            service.deleteMany(new int[] { id1, id2, -1 });
        } catch (Exception ex) {
            erreur = true;
            System.out.println(ex.toString());
        }
        // check
        assertTrue(erreur);
        // new list
        personnes = service.getAll();
        int nbPersonnes4 = personnes.size();
        // no person had to be deleted (rollback
        // automatic transaction)
        assertEquals(nbPersonnes4, nbPersonnes3);
        // we remove the two able-bodied people
        service.deleteMany(new int[] { id1, id2 });
        // checks
        // person p1
        erreur = false;
        int codeErreur = 0;
        try {
            p1 = service.getOne(id1);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
        // we must have a code 2 error
        assertTrue(erreur);
        assertEquals(2, codeErreur);
        // person p2
        erreur = false;
        codeErreur = 0;
        try {
            p1 = service.getOne(id2);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
        // we must have a code 2 error
        assertTrue(erreur);
        assertEquals(2, codeErreur);
        // new list
        personnes = service.getAll();
        int nbPersonnes5 = personnes.size();
        // verification - we must be back at the starting point
        assertEquals(nbPersonnes5, nbPersonnes1);
        // display
        doListe(personnes);
    }

}
  • righe 19–22: il programma verifica i livelli [dao] e [service] configurati dal file [spring-config-test-service-firebird.xml], di cui si è parlato nella sezione precedente.
  • I test da [test1] a [test6] sono concettualmente identici alle loro 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] ora vengono eseguiti all'interno di una transazione.
  • Lo scopo del metodo [test7] è quello di testare i metodi [saveMany] e [deleteMany]. Vogliamo verificare che siano effettivamente eseguiti all'interno di una transazione. Commentiamo il codice di questo metodo:
  • righe 62–63: contiamo il numero di persone [nbPersonnes1] attualmente presenti nell'elenco
  • righe 67–72: creiamo tre persone
  • righe 73–83: queste tre persone vengono salvate dal metodo [saveMany] – riga 77. Le prime due persone, p1 e p2, con un ID pari a -1, verranno aggiunte alla tabella [PERSONNES]. La persona p3 ha un ID pari a -2. Non si tratta quindi di un inserimento, ma di un aggiornamento. Questo aggiornamento fallirà perché non esiste nessuna persona con ID -2 nella tabella [PERSONS]. Il livello [dao] genererà quindi un'eccezione che si propagherà fino al livello [service]. L'esistenza di questa eccezione viene verificata alla riga 83.
  • A causa dell'eccezione precedente, il livello [service] dovrebbe eseguire il rollback di tutte le istruzioni SQL emesse durante l'esecuzione del metodo [saveMany], poiché questo metodo viene eseguito all'interno di una transazione. Righe 86–87: Verifichiamo che il numero di persone nell'elenco non sia cambiato, il che significa che gli inserimenti di p1 e p2 non hanno avuto luogo.
  • Righe 88–103: Aggiungiamo solo p1 e p2 e verifichiamo che ora ci siano due persone in più nell'elenco.
  • Righe 106–114: Eliminiamo un gruppo di persone composto dalle persone p1 e p2 che abbiamo appena aggiunto e da una persona inesistente (id = -1). A tal fine viene utilizzato il metodo [deleteMany], riga 108. Questo metodo fallirà perché non esiste alcuna persona con un id pari a –1 nella tabella [PERSONNES]. Il livello [dao] genererà quindi un'eccezione che si propagherà fino al livello [service]. L'esistenza di questa eccezione viene verificata alla riga 114.
  • A causa dell'eccezione precedente, il livello [service] dovrebbe eseguire un [rollback] di tutte le istruzioni SQL emesse durante l'esecuzione del metodo [deleteMany], poiché tale metodo viene eseguito all'interno di una transazione. Righe 116–117: Verifichiamo che il numero di persone nell'elenco non sia cambiato e che, di conseguenza, le eliminazioni di p1 e p2 non abbiano avuto luogo.
  • Riga 122: eliminiamo un gruppo composto esclusivamente dalle persone p1 e p2. L'operazione dovrebbe avere esito positivo. Il resto del metodo verifica che sia effettivamente così.

L'esecuzione dei test produce i seguenti risultati:

Image

Tutti e sette i test hanno avuto esito positivo. Considereremo il nostro livello [service] operativo.

17.7. Il livello [w eb]

Esaminiamo l'architettura generale dell'applicazione web da realizzare:

Abbiamo appena realizzato i livelli [dao] e [service] per lavorare con un database Firebird. Abbiamo scritto una versione 1 di questa applicazione in cui i livelli [dao] e [service] lavoravano con un elenco di persone in memoria. Il livello [web] scritto in quel momento rimane valido. Infatti, interagiva con un livello [service] che implementava l'interfaccia [IService]. Poiché il nuovo livello [service] implementa questa stessa interfaccia, il livello [web] non necessita di modifiche.

Nell'articolo precedente, la versione 1 dell'applicazione è stata testata con il progetto Eclipse [mvc-personnes-02B], in cui i livelli [web, service, dao, entities] erano impacchettati in file .jar:

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

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

Image

Nel progetto [mvc-personnes-03], esportiamo [File / Esporta / File Jar] i livelli [DAO] e [service] rispettivamente negli archivi [personnes-dao.jar] e [personnes-service.jar] nella 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 i file 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 file JAR sono necessari per i nuovi livelli [dao] e [service].

Una volta fatto ciò, includiamo i nuovi file JAR nel Classpath del progetto: [clic destro sul progetto -> Proprietà -> Java Build Path -> Aggiungi JAR].

La cartella [src] contiene i file di configurazione per i 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] utilizzato per configurare il test del livello di servizio nel progetto [mvc-personnes-03]. Copiamo e incolliamo quindi da uno all'altro:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- data source 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">
            <!-- warning: do not leave spaces between the two <value> tags -->
            <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>
    <!-- the [dao] layer access class -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
    <!-- transaction manager -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!-- access classes to the [service] layer -->
    <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 testare i livelli [dao] e [service]

Distribuiamo il progetto web [mvc-personnes-03B] all'interno di Tomcat:

Siamo pronti per il test . Il DBMS Firebird è in esecuzione. Il contenuto della tabella [PERSONNES] è il seguente:

Image

Viene quindi avviato Tomcat. Utilizzando un browser, inviamo una richiesta all'URL [http://localhost:8080/mvc-personnes-03B]:

Image

Aggiungiamo una nuova persona utilizzando il link [Add]:

Verifichiamo l'aggiunta nel database:

Image

Il lettore è invitato a eseguire altre operazioni [modifica, elimina].

Ora eseguiamo il test di conflitto di versione effettuato nella Versione 1. [Firefox] sarà il browser dell'utente U1. L'utente U1 richiede l'URL [http://localhost:8080/mvc-personnes-03B]:

Image

[IE] sarà il browser dell'utente U2. L'utente U2 richiede lo stesso URL:

Image

L'utente U1 inserisce i dettagli della persona [Perrichon]:

Image

L'utente U2 fa lo stesso:

Image

L'utente U1 apporta delle modifiche e invia:

L'utente U2 fa lo stesso:

L'utente U2 torna all'elenco delle persone utilizzando il link [Annulla] presente nel modulo:

Image

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

E il database? Diamo un'occhiata:

Image

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

17.8. Conclusione

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

in cui i livelli [dao] e [service] funzionavano con un elenco di dati in memoria che andava perso allo spegnimento del server web. Quella era la versione 1. Nella versione 2, i livelli [service] e [dao] sono stati riscritti in modo che l'elenco delle persone fosse memorizzato in una tabella del database. Ora è persistente. Proponiamo ora di esaminare l'impatto che la modifica del DBMS ha sulla nostra applicazione. Per farlo, realizzeremo tre nuove versioni della nostra applicazione web:

  • Versione 3: il DBMS è Postgres
  • Versione 4: il DBMS è MySQL
  • Versione 5: il DBMS è SQL Server Express 2005

Le modifiche vengono apportate nelle seguenti posizioni:

  • La classe [DaoImplFirebird] implementa le funzionalità del livello [dao] relative al DBMS Firebird. Se questo requisito persiste, verrà sostituita rispettivamente dalle classi [DaoImplPostgres], [DaoImplMySQL] e [DaoImplSqlExpress].
  • Il file di mappatura iBATIS [personnes-firebird.xml] per il DBMS Firebird sarà sostituito dai file di mappatura [personnes-postgres.xml], [personnes-mysql.xml] e [personnes-sqlexpress.xml], rispettivamente.
  • La configurazione dell'oggetto [DataSource] nel livello [dao] è specifica per un DBMS. Cambierà quindi con ogni versione.
  • Anche il driver JDBC del DBMS cambia con ogni versione

A parte questi punti, tutto il resto rimane invariato. Nelle sezioni seguenti descriviamo queste nuove versioni, concentrandoci esclusivamente sulle nuove funzionalità introdotte da ciascuna di esse.