Skip to content

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

Image

Die Tabelle [PERSONNES] enthält die Liste der von der Webanwendung verwalteten Personen. Sie wurde mit den folgenden Befehlen SQL erstellt:

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


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


ALTER TABLE PERSONNES ADD CONSTRAINT PK_PERSONNES PRIMARY KEY (ID);
  • 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:

Image

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

SELECT GEN_ID ( GEN_PERSONNES_ID,1 ) FROM RDB$DATABASE

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

Image

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

Image

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:

Image

  • [dbpersonnes.gdb] ist die Datenbank.
  • [dbpersonnes.sql] ist das Skript SQL zur Erstellung der Datenbank:
/******************************************************************************/
/***          Generiert von IBExpert 07.03.2006 27.04.2006 10:27:11          ***/
/******************************************************************************/

SET SQL DIALECT 3;

SET NAMES NONE;

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



/******************************************************************************/
/***                               Generatoren                               ***/
/******************************************************************************/

CREATE GENERATOR GEN_PERSONNES_ID;
SET GENERATOR GEN_PERSONNES_ID TO 787;



/******************************************************************************/
/***                                 Tabellen                                 ***/
/******************************************************************************/



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

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

COMMIT WORK;



/* Prüfung der Definition von Einschränkungen */

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


/******************************************************************************/
/***                              Primärschlüssel                              ***/
/******************************************************************************/

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

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:

Image

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

Image

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

package istia.st.mvc.personnes.dao;

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

import java.util.Collection;

public interface IDao {
    // Liste aller Personen
    Collection getAll();
    // Eine bestimmte Person abrufen
    Personne getOne(int id);
    // Person hinzufügen/bearbeiten
    void saveOne(Personne personne);
    // Person löschen
    void deleteOne(int id);
}
  • Die Schnittstelle verfügt über dieselben vier Methoden wie in der vorherigen Version.

Die Klasse [DaoImplCommon], die diese Schnittstelle implementiert, sieht wie folgt aus:

package istia.st.mvc.personnes.dao;

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

import java.util.Collection;

public class DaoImplCommon extends SqlMapClientDaoSupport implements
        IDao {

    // Liste der Personen
    public Collection getAll() {
...
    }

    // eine bestimmte Person abrufen
    public Personne getOne(int id) {
...
    }

    // Person löschen
    public void deleteOne(int id) {
...
    }

    // Person hinzufügen oder bearbeiten
    public void saveOne(Personne personne) {
        // Ist der Parameter „Person“ gültig?
        check(personne);
        // Hinzufügen oder Bearbeiten?
        if (personne.getId() == -1) {
            // Hinzufügen
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }

    // Person hinzufügen
    protected void insertPersonne(Personne personne) {
...
    }

    // Person bearbeiten
    protected void updatePersonne(Personne personne) {
...
    }

    // Gültigkeit einer Person prüfen
    private void check(Personne p) {
...
    }

...
}
  • 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:

Image

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

Image

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:

Image

Eine der Methoden dieser Klasse ermöglicht es, den Client [iBATIS] zu konfigurieren, mit dem die Datenbank genutzt werden soll:

Image

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:

  1. eine Verbindung aus einem Verbindungspool anfordern
  2. eine Transaktion eröffnen
  3. eine Reihe von SQL-Befehlen ausführen, die in einer Konfigurationsdatei gespeichert sind
  4. die Transaktion schließen
  5. 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]:

Image

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

Image

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

Image

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:

Image

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:

Image

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:

Image

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:

Image

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:

  1. den Namen des zu verwendenden Treibers JDBC – initialisiert mit [setDriverClassName]
  2. den Namen der URL der zu verwendenden Datenbank – initialisiert mit [setUrl]
  3. die Benutzer-ID des Eigentümers der Verbindung – initialisiert mit [setUsername] (und nicht mit setUserName, wie man vielleicht erwarten würde)
  4. 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:

Image

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

package istia.st.mvc.personnes.dao;

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

import java.util.Collection;

