Skip to content

3. Einführung in API JDBC

3.1. Einrichtung der Arbeitsumgebung

Wir werden mit einer Datenbank MySQL5 arbeiten.

Sie benötigen:

  • ein JDK (Java Development Kit) installiert haben (Abschnitt 23.1);
  • den Abhängigkeitsmanager Maven installiert haben (Abschnitt 23.2);
  • die Spring Tool Suite (STS) installiert haben (Abschnitt 23.3);
  • die SGBD MySQL5 (Absatz 23.4) und deren Client EMS MyManager (Absatz 23.5) installiert;
  • den Code aus dem Dokument [http://tahe.developpez.com/java/spring-database] heruntergeladen;

Im Folgenden wird davon ausgegangen, dass der Administrator von MySQL5 „root“ ist und das Passwort „root“ verwendet. Starten Sie SGBD, MySQL5 und dessen Client [MyManager]. Mit Hilfe von [MyManager] erstellen wir die Datenbank [dbproduits] [1-34]:

  • In [3] muss die Datenbank [dbproduits] heißen;
  • in [8-9], als „root“ mit dem Passwort „root“ (was auf dem obigen Screenshot nicht zu sehen ist);
  • in [14a], das Passwort lautet erneut root (was auf dem Screenshot nicht zu sehen ist);
  • bei [15] wurde die Datenbank [dbproduits] erstellt;
  • Bei [20] ist auf die ausgewählte Datenbank zu achten. Es muss sich um die Datenbank [dbproduits] handeln;
  • in [22], der Ordner lautet <exemples>/spring-database-config/mysql/databases, wobei <exemples> der Ordner mit den heruntergeladenen Beispielen ist;
  • bei [23] wählen Sie das Skript SQL [dbproduits.sql] aus. Es wird die Tabelle [PRODUITS] in der Datenbank [dbproduits] generieren;
  • in [30] wurde die Tabelle [produits] angelegt;
  • in [33] wurden die Spalten der Tabelle [produits];
  • in [34], sie ist zunächst leer;

Importieren Sie nun mit STS die folgenden Projekte (befolgen Sie dabei die Vorgehensweise für die Projekte im Ordner <exemples>/spring-core):

  • in [2]: Das Projekt [mysql-config-jdbc] befindet sich im Ordner [<exemples>/spring-database-config/mysql/eclipse/mysql-config-jdbc] [1];

Dieses Projekt konfiguriert die Schicht JDBC der folgenden Architektur:

Importieren Sie anschließend die folgenden drei Projekte erneut:

  • in [2]; die Projekte befinden sich im Ordner [<exemples>/spring-database-config/spring-jdbc] [1];

Diese drei Projekte sind Maven-Projekte, die das Maven-Projekt [mysql-config-jdbc] verwenden. Dieses Projekt generiert das folgende Maven-Artefakt (siehe pom.xml):


    <groupId>dvp.spring.database</groupId>
    <artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>

Das gleiche Artefakt wird vom Projekt [oracle-config-jdbc, db2-config-jdbc, ...] generiert. Um sicherzustellen, dass die derzeit in STS geladenen Projekte [spring-generic-jdbc-*] tatsächlich das Projekt [mysql-config-jdbc] verwenden:

  • stellen Sie sicher, dass kein weiteres Projekt [sgbd-config-jdbc] gleichzeitig geladen ist. Dies könnte zu schwer nachvollziehbaren Fehlern führen;
  • Aktualisieren Sie die Maven-Konfiguration der geladenen Projekte wie folgt:

Um Ihre Konfiguration zu überprüfen, führen Sie die Laufkonfiguration [spring-jdbc-generic-01.IntroJdbc01] [1-3] aus:

Sie sollten die folgenden Konsolenergebnisse erhalten:


------------------------------ Vidage de la table [PRODUITS]
------------------------------ Remplissage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.00000000000001,"description":"DESC10"}
------------------------------ Mise à jour de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":110.00000000000001,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.10000000000001,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.2,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.30000000000001,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.00000000000001,"description":"DESC10"}
------------------------------ Vidage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Insertion de deux produits de même clé primaire dans la table [PRODUITS]
Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire : 
- Duplicate entry '100' for key 'PRIMARY'
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Travail terminé

In den folgenden Beispielen hat der Leser folgende Möglichkeiten:

  • entweder direkt mit den zuvor geladenen Projekten arbeiten;
  • oder die Projekte selbst erstellen;

3.2. Schritte zur Nutzung einer Datenbank

In der oben dargestellten Architektur umfasst die Nutzung einer Datenbank durch das Konsolenprogramm die folgenden Schritte:

  1. Laden des Datenbanktreibers JDBC;
  1. Herstellen einer Verbindung zur Datenbank;
  2. Ausführung eines Befehls SQL an die Datenbank und Verarbeitung der Ergebnisse des Befehls SQL;
  3. Schließen der Verbindung;

Schritt 1 wird nur einmal ausgeführt. Die Schritte 2–4 werden wiederholt ausgeführt. Es ist zu beachten, dass keine Verbindung offen bleibt. Sie wird geschlossen, sobald sie nicht mehr benötigt wird.

3.2.1. Schritt 1 – Laden des Treibers JDBC in den Arbeitsspeicher

Der Code


        // Treiber wird geladen JDBC
        try {
            Class.forName(nom de la classe du pilote JDBC);
        } catch (ClassNotFoundException e1) {
            // Ausnahme behandeln
}

Der Vorgang in Zeile 3 dient dazu, den Treiber JDBC aus der Datenbank in den Speicher zu laden. Dieser Vorgang muss nur einmal durchgeführt werden. Eine Wiederholung führt jedoch nicht zu einem Fehler. Die Treiberklasse JDBC wird im Classpath des Projekts gesucht. Daher muss im Eclipse-Projekt die Datei „[jar]“, die die Treiberklasse „JDBC“ enthält, in den Classpath des Projekts aufgenommen worden sein.

3.2.2. Schritt 2 – Herstellen einer Verbindung

Sobald der Treiber JDBC eingerichtet ist, wird er aufgefordert, eine Verbindung mit dem BD herzustellen:

Der Code


package spring.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class IntroJdbc01 {

...
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // Verbindung herstellen
            connexion = DriverManager.getConnection(url, user, passwd);
...
        } catch (SQLException e1) {
            // Ausnahme wird behandelt
            ...
        } finally {
         // Verbindung schließen
         if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e2) {
                // Ausnahme behandeln
                ...
            }
         }
}
  • Zeilen 3–7: Die Implementierungsklassen der Schnittstelle JDBC befinden sich alle im Paket [java.sql]. Außerdem lösen sie im Fehlerfall alle eine Ausnahme vom Typ [SQLException] aus (Zeile 19, 27). Diese Ausnahme leitet sich von der Klasse [Exception] ab und ist eine sogenannte kontrollierte Ausnahme: Man muss entweder ein try/catch-Block einfügen, um sie zu behandeln, oder alternativ darauf verzichten und angeben, dass die Methode die Ausnahme weiterleitet, indem man die Signatur der Methode um [throws SQLException] ergänzt;
  • Zeile 17: [DriverManager.getConnection] ist eine statische Methode, die drei Parameter erwartet:
    • [url]: das URL aus der Datenbank. Es handelt sich um eine Zeichenkette, die vom verwendeten BD abhängt. Für MySQL hat sie die Form [jdbc:mysql://localhost:3306/nom_de_la_bd];
    • [user]: der Eigentümer der Verbindung;
    • [passwd]: sein Passwort;
  • Zeilen 24–30: Die Verbindung muss in der Klausel [finally] geschlossen werden, damit sie unabhängig davon, ob eine Ausnahme auftritt oder nicht, geschlossen wird.

3.2.3. Schritt 3 – Ausgabe von Befehlen SQL und [SELECT]

Sobald eine Verbindung hergestellt ist, können Befehle SQL gesendet werden. Die Handhabung von Leseaufträgen [SELECT] unterscheidet sich von der bei Aktualisierungsvorgängen [UPDATE, INSERT, DELETE] verwendeten Vorgehensweise. Wir beginnen mit den Befehlen SQL und [SELECT]:

Der Code


Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // Verbindung herstellen
            connexion = DriverManager.getConnection(url, user, passwd);
            // Transaktion starten
            connexion.setAutoCommit(false);
            // im schreibgeschützten Modus
            connexion.setReadOnly(true);
            // die Tabelle [PRODUITS] wird gelesen
            ps = connexion.prepareStatement("SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS");
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                System.out.println(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // Transaktion festschreiben
            connexion.commit();
        } catch (SQLException e1) {
            // Ausnahme wird behandelt
             doCatchException(connexion,e1);
        } finally {
            // „finally“-Block wird verarbeitet
            doFinally(rs, ps, connexion);
        }

    private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
....
}
  • Zeilen 8, 10: Eröffnung einer Transaktion (Zeile 8) im schreibgeschützten Modus (Zeile 10). Eine Transaktion ist eine Abfolge von Befehlen SQL, die entweder alle erfolgreich sind oder alle fehlschlagen. In einer Transaktion mit N Befehlen SQL werden also die vorherigen I Befehle storniert, wenn der Befehl I+1 fehlschlägt. Für einen Lesevorgang ist keine Transaktion erforderlich. Das Anlegen einer Transaktion im schreibgeschützten Modus kann jedoch bestimmten SGBD-Befehlen bestimmte Optimierungen ermöglichen;
  • Zeile 12: Verwendung eines [PreparedStatement]. Ein [PreparedStatement] hat normalerweise Parameter, die mit dem Zeichen ? gekennzeichnet sind. Hier gibt es keine. Ein [PreparedStatement] ist ein vom SGBD vorbereiteter Befehl. Diese Vorbereitung ist mit Kosten verbunden und wird nur einmal durchgeführt. Anschließend wird dieser vorbereitete Befehl vom SGBD mit verschiedenen effektiven Parametern ausgeführt, die die formalen Parameter „?“ ersetzen. Es ist zu beachten, dass es vorzuziehen ist, die gewünschten Spalten namentlich anzugeben, anstatt die Notation * zu verwenden, um alle Spalten abzurufen. Durch die Angabe der Spaltennamen können deren Werte anschließend anhand ihrer Position in der Abfrage SELECT abgerufen werden;
  • Zeile 13: Ausführung von [PreparedStatement]. Es wird ein Objekt vom Typ [ResultSet] abgerufen;

Ein Objekt vom Typ [ResultSet] stellt eine Tabelle dar, d. h. eine Sammlung von Zeilen und Spalten. Zu einem bestimmten Zeitpunkt hat man nur Zugriff auf eine Zeile der Tabelle, die als aktuelle Zeile bezeichnet wird. Bei der anfänglichen Erstellung von [ResultSet] gibt es keine aktuelle Zeile. Um eine aktuelle Zeile zu erhalten, muss eine Operation vom Typ [ResultSet.next()] durchgeführt werden. Die Signatur der Methode next lautet wie folgt:

    boolean next()

Diese Methode versucht, zur nächsten Zeile von [ResultSet] zu wechseln und gibt bei Erfolg true zurück, andernfalls false. Bei Erfolg wird die nächste Zeile zur neuen aktuellen Zeile. Die vorherige Zeile geht verloren und kann nicht mehr zurückgeholt werden.

Die Tabelle [ResultSet] enthält Spalten mit den Namen labelCol1, labelCol2, …, die in der ausgeführten Abfrage [SELECT] angegeben sind. Mit der Abfrage:

SELECT ID as myId, NOM as myNom, CATEGORIE as myCategorie, PRIX as myPrix, DESCRIPTION as myDescription FROM PRODUITS
  • wird die Spalte [ID] in eine Spalte der Tabelle [ResultSet] mit dem Namen [myId] übernommen;
  • die Spalte [NOM] wird in eine Spalte der Tabelle [ResultSet] mit dem Namen [myNom] verschoben;
  • ...

In der obigen Darstellung werden die Bezeichner [myCol] als Spaltenbezeichnungen bezeichnet. Fehlen diese Bezeichnungen, hängen die Spaltennamen der Tabelle [ResultSet] von der Tabelle SGBD ab. Wenn die Tabelle [SELECT] auf eine einzige Tabelle angewendet wird, sind die Spaltenbezeichnungen standardmäßig die Namen der Spalten, die von der Tabelle SELECT abgefragt werden. Das Problem tritt auf, wenn der [SELECT] auf mehrere Tabellen angewendet wird und diese identische Spaltennamen enthalten, wie im folgenden Beispiel:

SELECT PRODUITS.NOM, CATEGORIES.NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID

Angenommen, die Tabelle [PRODUITS] verfügt über einen Fremdschlüssel zur Tabelle [CATEGORIES], der durch die Beziehung [Produits] symbolisiert wird.CATEGORIE_ID --> [CATEGORIES].ID, und dass die Tabellen [PRODUITS] und [CATEGORIES] beide ein Feld [NOM] enthalten. In diesem Fall hängen die in der Tabelle [ResultSet] angegebenen Namen der Spalten [PRODUITS.NOM] und [CATEGORIES.NOM] von der Tabelle SGBD ab. Für die Portabilität zwischen SGBD müssen daher hier Spaltenbezeichnungen verwendet werden, und man schreibt:


