Java und die JDBC
Willemers Informatik-Ecke
XML Java Persistence API (JPA)

Professionelle Programmierer dürften oft mit Datenbanken zu tun haben. Die Datenbankhersteller haben sich viel Mühe gegeben, ihre Datenbanken mit besonderen Schnittstellen zu versehen, die sich von denen anderer Hersteller unterscheiden. Sie locken damit, dass so mehr Möglichkeiten und mehr Performance zu erreichen seien. Allerdings wird es so eben auch schwieriger, die Datenbank zu wechseln.

Bei Java ist es ganz einfach. Es gibt eine klar definierte Schnittstelle namens JDBC. Der Datenbankhersteller ermöglicht entweder einen Zugang zu seiner Datenbank auf der Basis dieser Schnittstelle oder die Datenbank spielt nicht mit. So braucht der Software-Entwickler nicht ständig neue Schnittstellen zu lernen. Datenbanken werden plötzlich vergleichbar und austauschbar.

JDBC übermittelt SQL-Befehle als Zeichenketten. Im Falle von Rückgabewerten, wie sie beispielsweise bei Datenbankanfragen erwartet werden, müssen die Ergebnisse durch erneute Aufrufe von Methoden ausgewertet werden.

Der JDBC-Treiber

Die Datenbanken unterscheiden sich durch den JDBC-Treiber. Diese JAR-Datei muss vor der Verbindungsaufnahme geladen werden. Die folgende Zeile greift auf einen PostgreSQL-JDBC-Treiber zu. Ist dieser nicht greifbar, kann das Programm die Datenbank nicht ansprechen und kann darum seinen Dienst auch genausogut einstellen.
try { 
    Class.forName("org.postgresql.Driver");
} catch (ClassNotFoundException e) {
    System.out.println("JDBC-Treiber fehlt. Programmende.");
    System.exit(1);
}
Der String, der den Ort und Namen des JDBC-Treiber anspricht, ist bei jeder Datenbank ein klein wenig anders. Hier ein paar Beispiele:
"oracle.jdbc.driver.OracleDriver"
"org.hsqldb.jdbcDriver"
"com.mysql.jdbc.Driver"
"COM.ibm.db2.jdbc.app.DB2Driver"
Die Treiber finden Sie auf den Homepages der Datenbankhersteller oder sie werden mit den Datenbanken mit ausgeliefert. Beispielsweise wird der JDBC-Treibers von PostgreSQL auf der Website http://jdbc.postgresql.org/download.html angeboten. Bei manchen Herstellern befinden sich die Treiber in ZIP-Archiven, manchmal bereits in JAR-Dateien.

JDBC-Treiber in Eclipse einrichten

Das Java-Projekt, das den Treiber benötigt, mit der rechten Maustaste anklicken und den Punkt Properties (Eigenschaften) anklicken. In dem Dialog sind auf der linken Seite die Themen der Eigenschaften aufgeführt. Hier wählt man Java Build Path aus. Auf der rechten Seite ist der Tab Libraries (Bibliotheken) anzuwählen. Dort sieht man das JRE aufgeführt und klickt den Button Add External JARs. Es öffnet sich eine Dateiauswahlbox, mit der man die JAR-Datei des JDBC-Treibers auswählt. Sie erscheint daraufhin unter dem Projekt als weitere Bibliothek.

JDBC-Treiber an der Konsole einrichten

Soll der JDBC-Treiber von der Konsole aus eingebunden werden, wird der JDBC-Treiber in das gleiche Verzeichnis wie das übersetzte Programm JdbcPG gelegt und mit der Option -cp eingebunden. Am Beispiel des PostgreSQL-Treibers sieht der Aufruf so aus:
java -cp .:postgresql-9.1-901.jdbc4.jar JdbcPG
JdbcPG ist das Programm, das den JDBC-Treiber verwendet. Das bewirkt, dass der lokale Pfad als auch der Inhalt der JAR-Datei in die Ausführung eingebunden wird. Sehr aufschlussreich ist es, die JAR-Datei zu entpacken:
jar xf postgresql-9.1-901.jdbc4.jar 
Daraufhin findet sich im lokalen Verzeichnis die Datei: org/postgresql/Driver.class. Tatsächlich funktioniert mit dem entpackten Treiber auch der direkte Aufruf des Programms JdbcPG ohne die Option -cp.

