Wichtig - Code SCHREIBEN wird erwartet

In der Klausur muesst ihr JDBC-Code nicht nur lesen und kommentieren, sondern auch selbst SCHREIBEN und AENDERN. Bonusaufgaben: Syntaxfehler korrigieren, Java-Programme umschreiben. Ueber die Muster unten sollen tippfertig sitzen.

Zusammenfassung

JDBC (Java Database Connectivity) ist die Java-API fuer den standardisierten Zugriff auf relationale Datenbanken via DBMS-spezifische Treiber (typisch Typ 4, pure Java). Kernelemente sind DriverManager, Connection, PreparedStatement und ResultSet - kombiniert mit expliziter Transaktionssteuerung ueber setAutoCommit/commit/rollback und automatischer Ressourcenfreigabe per try-with-resources. Der wichtigste Praxis-Reflex: immer PreparedStatement mit ?-Platzhaltern statt Statement mit String-Konkatenation verwenden, um SQL Injection zu verhindern.

Kernkonzepte

Class.forName und DriverManager

Class.forName(org.postgresql.Driver) laedt die Treiberklasse und registriert sie beim DriverManager. Danach liefert DriverManager.getConnection(url, user, pw) eine Connection. Die URL hat die Form jdbc:<dbms>://host:port/db. Seit JDBC 4 werden Treiber via ServiceLoader oft automatisch geladen - Class.forName wird aber in Lehrbeispielen weiter gezeigt.

Treiber laden, dann Verbindung holen - erst Class.forName, dann DriverManager.getConnection.

Connection

Repraesentiert die aktive Verbindung zur Datenbank. Ueber die Connection werden Statements erstellt und Transaktionen gesteuert (setAutoCommit, commit, rollback). Nach Gebrauch muss sie geschlossen werden - am besten via try-with-resources. Eine Connection kapselt einen Netzwerk-Socket zum DBMS.

Eine Verbindung, viele Statements - Connection ist teuer, Statements sind billig.

Statement vs PreparedStatement vs CallableStatement

Statement fuer statische SQL-Strings ohne Parameter (SQL Injection Gefahr bei Konkatenation, keine Wiederverwendung). PreparedStatement fuer parametrisierte Abfragen mit Platzhaltern ?, vorkompiliert, sicher gegen SQL Injection, ideal bei wiederholten Aufrufen. CallableStatement fuer Stored Procedure Calls mit Syntax {CALL name(?)}.

PreparedStatement fast immer, Statement fast nie - Injection-sicher und performant.

ResultSet

Datenkontainer / Cursor fuer SELECT-Ergebnisse. Startet VOR der ersten Zeile. rs.next() bewegt Cursor eine Zeile weiter und liefert boolean. Zugriff auf Werte via getInt(spalte), getString(spalte) - entweder per Spaltenname oder 1-basiertem Index. Standardmaessig Forward-Only und Read-Only.

Erst next, dann get - der Cursor steht anfangs vor der ersten Zeile.

Transaktionen mit setAutoCommit, commit, rollback

Standard: autoCommit=true (jedes Statement eigene Transaktion). Fuer atomare Multi-Statement-Operationen setAutoCommit(false) aufrufen. Bei Erfolg conn.commit(), bei Fehler conn.rollback() im catch-Block. Am Ende autoCommit wieder auf true zuruecksetzen. Ohne diese Steuerung sind zusammengehoerige Aenderungen NICHT atomar.

autoCommit aus, dann commit oder rollback - alles oder nichts.

Exception Handling mit SQLException

Fast alle JDBC-Methoden werfen SQLException (checked exception). Muss per try-catch behandelt oder deklariert werden. SQLException bietet getMessage, getSQLState (SQL-Standard-Codes) und getErrorCode (DBMS-spezifisch). Bei Transaktionen im catch-Block immer rollback aufrufen.

Jede JDBC-Zeile kann werfen - im catch immer an rollback denken.

Ressourcen schliessen mit try-with-resources