SELECT PRODUITS.NOM as p_NOM, CATEGORIES.NOM as c_NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID

Um die verschiedenen Felder der aktuellen Zeile von [ResultSet] auszuwerten, stehen folgende Methoden zur Verfügung:

Type getType("labelColi") 

um die Spalte mit dem Namen „labelColi“ der aktuellen Zeile und damit die Spalte von [SELECT] mit dieser Bezeichnung zu erhalten. Type bezeichnet den Typ des Feldes coli. Man kann die folgenden Methoden von [getType] verwenden: getInt, getLong, getString, getDouble, getFloat, getDate, ... Anstelle des Spaltennamens kann auch deren Position in der ausgeführten Abfrage [SELECT] verwendet werden:

Type getType(i) 

wobei i der Index der gewünschten Spalte ist (i ≥ 1).

  • Zeilen 15–17: Abruf der in der Abfrage BD gelesenen Werte;
  • Zeile 19: Die Transaktion wird bestätigt (man sagt auch „committed“). Dadurch wird sie beendet und die Ressourcen freigegeben, die die Transaktion SGBD für sie belegt hatte;
  • Zeile 25: Die Ressourcen werden in der Transaktion [finally] freigegeben. Diese ruft die folgende Methode [doFinally] auf:

private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // Abschluss von ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // Schließen [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        if (connexion != null) {
            try {
                // Verbindung schließen
                connexion.close();
            } catch (SQLException e3) {
                // Ausnahme behandeln
            }
        }
    }
  • Zeilen 3–9: Schließen des [ResultSet];
  • Zeilen 11–17: Schließen von [PreparedStatement];
  • Zeilen 18–27: Schließen der Verbindung;

Die Schließungen in den Zeilen 3–17 scheinen insofern redundant zu sein, als die Verbindung in den Zeilen 18–25 geschlossen wird. Tatsächlich sind sie in bestimmten Fällen jedoch nicht redundant, und es wird empfohlen, sie beizubehalten: [http://stackoverflow.com/questions/4507440/must-jdbc-resultsets-and-statements-be-closed-separately-although-the-connection].

  • Zeile 22: Die Ausnahme wird von der folgenden Methode [doCatchException] behandelt:

    private static void doCatchException(Connection connexion, Throwable th) {
        // Transaktion abbrechen
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // Ausnahme behandeln
        }
}
  • Zeilen 4–6: Die Transaktion wird abgebrochen. Dadurch wird sie beendet, und SGBD kann die für sie gebuchten Ressourcen freigeben;

3.2.4. Schritt 3 – Ausgabe der Befehle SQL und [INSERT, UPDATE, DELETE]

Die Befehle SQL und [INSERT, UPDATE, DELETE] sind Aktualisierungsvorgänge: Sie ändern die Datenbank, geben jedoch keine Zeilen zurück. Die einzige zurückgegebene Information ist die Anzahl der von der Aktualisierung betroffenen Zeilen.

Der Code


Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // Verbindung herstellen
            connexion = DriverManager.getConnection(url, user, passwd);
            // Transaktion starten
            connexion.setAutoCommit(false);
            // im Lese-/Schreibmodus
            connexion.setReadOnly(false);
            // Tabelle wird aktualisiert
            ps = connexion.prepareStatement("UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?");
            // Kategorie 1
            ps.setInt(1, 10);
            // Ausführung
            int nbLignes=ps.executeUpdate();
            // Transaktion festschreiben
            connexion.commit();
        } catch (SQLException e1) {
            // Ausnahme wird behandelt
            doCatchException(connexion, e1);
        } finally {
            // „finally“-Block wird verarbeitet
            doFinally(null, ps, connexion);
        }
    }
  • Zeile 9: Die Verbindung wird zum Lesen und Schreiben verwendet;
  • Zeile 11: ein [PreparedStatement] mit einem Parameter (durch „?“ symbolisiert). Es können mehrere Parameter vorhanden sein. Sie sind ab 1 nummeriert;
  • Zeile 13: Dem einzigen Parameter wird sein Wert zugewiesen. Der erste Parameter von [setType] ist die Position des Parameters im [PreparedStatement] (1, 2, ...) und der zweite der ihm zugewiesene Wert. Man kann die Methoden [setInt, setLong, setFloat, setDouble, setString, setDate, ...] verwenden;
  • Zeile 15: Es wird die Methode [executeUpdate] verwendet und nicht [executeQuery], die für Aufträge vom Typ SELECT reserviert ist. Die Methode gibt die Anzahl der von der Operation betroffenen Zeilen zurück. Kann 0 sein.
  • Zeile 17: Die Transaktion wird bestätigt;

3.2.5. Schritt 4 – Schließen der Verbindung

Eine Verbindung muss in einer Mehrbenutzerumgebung so schnell wie möglich geschlossen werden, da ein SGBD nur eine begrenzte Anzahl offener Verbindungen zulässt. In den vorangegangenen Beispielen wurde sie in der Klausel [finally] der Operationen SQL geschlossen, damit sie unabhängig davon, ob eine Ausnahme aufgetreten ist oder nicht, geschlossen wird.

3.3. Konfiguration der Schicht JDBC von SGBD MySQL5

Wir werden das Projekt [mysql-config-jdbc] untersuchen, das die unten stehende Ebene JDBC konfiguriert:

3.3.1. Das Eclipse-Projekt

 

3.3.2. Maven-Konfiguration

Die Datei [pom.xml] des Projekts lautet wie folgt:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>dvp.spring.database</groupId>
    <artifactId>generic-config-jdbc</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>configuration generic jdbc</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <!-- Variablenabhängigkeiten ********************************************** -->
        <!-- Treiber JDBC des SGBD -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- Konstante Abhängigkeiten ********************************************** -->
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- Bibliothek jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- Google Guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
        </dependency>
        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <!-- Protokolle -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>

In dieser Maven-Konfiguration wurden eine Reihe von Archiven zusammengefasst, die entweder für das Projekt [mysql-config-jdbc] oder für die darauf aufbauenden Projekte erforderlich sind:

  • Zeilen 4–6: das vom Projekt generierte Maven-Artefakt. Wie bereits erwähnt, generieren alle Projekte vom Typ [*-config-jdbc] dasselbe Artefakt. Es dürfen daher nicht zwei Projekte vom Typ [*-config-jdbc] gleichzeitig geladen werden;
  • Zeilen 9–13: das übergeordnete Maven-Projekt. Es definiert die Versionen einer Vielzahl von Archiven, die vom Spring-Ökosystem verwendet werden. Dadurch muss diese Angabe in den davon abgeleiteten Projekten entfallen;
  • Zeilen 18–21: Das Treiber-Archiv JDBC für SGBD und MySQL5. Dies ist das einzige Archiv, das für das Projekt [spring-jdbc-01] benötigt wird;
  • Zeilen 24–27: Das Artefakt [tomcat-jdbc] enthält ein Archiv, das für die Projekte JDBC und [spring-jdbc-02 à 04] erforderlich ist;
  • Zeilen 29–36: enthalten die für die Verwaltung von jSON erforderlichen Bibliotheken. Diese werden in fast allen Projekten des Dokuments verwendet;
  • Zeilen 38–42: Google Guava ist eine Bibliothek zur Verwaltung von Sammlungen. Sie wird in fast allen Projekten des Dokuments verwendet;
  • Zeilen 43–52: Bibliotheken zum Schreiben von Tests, die Spring und JUnit integrieren. Werden in fast allen Projekten des Dokuments verwendet;
  • Zeilen 54–57: Protokollierungsbibliotheken. Werden in fast allen Projekten des Dokuments verwendet;
  • Zeilen 67–71: Das Plugin, mit dem das Artefakt des Projekts [mysql-config-jdbc] im lokalen Maven-Repository installiert werden kann;

3.3.3. Die Konfigurationsklasse [ConfigJdbc]

  

Die Klasse [ConfigJdbc] sieht wie folgt aus:


package generic.jdbc.config;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;

public class ConfigJdbc {

    // Verbindungsparameter
    public final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
    public final static String URL_DBPRODUITS = "jdbc:mysql://localhost:3306/dbproduits";
    public final static String USER_DBPRODUITS = "root";
    public final static String PASSWD_DBPRODUITS = "root";
...
    // Befehle SQL [jdbc-01, jdbc-02]
    public final static String V1_INSERT_PRODUITS_WITH_ID = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
    public final static String V1_DELETE_PRODUITS = "DELETE FROM PRODUITS";
    //public final static String V1_DELETE_PRODUITS = String.format("DELETE FROM %s", TAB_PRODUITS);
    public final static String V1_SELECT_PRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    public final static String V1_UPDATE_PRODUITS = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
    public final static String V1_INSERT_PRODUITS_2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";

    // Aufträge SQL [jdbc-03]
    public final static String V2_INSERT_PRODUITS = "INSERT INTO PRODUITS(NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?)";
    public final static String V2_DELETE_ALLPRODUITS = "DELETE FROM PRODUITS";
    public final static String V2_DELETE_PRODUITS = "DELETE FROM PRODUITS WHERE ID=?";
    public final static String V2_SELECT_ALLPRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    public final static String V2_SELECT_PRODUIT_BYID = "SELECT NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE ID=?";
    public final static String V2_SELECT_PRODUIT_BYNAME = "SELECT ID, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE NOM=?";
    public final static String V2_UPDATE_PRODUITS = "UPDATE PRODUITS SET NOM=?, PRIX=?, CATEGORIE=?, DESCRIPTION=? WHERE ID=?";

...

}

Die Klasse [ConfigJdbc] dient zur Konfiguration der Schicht JDBC der vier Projekte [spring-jdbc-01 à 04]. Der größte Teil der Konfiguration betrifft das Projekt [spring-jdbc-04]. Wir werden diesen Teil vorstellen, wenn wir dieses Projekt näher betrachten. Oben wurde nur die Konfiguration der Projekte [spring-jdbc-01 à 03] beibehalten.

  • Zeilen 14–17: Die Verbindungsparameter zur Datenbank MySQL5 [dbproduits];
  • Zeilen 20–25: die Befehle SQL, die in den Projekten [spring-jdbc-01 et 02] verwendet werden;
  • Zeilen 28–34: die Befehle SQL, die im Projekt [spring-jdbc-03] verwendet werden;

Diese Befehle SQL nutzen die Tabelle [PRODUITS] aus der Datenbank MySQL5 [dbproduits], deren Struktur wie folgt aussieht:

 
  • [ID]: Primärschlüssel im Modus AUTO_INCREMENT (wenn kein Primärschlüssel angegeben wird, generiert SGBD diesen);
  • [NOM]: Name eines Produkts – eindeutig;
  • [CATEGORIE]: Nummer seiner Kategorie;
  • [PRIX]: Preis;
  • [DESCRIPTION]: eine Beschreibung des Produkts;

3.3.4. Die Klasse [Produit]

  

Die Klasse [Produit] ist die Darstellung einer Zeile aus der Tabelle [PRODUITS]:


package generic.jdbc.entities.dbproduits;

public class Produit {

    // Felder
    private int id;
    private String nom;
    private int categorie;
    private double prix;
    private String description;

    // Konstruktoren
    public Produit() {

    }

    public Produit(int id, String nom, int categorie, double prix, String description) {
        this.id = id;
        this.nom = nom;
        this.categorie = categorie;
        this.prix = prix;
        this.description = description;
    }

    // Getter und Setter
...
}

Später müssen wir zwei Produkte vergleichen, um festzustellen, ob sie gleich sind oder nicht. Wir sagen, dass zwei Produkte gleich sind, wenn alle ihre Felder gleich sind. Dazu werden wir die Methode [equals] der Klasse [Object] neu definieren, von der die Klasse [Produit] abgeleitet ist:


    // Gleichheitsmethode
    @Override
    public boolean equals(Object o) {
        // einfache Fälle
        if (o == null || o.getClass() != this.getClass()) {
            return false;
        }
        Produit p = (Produit) o;
        return this == o
                || (this.id == p.id && this.nom.equals(p.getNom()) && this.categorie == p.categorie
                        && Math.abs(this.prix - p.prix) < 1e-6 && this.description.equals(p.description));
}
  • Zeile 3: Die Methode [equals] erhält ein Objekt o, das sie mit dem Objekt this vergleichen muss;
  • Zeilen 5–7: die einfachen Fälle, in denen sofort festgestellt werden kann, dass die beiden Objekte nicht gleich sind. [Object].getClass() liefert eine Instanz des Typs [Class], eines Typs, der die tatsächliche Klasse des Objekts darstellt;
  • Zeile 8: Das Objekt o wird in das Produkt p umgewandelt;
  • Zeile 9: Sind die beiden Referenzen o und p auf ein Produkt gleich, handelt es sich physisch um dasselbe Produkt;
  • Zeile 9: Sind o und p zwei verschiedene Referenzen auf zwei Produkte mit denselben Feldern, so gelten sie als gleich. Da der Preis vom Typ [double] ist und es in der Informatik keine exakte Darstellung von reellen Zahlen gibt, betrachten wir zwei Preise als identisch, wenn sie bis auf 10⁻⁶ genau gleich sind;

Außerdem werden wir die Methode [hasCode] der Klasse [Object] neu definieren:


    // Hashcode
    @Override
    public int hashCode() {
        return id + 2 * nom.hashCode() + 3 * categorie + 4 * description.hashCode();
}

