Skip to content

6. [Cours]: Einführung in API JDBC

Stichworte: relationale Datenbanken, API JDBC, SQLException.

6.1. Support

Der Ordner „[support / chap-06]“ enthält die Eclipse-Projekte dieses Kapitels.

6.2. Architecture

Die Schicht JDBC (Java DataBase Connectivity) ist eine universelle Schnittstelle für den Zugriff auf Datenbanken. Sie bietet gegenüber der Schicht [DAO] stets dieselbe Schnittstelle. Wenn man von SGBD wechselt, muss lediglich der Treiber JDBC geändert werden. Die Schicht [DAO] bleibt unverändert.

6.3. Die Schritte beim Betrieb einer Datenbank

In der oben dargestellten Architektur umfasst der Betrieb einer Datenbank durch das Konsolenprogramm folgende Schritte:

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

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

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

Der Code


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

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

6.3.2. Schritt 2 – Herstellen einer Verbindung

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

Der Code


package spring.jdbc;

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

public class IntroJdbc01 {

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

6.3.3. Schritt 3 – Ausgabe der Befehle SQL und [SELECT]

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

Der Code


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

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

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

    boolean next()

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

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

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

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

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

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


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

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

Type getType("labelColi") 

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

Type getType(i) 

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

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

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

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

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

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

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

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

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

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

Der Code


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

6.3.5. Schritt 4 – Schließen der Verbindung

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

6.4. Ein Beispielprojekt

6.4.1. Support

Der Ordner [support / chap5] enthält die Eclipse-Projekte dieses Kapitels [1, 2]. Der Ordner [database] enthält das Skript SQL, mit dem die Beispieldatenbank MySQL aus diesem Kapitel ([1, 3]) erstellt werden kann.

6.4.2. Die verwendete Datenbank

Die folgenden Beispiele verwenden die folgende Datenbank „MySQL“:

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

Wir erstellen sie mit dem Tool [WampServer] wie folgt: [1-9]:

6.4.3. Das Eclipse-Projekt

  

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


<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>istia.st.jdbc</groupId>
    <artifactId>intro-jdbc-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.34</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.5.1</version>
        </dependency>
    </dependencies>
</project>
  • Zeilen 8–12: der Treiber JDBC für SGBD und MySQL5;
  • Zeilen 13–17: eine Bibliothek, die jSON (JavaScript Object Notation) verarbeiten kann (siehe Abschnitt 22.6). Wir werden sie verwenden, um die Produkte aus der Datenbank im jSON-Format anzuzeigen;

6.4.4. Die Produktklasse

Die Klasse [Produit] sieht wie folgt aus:


package istia.st.jdbc;

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

public class Produit {

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

    // Konstruktoren
    public Produit() {

    }

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

    // Getter und Setter
    ...

    // in String
    public String toString() {
        try {
            return new ObjectMapper().writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
}
  • Zeile 34: Wir verwenden die Bibliothek jSON, um die Zeichenkette jSON des Produkts anzuzeigen. Dies führt zu einer Anzeige, die in etwa wie folgt aussieht:
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"}

Der Vorteil der oben genannten Methode [toString] besteht darin, dass die Methode [toString] auch dann noch gültig ist, wenn der Klasse Felder hinzugefügt oder entfernt werden. Sind die Felder zudem selbst Objekte (Listen, Arrays, Dictionaries, Benutzerobjekte), können die Bibliotheken jSON diese wiederum in eine Zeichenkette jSON umwandeln;

6.4.5. Die Klasse [Static]

Die Klasse [Static] fasst in Methoden den in der Hauptklasse häufig verwendeten Code zusammen:


package istia.st.jdbc;

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

public class Static {

    public static List<String> getErreursFromThrowable(Throwable th) {
        // Die Liste der Fehlermeldungen der Ausnahme wird abgerufen
        List<String> erreurs = new ArrayList<String>();
        while (th != null) {
            // Fehlermeldung des Throwables
            erreurs.add(th.getMessage());
            // Weiter zur Ursache des Throwables
            th = th.getCause();
        }
        // Ergebnis
        return erreurs;
    }
    
    public static void show(String title, List<String> messages){
        // Titel
        System.out.println(String.format("%s : ",title));
        // Meldungen
        for(String message : messages){
            System.out.println(String.format("- %s",message));
        }
    }
}
  • Zeilen 8–19: Ermöglicht den Abruf der Fehlerliste, die in einem Objekt vom Typ [Throwable] gekapselt ist, welches die übergeordnete Klasse der Klasse [Exception] ist;
  • Zeilen 21–28: Zeigt eine Liste von Meldungen auf dem Bildschirm an;

Dieser Code könnte in der Hauptklasse stehen, da er hier ausschließlich von dieser verwendet wird. Wir betrachten hier jedoch einen allgemeineren Fall, in dem auch andere Klassen diesen Code benötigen würden.

6.4.6. Das Grundgerüst der Hauptklasse


package istia.st.jdbc;

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

public class IntroJdbc01 {

    // Konstanten
    final static String url = "jdbc:mysql://localhost:3306/dbIntroJdbc";
    final static String user = "root";
    final static String passwd = "";
    final static String insert = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
    final static String delete = "DELETE FROM PRODUITS";
    final static String select = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    final static String update = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
    final static String insert2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";

    public static void main(String[] args) {
        // Laden des Treibers JDBC von MySQL
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // Die Tabelle [PRODUITS] wird geleert
        delete();
        // die Tabelle wird gefüllt
        insert();
        // die Tabelle wird gelesen
        select();
        // Aktualisierung
        update();
        // Anzeige
        select();
        // Einfügen von zwei identischen Elementen
        // Das Einfügen muss fehlschlagen, und aufgrund der Transaktion wird keines der beiden Elemente eingefügt
        insert2();
        // Es wird geprüft
        select();
        // fertig
        System.out.println("Travail terminé");
    }

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

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

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

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

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

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

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

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

    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // Fehlermeldungen werden angezeigt
        Static.show(title, Static.getErreursFromThrowable(th));
        // Transaktion abbrechen
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // Fehlermeldungen werden angezeigt
            Static.show(title, Static.getErreursFromThrowable(e2));
        }
    }
}

6.4.7. Löschen des Inhalts der Produkttabelle

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


// Produkte löschen
    public static void delete() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // Verbindung herstellen
            connexion = DriverManager.getConnection(url, user, passwd);
            // Transaktion starten
            connexion.setAutoCommit(false);
            // Tabelle [PRODUITS] leeren
            ps = connexion.prepareStatement(delete);
            ps.executeUpdate();
            // Transaktion festschreiben
            connexion.commit();
            // Rückkehr in den Standardmodus
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // Ausnahme wird behandelt
            doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
        } finally {
            // „finally“-Block wird verarbeitet
            doFinally(null, null, connexion);
        }
    }