public class DaoImplCommon extends SqlMapClientDaoSupport implements
        IDao {

    // Personenliste
    public Collection getAll() {
...
    }

    // eine bestimmte Person abrufen
    public Personne getOne(int id) {
...
    }

    // Person löschen
    public void deleteOne(int id) {
...
    }

    // Person hinzufügen oder bearbeiten
    public void saveOne(Personne personne) {
        // Ist der Parameter „Person“ gültig?
        check(personne);
        // Hinzufügen oder Bearbeiten?
        if (personne.getId() == -1) {
            // Hinzufügen
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }

    // Person hinzufügen
    protected void insertPersonne(Personne personne) {
...
    }

    // Person bearbeiten
    protected void updatePersonne(Personne personne) {
...
    }

    // Gültigkeit einer Person prüfen
    private void check(Personne p) {
...
    }

...
}

Wir werden die Methoden nacheinander betrachten.


getAll


Mit dieser Methode lassen sich alle Personen aus der Liste abrufen. Der Code lautet wie folgt:

1
2
3
4
    // Liste der Personen
    public Collection getAll() {
        return getSqlMapClientTemplate().queryForList("Personne.getAll", null);
}

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:

Image

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:

Image

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:

Image

Er implementiert die folgende Schnittstelle [SqlMapClientOperations]:

Image

Diese Schnittstelle definiert Methoden, mit denen der Inhalt der Datei [personnes-firebird.xml] verarbeitet werden kann:

[queryForList]

Image

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]

Image

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]

Image

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]

Image

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]

Image

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:

1
2
3
4
    // Liste der Personen
    public Collection getAll() {
        return getSqlMapClientTemplate().queryForList("Personne.getAll", null);
}
  • 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:

        // eine bestimmte Person abrufen
    public Personne getOne(int id) {
        // wird aus der BD abgerufen
        Personne personne = (Personne) getSqlMapClientTemplate()
                .queryForObject("Personne.getOne", new Integer(id));
        // Wurde etwas abgerufen?
        if (personne == null) {
            // Es wird eine Ausnahme ausgelöst
            throw new DaoException(
                    "La personne d'id [" + id + "] n'existe pas", 2);
        }
        // Die Person wird zurückgegeben
        return personne;
    }
  • 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:

    // Person löschen
    public void deleteOne(int id) {
        // Die Person wird gelöscht
        int n = getSqlMapClientTemplate().delete("Personne.deleteOne",
                new Integer(id));
        // War der Vorgang erfolgreich?
        if (n == 0) {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        }
    }
  • 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:

        // Person hinzufügen oder bearbeiten
    public void saveOne(Personne personne) {
        // Ist der Parameter „Person“ gültig?
        check(personne);
        // Hinzufügen oder Bearbeiten?
        if (personne.getId() == -1) {
            // Hinzufügen
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }
...
  • 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:

// Person hinzufügen
    protected void insertPersonne(Personne personne) {
        // 1. Version
        personne.setVersion(1);
        // Es wird 10 ms gewartet – für Testzwecke „true“ anstelle von „false“ eingeben
        if (true)
            wait(10);
        // Die neue Person wird in die Tabelle von BD eingefügt
        getSqlMapClientTemplate().insert("Personne.insertOne", personne);
    }
  • 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.
  • 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:

// Person bearbeiten
    protected void updatePersonne(Personne personne) {
        // 10 ms warten – für Testzwecke „true“ anstelle von „false“ setzen
        if (true)
            wait(10);
        // Änderung
        int n = getSqlMapClientTemplate()
                .update("Personne.updateOne", personne);
        if (n == 0)
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] n'existe pas ou bien a été modifiée", 2);
    }
  • Eine Aktualisierung kann aus mindestens zwei Gründen fehlschlagen:
    1. Die zu aktualisierende Person existiert nicht
    2. 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:

Image

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

package istia.st.mvc.personnes.tests;

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

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

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

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

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:

Image

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

Image

Die Liste der Personen wurde erfolgreich ausgegeben. Wir können nun mit dem Test JUnit fortfahren.

Der Test JUnit [TestDaoFirebird] lautet wie folgt:

package istia.st.mvc.personnes.tests;

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

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

public class TestDaoFirebird extends TestCase {

    // Schicht [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

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

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

    // Personenliste
    private void doListe(Collection personnes) {
...
    }

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