Die Werte von hashCode zweier Produkte müssen identisch sein, wenn die Methode [equals] diese beiden Produkte als gleich erklärt hat. Dieser Wert von hashCode wird verwendet, um Objekte in Mengen wie beispielsweise Wörterbüchern zuzuordnen. Wenn also zwei Produkte identisch sind, haben sie auch denselben Wert für hashCode.

3.3.5. Die Ausnahme [UncheckedException]

  

Betrachten wir die folgende Architektur:

  • Die Schicht [JDBC] löst Ausnahmen vom Typ [SQLException] aus. Diese Ausnahme muss durch die Schichten nach oben weitergeleitet werden, bis sie die oberste Schicht erreicht, in diesem Fall die Testschicht;

Die Schicht [DAO] könnte sich damit begnügen, die Ausnahme [SQLException] bis zur Testschicht weiterleiten zu lassen. Da es sich bei dieser Ausnahme jedoch um eine unkontrollierte Ausnahme handelt (sie leitet sich direkt von [Exception] ab), würde dies bedeuten, dass die Schnittstelle [IDao] der Schicht [DAO] wie folgt aussehen müsste:


public interface IDao {

    // Produkte hinzufügen
    public List<Produit> addProduits(List<Produit> produits) throws SQLException;

    // Liste aller Produkte
    public List<Produit> getAllProduits() throws SQLException;

    // ein bestimmtes Produkt
    public Produit getProduitById(int id) throws SQLException;

    public Produit getProduitByName(String name) throws SQLException;

    // Mehrere Produkte aktualisieren
    public int updateProduits(List<Produit> produits) throws SQLException;

    // Alle Produkte löschen
    public int deleteAllProduits() throws SQLException;

    // Mehrere Produkte löschen
    public int deleteProduits(int[] ids) throws SQLException;
}

Und das ist sehr ärgerlich, da es uns daran hindert, die Schnittstelle [IDao] durch eine Klasse zu implementieren, die eine andere Ausnahme auslösen würde. Um dieses Problem zu umgehen, löst die Schicht [DAO] eine unkontrollierte Ausnahme [DaoException] (abgeleitet von [RuntimeException]) aus, wodurch wir die Klausel [throws] in der Signatur der Schnittstellenmethoden vermeiden können. Dadurch kann die Schnittstelle von jeder Klasse implementiert werden, die ebenfalls eine unkontrollierte Ausnahme auslöst, die sich von der Ausnahme [DaoException] unterscheiden kann. Unsere Architektur sieht nun wie folgt aus:

Um die Erstellung unkontrollierter Ausnahmen für verschiedene Schichten einer Anwendung zu vereinfachen, erstellen wir für diese eine übergeordnete Klasse [UncheckedException]:

  

package generic.jdbc.infrastructure;

import java.util.ArrayList;
import java.util.List;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

// generische Ausnahmeklasse
// Die Ausnahme ist unkontrolliert

public class UncheckedException extends RuntimeException {

    // Seriennummer ID generiert
    private static final long serialVersionUID = -2924871763340170310L;

    // Eigenschaften
    private int code;
    private String trace;
    private List<ShortException> exceptions;

    // Konstruktoren
    public UncheckedException() {
        super();
    }

    public UncheckedException(int code, Throwable e, String simpleClassName) {
        super(e);
        // lokal
        this.code = code;
        this.exceptions = getErreursForException(e);
        // Ablaufverfolgung
        String fileName = String.format("%s.java", simpleClassName);
        StackTraceElement[] traces = e.getStackTrace();
        boolean trouve = false;
        for (int i = 0; !trouve && i < traces.length; i++) {
            StackTraceElement trace = traces[i];
            if (fileName.equals(trace.getFileName())) {
                this.trace = String.format("[%s,%s,%s]", simpleClassName, trace.getMethodName(), trace.getLineNumber());
                trouve = true;
            }
        }
    }

    @Override
    public String getMessage() {
        return this.toString();
    }

    @Override
    public void printStackTrace() {
        System.out.println(this);
    }

    // Liste der Fehlermeldungen einer Ausnahme
    private List<ShortException> getErreursForException(Throwable th) {
        // Die Elemente des Ausnahmestapels werden abgerufen
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // die aktuelle Ausnahme abrufen
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // nächste Ausnahme
            cause = cause.getCause();
        }
        return exceptions;
    }

    @Override
    public String toString() {
        ObjectMapper jsonMapper = new ObjectMapper();
        try {
            return String.format("[code=%s, trace=%s, exceptions=%s", code, trace, jsonMapper.writeValueAsString(exceptions));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }

    // Getter und Setter
...
}
  • Zeile 12: Die Klasse leitet sich von [RuntimeException] ab und ist somit ein Typ einer unkontrollierten Ausnahme. Sie dient dazu, eine kontrollierte Ausnahme (SQLException) in einen Typ einer unkontrollierten Ausnahme (UncheckedException) zu kapseln;
  • Um Ausnahmen vom Typ [UncheckedException] voneinander zu unterscheiden, kann ihnen ein Code zugewiesen werden, der im privaten Feld in Zeile 18 gespeichert wird. Ein Java-Code, der eine Ausnahme vom Typ [UncheckedException] abfängt, hat über die Methode [getCode] (Zeile 80 ff.) Zugriff auf diesen Fehlercode;
  • Zeile 20: Speichert die Fehlermeldungen aus dem Ausnahmestapel der gekapselten Ausnahme;
  • Zeilen 23–43: die verschiedenen Möglichkeiten, ein Objekt vom Typ [UncheckedException] zu erstellen;
  • Zeilen 56–67: eine private Methode, mit der die Fehlerliste aus Zeile 20 ausgehend von einem Objekt vom Typ [Throwable] oder einem davon abgeleiteten Typ, insbesondere vom Typ [Exception], erstellt werden kann;
  • Zeilen 69–78: Die Methode [toString] gibt eine Zeichenkette zurück, die die Ausnahme darstellt. Um die Fehlerliste aus Zeile 20 anzuzeigen, verwendet sie eine Bibliothek namens jSON. Diese ist in den Maven-Abhängigkeiten des Projekts enthalten:

        <!-- Bibliothek jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
</dependency>
  • Zeilen 45–48: definieren die Methode [getMessage] der übergeordneten Klasse [RuntimeException] neu. Diese gibt hier die Signatur [toString] der Klasse zurück;
  • Zeilen 50–53: definieren die Methode [printStackTrace] der übergeordneten Klasse [RuntimeException] neu. Es wird die Signatur [toString] der Klasse angezeigt;

Die Klasse [UncheckedException] speichert im Feld der Zeile 20 eine Liste von Ausnahmen, die durch den folgenden Typ [ShortException] beschrieben werden:


package pam.dao.exceptions;

public class ShortException {

    // Eigenschaften
    private String className;
    private String errorMessage;

    // Konstruktoren
    public ShortException() {

    }

    public ShortException(String className, String errorMessage) {
        this.className = className;
        this.errorMessage = errorMessage;
    }

    // Getter und Setter
...
}
  • Zeile 6: der Name der Klasse der aufgetretenen Ausnahme;
  • Zeile 7: die zugehörige Fehlermeldung;

Betrachten wir den folgenden Konstruktor der Klasse [UncheckedException]:


    public UncheckedException(int code, Throwable e, String simpleClassName) {
        super(e);
        // lokal
        this.code = code;
        this.exceptions = getErreursForException(e);
        // Protokollierung
        String fileName = String.format("%s.java", simpleClassName);
        StackTraceElement[] traces = e.getStackTrace();
        boolean trouve = false;
        for (int i = 0; !trouve && i < traces.length; i++) {
            StackTraceElement trace = traces[i];
            if (fileName.equals(trace.getFileName())) {
                this.trace = String.format("[%s,%s,%s]", simpleClassName, trace.getMethodName(), trace.getLineNumber());
                trouve = true;
            }
        }
}
  • Zeile 1: Die Parameter lauten wie folgt:
    • [code]: ein Fehlercode;
    • [e]: die Ausnahm, die gekapselt wird. [Throwable] ist die übergeordnete Klasse der Klasse [Exception] und leitet sich direkt von der Klasse [Object] ab. Sie ist die übergeordnete Klasse aller Klassen C, mit denen man [throw c;] schreiben kann, wobei c eine Instanz von C ist;
    • [simpleClassName]: der einfache Name der Benutzerklasse, in der die Ausnahme e erkannt wurde;
  • Zeile 4: Der Fehlercode wird gespeichert;
  • Zeile 5: Die Liste von [ShortException] wird aus dem als Parameter übergebenen [Throwable e] erstellt;
  • Zeilen 7–16: Hier werden die sogenannten Ausnahmespuren untersucht. Eine ursprüngliche Ausnahme tritt an einer bestimmten Stelle im Code auf und wird dann an die Methode weitergeleitet, die die Methode aufgerufen hat, in der die Ausnahme aufgetreten ist, und so weiter, bis sie durch ein „try/catch“-Block abgefangen wird. Bei dieser Weiterleitung hinterlässt die ursprüngliche Ausnahme Spuren, die im Array [e.stackTrace] der Ausnahme e gespeichert werden. Diese werden hier in Zeile 8 anhand des als Parameter übergebenen [Throwable e] abgerufen. Jedes Element vom Typ [StackTraceElement] ist ein Objekt, das unter anderem folgende Felder enthält:
    • [fileName]: der Name der Java-Datei, in der die Ausnahme aufgetreten ist;
    • [lineNumber]: die Zeilennummer in dieser Datei, in der die Ausnahme aufgetreten ist;
    • [methodName]: der Name der Methode in dieser Datei, in der die Ausnahme aufgetreten ist;
  • In den Zeilen 10–16 wird im Array der Trace-Daten der als Parameter übergebenen Ausnahme nach dem ersten Vorkommen der Bedingung [trace.fileName==simpleClassName.java] gesucht, wobei [simpleClassName] der dritte Parameter des Konstruktors ist. Die Idee besteht darin, zu speichern, an welcher Stelle im Anwendercode die Ausnahme aufgetreten ist. Der Anwendercode kapselt eine Ausnahme wie folgt:
1
2
3
4
5
6
7
try{
// Code, der eine kontrollierte Ausnahme auslösen kann
...
}catch(UnTypeDexception e){
// Die kontrollierte Ausnahme e wird in eine unkontrollierte Ausnahme gekapselt
    throw new UncheckedException(189,e,getClass().getSimpleClassName())
}
  • Zeile 13: Es wird eine Zeichenkette vom Typ [fileName, methodName, lineNumber] erstellt, die die Stelle im Anwendercode kennzeichnet, an der die Ausnahme abgefangen wurde;

Betrachten wir nun den Code, der die Liste der Ausnahmen aus dem Ausnahmestapel der Ausnahme [Throwable th] aufzeichnet, die durch den vorherigen Konstruktor gekapselt wurde:


    // Liste der Fehlermeldungen einer Ausnahme
    private List<ShortException> getErreursForException(Throwable th) {
        // Die Elemente des Ausnahmestapels werden abgerufen
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // Abrufen der aktuellen Ausnahme
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // nächste Ausnahme
            cause = cause.getCause();
        }
        return exceptions;
}

Während ihres Aufstiegs zur Methode, die sie mittels eines try/catch-Blocks abgefangen hat, konnte die ursprüngliche Ausnahme e in eine weitere Ausnahme gekapselt worden sein. Es ist dann diese weitere Ausnahme, die ihren Aufstieg zur Methode fortsetzt, die sie endgültig abfangen wird. Sie kann daher ebenfalls einer Kapselung unterzogen werden. Letztendlich findet eine Methode, die beschließt, eine Ausnahme th abzufangen und zu verarbeiten, die ursprüngliche Ausnahme e ganz unten in einem Stapel von Ausnahmen verborgen vor. So ist im obigen Beispiel der Parameter [Throwable th] nur die Spitze des Eisbergs der Ausnahmen. Anhand ihres Attributs [th.cause] lässt sich die Ausnahme ermitteln, die sie selbst kapselt. Und so weiter. Wenn eine Ausnahme e den Wert [e.getCause()==null] hat, ist e die ursprüngliche Ausnahme.

  • Zeile 8: Für jede Ausnahme im Ausnahmestapel von [Throwable th] werden zwei Informationen gespeichert:
    • [getClass().getName()]: der vollständige Name der Ausnahme;
    • [getMessage()]: die zugehörige Fehlermeldung;

3.4. Beispiel-01

3.4.1. Die Architektur des Projekts

In diesem Beispiel nutzt ein Konsolenprogramm die Schnittstelle der Schicht [JDBC].

3.4.2. Das Eclipse-Projekt

Wir erstellen ein Spring-/Maven-Projekt [spring-jdbc-01] gemäß der Vorgehensweise in Abschnitt 2.5.2.1.

  

Das Projekt ist ein Maven-Projekt, das durch die folgende Datei [pom.xml] definiert ist:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-jdbc-generic-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-jdbc-generic-01</name>
    <description>Demo project for API JDBC</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- Übergeordnete Instanz aus dem Repository abrufen -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- Konfiguration JDBC des SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • Zeilen 28–32: Das Projekt verwendet das Artefakt [generic-config-jdbc] aus dem Projekt [mysql-config-jdbc], das wir gerade betrachtet haben. Das Projekt [spring-jdbc-01] hat somit Zugriff auf alle Elemente des Projekts [mysql-config-jdbc];