Connection, Statement, PreparedStatement und ResultSet implementieren AutoCloseable. try (Resource r = ...) ruft close automatisch auf - auch bei Exception, in umgekehrter Deklarationsreihenfolge. Loest das haeufige Problem vergessener close-Aufrufe (Ressourcen-Leak, Sperren, Deadlocks).

try-with-resources statt manuellem close in finally - Java 7 aufwaerts Standard.

PreparedStatement Parameter binden

Nach conn.prepareStatement(sql mit ?) werden Parameter mit setInt(index, wert), setString(index, wert), setDate, setBigDecimal etc. gebunden. Index ist 1-basiert (nicht 0-basiert). Erst danach executeQuery oder executeUpdate aufrufen. Reihenfolge der ? bestimmt den Index.

Index 1-basiert, Typ passend - setInt fuer INTEGER, setString fuer VARCHAR.

Wichtige Details

Code-Muster - Diese SOLLEN sitzen

Jedes Muster einmal mitschreiben. Der Kopieren-Button hilft beim Ueben in einer IDE.

1. Verbindung aufbauen und schliessen mit try-with-resources

Der absolute Standard-Einstieg jeder JDBC-Anwendung. Treiber wird geladen, Connection wird geoeffnet und garantiert wieder geschlossen - auch bei Exception. Das try-with-resources ruft close() automatisch auf, deshalb kein finally noetig.

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

public class DbConnect {
    public static void main(String[] args) {
        String url = "jdbc:postgresql://localhost:5432/uni";
        String user = "scott";
        String pw = "tiger";
        try {
            Class.forName("org.postgresql.Driver");
        } catch (ClassNotFoundException e) {
            System.err.println("Treiber nicht gefunden: " + e.getMessage());
            return;
        }
        try (Connection conn = DriverManager.getConnection(url, user, pw)) {
            System.out.println("Verbindung offen: " + !conn.isClosed());
        } catch (SQLException e) {
            System.err.println("Fehler: " + e.getMessage());
        }
    }
}
Kern-Zeilen zum Auswendiglernen:
  • Class.forName(org.postgresql.Driver)
  • DriverManager.getConnection(url, user, pw)
  • try (Connection conn = ...) - schliesst automatisch
  • catch (SQLException e)

2. SELECT mit PreparedStatement und ResultSet-Schleife

Ausfuehren einer Anfrage mit Benutzereingabe. Der Fragezeichen-Platzhalter und setString/setInt verhindern SQL Injection. Das ResultSet wird mit rs.next() Zeile fuer Zeile abgearbeitet.

String sql = "SELECT matrnr, name, semester FROM studenten WHERE semester >= ?";
try (Connection conn = DriverManager.getConnection(url, user, pw);
     PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setInt(1, 3);
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            int matrnr = rs.getInt("matrnr");
            String name = rs.getString("name");
            int sem = rs.getInt("semester");
            System.out.println(matrnr + " " + name + " " + sem);
        }
    }
} catch (SQLException e) {
    e.printStackTrace();
}
Kern-Zeilen zum Auswendiglernen:
  • conn.prepareStatement(sql) mit Platzhaltern
  • ps.setInt(1, wert) - Parameter binden, 1-basiert
  • ResultSet rs = ps.executeQuery()
  • while (rs.next()) { rs.getString(spalte); }

3. INSERT / UPDATE / DELETE mit PreparedStatement

Datenaenderungen werden mit executeUpdate ausgefuehrt (nicht executeQuery). Der Rueckgabewert ist die Anzahl betroffener Zeilen als int. Auch hier PreparedStatement mit setXxx-Methoden verwenden.

String insertSql = "INSERT INTO studenten (matrnr, name, semester) VALUES (?, ?, ?)";
try (Connection conn = DriverManager.getConnection(url, user, pw);
     PreparedStatement ps = conn.prepareStatement(insertSql)) {
    ps.setInt(1, 29999);
    ps.setString(2, "Meier");
    ps.setInt(3, 1);
    int rows = ps.executeUpdate();
    System.out.println(rows + " Zeile(n) eingefuegt");
} catch (SQLException e) {
    System.err.println("Insert fehlgeschlagen: " + e.getMessage());
}

