Skip to content

6. [Kurs]: Einführung in die JDBC-API

Stichworte: relationale Datenbanken, JDBC-API, SQLException.

6.1. Support

Der Ordner [support / chap-06] enthält die Eclipse-Projekte für dieses Kapitel.

6.2. Architektur

Die JDBC-Schicht (Java Database Connectivity) ist eine universelle Schnittstelle für den Datenbankzugriff. Sie stellt der [DAO]-Schicht stets dieselbe Schnittstelle zur Verfügung. Wenn Sie das DBMS wechseln, müssen Sie lediglich den JDBC-Treiber austauschen. Die [DAO]-Schicht bleibt unverändert.

6.3. Schritte zur Bedienung einer Datenbank

In der oben dargestellten Architektur umfasst die Bedienung einer Datenbank über das Konsolenprogramm die folgenden Schritte:

  1. Laden des JDBC-Treibers der Datenbank;
  2. Öffnen einer Verbindung zur Datenbank;
  3. Ausführen einer SQL-Anweisung in der Datenbank und Verarbeiten der Ergebnisse der SQL-Anweisung;
  4. 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.

6.3.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.

6.3.2. Schritt 2 – Eine Verbindung herstellen

Sobald der JDBC-Treiber eingerichtet 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 benötigt:
    • [url]: die Datenbank-URL. Dies ist eine Zeichenkette, die von der verwendeten Datenbank abhängt. Bei 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.

6.3.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 ist dies nicht der Fall. 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 formalen Parameter ? 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:

    boolean next()

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 Tabelle [ResultSet] 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

Angenommen, die Tabelle [PRODUCTS] verfügt über einen Fremdschlüssel zur Tabelle [CATEGORIES], dargestellt durch die Beziehung [PRODUCTS].CATEGORY_ID --> [CATEGORIES].ID, und sowohl die Tabelle [PRODUCTS] als auch die Tabelle [CATEGORIES] verfügen über ein Feld [NAME]. In diesem Fall hängen die Namen, die im [ResultSet] den Spalten [PRODUCTS.NAME] und [CATEGORIES.NAME] zugewiesen werden, vom DBMS ab. Um die Portabilität zwischen verschiedenen DBMS zu gewährleisten, müssen hier daher Spaltenbezeichnungen verwendet werden, und wir 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:

Type getType("labelColi") 

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:

Type getType(i) 

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: Schließen des [ResultSet];
  • 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;

6.3.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 sind 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;

6.3.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.

6.4. Ein Beispielprojekt

6.4.1. Support

Der Ordner [support / chap5] enthält die Eclipse-Projekte für dieses Kapitel [1, 2]. Der Ordner [database] enthält das SQL-Skript zum Anlegen der Beispiel-MySQL-Datenbank für dieses Kapitel [1, 3].

6.4.2. Die verwendete Datenbank

Die folgenden Beispiele verwenden die folgende MySQL-Datenbank:

 
  • [ID]: Primärschlüssel im AUTO_INCREMENT-Modus (wenn kein Primärschlüssel angegeben ist, generiert das DBMS ihn);
  • [NAME]: Produktname – eindeutig;
  • [CATEGORY]: Kategorienummer;
  • [PRICE]: Preis;
  • [DESCRIPTION]: eine Beschreibung des Produkts;

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

6.4.3. Das Eclipse-Projekt

  

Das Projekt ist ein Maven-Projekt, das durch die folgende [pom.xml]-Datei 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 JDBC-Treiber für das DBMS MySQL5;
  • Zeilen 13–17: eine Bibliothek zur Verarbeitung von JSON (JavaScript Object Notation) (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 [Product] sieht wie folgt aus:


package istia.st.jdbc;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
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
    ...
 
    // to String
    public String toString() {
        try {
            return new ObjectMapper().writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
}
  • Zeile 34: Wir verwenden die JSON-Bibliothek, um die JSON-Zeichenkette des Produkts anzuzeigen. Dies erzeugt eine Ausgabe, 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 [toString]-Methode besteht darin, dass sie auch dann gültig bleibt, wenn Felder zur Klasse hinzugefügt oder aus ihr entfernt werden. Wenn es sich bei den Feldern selbst um Objekte handelt (Listen, Arrays, Wörterbücher, Benutzerobjekte), können JSON-Bibliotheken diese wiederum in JSON-Zeichenfolgen umwandeln;

6.4.5. Die Klasse [Static]

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


package istia.st.jdbc;
 
import java.util.ArrayList;
import java.util.List;
 
public class Static {
 
    public 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;
    }
 
    public 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 8–19: Gibt eine Liste von Fehlern zurück, die in einem Objekt vom Typ [Throwable] gekapselt sind, der die Oberklasse der Klasse [Exception] darstellt;
  • Zeilen 21–28: Zeigt eine Liste von Meldungen auf dem Bildschirm an;

Dieser Code könnte in der Hauptklasse stehen, da er hier nur dort verwendet wird. Wir betrachten jedoch ein umfassenderes Szenario, in dem andere Klassen diesen Code möglicherweise benötigen.

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 {
 
    // constants
    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) {
        // loading the JDBC driver from MySQL
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // empty table [PRODUITS]
        delete();
        // fill it
        insert();
        // we read it
        select();
        // update
        update();
        // display
        select();
        // insertion of two identical elements
        // insertion must fail and neither element is inserted because of the transaction
        insert2();
        // we check
        select();
        // finish
        System.out.println("Travail terminé");
    }
 
    // product list
    private static void select() {
...
    }
 
    // product deletion
    public static void delete() {
...
    }
 
    // add products
    public static void insert() {
...
    }
 
    // add 2 products
    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) {
 
            }
        }
        // close connection
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // display error msg
                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) {
        // 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(title, Static.getErreursFromThrowable(e2));
        }
    }
}