Diesen letzten Punkt lässt sich auf zwei Arten erkennen, wenn man die Maven-Abhängigkeiten des Projekts überprüft:

  • In [2] sieht man, dass das Projekt [mysql-config-jdbc] zu den Maven-Abhängigkeiten des Projekts gehört. Da diese im Classpath des Projekts enthalten sind, bedeutet dies, dass auch das Projekt [mysql-config-jdbc] in diesem Classpath enthalten ist und dass daher seine Klassen und Schnittstellen im Projekt [spring-jdbc-01] sichtbar sind;

Das Maven-Projekt [mysql-config-jdbc] muss nicht auf der Registerkarte [Package Explorer] vorhanden sein, um von anderen Maven-Projekten genutzt werden zu können. Es reicht aus, wenn es im lokalen Maven-Repository vorhanden ist. Im Gegensatz zu einem IDE wie NetBeans erfolgt diese Einbindung bei Eclipse nicht automatisch. Sie muss erzwungen werden:

Die Voraussetzungen, die diese Generierung ermöglichen, haben wir in Abschnitt 2.3.5 behandelt. Sobald die Generierung abgeschlossen ist, kann das Projekt „[mysql-config-jdbc]“ aus der Registerkarte „[Package Explorer]“ entfernt werden:

  • Man darf [3] nicht ankreuzen, da dies das Projekt physisch von der Festplatte löscht und es somit unwiederherstellbar macht;

Dieser Vorgang löst die Neuberechnung der Maven-Abhängigkeiten der Projekte aus, die von dem aus dem [Package Explorer] entfernten Projekt abhängen. Dadurch ändert sich der Zweig [Maven Dependencies] dieser Projekte. Beispielsweise wird für das Projekt „[spring-jdbc-01]“ der Zweig „[Maven Dependencies]“ wie folgt geändert:

Diesmal bezieht sich die Abhängigkeit nicht mehr auf ein Projekt, sondern auf dessen Maven-Artefakt, in diesem Fall das Artefakt [generic-config-jdbc] [1]. Man sieht, dass man tatsächlich Zugriff auf alle Klassen und Schnittstellen dieses Artefakts hat. Wie bereits erwähnt, wird dieses Artefakt von allen Projekten [*-config-jdbc] generiert. Um Fehler zu vermeiden, gehen wir wie folgt vor:

  • immer nur ein einziges Projekt [*-config-jdbc] auf der Registerkarte [Package Explorer] beibehalten;
  • aktualisieren die Maven-Konfiguration aller Projekte auf der Registerkarte „[Package Explorer]“ (Alt-F5), damit diese das verwendete Projekt „[*-config-jdbc]“ in ihren Maven-Abhängigkeiten angeben;

3.4.3. Das Grundgerüst der Hauptklasse

  

Das Grundgerüst der Hauptklasse [IntroJdbc01] sieht wie folgt aus:


package spring.jdbc;

import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class IntroJdbc01 {

    // Konstanten
    final static ObjectMapper jsonMapper = new ObjectMapper();

    public static void main(String[] args) {
        // Laden des Treibers JDBC aus dem SGBD
        try {
            Class.forName(ConfigJdbc.DRIVER_CLASSNAME);
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // Die Tabelle [PRODUITS] wird geleert
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // die Tabelle wird gefüllt
        System.out.println(String.format("------------------------------ %s", "Remplissage de la table [PRODUITS]"));
        insert();
        // die Tabelle wird gelesen
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // Aktualisierung
        System.out.println(String.format("------------------------------ %s", "Mise à jour de la table [PRODUITS]"));
        update();
        // Anzeige
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // die Tabelle wird geleert [PRODUITS]
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // Anzeige
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // INSERTion mit zwei identischen Elementen
        // Der Befehl INSERTion muss fehlschlagen, und aufgrund der Transaktion wird keines der beiden Elemente eingefügt
        System.out.println(String.format("------------------------------ %s",
                "Insertion de deux produits de même clé primaire dans la table [PRODUITS]"));
        insert2();
        // wird überprüft
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // fertig
        System.out.println(String.format("------------------------------ %s", "Travail terminé"));
    }

    // Produktliste
    private static void select() {
    ...
    }

    // Anzeige eines Objekts jSON
    private static void affiche(Object object) {
...
    }

    // Produkte löschen
    public static void delete() {
...
    }

    // Produkte hinzufügen
    public static void insert() {
...
    }

    // Hinzufügen von 2 Produkten mit denselben Primärschlüsseln
    public static void insert2() {
...
    }

    // Aktualisierung bestimmter Produkte
    public static void update() {
...
    }

    private static void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // Schließen von ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // Schließen von [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        if (connexion != null) {
            try {
                // Verbindung schließen
                connexion.close();
            } catch (SQLException e3) {
                // Fehlermeldungen werden angezeigt
                show("Les erreurs suivantes se sont produites lors de la fermeture de la connexion",
                        getErreursFromThrowable(e3));
            }
        }
    }

    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // Fehlermeldungen werden angezeigt
        show(title, getErreursFromThrowable(th));
        // Transaktion abbrechen
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // Fehlermeldungen werden angezeigt
            show("Erreur lors de l'annulation de la transaction", getErreursFromThrowable(e2));
        }
    }

    private static List<String> getErreursFromThrowable(Throwable th) {
        // Die Liste der Fehlermeldungen der Ausnahme wird abgerufen
        List<String> erreurs = new ArrayList<String>();
        while (th != null) {
            // Fehlermeldung des „Throwable“
            erreurs.add(th.getMessage());
            // Weiter zur Ursache des „Throwable“
            th = th.getCause();
        }
        // Ergebnis
        return erreurs;
    }

    private static void show(String title, List<String> messages) {
        // Titel
        System.out.println(String.format("%s : ", title));
        // Meldungen
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }
}
  • Zeilen 23–29: Laden des Treibers JDBC aus dem Projekt SGBD. In Zeile 25 wird die im Projekt [mysql-config-jdbc] definierte Konstante [ConfigJdbc.DRIVER_CLASSNAME] verwendet;
  • Zeilen 136–147: Die Methode [getErreursFromThrowable] gibt die Liste der Fehlermeldungen zurück, die in einem Objekt vom Typ [Throwable] gekapselt sind, welches die übergeordnete Klasse der Klasse [Exception] ist. Eine Ausnahme kann eine weitere Ausnahme enthalten, die mit der Methode [Throwable].getCause() abgerufen wird. Auf diese Weise werden alle im Objekt [Throwable] gekapselten Ausnahmen durchlaufen;
  • Zeilen 149–156: Die Methode [show(String title, List<String> messages)] gibt die Meldungen aus, denen der Text [title] vorangestellt ist;
  • Zeilen 122–134: Die Methode [doCatchException(String title, Connection connexion, Throwable th))] behandelt die Ausnahmen, die bei den Methoden der Klasse auftreten. Die behandelte Ausnahme wird durch den Parameter [Throwable th] dargestellt. Das Ziel der Methode ist:
    • die laufende Transaktion des Objekts [Connection connexion] abzubrechen (Zeilen 127–129);
    • die in der Ausnahme [Throwable th] enthaltenen Fehlermeldungen auszugeben (Zeilen 124, 132);
  • Zeilen 93–120: Die Methode [doFinally(ResultSet rs, PreparedStatement ps, Connection connexion)] verwaltet den Zweig [finally] der Zugriffsmethoden auf SGBD. Ihr Zweck ist es, die durch die Verbindung belegten Ressourcen freizugeben;

3.4.4. Löschen des Inhalts der Produkttabelle

Die Methode [delete] löscht den Inhalt der Tabelle:


    // Produkte löschen
    public static void delete() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // Verbindung herstellen
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // Transaktionsstart
            connexion.setAutoCommit(false);
            // im Lese-/Schreibmodus
            connexion.setReadOnly(false);
            // Tabelle wird geleert [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V1_DELETE_PRODUITS);
            ps.executeUpdate();
            // Transaktion festschreiben
            connexion.commit();
        } catch (SQLException e1) {
            // Ausnahme wird behandelt
            doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
        } finally {
            // „finally“-Block wird verarbeitet
            doFinally(null, ps, connexion);
        }
}

In Zeile 7 werden die folgenden Konstanten der Klasse [ConfigJdbc] verwendet:


public final static String URL_DBPRODUITS = "jdbc:mysql://localhost:3306/dbproduits";
public final static String USER_DBPRODUITS = "root";
public final static String PASSWD_DBPRODUITS = "";

In Zeile 13 lautet der vorbereitete Befehl SQL wie folgt:


public final static String V1_DELETE_PRODUITS = "DELETE FROM PRODUITS";

Die Methode [delete] verwendet Transaktionen. Eine Transaktion ermöglicht es, Aufträge SQL zu bündeln, die entweder alle erfolgreich ausgeführt oder alle storniert werden müssen. Es sind vier Vorgänge zu beachten:

  • Start einer Transaktion: [connexion.setAutoCommit(false)];
  • Ende einer Transaktion mit Erfolg: [connexion.commit()]. In diesem Fall werden alle während der Transaktion mit BD durchgeführten Vorgänge bestätigt;
  • Abbruch einer Transaktion: [connexion.rollback()]. In diesem Fall werden alle während der Transaktion unter BD durchgeführten Vorgänge rückgängig gemacht;

In unseren Beispielen brechen wir die Transaktion jedes Mal, wenn eine Ausnahme auftritt, in der Methode [doCatchException] ab:


    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // Fehlermeldungen werden angezeigt
        Static.show(title, Static.getErreursFromThrowable(th));
        // Transaktion rückgängig machen
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // Fehlermeldungen werden angezeigt
            Static.show("Erreur lors de l'annulation de la transaction", Static.getErreursFromThrowable(e2));
        }
}

3.4.5. Erstellung des Inhalts der Produkttabelle

Die Methode [insert] erstellt den Inhalt der Tabelle:


public static void insert() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // Verbindung wird hergestellt
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // Transaktion starten
            connexion.setAutoCommit(false);
            // im Lese-/Schreibmodus
            connexion.setReadOnly(false);
            // Tabelle wird gefüllt
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_WITH_ID);
            for (int i = 0; i < 10; i++) {
                // Vorbereitung
                int n = i + 1;
                ps.setInt(1, n);
                ps.setString(2, String.format("NOM%s", n));
                ps.setInt(3, n / 5 + 1);
                ps.setDouble(4, 100 * (1 + (double) i / 100));
                ps.setString(5, String.format("DESC%s", n));
                // Ausführung
                ps.executeUpdate();
            }
            // Transaktion festschreiben
            connexion.commit();
        } catch (SQLException e1) {
            // Ausnahme wird behandelt
            doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
        } finally {
            // Behandlung des „finally“-Blocks
            doFinally(null, ps, connexion);
        }
    }

Zeile 12: Der vorbereitete Befehl SQL lautet wie folgt:


public final static String V1_INSERT_PRODUITS_WITH_ID = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";

3.4.6. Anzeige des Inhalts der Produkttabelle

Die Methode [select] zeigt den Inhalt der Tabelle an:


// Produktliste
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // Verbindung herstellen
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // Transaktion starten
            connexion.setAutoCommit(false);
            // im schreibgeschützten Modus
            connexion.setReadOnly(true);
            // die Tabelle [PRODUITS] wird gelesen
            ps = connexion.prepareStatement(ConfigJdbc.V1_SELECT_PRODUITS);
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                affiche(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // Transaktion festschreiben
            connexion.commit();
        } catch (SQLException e1) {
            // Ausnahme wird behandelt
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // „finally“-Block wird verarbeitet
            doFinally(rs, ps, connexion);
        }
    }

In Zeile 14 lautet der vorbereitete Befehl SQL wie folgt:


public final static String V1_SELECT_PRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";

Die Methode [affiche] (Zeile 18) lautet wie folgt:


    // Anzeige eines Objekts jSON
    private static void affiche(Object object) {
        try {
            System.out.println(jsonMapper.writeValueAsString(object));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
}

Sie zeigt die Darstellung jSON des als Parameter übergebenen Objekts an (siehe jSON, Abschnitt 23.12).

3.4.7. Aktualisierung des Tabelleninhalts

Die Methode [update] aktualisiert bestimmte Produkte:


    // Aktualisierung bestimmter Produkte
    public static void update() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // Verbindung wird hergestellt
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // Transaktion starten
            connexion.setAutoCommit(false);
            // im Lese-/Schreibmodus
            connexion.setReadOnly(false);
            // Die Tabelle wird aktualisiert
            ps = connexion.prepareStatement(ConfigJdbc.V1_UPDATE_PRODUITS);
            // Kategorie 1
            ps.setInt(1, 1);
            // Ausführung
            ps.executeUpdate();
            // Transaktion festschreiben
            connexion.commit();
        } catch (SQLException e1) {
            // Ausnahme wird behandelt
            doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
        } finally {
            // „finally“-Block wird verarbeitet
            doFinally(null, ps, connexion);
        }
}

Zeile 13: Der vorbereitete Befehl SQL lautet wie folgt:


public final static String V1_UPDATE_PRODUITS = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";