// UPDATE-Muster analog:
String updSql = "UPDATE studenten SET semester = ? WHERE matrnr = ?";
try (Connection conn = DriverManager.getConnection(url, user, pw);
     PreparedStatement ps = conn.prepareStatement(updSql)) {
    ps.setInt(1, 4);
    ps.setInt(2, 29999);
    int updated = ps.executeUpdate();
}

// DELETE analog:
String delSql = "DELETE FROM studenten WHERE matrnr = ?";
try (PreparedStatement ps = conn.prepareStatement(delSql)) {
    ps.setInt(1, 29999);
    ps.executeUpdate();
}
Kern-Zeilen zum Auswendiglernen:
  • executeUpdate() statt executeQuery() bei INSERT/UPDATE/DELETE
  • Rueckgabewert int = Anzahl betroffener Zeilen
  • ps.setInt / ps.setString mit 1-basiertem Index
  • PreparedStatement wiederverwendbar durch setXxx neu setzen

4. Transaktion mit setAutoCommit, commit und rollback

Zwei zusammengehoerige Aenderungen (klassisch: Ueberweisung zwischen zwei Konten) muessen atomar erfolgen. Ohne setAutoCommit(false) wird jedes Statement einzeln committet - das darf hier nicht passieren.

String abbuchen = "UPDATE konto SET saldo = saldo - ? WHERE id = ?";
String gutschreiben = "UPDATE konto SET saldo = saldo + ? WHERE id = ?";
Connection conn = null;
try {
    conn = DriverManager.getConnection(url, user, pw);
    conn.setAutoCommit(false);
    try (PreparedStatement ps1 = conn.prepareStatement(abbuchen);
         PreparedStatement ps2 = conn.prepareStatement(gutschreiben)) {
        ps1.setBigDecimal(1, new BigDecimal("100.00"));
        ps1.setInt(2, 1001);
        ps1.executeUpdate();
        ps2.setBigDecimal(1, new BigDecimal("100.00"));
        ps2.setInt(2, 1002);
        ps2.executeUpdate();
        conn.commit();
    }
} catch (SQLException e) {
    if (conn != null) {
        try { conn.rollback(); } catch (SQLException ex) { /* log */ }
    }
    throw new RuntimeException(e);
} finally {
    if (conn != null) {
        try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { /* log */ }
    }
}
Kern-Zeilen zum Auswendiglernen:
  • conn.setAutoCommit(false) - Transaktion beginnt
  • conn.commit() - am Ende des try-Blocks nach allen Statements
  • conn.rollback() - im catch bei jedem Fehler
  • finally: setAutoCommit(true) und close()

5. Batch-Update fuer viele Datensaetze

1000 einzelne INSERTs = 1000 Netzwerk-Roundtrips = langsam. Batch fasst mehrere Statements zusammen und sendet sie als Block. Idealerweise kombiniert mit einer Transaktion.

String sql = "INSERT INTO logeintraege (zeitstempel, nachricht) VALUES (?, ?)";
try (Connection conn = DriverManager.getConnection(url, user, pw)) {
    conn.setAutoCommit(false);
    try (PreparedStatement ps = conn.prepareStatement(sql)) {
        for (int i = 0; i < 1000; i++) {
            ps.setTimestamp(1, new Timestamp(System.currentTimeMillis()));
            ps.setString(2, "Nachricht Nr " + i);
            ps.addBatch();
            if (i % 100 == 0) {
                ps.executeBatch();
            }
        }
        int[] ergebnisse = ps.executeBatch();
        conn.commit();
        System.out.println("Insgesamt " + ergebnisse.length + " Statements");
    } catch (SQLException e) {
        conn.rollback();
        throw e;
    }
}
Kern-Zeilen zum Auswendiglernen:
  • ps.addBatch() - Statement zur Batch hinzufuegen
  • ps.executeBatch() - alle gesammelten Statements ausfuehren
  • conn.setAutoCommit(false) - Batch in einer Transaktion
  • Rueckgabe int[] = Anzahl betroffener Zeilen pro Statement