In diesem Beispiel werden Transaktionen verwendet. Eine Transaktion ermöglicht es, Aufträge SQL zu bündeln, die entweder alle erfolgreich ausgeführt oder alle abgebrochen werden müssen. Es sind vier Vorgänge zu beachten:

  • Start einer Transaktion: [connexion.setAutoCommit(false)];
  • Erfolgreicher Abschluss einer Transaktion: [connexion.commit()]. In diesem Fall werden alle Vorgänge, die während der Transaktion unter der Nummer BD durchgeführt wurden, freigegeben;
  • Abbruch einer Transaktion: [connexion.rollback()]. In diesem Fall werden alle während der Transaktion an der BD durchgeführten Vorgänge storniert;
  • Rückkehr in den Modus [auto-commit], der der Standardmodus von API ist: JDBC: [connexion.setAutoCommit(true)]. In diesem Modus ist jeder Befehl SQL Gegenstand einer Transaktion. Wenn also zwei Einfügungen vorgenommen werden, von denen die zweite fehlschlägt:
    • im Modus [AutoCommit=true] bleibt der erste Einfügevorgang erhalten (er wurde durch den ersten Befehl AutoCommit bestätigt);
    • im Modus [AutoCommit=false] wird der erste Einfügevorgang rückgängig gemacht;

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


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

6.4.8. Erstellung des Inhalts der Produkttabelle

Die Methode [insert] erstellt den Inhalt der Tabelle:


// Produkte hinzufügen
    public static void insert() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // Verbindung herstellen
            connexion = DriverManager.getConnection(url, user, passwd);
            // Transaktion starten
            connexion.setAutoCommit(false);
            // Tabelle wird gefüllt
            ps = connexion.prepareStatement(insert);
            for (int i = 0; i < 10; i++) {
                // Vorbereitung
                int n = i + 1;
                ps.setInt(1, n);
                ps.setString(2, String.format("NOM%s", n));
                ps.setInt(3, n / 5 + 1);
                ps.setDouble(4, 100 * (1 + (double) i / 100));
                ps.setString(5, String.format("DESC%s", n));
                // Ausführung
                ps.executeUpdate();
            }
            // Transaktion festschreiben
            connexion.commit();
            // Zurück zum Standardmodus
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // Ausnahme wird behandelt
            doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
        } finally {
            // „finally“-Block wird verarbeitet
            doFinally(null, null, connexion);
        }
    }

6.4.9. Anzeige des Inhalts der Produkttabelle

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


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

6.4.10. Aktualisierung des Tabelleninhalts

Die Methode [update] aktualisiert bestimmte Produkte:


// Aktualisierung bestimmter Produkte
    public static void update() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // Verbindung herstellen
            connexion = DriverManager.getConnection(url, user, passwd);
            // Transaktionsbeginn
            connexion.setAutoCommit(false);
            // die Tabelle wird aktualisiert
            ps = connexion.prepareStatement(update);
            // Kategorie 1
            ps.setInt(1, 1);
            // Ausführung
            ps.executeUpdate();
            // Transaktion festschreiben
            connexion.commit();
            // Rückkehr zum Standardmodus
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // Ausnahme wird behandelt
            doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
        } finally {
            // „finally“-Block wird verarbeitet
            doFinally(null, null, connexion);
        }
    }