3.4.8. Rolle der Transaktion

Die Methode [insert2] fügt zwei Produkte mit demselben Primärschlüssel in die Tabelle ein, was nicht möglich ist. Da es sich um eine Transaktion handelt, wird der erste Einfügevorgang rückgängig gemacht.


    // Hinzufügen von 2 Produkten mit denselben Primärschlüsseln
    public static void insert2() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // Verbindung herstellen
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // Transaktion starten
            connexion.setAutoCommit(false);
            // im Lese-/Schreibmodus
            connexion.setReadOnly(false);
            // 1 Zeile wird hinzugefügt
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_2);
            // Ausführung
            ps.executeUpdate();
            // Es wird dieselbe Zeile ein zweites Mal hinzugefügt, also mit demselben Primärschlüssel
            // INSERTion muss fehlschlagen, und aufgrund der Transaktion darf keines der beiden Elemente eingefügt werden
            ps.executeUpdate();
            // Transaktion abschließen
            connexion.commit();
        } catch (SQLException e1) {
            // die Ausnahme wird behandelt
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire",
                    connexion, e1);
        } finally {
            // „finally“-Block wird verarbeitet
            doFinally(null, ps, connexion);
        }
}

Zeile 13: Der vorbereitete Befehl SQL lautet wie folgt:


public final static String V1_INSERT_PRODUITS_2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";

3.4.9. Ergebnisse

Wir führen die Ausführungskonfiguration mit dem Namen [spring-jdbc-generic-01.IntroJdbc01] aus:

 

Es werden folgende Konsolenergebnisse ausgegeben:


------------------------------ Vidage de la table [PRODUITS]
------------------------------ Remplissage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
------------------------------ Mise à jour de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":110.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
------------------------------ Vidage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Insertion de deux produits de même clé primaire dans la table [PRODUITS]
Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire : 
- Duplicate entry '100' for key 'PRIMARY'
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Travail terminé
  • Zeile 30: Vor dem Einfügen der beiden Produkte mit demselben Primärschlüssel ist die Tabelle leer;
  • Zeile 35: Nach dem Einfügen der beiden Produkte mit demselben Primärschlüssel ist die Tabelle leer. Dies verdeutlicht die Rolle der Transaktion:
    • Das erste Einfügen ist erfolgreich. Es gibt keinen Grund, warum es fehlschlagen sollte;
    • der zweite Einfügevorgang schlägt fehl (Zeile 32). Da sich diese beiden Einfügevorgänge innerhalb derselben Transaktion befinden, werden alle Befehle SQL dieser Transaktion rückgängig gemacht, darunter auch der erste Einfügevorgang.

3.4.10. Fazit

Bemerkenswert am vorstehenden Code ist der große Umfang der Ausnahmebehandlung für [SQLException]. Da jede Operation mit JDBC diese Ausnahme auslösen kann, gibt es im Code zahlreiche try/catch-Anweisungen.

3.5. Beispiel-02

Wir greifen die vorherige Anwendung wieder auf und verwenden dabei eine Datenquelle vom Typ [javax.sql.DataSource]:

Image

Wir verwenden eine Datenquelle, die durch die Klasse [org.apache.tomcat.jdbc.pool.DataSource] implementiert wird. Diese Klasse nutzt einen Verbindungspool, d. h. eine Reihe offener Verbindungen:

  • Wenn der Pool instanziiert wird, wird eine bestimmte Anzahl von Verbindungen zur Datenbank geöffnet. Diese Anzahl ist konfigurierbar;
  • wenn der Java-Code eine Verbindung öffnet, wird diese vom Pool bereitgestellt;
  • Wenn der Java-Code eine Verbindung schließt, wird diese an den Pool zurückgegeben;

Letztendlich werden die Verbindungen nur einmal geöffnet, was die Leistung beim Zugriff auf die Datenbank verbessert. Die Datenquelle wird in einer Spring-Konfigurationsklasse definiert

3.5.1. Die Architektur des Projekts

In diesem Beispiel nutzt ein Konsolenprogramm die Schnittstelle der Schicht [JDBC].

3.5.2. Das Eclipse-Projekt

Das neue Eclipse-Projekt kann durch Kopieren des vorherigen Projekts [1-6] erstellt werden:

Anschließend wird das Projekt von [6] in [7] umgewandelt:

3.5.3. Maven-Konfiguration

Das Projekt [7] ist ein Maven-Projekt, das durch die folgende Datei [pom.xml] definiert ist:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-jdbc-generic-02</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-jdbc-generic-02</name>
    <description>Demo project for API JDBC</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- Übergeordnetes Element aus dem Repository abrufen -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- Konfiguration JDBC des SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • Zeilen 28–33: die Maven-Abhängigkeit vom Projekt [mysql-config-jdbc];

Es ist das Projekt [mysql-config-jdbc], das in seinen Maven-Abhängigkeiten die Bibliothek enthält, die eine Implementierung einer Datenquelle vom Typ [javax.sql.DataSource] bereitstellt (siehe Abschnitt 3.3.2):


        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
</dependency>

3.5.4. Spring-Konfiguration

  

Die Spring-Konfigurationsklasse [AppConfig] lautet wie folgt:


package spring.jdbc;

import generic.jdbc.config.ConfigJdbc;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({ generic.jdbc.config.ConfigJdbc.class })
public class AppConfig {
    // Datenquelle
    @Bean
    public DataSource dataSource() {
        // Datenquelle TomcatJdbc
        DataSource dataSource = new DataSource();
        // Zugriffskonfiguration JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // anfänglich offene Verbindungen
        dataSource.setInitialSize(5);
        // Ergebnis
        return dataSource;
    }

}
  • Zeile 10: [AppConfig] ist eine Spring-Konfigurationsklasse;
  • Zeile 11: Import der im Projekt [mysql-config-jdbc] definierten Konfigurationsklasse [generic.jdbc.config.ConfigJdbc.class]. Das bedeutet, dass alle in dieser Konfigurationsdatei definierten Beans zur Verfügung stehen;
  • Zeilen 14–27: Das Spring-Bean, das die Datenquelle definiert;
  • Zeile 17: Erstellung der noch nicht konfigurierten Datenquelle;
  • Zeilen 19–22: Die Informationen, die es der Datenquelle ermöglichen, eine Verbindung zur Datenbank herzustellen;
  • Zeile 24: Erstellt einen Pool mit 5 Verbindungen. Hier wird nur eine benötigt. Es gibt nie mehrere gleichzeitige Verbindungen;

3.5.5. Die Hauptklasse

Die Hauptklasse [IntroJdbc02] sieht wie folgt aus:


package spring.jdbc;

import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class IntroJdbc02 {

    // Zuordnung jSON
    final static ObjectMapper jsonMapper = new ObjectMapper();
    // Datenquelle
    private static DataSource dataSource;

    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = null;
        try {
            // Abruf des Spring-Kontexts
            ctx = new AnnotationConfigApplicationContext(AppConfig.class);
            // Abruf der Datenquelle
            dataSource = ctx.getBean(DataSource.class);
            // Die Tabelle wird geleert [PRODUITS]
            System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
            delete();
...
        // Fertig
        System.out.println(String.format("------------------------------ %s", "Travail terminé"));
    }

    // Produktliste
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // Verbindung wird hergestellt
            connexion = dataSource.getConnection();
            // Transaktion starten
            connexion.setAutoCommit(false);
            // im schreibgeschützten Modus
            connexion.setReadOnly(true);
            // die Tabelle [PRODUITS] wird gelesen
            ps = connexion.prepareStatement(ConfigJdbc.V1_SELECT_PRODUITS);
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                affiche(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // Transaktion festschreiben
            connexion.commit();
        } catch (SQLException e1) {
            // Ausnahme wird behandelt
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // „finally“-Block wird verarbeitet
            doFinally(rs, ps, connexion);
        }
    }
...
  • Zeile 25: Die Datenquelle. Beachten Sie, dass sie vom Typ [javax.sql.DataSource] (Zeile 13) ist, bei dem es sich um eine Schnittstelle handelt;
  • Zeile 31: Instanziierung der Spring-Objekte;
  • Zeile 32: Abruf einer Referenz auf die Datenquelle. Es ist zu beachten, dass zu keinem Zeitpunkt die tatsächlich verwendete Klasse genannt wird. Somit lässt hier nichts darauf schließen, dass eine Implementierung von [TomcatJdbc] verwendet wird;
  • Zeile 49: Abrufen einer offenen Verbindung. Auf diese Weise erhalten die verschiedenen Methoden von [IntroJdbc02] eine Verbindung zur Datenbank. Der Rest des Codes ist identisch mit dem der Klasse [IntroJdbc01];

3.5.6. Die Tests

Wir führen die Ausführungskonfiguration mit dem Namen [spring-jdbc-generic-02.IntroJdbc02] aus:

 

Es werden dieselben Ergebnisse wie zuvor (Abschnitt 3.4.9) erzielt.

3.6. Beispiel-03

3.6.1. Die Projektarchitektur

In diesem Beispiel sind die Methoden für den Datenzugriff in einer Schicht [dao] isoliert. Sie werden durch einen Test JUnit getestet.

3.6.2. Das Eclipse-Projekt

Das Eclipse-Projekt [spring-jdbc-03] ist ein Spring-/Maven-Projekt, das wie das vorherige aufgebaut und anschließend wie folgt ergänzt wurde:

 

Die verschiedenen Pakete haben folgende Funktionen:

  • [spring.jdbc.config]: Konfiguration des Spring-Projekts;
  • [spring.jdbc.dao]: Implementierung der Schicht [DAO];
  • [spring.jdbc.infrastructure]: Implementierung der unkontrollierten Ausnahme [DaoException];

3.6.3. Maven-Konfiguration

Das Maven-Projekt wird durch die folgende Datei „[pom.xml]“ konfiguriert:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-jdbc-generic-03</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-jdbc-generic-03</name>
    <description>Demo project for API JDBC</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- Übergeordnetes Element aus dem Repository abrufen -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- Konfiguration JDBC des SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>

Sie ist identisch mit der des Projekts [spring-jdbc-02]. Sie nutzt insbesondere die Maven-Abhängigkeit des Projekts [mysql-config-jdbc] (Zeilen 28–32).

3.6.4. Schnittstelle der Schicht [DAO]

  

Die Schicht [DAO] verfügt über die folgende Schnittstelle [IDao]:


package spring.jdbc.dao;

import java.util.List;

import spring.jdbc.entities.Produit;

public interface IDao {

    // Produkte hinzufügen
    public List<Produit> addProduits(List<Produit> produits);

    // Liste aller Produkte
    public List<Produit> getAllProduits();

    // ein bestimmtes Produkt
    public Produit getProduitById(int id);

    public Produit getProduitByName(String name);

    // Mehrere Produkte aktualisieren
    public int updateProduits(List<Produit> produits);

    // Alle Produkte löschen
    public int deleteAllProduits();

    // Mehrere Produkte löschen
    public int deleteProduits(int[] ids);
}

3.6.5. Die Klasse [DaoException]

Die Klasse [DaoException] erweitert lediglich die in Abschnitt 3.3.5 vorgestellte Klasse [UncheckedException]:

  

package spring.jdbc.infrastructure;

public class DaoException extends UncheckedException {

    private static final long serialVersionUID = 1L;

    // Hersteller
    public DaoException() {
        super();
    }

    public DaoException(int code, Throwable e, String className) {
        super(code, e, className);
    }

}

3.6.6. Konfiguration des Spring-Projekts

  

Die Klasse [AppConfig], die das Spring-Projekt konfiguriert, ist bis auf Zeile 11 identisch mit der Spring-Konfigurationsdatei des Beispiels [spring-jdbc-02]:


package spring.jdbc.config;

import generic.jdbc.config.ConfigJdbc;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = { "spring.jdbc.dao" })
public class AppConfig {
    // Datenquelle
    @Bean
    public DataSource dataSource() {
        // Datenquelle TomcatJdbc
        DataSource dataSource = new DataSource();
        // Zugriffskonfiguration JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // Anfangs offene Verbindungen
        dataSource.setInitialSize(5);
        // Ergebnis
        return dataSource;
    }
}
  • Zeile 11: Das Paket [spring.jdbc.dao] wird nach weiteren Spring-Komponenten durchsucht, die über die in dieser Konfigurationsdatei definierten hinausgehen;

3.6.7. Implementierung der Schicht [DAO]

  

Zur Erinnerung (Abschnitt 3.6.4): Die Schicht [DAO] implementiert die folgende Schnittstelle [IDao]:


package spring.jdbc.dao;

import generic.jdbc.entities.dbproduits.Produit;

import java.util.List;

public interface IDao {

    // Produkte hinzufügen
    public List<Produit> addProduits(List<Produit> produits);

    // Liste aller Produkte
    public List<Produit> getAllProduits();

    // ein bestimmtes Produkt
    public Produit getProduitById(int id);

    public Produit getProduitByName(String name);

    // Aktualisierung mehrerer Produkte
    public int updateProduits(List<Produit> produits);

    // Alle Produkte löschen
    public int deleteAllProduits();

    // Mehrere Produkte löschen
    public int deleteProduits(int[] ids);
}