    // Änderung/Löschung eines nicht vorhandenen Elements
    public void test2() throws ParseException {
..
    }

    // Verwaltung von Personenversionen
    public void test3() throws ParseException, InterruptedException {
...
    }

    // Optimistisches Locking – Multithread-Zugriff
    public void test4() throws Exception {
...
    }

    // Gültigkeitsprüfungen für saveOne
    public void test5() throws ParseException {
....
    }

    // Einfügungen in Multithread-Umgebungen
    public void test6() throws ParseException, InterruptedException{
...
}
  • 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:

// Person bearbeiten
    protected void updatePersonne(Personne personne) {
        // 10 ms warten – für die Tests „true“ anstelle von „false“ setzen
        if (true)
            wait(10);
        // Änderung
        int n = getSqlMapClientTemplate()
                .update("Personne.updateOne", personne);
        if (n == 0)
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] n'existe pas ou bien a été modifiée", 2);
    }
  • 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.

    public void test4() throws Exception {
        // Hinzufügen einer Person
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        dao.saveOne(p1);
        int id1 = p1.getId();
        // Erstellung von N Threads zur Aktualisierung der Anzahl der Kinder
        final int N = 100;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadDaoMajEnfants("thread n° " + i, dao, id1);
            taches[i].start();
        }
        // Warten auf das Ende der Threads
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
        // die Person wird abgerufen
        p1 = dao.getOne(id1);
        // sie muss N Kinder haben
        assertEquals(N, p1.getNbEnfants());
        // Person p1 löschen
        dao.deleteOne(p1.getId());
        // Überprüfung
        boolean erreur = false;
        int codeErreur = 0;
        try {
            p1 = dao.getOne(p1.getId());
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
        // Es muss ein Fehler mit dem Code 2 vorliegen
        assertTrue(erreur);
        assertEquals(2, codeErreur);
    }

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:

package istia.st.mvc.personnes.tests;

import java.util.Date;

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

public class ThreadDaoMajEnfants extends Thread {
    // Name des Threads
    private String name;

    // Referenz auf die Ebene [dao]
    private IDao dao;

    // ID der Person, an der gearbeitet wird
    private int idPersonne;

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

    // Kern des Threads
    public void run() {
        // Verfolgung
        suivi("lancé");
        // Es wird so lange eine Schleife durchlaufen, bis die Zahl um 1 erhöht werden konnte
        // die Anzahl der Kinder der Person idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
            // Es wird eine Kopie der Person aus idPersonne abgerufen
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
            // Weiter
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1)
                    + " pour la version " + personne.getVersion());
            // 10 ms warten, um den Prozessor freizugeben
            try {
                // Weiter
                suivi("début attente");
                // Unterbrechung, um den Prozessor freizugeben
                Thread.sleep(10);
                // Weiter
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
            // Wartezeit beendet – Versuch, die Kopie zu validieren
            // In der Zwischenzeit konnten andere Threads das Original ändern
            int codeErreur = 0;
            try {
                // Die Anzahl der Nachkommen dieser Kopie wird um 1 erhöht
                personne.setNbEnfants(nbEnfants + 1);
                // Es wird versucht, das Original zu ändern
                dao.saveOne(personne);
                // Es ist gelungen – das Original wurde geändert
                fini = true;
            } catch (DaoException ex) {
                // Der Fehlercode wird abgerufen
                codeErreur = ex.getCode();
                // Bei einem Fehler von ID oder einem Fehlercode der Version 2 wird die Aktualisierung erneut versucht
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                default:
                    // Unbehandelte Ausnahme – wird weitergeleitet
                    throw ex;
                }
            }
        }
        // Weiterverfolgung
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

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

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:

// Person hinzufügen
    protected void insertPersonne(Personne personne) {
        // 1. Version
        personne.setVersion(1);
        // 10 ms warten – für Testzwecke „true“ anstelle von „false“ setzen
        if (true)
            wait(10);
        // Die neue Person wird in die Tabelle von BD eingefügt
        getSqlMapClientTemplate().insert("Personne.insertOne", personne);
    }
  • 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:

    // Einfügungen im Multithread-Modus
    public void test6() throws ParseException, InterruptedException{
        // Anlegen einer Person
        Personne p = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        // die N-mal in ein Array dupliziert wird
        final int N = 100;
        Personne[] personnes=new Personne[N];
        for(int i=0;i<personnes.length;i++){
            personnes[i]=new Personne(p);
        }
        // Erstellung von N Einfüge-Threads – jeder Thread fügt 1 Person ein
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadDaoInsertPersonne("thread n° " + i, dao, personnes[i]);
            taches[i].start();
        }
        // Warten auf das Ende der Threads
        for (int i = 0; i < taches.length; i++) {
            // Thread Nr. i
            taches[i].join();
            // Person löschen
            dao.deleteOne(personnes[i].getId());
        }
}

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:

package istia.st.mvc.personnes.tests;

import java.util.Date;

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

public class ThreadDaoInsertPersonne extends Thread {
    // Name des Threads
    private String name;

    // Referenz auf der Ebene [dao]
    private IDao dao;

    // ID der Person, an der gearbeitet wird
    private Personne personne;

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

    // Kern des Threads
    public void run() {
        // Nachverfolgung
        suivi("lancé");
        // Einfügung
        dao.saveOne(personne);
        // Nachverfolgung
        suivi("a terminé");
    }

    // Nachverfolgung
    private void suivi(String message) {
        System.out.println(name + " [" + new Date().getTime() + "] : "
                + message);
    }
}
  • 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:

            try {
                // erhöht die Anzahl der untergeordneten Elemente dieser Kopie um 1
                personne.setNbEnfants(nbEnfants + 1);
                // Es wird versucht, das Original zu ändern
                dao.saveOne(personne);
                // Es ist gelungen  das Original wurde geändert
                fini = true;
            } catch (DaoException ex) {
                // Der Fehlercode wird abgerufen
                codeErreur = ex.getCode();
                // Bei einem Fehler von ID oder einem Versionsfehler 2 wird die Aktualisierung erneut versucht
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                default:
                    // Unbehandelte Ausnahme  wird weitergeleitet
                    throw ex;
                }
  • 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:

Image

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:

Image

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:

Image

Man erhält das Objekt vom Typ [Throwable], das vom Treiber JDBC ausgelöst wurde:

Image

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:

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

public class ThreadDaoMajEnfants extends Thread {
...

    // Kern des Threads
    public void run() {
...
        while (!fini) {
            // Es wird eine Kopie der Person aus idPersonne abgerufen
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
...
            // Wartezeit abgelaufen – Versuch, die Kopie zu validieren
            // In der Zwischenzeit konnten andere Threads das Original möglicherweise geändert haben
            int codeErreur = 0;
            try {
                // Die Anzahl der untergeordneten Elemente dieser Kopie wird um 1 erhöht
                personne.setNbEnfants(nbEnfants + 1);
                // Es wird versucht, das Original zu ändern
                dao.saveOne(personne);
                // Es ist gelungen – das Original wurde geändert
                fini = true;
            } catch (DaoException ex) {
                // Der Fehlercode wird abgerufen
                codeErreur = ex.getCode();
                // Bei einem Fehler von ID oder Version 2 oder einem Deadlock 4 wird
                // wird die Aktualisierung erneut versucht
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                case 4:
                    suivi("conflit de mise à jour");
                    break;
                default:
                    // Unbehandelte Ausnahme – wird weitergeleitet
                    throw ex;
                }
            }
        }
        // Weiterverfolgung
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }
...
}
  • 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:

// eine Person bearbeiten
    protected void updatePersonne(Personne personne) {
        // 10 ms warten – für Testzwecke „true“ anstelle von „false“ setzen
        if (true)
            wait(10);
        // Änderung
        int n = getSqlMapClientTemplate()
                .update("Personne.updateOne", personne);
        if (n == 0)
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] n'existe pas ou bien a été modifiée", 2);
    }

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:

package istia.st.mvc.personnes.dao;

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

public class DaoImplFirebird extends DaoImplCommon {

