17. Webanwendung MVC in einer 3-Tier-Architektur – Beispiel 3 – Firebird-Datenbank
17.1. Die Firebird-Datenbank
In dieser neuen Version werden wir die Personenliste in einer Firebird-Datenbanktabelle speichern. Im Dokument [http://tahe.developpez.com/divers/sql-firebird/] finden Sie Informationen zur Installation und Verwaltung dieses SGBD. Die folgenden Screenshots stammen aus IBExpert, einem Verwaltungsprogramm für die SGBD-Datenbanken Interbase und Firebird.
Die Datenbank heißt [dbpersonnes.gdb]. Sie enthält eine Tabelle namens [PERSONNES]:

Die Tabelle [PERSONNES] enthält die Liste der von der Webanwendung verwalteten Personen. Sie wurde mit den folgenden Befehlen SQL erstellt:
- Zeilen 2–10: Die Struktur der Tabelle [PERSONNES], die zum Speichern von Objekten des Typs [Personne] dient, spiegelt die Struktur dieses Objekts wider. Da der Typ „boolesch“ in Firebird nicht existiert, wurde das Feld [MARIE] (Zeile 8) als Typ [SMALLINT], also als Ganzzahl, deklariert. Sein Wert ist entweder 0 (unverheiratet) oder 1 (verheiratet).
- Zeilen 13–16: Integritätsbeschränkungen, die denen des Datenvalidators [ValidatePersonne] entsprechen.
- Zeile 19: Das Feld ID ist der Primärschlüssel der Tabelle [PERSONNES]
Die Tabelle [PERSONNES] könnte folgenden Inhalt haben:

Die Datenbank [dbpersonnes.gdb] enthält neben der Tabelle [PERSONNES] ein Objekt namens „Generator“ mit der Bezeichnung [GEN_PERSONNES_ID]. Dieser Generator liefert aufeinanderfolgende Ganzzahlen, die wir verwenden, um dem Primärschlüssel [ID] der Klasse [PERSONNES] einen Wert zuzuweisen. Betrachten wir ein Beispiel, um die Funktionsweise zu veranschaulichen:
![]() |
![]() |
Man kann feststellen, dass sich der Wert des Generators [GEN_PERSONNES_ID] geändert hat (Doppelklick darauf + F5 zum Aktualisieren):
Die Reihenfolge SQL
Dies führt somit zu folgendem Wert des Generators [GEN_PERSONNES_ID]. GEN_ID ist eine interne Funktion von Firebird und [RDB$DATABASE] eine Systemtabelle dieses SGBD.
17.2. Das Eclipse-Projekt der Schichten [dao] und [service]
Um die Schichten [dao] und [service] unserer datenbankgestützten Anwendung zu entwickeln, verwenden wir das folgende Eclipse-Projekt [mvc-personnes-03]:

Das Projekt ist ein einfaches Java-Projekt, kein Tomcat-Webprojekt. Zur Erinnerung: Version 2 unserer Anwendung wird die Schicht [web] aus Version 1 verwenden. Diese Schicht muss daher nicht neu geschrieben werden.
Ordner [src]
Dieser Ordner enthält den Quellcode der Schichten [dao] und [service]:

Darin befinden sich verschiedene Pakete:
- [istia.st.mvc.personnes.dao]: enthält die Schicht [dao]
- [istia.st.mvc.personnes.entites]: enthält die Klasse [Personne]
- [istia.st.mvc.personnes.service]: enthält die Klasse [service]
- [istia.st.mvc.personnes.tests]: enthält die Tests JUnit der Schichten [dao] und [service]
sowie Konfigurationsdateien, die sich im Verzeichnis ClassPath der Anwendung befinden müssen.
Ordner [database]
Dieser Ordner enthält die Firebird-Datenbank mit den Personen:
![]()
- [dbpersonnes.gdb] ist die Datenbank.
- [dbpersonnes.sql] ist das Skript SQL zur Erstellung der Datenbank:
Ordner [lib]
Dieser Ordner enthält die für die Anwendung erforderlichen Archive:
![]() |
Zu beachten ist das Vorhandensein des Treibers JDBC [firebirdsql-full.jar] von SGBD Firebird sowie einer Reihe von Archiven [spring-*.jar]. Wir hätten auch das einzige Archiv [spring.jar] verwenden können, das sich im Ordner [dist] der Distribution befindet und alle Spring-Klassen enthält. Man kann auch nur die für das Projekt erforderlichen Archive verwenden. Genau das haben wir hier getan, wobei wir uns an den von Eclipse gemeldeten Fehlern wegen fehlender Klassen und an den Namen der Teilarchive von Spring orientiert haben. Alle diese Archive aus dem Ordner „[lib]“ wurden in das Projektverzeichnis „Classpath“ verschoben.
Ordner [dist]
Dieser Ordner enthält die Archive, die bei der Kompilierung der Klassen der Anwendung entstanden sind:
![]()
- [personnes-dao.jar]: Archiv der Schicht [dao]
- [personnes-service.jar]: Archiv der Ebene [service]
17.3. Die Ebene [dao]
17.3.1. Die Komponenten der Schicht [dao]
Die Schicht [dao] besteht aus den folgenden Klassen und Schnittstellen:

- [IDao] ist die Schnittstelle, die von der Schicht [dao] bereitgestellt wird
- [DaoImplCommon] ist eine Implementierung davon, bei der sich die Personengruppe in einer Datenbanktabelle befindet. [DaoImplCommon] fasst Funktionen zusammen, die unabhängig von SGBD sind.
- [DaoImplFirebird] ist eine von [DaoImplCommon] abgeleitete Klasse zur spezifischen Verwaltung einer Firebird-Datenbank.
- [DaoException] ist der Typ der unkontrollierten Ausnahmen, die von der Schicht [dao] ausgelöst werden. Diese Klasse stammt aus Version 1.
Die Schnittstelle [IDao] lautet wie folgt:
- Die Schnittstelle verfügt über dieselben vier Methoden wie in der vorherigen Version.
Die Klasse [DaoImplCommon], die diese Schnittstelle implementiert, sieht wie folgt aus:
- Zeilen 8–9: Die Klasse [DaoImpl] implementiert die Schnittstelle [IDao] und damit die vier Methoden [getAll, getOne, saveOne, deleteOne].
- Zeilen 27–37: Die Methode [saveOne] verwendet zwei interne Methoden, [insertPersonne] und [updatePersonne], je nachdem, ob eine Person hinzugefügt oder geändert werden soll.
- Zeile 50: Die private Methode [check] stammt aus der vorherigen Version. Wir werden nicht näher darauf eingehen.
- Zeile 8: Zur Implementierung der Schnittstelle [IDao] leitet sich die Klasse [DaoImpl] von der Spring-Klasse [SqlMapClientDaoSupport] ab.
17.3.2. Die Datenzugriffsschicht [iBATIS]
Die Spring-Klasse [SqlMapClientDaoSupport] nutzt ein Framework eines Drittanbieters [Ibatis SqlMap], das unter der URL [http://ibatis.apache.org/] verfügbar ist:

[iBATIS] ist ein Apache-Projekt, das die Erstellung datenbankgestützter Schichten ([dao]) erleichtert. Mit [iBATIS] sieht die Architektur der Datenzugriffsschicht wie folgt aus:
![]() |
[iBATIS] wird zwischen die Anwendungsschicht [dao] und den Datenbanktreiber JDBC eingefügt. Es gibt Alternativen zu [iBATIS], wie beispielsweise die Alternative [Hibernate]:

![]() |
Für die Verwendung des Frameworks [iBATIS] sind zwei [ibatis-common, ibatis-sqlmap]-Archive erforderlich, die beide im Ordner [lib] des Projekts abgelegt wurden:
![]() |
Die Klasse [SqlMapClientDaoSupport] kapselt den generischen Teil der Nutzung des Frameworks [iBATIS], c.a.d. Codeabschnitte, die in allen Schichten [dao] vorkommen, die das Tool [iBATIS] verwenden. Um den nicht generischen Teil des Codes zu schreiben, d. h. den Teil, der spezifisch für die Schicht [dao] ist, muss lediglich die Klasse [SqlMapClientDaoSupport] abgeleitet werden. Genau das tun wir hier.
Die Klasse [SqlMapClientDaoSupport] ist wie folgt definiert:

Eine der Methoden dieser Klasse ermöglicht es, den Client [iBATIS] zu konfigurieren, mit dem die Datenbank genutzt werden soll:
![]()
Das Objekt [SqlMapClient sqlMapClient] ist das Objekt [IBATIS], das für den Zugriff auf eine Datenbank verwendet wird. Es implementiert für sich allein die Schicht [iBATIS] unserer Architektur:
![]() |
Eine typische Abfolge von Aktionen mit diesem Objekt sieht wie folgt aus:
- eine Verbindung aus einem Verbindungspool anfordern
- eine Transaktion eröffnen
- eine Reihe von SQL-Befehlen ausführen, die in einer Konfigurationsdatei gespeichert sind
- die Transaktion schließen
- die Verbindung an den Pool zurückgeben
Würde unsere Implementierung [DaoImplCommon] direkt mit [iBATIS] arbeiten, müsste sie diese Abfolge wiederholt ausführen. Nur Schritt 3 ist spezifisch für die Schicht [dao], die übrigen Schritte sind generisch. Die Spring-Klasse [SqlMapClientDaoSupport] übernimmt die Vorgänge 1, 2, 4 und 5 selbst und delegiert den Vorgang 3 an ihre abgeleitete Klasse, in diesem Fall die Klasse [DaoImplCommon].
Um funktionieren zu können, benötigt die Klasse [SqlMapClientDaoSupport] eine Referenz auf das Objekt iBATIS [SqlMapClient sqlMapClient], das die Kommunikation mit der Datenbank übernimmt. Dieses Objekt benötigt zwei Dinge, um zu funktionieren:
- ein mit der Datenbank verbundenes Objekt [DataSource], bei dem es Verbindungen anfordert
- eine (oder mehrere) Konfigurationsdatei(en), in der (denen) die auszuführenden SQL-Befehle ausgelagert sind. Diese Befehle sind nämlich nicht im Java-Code enthalten. Sie werden durch einen Code in einer Konfigurationsdatei identifiziert, und das Objekt [SqlMapClient sqlMapClient] verwendet diesen Code, um einen bestimmten Befehl SQL ausführen zu lassen.
Ein erster Entwurf für die Konfiguration unserer Schicht [dao], der die oben dargestellte Architektur widerspiegeln würde, sähe wie folgt aus:
<!-- Zugriffsklassen auf die Ebene [dao] -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
Hier wird die Eigenschaft [sqlMapClient] (Zeile 3) der Klasse [DaoImplCommon] (Zeile 2) initialisiert. Dies geschieht durch die Methode [setSqlMapClient] der Klasse [DaoImpl]. Diese Klasse verfügt nicht über diese Methode. Es ist ihre übergeordnete Klasse [SqlMapClientDaoSupport], die sie besitzt. Daher wird hier tatsächlich diese Klasse initialisiert.
Nun zu Zeile 4: Hier wird auf ein Objekt namens „sqlMapClient“ verwiesen, das noch erstellt werden muss. Dieses ist, wie bereits erwähnt, vom Typ [SqlMapClient], ein Typ [iBATIS]:

[SqlMapClient] ist eine Schnittstelle. Spring stellt die Klasse [SqlMapClientFactoryBean] bereit, um ein Objekt zu erhalten, das diese Schnittstelle implementiert:

Zur Erinnerung: Wir möchten ein Objekt instanziieren, das die Schnittstelle [SqlMapClient] implementiert. Dies ist bei der Klasse [SqlMapClientFactoryBean] offenbar nicht der Fall. Diese implementiert die Schnittstelle [FactoryBean] (siehe oben). Diese verfügt über die folgende Methode [getObject()]:
![]()
Wenn man Spring um eine Instanz eines Objekts bittet, das die Schnittstelle [FactoryBean] implementiert, dann:
- eine Instanz [I] der Klasse erstellen – hier erstellt es eine Instanz vom Typ [SqlMapClientFactoryBean].
- gibt der aufrufenden Methode das Ergebnis der Methode [I] zurück.getObject() – die Methode [SqlMapClientFactoryBean].getObject() gibt hier ein Objekt zurück, das die Schnittstelle [SqlMapClient] implementiert.
Um ein Objekt zurückgeben zu können, das die Schnittstelle [SqlMapClient] implementiert, benötigt die Klasse [SqlMapClientFactoryBean] zwei für dieses Objekt erforderliche Informationen:
- ein mit der Datenbank verbundenes Objekt vom Typ [DataSource], bei dem es Verbindungen anfordern wird
- eine (oder mehrere) Konfigurationsdatei(en), in der (denen) die auszuführenden SQL-Befehle extern gespeichert sind
Die Klasse [SqlMapClientFactoryBean] verfügt über die set-Methoden zur Initialisierung dieser beiden Eigenschaften:

Wir kommen voran … Unsere Konfigurationsdatei nimmt Gestalt an und sieht nun wie folgt aus:
<!-- 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>
<!-- Zugriffsklassen auf die Ebene [dao] -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
- Zeilen 2–3: Der Bean „sqlMapClient“ ist vom Typ [SqlMapClientFactoryBean]. Aus den vorangegangenen Erläuterungen wissen wir: Wenn wir Spring um eine Instanz dieser Bean bitten, erhalten wir ein Objekt, das die Schnittstelle iBATIS [SqlMapClient] implementiert. Dieses Objekt wird also in Zeile 14 zurückgegeben.
- Zeilen 7–9: Wir geben an, dass die für das Objekt iBATIS [SqlMapClient] erforderliche Konfigurationsdatei den Namen „sql-map-config-firebird.xml“ trägt und im Verzeichnis ClassPath der Anwendung gesucht werden muss. Hier wird die Methode [SqlMapClientFactoryBean].setConfigLocation verwendet.
- Zeilen 4–6: Wir initialisieren die Eigenschaft [dataSource] von [SqlMapClientFactoryBean] mit dessen Methode [setDataSource].
In Zeile 5 verweisen wir auf eine Bean namens „dataSource“, die noch erstellt werden muss. Betrachtet man den von der Methode [setDataSource] von [SqlMapClientFactoryBean] erwarteten Parameter, so sieht man, dass er vom Typ [DataSource] ist:

Wir haben es erneut mit einer Schnittstelle zu tun, für die wir eine Implementierungsklasse finden müssen. Die Aufgabe einer solchen Klasse besteht darin, einer Anwendung auf effiziente Weise Verbindungen zu einer bestimmten Datenbank bereitzustellen. Ein SGBD kann nicht gleichzeitig eine große Anzahl von Verbindungen offen halten. Um die Anzahl der zu einem bestimmten Zeitpunkt offenen Verbindungen zu verringern, müssen wir bei jedem Datenaustausch mit der Datenbank:
- eine Verbindung öffnen
- eine Transaktion starten
- SQL-Befehle ausführen
- die Transaktion schließen
- die Verbindung schließen
Das wiederholte Öffnen und Schließen von Verbindungen ist zeitaufwendig. Um diese beiden Probleme zu lösen (sowohl die Anzahl der zu einem bestimmten Zeitpunkt offenen Verbindungen zu begrenzen als auch den Aufwand für das Öffnen und Schließen dieser Verbindungen zu minimieren), gehen Klassen, die die Schnittstelle [DataSource] implementieren, häufig wie folgt vor:
- Sie öffnen unmittelbar nach ihrer Instanziierung N Verbindungen zur Ziel-Datenbank. N hat in der Regel einen Standardwert und kann meist in einer Konfigurationsdatei festgelegt werden. Diese N Verbindungen bleiben die ganze Zeit über offen und bilden einen Pool verfügbarer Verbindungen für die Threads der Anwendung.
- Wenn ein Anwendungs-Thread eine Verbindung anfordert, weist ihm das Objekt [DataSource] eine der N beim Start geöffneten Verbindungen zu, sofern noch welche verfügbar sind. Wenn die Anwendung die Verbindung schließt, wird diese in Wirklichkeit nicht geschlossen, sondern lediglich wieder in den Pool der verfügbaren Verbindungen zurückgeführt.
Es gibt verschiedene frei verfügbare Implementierungen der Schnittstelle [DataSource]. Wir verwenden hier die Implementierung [commons DBCP], die unter der URL [http://jakarta.apache.org/commons/dbcp/] verfügbar ist:

Für die Verwendung des Tools [commons DBCP] sind zwei Archive [commons-dbcp, commons-pool] erforderlich, die beide im Ordner [lib] des Projekts abgelegt wurden:
![]() |
Die Klasse [BasicDataSource] von [commons DBCP] stellt die von uns benötigte Implementierung [DataSource] bereit:

Diese Klasse stellt uns einen Verbindungspool für den Zugriff auf die Firebird-Datenbank [dbpersonnes.gdb] unserer Anwendung zur Verfügung. Dazu müssen wir ihr die Informationen bereitstellen, die sie zum Anlegen der Verbindungen im Pool benötigt:
- den Namen des zu verwendenden Treibers JDBC – initialisiert mit [setDriverClassName]
- den Namen der URL der zu verwendenden Datenbank – initialisiert mit [setUrl]
- die Benutzer-ID des Eigentümers der Verbindung – initialisiert mit [setUsername] (und nicht mit setUserName, wie man vielleicht erwarten würde)
- sein Passwort – initialisiert mit [setPassword]
Die Konfigurationsdatei unserer Schicht [dao] könnte wie folgt aussehen:
<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- die Datenquelle DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>org.firebirdsql.jdbc.FBDriver</value>
</property>
<!-- Achtung: Zwischen den beiden <value>-Tags der URL dürfen keine Leerzeichen stehen -->
<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>
<!-- Zugriffsklasse auf die Schicht [dao] -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
</beans>
- Zeilen 7–9: der Name des Firebird-Treibers JDBC
- Zeilen 11–13: Die URL der Firebird-Datenbank [dbpersonnes.gdb]. Achten Sie besonders auf die Schreibweise dieser URL. Zwischen den Tags <value> und der URL darf kein Leerzeichen stehen.
- Zeilen 14–16: Der Eigentümer der Verbindung – hier [sysdba], der Standardadministrator der Firebird-Distributionen
- Zeilen 17–19: sein Passwort [masterkey] – ebenfalls der Standardwert
Wir sind schon weit gekommen, aber es gibt noch einige Konfigurationspunkte zu klären: Zeile 28 verweist auf die Datei [sql-map-config-firebird.xml], die den Client [SqlMapClient] von iBATIS konfigurieren soll. Bevor wir uns mit ihrem Inhalt befassen, wollen wir zunächst den Speicherort dieser Konfigurationsdateien in unserem Eclipse-Projekt zeigen:

- [spring-config-test-dao-firebird.xml] ist die Konfigurationsdatei der Ebene [dao], die wir gerade untersucht haben
- [sql-map-config-firebird.xml] wird von [spring-config-test-dao-firebird.xml] referenziert. Wir werden sie nun untersuchen.
- [personnes-firebird.xml] wird von [sql-map-config-firebird.xml] referenziert. Wir werden sie untersuchen.
Die drei oben genannten Dateien befinden sich im Ordner „[src]“. In Eclipse bedeutet dies, dass sie bei der Ausführung im Ordner „[bin]“ des Projekts vorhanden sein werden (oben nicht dargestellt). Dieser Ordner ist Teil des Ordners „ClassPath“ der Anwendung. Letztendlich werden die drei oben genannten Dateien also tatsächlich im Ordner „ClassPath“ der Anwendung vorhanden sein. Dies ist erforderlich.
Die Datei [sql-map-config-firebird.xml] lautet wie folgt:
<?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>
- Diese Datei muss <sqlMapConfig> als Stamm-Tag enthalten (Zeilen 6 und 8)
- Zeile 7: Das Tag <sqlMap> dient dazu, die Dateien zu kennzeichnen, die die auszuführenden Befehle SQL enthalten. Häufig, aber nicht zwingend, gibt es eine Datei pro Tabelle. So lassen sich die Befehle SQL für eine bestimmte Tabelle in einer einzigen Datei zusammenfassen. Häufig gibt es jedoch Befehle SQL, die mehrere Tabellen betreffen. In diesem Fall trifft die vorstehende Aufteilung nicht zu. Man muss lediglich beachten, dass alle Dateien, die durch die Tags <sqlMap> gekennzeichnet sind, zusammengeführt werden. Diese Dateien werden in der Datei ClassPath der Anwendung gesucht.
Die Datei [personnes-firebird.xml] beschreibt die Befehle SQL, die an die Tabelle [PERSONNES] der Firebird-Datenbank [dbpersonnes.gdb] gesendet werden. Ihr Inhalt lautet wie folgt:
<?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 der Klasse [Personne] -->
<typeAlias alias="Personne.classe"
type="istia.st.mvc.personnes.entites.Personne"/>
<!-- Zuordnungstabelle [PERSONNES] – Objekt [Personne] -->
<resultMap id="Personne.map"
class="Personne.classe">
<result property="id" column="ID" />
<result property="version" column="VERSION" />
<result property="nom" column="NOM"/>
<result property="prenom" column="PRENOM"/>
<result property="dateNaissance" column="DATENAISSANCE"/>
<result property="marie" column="MARIE"/>
<result property="nbEnfants" column="NBENFANTS"/>
</resultMap>
<!-- Liste aller Personen -->
<select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM,
PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
<!-- eine bestimmte Person abrufen -->
<select id="Personne.getOne" resultMap="Personne.map" >select ID, VERSION, NOM,
PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES WHERE ID=#Wert#</select>
<!-- eine Person hinzufügen -->
<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#, #Nachname#, #Vorname#, #dateNaissance#, #verheiratet#,
#nbEnfants#) </insert>
<!-- Person aktualisieren -->
<update id="Personne.updateOne" parameterClass="Personne.classe"> update
PERSONNES set VERSION=#Version#+1, NOM=#Nachname#, PRENOM=#Vorname#, DATENAISSANCE=#dateNaissance#,
MARIE=#Ehepartner#, NBENFANTS=#nbEnfants# WHERE ID=#id# und
VERSION=#Version#</update>
<!-- eine Person löschen -->
<delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE
ID=#Wert# </delete>
</sqlMap>
- Die Datei muss <sqlMap> als Stamm-Tag enthalten (Zeilen 7 und 45)
- Zeilen 9–10: Um das Schreiben der Datei zu vereinfachen, wird der Klasse [istia.st.springmvc.personnes.entites.Personne] der Alias (Synonym) [Personne.classe] zugewiesen.
- Zeilen 12–21: Legen die Zuordnungen zwischen den Spalten der Tabelle [PERSONNES] und den Feldern des Objekts [Personne] fest.
- Zeilen 23–24: Die Reihenfolge SQL [select], um alle Personen aus der Tabelle [PERSONNES] abzurufen
- Zeilen 26–27: Die Anweisung SQL [select], um eine bestimmte Person aus der Tabelle [PERSONNES] abzurufen
- Zeilen 29–36: Der Befehl SQL [insert], der eine Person in die Tabelle [PERSONNES] einfügt
- Zeilen 38–41: Der Befehl SQL [update], der eine Person in der Tabelle [PERSONNES] aktualisiert
- Zeilen 42–44: Der Befehl SQL [delete], der eine Person aus der Tabelle [PERSONNES] löscht
Die Rolle und die Bedeutung des Inhalts der Datei [personnes-firebird.xml] werden anhand der Klasse [DaoImplCommon] erläutert, die die Schicht [dao] implementiert.
17.3.3. Die Klasse [DaoImplCommon]
Kehren wir zur Architektur des Datenzugriffs zurück:
![]() |
Die Klasse [DaoImplCommon] sieht wie folgt aus:
Wir werden die Methoden nacheinander betrachten.
getAll
Mit dieser Methode lassen sich alle Personen aus der Liste abrufen. Der Code lautet wie folgt:
Erinnern wir uns zunächst daran, dass die Klasse [DaoImplCommon] von der Spring-Klasse [SqlMapClientDaoSupport] abgeleitet ist. Diese Klasse enthält die Methode [getSqlMapClientTemplate()], die in Zeile 3 oben verwendet wird. Diese Methode hat die folgende Signatur:
![]()
Der Typ [SqlMapClientTemplate] kapselt das Objekt [SqlMapClient] der Schicht [iBATIS]. Über ihn erfolgt der Zugriff auf die Datenbank. Der Typ [iBATIS] SqlMapClient könnte direkt verwendet werden, da die Klasse [SqlMapClientDaoSupport] darauf zugreifen kann:
![]()
Der Nachteil der Klasse [iBATIS] SqlMapClient besteht darin, dass sie Ausnahmen vom Typ [SQLException] auslöst, einen kontrollierten Ausnahmetyp, c.a.d. Diese muss entweder durch ein try/catch-Block abgefangen oder in der Signatur der Methoden, die sie auslösen, deklariert werden. Wir sollten uns jedoch daran erinnern, dass die Schicht [dao] eine Schnittstelle [IDao] implementiert, deren Methoden in ihren Signaturen keine Ausnahmen enthalten. Die Methoden der Klassen, die die Schnittstelle [IDao] implementieren, dürfen daher ebenfalls keine Ausnahmen in ihren Signaturen enthalten. Wir müssen daher jede von der Schicht [iBATIS] ausgelöste Ausnahme vom Typ [SQLException] abfangen und in eine unkontrollierte Ausnahme kapseln. Der Typ [DaoException] aus unserem Projekt wäre für diese Kapselung geeignet.
Anstatt diese Ausnahmen selbst zu behandeln, werden wir sie dem Spring-Typ [SqlMapClientTemplate] übergeben, der das Objekt [SqlMapClient] der Schicht [iBATIS] kapselt. Tatsächlich wurde [SqlMapClientTemplate] entwickelt, um die von der Schicht [SqlMapClient] ausgelösten Ausnahmen vom Typ [SQLException] abzufangen und sie in einen unkontrollierten Typ [DataAccessException] zu kapseln. Dieses Verhalten kommt uns entgegen. Man sollte lediglich beachten, dass die Schicht [dao] nun zwei Arten von unkontrollierten Ausnahmen auslösen kann:
- unseren proprietären Typ [DaoException]
- den Spring-Typ [DataAccessException]
Der Typ [SqlMapClientTemplate] ist wie folgt definiert:

Er implementiert die folgende Schnittstelle [SqlMapClientOperations]:

Diese Schnittstelle definiert Methoden, mit denen der Inhalt der Datei [personnes-firebird.xml] verarbeitet werden kann:
[queryForList]
![]()
Mit dieser Methode kann ein Auftrag [SELECT] erteilt und das Ergebnis in Form einer Objektliste abgerufen werden:
- [statementName]: die Kennung (ID) des Auftrags [select] in der Konfigurationsdatei
- [parameterObject]: das Objekt „Parameter“ für einen parametrisierten Befehl [select]. Das Objekt „Parameter“ kann zwei Formen annehmen:
- ein Objekt, das dem Javabean-Standard entspricht: Die Parameter des Befehls [select] sind dann die Namen der Felder des Javabeans. Bei der Ausführung des Befehls [select] werden sie durch die Werte dieser Felder ersetzt.
- ein Dictionary: Die Parameter des Befehls [select] sind dann die Schlüssel des Dictionaries. Bei der Ausführung des Befehls [select] werden diese durch ihre zugehörigen Werte im Dictionary ersetzt.
- Wenn [SELECT] keine Zeilen zurückgibt, ist das Ergebnis [List] ein leeres Objekt, null jedoch nicht (zu überprüfen).
[queryForObject]
![]()
Diese Methode entspricht im Grundsatz der vorherigen, gibt jedoch nur ein einziges Objekt zurück. Wenn [SELECT] keine Zeile zurückgibt, ist das Ergebnis der Zeiger null.
[insert]
![]()
Mit dieser Methode kann ein Befehl SQL [insert] ausgeführt werden, der durch den zweiten Parameter parametrisiert ist. Das Rückgabeobjekt ist der Primärschlüssel der Zeile, die eingefügt wurde. Es besteht keine Verpflichtung, dieses Ergebnis zu verwenden.
[update]
![]()
Mit dieser Methode kann ein Befehl SQL [update] ausgeführt werden, der durch den zweiten Parameter parametrisiert wird. Das Ergebnis ist die Anzahl der Zeilen, die durch den Befehl SQL [update] geändert wurden.
[delete]
![]()
Mit dieser Methode kann ein Befehl SQL [delete] ausgeführt werden, der durch den zweiten Parameter parametrisiert ist. Das Ergebnis ist die Anzahl der Zeilen, die durch den Befehl SQL [delete] gelöscht wurden.
Kehren wir zur Methode [getAll] der Klasse [DaoImplCommon] zurück:
- Zeile 4: Der Befehl [select] mit dem Namen „Personne.getAll“ wird ausgeführt. Er ist nicht parametrisiert, daher ist das Objekt „Parameter“ null.
In [personnes-firebird.xml] lautet der Befehl [select] mit dem Namen „Personne.getAll“ wie folgt:
<?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 der Klasse [Personne] -->
<typeAlias alias="Personne.classe"
type="istia.st.mvc.personnes.entites.Personne"/>
<!-- Zuordnungstabelle [PERSONNES] – Objekt [Personne] -->
<resultMap id="Personne.map"
class="Personne.classe">
<result property="id" column="ID" />
<result property="version" column="VERSION" />
<result property="nom" column="NOM"/>
<result property="prenom" column="PRENOM"/>
<result property="dateNaissance" column="DATENAISSANCE"/>
<result property="marie" column="MARIE"/>
<result property="nbEnfants" column="NBENFANTS"/>
</resultMap>
<!-- Liste aller Personen -->
<select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM,
PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
...
</sqlMap>
- Zeile 23: Der Befehl SQL „Personne.getAll“ ist nicht parametrisiert (keine Parameter im Abfragetext).
- Zeile 3 der Methode [getAll] fordert die Ausführung der Abfrage [select] mit dem Namen „Personne.getAll“ an. Diese wird nun ausgeführt. [iBATIS] stützt sich auf JDBC. Daher ist bekannt, dass das Ergebnis der Abfrage in Form eines Objekts [ResultSet] zurückgegeben wird. In Zeile 23 gibt das Attribut [resultMap] des Tags <select> an, welches „resultMap “ es verwenden soll, um jede Zeile des erhaltenen [ResultSet]-Objekts umzuwandeln. Es ist das „resultMap“ [Personne.map], das in den Zeilen 12–21 definiert ist und angibt, wie von einer Zeile der Tabelle [PERSONNES] zu einem Objekt vom Typ [Personne] übergegangen wird. [iBATIS] nutzt diese Zuordnungen, um aus den Zeilen des Objekts [ResultSet] eine Liste von Objekten vom Typ [Personne] zu erstellen.
- Zeile 3 der Methode [getAll] gibt dann eine Sammlung von Objekten vom Typ [Personne] zurück.
- Die Methode [queryForList] kann eine Spring-Ausnahme [DataAccessException] auslösen. Wir lassen diese nach oben weiterleiten.
Die übrigen Methoden der Klasse [AbstractDaoImpl] erläutern wir nur kurz, da das Wesentliche zur Verwendung von [iBATIS] bereits bei der Betrachtung der Methode [getAll] gesagt wurde.
getOne
Mit dieser Methode lässt sich eine Person anhand ihres [id] identifizieren. Der Code lautet wie folgt:
- Zeile 4: Fordert die Ausführung des Befehls [select] mit dem Namen „Personne.getOne“ an. Dieser lautet in der Datei [personnes-firebird.xml] wie folgt:
<!-- eine bestimmte Person abrufen -->
<select id="Personne.getOne" resultMap="Personne.map" parameterClass="int">
select ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM
PERSONNES WHERE ID=#Wert#</select>
Der Befehl SQL wird durch den Parameter #value# (Zeile 4) konfiguriert. Das Attribut #value# bezeichnet den Wert des Parameters, der an den Befehl SQL übergeben wird, sofern dieser Parameter vom Typ „einfach“ ist: Integer, Double, String, … In den Attributen des Tags <select> gibt das Attribut [parameterClass] an, dass der Parameter vom Typ „Ganzzahl“ ist (Zeile 2). In Zeile 5 von [getOne] ist zu sehen, dass dieser Parameter die Kennung der gesuchten Person in Form eines Integer-Objekts ist. Diese Typänderung ist zwingend erforderlich, da der zweite Parameter von [queryForList] vom Typ [Object] sein muss.
Das Ergebnis der Abfrage [select] muss über das Attribut [resultMap="Personne.map"] (Zeile 2) in ein Objekt umgewandelt werden. Man erhält somit den Typ [Personne].
- Zeilen 7–11: Wenn die Abfrage [select] keine Zeilen zurückgegeben hat, wird der Zeiger null aus Zeile 4 abgerufen. Das bedeutet, dass die gesuchte Person nicht gefunden wurde. In diesem Fall wird eine Abfrage mit dem Code 2 (Zeilen 9–10) gestartet: [DaoException].
- Zeile 13: Wenn keine Ausnahme aufgetreten ist, wird das angeforderte Objekt [Personne] zurückgegeben.
deleteOne
Mit dieser Methode kann eine Person gelöscht werden, die durch ihre [id] identifiziert wird. Der Code lautet wie folgt:
- Zeilen 4–5: Fordert die Ausführung des Befehls [delete] mit dem Namen „Personne.deleteOne“ an. Dieser lautet in der Datei [personnes-firebird.xml] wie folgt:
<!-- eine Person löschen -->
<delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE
ID=#Wert# </delete>
Der Befehl SQL wird durch den Parameter #value# (Zeile 3) vom Typ [parameterClass="int"] (Zeile 2) parametrisiert. Dies ist die Kennung der gesuchten Person (Zeile 5 von deleteOne)
- Zeile 4: Das Ergebnis der Methode [SqlMapClientTemplate].delete ist die Anzahl der gelöschten Zeilen.
- Zeilen 7–8: Wenn die Abfrage [delete] keine Zeilen gelöscht hat, bedeutet dies, dass die Person nicht existiert. Es wird eine [DaoException] mit Code 2 (Zeile 8) gestartet.
saveOne
Mit dieser Methode kann eine neue Person hinzugefügt oder eine bestehende Person geändert werden. Der Code lautet wie folgt:
- Zeile 4: Die Gültigkeit der Person wird mit der Methode [check] überprüft. Diese Methode war bereits in der vorherigen Version vorhanden und war damals auskommentiert. Sie löst eine [DaoException] aus, wenn die Person ungültig ist. Diese wird weitergeleitet.
- Zeile 6: Wenn es bis hierher gelangt ist, liegt keine Ausnahme vor. Die Person ist also gültig.
- Zeilen 6–11: Je nach ID der Person handelt es sich um einen Neuzug (ID = -1) oder eine Aktualisierung (ID ≠ -1). In beiden Fällen werden zwei klasseninterne Methoden aufgerufen:
- insertPersonne: für das Hinzufügen
- updatePersonne: für die Aktualisierung
insertPersonne
Mit dieser Methode kann eine neue Person hinzugefügt werden. Der Code lautet wie folgt:
- Zeile 4: Die Versionsnummer der Person, die gerade angelegt wird, wird auf 1 gesetzt
- Zeile 9: Der Eintrag erfolgt über die Abfrage „Personne.insertOne“, die wie folgt lautet:
<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#, #nachname#, #vorname#, #dateNaissance#, #marie#,
#nbEnfants#) </insert>
Es handelt sich um eine parametrisierte Abfrage, deren Parameter vom Typ [Personne] ist (parameterClass = „Personne.classe“, Zeile 1). Die Felder des Objekts [Personne], die als Parameter übergeben werden (Zeile 9 von insertPersonne), werden verwendet, um die Spalten der Zeile zu füllen, die in die Tabelle [PERSONNES] eingefügt werden soll (Zeilen 5–8). Es gibt ein Problem, das gelöst werden muss. Bei einem Einfügevorgang hat das einzufügende Objekt [Personne] die ID -1. Dieser Wert muss durch einen gültigen Primärschlüssel ersetzt werden. Dazu werden die Zeilen 2–4 des oben genannten Tags <selectKey> verwendet. Sie lauten:
- (Fortsetzung)
- Die Abfrage SQL, die ausgeführt werden muss, um einen Primärschlüsselwert zu erhalten. Die hier angegebene Abfrage ist diejenige, die wir in Abschnitt 17.1 vorgestellt haben. Zwei Punkte sind zu beachten:
- „as 'value'“ ist obligatorisch. Man könnte auch „as value“ schreiben, aber „value“ ist ein Firebird-Schlüsselwort, das daher in Anführungszeichen gesetzt werden musste.
- Die Firebird-Tabelle heißt eigentlich [RDB$DATABASE]. Das Zeichen $ wird jedoch von [iBATIS] interpretiert. Es wurde durch doppelte Schreibweise geschützt.
- Das Feld des Objekts [Personne], das mit dem durch den Befehl [SELECT] abgerufenen Wert initialisiert werden muss, ist hier das Feld [id]. Dieses Feld wird durch das Attribut [keyProperty] in Zeile 2 angegeben.
- Die Abfrage SQL, die ausgeführt werden muss, um einen Primärschlüsselwert zu erhalten. Die hier angegebene Abfrage ist diejenige, die wir in Abschnitt 17.1 vorgestellt haben. Zwei Punkte sind zu beachten:
- Zeilen 6–7: Für Testzwecke werden wir 10 ms warten, bevor wir den Eintrag vornehmen, um zu prüfen, ob es Konflikte zwischen Threads gibt, die gleichzeitig Einträge vornehmen möchten.
updatePersonne
Mit dieser Methode lässt sich eine bereits in der Tabelle [PERSONNES] vorhandene Person ändern. Der Code lautet wie folgt:
- Eine Aktualisierung kann aus mindestens zwei Gründen fehlschlagen:
- Die zu aktualisierende Person existiert nicht
- Die zu aktualisierende Person existiert zwar, aber der Thread, der sie ändern möchte, verfügt nicht über die richtige Version
- Zeilen 7–8: Die Abfrage SQL [update] mit dem Namen „Personne.updateOne“ wird ausgeführt. Sie lautet wie folgt:
<!-- Person aktualisieren -->
<update id="Personne.updateOne" parameterClass="Personne.classe"> update
PERSONNES set VERSION=#Version#+1, NOM=#Nachname#, PRENOM=#Vorname#, DATENAISSANCE=#dateNaissance#,
MARIE=#Ehepartner#, NBENFANTS=#nbEnfants# WHERE ID=#id# und
VERSION=#Version#</update>
- (Fortsetzung)
- Zeile 2: Die Abfrage wird parametrisiert und akzeptiert als Parameter einen Typ [Personne] (parameterClass = „Personne.classe“). Dabei handelt es sich um die zu ändernde Person (Zeile 8 – updatePersonne).
- Es soll nur die Person aus der Tabelle [PERSONNES] geändert werden, die dieselbe Nummer [id] und dieselbe Version [version] wie der Parameter hat. Deshalb gibt es die Einschränkung [WHERE ID=#id# and VERSION=#version#]. Wird diese Person gefunden, wird sie mit der Parameterperson aktualisiert und ihre Version wird um 1 erhöht (Zeile 3 oben).
- Zeile 9: Die Anzahl der aktualisierten Zeilen wird ermittelt.
- Zeilen 10–11: Ist diese Zahl gleich Null, wird ein [DaoException] mit Code 2 ausgelöst, was darauf hinweist, dass entweder die zu aktualisierende Person nicht existiert oder sich ihre Version in der Zwischenzeit geändert hat.
17.4. Tests der Schicht [dao]
17.4.1. Tests der Implementierung von [DaoImplCommon]
Nachdem wir nun die Schicht [dao] geschrieben haben, wollen wir sie mit den Tests JUnit testen:

Bevor wir intensive Tests durchführen, können wir zunächst mit einem einfachen Programm vom Typ [main] beginnen, das den Inhalt der Tabelle [PERSONNES] anzeigt. Dabei handelt es sich um die Klasse [MainTestDaoFirebird]:
Die Konfigurationsdatei [spring-config-test-dao-firebird.xml] der Schicht [dao], die in den Zeilen 13–14 verwendet wird, lautet wie folgt:
<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- Datenquelle DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>org.firebirdsql.jdbc.FBDriver</value>
</property>
<!-- Achtung: Zwischen den beiden <value>-Tags dürfen keine Leerzeichen stehen -->
<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>
<!-- Zugriffsklasse für die Ebene [dao] -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
</beans>
Diese Datei wurde in Abschnitt 17.3.2 behandelt.
Zum Test wird Firebird mit der Datei SGBD gestartet. Der Inhalt der Tabelle [PERSONNES] lautet wie folgt:

Die Ausführung des Programms [MainTestDaoFirebird] liefert folgende Bildschirmausgaben:

Die Liste der Personen wurde erfolgreich ausgegeben. Wir können nun mit dem Test JUnit fortfahren.
Der Test JUnit [TestDaoFirebird] lautet wie folgt:
- Die Tests [test1] bis [test5] sind dieselben wie in Version 1, mit Ausnahme von [test4], der leicht angepasst wurde. Der Test [test6] ist hingegen neu. Wir gehen nur auf diese beiden Tests ein.
[test4]
[test4] dient dazu, die Methode [updatePersonne - DaoImplCommon] zu testen. Hier noch einmal der Code dieser Methode:
- Zeilen 4–5: Es wird 10 ms gewartet. Dadurch wird der Thread, der [updatePersonne] ausführt, gezwungen, die CPU-Kontrolle abzugeben, was die Wahrscheinlichkeit erhöht, dass es zu Zugriffskonflikten zwischen konkurrierenden Threads kommt.
[test4] startet N=100 Threads, die gleichzeitig die Anzahl der Kinder derselben Person um 1 erhöhen sollen. Wir wollen beobachten, wie Versionskonflikte und Zugriffskonflikte behandelt werden.
Die Threads werden in den Zeilen 8–13 erstellt. Jeder erhöht die Anzahl der Kinder der in den Zeilen 3–5 erstellten Person um 1. Die Aktualisierungs-Threads [ThreadDaoMajEnfants ] lauten wie folgt:
Eine Aktualisierung einer Person kann fehlschlagen, weil die Person, die geändert werden soll, nicht existiert oder bereits zuvor von einem anderen Thread aktualisiert wurde. Diese beiden Fälle werden hier in den Zeilen 67–69 behandelt. In beiden Fällen löst die Methode [updatePersonne] einen [DaoException] mit dem Code 2 aus. Der Thread wird dann zurückgesetzt, um den Aktualisierungsvorgang von Anfang an erneut zu starten (while-Schleife, Zeile 34).
[test6]
[test6] dient dazu, die Methode [insertPersonne - DaoImplCommon] zu testen. Hier noch einmal der Code dieser Methode:
- Zeilen 6–7: Es wird 10 ms gewartet, um den Thread, der [insertPersonne] ausführt, dazu zu zwingen, die Prozessorsteuerung abzugeben, und so die Wahrscheinlichkeit zu erhöhen, dass Konflikte auftreten, die durch Threads verursacht werden, die gleichzeitig Einfügungen vornehmen.
Der Code von [test6] lautet wie folgt:
Es werden 100 Threads erstellt, die gleichzeitig 100 verschiedene Personen einfügen. Diese 100 Threads erhalten alle einen Primärschlüssel für die Person, die sie einfügen sollen, und werden dann für 10 ms unterbrochen (Zeile 10 – insertPersonne), bevor sie ihre Einfügung vornehmen können. Wir wollen überprüfen, ob alles ordnungsgemäß abläuft und insbesondere, ob sie tatsächlich unterschiedliche Primärschlüsselwerte erhalten.
- Zeilen 7–11: Es wird ein Array mit 100 Personen erstellt. Diese Personen sind allesamt Kopien der in den Zeilen 4–5 erstellten Person p.
- Zeilen 14–17: Die 100 Einfüge-Threads werden gestartet. Jeder von ihnen hat die Aufgabe, eine der zuvor erstellten 100 Personen einzufügen.
- Zeilen 19–23: [test6] wartet auf das Ende jedes der 100 von ihm gestarteten Threads. Sobald er das Ende des Threads Nr. i festgestellt hat, löscht er die Person, die dieser Thread gerade eingefügt hat.
Der Einfüge-Thread [ThreadDaoInsertPersonne] sieht wie folgt aus:
- Zeilen 19–22: Der Thread-Konstruktor speichert die Person, die eingefügt werden soll, sowie die Ebene [dao], die für diese Einfügung verwendet werden soll.
- Zeile 30: Die Person wird eingefügt. Tritt eine Ausnahme auf, wird diese an [test6] weitergeleitet.
Tests
Bei den Tests erhält man folgende Ergebnisse:
![]() |
Der Test [test4] schlägt also fehl. Die Anzahl der Kinder ist auf 69 gesunken, statt der erwarteten 100. Was ist passiert? Sehen wir uns die Bildschirmprotokolle an. Sie zeigen, dass von Firebird Ausnahmen ausgelöst wurden:
Exception in thread "Thread-62" org.springframework.jdbc.UncategorizedSQLException: SqlMapClient operation; uncategorized SQLException for SQL []; SQL state [HY000]; error code [335544336];
--– Der Fehler trat in personnes-firebird.xml auf.
--– Der Fehler trat beim Anwenden einer Parameterzuordnung auf.
--– Überprüfen Sie die Datei Personne.updateOne-InlineParameterMap.
--– Überprüfen Sie die Anweisung (Aktualisierung fehlgeschlagen).
--– Ursache: org.firebirdsql.jdbc.FBSQLException: GDS-Ausnahme. 335544336. Deadlock
update conflicts with concurrent update; nested exception is com.ibatis.common.jdbc.exception.NestedSQLException:
--– Der Fehler trat in personnes-firebird.xml auf.
--– Der Fehler trat beim Anwenden einer Parameterzuordnung auf.
- Zeile 1 – Es ist eine Spring-Ausnahme vom Typ [org.springframework.jdbc.UncategorizedSQLException] aufgetreten. Dabei handelt es sich um eine unkontrollierte Ausnahme, die verwendet wurde, um eine vom Firebird-Treiber JDBC ausgelöste Ausnahme zu kapseln, die in Zeile 6 beschrieben ist.
- Zeile 6 – Der Firebird-Treiber JDBC hat eine Ausnahme vom Typ [org.firebirdsql.jdbc.FBSQLException] mit dem Fehlercode 335544336 ausgelöst.
- Zeile 7: Weist darauf hin, dass es zu einem Zugriffskonflikt zwischen zwei Threads gekommen ist, die gleichzeitig dieselbe Zeile der Tabelle [PERSONNES] aktualisieren wollten.
Dies ist kein nicht behebbarer Fehler. Der Thread, der diese Ausnahme abfängt, kann die Aktualisierung erneut versuchen. Dazu muss der Code von [ThreadDaoMajEnfants] geändert werden:
- Zeile 8: Hier wird eine Ausnahme vom Typ [DaoException] behandelt. Nach den bisherigen Ausführungen müssten wir die Ausnahme behandeln, die bei den Tests aufgetreten ist, nämlich den Typ [org.springframework.jdbc.UncategorizedSQLException]. Es reicht jedoch nicht aus, nur diesen Typ zu behandeln, da es sich um einen generischen Spring-Typ handelt, der dazu dient, ihm unbekannte Ausnahmen zu kapseln. Spring kennt die Ausnahmen, die von den Treibern JDBC einer Reihe von SGBD wie Oracle, MySQL, Postgres, DB2, SQL Server usw. – jedoch nicht die von Firebird. Daher wird jede vom Firebird-Treiber JDBC ausgelöste Ausnahme im Spring-Typ [org.springframework.jdbc.UncategorizedSQLException] gekapselt:

Wie oben zu sehen ist, leitet sich die Klasse [UncategorizedSQLException] von der Klasse [DataAccessException] ab, die wir in Abschnitt 17.3.3 erwähnt haben. Die in [UncategorizedSQLException] gekapselte Ausnahme lässt sich anhand ihrer Methode [getSQLException] ermitteln:
![]()
Diese Ausnahme vom Typ [SQLException] wurde von der Schicht [iBATIS] ausgelöst, die ihrerseits die vom Datenbanktreiber JDBC ausgelöste Ausnahme kapselt. Die genaue Ursache der Ausnahme vom Typ [SQLException] lässt sich mit folgender Methode ermitteln:
![]()
Man erhält das Objekt vom Typ [Throwable], das vom Treiber JDBC ausgelöst wurde:

Der Typ [Throwable] ist die übergeordnete Klasse von [Exception].
Hier müssen wir überprüfen, ob das Objekt vom Typ [Throwable], das vom Firebird-Treiber JDBC ausgelöst wurde und Ursache für dieAusnahme [SQLException], die von der Schicht [iBATIS] ausgelöst wurde, tatsächlich eine Ausnahme vom Typ [org.firebirdsql.gds.GDSException] mit dem Fehlercode 335544336 ist. Um den Fehlercode abzurufen, können wir die Methode [getErrorCode()] der Klasse [org.firebirdsql.gds.GDSException] verwenden.
Wenn wir im Code von [ThreadDaoMajEnfants] die Ausnahme [org.firebirdsql.gds.GDSException] verwenden, kann dieser Thread nur mit dem Firebird-Thread SGBD arbeiten. Das Gleiche gilt für den Test [test4], der diesen Thread verwendet. Das wollen wir vermeiden. Denn wir möchten, dass unsere Tests JUnit unabhängig von der verwendeten Instanz SGBD gültig bleiben. Um dieses Ergebnis zu erzielen, beschließen wir, dass die Schicht [dao] einen [DaoException] mit Code 4 startet, sobald eine Ausnahme vom Typ „Aktualisierungskonflikt“ erkannt wird – und zwar unabhängig vom zugrunde liegenden SGBD. Somit lässt sich der Thread [ThreadDaoMajEnfants] wie folgt umschreiben:
- Zeilen 34–36: Die Ausnahme vom Typ [DaoException] mit Code 4 wird abgefangen. Der Thread [ThreadDaoMajEnfants] wird gezwungen, den Aktualisierungsvorgang von vorne zu beginnen (Zeile 10)
Unsere Schicht [dao] muss daher in der Lage sein, eine Ausnahme vom Typ „Aktualisierungskonflikt“ zu erkennen. Diese wird von einem Treiber JDBC ausgelöst und ist für diesen spezifisch. Diese Ausnahme muss in der Methode [updatePersonne] der Klasse [DaoImplCommon] behandelt werden:
Die Zeilen 7–11 müssen von einem try/catch-Block umschlossen werden. Für den Firebird-Treiber SGBD müssen wir überprüfen, ob die Ausnahme, die zum Fehlschlagen der Aktualisierung geführt hat, vom Typ [org.firebirdsql.gds.GDSException] ist und den Fehlercode 335544336 aufweist. Wenn wir diese Art von Test in „[DaoImplCommon]“ einfügen, verknüpfen wir diese Klasse mit dem Firebird-Code „SGBD“, was natürlich nicht wünschenswert ist. Wenn wir die Klasse [DaoImplCommon] allgemein gehalten lassen wollen, müssen wir sie ableiten und die Ausnahme in einer Firebird-spezifischen Klasse behandeln. Genau das tun wir nun.
17.4.2. Die Klasse [DaoImplFirebird]
Ihr Code lautet wie folgt:
- Zeile 5: Die Klasse [DaoImplFirebird] leitet sich von [DaoImplCommon] ab, der Klasse, die wir gerade betrachtet haben. Sie definiert in den Zeilen 8–33 die Methode [updatePersonne] neu, die uns Probleme bereitet.
- Zeile 20: Wir fangen die Spring-Ausnahme vom Typ [UncategorizedSQLException] ab
- Zeilen 21–22: Wir überprüfen, ob die zugrunde liegende Ausnahme vom Typ [SQLException], die von der Schicht [iBATIS] ausgelöst wurde, durch eine Ausnahme vom Typ [org.firebirdsql.jdbc.FBSQLException] verursacht wurde
- Zeile 25: Außerdem wird überprüft, ob der Fehlercode dieser Firebird-Ausnahme 335544336 lautet, also der Fehlercode für ein „Deadlock“.
- Zeilen 26–27: Sind alle diese Bedingungen erfüllt, wird ein [DaoException] mit dem Code 4 ausgelöst.
- Zeilen 36–44: Die Methode [wait] ermöglicht es, den aktuellen Thread für N Millisekunden anzuhalten. Sie dient ausschließlich zu Testzwecken.
Wir sind bereit für die Tests der neuen Schicht [dao].
17.4.3. Tests der Implementierung [DaoImplFirebird]
Die Testkonfigurationsdatei [spring-config-test-dao-firebird.xml] wird geändert, um die Implementierung [DaoImplFirebird] zu verwenden:
<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- die Datenquelle DBCP -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName">
<value>org.firebirdsql.jdbc.FBDriver</value>
</property>
<!-- Achtung: Zwischen den beiden <value>-Tags dürfen keine Leerzeichen stehen -->
<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>
<!-- Zugriffsklasse für die Ebene [dao] -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
</beans>
- Zeile 32: die neue Implementierung [DaoImplFirebird] der Schicht [dao].
Die Ergebnisse des Tests [test4], der zuvor fehlgeschlagen war, lauten wie folgt:

[test4] wurde erfolgreich ausgeführt. Die letzten Zeilen des Bildschirmprotokolls lauten wie folgt:
Die letzte Zeile zeigt an, dass Thread Nr. 36 als letzter beendet wurde. Zeile 3 weist auf einen Versionskonflikt hin, der Thread Nr. 36 dazu zwang, den Aktualisierungsvorgang für die Person erneut zu starten (Zeile 4). Weitere Protokolleinträge zeigen Zugriffskonflikte bei den Aktualisierungen:
Zeile 2 zeigt, dass Thread Nr. 75 bei seiner Aktualisierung aufgrund eines Aktualisierungskonflikts gescheitert ist: Als der Befehl SQL [update] für die Tabelle [PERSONNES] ausgegeben wurde, war die zu aktualisierende Zeile von einem anderen Thread gesperrt. Dieser Zugriffskonflikt zwingt den Thread Nr. 75 dazu, die Aktualisierung erneut zu versuchen.
Abschließend ist bei [test4] ein deutlicher Unterschied zu den Ergebnissen desselben Tests in Version 1 festzustellen, wo dieser aufgrund von Synchronisationsproblemen fehlgeschlagen war. Da die Methoden der Schicht [dao] in Version 1 nicht synchronisiert waren, traten Zugriffskonflikte auf. Hier mussten wir die Schicht [dao] nicht synchronisieren. Wir haben lediglich die von Firebird gemeldeten Zugriffskonflikte behandelt.
Führen wir nun den gesamten Test JUnit der Schicht [dao] aus:

Es scheint also, dass wir eine gültige Schicht [dao] haben. Um sie mit hoher Wahrscheinlichkeit als gültig zu erklären, müssten wir weitere Tests durchführen. Dennoch betrachten wir sie als betriebsbereit.
17.5. Die Schicht [service]
17.5.1. Die Komponenten der Schicht [service]
Die Schicht [service] besteht aus den folgenden Klassen und Schnittstellen:
![]()
- [IService] ist die Schnittstelle, die von der Schicht [service] bereitgestellt wird
- [ServiceImpl] ist eine Implementierung dieser Schnittstelle
Die Schnittstelle [IService] lautet wie folgt:
- Die Schnittstelle verfügt über dieselben vier Methoden wie in Version 1, hat jedoch zwei weitere:
- saveMany: Ermöglicht das atomare Speichern mehrerer Personen gleichzeitig. Entweder werden alle gespeichert oder keine.
- deleteMany: Ermöglicht das atomare Löschen mehrerer Personen gleichzeitig. Entweder werden alle gelöscht oder keine.
Diese beiden Methoden werden von der Webanwendung nicht verwendet. Wir haben sie hinzugefügt, um das Konzept der Transaktion in einer Datenbank zu veranschaulichen. Die beiden Methoden müssen nämlich innerhalb einer Transaktion ausgeführt werden, um die gewünschte Atomizität zu erreichen.
Die Klasse [ServiceImpl], die diese Schnittstelle implementiert, sieht wie folgt aus:
- Die Methoden [getAll, getOne, insertOne, saveOne] greifen auf die gleichnamigen Methoden der Schicht [dao] zurück.
- Zeilen 42–47: Die Methode [saveMany] speichert nacheinander die Personen aus dem als Parameter übergebenen Array.
- Zeilen 50–55: Die Methode [deleteMany] löscht nacheinander die Personen, deren Array als Parameter von der Methode id übergeben wurde.
Wir haben bereits erwähnt, dass die Methoden [saveMany] und [deleteMany] innerhalb einer Transaktion ausgeführt werden müssen, um den „Alles-oder-nichts“-Charakter dieser Methoden zu gewährleisten. Wir können feststellen, dass der obige Code dieses Transaktionskonzept völlig außer Acht lässt. Dieses taucht erst in der Konfigurationsdatei der Schicht [service] auf.
17.5.2. Konfiguration der Schicht [service]
Oben, in Zeile 11, ist zu sehen, dass die Implementierung [ServiceImpl] eine Referenz auf die Schicht [dao] enthält. Diese wird, wie in Version 1, von Spring zum Zeitpunkt der Instanziierung der Schicht [service - ServiceImpl] initialisiert. Die Konfigurationsdatei, die die Instanziierung der Schicht [service] ermöglicht, lautet wie folgt:
<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- die Datenquelle 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">
<!-- Achtung: Zwischen den beiden <value>-Tags dürfen keine Leerzeichen stehen -->
<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>
<!-- Zugriffsklasse für die Schicht [dao] -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
<!-- Transaktionsmanager -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource">
<ref local="dataSource"/>
</property>
</bean>
<!-- Zugriffsklassen auf die Schicht [service] -->
<bean id="service"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager">
<ref local="transactionManager"/>
</property>
<property name="target">
<bean class="istia.st.mvc.personnes.service.ServiceImpl">
<property name="dao">
<ref local="dao"/>
</property>
</bean>
</property>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
<prop key="save*">PROPAGATION_REQUIRED</prop>
<prop key="delete*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
</beans>
- Zeilen 1–36: Konfiguration der Schicht [dao]. Diese Konfiguration wurde bei der Betrachtung der Schicht [dao] in Abschnitt 17.3.2 erläutert.
- Zeilen 38–64: Konfiguration der Schicht [service]
In Zeile 46 ist zu sehen, dass die Implementierung der Schicht [service] durch den Typ [TransactionProxyFactoryBean] erfolgt. Wir hatten erwartet, den Typ [ServiceImpl] zu finden. [TransactionProxyFactoryBean] ist ein vordefinierter Typ von Spring. Wie kann es sein, dass ein vordefinierter Typ die Schnittstelle [IService] implementieren kann, die wiederum spezifisch für unsere Anwendung ist?
Schauen wir uns zunächst die Klasse [TransactionProxyFactoryBean] an:

Wir sehen, dass sie die Schnittstelle [FactoryBean] implementiert. Diese Schnittstelle ist uns bereits bekannt. Wir wissen, dass, wenn eine Anwendung von Spring eine Instanz eines Typs anfordert, der [FactoryBean] implementiert, gibt Spring nicht eine Instanz vom Typ [I] dieses Typs zurück, sondern das Objekt, das von der Methode [I].getObject() zurückgegeben wird:
![]()
In unserem Fall wird die Schicht [service] durch das von [TransactionProxyFactoryBean].getObject() gerenderte Objekt implementiert. Um was für ein Objekt handelt es sich dabei? Wir werden nicht ins Detail gehen, da dies sehr komplex ist. Es handelt sich dabei um das, was man als Spring AOP (Aspect Oriented Programming) bezeichnet. Wir werden versuchen, die Sachverhalte anhand einfacher Schemata zu verdeutlichen. AOP ermöglicht Folgendes:
- Wir haben zwei Klassen, C1 und C2, wobei C1 die von C2 bereitgestellte Schnittstelle [I2] nutzt:
![]() |
- Dank AOP kann für beide Klassen transparent ein Interceptor zwischen den Klassen C1 und C2 eingefügt werden:
![]() |
Die Klasse [C1] wurde so kompiliert, dass sie mit der Schnittstelle [I2] zusammenarbeitet, die von [C2] implementiert wird. Zur Laufzeit fügt AOP die Klasse [intercepteur] zwischen [C1] und [C2] ein. Damit dies möglich ist, muss die Klasse [intercepteur] gegenüber [C1] natürlich dieselbe Schnittstelle [I2] präsentieren wie [C2].
Wozu kann das dienen? Die Spring-Dokumentation liefert einige Beispiele. Man möchte beispielsweise bei Aufrufen einer bestimmten Methode M von [C2] Protokolle erstellen, um diese Methode zu überprüfen. In [intercepteur] schreibt man dann eine Methode [M], die diese Protokolle erstellt. Der Aufruf von [C1] an [C2].M läuft wie folgt ab (siehe Schema oben):
- [C1] ruft die Methode M von [C2] auf. Tatsächlich wird dabei die Methode M von [intercepteur] aufgerufen. Dies ist möglich, wenn [C1] eine Schnittstelle [I2] anstelle einer bestimmten Implementierung von [I2] anspricht. Es reicht dann aus, dass [intercepteur] [I2] implementiert.
- Die Methode M von [intercepteur] erstellt die Protokolle und ruft die Methode M von [C2] auf, auf die ursprünglich von [C1] verwiesen wurde.
- Die Methode M von [C2] wird ausgeführt und gibt ihr Ergebnis an die Methode M von [intercepteur] zurück, die gegebenenfalls etwas zu dem in Schritt 2 Erledigten hinzufügen kann.
- Die Methode M von [intercepteur] gibt ein Ergebnis an die aufrufende Methode von [C1] zurück.
Es ist ersichtlich, dass die Methode M von [intercepteur] vor und nach dem Aufruf der Methode M von [C2] etwas ausführen kann. Im Vergleich zu [C1] erweitert sie somit die Methode M von [C2]. Man kann die Technologie AOP daher als eine Möglichkeit betrachten, die von einer Klasse bereitgestellte Schnittstelle zu erweitern.
Wie lässt sich dieses Konzept auf unsere Schicht [service] anwenden? Wenn wir die Schicht [service] direkt mit einer Instanz von [ServiceImpl] implementieren, hat unsere Webanwendung folgende Architektur:
![]() |
Wenn wir die Schicht [service] mit einer Instanz [TransactionProxyFactoryBean] implementieren, ergibt sich folgende Architektur:
![]() |
Man kann sagen, dass die Schicht [service] mit zwei Objekten instanziiert wird:
- das Objekt, das wir oben als [proxy transactionnel] bezeichnen und das tatsächlich das Objekt ist, das von der Methode [getObject] von [TransactionProxyFactoryBean] zurückgegeben wird. Dieses Objekt bildet die Schnittstelle zwischen der Schicht [service] und der Schicht [web]. Es implementiert per Definition die Schnittstelle [IService].
- eine Instanz [ServiceImpl], die ebenfalls die Schnittstelle [IService] implementiert. Nur sie weiß, wie man mit der Schicht [dao] arbeitet, daher ist sie erforderlich.
Nehmen wir an, die Schicht [web] ruft die Methode [saveMany] der Schnittstelle [IService] auf. Wir wissen, dass die von dieser Methode durchgeführten Hinzufügungen bzw. Aktualisierungen funktional innerhalb einer Transaktion erfolgen müssen. Entweder gelingen sie alle, oder es wird keine davon durchgeführt. Wir haben die Methode [saveMany] der Klasse [ServiceImpl] vorgestellt und darauf hingewiesen, dass sie kein Transaktionskonzept kennt. Die Methode [saveMany] der Klasse [proxy transactionnel] wird die Methode [saveMany] der Klasse [ServiceImpl] um dieses Transaktionskonzept erweitern. Folgen wir dem obigen Schema:
- Die Schicht [web] ruft die Methode [saveMany] der Schnittstelle [IService] auf.
- Die Methode [saveMany] von [proxy transactionnel] wird ausgeführt. Sie startet eine Transaktion. Dazu benötigt sie ausreichende Informationen, insbesondere ein Objekt [DataSource], um eine Verbindung zu SGBD herzustellen. Anschließend ruft sie die Methode [saveMany] von [ServiceImpl] auf.
- Diese wird ausgeführt. Sie ruft wiederholt die Schicht [dao] auf, um die Einfügungen oder Aktualisierungen durchzuführen. Die dabei ausgeführten Befehle SQL werden in der in Schritt 2 gestarteten Transaktion ausgeführt.
- Nehmen wir an, einer dieser Vorgänge schlägt fehl. Die Schicht [dao] lässt eine Ausnahme an die Schicht [service] weiterleiten, in diesem Fall an die Methode [saveMany] der Instanz [ServiceImpl].
- Diese führt keine Aktion aus und leitet die Ausnahme weiter an die Methode [saveMany] von [proxy transactionnel].
- Nach Erhalt der Ausnahme führt die Methode [saveMany] von [proxy transactionnel], die Eigentümerin der Transaktion ist, einen [rollback] für diese durch, um alle Aktualisierungen rückgängig zu machen, und lässt die Ausnahme anschließend bis zur Ebene [web] weiterleiten, die für deren Bearbeitung zuständig ist.
In Schritt 4 sind wir davon ausgegangen, dass einer der Einfüge- oder Aktualisierungsvorgänge fehlschlägt. Ist dies nicht der Fall, wird in [5] keine Ausnahme weitergeleitet. Gleiches gilt für [6]. In diesem Fall führt die Methode [saveMany] von [proxy transactionnel] einen [commit] der Transaktion durch, um alle Aktualisierungen zu validieren.
Wir haben nun eine genauere Vorstellung von der Architektur, die von der Bean [TransactionProxyFactoryBean] umgesetzt wird. Kommen wir noch einmal auf deren Konfiguration zurück:
<!-- Transaktionsmanager -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource">
<ref local="dataSource"/>
</property>
</bean>
<!-- Zugriffsklassen der Schicht [service] -->
<bean id="service"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager">
<ref local="transactionManager"/>
</property>
<property name="target">
<bean class="istia.st.mvc.personnes.service.ServiceImpl">
<property name="dao">
<ref local="dao"/>
</property>
</bean>
</property>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="save*">PROPAGATION_REQUIRED</prop>
<prop key="delete*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
Betrachten wir diese Konfiguration vor dem Hintergrund der eingerichteten Architektur:
![]() |
- [proxy transactionnel] übernimmt die Verwaltung der Transaktionen. Spring bietet hierfür mehrere Strategien an. [proxy transactionnel] benötigt eine Referenz auf den ausgewählten Transaktionsmanager.
- Zeilen 11–13: Definieren das Attribut [transactionManager] des Beans [TransactionProxyFactoryBean] mit einer Referenz auf einen Transaktionsmanager. Dieser wird in den Zeilen 2–7 definiert.
- Zeilen 2–7: Der Transaktionsmanager ist vom Typ [DataSourceTransactionManager]:

[DataSourceTransactionManager] ist ein Transaktionsmanager, der für SGBD geeignet ist, auf die über ein Objekt [DataSource] zugegriffen wird. Er kann nur Transaktionen auf einem einzigen SGBD verwalten. Er kann keine Transaktionen verwalten, die auf mehrere SGBD verteilt sind. In diesem Fall haben wir nur ein einziges SGBD. Daher ist dieser Transaktionsmanager geeignet. Wenn [proxy transactionnel] eine Transaktion startet, erfolgt dies über eine mit dem Thread verbundene Verbindung. Diese Verbindung wird in allen Schichten verwendet, die zur Datenbank führen: [ServiceImpl, DaoImplCommon, SqlMapClientTemplate, JDBC].
Die Klasse [DataSourceTransactionManager] muss die Datenquelle kennen, bei der sie eine Verbindung anfordern muss, um diese an den Thread anzuhängen. Diese wird in den Zeilen 4–6 definiert: Es handelt sich um dieselbe Datenquelle, die auch von der Schicht [dao] verwendet wird (siehe Abschnitt 17.5.2).
- Zeilen 14–19: Das Attribut „target“ gibt die Klasse an, die abgefangen werden soll, in diesem Fall die Klasse [ServiceImpl]. Diese Information ist aus zwei Gründen erforderlich:
- Die Klasse [ServiceImpl] muss instanziiert werden, da sie den Dialog mit der Schicht [dao] gewährleistet
- [TransactionProxyFactoryBean] muss einen Proxy generieren, der der Schicht [web] dieselbe Schnittstelle wie [ServiceImpl] präsentiert.
- Zeilen 21–27: Geben an, welche Methoden von [ServiceImpl] der Proxy abfangen muss. Das Attribut [transactionAttributes] in Zeile 21 gibt an, welche Methoden von [ServiceImpl] eine Transaktion erfordern und welche Attribute diese aufweist:
- Zeile 23: Methoden, deren Name mit „get [getOne, getAll]“ beginnt, werden in einer Transaktion mit dem Attribut [PROPAGATION_REQUIRED,readOnly] ausgeführt:
- PROPAGATION_REQUIRED: Die Methode wird in einer Transaktion ausgeführt, sofern bereits eine Transaktion mit dem Thread verknüpft ist; andernfalls wird eine neue Transaktion angelegt und die Methode darin ausgeführt.
- readOnly: schreibgeschützte Transaktion
Hier werden die Methoden [getOne] und [getAll] von [ServiceImpl] in einer Transaktion ausgeführt, obwohl dies eigentlich nicht erforderlich ist. Es handelt sich jedes Mal um einen Vorgang, der aus einem einzigen Befehl SELECT besteht. Wir sehen keinen Sinn darin, diesen Befehl SELECT in eine Transaktion einzubinden.
- Zeile 24: Die Methoden, deren Name mit „save“ beginnt, [saveOne, saveMany], werden in einer Attributtransaktion [PROPAGATION_REQUIRED] ausgeführt.
- Zeile 25: Die Methoden „[deleteOne]“ und „[deleteMany]“ von „[ServiceImpl]“ sind identisch mit den Methoden „[saveOne, saveMany]“ konfiguriert.
In unserer Schicht [service] müssen nur die Methoden [saveMany] und [deleteMany] in einer Transaktion ausgeführt werden. Die Konfiguration hätte auf die folgenden Zeilen reduziert werden können:
<property name="transactionAttributes">
<props>
<prop key="saveMany">PROPAGATION_REQUIRED</prop>
<prop key="deleteMany">PROPAGATION_REQUIRED</prop>
</props>
</property>
17.6. Tests der Schicht [service]
Nachdem wir nun die Schicht [service] geschrieben und konfiguriert haben, wollen wir sie mit den Tests JUnit testen:

Die Konfigurationsdatei [spring-config-test-service-firebird.xml] der Schicht [service] ist diejenige, die in Abschnitt 17.5.2 beschrieben wurde.
Der Test JUnit [TestServiceFirebird] lautet wie folgt:
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 | |
- Zeilen 19–22: Das Programm testet die Ebenen [dao] und [service], die durch die Datei [spring-config-test-service-firebird.xml] konfiguriert wurden, die im vorigen Abschnitt behandelt wurde.
- Die Tests [test1] bis [test6] entsprechen im Grundsatz ihren gleichnamigen Pendants in der Testklasse [TestDaoFirebird] der Schicht [dao]. Der einzige Unterschied besteht darin, dass die Methoden [saveOne] und [deleteOne] nun aufgrund der Konfiguration in einer Transaktion ausgeführt werden.
- Die Methode [test7] dient dazu, die Methoden [saveMany] und [deleteMany] zu testen. Es soll überprüft werden, ob sie tatsächlich in einer Transaktion ausgeführt werden. Kommentieren wir den Code dieser Methode:
- Zeilen 62–63: Wir zählen die Anzahl der Personen, die derzeit in der Liste von [nbPersonnes1] enthalten sind
- Zeilen 67–72: Es werden drei Personen angelegt
- Zeilen 73–83: Diese drei Personen werden durch die Methode [saveMany] gespeichert – Zeile 77. Die ersten beiden Personen p1 und p2 mit der ID -1 werden zur Tabelle [PERSONNES] hinzugefügt. Die Person p3 hat hingegen eine ID von -2. Es handelt sich also nicht um einen Eintrag, sondern um eine Aktualisierung. Diese schlägt fehl, da es in der Tabelle [PERSONNES] keine Person mit der ID -2 gibt. Die Schicht [dao] löst daher eine Ausnahme aus, die bis zur Schicht [service] weitergeleitet wird. Das Vorliegen dieser Ausnahme wird in Zeile 83 geprüft.
- Aufgrund der vorangegangenen Ausnahme sollte die Schicht [service] aus allen Befehlen SQL, die während der Ausführung der Methode [saveMany] ausgegeben wurden, einen Befehl [rollback] erstellen, da diese Methode in einer Transaktion ausgeführt wird. In den Zeilen 86–87 wird überprüft, ob sich die Anzahl der Personen in der Liste nicht verändert hat und somit die Einfügungen von p1 und p2 nicht stattgefunden haben.
- Zeilen 88–103: Es werden ausschließlich die Personen p1 und p2 hinzugefügt, und es wird überprüft, ob die Liste anschließend zwei Personen mehr enthält.
- Zeilen 106–114: Es wird eine Personengruppe gelöscht, die aus den soeben hinzugefügten Personen p1 und p2 sowie einer nicht existierenden Person (id = -1) besteht. Dazu wird die Methode [deleteMany] verwendet (Zeile 108). Diese Methode schlägt fehl, da es in der Tabelle [PERSONNES] keine Person mit der ID –1 gibt. Die Schicht [dao] löst daher eine Ausnahme aus, die bis zur Schicht [service] weitergeleitet wird. Das Vorliegen dieser Ausnahme wird in Zeile 114 geprüft.
- Aufgrund der vorherigen Ausnahme sollte die Schicht [service] einen [rollback] für alle Befehle SQL erstellen, die während der Ausführung der Methode [deleteMany] ausgegeben wurden, da diese Methode in einer Transaktion ausgeführt wird. In den Zeilen 116–117 wird überprüft, ob sich die Anzahl der Personen in der Liste nicht verändert hat und somit die Löschungen von p1 und p2 nicht stattgefunden haben.
- Zeile 122: Es wird eine Gruppe gelöscht, die ausschließlich aus den Personen p1 und p2 besteht. Dies sollte erfolgreich sein. Der Rest der Methode überprüft, ob dies tatsächlich der Fall ist.
Die Ausführung der Tests liefert folgende Ergebnisse:

Alle sieben Tests wurden erfolgreich abgeschlossen. Wir betrachten unsere Schicht [service] als betriebsbereit.
17.7. Die Schicht [web]
Erinnern wir uns an die allgemeine Architektur der zu erstellenden Webanwendung:
![]() |
Wir haben soeben die Schichten [dao] und [service] erstellt, die die Arbeit mit einer Firebird-Datenbank ermöglichen. Wir haben eine Version 1 dieser Anwendung geschrieben, in der die Schichten [dao] und [service] mit einer im Speicher befindlichen Personenliste arbeiteten. Die bei dieser Gelegenheit geschriebene Schicht [web] bleibt weiterhin gültig. Sie war nämlich für eine Schicht [service] bestimmt, die die Schnittstelle [IService] implementierte. Da die neue Schicht [service] dieselbe Schnittstelle implementiert, muss die Schicht [web] nicht geändert werden.
Im vorherigen Artikel wurde Version 1 der Anwendung mit dem Eclipse-Projekt [mvc-personnes-02B] getestet, bei dem die Schichten [web, service, dao, entites] in .jar-Archive gepackt worden waren:
![]() |
Der Ordner [src] war leer. Die Klassen der Schichten befanden sich in den Archiven [personnes-*.jar ]:
![]() |
Um Version 2 zu testen, duplizieren wir unter Eclipse den Eclipse-Ordner [mvc-personnes-02B] in [mvc-personnes-03B] (Kopieren/Einfügen):

Im Projekt [mvc-personnes-03] exportieren wir die Ebenen [dao] und [service] aus [File / Export / Jar file] in die Archive [personnes-dao.jar] bzw. [personnes-service.jar] im Ordner [dist] des Projekts:

Wir kopieren diese beiden Dateien und fügen sie anschließend in Eclipse in den Ordner „[WEB-INF/lib]“ des Projekts „[mvc-personnes-03B]“ ein, wo sie die gleichnamigen Dateien der vorherigen Version ersetzen.
![]() |
Außerdem kopieren und fügen wir die Archive [commons-dbcp-*.jar, commons-pool-*.jar, firebirdsql-full.jar, ibatis-common-2.jar, ibatis-sqlmap-2.jar] aus dem Ordner [lib] des Projekts [mvc-personnes-03] in den Ordner [WEB-INF/lib] des Projekts [mvc-personnes-03B] ein. Diese Archive werden für die neuen Ebenen [dao] und [service] benötigt.
Anschließend fügen wir die neuen Archive in den Classpath des Projekts [clic droit sur projet -> Properties -> Java Build Path -> Add Jars] ein.
Der Ordner [src] enthält die Konfigurationsdateien der Schichten [dao] und [service]:

Die Datei [spring-config.xml] konfiguriert die Schichten [dao] und [service] der Webanwendung. In der neuen Version entspricht sie der Datei [spring-config-test-service-firebird.xml], die zur Konfiguration des Tests der Service-Ebene im Projekt [mvc-personnes-03] verwendet wurde. Man kopiert also den Inhalt von der einen in die andere Datei:
<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!-- Die Datenquelle 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">
<!-- Achtung: Keine Leerzeichen zwischen den beiden <value>-Tags lassen -->
<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>
<!-- Zugriffsklasse auf die Schicht [dao] -->
<bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
<property name="sqlMapClient">
<ref local="sqlMapClient"/>
</property>
</bean>
<!-- Transaktionsmanager -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource">
<ref local="dataSource"/>
</property>
</bean>
<!-- Zugriffsklassen auf die Schicht [service] -->
<bean id="service"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager">
<ref local="transactionManager"/>
</property>
<property name="target">
<bean class="istia.st.mvc.personnes.service.ServiceImpl">
<property name="dao">
<ref local="dao"/>
</property>
</bean>
</property>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
<prop key="save*">PROPAGATION_REQUIRED</prop>
<prop key="delete*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
</beans>
- Zeile 12: Die URL der Firebird-Datenbank. Wir verwenden weiterhin die Datenbank, die für die Tests der Schichten [dao] und [service] verwendet wurde.
Wir stellen das Webprojekt [mvc-personnes-03B] in Tomcat bereit:
![]() | ![]() |
Wir sind bereit für die Test- . Der Firebird-Server SGBD wird gestartet. Der Inhalt der Tabelle [PERSONNES] sieht dann wie folgt aus:

Tomcat wird ebenfalls gestartet. Mit einem Browser rufen wir die URL [http://localhost:8080/mvc-personnes-03B] auf:

Wir fügen eine neue Person über den Link [Ajout] hinzu:
![]() | ![]() |
Wir überprüfen den Eintrag in der Datenbank:

Der Leser wird gebeten, weitere Tests durchzuführen: [modification, suppression].
Führen wir nun den Test auf Versionskonflikte durch, der bereits in Version 1 durchgeführt wurde. [Firefox] ist der Browser des Benutzers U1. Dieser ruft die URL [http://localhost:8080/mvc-personnes-03B] auf:

[IE] ist der Browser des Benutzers U2. Dieser fordert dieselbe URL an:

Der Benutzer U1 ruft die Bearbeitungsseite der Person [Perrichon] auf:

Der Benutzer U2 tut dasselbe:

Der Benutzer U1 nimmt Änderungen vor und bestätigt diese:
![]() |
Der Benutzer U2 macht dasselbe:
![]() |
Der Benutzer U2 kehrt über den Link [Annuler] im Formular zur Personenliste zurück:

Er findet die Person [Perrichon] so, wie U1 sie geändert hat (Name in Großbuchstaben umgewandelt).
Und wie sieht es mit der Datenbank aus? Schauen wir mal:

Der Name der Person Nr. 899 ist nach der Änderung durch U1 nun tatsächlich in Großbuchstaben geschrieben.
17.8. Fazit
Erinnern wir uns daran, was wir erreichen wollten. Wir hatten eine Webanwendung mit der folgenden 3-Tier-Architektur:
wobei die Schichten [dao] und [service] mit einer im Speicher befindlichen Datenliste arbeiteten, die somit beim Herunterfahren des Webservers verloren ging. Das war Version 1. In Version 2 wurden die Schichten [service] und [dao] so umgeschrieben, dass die Personenliste in einer Datenbanktabelle gespeichert wird. Sie ist somit nun persistent. Nun wollen wir untersuchen, welche Auswirkungen die Änderung an SGBD auf unsere Anwendung hat. Dazu erstellen wir drei neue Versionen unserer Webanwendung:
![]() |
- Version 3: SGBD läuft unter Postgres
- Version 4: SGBD ist MySQL
- Version 5: SGBD ist SQL Server Express 2005
Die Änderungen erfolgen an folgenden Stellen:
- Die Klasse [DaoImplFirebird] implementiert Funktionen der Schicht [dao], die mit SGBD Firebird zusammenhängen. Sollte dieser Bedarf weiterhin bestehen, wird sie jeweils durch die Klassen [DaoImplPostgres], [DaoImplMySQL] und [DaoImplSqlExpress] ersetzt.
- Die Mapping-Datei [personnes-firebird.xml] von iBATIS für den Firebird SGBD wird jeweils durch die Mapping-Dateien [personnes-postgres.xml], [personnes-mysql.xml] und [personnes-sqlexpress.xml] ersetzt.
- Die Konfiguration des Objekts [DataSource] der Ebene [dao] ist spezifisch für ein SGBD. Sie ändert sich daher mit jeder Version.
- Der Treiber JDBC des SGBD ändert sich ebenfalls mit jeder Version.
Abgesehen von diesen Punkten bleibt alles unverändert. Im Folgenden beschreiben wir diese neuen Versionen und konzentrieren uns dabei ausschließlich auf die jeweiligen Neuerungen, die jede einzelne mit sich bringt.

