Die Klassen [Dao1, Dao2] implementieren beide diese Schnittstelle. Die Klasse [Dao2] ist eine Variante der Klasse [Dao1], die eine syntaktische Neuerung einführt. Wir werden uns auf die Klasse [Dao1] konzentrieren. Das Grundgerüst dieser Klasse sieht wie folgt aus:


package spring.jdbc.dao;

import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import spring.jdbc.infrastructure.DaoException;

@Component("dao1")
public class Dao1 implements IDao {

    // Name der Klasse
    private String simpleClassName = getClass().getSimpleName();
    // Datenquelle
    @Autowired
    protected DataSource dataSource;

    // Hersteller
    public Dao1() {
        System.out.println("building Dao1...");
    }

    // ------------------------------- Schnittstelle
    @Override
    public List<Produit> getAllProduits() {
...
    }

    @Override
    public Produit getProduitById(int id) {
...
    }

    @Override
    public Produit getProduitByName(String name) {
...
    }

    @Override
    public List<Produit> addProduits(List<Produit> produits) {
....
    }

    @Override
    public int updateProduits(List<Produit> produits) {
...
    }

    @Override
    public int deleteAllProduits() {
...
    }

    @Override
    public int deleteProduits(int[] ids) {
...
    }

    // ---------------------------------------- lokale Methoden
    // finally-Abwicklung
    protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
            DaoException daoException) {
        ...
    }

    // catch-Behandlung
    protected DaoException doCatchException(Connection connexion, Throwable th, int code, DaoException daoException) {
...
}
  • Zeile 20: Die Klasse [Dao] ist eine Spring-Komponente mit dem Namen [dao1]. Dieser Name ist optional. Fehlt er, wird der Name der Klasse verwendet, wobei der erste Großbuchstabe in einen Kleinbuchstaben umgewandelt wird;
  • Zeile 24: Der Name der Klasse. Man vermeidet es, „[Dao]“ fest zu schreiben, um die Möglichkeit zu lassen, die Klasse umzubenennen, ohne dieses Feld neu definieren zu müssen, das somit immer gültig bleibt;
  • Zeilen 26–27: Einbindung der Datenquelle „[tomcat-jdbc]“, die in der Konfigurationsklasse „[AppConfig]“ definiert ist;
  • Zeilen 36–68: Implementierung der Schnittstelle [IDao];
  • Zeilen 78–80: Zentralisierte Verwaltung der verschiedenen Methoden von catch;
  • Zeilen 72–75: Zentralisierte Verwaltung des finally der verschiedenen Methoden;

Das catch der verschiedenen Methoden wird wie folgt verwaltet:


    // catch-Behandlung
    protected DaoException doCatchException(Connection connexion, Throwable th, int code) {
        // Transaktionsstornierung
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            e2.printStackTrace();
        }
        // daoException
        return new DaoException(code, th, simpleClassName);
}
  • Zeile 2: Die Methode wird als [protected] deklariert, wodurch die untergeordneten Klassen sie nutzen können, ohne dass sie öffentlich ist. Sie erhält folgende Parameter:
    • [Connection connexion]: die Verbindung mit SGBD – möglicherweise null;
    • [Throwable th]: die aufgetretene Ausnahme, die in einen Typ [DaoException] gekapselt wird;
    • [int code]: Ein Fehlercode, der verwendet werden soll, wenn die Methode ein neues [DaoException] erstellt;
  • Zeilen 4–7: Die Hauptaufgabe dieser Methode besteht darin, die Transaktion rückgängig zu machen, die mit der als Parameter 1 übergebenen Verbindung verbunden ist;
  • Zeilen 8–10: Wenn das Abbrechen der Transaktion fehlgeschlagen ist, wird der Ausnahmeprotokollauszug in die Konsole geschrieben. Viel mehr kann man nicht tun, da in Zeile 12 eine Ausnahme ausgelöst wird;

Der finally der verschiedenen Methoden wird wie folgt verwaltet:


// Finally-Verwaltung
    protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
            DaoException daoException) {
        // Schließen ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // Schließung [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        // Verbindung schließen
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // Der Fehler wird protokolliert, sofern möglich
                if (daoException == null) {
                    daoException = new DaoException(code, e3, simpleClassName);
                }
            }
        }
        // Ergebnis
        return daoException;
    }
  • Zeile 2: Auch diese Methode ist als [protected] deklariert. Sie erhält folgende Parameter:
    • [ResultSet rs]: das eventuelle [ResultSet], falls eine Operation [SELECT] ausgeführt wurde – möglicherweise null;
    • [PreparedStatement ps]: der [PreparedStatement], der ausgeführt wurde – möglicherweise null;
    • [Connection connexion]: die Verbindung mit dem SGBD – möglicherweise null;
    • [int code]: Ein Fehlercode, der verwendet werden soll, wenn die Methode ein neues [DaoException] erstellt;
    • [DaoException daoException]: die mögliche [DaoException], falls vor der finally eine Ausnahme aufgetreten ist – möglicherweise null;
  • Zeilen 21–30: Das Hauptziel dieser Methode ist es, die Verbindung zu schließen (Zeile 23);
  • Zeilen 24–29: Wenn beim Schließen eine Ausnahme auftritt, wird der Status des übergebenen Parameters [DaoException daoException] überprüft: Ist der Wert [daoException == null], wird ein neuer [DaoException] mit dem als Parameter übergebenen Code erstellt;
  • Zeile 32: Die alte oder die neue Instanz von `[DaoException]` wird als Ergebnis zurückgegeben;

Wir werden nicht alle Methoden der Klasse [Dao] vorstellen, sondern nur einige wenige. Sie ähneln sich alle.

3.6.7.1. Die Methode [getProduitById]

Die Methode [getProduitById] gibt das Produkt zurück, dessen Primärschlüssel dem Parameter [id] entspricht, andernfalls;


@Override
    public Produit getProduitById(int id) {
        // Verbindungsressourcen
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        // Zunächst keine Ausnahme
        DaoException daoException = null;
        // Das gesuchte Produkt
        Produit produit = null;
        try {
            // Verbindung hergestellt
            connexion = dataSource.getConnection();
            // Transaktionsstart
            connexion.setAutoCommit(false);
            // im schreibgeschützten Modus
            connexion.setReadOnly(true);
            // die Tabelle [PRODUITS] wird gelesen
            ps = connexion.prepareStatement(ConfigJdbc.V2_SELECT_PRODUIT_BYID);
            ps.setInt(1, id);
            rs = ps.executeQuery();
            if (rs.next()) {
                produit = new Produit(id, rs.getString(1), rs.getInt(2), rs.getDouble(3), rs.getString(4));
            }
            // Transaktion festschreiben
            connexion.commit();
            // Rückkehr in den Standardmodus
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // Ausnahme wird behandelt
            daoException = doCatchException(connexion, e1, 112);
        } finally {
            // „finally“-Block wird verarbeitet
            daoException = doFinally(rs, ps, connexion, 113, daoException);
        }
        // Ausnahme?
        if (daoException != null) {
            throw daoException;
        }
        // Ergebnis
        return produit;
    }
  • Zeile 10: Das zurückzugebende Produkt wird auf null gesetzt;
  • Zeile 19: Der Auftrag SQL [ConfigJdbc.V2_SELECT_PRODUIT_BYID] lautet wie folgt:

public final static String V2_SELECT_PRODUIT_BYID = "SELECT NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE ID=?";

  • Zeilen 22–24: Wenn [ResultSet] eine Zeile enthält, wird diese zur Erstellung des zurückzusendenden Produkts verwendet, andernfalls bleibt das zurückzusendende Produkt bei null;
  • Zeile 41: Das Produkt wird zurückgegeben;
  • Zeile 8: Die Ausnahme [DaoException] der Methode wird auf null initialisiert;
  • Zeile 31: Die Methode [doCatchException] löst eine Ausnahme [DaoException] aus;
  • Zeile 34: Der Parameter [daoException] der Methode [doFinally] ist entweder null, oder die von der Methode [doCatchException] ausgelöste Ausnahme. Die Methode [doFinally]:
    • belässt diesen Parameter unverändert, wenn es ihr gelingt, die Verbindung zu schließen;
    • lässt diesen Parameter unverändert, wenn sie die Verbindung nicht schließen kann und zuvor bereits ein Aufruf von [DaoException] stattgefunden hat;
    • erstellt eine neue [DaoException], wenn die Verbindung nicht geschlossen werden kann und zuvor noch kein [DaoException] ausgeführt wurde;
  • Zeilen 37–39: Wenn die lokale Ausnahme [daoException] nicht null entspricht, wird sie ausgelöst; andernfalls wird das angeforderte Ergebnis zurückgegeben (Zeile 41);

3.6.7.2. Die Methode [deleteProduits]

Die Methode [deleteProduits] löscht die Produkte, deren Primärschlüssel als Parameter übergeben werden. Sie gibt die Anzahl der gelöschten Produkte zurück.


@Override
    public int deleteProduits(int[] ids) {
        // Verbindungsressourcen
        PreparedStatement ps = null;
        Connection connexion = null;
        // zunächst keine Ausnahme
        DaoException daoException = null;
        // Anzahl der aktualisierten Produkte
        int nbProduits = 0;
        try {
            // Verbindung hergestellt
            connexion = dataSource.getConnection();
            // Transaktionsstart
            connexion.setAutoCommit(false);
            // im Lese-/Schreibmodus
            connexion.setReadOnly(false);
            // Produkte werden gelöscht
            ps = connexion.prepareStatement(ConfigJdbc.V2_DELETE_PRODUITS);
            for (int id : ids) {
                // Parameter
                ps.setInt(1, id);
                // Ausführung
                nbProduits += ps.executeUpdate();
            }
            // Transaktion festschreiben
            connexion.commit();
            // Zurück zum Standardmodus
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // Die Ausnahme wird behandelt
            daoException = doCatchException(connexion, e1, 171);
        } finally {
            // „finally“-Block wird ausgeführt
            daoException = doFinally(null, ps, connexion, 172, daoException);
        }
        // Ausnahme?
        if (daoException != null) {
            throw daoException;
        }
        // Ergebnis
        return nbProduits;
    }
  • Zeile 18, die Reihenfolge SQL [ConfigJdbc.V2_DELETE_PRODUITS] lautet wie folgt:

public final static String V2_DELETE_PRODUITS = "DELETE FROM PRODUITS WHERE ID=?";

  • Zeilen 18–24: Der Code zum Löschen der Produkte. Man sieht, dass der Befehl SQL einmal vorbereitet (Zeile 18) und n-mal ausgeführt wird (Zeilen 19–24). Darin liegt der Nutzen des Objekts [PreparedStatement];
  • Zeile 23: Die Methode [PreparedStatement].executeUpdate() gibt die Anzahl der von der Aktualisierung betroffenen Zeilen zurück;
  • Zeile 41: Es wird die Anzahl der aktualisierten Produkte zurückgegeben;

3.6.7.3. Die Methode [updateProduits]

Die Methode [updateProduits] aktualisiert die Produkte, die ihr als Parameter übergeben werden, in der Datenbank. Sie gibt die Anzahl der aktualisierten Produkte zurück.


@Override
    public int updateProduits(List<Produit> produits) {
        // Verbindungsressourcen
        PreparedStatement ps = null;
        Connection connexion = null;
        // zunächst keine Ausnahme
        DaoException daoException = null;
        // Anzahl der aktualisierten Produkte
        int nbProduits = 0;
        try {
            // Verbindung hergestellt
            connexion = dataSource.getConnection();
            // Transaktionsstart
            connexion.setAutoCommit(false);
            // im Lese-/Schreibmodus
            connexion.setReadOnly(false);
            // Die Tabelle [PRODUITS] wird aktualisiert
            ps = connexion.prepareStatement(ConfigJdbc.V2_UPDATE_PRODUITS);
            for (Produit produit : produits) {
                // Parameter
                ps.setString(1, produit.getNom());
                ps.setDouble(2, produit.getPrix());
                ps.setInt(3, produit.getCategorie());
                ps.setString(4, produit.getDescription());
                ps.setInt(5, produit.getId());
                // Ausführung
                nbProduits += ps.executeUpdate();
            }
            // Transaktion festschreiben
            connexion.commit();
            // Zurück zum Standardmodus
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // Ausnahme wird behandelt
            daoException = doCatchException(connexion, e1, 131);
        } finally {
            // „finally“-Block wird verarbeitet
            daoException = doFinally(null, ps, connexion, 132, daoException);
        }
        // Ausnahme?
        if (daoException != null) {
            throw daoException;
        }
        // Ergebnis
        return nbProduits;
    }
  • Zeile 18: Die Reihenfolge SQL [ConfigJdbc.V2_UPDATE_PRODUITS] lautet wie folgt:

public final static String V2_UPDATE_PRODUITS = "UPDATE PRODUITS SET NOM=?, PRIX=?, CATEGORIE=?, DESCRIPTION=? WHERE ID=?";
  • Zeilen 19–28: Der Code zur Aktualisierung der Produkte;

3.6.7.4. Die Methode [addProduits]

Die Methode [addProduits] fügt die Produkte, die ihr als Parameter übergeben werden, in die Datenbank ein. Sie gibt dieselben Produkte mit ihren Primärschlüsseln zurück (vor dem Einfügen in die Datenbank haben die Produkte keinen Primärschlüssel).