6. Fehlerhaftes Snippet erkennen und korrigieren

Klassische Klausur-Aufgabe. Das erste Snippet enthaelt mehrere Anti-Patterns: Statement (kein PreparedStatement) mit String-Konkatenation (SQL Injection), kein close (Ressourcen-Leak), autoCommit nicht gesetzt bei zwei zusammengehoerigen Statements, kein Rollback im Fehlerfall.

// FEHLERHAFT:
Connection conn = DriverManager.getConnection(url, user, pw);
Statement stmt = conn.createStatement();
String userInput = request.getParameter("name");
ResultSet rs = stmt.executeQuery("SELECT * FROM users WHERE name = '" + userInput + "'");
stmt.executeUpdate("UPDATE konto SET saldo = saldo - 100 WHERE id = 1");
stmt.executeUpdate("UPDATE konto SET saldo = saldo + 100 WHERE id = 2");
// KORRIGIERT:
String sel = "SELECT * FROM users WHERE name = ?";
String upd1 = "UPDATE konto SET saldo = saldo - ? WHERE id = ?";
String upd2 = "UPDATE konto SET saldo = saldo + ? WHERE id = ?";
try (Connection conn = DriverManager.getConnection(url, user, pw)) {
    conn.setAutoCommit(false);
    try (PreparedStatement ps = conn.prepareStatement(sel)) {
        ps.setString(1, userInput);
        try (ResultSet rs = ps.executeQuery()) {
            while (rs.next()) { /* ... */ }
        }
    }
    try (PreparedStatement p1 = conn.prepareStatement(upd1);
         PreparedStatement p2 = conn.prepareStatement(upd2)) {
        p1.setInt(1, 100); p1.setInt(2, 1); p1.executeUpdate();
        p2.setInt(1, 100); p2.setInt(2, 2); p2.executeUpdate();
        conn.commit();
    } catch (SQLException e) {
        conn.rollback();
        throw e;
    }
}
Kern-Zeilen zum Auswendiglernen:
  • Statement + String-Konkatenation ist SQL Injection anfaellig
  • PreparedStatement mit ? und setXxx ist sicher
  • Fehlendes close() = Ressourcen-Leak - try-with-resources loest das
  • Zwei zusammengehoerige Updates brauchen setAutoCommit(false) + commit/rollback

7. Stored Procedure per CallableStatement aufrufen

Zum Aufruf einer gespeicherten Prozedur in der Datenbank nutzt man CallableStatement mit prepareCall und der Syntax {CALL name(?, ?)}. Parameter werden wie bei PreparedStatement gebunden.

String call = "{CALL upd_customer_order(?)}";
try (Connection conn = DriverManager.getConnection(url, user, pw);
     CallableStatement cstmt = conn.prepareCall(call)) {
    cstmt.setInt(1, 4711);
    try (ResultSet rs = cstmt.executeQuery()) {
        while (rs.next()) {
            System.out.println(rs.getString(1));
        }
    }
} catch (SQLException e) {
    e.printStackTrace();
}
Kern-Zeilen zum Auswendiglernen:
  • CallableStatement statt PreparedStatement
  • conn.prepareCall({CALL name(?)}) mit geschweiften Klammern
  • setInt / setString wie sonst auch
  • executeQuery() bzw executeUpdate() je nach Prozedur

Beispiele

Vollstaendiger JDBC-Ablauf fuer SELECT

Klassisches 5-Schritt-Muster: Treiber laden, Verbindung aufbauen, PreparedStatement erstellen mit Platzhaltern, Parameter binden, executeQuery aufrufen, ResultSet in while-Schleife durchlaufen und mit rs.next() Zeile fuer Zeile abarbeiten, Ressourcen im richtigen umgekehrten Reihenfolge schliessen (rs, ps, conn) - am besten via try-with-resources damit dies automatisch geschieht.