Verbindung mit der Datenbank

Ist der JDBC-Treiber vorhanden und eingebunden, kann eine Verbindung zur Datenbank aufgebaut werden. Auf der Basis dieser Verbindung laufen alle Datenbankoperationen ab.
Connection dbConnect = DriverManager.getConnection(
    "jdbc:postgresql://debianserver/mydb",
    "myuser",
    "geheim");
Der Methode getConnection des DriverManagers werden die Daten zur Verbindung übergeben. Der erste String enthält die Daten über die Verbindungsart. Es ist eine JDBC-Verbindung. Die Datenbank ist PostgreSQL. Der Name des Hosts heißt debianserver und die Datenbank heißt mydb.

Bei MySQL würde statt postgresql in dem String mysql stehen, bei einer Oracle-Datenbank oracle. Der String kann sich je nach Datenbank unterscheiden. Es ist die Information an den JDBC-Treiber, damit er seine Datenbank findet. Wird die Datenbank nicht über das Netzwerk sondern lokal zugegriffen, kann der Name des Datenbankservers entfallen oder durch localhost ersetzt werden.

Der zweite String enthält den Datenbankbenutzer und der dritte dessen Passwort.

Datenbankoperationen

Das Objekt der Klasse Connection kann verwendet werden, um einen SQL-Befehl vorzubereiten, eine Transaktion abzuschließen oder zurüchzurollen oder um die Datenbankverbindung zu schließen.

SELECT

Betrachten wir eine kleine Beispielprogramm, das eine SELECT-Anweisung an die Tabelle artikel sendet und das Ergebnis auf dem Bildschirm anzeigt. Im Konstruktor wird der JDBC-Treiber angesprochen. In der Methode select wird dann die Verbindung hergestellt, die Anfrage übergeben und ausgeführt. Anschließend wird die Ergebnismenge Satz für Satz ausgelesen. Zu guter Letzt wird erst die Ergebnismenge und dann die Datenbank geschlossen.
import java.sql.*;

public class jdbcSelect {

    public jdbcSelect() {
         try { 
            Class.forName("org.postgresql.Driver");
        } catch (ClassNotFoundException e) {
            System.out.println("JDBC-Treiber fehlt. Programmende.");
            System.exit(1);
        }
    }

    public static void main(String args[])
    {
        jdbcSelect db = new jdbcSelect();
        db.select();
    }

    public void select() {
        try{
            Connection dbConnect = DriverManager.getConnection(
                 "jdbc:postgresql://debian/mydb",
                 "arnold",
                 "geheim");
            PreparedStatement sqlBefehl = dbConnect.prepareStatement("SELECT " +
                    "name, preis " +
                    "FROM artikel"
                    );
            ResultSet sqlErgebnis = sqlBefehl.executeQuery();
            while ( sqlErgebnis.next() ) {
                System.out.print("Name: ");
                System.out.print(sqlErgebnis.getString("name"));
                System.out.print("Preis: ");
                System.out.println(sqlErgebnis.getDouble("preis"));
            }
            sqlErgebnis.close(); // Ergebnismenge schliessen
            dbConnect.close();   // Datenbank schliessen
        } catch (SQLException e) {
            System.out.print(e.toString());
            System.out.print(" - SQL-Status: " + e.getSQLState());
            System.out.println(" - ErrorCode: " + e.getErrorCode());
        }
    }
}
Die Methode prepareStatement erhält einen String mit den SQL-Befehl. Der Rückgabewert ist ein Objekt der Klasse PreparedStatement. Dieses Objekt bietet seinerseits die Methode executeQuery an, die für die Ausführung der SQL-Anweisung führt und als Ergebnis einen ResultSet zurückgibt.

Der ResultSet verweist auf die Ergebnismenge der Anfrage. Sie steht vor dem ersten Datensatz und kann mit der Methode next um einen Satz weiterbewegt werden. Um einzelne Felder aus dem Satz zu ermitteln, bietet der ResultSet get-Methoden an. Als Parameter erhält die get-Methode den Namen der Spalte oder deren Nummer. Um das Feld als String auszulesen, ruft man getString auf, will man einen Fließkommawert haben, getDouble.

INSERT

