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

La tabella [PERSONNES] conterrà l'elenco delle persone gestite dall'applicazione web. È stata creata utilizzando le seguenti istruzioni SQL:
- 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:

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

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

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:
![]()
- [dbpersonnes.gdb] è il database.
- [dbpersonnes.sql] è lo script SQL per la generazione del database:
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:
![]()
- [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:

- [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:
- L'interfaccia presenta gli stessi quattro metodi della versione precedente.
La classe [DaoImplCommon] che implementa questa interfaccia sarà la seguente:
- 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/]:

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

![]() |
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:

Tra i metodi di questa classe, uno ci permette di configurare il client [iBATIS] con cui gestiremo il database:
![]()
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:
- richiedere una connessione da un pool di connessioni
- aprire una transazione
- eseguire una serie di istruzioni SQL memorizzate in un file di configurazione
- chiudere la transazione
- 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]:

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

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()]:
![]()
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à:

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

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

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:

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:
- il nome del driver JDBC da utilizzare – inizializzato con [setDriverClassName]
- l'URL del database da utilizzare – inizializzato con [setUrl]
- il nome utente dell'utente titolare della connessione – inizializzato con [setUsername] (non setUserName come ci si potrebbe aspettare)
- 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:

- [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:
Esamineremo i metodi uno per uno.
getAll
Questo metodo recupera tutte le persone presenti nell'elenco. Il codice è il seguente:
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:
![]()
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:
![]()
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:

Implementa la seguente interfaccia [SqlMapClientOperations]:

Questa interfaccia definisce metodi in grado di utilizzare il contenuto del file [people-firebird.xml]:
[queryForList]
![]()
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]
![]()
Questo metodo è concettualmente identico al precedente ma restituisce solo un singolo oggetto. Se [SELECT] non restituisce righe, il risultato è il puntatore null.
[insert]
![]()
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]
![]()
Questo metodo esegue un'istruzione SQL [update] configurata dal secondo parametro. Il risultato è il numero di righe modificate dall'istruzione SQL [update].
[delete]
![]()
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]:
- 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:
- 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:
- 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:
- 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:
- 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.
- 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:
- 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:
- Un aggiornamento può fallire per almeno due motivi:
- la persona da aggiornare non esiste
- 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:

Prima di eseguire test approfonditi, possiamo iniziare con un semplice programma [main] che visualizzerà il contenuto della tabella [PERSONNES]. Questa è la classe [MainTestDaoFirebird]:
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:

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

Abbiamo ottenuto con successo l'elenco delle persone. Ora possiamo procedere al test JUnit.
Il test JUnit [TestDaoFirebird] è il seguente:
- 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:
- 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.
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:
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:
- 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:
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:
- 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]:
- 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]:

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]:
![]()
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:
![]()
Otteniamo l'oggetto di tipo [Throwable] generato dal driver JDBC:

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

[test4] superato. Le ultime righe dei log sullo schermo sono le seguenti:
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:
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]:

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:
![]()
- [IService] è l'interfaccia presentata dal livello [service]
- [ServiceImpl] è un'implementazione di questa interfaccia
L'interfaccia [IService] è la seguente:
- 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:
- 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]:

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():
![]()
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):
- [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].
- Il metodo M di [interceptor] registra le informazioni e chiama il metodo M di [C2] che era inizialmente il bersaglio di [C1].
- 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.
- 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:
- Il livello [web] chiama il metodo [saveMany] dell'interfaccia [IService].
- 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].
- 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.
- 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].
- Questo metodo non fa nulla e permette all'eccezione di propagarsi fino al metodo [saveMany] di [transactional proxy].
- 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]:

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

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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | |
- 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:

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

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:

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

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:

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

Aggiungiamo una nuova persona utilizzando il link [Add]:
![]() | ![]() |
Verifichiamo l'aggiunta nel database:

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

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

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

L'utente U2 fa lo stesso:

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:

Trova la persona [Perrichon] così come modificata da U1 (nome in maiuscolo).
E il database? Diamo un'occhiata:

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.

