@Override
    public List<Produit> addProduits(List<Produit> produits) {
        // Verbindungsressourcen
        PreparedStatement ps = null;
        Connection connexion = null;
        // zunächst keine Ausnahme
        DaoException daoException = null;
        try {
            // Verbindung wird geöffnet
            connexion = dataSource.getConnection();
            // im Lese-/Schreibmodus
            connexion.setReadOnly(false);
            // Transaktion gestartet
            connexion.setAutoCommit(false);
            // Elemente werden zur Tabelle [PRODUITS] hinzugefügt
            String generatedColumns[] = { ConfigJdbc.TAB_PRODUITS_ID };
            ps = connexion.prepareStatement(ConfigJdbc.V2_INSERT_PRODUITS, generatedColumns);
            for (Produit produit : produits) {
                // Parameter
                ps.setString(1, produit.getNom());
                ps.setLong(2, produit.getCategorie());
                ps.setDouble(3, produit.getPrix());
                ps.setString(4, produit.getDescription());
                // Befehl wird ausgeführt
                ps.executeUpdate();
                // Primärschlüssel generiert
                ResultSet generatedKeys = ps.getGeneratedKeys();
                if (generatedKeys.next()) {
                    produit.setId(generatedKeys.getInt(1));
                } else {
                    throw new RuntimeException(String.format("Le produit de nom [%s] n'a pas récupéré de clé primaire",
                            produit.getNom()));
                }
            }
            // Transaktion festschreiben
            connexion.commit();
            // Rückkehr zum Standardmodus
            connexion.setAutoCommit(true);
        } catch (SQLException | RuntimeException e1) {
            // Ausnahme wird verarbeitet
            daoException = doCatchException(connexion, e1, 151);
        } finally {
            // „finally“-Block wird verarbeitet
            daoException = doFinally(null, ps, connexion, 152, daoException);
        }
        // Ausnahme?
        if (daoException != null) {
            throw daoException;
        }
        // Ergebnis
        return produits;
}
  • Zeile 16, die Reihenfolge SQL [ConfigJdbc.V2_INSERT_PRODUITS] lautet wie folgt:

public final static String V2_INSERT_PRODUITS = "INSERT INTO PRODUITS(NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?)";

Im obigen Befehl zum Einfügen eines Produkts fehlt der Primärschlüssel [ID]. Da der Primärschlüssel der Datenbank MySQL das Attribut [AUTOINCREMENT] hat, generiert SGBD bei jedem Einfügevorgang einen Primärschlüssel. Es stellt sich das Problem, diesen Schlüssel abzurufen. Dies ist ein wichtiger Punkt, da Operationen an den Produkten über ihre Primärschlüssel erfolgen. Man muss diese also kennen;

  • Zeilen 17–33: die Schleife zum Einfügen der Produkte;
  • Zeile 16: eine spezielle Form der Methode [prepareStatement]. Der zweite Parameter [generatedColumns] ist ein Array mit Spaltennamen, deren Werte nach dem Einfügen abgerufen werden sollen. In Zeile 16 haben wir angegeben, dass wir den Wert der Spalte [id] abrufen möchten. Hier ist anzumerken, dass die Spaltennamen einer Tabelle zwar nicht zwischen Groß- und Kleinschreibung unterscheiden, SGBD PostgreSQL jedoch verlangt hat, dass dieser Name in Kleinbuchstaben geschrieben wird. Das ist typischerweise die Art von Problemen, die bei der Portierung von Code von einem SGBD auf ein anderes System auftreten;
  • Zeile 24: Einfügen einer Zeile in die Datenbank;
  • Zeile 26: Abruf der Liste der Werte der in Zeile 16 in einer [ResultSet]-Datei angegebenen Spalten. Hier enthält die [ResultSet]-Datei bei einem Eintrag eine Zeile, und diese Zeile enthält eine einzige Spalte, die den Primärschlüssel enthält;
  • Zeile 28: Der vom SGBD generierte Primärschlüssel wird abgerufen;
  • Zeilen 29–32: Wenn der generierte Primärschlüssel nicht abgerufen wird, wird ein [RuntimeException] gestartet, der in einen [DaoException] (Zeilen 38–40) eingebettet wird;

3.6.8. Die Klasse [Dao2]

  

Die Klasse [Dao2] ist eine Variante der Klasse [Dao1], die eine Syntax namens „try-with-resource(resource)“ verwendet:

1
2
3
4
try(resource){
...
}
...
  • [resource] ist eine Ressource, die die Schnittstelle [java.lang.AutoCloseable] implementiert. Alle Ressourcen, die mit der Methode [close] freigegeben werden, gehören dazu. Diese Syntax stellt sicher, dass in Zeile 4 die Ressource [resource] geschlossen wird. Dadurch entfällt das Schreiben einer [finally]-Klausel, um diese Schließung durchzuführen;

Nehmen wir als Beispiel die Methode [getAllProduits] der Klasse [Dao2]:


    @Override
    public List<Produit> getAllProduits() {
        // mögliche Ausnahme
        DaoException daoException = null;
        // Produktliste
        List<Produit> produits = new ArrayList<Produit>();
        try (Connection connexion = dataSource.getConnection()) {
            // Transaktionsstart
            connexion.setAutoCommit(false);
            // im schreibgeschützten Modus
            connexion.setReadOnly(true);
            // die Tabelle [PRODUITS] wird gelesen
            try (PreparedStatement ps = connexion.prepareStatement(ConfigJdbc.V2_SELECT_ALLPRODUITS)) {
                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next()) {
                        produits.add(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
                    }
                }
                // Transaktionsende
                connexion.commit();
                // Rückkehr zum Standardmodus
                connexion.setAutoCommit(true);
            } catch (SQLException e1) {
                // Transaktion wird abgebrochen
                daoException = doRollback(connexion, e1, 111);
            }
        } catch (SQLException e2) {
            // Ausnahme wird verarbeitet
            if (daoException == null) {
                daoException = new DaoException(112, e2, simpleClassName);
            }
        }
        // Ausnahme?
        if (daoException != null) {
            throw daoException;
        }
        // Ergebnis
        return produits;
}
  • Zeile 7: „try“-Anweisung mit der Ressource [Connection]. In Zeile 27 ist sichergestellt, dass diese geschlossen ist;
  • Zeile 13: „try“-Anweisung mit der Ressource [PreparedStatement]. In Zeile 23 ist sichergestellt, dass diese geschlossen ist;
  • Zeile 14: „try“ mit der Ressource [ResultSet]. In Zeile 19 ist sichergestellt, dass diese geschlossen ist;
  • Zeile 25: Die Transaktion wird wie folgt rückgängig gemacht:

    private DaoException doRollback(Connection connexion, Throwable e1, int code) {
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        // Auslösung der Ausnahme
        return new DaoException(code, e1, simpleClassName);
}

Letztendlich erhalten wir einen besser lesbaren Code.

3.6.9. Implementierung der Testschicht

3.6.9.1. Die Testklassen

  
  • Der Test [JUnitTestDao1] ist ein Test JUnit der Klasse [Dao1];
  • Der Test [JUnitTestDao2] ist ein Test JUnit der Klasse [Dao2];
  • [AbstractJUnitTestDao] ist die übergeordnete Klasse der beiden vorangegangenen Testklassen;
  • [MainTestDao1] ist eine Testkonsolenklasse der Klasse [Dao1];
  • [MainTestDao2] ist eine Test-Konsolenklasse der Klasse [Dao2];
  • [AbstractMainTestDao] ist die übergeordnete Klasse der beiden vorangegangenen Klassen. Sie übernimmt den Code der bereits vorgestellten Konsolenklassen [IntroJdbc01, IntroJdbc02], daher werden wir diese Konsolenklassen nicht näher betrachten;

Die Klasse [JUnitTestDao1] lautet wie folgt:


package spring.jdbc.tests;

import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;

@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestDao1 extends AbstractJUnitTestDao {

    // Schicht [DAO]
    @Autowired
    @Qualifier("dao1")
    private IDao dao;

    @Override
    IDao getDao() {
        return dao;
    }

}
  • Die Anmerkungen zu den Zeilen 12–13 wurden in Abschnitt 2.5.5 vorgestellt. Sie ermöglichen einem JUnit-Test einen einfachen Zugriff auf den Spring-Kontext und dessen Beans. Dieser Kontext wird durch die Klasse [AppConfig] (Zeile 12) konfiguriert, die in Abschnitt 2.4.3 behandelt wurde;
  • Zeile 14: Die Klasse erweitert die Klasse [AbstractJUnitTestDao], die wir im Folgenden vorstellen werden. In dieser Klasse befinden sich die Testmethoden JUnit;
  • Zeilen 17–19: Die Bean mit dem Namen [dao1] (Zeile 18) wird injiziert (Zeile 17). Es handelt sich also um eine Instanz der Klasse [Dao1], die hier injiziert wird;
  • Zeilen 21–24: Die Methode [getDao] überschreibt die gleichnamige Methode in der übergeordneten Klasse;

Letztendlich besteht der Zweck dieser Klasse darin, der übergeordneten Klasse eine Referenz auf die zu testende Ebene [DAO] zurückzugeben, in diesem Fall eine Instanz von [Dao1]. Ebenso übergibt die Klasse [JUnitTestDao2] der übergeordneten Klasse [AbstractJUnitTestDao] eine Instanz der Klasse [Dao2].

Die Klasse [AbstractJUnitTestDao] ist eine Testklasse für JUnit:


package spring.jdbc.tests;

import generic.jdbc.entities.dbproduits.Produit;

import java.util.ArrayList;
import java.util.List;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.BeansException;

import spring.jdbc.dao.IDao;
import spring.jdbc.infrastructure.DaoException;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public abstract class AbstractJUnitTestDao {

    // Schicht [DAO]
    abstract IDao getDao();

    // Zuordnung jSON
    final static ObjectMapper jsonMapper = new ObjectMapper();

    @Before
    public void clean() {
        // Die Basis wird vor jedem Test gereinigt
        log("Vidage de la base de données", 1);
        getDao().deleteAllProduits();
    }

    @Test
    public void getProduits() throws JsonProcessingException {
    ...
    }

    @Test
    public void getProduitBy() {
    ...
    }

    @Test
    public void doInsertsInTransaction() {
...
    }

    @Test
    public void updateProduits() {
    ...
    }

    @Test
    public void deleteProduits() {
    ....
    }

    @Test
    public void perf1() {
        ...
    }

    @Test
    public void perf2() {
    ...
    }

    @Test
    public void perf3() {
    ....
    }

    // -------------- private Methoden
...
}
  • Zeile 19: Die Klasse [AbstractJUnitTestDao] ist abstrakt;
  • Zeile 22: die abstrakte Methode [getDao], die es ermöglicht, die Referenz auf die zu testende Schicht [DAO] abzurufen. Diese Methode wird von den untergeordneten Klassen implementiert;
  • Zeile 25: Ein Mapper jSON, mit dem wir den Wert jSON der Produkte auf der Konsole anzeigen können;
  • Zeilen 27–32: Vor jedem Test (Zeile 27) wird die Tabelle [PRODUITS] geleert;

3.6.9.2. Die private Methode [fill]

Die private Methode [fill] wird verwendet, um Produkte in die Tabelle [PRODUITS] einzufügen.


private List<Produit> fill(int nbProduits) {
        log("Remplissage de la base de données", 1);
        // Es wird eine Produktliste erstellt
        List<Produit> produits = new ArrayList<Produit>();
        for (int i = 0; i < nbProduits; i++) {
            int n = i + 1;
            // int id, String name, int category, double price, String description
            produits.add(new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format(
                    "DESC%s", n)));
        }
        // Die Liste wird in der Datenbank gespeichert – Produkte werden anhand ihres Primärschlüssels abgerufen
        produits = getDao().addProduits(produits);
        // Es wird ein Produkt-Dictionary erstellt, um die Produkte leichter wiederzufinden
        // Der Schlüssel des Dictionaries ist der Primärschlüssel des Produkts in der Datenbank
        for (Produit produit : produits) {
            mapProduits.put(produit.getId(), produit);
        }
        // Die Produkte werden zurückgegeben
        return produits;
    }
  • Zeile 1: Die Methode [fill] fügt [nbProduits] in die Tabelle [PRODUITS] ein, die als leer angenommen wird;
  • Zeilen 3–10: Erstellung einer Produktliste in folgender Form:

new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format("DESC%s", n)));

die den Konstruktor Produit(<span style="color: #3f7f5f">int id, String nom, int categorie, double prix, String description</span>) verwendet. Der Wert des ersten Parameters [id] (Primärschlüssel der Tabelle [PRODUITS]) spielt keine Rolle, da die Methode [addProduits] in Zeile 10 ihn nicht in die Datenbank einfügt und es dem SGBD überlässt, seinen Wert zu generieren;

  • Zeile 12: Die Produktliste wird in der Datenbank gespeichert. Jedes Produkt dieser Liste wird um einen neuen Primärschlüssel [id] ergänzt. Die Methode [addProduits] gibt ihren Parameter [produits] als Ergebnis zurück. Man hätte das Ergebnis also auch nicht abrufen können;
  • Zeilen 15–17: Die Produkte werden in ein Dictionary gespeichert:

    // Produktverzeichnis
    private Map<Integer, Produit> mapProduits = new HashMap<Integer, Produit>();

