W3docs

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

MethodeVerwendungRückgabewert
executeQuery(sql)SELECTein ResultSet
executeUpdate(sql)INSERT / UPDATE / DELETE / DDLint betroffene Zeilen
execute(sql)unbekannt / mehrere Ergebnisseboolean (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.

Warnung

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.

java— editable, runs on the server

Was aus der Ausführung zu entnehmen ist:

  • Die Cursor-Konstanten sind schlichte int-Werte, die an createStatement übergeben werden. TYPE_FORWARD_ONLY + CONCUR_READ_ONLY ist der Standard und am günstigsten; ein scrollbarer oder aktualisierbarer Cursor wird nur angefragt, wenn er wirklich benötigt wird.
  • Statement.RETURN_GENERATED_KEYS ist das Flag, das einem INSERT ermöglicht, die neue Auto-Increment-Id über getGeneratedKeys() zurückzugeben – ohne es kann der datenbankzugewiesene Schlüssel nicht abgerufen werden.
  • Die erste verkettete Abfrage ist harmlos, weil Acme keine 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 SELECT zu einem SELECT gefolgt von einem DROP TABLE wird. 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 PreparedStatement die Vorlage und die Daten getrennt senden lassen – das Thema des nächsten Kapitels.

Übungen

Übung
Dein Code erstellt eine Abfrage, indem er einen Web-Formular-Wert direkt nach WHERE owner = in den SQL-String verkettet. Was ist die richtige Lösung?
Dein Code erstellt eine Abfrage, indem er einen Web-Formular-Wert direkt nach WHERE owner = in den SQL-String verkettet. Was ist die richtige Lösung?
Was this page helpful?