Java JDBC Statement
SQL in Java mit dem Statement-Interface ausführen – wann es gegenüber PreparedStatement sinnvoll ist.
Ein Statement sendet einen vollständigen, festen SQL-String an die Datenbank. Man erstellt eines aus einer Connection, übergibt SQL und erhält entweder ein ResultSet (bei Abfragen) oder eine Anzahl geänderter Zeilen (bei Änderungen) zurück. Es ist der einfachste der drei JDBC-Statement-Typen – und derjenige, den man am seltensten verwenden sollte, da variable Daten im SQL manuell per Zeichenkettenverkettung eingefügt werden müssen, was der Ursprung von SQL-Injection-Fehlern ist.
Dieses Kapitel behandelt die Erstellung und Ausführung eines Statement, die drei Ausführungsmethoden und wann welche anzuwenden ist, die Anpassung des Cursors und das Lesen generierter Schlüssel sowie – am wichtigsten – wann man stattdessen ein PreparedStatement verwenden sollte. Wer neu bei JDBC ist, beginnt am besten mit der JDBC-Einführung.
Erstellen und Ausführen
try (Connection conn = DriverManager.getConnection(url, user, pw);
Statement st = conn.createStatement()) {
// a query → ResultSet
try (ResultSet rs = st.executeQuery("SELECT count(*) FROM product")) {
rs.next();
System.out.println(rs.getInt(1));
}
// a change → update count
int rows = st.executeUpdate("UPDATE product SET active = true WHERE price > 0");
System.out.println(rows + " rows updated");
}Drei Ausführungsmethoden
| Methode | Verwendung | Rückgabewert |
|---|---|---|
executeQuery(sql) | SELECT | ein ResultSet |
executeUpdate(sql) | INSERT / UPDATE / DELETE / DDL | int betroffene Zeilen |
execute(sql) | unbekannt / mehrere Ergebnisse | boolean (true wenn ein ResultSet) |
Verwende executeQuery und executeUpdate, wenn du im Voraus weißt, welche Art von Statement du ausführst – sie geben direkt den richtigen Typ zurück. Greife auf execute nur in generischen Werkzeugen zurück (eine SQL-Konsole, ein Migrationsläufer), wo das SQL erst zur Laufzeit bekannt ist; danach rufst du getResultSet() oder getUpdateCount() auf, um das Ergebnis abzurufen.
executeUpdate gibt 0 für DDL wie CREATE TABLE zurück; bei INSERT/UPDATE/DELETE gibt es die Anzahl der betroffenen Zeilen zurück – nützlich, um zu bestätigen, dass ein Update tatsächlich eine Zeile gefunden hat.
Cursor und generierte Schlüssel anpassen
Beim Erstellen eines Statements kannst du das Verhalten des resultierenden Cursors mit createStatement(resultSetType, resultSetConcurrency) festlegen – zum Beispiel TYPE_FORWARD_ONLY, CONCUR_READ_ONLY (der Standard und die schnellste Variante). Verwende TYPE_SCROLL_INSENSITIVE nur, wenn du rückwärts durch das Ergebnis navigieren musst, und CONCUR_UPDATABLE nur, wenn du Zeilen über den Cursor bearbeiten möchtest; beides kostet mehr Ressourcen.
Für Einfügungen übergib Statement.RETURN_GENERATED_KEYS und lese dann den datenbankzugewiesenen Primärschlüssel mit getGeneratedKeys() aus:
try (Statement st = conn.createStatement()) {
st.executeUpdate(
"INSERT INTO product(name, price) VALUES ('Widget', 9.99)",
Statement.RETURN_GENERATED_KEYS);
try (ResultSet keys = st.getGeneratedKeys()) {
if (keys.next()) {
long newId = keys.getLong(1);
System.out.println("inserted id = " + newId);
}
}
}Ohne dieses Flag gelingt der Aufruf zwar, aber getGeneratedKeys() gibt ein leeres ResultSet zurück, sodass die neue Id nicht wiederhergestellt werden kann.
Wann Statement NICHT verwendet werden sollte
Sobald ein Teil des SQL aus einer Variable stammt – ein Benutzername, eine Id, ein Suchbegriff – sollte man aufhören und stattdessen ein PreparedStatement verwenden. Werte in einen Statement-String zu verketten ist unsicher: Ein Wert mit einem Anführungszeichen kann die Bedeutung des Befehls verändern. PreparedStatement speichert außerdem seinen Parse-Plan zwischen, sodass eine Abfrage in einer Schleife als Prepared Statement schneller ist. Das nächste Kapitel widmet sich dieser sicheren Alternative; für gespeicherte Prozeduren siehe CallableStatement.
Behalte Statement für festes, wertfreies SQL: Schema-Setup (CREATE TABLE …), einmalige DDL oder ein fest codiertes SELECT ohne variablen Teil.
Schließe ein Statement niemals, solange du noch sein ResultSet benötigst – das Schließen des Statements schließt jedes davon produzierte Ergebnis. Verwende einen try-with-resources-Block, wie in den obigen Beispielen, damit jedes Objekt in der richtigen Reihenfolge geschlossen wird.
Ein vollständiges Beispiel: die Cursor-Konstanten und die Injection-Falle
Dieses Programm gibt die ResultSet-/Statement-Anpassungskonstanten aus, die beim Erstellen eines Statements übergeben werden, und zeigt dann konkret, warum per Zeichenkettenverkettung erstelltes SQL gefährlich ist – indem es zeigt, was ein bösartiger Wert mit dem Befehlstext anstellt.
Was aus der Ausführung zu entnehmen ist:
- Die Cursor-Konstanten sind schlichte
int-Werte, die ancreateStatementübergeben werden.TYPE_FORWARD_ONLY+CONCUR_READ_ONLYist der Standard und am günstigsten; ein scrollbarer oder aktualisierbarer Cursor wird nur angefragt, wenn er wirklich benötigt wird. Statement.RETURN_GENERATED_KEYSist das Flag, das einemINSERTermöglicht, die neue Auto-Increment-Id übergetGeneratedKeys()zurückzugeben – ohne es kann der datenbankzugewiesene Schlüssel nicht abgerufen werden.- Die erste verkettete Abfrage ist harmlos, weil
Acmekeine SQL-Metazeichen enthält. Genau deshalb scheint die Zeichenkettenverkettung beim Testen zu funktionieren – und schlägt dann in der Produktion mit realen Eingabedaten fehl. - Der zweite Wert enthält ein Anführungszeichen und ein Semikolon, sodass das einzelne beabsichtigte
SELECTzu einemSELECTgefolgt von einemDROP TABLEwird. Die Daten haben ihre Anführungszeichen verlassen und sind zu ausführbarem SQL geworden – das Lehrbuchbeispiel für Injection. - Die Lösung lautet niemals „die Anführungszeichen selbst escapen". Stattdessen muss man aufhören, SQL aus Werten zu bauen, und
PreparedStatementdie Vorlage und die Daten getrennt senden lassen – das Thema des nächsten Kapitels.