6.4.7. Löschen des Inhalts der Produkttabelle

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(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // empty table [PRODUITS]
            ps = connexion.prepareStatement(delete);
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } 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, null, connexion);
        }
    }

In diesem Beispiel werden Transaktionen verwendet. 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:

  • Starten einer Transaktion: [connection.setAutoCommit(false)];
  • Erfolgreiches Beenden einer Transaktion: [connection.commit()]. In diesem Fall werden alle während der Transaktion an der Datenbank durchgeführten Operationen festgeschrieben;
  • Beenden einer Transaktion mit Fehler: [connection.rollback()]. In diesem Fall werden alle während der Transaktion an der Datenbank durchgeführten Operationen zurückgesetzt;
  • Zurück zum [Auto-Commit]-Modus, dem Standardmodus für die JDBC-API: [connection.setAutoCommit(true)]. In diesem Modus ist jede SQL-Anweisung Teil einer Transaktion. Wenn Sie also zwei Einfügungen durchführen und die zweite fehlschlägt:
    • im Modus [AutoCommit=true] bleibt der erste Einfügevorgang erhalten (er wurde durch den ersten AutoCommit bestätigt);
    • im Modus [AutoCommit=false] wird der erste Einfügevorgang rückgängig gemacht;

In unseren Beispielen rollen wir die Transaktion in der Methode [doCatchException] zurück, sobald eine Ausnahme auftritt:


    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));
        }
}

6.4.8. Erstellen des Inhalts der Produkttabelle

Die Methode [insert] erstellt den Tabelleninhalt:


// add products
    public static void insert() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // fill the table
            ps = connexion.prepareStatement(insert);
            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();
            // return to default mode
            connexion.setAutoCommit(true);
        } 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, null, connexion);
        }
    }

6.4.9. 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(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // table [PRODUITS] is read
            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)));
            }
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } 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(null, null, connexion);
        }
}

6.4.10. Aktualisierung 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(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // table is updated
            ps = connexion.prepareStatement(update);
            // category 1
            ps.setInt(1, 1);
            // execution
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } 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, 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 zulässig ist. Da wir uns in einer Transaktion befinden, wird der erste Einfügevorgang rückgängig gemacht.


// add 2 pro ducts with the same primary keys
    public static void insert2() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // add 1 line
            ps = connexion.prepareStatement(insert2);
            // execution
            ps.executeUpdate();
            // we add the same line a 2nd time, with the same primary key
            // the insertion must fail and neither of the two elements must be inserted because of the transaction
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(null, null, connexion);
        }
    }

6.4.12. Ergebnisse

Die Ergebnisse der Ausführung der Methode [main] lauten wie folgt:

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 [DataSource]-Datenquelle

Wir werden die vorherige Anwendung unter Verwendung einer [javax.sql.DataSource]-Datenquelle erneut betrachten:

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

6.5.1. Das Eclipse-Projekt

  

Das Projekt ist ein Maven-Projekt, das durch die folgende [pom.xml]-Datei 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>
        <!-- library 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] sieht wie folgt aus:


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 {
 
    // constantes
    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() {
        // source de données TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration accès JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // une connexion ouverte initialement
        dataSource.setInitialSize(1);
        // résultat
        return dataSource;
    }
}
  • Zeilen 11–19: Die zuvor in [IntroJdbc01] definierten Konstanten wurden nach [AppConfig] verschoben;
  • Zeilen 31–34: die Spring-Bean, die die Datenquelle definiert;
  • Zeile 24: Erstellung der Datenquelle, die noch nicht konfiguriert ist;
  • Zeilen 26–29: die Informationen, die es der Datenquelle ermöglichen, eine Verbindung zur Datenbank herzustellen;
  • Zeile 31: Erstellt einen Pool mit 1 Verbindung. Mehr brauchen wir hier nicht. Es gibt niemals 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 {
    // data source
    private static DataSource dataSource;
 
    public static void main(String[] args) {
        // spring context retrieval
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        // data source recovery
        dataSource = ctx.getBean(DataSource.class);
        // empty table [PRODUITS]
        delete();
...
        // finish
        ctx.close();
        System.out.println("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);
...
        } 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(null, null, connexion);
        }
    }
...
}
  • Zeile 14: die Datenquelle. Beachten Sie, dass es sich um den Typ [javax.sql.DataSource] handelt, der eine Schnittstelle ist;
  • Zeile 18: Instanziierung von Spring-Objekten;
  • Zeile 20: 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 36: Abrufen einer offenen Verbindung;
  • Der Rest des Codes ist identisch mit dem der Klasse [IntroJdbc01];

6.6. Fazit

Weitere Informationen zur Datenbankverwaltung finden Sie im Dokument [Arbeiten mit einer relationalen Datenbank unter Verwendung des Spring-Ökosystems].