1. Class.forName(org.postgresql.Driver) - Treiber registrieren
2. DriverManager.getConnection(url, user, pw) - Verbindung
3. conn.prepareStatement(SELECT ... WHERE x = ?) - PreparedStatement
4. ps.setInt(1, wert) - Parameter binden
5. ResultSet rs = ps.executeQuery() - Ausfuehren
6. while (rs.next()) { rs.getString(name); rs.getInt(alter); } - Iterieren
7. Ressourcen schliessen (automatisch mit try-with-resources)

Banktransaktion mit Rollback

Klassische Uni-Aufgabe: Betrag von Konto A abbuchen und auf Konto B gutschreiben. Beide UPDATEs muessen zusammen erfolgreich sein oder gar nicht. autoCommit muss auf false gesetzt werden, damit die zwei Statements in einer Transaktion gebuendelt werden. Bei Exception: rollback im catch-Block. Bei Erfolg: commit am Ende des try-Blocks. Im finally: autoCommit wieder auf true setzen und Connection schliessen.

conn.setAutoCommit(false)
UPDATE konto SET saldo = saldo - 100 WHERE id = A
UPDATE konto SET saldo = saldo + 100 WHERE id = B
conn.commit()
catch (SQLException e) { conn.rollback(); }

Fehleranalyse eines schlechten Snippets

Ein typisches Klausur-Beispiel: Statement (kein PreparedStatement) mit String-Konkatenation, kein close, kein try-catch, autoCommit nicht gesetzt. Studenten muessen die drei bis vier Fehler benennen (SQL Injection Risiko, Ressourcen-Leak, keine Transaktion, kein Exception-Handling) und den Code korrigieren.

Fehlerhafter Code: stmt.executeQuery(SELECT * FROM users WHERE name = + userInput)
Korrekt: PreparedStatement mit ? und ps.setString(1, userInput) in try-with-resources

Batch-Update fuer 1000 Datensaetze

Grosse Datenmengen mit einzelnen executeUpdate-Aufrufen verursachen viele Roundtrips. Loesung: PreparedStatement einmal vorbereiten, in Schleife addBatch aufrufen und am Ende executeBatch. Zusaetzlich autoCommit=false verwenden damit alle 1000 Inserts in einer Transaktion laufen. Am Ende commit.

JDBC-Ablauf im Ueberblick

1. Treiber Class.forName DriverManager 2. Connection getConnection (url, user, pw) 3. Statement PreparedStatement setInt / setString 4. ResultSet executeQuery rs.next / getInt 5. close try-with- resources JDBC Ablauf - 5 Schritte Bei Transaktionen: nach Connection setAutoCommit(false), am Ende commit oder rollback

Haeufige Fragen