Der Schlüssel des Dictionaries ist der Primärschlüssel des Produkts und der zugehörige Wert ist das Produkt selbst;

  • Zeile 19: Die Liste der Produkte wird zurückgegeben;

3.6.9.3. Der Test [getProduits]

Dieser lautet wie folgt:


    @Test
    public void getProduits() throws JsonProcessingException {
        // Befüllung
        fill(10);
        // Produktliste
        log("Liste des produits", 2);
        List<Produit> produits = getDao().getAllProduits();
        affiche(produits);
        // Es wird überprüft, ob die abgerufene und die gespeicherte Liste identisch sind
        for (Produit produit : produits) {
            Produit found = mapProduits.get(produit.getId());
            Assert.assertEquals(found, produit);
            mapProduits.remove(found.getId());
        }
        // Alle ursprünglichen Produkte müssen aus dem Verzeichnis entfernt worden sein
        Assert.assertEquals(0, mapProduits.size());
}
}
  • Zeile 4: 10 Produkte werden in die Datenbank eingefügt;
  • Zeile 7: Anschließend werden alle Produkte aus der Datenbank abgefragt;
  • Zeile 8: Diese werden angezeigt. Ziel ist es, zu überprüfen, ob die Produkte tatsächlich gespeichert wurden und über einen Primärschlüssel verfügen;
  • Zeilen 10–13: Es wird überprüft, ob die gefundenen Produkte mit den gespeicherten übereinstimmen und ob sie im Verzeichnis [mapProduits] zu finden sind;
  • Zeile 11: Aus dem Wörterbuch wird das Produkt mit demselben Primärschlüssel abgerufen wie das aus der Datenbank zurückgegebene. Dies zeigt, dass die gespeicherten Produkte tatsächlich einen Primärschlüssel erhalten haben;
  • Zeile 12: Es wird sichergestellt, dass die beiden Produkte identisch sind. Zur Erinnerung: Die Klasse [Produit] hat eine Methode [equals] definiert (siehe Abschnitt 3.3.4);
  • Zeile 13: Das gefundene Element wird aus dem Wörterbuch gelöscht;
  • Zeile 16: Es wird überprüft, ob das Wörterbuch der ursprünglichen Produkte tatsächlich leer ist, was bedeutet, dass diese ursprünglichen Produkte alle in der aus der Datenbank zurückgegebenen Produktliste enthalten waren;

Die Methode [affiche] in Zeile 8 ist die folgende private Methode:


    // Anzeige der Produktliste
    private <T> void affiche(List<T> elements) throws JsonProcessingException {
        for (T element : elements) {
            System.out.println(jsonMapper.writeValueAsString(element));
        }
}
  • Zeile 2: Die Methode [affiche] ist eine generische Methode. Sie wird durch einen Typ T parametrisiert, der syntaktisch als <T> bezeichnet wird. Würde sie durch zwei Typen T1 und T2 parametrisiert, würde man <T1,T2> schreiben. Die Syntax einer Methode m, die durch einen Typ T parametrisiert ist, lautet wie folgt:
portée <T> type_résultat m(... , T value1, ...){
...
    T value2=...
}

Im Code der Methode m finden sich Daten vom Typ T. Die Methode m einer Instanz c einer Klasse C kann dann wie folgt aufgerufen werden:

type_résultat r=c.<T1>m(..., T1 value1, ..) ;

wobei T1 der effektive Typ ist, der den formalen Typ T der Methode m ersetzt. In den meisten Fällen kann der Compiler den Typ T1 anhand der Argumente der Methode m ableiten. Daher wird die obige Anweisung meist wie folgt vereinfacht:

type_résultat r=c.m(..., T1 value1, ..) ;

Kommen wir zurück zur Methode [affiche]. Sie gibt eine Liste von Elementen vom Typ T aus. Dies ist möglich, weil der in Zeile 4 verwendete Mapper jSON in der Lage ist, die Darstellung jSON für jeden Objekttyp zu erzeugen. In diesem konkreten Beispiel wird als einziger Typ T der Typ [Produit] verwendet.

Die Methode [affiche] hätte auch wie folgt geschrieben werden können:


    // Anzeige der Produktliste
    private void affiche(Object o) throws JsonProcessingException {
            System.out.println(jsonMapper.writeValueAsString(o));
        }

Da der tatsächliche Parameter eine Produktliste ist, hätte Zeile 3 die Darstellung jSON dieser Liste geschrieben. Dies ist nicht dasselbe wie die Darstellung jedes einzelnen Elements nacheinander zu schreiben.

Die durch den Test [getProduits] erzeugte Anzeige sieht wie folgt aus:

-- Liste des produits
{"id":150189,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":150190,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":150191,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":150192,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":150193,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":150194,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":150195,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":150196,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":150197,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":150198,"nom":"NOM10","categorie":3,"prix":109.00000000000001,"description":"DESC10"}

3.6.9.4. Der Test [getProduitBy]

Die Ausgabe dieses Tests lautet wie folgt:


    @Test
    public void getProduitBy() {
        // Befüllung
        fill(10);
        log("getProduitBy", 1);
        Produit produit = getDao().getProduitByName("NOM3");
        Produit produit2 = getDao().getProduitById(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
}
  • Zeile 6: Die Methode [getProduitByName] der Schnittstelle [IDao] wird verwendet, um das Produkt mit dem Namen [NOM3] abzurufen;
  • Zeile 7: Anschließend wird die Methode [getProduitById] der Schnittstelle [IDao] verwendet, um dasselbe Produkt abzurufen, das diesmal anhand seines Primärschlüssels identifiziert wird;
  • Zeilen 8–10: Es wird überprüft, ob [produit2] und [produit] dieselben Merkmale aufweisen;

3.6.9.5. Der Test [doInsertsInTransaction]

Dieser lautet wie folgt:


    @Test
    public void doInsertsInTransaction() {
        log("Ajout de deux produits de même nom", 1);
        // Einfügen
        List<Produit> inserts = new ArrayList<Produit>();
        inserts.add(new Produit(0, "x", 1, 1.0, ""));
        inserts.add(new Produit(0, "x", 1, 1.0, ""));
        boolean erreur = false;
        try {
            getDao().addProduits(inserts);
        } catch (DaoException daoException) {
            erreur = true;
        }
        // Prüfungen
        Assert.assertTrue(erreur);
        List<Produit> produits = getDao().getAllProduits();
        Assert.assertEquals(0, produits.size());
}
  • Zeilen 5–7: Es wird eine Liste mit zwei Produkten erstellt, die denselben Namen „[x]“ haben;
  • Zeile 10: Diese beiden Produkte werden in die Tabelle [PRODUITS] eingefügt, die leer ist (Methode [clean], annotiert mit [@Before]). Das erste Einfügen wird durchgeführt, das zweite jedoch nicht, da die Tabelle [PRODUITS] eine Eindeutigkeitsbeschränkung für den Produktnamen enthält. Es muss daher eine Ausnahme auftreten. Diese wird in Zeile 15 getestet;
  • da alle Methoden der Schnittstelle [IDao] innerhalb einer Transaktion ausgeführt werden, führt das Scheitern des zweiten Einfügungsvorgangs dazu, dass die gesamte Transaktion – und damit auch der erste Einfügungsvorgang – rückgängig gemacht wird. Letztendlich darf kein Einfügungsvorgang in die Tabelle [PRODUITS] erfolgen;
  • Zeilen 16–17: Dies wird überprüft, indem die Liste der in der Tabelle [PRODUITS] enthaltenen Produkte abgefragt und sichergestellt wird, dass diese Liste leer ist;

3.6.9.6. Der Test [updateProduits]

Dieser lautet wie folgt:


    @Test
    public void updateProduits() {
        // Ausfüllen
        fill(10);
        log("Mise à jour du prix des produits de catégorie 1", 1);
        // Produkte abrufen
        List<Produit> produits = getDao().getAllProduits();
        // die Produkte der Kategorie 1 werden aktualisiert
        List<Produit> updated = new ArrayList<Produit>();
        int nbUpdated = 0;
        for (Produit produit : produits) {
            if (produit.getCategorie() == 1) {
                // int id, String name, int category, double price, String description
                updated
                        .add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
                nbUpdated++;
            }
        }
        int nbProduits = getDao().updateProduits(updated);
        // Prüfungen
        // Assert.assertEquals(nbUpdated, nbProduits); -- funktioniert nicht mit DB2
        for (Produit produit : updated) {
            Produit produit2 = getDao().getProduitById(produit.getId());
            Assert.assertEquals(produit2.getPrix(), produit.getPrix(), 1e-6);
        }
}
  • Zeile 4: Man fügt 10 Produkte in die Datenbank ein;
  • Zeile 7: Man ruft sie ab;
  • Zeilen 9–18: Die Preise der Produkte der Kategorie Nr. 1 werden um 10 % erhöht;
  • Zeile 19: Diese Änderungen werden in der Datenbank gespeichert;
  • Zeilen 22–25: Die Produktliste, die für die Aktualisierung verwendet wurde, wird im Speicher durchlaufen. Für jedes Produkt wird in der Datenbank das Produkt mit dem gleichen Primärschlüssel abgerufen und überprüft, ob die Preisaktualisierung tatsächlich stattgefunden hat;
  • Zeile 19: Die Anzahl der durch den Vorgang [updateProduits] aktualisierten Produkte wird ermittelt;
  • Zeile 21: Es wird überprüft, ob diese Anzahl der erwarteten entspricht. Dieser Test ist für alle SGBD erfolgreich, außer für SGBD und DB2. Diese wurden daher auskommentiert;

3.6.9.7. Der Test [deleteProduits]

Dieser lautet wie folgt:


    @Test
    public void deleteProduits() {
        // Ausfüllen
        fill(10);
        log("deleteProduits", 1);
        // Produktliste
        List<Produit> produits = getDao().getAllProduits();
        // Löschung von zwei Produkten
        Produit produit0 = produits.get(0);
        Produit produit5 = produits.get(5);
        int nbDeleted = getDao().deleteProduits(new int[] { produit0.getId(), produit5.getId() });
        // Überprüfungen
        // Assert.assertEquals(2, nbDeleted); -- funktioniert nicht mit DB2
        Assert.assertNull(getDao().getProduitById(produit0.getId()));
        Assert.assertNull(getDao().getProduitById(produit5.getId()));
        Assert.assertEquals(produits.size() - 2, getDao().getAllProduits().size());
}
  • Zeile 4: Wir fügen 10 Produkte in die Datenbank ein;
  • Zeilen 7–11: Wir rufen alle Produkte aus der Datenbank ab und löschen die an den Positionen 0 und 5 abgerufenen Produkte aus der Datenbank;
  • Zeilen 14–16: Es wird überprüft, ob die beiden Produkte nicht mehr in der Datenbank vorhanden sind und ob diese nun zwei Produkte weniger enthält;
  • Der Test in Zeile 13 schlägt bei den SGBD und DB2 fehl. Bei den anderen SGBD ist er erfolgreich;

3.6.9.8. Die Leistungstests

Wir haben drei Methoden in die Tests aufgenommen, die ausschließlich dazu dienen, die Leistung des SGBD zu bewerten:


    @Test
    public void perf1() {
        // Ausfüllen
        fill(10000);
    }

    @Test
    public void perf2() {
        // Ausfüllen
        fill(10000);
        // Änderung
        List<Produit> produits = getDao().getAllProduits();
        // Die Einträge der Kategorie 1 werden aktualisiert
        List<Produit> updated = new ArrayList<Produit>();
        for (Produit produit : produits) {
            // int id, String name, int category, double price, String description
            updated.add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
        }
        getDao().updateProduits(updated);
    }

    @Test
    public void perf3() {
        // Befüllung
        fill(10000);
        // Löschen
        List<Produit> produits = getDao().getAllProduits();
        // Primärschlüssel
        int[] keys = new int[produits.size()];
        for (int i = 0; i < keys.length; i++) {
            keys[i] = produits.get(i).getId();
        }
        getDao().deleteProduits(keys);
}
  • Zeilen 1–5: Einfügen von 10.000 Produkten;
  • Zeilen 8–20: Einfügen von 10.000 Produkten und anschließende Änderung dieser Produkte über ihre Primärschlüssel;
  • Zeilen 23–34: Einfügen von 10.000 Produkten und anschließendes Löschen dieser über ihre Primärschlüssel;

Um die Tests [JUnitTestDao1] und [JUnitTestDao2] auszuführen, können die folgenden Ausführungskonfigurationen verwendet werden:

Die Ergebnisse des Tests [JUnitTestDao1] lauten wie folgt:

In [1] sind die Ergebnisse von [JUnitTestDao1] enthalten, und in [2] die von [JUnitTestDao2]. Es gibt keine nennenswerten Unterschiede zwischen ihnen. In [1]:

  • Der Test ist erfolgreich;
  • das Einfügen von 10.000 Produkten dauert 3,15 Sekunden;
  • das Einfügen von 10.000 Produkten mit anschließender Bearbeitung dauert 4,80 Sekunden;
  • das Einfügen von 10.000 Produkten mit anschließender Löschung dauert 4,40 Sekunden;
  • das Einfügen ist also am zeitaufwendigsten;