Vergleichsweise einfach ist das Schreiben von Werten in die Datenbank. Der Befehlsstring für den SQL-Befehl INSERT wird so zusammengesetzt, dass die neuen Werte in der zweiten Klammer stehen. Will man dagegen mehrere Werte nacheinander einfügen, kann es sich lohnen, über die Fragezeichen als Platzhalter zu gehen. Dabei wird der Befehl einmal vorbereitet und bei jeder Ausführung mit unterschiedlichen Werten ausgeführt. Das folgende Beispiel zeigt eine Methode insert, die analog zur zuvor gezeigten Methode select genau dies tut:
public void insert() {
    try{
    	Connection dbConnect = DriverManager.getConnection(
             "jdbc:postgresql://debian/mydb",
             "arnold",
             "geheim");
        PreparedStatement sqlBefehl = dbConnect.prepareStatement(
        		"INSERT INTO artikel (name, preis) VALUES (?,?)");
        sqlBefehl.setString(1, "Kartoffelsalat");
        sqlBefehl.setDouble(2, 1.98);
        sqlBefehl.executeUpdate();
        sqlBefehl.setString(1, "Würstchen");
        sqlBefehl.setDouble(2, 2.98);
        sqlBefehl.executeUpdate();
        dbConnect.close();   // Datenbank schliessen
    } catch (SQLException e) {
    	System.out.print(e.toString());
    	System.out.print(" - SQL-Status: " + e.getSQLState());
    	System.out.println(" - ErrorCode: " + e.getErrorCode());
    }
}

Datum

Das Datum wird bei JDBC in der Klasse java.sql.Date definiert. Mit diesem Typ ist der Austausch mit einer Datenbanktabellenspalte vom Typ DATE problemlos möglich. Die meisten Java-Bibliotheken verwenden noch den Datumstyp java.util.Date. Der Unterschied zwischen beiden ist, dass die Util-Variante auch eine Uhrzeit speichern kann. Darum kann ein Sql-Date einem Util-Date direkt zugewiesen werden, anders herum aber nicht. Dazu wird ein Konstruktor des Sql-Dates verwendet, das als Parameter die Zeit in Millisekungen akzeptiert..
// Überführung von java.util.Date nach java.sql.Date
java.util.Date utilDatum = ...
PreparedStatement sqlBefehl = ...
sqlBefehl.setDate(3, new java.sql.Date(utilDatum.getTime()));
Umgekehrt lässt sich aufgrund der Vererbungshierarchie ein von der Datenbank ausgelesenes java.sql.Date direkt einem Objekt vom Typ java.util.Date zuweisen.
utilDatum = resultSet.getDate("von"));

Transaktionen

Eine Transaktion führt eine Datenbank von einem konsistenten Zustand in einen konsistenten Zustand über. Als Gegenbeispiel wird immer wieder gern die Buchung von Flugreisen gewählt. In zwei Reisebüros steht je ein Paar und möchte eine Reise buchen, bei der nur noch drei Plätze frei sind. Beide lesen die Zahl der freien Plätze. Beide Paare nicken. Es werden gleichzeitig 2 von den freien Plätzen abgezogen und damit beide Male eine 1 in die Anzahl der freien Platze geschrieben, während gleichzeitig alle vier Personen in die Liste der Gäste eingetragen werden.

Die Inkonsistenz liegt darin, dass die Anzahl der gebuchten Personen die Anzahl der Plätze übersteigt und sogar noch ein freier Platz ausgewiesen wird.

Um diese Überbuchung zu vermeiden, müsste das Lesen der Plätze, das Abziehen der gebuchten Plätze, das Schreiben der freien Platzzahl und das Speichern der Gäste zu einer ununterbrechbaren Einheit zusammengefasst werden, also zu einer Transaktion.

Eine Transaktion beginnt mit Aufsetzen des ersten Befehls und führt mehrere weitere Befehle durch. Sobald der nächste konsistente Zustand erreicht wird, gibt die Anwendung den Befehl COMMIT, die Anweisungen in einem Zug durchzuführen. Stellt die Anwendung dagegen fest, dass die Anweisungen nicht zu einem konsistenten Zustand führen, gibt sie die Anweisung ROLLBACK. Das bewirkt, dass der Zustand der Datenbank auf den Zustand zurückgesetzt wird, wie er vor der ersten Anweisung bestand.

