3. Einführung in die JDBC-API
3.1. Einrichten der Arbeitsumgebung
Wir werden mit einer MySQL5-Datenbank arbeiten.
Sie benötigen:
- ein JDK (Java Development Kit) installiert haben (Abschnitt 23.1);
- den Maven-Abhängigkeitsmanager installiert haben (Abschnitt 23.2);
- die Spring Tool Suite (STS) IDE installiert haben (Abschnitt 23.3);
- das DBMS MySQL5 (Abschnitt 23.4) und dessen EMS-MyManager-Client (Abschnitt 23.5) installiert;
- den Quellcode von [http://tahe.developpez.com/java/spring-database] heruntergeladen haben;
Wir gehen im Folgenden davon aus, dass der MySQL5-Administrator root mit dem Passwort root ist. Starten Sie das MySQL5-DBMS und dessen Client [MyManager]. Mit [MyManager] erstellen wir die Datenbank [dbproduits] [1-34]:
![]() |
- in [3] muss die Datenbank den Namen [dbproduits] tragen;
![]() |
- in [8-9], Root mit dem Root-Passwort (das im obigen Screenshot nicht angezeigt wird);
![]() |
- in [14a] lautet das Passwort erneut „root“ (was auf dem Screenshot nicht zu sehen ist);
- in [15] wurde die Datenbank [dbproduits] erstellt;
![]() |
- Achten Sie in [20] auf die ausgewählte Datenbank. Es muss sich um die Datenbank [dbproduits] handeln;
![]() |
- in [22] lautet der Ordner <examples>/spring-database-config/mysql/databases, wobei <examples> der Ordner ist, der die heruntergeladenen Beispiele enthält;
- Wählen Sie in [23] das SQL-Skript [dbproduits.sql] aus. Dadurch wird die Tabelle [PRODUITS] in der Datenbank [dbproduits] erstellt;
![]() |
![]() |
- In [30] wurde die Tabelle [products] erstellt;
![]() |
- in [33] die Spalten der Tabelle [products];
![]() |
- in [34] ist sie zunächst leer;
Importieren Sie nun mithilfe von STS die folgenden Projekte (gehen Sie dabei genauso vor wie bei den Projekten im Ordner <examples>/spring-core):
![]() |
- In [2] befindet sich das Projekt [mysql-config-jdbc] im Ordner [<examples>/spring-database-config/mysql/eclipse/mysql-config-jdbc] [1];
Dieses Projekt konfiguriert die JDBC-Schicht der folgenden Architektur:
![]() |
Importieren Sie anschließend die folgenden drei Projekte erneut:
![]() |
- In [2] befinden sich die Projekte im Ordner [<examples>/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 von den Projekten [oracle-config-jdbc, db2-config-jdbc, ...] generiert. Um sicherzustellen, dass die derzeit in STS geladenen [spring-generic-jdbc-*]-Projekte tatsächlich das Projekt [mysql-config-jdbc] verwenden:
- Stellen Sie sicher, dass kein anderes [sgbd-config-jdbc]-Projekt 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 Build-Konfiguration [spring-jdbc-generic-01.IntroJdbc01] aus [1-3]:
![]() |
Sie sollten die folgende Konsolenausgabe sehen:
In den folgenden Beispielen kann der Leser:
3.2. Schritte zur Bedienung einer Datenbank
![]() |
In der oben dargestellten Architektur umfasst die Bedienung einer Datenbank über das Konsolenprogramm die folgenden Schritte:
- Laden des JDBC-Treibers der Datenbank;
- Öffnen einer Verbindung zur Datenbank;
- Ausführen einer SQL-Abfrage in der Datenbank und Verarbeiten der Ergebnisse der SQL-Abfrage;
- Schließen der Verbindung;
Schritt 1 wird nur einmal ausgeführt. Die Schritte 2–4 werden wiederholt ausgeführt. Beachten Sie, dass Verbindungen nicht offen bleiben; sie werden geschlossen, sobald sie nicht mehr benötigt werden.
3.2.1. Schritt 1 – Laden des JDBC-Treibers in den Arbeitsspeicher
Der Code
// driver loading JDBC
try {
Class.forName(nom de la classe du pilote JDBC);
} catch (ClassNotFoundException e1) {
// handle the exception
}
Der Zweck des Vorgangs in Zeile 3 besteht darin, den JDBC-Treiber der Datenbank in den Arbeitsspeicher zu laden. Dieser Vorgang muss nur einmal durchgeführt werden. Eine Wiederholung führt jedoch nicht zu einem Fehler. Die JDBC-Treiberklasse wird im Klassenpfad des Projekts gesucht. Daher muss im Eclipse-Projekt die [jar]-Datei, die die JDBC-Treiberklasse enthält, in den Klassenpfad des Projekts aufgenommen worden sein.
3.2.2. Schritt 2 – Eine Verbindung öffnen
Sobald der JDBC-Treiber vorhanden ist, weisen wir ihn an, eine Verbindung zur Datenbank 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 {
// opening connection
connexion = DriverManager.getConnection(url, user, passwd);
...
} catch (SQLException e1) {
// we handle the exception
...
} finally {
// close connection
if (connexion != null) {
try {
connexion.close();
} catch (SQLException e2) {
// handle the exception
...
}
}
}
- Zeilen 3–7: Die Klassen, die die JDBC-Schnittstelle implementieren, befinden sich alle im Paket [java.sql]. Außerdem lösen sie im Fehlerfall alle eine [SQLException] aus (Zeilen 19, 27). Diese Ausnahme leitet sich von der Klasse [Exception] ab und ist eine sogenannte geprüfte Ausnahme: Sie müssen einen try/catch-Block verwenden, um sie zu behandeln, oder alternativ entscheiden, sie nicht zu behandeln, und durch Hinzufügen von [throws SQLException] zur Methodensignatur angeben, dass die Methode die Weitergabe der Ausnahme zulässt;
- Zeile 17: [DriverManager.getConnection] ist eine statische Methode, die drei Parameter erwartet:
- [url]: die Datenbank-URL. Dies ist eine Zeichenkette, die von der verwendeten Datenbank abhängt. Für MySQL hat sie das Format [jdbc:mysql://localhost:3306/db_name];
- [user]: der Eigentümer der Verbindung;
- [passwd]: das Passwort des Benutzers;
- Zeilen 24–30: Die Verbindung muss in der [finally]-Klausel geschlossen werden, damit sie unabhängig davon geschlossen wird, ob eine Ausnahme auftritt oder nicht.
3.2.3. Schritt 3 – Ausführen von SQL-[SELECT]-Anweisungen
Sobald eine Verbindung hergestellt ist, können SQL-Befehle ausgeführt werden. Die Art und Weise, wie Leseanweisungen [SELECT] behandelt werden, unterscheidet sich von der für Aktualisierungsoperationen [UPDATE, INSERT, DELETE]. Wir beginnen mit [SELECT]-SQL-Befehlen:
Der Code
Connection connexion = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// opening connection
connexion = DriverManager.getConnection(url, user, passwd);
// start of transaction
connexion.setAutoCommit(false);
// in read-only mode
connexion.setReadOnly(true);
// table [PRODUITS] is read
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)));
}
// commit transaction
connexion.commit();
} catch (SQLException e1) {
// we handle the exception
doCatchException(connexion,e1);
} finally {
// we treat the finally
doFinally(rs, ps, connexion);
}
private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
....
}
- Zeilen 8, 10: Öffnen einer Transaktion (Zeile 8) im schreibgeschützten Modus (Zeile 10). Eine Transaktion ist eine Abfolge von SQL-Anweisungen, die entweder alle erfolgreich sind oder alle fehlschlagen. Wenn also in einer Transaktion mit N SQL-Anweisungen die (I+1)-te Anweisung fehlschlägt, werden die vorangegangenen I Anweisungen zurückgesetzt. Für einen Lesevorgang ist keine Transaktion erforderlich. Das Erstellen einer schreibgeschützten Transaktion kann es bestimmten DBMS jedoch ermöglichen, bestimmte Optimierungen durchzuführen;
- Zeile 12: Verwendung eines [PreparedStatement]. Ein [PreparedStatement] enthält normalerweise Parameter, die durch das Zeichen ? gekennzeichnet sind. Hier enthält es keine. Ein [PreparedStatement] ist eine vom DBMS vorbereitete Anweisung. Diese Vorbereitung ist mit Aufwand verbunden und wird nur einmal durchgeführt. Diese vorbereitete Anweisung wird dann vom DBMS mit tatsächlichen Parametern ausgeführt, die die Platzhalterparameter ? ersetzen. Beachten Sie, dass es vorzuziehen ist, die gewünschten Spalten anzugeben, anstatt die *-Notation zu verwenden, um alle Spalten abzurufen. Durch die Angabe der Spaltennamen können deren Werte dann basierend auf ihrer Position in der SELECT-Anweisung abgerufen werden;
- Zeile 13: Ausführung des [PreparedStatement]. Ein [ResultSet]-Objekt wird abgerufen;
Ein [ResultSet]-Objekt repräsentiert eine Tabelle, d. h. eine Menge von Zeilen und Spalten. Zu jedem Zeitpunkt haben wir nur Zugriff auf eine Zeile der Tabelle, die als aktuelle Zeile bezeichnet wird. Wenn das [ResultSet] anfänglich erstellt wird, gibt es keine aktuelle Zeile. Wir müssen eine [ResultSet.next()]-Operation ausführen, um sie zu erhalten. Die Signatur der next-Methode lautet wie folgt:
Diese Methode versucht, zur nächsten Zeile des [ResultSet] zu springen, 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 wiederhergestellt werden.
Die [ResultSet]-Tabelle enthält Spalten mit den Namen labelCol1, labelCol2, ..., wie in der ausgeführten [SELECT]-Abfrage angegeben. 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 im [ResultSet] mit dem Namen [myId] übernommen;
- die Spalte [NAME] wird in eine Spalte im [ResultSet] mit dem Namen [myName] übernommen;
- ...
Im obigen Beispiel werden die Bezeichner [myCol] als Spaltenbezeichnungen bezeichnet. Ohne diese Bezeichnungen hängen die Namen der [ResultSet]-Spalten vom DBMS ab. Wenn die [SELECT]-Anweisung auf eine einzelne Tabelle angewendet wird, entsprechen die Spaltenbezeichnungen standardmäßig den Namen der von der SELECT-Anweisung angeforderten Spalten. Das Problem tritt auf, wenn die [SELECT]-Anweisung auf mehrere Tabellen angewendet wird und diese Tabellen identische Spaltennamen enthalten, wie im folgenden Beispiel:
SELECT PRODUITS.NOM, CATEGORIES.NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID
vorausgesetzt, die Tabelle [PRODUCTS] verfügt über einen Fremdschlüssel zur Tabelle [CATEGORIES], der durch die Beziehung [PRODUCTS].CATEGORY_ID --> [CATEGORIES].ID dargestellt wird, und dass sowohl die Tabelle [PRODUCTS] als auch die Tabelle [CATEGORIES] ein Feld [NAME] enthalten. In diesem Fall sind die Namen, die im [ResultSet] den Spalten [PRODUITS.NOM] und [CATEGORIES.NOM] zugewiesen werden, DBMS-abhängig. Um die Portabilität zwischen verschiedenen DBMS zu gewährleisten, müssen hier daher Spaltenbezeichnungen verwendet werden, und wir würden schreiben:
SELECT PRODUITS.NOM as p_NOM, CATEGORIES.NOM as c_NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID
Um auf die verschiedenen Felder der aktuellen Zeile im [ResultSet] zuzugreifen, stehen folgende Methoden zur Verfügung:
um die Spalte mit dem Namen „labelColi“ aus der aktuellen Zeile abzurufen, d. h. die Spalte in der [SELECT]-Anweisung mit dieser Bezeichnung. „Type“ bezieht sich auf den Datentyp des Feldes „labelColi“. Die folgenden [getType]-Methoden können verwendet werden: getInt, getLong, getString, getDouble, getFloat, getDate, ... Anstelle des Spaltennamens können Sie dessen Position in der ausgeführten [SELECT]-Abfrage verwenden:
wobei i der Index der gewünschten Spalte ist (i >= 1).
- Zeilen 15–17: Abruf der aus der Datenbank gelesenen Werte;
- Zeile 19: Die Transaktion wird validiert (auch als „committed“ bezeichnet). Dadurch wird sie beendet und die Ressourcen, die das DBMS dafür zugewiesen hatte, werden freigegeben;
- Zeile 25: Die Ressourcen werden im [finally]-Block freigegeben. Dies ruft die folgende [doFinally]-Methode auf:
private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
// closure ResultSet
if (rs != null) {
try {
rs.close();
} catch (SQLException e1) {
}
}
// closure [PreparedStatement]
if (ps != null) {
try {
ps.close();
} catch (SQLException e2) {
}
}
if (connexion != null) {
try {
// close connection
connexion.close();
} catch (SQLException e3) {
// handle the exception
}
}
}
- Zeilen 3–9: [ResultSet] schließen;
- Zeilen 11–17: Schließen des [PreparedStatement];
- Zeilen 18–27: Schließen der Verbindung;
Die Schließungen in den Zeilen 3–17 scheinen überflüssig, da die Verbindung in den Zeilen 18–25 geschlossen wird. Tatsächlich sind sie in manchen Fällen jedoch nicht überflüssig, 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 [doCatchException]-Methode behandelt:
private static void doCatchException(Connection connexion, Throwable th) {
// cancel transaction
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e2) {
// handle the exception
}
}
- Zeilen 4–6: Die Transaktion wird zurückgesetzt. Dadurch wird sie beendet, und das DBMS kann die ihr zugewiesenen Ressourcen freigeben;
3.2.4. Schritt 3 – Ausführen von SQL-Anweisungen [INSERT, UPDATE, DELETE]
SQL-Anweisungen [INSERT, UPDATE, DELETE] sind Aktualisierungsoperationen: Sie ändern die Datenbank, geben jedoch keine Zeilen zurück. Die einzige zurückgegebene Information ist die Anzahl der von der Aktualisierungsoperation betroffenen Zeilen.
Der Code
Connection connexion = null;
PreparedStatement ps = null;
try {
// ouverture connexion
connexion = DriverManager.getConnection(url, user, passwd);
// début transaction
connexion.setAutoCommit(false);
// en mode lecture / écriture
connexion.setReadOnly(false);
// on met à jour la table
ps = connexion.prepareStatement("UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?");
// catégorie 1
ps.setInt(1, 10);
// exécution
int nbLignes=ps.executeUpdate();
// commit transaction
connexion.commit();
} catch (SQLException e1) {
// on traite l'exception
doCatchException(connexion, e1);
} finally {
// on traite le finally
doFinally(null, ps, connexion);
}
}
- Zeile 9: Die Verbindung wird zum Lesen und Schreiben verwendet;
- Zeile 11: ein [PreparedStatement] mit 1 Parameter (dargestellt durch ?). Es können mehrere Parameter vorhanden sein. Sie werden beginnend mit 1 nummeriert;
- Zeile 13: Sein Wert wird dem einzelnen Parameter zugewiesen. Der erste Parameter von [setType] ist die Position des Parameters im [PreparedStatement] (1, 2, ...) und der zweite ist der ihm zugewiesene Wert. Sie können die Methoden [setInt, setLong, setFloat, setDouble, setString, setDate, ...] verwenden;
- Zeile 15: Es wird die Methode [executeUpdate] verwendet, nicht [executeQuery], die für SELECT-Anweisungen reserviert ist. Die Methode gibt die Anzahl der von der Operation betroffenen Zeilen zurück. Kann 0 sein.
- Zeile 17: Die Transaktion wird festgeschrieben;
3.2.5. Schritt 4 – Schließen der Verbindung
Eine Verbindung muss in einer Mehrbenutzerumgebung so schnell wie möglich geschlossen werden, da ein DBMS nur eine begrenzte Anzahl offener Verbindungen akzeptiert. In den vorherigen Beispielen wurde sie in der [finally]-Klausel der SQL-Operationen geschlossen, damit sie unabhängig davon geschlossen wird, ob eine Ausnahme aufgetreten ist oder nicht.
3.3. Konfiguration der JDBC-Schicht für das Datenbankmanagementsystem MySQL 5
Wir werden uns das Projekt [mysql-config-jdbc] ansehen, das die JDBC-Schicht wie folgt konfiguriert:
![]() |
3.3.1. Das Eclipse-Projekt
![]() |
3.3.2. Maven-Konfiguration
Die [pom.xml]-Datei des Projekts sieht wie folgt aus:
<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>
<!-- dépendances variables ********************************************** -->
<!-- driver JDBC from SGBD -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- dépendances constantes ********************************************** -->
<!-- Tomcat JDBC -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
<!-- library 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>
<!-- logs -->
<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>
Diese Maven-Konfiguration enthält eine Reihe von Archiven, die entweder vom Projekt [mysql-config-jdbc] oder von Projekten benötigt werden, die davon abhängen:
- Zeilen 4–6: das vom Projekt generierte Maven-Artefakt. Wie bereits erwähnt, generieren alle [*-config-jdbc]-Projekte dasselbe Artefakt. Daher dürfen nicht zwei [*-config-jdbc]-Projekte gleichzeitig geladen werden;
- Zeilen 9–13: das übergeordnete Maven-Projekt für dieses Projekt. Es definiert die Versionen einer großen Anzahl von Archiven, die vom Spring-Ökosystem verwendet werden. Dadurch wird vermieden, dass diese in den davon abgeleiteten Projekten angegeben werden müssen;
- Zeilen 18–21: das JDBC-Treiberarchiv für das DBMS MySQL5. Dies ist das einzige Archiv, das vom Projekt [spring-jdbc-01] benötigt wird;
- Zeilen 24–27: Das Artefakt [tomcat-jdbc] stellt ein Archiv bereit, das von den JDBC-Projekten [spring-jdbc-02 bis 04] benötigt wird;
- Zeilen 29–36: stellen die für die JSON-Verarbeitung erforderlichen Bibliotheken bereit. Werden in fast allen Projekten in diesem Dokument verwendet;
- Zeilen 38–42: Google Guava ist eine Bibliothek zur Verwaltung von Sammlungen. Wird in fast allen Projekten in diesem Dokument verwendet;
- Zeilen 43–52: Bibliotheken zum Schreiben von Tests, die Spring und JUnit integrieren. Wird in fast allen Projekten in diesem Dokument verwendet;
- Zeilen 54–57: Protokollierungsbibliotheken. Wird in fast allen Projekten in diesem Dokument verwendet;
- Zeilen 67–71: Das Plugin, das verwendet wird, um das Projekt-Artefakt [mysql-config-jdbc] im lokalen Maven-Repository zu installieren;
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 {
// paramètres de connexion
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";
...
// ordres 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')";
// ordres 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 JDBC-Schicht für die vier Projekte [spring-jdbc-01 bis 04]. Der Großteil der Konfiguration betrifft das Projekt [spring-jdbc-04]. Wir werden diesen Abschnitt behandeln, wenn wir dieses Projekt untersuchen. Oben ist nur die Konfiguration für die Projekte [spring-jdbc-01 bis 03] dargestellt.
- Zeilen 14–17: Verbindungsparameter für die MySQL5-Datenbank [dbproduits];
- Zeilen 20–25: die in den Projekten [spring-jdbc-01 und 02] verwendeten SQL-Anweisungen;
- Zeilen 28–34: die im Projekt [spring-jdbc-03] verwendeten SQL-Anweisungen;
Diese SQL-Anweisungen verwenden die Tabelle [PRODUCTS] in der MySQL5-Datenbank [dbproducts], die folgende Struktur aufweist:
![]() |
- [ID]: Primärschlüssel im AUTO_INCREMENT-Modus (wenn kein Primärschlüssel angegeben ist, generiert das DBMS ihn);
- [NAME]: Name eines Produkts – eindeutig;
- [CATEGORY]: Kategorienummer;
- [PRICE]: dessen Preis;
- [DESCRIPTION]: eine Beschreibung des Produkts;
3.3.4. Die Klasse [Product]
![]() |
Die Klasse [Product] repräsentiert eine Zeile in der Tabelle [PRODUCTS]:
package generic.jdbc.entities.dbproduits;
public class Produit {
// fields
private int id;
private String nom;
private int categorie;
private double prix;
private String description;
// manufacturers
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;
}
// getters and setters
...
}
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 überschreiben wir die [equals]-Methode der [Object]-Klasse, von der die [Product]-Klasse abgeleitet ist:
// méthode d'égalité
@Override
public boolean equals(Object o) {
// cas simples
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 wir sofort erkennen können, dass die beiden Objekte nicht gleich sind. [Object].getClass() gibt eine Instanz vom Typ [Class] zurück, einen Typ, der die tatsächliche Klasse des Objekts darstellt;
- Zeile 8: Das Objekt o wird in ein Produkt p umgewandelt;
- Zeile 9: Sind die beiden Referenzen o und p auf ein Produkt gleich, so verweisen sie physisch auf dasselbe Produkt;
- Zeile 9: Sind o und p zwei verschiedene Referenzen auf zwei Produkte mit denselben Feldern, sagen wir, dass sie gleich sind. 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 innerhalb von 10⁻⁶ voneinander liegen;
Außerdem definieren wir die Methode [hashCode] der Klasse [Object] neu:
// hashcode
@Override
public int hashCode() {
return id + 2 * nom.hashCode() + 3 * categorie + 4 * description.hashCode();
}
Die hashCode-Werte zweier Produkte müssen identisch sein, wenn die [equals]-Methode diese beiden Produkte als gleich erklärt hat. Dieser hashCode-Wert wird verwendet, um Objekte in Sammlungen wie beispielsweise Wörterbüchern zu sortieren. Im obigen Beispiel haben zwei identische Produkte tatsächlich denselben hashCode.
3.3.5. Die [UncheckedException]
![]() |
Betrachten Sie die folgende Architektur:
![]() |
- Die [JDBC]-Schicht löst [SQLException]-Ausnahmen aus. Diese Ausnahme muss durch die Schichten nach oben weitergeleitet werden, bis sie die oberste Schicht erreicht, in diesem Fall die Testschicht;
Die [DAO]-Schicht könnte die [SQLException] einfach bis zur Testschicht weiterleiten. Da es sich bei dieser Ausnahme jedoch um eine ungeprüfte Ausnahme handelt (sie leitet sich direkt von [Exception] ab), würde dies bedeuten, dass die [IDao]-Schnittstelle der [DAO]-Schicht wie folgt aussehen würde:
public interface IDao {
// ajouter des produits
public List<Produit> addProduits(List<Produit> produits) throws SQLException;
// liste de tous les produits
public List<Produit> getAllProduits() throws SQLException;
// un produit particulier
public Produit getProduitById(int id) throws SQLException;
public Produit getProduitByName(String name) throws SQLException;
// mise à jour de plusieurs produits
public int updateProduits(List<Produit> produits) throws SQLException;
// suppression de tous les produits
public int deleteAllProduits() throws SQLException;
// suppression de plusieurs produits
public int deleteProduits(int[] ids) throws SQLException;
}
Und das ist sehr ärgerlich, da es uns daran hindert, die [IDao]-Schnittstelle mit einer Klasse zu implementieren, die eine andere Ausnahme auslösen würde. Um dieses Problem zu umgehen, löst die [DAO]-Schicht eine unbehandelte [DaoException] (abgeleitet von [RuntimeException]) aus, wodurch wir die [throws]-Klausel in den Signaturen der Schnittstellenmethoden weglassen können. Dadurch kann die Schnittstelle von jeder Klasse implementiert werden, die ebenfalls eine ungeprüfte Ausnahme auslöst, die sich von der [DaoException] unterscheiden kann. Unsere Architektur sieht nun wie folgt aus:
![]() |
Um die Erstellung ungeprüfter 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;
// generic exception class
// the exception is uncontrolled
public class UncheckedException extends RuntimeException {
// serial ID generated
private static final long serialVersionUID = -2924871763340170310L;
// properties
private int code;
private String trace;
private List<ShortException> exceptions;
// manufacturers
public UncheckedException() {
super();
}
public UncheckedException(int code, Throwable e, String simpleClassName) {
super(e);
// local
this.code = code;
this.exceptions = getErreursForException(e);
// trace
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);
}
// list of exception error messages
private List<ShortException> getErreursForException(Throwable th) {
// retrieve the elements of the exception stack
Throwable cause = th;
List<ShortException> exceptions = new ArrayList<ShortException>();
while (cause != null) {
// retrieve the current exception
exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
// following exception
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;
}
}
// getters and setters
...
}
- Zeile 12: Die Klasse erweitert [RuntimeException] und ist daher ein Typ für ungeprüfte Ausnahmen. Sie wird verwendet, um eine geprüfte Ausnahme (SQLException) in einen Typ für ungeprüfte Ausnahmen (UncheckedException) zu verpacken;
- Um zwischen [UncheckedException]-Ausnahmen zu unterscheiden, können wir ihnen einen Code zuweisen, der im privaten Feld in Zeile 18 gespeichert wird. Java-Code, der eine [UncheckedException] abfängt, hat über die Methode [getCode] Zugriff auf diesen Fehlercode (Zeilen 80 ff.);
- Zeile 20: Speichert die Fehlermeldungen aus dem Stack der gekapselten Ausnahme;
- Zeilen 23–43: die verschiedenen Möglichkeiten, ein Objekt vom Typ [UncheckedException] zu konstruieren;
- Zeilen 56–67: eine private Methode, die es ermöglicht, die Fehlerliste aus Zeile 20 aus einem [Throwable]-Objekt oder einem abgeleiteten Typ, insbesondere dem Typ [Exception], zu erstellen;
- Zeilen 69–78: Die Methode [toString] gibt eine Zeichenkette zurück, die die Ausnahme darstellt. Um die Fehlerliste aus Zeile 20 anzuzeigen, wird eine JSON-Bibliothek verwendet. Diese Bibliothek ist in den Maven-Abhängigkeiten des Projekts enthalten:
<!-- library 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: Neudefinition der Methode [getMessage] der übergeordneten Klasse [RuntimeException]. Hier wird die Signatur [toString] der Klasse zurückgegeben;
- Zeilen 50–53: Neudefinition der Methode [printStackTrace] der übergeordneten Klasse [RuntimeException]. Die [toString]-Signatur der Klasse wird angezeigt;
Die Klasse [UncheckedException] speichert im Feld in Zeile 20 eine Liste von Ausnahmen, die durch den folgenden Typ [ShortException] beschrieben werden:
package pam.dao.exceptions;
public class ShortException {
// properties
private String className;
private String errorMessage;
// manufacturers
public ShortException() {
}
public ShortException(String className, String errorMessage) {
this.className = className;
this.errorMessage = errorMessage;
}
// getters and setters
...
}
- 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);
// local
this.code = code;
this.exceptions = getErreursForException(e);
// trace
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 zu kapselnde Ausnahme. [Throwable] ist die Oberklasse der Klasse [Exception] und leitet sich direkt von der Klasse [Object] ab. Sie ist die Oberklasse aller C-Klassen, mit denen man [throw c;] schreiben kann, wobei c eine Instanz von C ist;
- [simpleClassName]: der einfache Name der Benutzer-Code-Klasse, in der die Ausnahme e erkannt wurde;
- Zeile 4: Der Fehlercode wird aufgezeichnet;
- Zeile 5: Die Liste von [ShortException] wird aus dem als Parameter übergebenen [Throwable e] erstellt;
- Zeilen 7–16: Die sogenannten Ausnahmepfade werden untersucht. Eine ursprüngliche Ausnahme tritt an einer bestimmten Stelle im Code auf und wird dann zurück an die Methode weitergeleitet, die diejenige aufgerufen hat, in der die Ausnahme aufgetreten ist, und so weiter, bis ein try/catch-Block sie abfängt. Während dieser Weiterleitung hinterlässt die ursprüngliche Ausnahme Spuren, die im Array [e.stackTrace] der Ausnahme e gespeichert sind. Diese werden hier in Zeile 8 aus dem als Parameter übergebenen [Throwable e] abgerufen. Jedes Element vom Typ [StackTraceElement] ist ein Objekt mit den folgenden Feldern:
- [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 das Array der Traces nach der als Parameter übergebenen Ausnahme durchsucht, wobei nach dem ersten Vorkommen der Bedingung [trace.fileName == simpleClassName.java] gesucht wird, wobei [simpleClassName] der dritte Parameter des Konstruktors ist. Die Idee dahinter ist, aufzuzeichnen, wo die Ausnahme im Benutzercode aufgetreten ist. Der Benutzercode wird eine Ausnahme wie folgt umschließen:
- Zeile 13: Wir erstellen eine Zeichenkette vom Typ [Dateiname, Methodenname, Zeilennummer], die die Stelle im Benutzercode beschreibt, an der die Ausnahme e abgefangen wurde;
Betrachten wir nun den Code, der die Liste der Ausnahmen aus dem Ausnahmestapel der Ausnahme [Throwable th] aufzeichnet, die vom vorherigen Konstruktor gekapselt wurde:
// liste des messages d'erreur d'une exception
private List<ShortException> getErreursForException(Throwable th) {
// on récupère les éléments de la pile de l'exception
Throwable cause = th;
List<ShortException> exceptions = new ArrayList<ShortException>();
while (cause != null) {
// on récupère l'exception courante
exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
// exception suivante
cause = cause.getCause();
}
return exceptions;
}
Da sie über einen try/catch-Block an die Methode zurückgegeben wird, die sie abgefangen hat, könnte die ursprüngliche Ausnahme e in einer anderen Ausnahme gekapselt worden sein. Es ist dann diese letztere Ausnahme, die an die Methode zurückgegeben wird, die sie schließlich abfängt. Auch sie kann daher gekapselt sein. Letztendlich findet eine Methode, wenn sie beschließt, eine Ausnahme th abzufangen und zu behandeln, die ursprüngliche Ausnahme e ganz unten in einem Stapel von Ausnahmen verborgen. Somit ist im obigen Beispiel der Parameter [Throwable th] lediglich die Spitze des Ausnahmereisebergs. Sein Attribut [th.cause] gibt die Ausnahme preis, die er selbst kapselt. Und so weiter. Wenn eine Ausnahme e die Bedingung [e.getCause()==null] erfüllt, bedeutet dies, dass e die ursprüngliche Ausnahme ist.
- 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. Projektarchitektur
![]() |
In diesem Beispiel verwendet ein Konsolenprogramm die Schnittstelle der [JDBC]-Schicht.
3.4.2. Das Eclipse-Projekt
Wir erstellen ein Spring/Maven-Projekt [spring-jdbc-01], indem wir die Vorgehensweise in Abschnitt 2.5.2.1 befolgen.
![]() |
Das Projekt ist ein Maven-Projekt, das durch die folgende [pom.xml]-Datei 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 /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- configuration JDBC of 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 nutzt das Artefakt [generic-config-jdbc] aus dem soeben untersuchten Projekt [mysql-config-jdbc]. Das Projekt [spring-jdbc-01] hat somit Zugriff auf alle Elemente des Projekts [mysql-config-jdbc];
Wir können diesen letzten Punkt auf zwei Arten erkennen, indem wir die Maven-Abhängigkeiten des Projekts untersuchen:
![]() |
- In [2] sehen wir, dass das Projekt [mysql-config-jdbc] in den Maven-Abhängigkeiten des Projekts aufgeführt ist. Da sich diese Abhängigkeiten im Classpath des Projekts befinden, bedeutet dies, dass sich auch das Projekt [mysql-config-jdbc] in diesem Classpath befindet und 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 muss lediglich im lokalen Maven-Repository vorhanden sein. Im Gegensatz zu einer IDE wie NetBeans geschieht dies bei Eclipse nicht automatisch. Es muss erzwungen werden:
![]() |
Wir haben die Voraussetzungen für diese Generierung in Abschnitt 2.3.5 behandelt. Sobald dies abgeschlossen ist, können Sie das Projekt [mysql-config-jdbc] aus der Registerkarte [Package Explorer] entfernen:
![]() |
- Aktivieren Sie nicht [3], da dies das Projekt physisch von der Festplatte löscht und es somit unwiederherstellbar macht;
Dieser Vorgang berechnet die Maven-Abhängigkeiten der Projekte neu, die von dem aus dem [Package Explorer] entfernten Projekt abhängen. Dadurch ändert sich der Zweig [Maven-Abhängigkeiten] dieser Projekte. Für das Projekt [spring-jdbc-01] sieht der Zweig [Maven-Abhängigkeiten] beispielsweise wie folgt aus:
![]() |
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]. Wir können sehen, dass wir tatsächlich Zugriff auf alle Klassen und Schnittstellen dieses Artefakts haben. Wie bereits erwähnt, wird dieses Artefakt von allen [*-config-jdbc]-Projekten generiert. Um Fehler zu vermeiden, gehen wir wie folgt vor:
- immer nur ein einziges [*-config-jdbc]-Projekt in der Registerkarte [Package Explorer] behalten;
- aktualisieren die Maven-Konfiguration aller Projekte im Reiter [Package Explorer] (Alt-F5), sodass sie das [*-config-jdbc]-Projekt in ihre Maven-Abhängigkeiten aufnehmen;
3.4.3. Das Gerü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 {
// constants
final static ObjectMapper jsonMapper = new ObjectMapper();
public static void main(String[] args) {
// loading the JDBC driver from SGBD
try {
Class.forName(ConfigJdbc.DRIVER_CLASSNAME);
} catch (ClassNotFoundException e1) {
doCatchException("Pilote JDBC introuvable", null, e1);
return;
}
// empty table [PRODUITS]
System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
delete();
// fill it
System.out.println(String.format("------------------------------ %s", "Remplissage de la table [PRODUITS]"));
insert();
// we read it
System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
select();
// update
System.out.println(String.format("------------------------------ %s", "Mise à jour de la table [PRODUITS]"));
update();
// display
System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
select();
// empty table [PRODUITS]
System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
delete();
// we display it
System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
select();
// INSERTion of two identical elements
// the INSERTion must fail and neither element is inserted because of the transaction
System.out.println(String.format("------------------------------ %s",
"Insertion de deux produits de même clé primaire dans la table [PRODUITS]"));
insert2();
// we check
System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
select();
// finish
System.out.println(String.format("------------------------------ %s", "Travail terminé"));
}
// product list
private static void select() {
...
}
// display jSON of an object
private static void affiche(Object object) {
...
}
// product deletion
public static void delete() {
...
}
// add products
public static void insert() {
...
}
// add 2 products with the same primary keys
public static void insert2() {
...
}
// product updates
public static void update() {
...
}
private static void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
// closure ResultSet
if (rs != null) {
try {
rs.close();
} catch (SQLException e1) {
}
}
// closure [PreparedStatement]
if (ps != null) {
try {
ps.close();
} catch (SQLException e2) {
}
}
if (connexion != null) {
try {
// close connection
connexion.close();
} catch (SQLException e3) {
// display error msg
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) {
// display error msg
show(title, getErreursFromThrowable(th));
// cancel transaction
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e2) {
// display error msg
show("Erreur lors de l'annulation de la transaction", getErreursFromThrowable(e2));
}
}
private static List<String> getErreursFromThrowable(Throwable th) {
// retrieve the list of exception error msgs
List<String> erreurs = new ArrayList<String>();
while (th != null) {
// throwable error message
erreurs.add(th.getMessage());
// we move on to the cause of throwable
th = th.getCause();
}
// result
return erreurs;
}
private static void show(String title, List<String> messages) {
// title
System.out.println(String.format("%s : ", title));
// messages
for (String message : messages) {
System.out.println(String.format("- %s", message));
}
}
}
- Zeilen 23–29: Laden des JDBC-Treibers für das DBMS. In Zeile 25 wird die im Projekt [mysql-config-jdbc] definierte Konstante [ConfigJdbc.DRIVER_CLASSNAME] verwendet;
- Zeilen 136–147: Die Methode [getErrorsFromThrowable] gibt die Liste der Fehlermeldungen zurück, die in einem Objekt vom Typ [Throwable] gekapselt sind, der die Oberklasse der Klasse [Exception] ist. Eine Ausnahme kann eine weitere Ausnahme enthalten, die mit der Methode [Throwable].getCause() abgerufen werden kann. Dies ermöglicht es uns, alle im [Throwable]-Objekt gekapselten Ausnahmen zu durchlaufen;
- Zeilen 149–156: Die Methode [show(String title, List<String> messages)] zeigt die Meldungen an, denen der Text [title] vorangestellt ist;
- Zeilen 122–134: Die Methode [doCatchException(String title, Connection connection, Throwable th)] behandelt Ausnahmen, die von den Methoden der Klasse ausgelöst werden. Die behandelte Ausnahme wird durch den Parameter [Throwable th] repräsentiert. Der Zweck der Methode ist:
- die aktuelle Transaktion des Objekts [Connection connection] zurückzusetzen (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 connection)] verarbeitet den [finally]-Block der DBMS-Zugriffsmethoden. Ihr Zweck ist es, die von der Verbindung zugewiesenen Ressourcen freizugeben;
3.4.4. Löschen des Inhalts der Tabelle „products“
Die Methode [delete] löscht den Inhalt der Tabelle:
// product deletion
public static void delete() {
Connection connexion = null;
PreparedStatement ps = null;
try {
// opening connection
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// start of transaction
connexion.setAutoCommit(false);
// in read/write mode
connexion.setReadOnly(false);
// empty table [PRODUITS]
ps = connexion.prepareStatement(ConfigJdbc.V1_DELETE_PRODUITS);
ps.executeUpdate();
// commit transaction
connexion.commit();
} catch (SQLException e1) {
// we handle the exception
doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
} finally {
// we treat the finally
doFinally(null, ps, connexion);
}
}
Zeile 7 verwendet die folgenden Konstanten aus der Klasse [ConfigJdbc]:
public final static String URL_DBPRODUITS = "jdbc:mysql://localhost:3306/dbproduits";
public final static String USER_DBPRODUITS = "root";
public final static String PASSWD_DBPRODUITS = "";
Zeile 13, die vorbereitete SQL-Anweisung lautet wie folgt:
public final static String V1_DELETE_PRODUITS = "DELETE FROM PRODUITS";
Die Methode [delete] verwendet Transaktionen. Eine Transaktion ermöglicht es Ihnen, SQL-Anweisungen zu gruppieren, die entweder alle erfolgreich sein müssen oder alle zurückgesetzt werden. Es gibt vier Operationen, die Sie beachten sollten:
- Start einer Transaktion: [connection.setAutoCommit(false)];
- Ende einer erfolgreichen Transaktion: [connection.commit()]. In diesem Fall werden alle während der Transaktion an der Datenbank durchgeführten Operationen festgeschrieben;
- Ende einer fehlgeschlagenen Transaktion: [connection.rollback()]. In diesem Fall werden alle während der Transaktion an der Datenbank durchgeführten Operationen zurückgesetzt;
In unseren Beispielen führen wir bei Auftreten einer Ausnahme in der Methode [doCatchException] ein Rollback der Transaktion durch:
private static void doCatchException(String title, Connection connexion, Throwable th) {
// display error msg
Static.show(title, Static.getErreursFromThrowable(th));
// cancel transaction
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e2) {
// display error msg
Static.show("Erreur lors de l'annulation de la transaction", Static.getErreursFromThrowable(e2));
}
}
3.4.5. Erstellen des Inhalts der Produkttabelle
Die Methode [insert] erstellt den Tabelleninhalt:
public static void insert() {
Connection connexion = null;
PreparedStatement ps = null;
try {
// opening connection
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// start of transaction
connexion.setAutoCommit(false);
// in read/write mode
connexion.setReadOnly(false);
// fill the table
ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_WITH_ID);
for (int i = 0; i < 10; i++) {
// preparation
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));
// execution
ps.executeUpdate();
}
// commit transaction
connexion.commit();
} catch (SQLException e1) {
// we handle the exception
doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
} finally {
// we treat the finally
doFinally(null, ps, connexion);
}
}
Zeile 12, die vorbereitete SQL-Anweisung lautet wie folgt:
public final static String V1_INSERT_PRODUITS_WITH_ID = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
3.4.6. Anzeigen des Inhalts der Tabelle „products“
Die Methode [select] zeigt den Tabelleninhalt an:
// product list
private static void select() {
Connection connexion = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// opening connection
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// start of transaction
connexion.setAutoCommit(false);
// in read-only mode
connexion.setReadOnly(true);
// table [PRODUITS] is read
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)));
}
// commit transaction
connexion.commit();
} catch (SQLException e1) {
// we handle the exception
doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
} finally {
// we treat the finally
doFinally(rs, ps, connexion);
}
}
In Zeile 14 lautet die vorbereitete SQL-Anweisung wie folgt:
public final static String V1_SELECT_PRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
Die Methode [display] (Zeile 18) lautet wie folgt:
// display jSON of an object
private static void affiche(Object object) {
try {
System.out.println(jsonMapper.writeValueAsString(object));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
Es zeigt die JSON-Darstellung des als Parameter übergebenen Objekts an (siehe JSON in Abschnitt 23.12).
3.4.7. Aktualisieren des Tabelleninhalts
Die Methode [update] aktualisiert bestimmte Produkte:
// product updates
public static void update() {
Connection connexion = null;
PreparedStatement ps = null;
try {
// opening connection
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// start of transaction
connexion.setAutoCommit(false);
// in read/write mode
connexion.setReadOnly(false);
// table is updated
ps = connexion.prepareStatement(ConfigJdbc.V1_UPDATE_PRODUITS);
// category 1
ps.setInt(1, 1);
// execution
ps.executeUpdate();
// commit transaction
connexion.commit();
} catch (SQLException e1) {
// we handle the exception
doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
} finally {
// we treat the finally
doFinally(null, ps, connexion);
}
}
Zeile 13, die vorbereitete SQL-Anweisung 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 wir uns in einer Transaktion befinden, wird der erste Einfügevorgang rückgängig gemacht.
// add 2 products with the same primary keys
public static void insert2() {
Connection connexion = null;
PreparedStatement ps = null;
try {
// opening connection
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// start of transaction
connexion.setAutoCommit(false);
// in read/write mode
connexion.setReadOnly(false);
// add 1 line
ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_2);
// execution
ps.executeUpdate();
// we add the same line a 2nd time, with the same primary key
// the INSERTion must fail and neither element must be inserted because of the transaction
ps.executeUpdate();
// commit transaction
connexion.commit();
} catch (SQLException e1) {
// we handle the exception
doCatchException("Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire",
connexion, e1);
} finally {
// we treat the finally
doFinally(null, ps, connexion);
}
}
Zeile 13, die vorbereitete SQL-Anweisung 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 wird die folgende Konsolenausgabe angezeigt:
------------------------------ 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;
- das zweite Einfügen schlägt fehl (Zeile 32). Da diese beiden Einfügungen Teil derselben Transaktion sind, werden folglich alle SQL-Anweisungen in dieser Transaktion zurückgesetzt, einschließlich des ersten Einfügens.
3.4.10. Fazit
Auffällig an den vorangegangenen Codeausschnitten ist der erhebliche Platz, der für die Behandlung der [SQLException] aufgewendet wird. Da jede JDBC-Operation potenziell diese Ausnahme auslösen kann, gibt es zahlreiche try/catch-Blöcke im Code.
3.5. Beispiel-02
Wir werden die vorherige Anwendung unter Verwendung einer [javax.sql.DataSource]-Datenquelle erneut betrachten:

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 sie an den Pool zurückgegeben;
Letztendlich werden Verbindungen nur einmal geöffnet, was die Leistung beim Datenbankzugriff verbessert. Die Datenquelle wird in einer Spring-Konfigurationsklasse definiert
3.5.1. Projektarchitektur
![]() |
In diesem Beispiel verwendet ein Konsolenprogramm die Schnittstelle der [JDBC]-Schicht.
3.5.2. Das Eclipse-Projekt
Das neue Eclipse-Projekt kann durch Kopieren des vorherigen Projekts [1-6] erstellt werden:
![]() |
![]() |
Anschließend entwickeln wir das Projekt von [6] zu [7] weiter:
3.5.3. Maven-Konfiguration
Das Projekt [7] ist ein Maven-Projekt, das durch die folgende [pom.xml]-Datei 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 /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- configuration JDBC of 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 [mysql-config-jdbc]-Projekt, das in seinen Maven-Abhängigkeiten die Bibliothek enthält, die eine Implementierung einer [javax.sql.DataSource]-Datenquelle 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] sieht wie folgt aus:
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 {
// data source
@Bean
public DataSource dataSource() {
// data source TomcatJdbc
DataSource dataSource = new DataSource();
// configuration access JDBC
dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
// initially open connections
dataSource.setInitialSize(5);
// result
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 verfügbar sind;
- Zeilen 14–27: die Spring-Bean, die die Datenquelle definiert;
- Zeile 17: Erstellung der Datenquelle, die noch nicht konfiguriert ist;
- Zeilen 19–22: die Informationen, die es der Datenquelle ermöglichen, eine Verbindung zur Datenbank herzustellen;
- Zeile 24: Erstellt einen Pool mit 5 Verbindungen. Wir benötigen hier nur eine. Es gibt niemals 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 {
// mapper jSON
final static ObjectMapper jsonMapper = new ObjectMapper();
// data source
private static DataSource dataSource;
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = null;
try {
// spring context retrieval
ctx = new AnnotationConfigApplicationContext(AppConfig.class);
// data source recovery
dataSource = ctx.getBean(DataSource.class);
// empty table [PRODUITS]
System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
delete();
...
// finish
System.out.println(String.format("------------------------------ %s", "Travail terminé"));
}
// product list
private static void select() {
Connection connexion = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// opening connection
connexion = dataSource.getConnection();
// start of transaction
connexion.setAutoCommit(false);
// in read-only mode
connexion.setReadOnly(true);
// table [PRODUITS] is read
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)));
}
// commit transaction
connexion.commit();
} catch (SQLException e1) {
// we handle the exception
doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
} finally {
// we treat the finally
doFinally(rs, ps, connexion);
}
}
...
- Zeile 25: die Datenquelle. Beachten Sie, dass es sich um den Typ [javax.sql.DataSource] (Zeile 13) handelt, der eine Schnittstelle ist;
- Zeile 31: Instanziierung von Spring-Objekten;
- Zeile 32: Abrufen einer Referenz auf die Datenquelle. Beachten Sie, dass die tatsächlich verwendete Klasse nie erwähnt wird. Daher gibt es hier keinen Hinweis darauf, dass eine [TomcatJdbc]-Implementierung verwendet wird;
- Zeile 49: Abrufen einer offenen Verbindung. Auf diese Weise stellen die verschiedenen Methoden in [IntroJdbc02] eine Verbindung zur Datenbank her. 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:
![]() |
Wir erhalten dieselben Ergebnisse wie zuvor (Abschnitt 3.4.9).
3.6. Beispiel-03
3.6.1. Projektarchitektur
![]() |
In diesem Beispiel sind die Datenzugriffsmethoden in einer [dao]-Schicht isoliert. Sie werden mit einem JUnit-Test getestet.
3.6.2. Das Eclipse-Projekt
Das Eclipse-Projekt [spring-jdbc-03] ist ein Spring/Maven-Projekt, das wie das vorherige aufgebaut und dann wie folgt ergänzt wurde:
![]() | ![]() |
Die verschiedenen Pakete haben folgende Funktionen:
- [spring.jdbc.config]: Spring-Projektkonfiguration;
- [spring.jdbc.dao]: Implementierung der [DAO]-Schicht;
- [spring.jdbc.infrastructure]: Implementierung der unbehandelten Ausnahme [DaoException];
3.6.3. Maven-Konfiguration
Das Maven-Projekt wird durch die folgende [pom.xml]-Datei 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 /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- configuration JDBC of 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>
Es ist identisch mit dem des [spring-jdbc-02]-Projekts. Insbesondere nutzt es die Maven-Abhängigkeit des [mysql-config-jdbc]-Projekts (Zeilen 28–32).
3.6.4. [DAO]-Schicht-Schnittstelle
![]() |
Die [DAO]-Schicht stellt die folgende [IDao]-Schnittstelle bereit:
package spring.jdbc.dao;
import java.util.List;
import spring.jdbc.entities.Produit;
public interface IDao {
// add products
public List<Produit> addProduits(List<Produit> produits);
// list of all products
public List<Produit> getAllProduits();
// a special product
public Produit getProduitById(int id);
public Produit getProduitByName(String name);
// several product updates
public int updateProduits(List<Produit> produits);
// removal of all products
public int deleteAllProduits();
// removal of several products
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;
// manufacturers
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 identisch mit der Spring-Konfigurationsdatei im Beispiel [spring-jdbc-02], mit Ausnahme von Zeile 11:
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 {
// data source
@Bean
public DataSource dataSource() {
// data source TomcatJdbc
DataSource dataSource = new DataSource();
// configuration access JDBC
dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
// initially open connections
dataSource.setInitialSize(5);
// result
return dataSource;
}
}
- Zeile 11: Das Paket [spring.jdbc.dao] wird durchsucht, um neben den in dieser Konfigurationsdatei definierten Komponenten weitere Spring-Komponenten zu finden;
3.6.7. Implementierung der [DAO]-Schicht
![]() |
![]() |
Erinnern Sie sich (Abschnitt 3.6.4), dass die [DAO]-Schicht die folgende [IDao]-Schnittstelle implementiert:
package spring.jdbc.dao;
import generic.jdbc.entities.dbproduits.Produit;
import java.util.List;
public interface IDao {
// add products
public List<Produit> addProduits(List<Produit> produits);
// list of all products
public List<Produit> getAllProduits();
// a special product
public Produit getProduitById(int id);
public Produit getProduitByName(String name);
// several product updates
public int updateProduits(List<Produit> produits);
// removal of all products
public int deleteAllProduits();
// removal of several products
public int deleteProduits(int[] ids);
}
Die Klassen [Dao1] und [Dao2] implementieren beide diese Schnittstelle. Die Klasse [Dao2] ist eine Variante der Klasse [Dao1], die eine neue Syntaxfunktion einführt. Wir konzentrieren uns auf die Klasse [Dao1]. Ihr Grundgerüst 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 {
// class name
private String simpleClassName = getClass().getSimpleName();
// data source
@Autowired
protected DataSource dataSource;
// manufacturer
public Dao1() {
System.out.println("building Dao1...");
}
// ------------------------------- interface
@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) {
...
}
// ---------------------------------------- local methods
// management finally
protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
DaoException daoException) {
...
}
// catch management
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. Wenn er nicht vorhanden ist, wird der Klassenname verwendet, wobei der erste Buchstabe kleingeschrieben wird;
- Zeile 24: der Klassenname. Wir vermeiden eine feste Codierung von [Dao], damit die Klasse umbenannt werden kann, ohne dass dieses Feld neu definiert werden muss, das somit 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 Behandlung der catch-Blöcke für die verschiedenen Methoden;
- Zeilen 72–75: Zentralisierte Behandlung der `finally`-Blöcke für die verschiedenen Methoden;
Die catch-Blöcke der verschiedenen Methoden werden wie folgt behandelt:
// gestion catch
protected DaoException doCatchException(Connection connexion, Throwable th, int code) {
// annulation transaction
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e2) {
e2.printStackTrace();
}
// daoException
return new DaoException(code, th, simpleClassName);
}
- Zeile 2: Die Methode ist als [protected] deklariert, wodurch Unterklassen sie nutzen können, ohne dass sie öffentlich ist. Sie nimmt die folgenden Parameter entgegen:
- [Connection connection]: die Verbindung zum DBMS – kann null sein;
- [Throwable th]: die aufgetretene Ausnahme, die in einen Typ [DaoException] verpackt wird;
- [int code]: ein Fehlercode, der verwendet wird, wenn die Methode eine neue [DaoException] erzeugt;
- Zeilen 4–7: Die Hauptaufgabe dieser Methode besteht darin, die Transaktion zurückzusetzen, die mit der als Parameter 1 übergebenen Verbindung verbunden ist;
- Zeilen 8–10: Wenn das Zurücksetzen der Transaktion fehlschlägt, wird die Ausnahmeprotokollierung in die Konsole geschrieben. Viel mehr können wir nicht tun, da wir in Zeile 12 eine Ausnahme auslösen werden;
Die *finally-Blöcke* der verschiedenen Methoden werden wie folgt behandelt:
// management finally
protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
DaoException daoException) {
// closure ResultSet
if (rs != null) {
try {
rs.close();
} catch (SQLException e1) {
}
}
// closure [PreparedStatement]
if (ps != null) {
try {
ps.close();
} catch (SQLException e2) {
}
}
// close connection
if (connexion != null) {
try {
connexion.close();
} catch (SQLException e3) {
// record the error if possible
if (daoException == null) {
daoException = new DaoException(code, e3, simpleClassName);
}
}
}
// result
return daoException;
}
- Zeile 2: Diese Methode ist ebenfalls als [protected] deklariert. Sie nimmt die folgenden Parameter entgegen:
- [ResultSet rs]: das [ResultSet], falls eine [SELECT]-Anweisung ausgeführt wurde – kann null sein;
- [PreparedStatement ps]: das [PreparedStatement], das ausgeführt wurde – kann null sein;
- [Connection connection]: die Verbindung zum DBMS – kann null sein;
- [int code]: ein Fehlercode, der verwendet wird, wenn die Methode eine neue [DaoException] auslöst;
- [DaoException daoException]: die [DaoException], falls vor dem finally-Block eine aufgetreten ist – kann null sein;
- Zeilen 21–30: Der Hauptzweck dieser Methode ist das Schließen der Verbindung (Zeile 23);
- Zeilen 24–29: Tritt während dieses Schließvorgangs eine Ausnahme auf, prüfen wir den Status des an uns übergebenen Parameters [DaoException daoException]: Ist [daoException == null], erstellen wir eine neue [DaoException] mit dem als Parameter übergebenen Code;
- Zeile 32: Die alte oder neue [DaoException] wird als Ergebnis zurückgegeben;
Wir werden nicht alle Methoden der Klasse [Dao] vorstellen, sondern nur einige wenige. Sie sind alle ähnlich.
3.6.7.1. Die Methode [getProductById]
Die Methode [getProductById] gibt das Produkt zurück, dessen Primärschlüssel dem Parameter [id] entspricht, andernfalls null;
@Override
public Produit getProduitById(int id) {
// connection resources
Connection connexion = null;
PreparedStatement ps = null;
ResultSet rs = null;
// initially no exceptions
DaoException daoException = null;
// the product you are looking for
Produit produit = null;
try {
// opening connection
connexion = dataSource.getConnection();
// start of transaction
connexion.setAutoCommit(false);
// in read-only mode
connexion.setReadOnly(true);
// table [PRODUITS] is read
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));
}
// commit transaction
connexion.commit();
// return to default mode
connexion.setAutoCommit(true);
} catch (SQLException e1) {
// we handle the exception
daoException = doCatchException(connexion, e1, 112);
} finally {
// we treat the finally
daoException = doFinally(rs, ps, connexion, 113, daoException);
}
// exception?
if (daoException != null) {
throw daoException;
}
// result
return produit;
}
- Zeile 10: Das zurückzugebende Produkt wird auf null gesetzt;
- Zeile 19: Die SQL-Anweisung [ConfigJdbc.V2_SELECT_PRODUCT_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 das [ResultSet] eine Zeile enthält, wird diese verwendet, um das zurückzugebende Produkt zu erstellen; andernfalls bleibt das zurückzugebende Produkt null;
- Zeile 41: Das Produkt wird zurückgegeben;
- Zeile 8: Die [DaoException] der Methode wird auf null gesetzt;
- Zeile 31: Die Methode [doCatchException] erstellt eine [DaoException];
- Zeile 34: Der Parameter [daoException] der Methode [doFinally] ist entweder null oder die von der Methode [doCatchException] erzeugte Ausnahme. Die Methode [doFinally]:
- lässt diesen Parameter unverändert, wenn sie die Verbindung erfolgreich schließt;
- lässt diesen Parameter unverändert, wenn das Schließen der Verbindung fehlschlägt und zuvor bereits eine [DaoException] aufgetreten ist;
- erzeugt eine neue [DaoException], wenn das Schließen der Verbindung fehlschlägt und zuvor noch keine [DaoException] aufgetreten ist;
- Zeilen 37–39: Wenn die lokale [daoException] nicht null ist, wird sie ausgelöst; andernfalls wird das angeforderte Ergebnis zurückgegeben (Zeile 41);
3.6.7.2. Die Methode [deleteProducts]
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) {
// connection resources
PreparedStatement ps = null;
Connection connexion = null;
// initially no exceptions
DaoException daoException = null;
// number of products updated
int nbProduits = 0;
try {
// opening connection
connexion = dataSource.getConnection();
// start of transaction
connexion.setAutoCommit(false);
// in read/write mode
connexion.setReadOnly(false);
// we do away with products
ps = connexion.prepareStatement(ConfigJdbc.V2_DELETE_PRODUITS);
for (int id : ids) {
// settings
ps.setInt(1, id);
// execution
nbProduits += ps.executeUpdate();
}
// commit transaction
connexion.commit();
// return to default mode
connexion.setAutoCommit(true);
} catch (SQLException e1) {
// we handle the exception
daoException = doCatchException(connexion, e1, 171);
} finally {
// we treat the finally
daoException = doFinally(null, ps, connexion, 172, daoException);
}
// exception?
if (daoException != null) {
throw daoException;
}
// result
return nbProduits;
}
- Zeile 18, die SQL-Anweisung [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 von Produkten. Wir sehen, dass die SQL-Anweisung einmal vorbereitet (Zeile 18) und n-mal ausgeführt wird (Zeilen 19–24). Dies ist der Vorteil des [PreparedStatement]-Objekts;
- Zeile 23: Die Methode [PreparedStatement].executeUpdate() gibt die Anzahl der von der Aktualisierungsoperation betroffenen Zeilen zurück;
- Zeile 41: gibt die Anzahl der aktualisierten Produkte zurück;
3.6.7.3. Die Methode [updateProducts]
Die Methode [updateProducts] aktualisiert die Produkte, die ihr als Parameter übergeben wurden, in der Datenbank. Sie gibt die Anzahl der aktualisierten Produkte zurück.
@Override
public int updateProduits(List<Produit> produits) {
// connection resources
PreparedStatement ps = null;
Connection connexion = null;
// initially no exceptions
DaoException daoException = null;
// number of products updated
int nbProduits = 0;
try {
// opening connection
connexion = dataSource.getConnection();
// start of transaction
connexion.setAutoCommit(false);
// in read/write mode
connexion.setReadOnly(false);
// table [PRODUITS] is updated
ps = connexion.prepareStatement(ConfigJdbc.V2_UPDATE_PRODUITS);
for (Produit produit : produits) {
// settings
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());
// execution
nbProduits += ps.executeUpdate();
}
// commit transaction
connexion.commit();
// return to default mode
connexion.setAutoCommit(true);
} catch (SQLException e1) {
// we handle the exception
daoException = doCatchException(connexion, e1, 131);
} finally {
// we treat the finally
daoException = doFinally(null, ps, connexion, 132, daoException);
}
// exception?
if (daoException != null) {
throw daoException;
}
// result
return nbProduits;
}
- Zeile 18: Die SQL-Anweisung [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 Produktaktualisierung;
3.6.7.4. Die Methode [addProducts]
Die Methode [addProducts] fügt die als Parameter übergebenen Produkte zur Datenbank hinzu. Sie gibt dieselben Produkte mit ihren Primärschlüsseln zurück (vor dem Hinzufügen zur Datenbank haben die Produkte keinen Primärschlüssel).
@Override
public List<Produit> addProduits(List<Produit> produits) {
// connection resources
PreparedStatement ps = null;
Connection connexion = null;
// initially no exceptions
DaoException daoException = null;
try {
// opening connection
connexion = dataSource.getConnection();
// in read/write mode
connexion.setReadOnly(false);
// start of transaction
connexion.setAutoCommit(false);
// add elements to table [PRODUITS]
String generatedColumns[] = { ConfigJdbc.TAB_PRODUITS_ID };
ps = connexion.prepareStatement(ConfigJdbc.V2_INSERT_PRODUITS, generatedColumns);
for (Produit produit : produits) {
// settings
ps.setString(1, produit.getNom());
ps.setLong(2, produit.getCategorie());
ps.setDouble(3, produit.getPrix());
ps.setString(4, produit.getDescription());
// order execution
ps.executeUpdate();
// generated primary key
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()));
}
}
// commit transaction
connexion.commit();
// return to default mode
connexion.setAutoCommit(true);
} catch (SQLException | RuntimeException e1) {
// we handle the exception
daoException = doCatchException(connexion, e1, 151);
} finally {
// we treat the finally
daoException = doFinally(null, ps, connexion, 152, daoException);
}
// exception?
if (daoException != null) {
throw daoException;
}
// result
return produits;
}
- Zeile 16, die SQL-Anweisung [ConfigJdbc.V2_INSERT_PRODUITS] lautet wie folgt:
public final static String V2_INSERT_PRODUITS = "INSERT INTO PRODUITS(NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?)";
Im obigen Beispiel enthält der Befehl zum Einfügen von Produkten nicht den Primärschlüssel [ID]. Da der Primärschlüssel in der MySQL-Datenbank das Attribut [AUTOINCREMENT] hat, generiert das DBMS bei jedem Einfügen einen Primärschlüssel. Das Problem besteht darin, diesen Schlüssel abzurufen. Dies ist ein wichtiger Punkt, da Operationen an Produkten über ihre Primärschlüssel durchgeführt werden. Wir müssen diese Schlüssel daher kennen;
- Zeilen 17–33: die Schleife zum Einfügen von Produkten;
- Zeile 16: eine spezielle Form der Methode [prepareStatement]. Der zweite Parameter [generatedColumns] ist ein Array von Spaltennamen, deren Werte wir nach dem Einfügen abrufen möchten. In Zeile 16 haben wir angegeben, dass wir den Wert der Spalte [id] abrufen möchten. Beachten Sie hierbei, dass die Namen der Spalten einer Tabelle zwar nicht zwischen Groß- und Kleinschreibung unterscheiden, das PostgreSQL-DBMS jedoch verlangt, dass dieser Name in Kleinbuchstaben geschrieben wird. Dies ist typischerweise die Art von Problem, auf die man stößt, wenn man Code von einem DBMS auf ein anderes portiert;
- Zeile 24: Einfügen einer Zeile in die Datenbank;
- Zeile 26: Ruft die Liste der Werte aus den in Zeile 16 angegebenen Spalten in ein [ResultSet] ab. Hier enthält das [ResultSet] bei einem einzelnen Einfügevorgang eine Zeile, und diese Zeile hat eine einzige Spalte, die den Primärschlüssel enthält;
- Zeile 28: Ruft den vom DBMS generierten Primärschlüssel ab;
- Zeilen 29–32: Wenn der generierte Primärschlüssel nicht abgerufen werden kann, wird eine [RuntimeException] ausgelöst, die in eine [DaoException] verpackt wird (Zeilen 38–40);
3.6.8. Die Klasse [Dao2]
![]() |
![]() |
Die Klasse [Dao2] ist eine Variante der Klasse [Dao1], die eine Syntax namens try-with-resource(resource) verwendet:
- [resource] ist eine Ressource, die die Schnittstelle [java.lang.AutoCloseable] implementiert. Alle Ressourcen, die mit der Methode [close] freigegeben werden, fallen in diese Kategorie. Diese Syntax stellt sicher, dass [resource] in Zeile 4 geschlossen wird. Dadurch muss keine [finally]-Klausel geschrieben werden, um diesen Schließvorgang durchzuführen;
Nehmen wir als Beispiel die Methode [getAllProducts] der Klasse [Dao2]:
@Override
public List<Produit> getAllProduits() {
// possible exception
DaoException daoException = null;
// product list
List<Produit> produits = new ArrayList<Produit>();
try (Connection connexion = dataSource.getConnection()) {
// start of transaction
connexion.setAutoCommit(false);
// in read-only mode
connexion.setReadOnly(true);
// table [PRODUITS] is read
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)));
}
}
// end transaction
connexion.commit();
// return to default mode
connexion.setAutoCommit(true);
} catch (SQLException e1) {
// cancel the transaction
daoException = doRollback(connexion, e1, 111);
}
} catch (SQLException e2) {
// we handle the exception
if (daoException == null) {
daoException = new DaoException(112, e2, simpleClassName);
}
}
// exception?
if (daoException != null) {
throw daoException;
}
// result
return produits;
}
- Zeile 7: try-Block mit der [Connection]-Ressource. Zeile 27 stellt sicher, dass diese geschlossen wird;
- Zeile 13: try-Block mit der Ressource [PreparedStatement]. Zeile 23 stellt sicher, dass diese geschlossen wird;
- Zeile 14: try-Block mit der Ressource [ResultSet]. Zeile 19 stellt sicher, dass er geschlossen wird;
- Zeile 25: Die Transaktion wird wie folgt zurückgesetzt:
private DaoException doRollback(Connection connexion, Throwable e1, int code) {
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e) {
e.printStackTrace();
}
// génération de l'exception
return new DaoException(code, e1, simpleClassName);
}
Am Ende haben wir einen Code, der leichter zu lesen ist.
3.6.9. Implementierung der Testschicht
3.6.9.1. Die Testklassen
![]() |
![]() |
- Der Test [JUnitTestDao1] ist ein JUnit-Test für die Klasse [Dao1];
- Der Test [JUnitTestDao2] ist ein JUnit-Test für die Klasse [Dao2];
- [AbstractJUnitTestDao] ist die übergeordnete Klasse der beiden vorherigen Testklassen;
- [MainTestDao1] ist eine Konsolentestklasse für die Klasse [Dao1];
- [MainTestDao2] ist eine Konsolentestklasse für die Klasse [Dao2];
- [AbstractMainTestDao] ist die übergeordnete Klasse der beiden vorangegangenen Klassen. Sie verwendet den Code aus den bereits vorgestellten Konsolenklassen [IntroJdbc01, IntroJdbc02] wieder, daher werden wir diese Konsolenklassen nicht näher betrachten;
Die Klasse [JUnitTestDao1] sieht wie folgt aus:
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 {
// layer [DAO]
@Autowired
@Qualifier("dao1")
private IDao dao;
@Override
IDao getDao() {
return dao;
}
}
- Die Annotationen in den Zeilen 12–13 wurden in Abschnitt 2.5.5 behandelt. Sie ermöglichen einem JUnit-Test den einfachen Zugriff auf den Spring-Kontext und dessen Beans. Dieser Kontext wird durch die in Abschnitt 2.4.3 behandelte Klasse [AppConfig] (Zeile 12) konfiguriert;
- Zeile 14: Die Klasse erweitert die Klasse [AbstractJUnitTestDao], auf die wir gleich noch eingehen werden. Die JUnit-Testmethoden befinden sich innerhalb dieser Klasse;
- Zeilen 17–19: Die Bean mit dem Namen [dao1] (Zeile 18) wird injiziert (Zeile 17). Somit wird hier eine Instanz der Klasse [Dao1] injiziert;
- 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 [DAO]-Schicht bereitzustellen, in diesem Fall eine Instanz von [Dao1]. In ähnlicher Weise stellt die Klasse [JUnitTestDao2] der übergeordneten Klasse [AbstractJUnitTestDao] eine Instanz der Klasse [Dao2] bereit.
Die Klasse [AbstractJUnitTestDao] ist eine JUnit-Testklasse:
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 {
// layer [DAO]
abstract IDao getDao();
// mapper jSON
final static ObjectMapper jsonMapper = new ObjectMapper();
@Before
public void clean() {
// the base is cleaned before each test
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 methods
...
}
- Zeile 19: Die Klasse [AbstractJUnitTestDao] ist abstrakt;
- Zeile 22: die abstrakte Methode [getDao], die einen Verweis auf die zu testende [DAO]-Schicht bereitstellt. Diese Methode wird von den Unterklassen implementiert;
- Zeile 25: Ein JSON-Mapper, mit dem wir den JSON-Wert von Produkten auf der Konsole anzeigen können;
- Zeilen 27–32: Vor jedem Test (Zeile 27) wird die Tabelle [PRODUCTS] gelöscht;
3.6.9.2. Die private Methode [fill]
Die private Methode [fill] wird verwendet, um Produkte zur Tabelle [PRODUCTS] hinzuzufügen.
private List<Produit> fill(int nbProduits) {
log("Remplissage de la base de données", 1);
// on crée une liste de produits
List<Produit> produits = new ArrayList<Produit>();
for (int i = 0; i < nbProduits; i++) {
int n = i + 1;
// int id, String nom, int categorie, double prix, 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)));
}
// on la persiste en base - on récupère des produits avec leur clé primaire
produits = getDao().addProduits(produits);
// on crée un dictionnaire des produits pour pouvoir les retrouver + facilement
// la clé du dictionnaire est la clé primaire du produit en base
for (Produit produit : produits) {
mapProduits.put(produit.getId(), produit);
}
// on rend les produits
return produits;
}
- Zeile 1: Die Methode [fill] fügt [nbProducts] in die Tabelle [PRODUCTS] ein, von der angenommen wird, dass sie leer ist;
- Zeilen 3–10: Erstellung einer Liste von Produkten in der 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 Product(int id, String name, int category, double price, String description) verwendet. Der Wert des ersten Parameters [id] (Primärschlüssel der Tabelle [PRODUCTS]) ist irrelevant, da die Methode [addProducts] in Zeile 10 ihn nicht in die Datenbank einfügt und das DBMS seinen Wert generieren lässt;
- Zeile 12: Die Liste der Produkte wird in der Datenbank gespeichert. Jedem Produkt in dieser Liste wird ein neuer Primärschlüssel [id] zugewiesen. Die Methode [addProduits] gibt ihren Parameter [produits] zurück. Wir hätten daher das Abrufen des Ergebnisses weglassen können;
- Zeilen 15–17: Die Produkte werden in ein Dictionary eingefügt:
// dictionnaire des produits
private Map<Integer, Produit> mapProduits = new HashMap<Integer, Produit>();
Der Schlüssel des Wörterbuchs ist der Primärschlüssel des Produkts, und der zugehörige Wert ist das Produkt selbst;
- Zeile 19: Wir geben die Liste der Produkte zurück;
3.6.9.3. Der [getProducts]-Test
Dieser sieht wie folgt aus:
@Test
public void getProduits() throws JsonProcessingException {
// remplissage
fill(10);
// liste des produits
log("Liste des produits", 2);
List<Produit> produits = getDao().getAllProduits();
affiche(produits);
// on vérifie que la liste récupérée et celle persistée sont les mêmes
for (Produit produit : produits) {
Produit found = mapProduits.get(produit.getId());
Assert.assertEquals(found, produit);
mapProduits.remove(found.getId());
}
// tous les produits initiaux doivent avoir disparu du dictionnaire
Assert.assertEquals(0, mapProduits.size());
}
}
- Zeile 4: 10 Produkte werden zur Datenbank hinzugefügt;
- Zeile 7: Sobald dies geschehen ist, fordern wir die Anzeige aller Produkte in der Datenbank an;
- Zeile 8: Wir zeigen sie an. Das Ziel ist es, zu überprüfen, ob die Produkte erfolgreich gespeichert wurden und ob sie einen Primärschlüssel haben;
- Zeilen 10–13: Wir überprüfen, ob die abgerufenen Produkte mit den von uns gespeicherten identisch sind und ob sie im Verzeichnis [mapProduits] zu finden sind;
- Zeile 11: Wir rufen aus dem Wörterbuch das Produkt mit demselben Primärschlüssel ab, wie er von der Datenbank zurückgegeben wurde. Dies zeigt, dass den gespeicherten Produkten tatsächlich ein Primärschlüssel zugewiesen wurde;
- Zeile 12: Wir stellen sicher, dass die beiden Produkte identisch sind. Zur Erinnerung: Die Klasse [Product] hat eine [equals]-Methode definiert (siehe Abschnitt 3.3.4);
- Zeile 13: Das gefundene Element wird aus dem Wörterbuch entfernt;
- Zeile 16: Wir überprüfen, 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 abgerufenen Produktliste enthalten waren;
Die Methode [display] in Zeile 8 ist die folgende private Methode:
// product list display
private <T> void affiche(List<T> elements) throws JsonProcessingException {
for (T element : elements) {
System.out.println(jsonMapper.writeValueAsString(element));
}
}
- Zeile 2: Die Methode [display] ist eine generische Methode. Sie ist durch einen Typ T parametrisiert, der syntaktisch als <T> bezeichnet wird. Wäre sie durch zwei Typen T1 und T2 parametrisiert, würden wir <T1,T2> schreiben. Die Syntax einer durch einen Typ T parametrisierten Methode m lautet wie folgt:
Im Code der Methode m finden wir Daten vom Typ T. Die Methode m einer Instanz c der Klasse C kann dann wie folgt aufgerufen werden:
wobei T1 der tatsächliche Typ ist, der den formalen Typ T der Methode m ersetzt. Meistens kann der Compiler den Typ T1 aus den Argumenten der Methode m ableiten. Daher wird die vorstehende Anweisung meist wie folgt vereinfacht:
Kehren wir zur Methode [display] zurück. Sie zeigt eine Liste von Elementen des Typs T an. Dies ist möglich, weil der in Zeile 4 verwendete JSON-Mapper in der Lage ist, die JSON-Darstellung jedes Objekttyps zu rendern. In diesem konkreten Beispiel wird als einziger Typ T der Typ [Product] verwendet.
Die Methode [display] hätte auch wie folgt geschrieben werden können:
// product list display
private void affiche(Object o) throws JsonProcessingException {
System.out.println(jsonMapper.writeValueAsString(o));
}
Da der tatsächliche Parameter eine Liste von Produkten ist, hätte Zeile 3 die JSON-Darstellung dieser Liste ausgegeben. Dies ist nicht dasselbe wie die Darstellung jedes einzelnen Elements nacheinander auszugeben.
Die vom Test [getProducts] erzeugte Ausgabe lautet wie folgt:
3.6.9.4. Der [getProductBy]-Test
Dieser sieht wie folgt aus:
@Test
public void getProduitBy() {
// remplissage
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 [getProductByName] der Schnittstelle [IDao] wird verwendet, um das Produkt mit dem Namen [NAME3] abzurufen;
- Zeile 7: Die Methode [getProductById] der Schnittstelle [IDao] wird dann verwendet, um dasselbe Produkt abzurufen, diesmal anhand seines Primärschlüssels;
- Zeilen 8–10: Wir überprüfen, ob [product2] und [product] dieselben Eigenschaften 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);
// on fait l'insertion
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;
}
// vérifications
Assert.assertTrue(erreur);
List<Produit> produits = getDao().getAllProduits();
Assert.assertEquals(0, produits.size());
}
- Zeilen 5–7: Wir erstellen eine Liste mit zwei Produkten mit demselben Namen [x];
- Zeile 10: Diese beiden Produkte werden in die Tabelle [PRODUCTS] eingefügt, die leer ist (die mit [@Before] annotierte Methode [clean]). Der erste Einfügevorgang wird erfolgreich sein, der zweite jedoch nicht, da die Tabelle [PRODUCTS] eine Eindeutigkeitsbeschränkung für Produktnamen enthält. Daher muss eine Ausnahme auftreten. Dies wird in Zeile 15 getestet;
- Da alle Methoden der [IDao]-Schnittstelle innerhalb einer Transaktion ausgeführt werden, führt das Scheitern des zweiten Einfügungsversuchs zu einem Rollback der gesamten Transaktion, einschließlich des ersten Einfügungsversuchs. Letztendlich sollten keine Einfügungen in die Tabelle [PRODUCTS] vorgenommen werden;
- Zeilen 16–17: Wir überprüfen dies, indem wir die Liste der Produkte aus der Tabelle [PRODUCTS] abrufen und sicherstellen, dass diese Liste leer ist;
3.6.9.6. Der [updateProducts]-Test
Dieser sieht wie folgt aus:
@Test
public void updateProduits() {
// remplissage
fill(10);
log("Mise à jour du prix des produits de catégorie 1", 1);
// on récupère les produits
List<Produit> produits = getDao().getAllProduits();
// on met à jour ceux de catégorie 1
List<Produit> updated = new ArrayList<Produit>();
int nbUpdated = 0;
for (Produit produit : produits) {
if (produit.getCategorie() == 1) {
// int id, String nom, int categorie, double prix, String description
updated
.add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
nbUpdated++;
}
}
int nbProduits = getDao().updateProduits(updated);
// vérifications
// Assert.assertEquals(nbUpdated, nbProduits); -- does not work with DB2
for (Produit produit : updated) {
Produit produit2 = getDao().getProduitById(produit.getId());
Assert.assertEquals(produit2.getPrix(), produit.getPrix(), 1e-6);
}
}
- Zeile 4: Wir fügen 10 Produkte in die Datenbank ein;
- Zeile 7: rufen sie ab;
- Zeilen 9–18: Wir erhöhen die Preise der Produkte in Kategorie Nr. 1 um 10 %;
- Zeile 19: Diese Änderungen werden in der Datenbank gespeichert;
- Zeilen 22–25: Wir durchlaufen die Liste der für die Aktualisierung verwendeten Produkte im Speicher. Für jedes Produkt suchen wir das Produkt mit demselben Primärschlüssel in der Datenbank und überprüfen, ob die Preisaktualisierung erfolgreich war;
- Zeile 19: Abrufen der Anzahl der durch die Operation [updateProducts] aktualisierten Produkte;
- Zeile 21: Wir überprüfen, ob diese Zahl tatsächlich der erwarteten entspricht. Dieser Test wird bei allen DBMS außer DB2 bestanden. Wir haben ihn daher auskommentiert;
3.6.9.7. Der [deleteProducts]-Test
Dieser Test läuft wie folgt ab:
@Test
public void deleteProduits() {
// filling
fill(10);
log("deleteProduits", 1);
// product list
List<Produit> produits = getDao().getAllProduits();
// discontinuation of two products
Produit produit0 = produits.get(0);
Produit produit5 = produits.get(5);
int nbDeleted = getDao().deleteProduits(new int[] { produit0.getId(), produit5.getId() });
// checks
// Assert.assertEquals(2, nbDeleted); -- does not pass with 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 entfernen die Produkte an den Positionen 0 und 5;
- Zeilen 14–16: Wir überprüfen, ob die beiden Produkte nicht mehr in der Datenbank vorhanden sind und ob die Datenbank nun zwei Produkte weniger enthält;
- Der Test in Zeile 13 schlägt beim DB2-DBMS fehl. Bei den anderen DBMS-Systemen ist er erfolgreich;
3.6.9.8. Leistungstests
Wir haben drei Methoden in die Tests aufgenommen, deren einziger Zweck darin besteht, die Leistung des DBMS zu bewerten:
@Test
public void perf1() {
// remplissage
fill(10000);
}
@Test
public void perf2() {
// remplissage
fill(10000);
// modification
List<Produit> produits = getDao().getAllProduits();
// on met à jour ceux de catégorie 1
List<Produit> updated = new ArrayList<Produit>();
for (Produit produit : produits) {
// int id, String nom, int categorie, double prix, String description
updated.add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
}
getDao().updateProduits(updated);
}
@Test
public void perf3() {
// remplissage
fill(10000);
// suppression
List<Produit> produits = getDao().getAllProduits();
// clés primaires
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ßendes Ändern anhand ihrer Primärschlüssel;
- Zeilen 23–34: Einfügen von 10.000 Produkten und anschließendes Löschen anhand ihrer Primärschlüssel;
Um die Tests [JUnitTestDao1] und [JUnitTestDao2] auszuführen, können die folgenden Testkonfigurationen verwendet werden:
![]() | ![]() |
Die Ergebnisse des Tests [JUnitTestDao1] lauten wie folgt:
![]() |
In [1] die Ergebnisse von [JUnitTestDao1] und in [2] die von [JUnitTestDao2]. Es gibt keine signifikanten 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, gefolgt von deren Änderung, dauert 4,80 Sekunden;
- das Einfügen von 10.000 Produkten, gefolgt von deren Löschung, dauert 4,40 Sekunden;
- die aufwendigste Operation ist also das Einfügen;



















