    // eine Person bearbeiten
    protected void updatePersonne(Personne personne) {
        // Es wird 10 ms gewartet – für Testzwecke „true“ anstelle von „false“ eingeben
        if (true)
            wait(10);
        // Änderung
        try {
            // Die Person, die über die richtige Version verfügt, wird geändert
            int n = getSqlMapClientTemplate().update("Personne.updateOne",
                    personne);
            if (n == 0)
                throw new DaoException("La personne d'Id [" + personne.getId()
                        + "] n'existe pas ou bien a été modifiée", 2);
        } catch (org.springframework.jdbc.UncategorizedSQLException ex) {
            if (ex.getSQLException().getCause().getClass().isAssignableFrom(
                    org.firebirdsql.jdbc.FBSQLException.class)) {
                org.firebirdsql.jdbc.FBSQLException cause = (org.firebirdsql.jdbc.FBSQLException) ex
                        .getSQLException().getCause();
                if (cause.getErrorCode() == 335544336) {
                    throw new DaoException(
                            "Conflit d'accès au même enregistrement", 4);
                }
            } else {
                throw ex;
            }
        }
    }

    // Warten
    private void wait(int N) {
        // Es wird N ms gewartet
        try {
            Thread.sleep(N);
        } catch (InterruptedException e) {
            // die Ausnahmeprotokolle werden angezeigt
            e.printStackTrace();
            return;
        }
    }

}
  • 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:

Image

[test4] wurde erfolgreich ausgeführt. Die letzten Zeilen des Bildschirmprotokolls lauten wie folgt:

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

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:

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

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:

Image

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:

Image

  • [IService] ist die Schnittstelle, die von der Schicht [service] bereitgestellt wird
  • [ServiceImpl] ist eine Implementierung dieser Schnittstelle

Die Schnittstelle [IService] lautet wie folgt:

package istia.st.mvc.personnes.service;

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

import java.util.Collection;

public interface IService {
    // Liste aller Personen
    Collection getAll();

    // eine bestimmte Person abrufen
    Personne getOne(int id);

    // eine Person hinzufügen/bearbeiten
    void saveOne(Personne personne);

    // eine Person löschen
    void deleteOne(int id);

    // Mehrere Personen speichern
    void saveMany(Personne[] personnes);

    // Mehrere Personen löschen
    void deleteMany(int ids[]);
}
  • 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:

package istia.st.mvc.personnes.service;

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

import java.util.Collection;

public class ServiceImpl implements IService {

    // die Ebene [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

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

    // Personenliste
    public Collection getAll() {
        return dao.getAll();
    }

    // eine bestimmte Person abrufen
    public Personne getOne(int id) {
        return dao.getOne(id);
    }

    // Person hinzufügen oder bearbeiten
    public void saveOne(Personne personne) {
        dao.saveOne(personne);
    }

    // Person löschen
    public void deleteOne(int id) {
        dao.deleteOne(id);
    }

    // Personensammlung speichern
    public void saveMany(Personne[] personnes) {
        // Durchlaufen des Personenarrays
        for (int i = 0; i < personnes.length; i++) {
            dao.saveOne(personnes[i]);
        }
    }

    // Eine Personensammlung löschen
    public void deleteMany(int[] ids) {
        // ids: die IDs der zu löschenden Personen
        for (int i = 0; i < ids.length; i++) {
            dao.deleteOne(ids[i]);
        }
    }
}
  • 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:

Image

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:

Image

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

  1. [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.
  2. Die Methode M von [intercepteur] erstellt die Protokolle und ruft die Methode M von [C2] auf, auf die ursprünglich von [C1] verwiesen wurde.
  3. 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.
  4. 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:

  1. Die Schicht [web] ruft die Methode [saveMany] der Schnittstelle [IService] auf.
  2. 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.
  3. 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.
  4. 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].
  5. Diese führt keine Aktion aus und leitet die Ausnahme weiter an die Methode [saveMany] von [proxy transactionnel].
  6. 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]:

Image

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

Image

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:

package istia.st.mvc.personnes.tests;

...

public class TestServiceFirebird extends TestCase {

    // Schicht [service]
    private IService service;

    public IService getService() {
        return service;
    }

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

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

    // Personenliste
    private void doListe(Collection personnes) {
...
    }

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

    // Änderung/Löschung eines nicht vorhandenen Elements
    public void test2() throws ParseException {
...
    }