Standardmäßig wird jede SQL-Anweisung als Transaktion angesehen und sofort durchgeführt. Die Datenbankverbindung hat eine Methode setAutoCommit(boolean), die also standardmäßig auf true steht. Wenn Sie also Transaktionen ausführen wollen, schalten Sie setAutoCommit auf false.

Nun setzen Sie solange Befehle ab, bis ein neuer konsistenter Zustand erreicht ist. Dann rufen Sie die Methode commit(). Andernfalls rufen Sie rollback().

HSQLDB

Die relationale Datenbank HSQLDB ist eine reine Java-Entwicklung unter FreeBSD-Lizenz.

Wie bei anderen Datenbanken muss der JDBC-Datenbankdatei als JAR-Datei beschafft und ins Projekt integriert werden. Im Unterschied zu anderen Datenbanken besteht sie aber nicht nur aus dem JDBC-Treiber sondern enthält die komplette Datenbank.

Das Paket kann in einer ZIP-Datei unter der URL http://hsqldb.org heruntergeladen werden. Im Verzeichnis lib findet sich die Datei hsqldb.jar. Sie wird als externe Bibliothek in das Projekt eingebunden. Rechter Mausklick auf das Projekt - Properties - Java Build Path - Libraries - Add External JARs und die Datei auswählen.

Datenbank verwalten

In dem JAR-Paket ist auch ein Utility namens DatabaseManagerSwing enthalten. Es befindet sich im Verzeichnis util. Dieses kann entweder direkt aufgerufen werden, indem das JAR-Paket entpackt wird oder in Eclipse eingebunden werden. Hier die Befehle aus einer Linux-Umgebung:
cp ~/Downloads/hsqldb.jar . 
jar xf hsqldb.jar 
java org.hsqldb.util.DatabaseManagerSwing 
Unter Eclipse wird die JAR-Datei sowieso als externe Library installiert. Dann kann man eine neue Run-Konfigration erstellen, über die man das Programm aufrufen kann.

Dazu ruft man über das Menü Run - Run Configurations. Das Projekt in der linken Spalte mit der rechten Maustaste anklicken. Dort wird New... angeklickt. Es entsteht ein zweites Projekt mit gleichem Namen und einer (1) dahinter. Dieses wird angewählt. Auf der rechten Seite steht unter dem Tab Main das Eingabefeld Main Class. Über den Search-Button erreicht man einen Dialog, in dem DatabaseManagerSwing ausgewählt wird. Apply und Run. Und schon erscheint das Fenster des Database-Managers. Im rechten oberen Feld können SQL-Befehle abgesetzt werden. Das Erzeugen einer Testtabelle erfolgt mit dem Befehl:

create table artikel ( name varchar(255), preis dec(10,3) );

Programmierung

Die Datenbank kann im Server-Modus betrieben werden und wird dann prinziell behandelt wie jede andere Datenbank auch. Im Server-Modus wird die Verbindung folgendermaßen hergestellt:
try {
    Class.forName("org.hsqldb.jdbcDriver" );
} catch (Exception e) {
    System.out.println("Fehler im JDBC-Treiber.");
}
...
Connection c = DriverManager.getConnection("jdbc:hsqldb:hsql://debianserver/mydb", "arnold", "geheim");
Das Besondere an dieser Datenbank ist allerdings der Stand-Alone-Betrieb. In diesem Fall ist das Datenbanksystem Bestandteil der Anwendung und sie verwaltet die Daten selbst in mehreren Dateien mit gleichem Namen, aber unterschiedlichen Extensionen. Über diesen Dateinamen -- gegebenenfalls mit Pfadangabe -- kann die Datenbank lokal angesprochen werden.
Connection c = DriverManager.getConnection("jdbc:hsqldb:file:/home/arnold/mydb", "arnold", "geheim");
Laufwerksbuchstaben unter Windows werden allerdings nicht unterstützt. Bei relativen Pfaden wird die Datenbank auf dem gleichen Laufwerk vermutet, bei absoluten Pfadnamen wird vom Laufwerk C: ausgegangen.

Leider vergisst die Datenbank die Daten wieder, wenn sie nicht korrekt heruntergefahren wird. Darum muss im Falle des Stand-Alone-Betriebs der Befehl SHUTDOWN vor dem Ende des Programms gegeben werden.

dbConnect.createStatement().execute("SHUTDOWN");