Warum PreparedStatement statt Statement?
Drei Gruende: 1) Schutz vor SQL Injection durch getrennte Uebergabe von SQL-Struktur und Parametern, 2) Performance durch Vorkompilierung und Wiederverwendung des Query-Plans bei wiederholtem Aufruf, 3) Typsicherheit durch setInt, setString, setDate statt String-Konkatenation.
Was macht Class.forName und ist es noch noetig?
Class.forName laedt die Treiber-Klasse und registriert sie beim DriverManager. Seit JDBC 4 (Java 6) ist der Aufruf oft nicht mehr noetig, da moderne Treiber via ServiceLoader automatisch geladen werden. In Klausuraufgaben wird die explizite Registrierung aber meist noch gezeigt und gefordert.
Wie unterbindet man SQL Injection zuverlaessig?
Immer PreparedStatement mit Platzhaltern ? verwenden und Parameter via setInt, setString etc. binden. NIE Benutzereingaben per String-Konkatenation in den SQL-String einfuegen. Alternativ Stored Procedures oder ein Persistenz-Framework (JPA/Hibernate) verwenden.
Was passiert wenn man autoCommit=true laesst und mitten in einer Transaktion einen Fehler bekommt?
Bei autoCommit=true wird jedes einzelne Statement sofort committet. Das erste UPDATE (Abbuchung) waere also schon persistiert wenn das zweite (Gutschrift) fehlschlaegt - die Datenbank ist inkonsistent. Deshalb setAutoCommit(false) verwenden und explizit commit oder rollback aufrufen.
Warum try-with-resources statt manuellem close in finally?
try-with-resources ruft close() automatisch in umgekehrter Reihenfolge der Deklaration auf - auch bei Exceptions. Das eliminiert Boilerplate-Code, verhindert vergessene close-Aufrufe (Ressourcen-Leak) und ist die empfohlene Java-Praxis seit Java 7. Alle JDBC-Objekte (Connection, Statement, ResultSet) implementieren AutoCloseable.
Was ist der Unterschied zwischen executeQuery, executeUpdate und execute?
executeQuery: fuer SELECT, gibt ResultSet zurueck. executeUpdate: fuer INSERT/UPDATE/DELETE/DDL, gibt int (Anzahl betroffener Zeilen) zurueck. execute: universell, gibt boolean zurueck (true wenn ResultSet vorhanden), danach getResultSet oder getUpdateCount aufrufen. Fuer Klausur: executeQuery nur bei SELECT.
Was macht ResultSet.next() genau?
next() bewegt den Cursor eine Zeile weiter und gibt true zurueck wenn eine weitere Zeile existiert, false sonst. Vor dem ersten Aufruf steht der Cursor VOR der ersten Zeile - man muss also immer erst next() aufrufen bevor man mit getInt/getString zugreift. Deshalb das Muster while (rs.next()) { ... }.
Was ist der Unterschied zwischen JDBC Typ 2 und Typ 4 Treiber?
Typ 2 (Native API): JDBC ruft eine DBMS-spezifische native C-Library auf, die clientseitig installiert sein muss. Typ 4 (Java-nativ, pure Java): Treiber ist komplett in Java geschrieben und kommuniziert direkt ueber das Netzwerkprotokoll der Datenbank. Typ 4 ist heute Standard (portabel, keine native Installation noetig).

Pruefungsfragen

Fortschritt
0 / 14
Frage 1 kurzantwort
Welcher JDBC-Aufruf laedt die Treiberklasse fuer PostgreSQL?
Frage 2 multiple-choice
Welches Interface schuetzt vor SQL Injection?
Frage 3 wahr-falsch
Wahr oder falsch: rs.next() muss vor dem ersten Zugriff auf ResultSet-Werte aufgerufen werden.
Frage 4 code-schreiben
Ergaenze das PreparedStatement: Schreibe Java-Code der alle Studenten mit Semester groesser als einem Parameter selectiert und ausgibt.
Frage 5 kurzantwort
Was ist der Unterschied zwischen executeQuery und executeUpdate?
Frage 6 code-schreiben
Schreibe eine Banktransaktion die 50 Euro von Konto 1 auf Konto 2 ueberweist mit korrektem Rollback im Fehlerfall.
Frage 7 multiple-choice
Welche der folgenden Aussagen zu try-with-resources sind korrekt?
Frage 8 erkennen
Erkenne die Fehler im Code: Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM users WHERE name = '" + name + "'");
Frage 9 wahr-falsch
Wahr oder falsch: setAutoCommit(false) ist per Default gesetzt.
Frage 10 multiple-choice
Wozu dient conn.rollback()?
Frage 11 code-schreiben
Schreibe einen Batch-Update der 5 Log-Eintraege einfuegt.
Frage 12 kurzantwort
Warum ist der 1-basierte Index bei setInt/setString eine haeufige Fehlerquelle?
Frage 13 wahr-falsch
Wahr oder falsch: Ein ResultSet kann nach dem Schliessen der Connection noch verwendet werden.
Frage 14 kurzantwort
Nenne drei Vorteile von PreparedStatement gegenueber Statement.