    // Verwaltung von Personenversionen
    public void test3() throws ParseException, InterruptedException {
...
    }

    // Optimistisches Locking – Multithread-Zugriff
    public void test4() throws Exception {
...
    }

    // Gültigkeitsprüfungen für saveOne
    public void test5() throws ParseException {
...
    }

        // Einfügungen im Multithread-Betrieb
    public void test6() throws ParseException, InterruptedException{
...
    }

    // Tests der Methode deleteMany
    public void test7() throws ParseException {
        // Aktuelle Liste
        Collection personnes = service.getAll();
        int nbPersonnes1 = personnes.size();
        // Anzeige
        doListe(personnes);
        // Anlegen von drei Personen
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        Personne p2 = new Personne(-1, "Y", "Y", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/03/2006"), false, 0);
        Personne p3 = new Personne(-2, "Z", "Z", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/04/2006"), true, 2);
        // Hinzufügen der 3 Personen – die Person p3 mit der ID -2  wird Folgendes auslösen
        // eine Ausnahme
        boolean erreur = false;
        try {
            service.saveMany(new Personne[] { p1, p2, p3 });
        } catch (Exception ex) {
            erreur = true;
            System.out.println(ex.toString());
        }
        // Überprüfung
        assertTrue(erreur);
        // neue Liste – die Anzahl der Elemente sollte sich nicht geändert haben
        // aufgrund des automatischen Rollbacks der Transaktion
        int nbPersonnes2 = service.getAll().size();
        assertEquals(nbPersonnes1, nbPersonnes2);
        // Hinzufügen der beiden gültigen Personen
        // Ihre IDs werden auf -1 zurückgesetzt
        p1.setId(-1);
        p2.setId(-1);
        service.saveMany(new Personne[] { p1, p2 });
        // ihre IDs werden abgerufen
        int id1 = p1.getId();
        int id2 = p2.getId();
        // Prüfungen
        p1 = service.getOne(id1);
        assertEquals(p1.getNom(), "X");
        p2 = service.getOne(id2);
        assertEquals(p2.getNom(), "Y");
        // Neue Liste – es müssen 2 Elemente mehr vorhanden sein
        int nbPersonnes3 = service.getAll().size();
        assertEquals(nbPersonnes1 + 2, nbPersonnes3);
        // p1 und p2 sowie eine nicht vorhandene Person werden entfernt
        // Es muss eine Ausnahme auftreten
        erreur = false;
        try {
            service.deleteMany(new int[] { id1, id2, -1 });
        } catch (Exception ex) {
            erreur = true;
            System.out.println(ex.toString());
        }
        // Überprüfung
        assertTrue(erreur);
        // neue Liste
        personnes = service.getAll();
        int nbPersonnes4 = personnes.size();
        // Es durfte keine Person gelöscht werden (automatischer Rollback
        // der Transaktion)
        assertEquals(nbPersonnes4, nbPersonnes3);
        // beide gültigen Personen werden gelöscht
        service.deleteMany(new int[] { id1, id2 });
        // Prüfungen
        // Person p1
        erreur = false;
        int codeErreur = 0;
        try {
            p1 = service.getOne(id1);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
        // Es muss ein Fehler mit dem Code 2 vorliegen
        assertTrue(erreur);
        assertEquals(2, codeErreur);
        // Person p2
        erreur = false;
        codeErreur = 0;
        try {
            p1 = service.getOne(id2);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
        // Es muss ein Fehler mit Code 2 vorliegen
        assertTrue(erreur);
        assertEquals(2, codeErreur);
        // neue Liste
        personnes = service.getAll();
        int nbPersonnes5 = personnes.size();
        // Überprüfung – man sollte wieder am Ausgangspunkt sein
        assertEquals(nbPersonnes5, nbPersonnes1);
        // Anzeige
        doListe(personnes);
    }

}
  • 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:

Image

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

Image

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:

Image

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

Image

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:

Image

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

Image

Wir fügen eine neue Person über den Link [Ajout] hinzu:

Wir überprüfen den Eintrag in der Datenbank:

Image

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:

Image

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

Image

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

Image

Der Benutzer U2 tut dasselbe:

Image

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:

Image

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:

Image

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.