6.4.11. Rolle der Transaktion

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


// Hinzufüge n von 2 Produkten mit denselben Primärschlüsseln
    public static void insert2() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // Verbindung wird geöffnet
            connexion = DriverManager.getConnection(url, user, passwd);
            // Transaktion wird gestartet
            connexion.setAutoCommit(false);
            // 1 Zeile wird hinzugefügt
            ps = connexion.prepareStatement(insert2);
            // Ausführung
            ps.executeUpdate();
            // Es wird dieselbe Zeile ein zweites Mal hinzugefügt, also mit demselben Primärschlüssel
            // Das Einfügen muss fehlschlagen, und aufgrund der Transaktion darf keines der beiden Elemente eingefügt werden
            ps.executeUpdate();
            // Transaktion festschreiben
            connexion.commit();
            // Rückkehr zum Standardmodus
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // die Ausnahme wird behandelt
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout", connexion, e1);
        } finally {
            // „finally“-Block wird verarbeitet
            doFinally(null, null, connexion);
        }
    }

6.4.12. Ergebnisse

Die Ausführung der Methode [main] liefert folgende Ergebnisse:

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"}
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"}
Les erreurs suivantes se sont produites lors de l'ajout : 
- Duplicate entry '100' for key 'PRIMARY'
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"}
Travail terminé

6.5. Verwendung einer Datenquelle vom Typ [DataSource]

Wir werden die vorherige Anwendung unter Verwendung einer Datenquelle vom Typ [javax.sql.DataSource] fortsetzen:

Image

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

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

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

6.5.1. Das Eclipse-Projekt

  

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


<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>istia.st.jdbc</groupId>
    <artifactId>intro-jdbc-02</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.34</version>
        </dependency>
        <!-- Bibliothek jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.5.1</version>
        </dependency>
        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.1.3.RELEASE</version>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
            <version>8.0.20</version>
        </dependency>
    </dependencies>
</project>
  • Zeilen 21–25: Abhängigkeit von Spring;
  • Zeilen 27–31: Abhängigkeit von der Bibliothek, die die Datenquelle bereitstellt;

Die Spring-Konfigurationsklasse [AppConfig] lautet wie folgt:


package istia.st.jdbc;

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

@Configuration
public class AppConfig {

    // Konstanten
    final static String URL = "jdbc:mysql://localhost:3306/dbIntroJdbc";
    final static String USER = "root";
    final static String PASSWD = "";
    final static String INSERT = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
    final static String DELETE = "DELETE FROM PRODUITS";
    final static String SELECT = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    final static String UPDATE = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
    final static String INSERT2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";
    final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
    
    @Bean
    public DataSource dataSource() {
        // Datenquelle TomcatJdbc
        DataSource dataSource = new DataSource();
        // Zugriffskonfiguration JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // eine ursprünglich geöffnete Verbindung
        dataSource.setInitialSize(1);
        // Ergebnis
        return dataSource;
    }
}
  • Zeilen 11–19: Die zuvor in [IntroJdbc01] definierten Konstanten wurden in [AppConfig] verschoben;
  • Zeilen 31–34: Der Spring-Bean, der die Datenquelle definiert;
  • Zeile 24: Erstellung der noch nicht konfigurierten Datenquelle;
  • Zeilen 26–29: Die Informationen, die es der Datenquelle ermöglichen, eine Verbindung zur Datenbank herzustellen;
  • Zeile 31: Erstellt einen Pool mit einer Verbindung. Mehr wird hier nicht benötigt. Es gibt nie mehrere gleichzeitige Verbindungen;

6.5.2. Die Hauptklasse

Die Hauptklasse [IntroJdbc02] sieht wie folgt aus:


package istia.st.jdbc;

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

import javax.sql.DataSource;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class IntroJdbc02 {
    // Datenquelle
    private static DataSource dataSource;

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

    // Produktliste
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // Verbindung herstellen
            connexion = dataSource.getConnection();
            // Transaktion starten
            connexion.setAutoCommit(false);
...
        } catch (SQLException e1) {
            // Ausnahme wird behandelt
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // „finally“-Block wird verarbeitet
            doFinally(null, null, connexion);
        }
    }
...
}
  • Zeile 14: die Datenquelle. Es ist zu beachten, dass es sich um den Typ [javax.sql.DataSource] handelt, bei dem es sich um eine Schnittstelle handelt;
  • Zeile 18: Instanziierung der Spring-Objekte;
  • Zeile 20: Abruf einer Referenz auf die Datenquelle. Es ist zu beachten, dass die tatsächlich verwendete Klasse zu keinem Zeitpunkt genannt wird. Hier lässt also nichts darauf schließen, dass eine Implementierung [TomcatJdbc] verwendet wird;
  • Zeile 36: Abruf einer offenen Verbindung;
  • Der restliche Code ist identisch mit dem der Klasse [IntroJdbc01];

6.6. Conclusion

Weitere Informationen zur Datenbankverwaltung finden Sie im Dokument [Exploiter une base relationnelle avec l'écosystème Spring].