Thema in Kurzform
Bei der Kommunikation mit MySQL-Datenbanken ist es ratsam, keine einfachen SQL-Statements zu verwenden, sondern auf sogenannten PreparedStatements zu setzen. Das ist nicht nur sicherer, sondern auch besser in Hinsicht auf Performance und Handling.
// Schnelles Code Beispiel
String sql = "SELECT * FROM personen WHERE name = ? AND vorname = ?";
PreparedStatement ps = con.prepareStatement(sql);
ps.setString(1, "Musk");
ps.setString(2, "Elon");
ResultSet result = ps.executeQuery();
Ein PreparedStatement in Java ist eine besondere Art von Statement. Es erweitert das Interface Statement und hat diesem gegnüber einige Vorteile:
Übersetzt bedeutet PreparedStatement "vorbereitete Anweisung". "Vorbereitet" deshalb, weil die SQL-Anweisung (SELECT, INSERT, UPDATE) in einem ersten Schritt zur Datenbank lediglich geschickt wird, die tatsächliche Ausführung aber zu einem späteren Zeitpunkt erfolgt.
Im Unterschied zum "normalen" Statement funktioniert das PreparedStatementstatt mit konkreten Werten mit Platzhaltern (Placeholder), die durch ein Fragezeichen (?) ausgedrückt werden.
So erstellst du ("bereitest vor") ein PreparedStatement:
String sql = "SELECT * FROM personen WHERE id = ?";
PreparedStatement ps = con.prepareStatement(sql);
Nach Erhalt des PreparedStatement wartet die Datenbank auf die Ausführung. Die Ausführung des PreparedStatements erfolgt dann in einem zweiten Schritt, nachdem die fehlenden Werte für die SQL-Anweisung mit speziellen Methoden nachgereicht worden sind.
Für jedes Fragezeichen (?) muss im PreparedStatement ein passender Wert eingesetzt werden. Hierfür gibt es spezielle Setter-Methoden:
Die Setter-Methoden von PreparedStatement | |||
---|---|---|---|
setInt(int paramIndex, int value) | Setzt einen int-Wert auf den gewählten Platzhalter-Index | ||
setString(int paramIndex, String value) | Setzt einen String auf den gewählten Platzhalter-Index | ||
setDouble(int paramIndex, double value) | Setzt einen double-Wert auf den gewählten Platzhalter-Index |
Die Indices der Platzhalter beginnen bei 1 (nicht wie gewohnt bei 0).
Um also im Beispiel oben den Platzhalter bei id mit einem konkreten Wert zu ersetzen, kann die setInt()-Methode ausgeführt werden:
ps.setInt(1, 7);
Damit wird im PreparedStatement der Platzhalter auf Indexposition 1 mit dem Wert 7 ersetzt.
Man kan auch mehrere Platzhalter einsetzen. Hier ein Beispiel:
String sql = "SELECT * FROM personen WHERE name = ? AND vorname = ?";
PreparedStatement ps = con.prepareStatement(sql);
ps.setString(1, "Musk");
ps.setString(2, "Elon");
Um das PreparedStatement nun endlich auszuführen, gibt es zwei Methoden, nämlich executeQuery() und executeUpdate().
executeQuery() wird für alle SELECT-Abfragen eingesetzt:
String sql = "SELECT * FROM personen WHERE name = ? AND vorname = ?";
PreparedStatement ps = con.prepareStatement(sql);
ps.setString(1, "Musk");
ps.setString(2, "Elon");
ResultSet result = ps.executeQuery();
Wie bei gewöhnlichen Statements auch, wird das Ergebnis in einem Objekt vom Typ ResultSet gespeichert. Wie wir durch das ResultSet iterieren, siehst du im Tutorial zu Java MySQL Daten auslesen.
executeUpdate() dagegen verwendet man für alle SQL-Anweisungen, die den Inhalt von Tabellen manipulieren (INSERT, UPDATE, DELETE):
String sql = "UPDATE personen SET name = ?, vorname = ? WHERE id = ?";
PreparedStatement ps = con.prepareStatement(sql);
ps.setString(1, "Magellan");
ps.setString(2, "Ferdinand");
ps.setInt(3, 123);
int rows = ps.executeUpdate();
Die executeUpdate()-Methode liefert einen int-Wert zurück, der anzeigt, wieviele Datensätze von der Änderung betroffen sind.
Das Grundprinzip hinter einem PreparedStatement in Java siehst du zusammengefasst in folgender Infografik:
Wie gesagt, solltest du in einem Produktivsystem auf PreparedStatements setzen. Wir zeigen dir im Folgenden die Gründe dafür:
Einen Datenbank benötigt immer eine gewisse Zeit, um einen SQL-String zu verarbeiten: Zuerst wird der SQL-String zur Datenbank geschickt. Danach wird er dort auf syntaktische Gültigkeit geprüft. Anschließend wird ein interner Ausführungsplan (Query plan) erzeugt, der dazu dient, die Ausführung der SQL-Abfrage möglichst effizient durchzuführen.
Jedes mal, wenn ein neues Statement an die Datenbank geschickt wird, wird dieser Vorgang wiederholt. Die Arbeitszeit, die die dafür benötigt wird, ist messbar.
Indem wir aber ein PreparedStatement mit seiner Vorübersetzung einsetzen, müssen sowohl die Syntax-Prüfung des SQL-Strings als auch das Aufstellen des internen Ausführungsplans nur ein einziges Mal durchgeführt werden.
Mit einem PreparedStatement gewinnen wir also gerade vielen Abfragen (Schleifen!) signifikante Performance-Vorteile!
Ein PreparedStatement sorgt für deutlich leichter zu lesenden Code.
Sieh dir zunächst einmal dieses "normale" Statement an:
int id = 100;
String kategorie = "Stahl";
String sql = "SELECT * FROM kunden
"WHERE id > " + id + " AND kategorie = '" + kategorie + "';";
Statement stm = con.createStatement();
ResultSet rs = stm.executeQuery(sql);
Ganz klar: Durch die String-Verkettung und die Ausrufezeichen (einfache und doppelte) wirkt das Ganze recht unübersichtlich. Das Schreiben solcher Strings ist auch nicht immer eine Freude. Je mehr Variablen im Spiel sind, desto grausamer wird das Ganze.
Besser ist das Ganze mit einem PreparedStatement:
String sql = "SELECT * FROM kunden WHERE id > ? AND kategorie = ?;";
PreparedStatement ps = con.prepareStatement(sql);
ps.setInt(1, 100);
ps.setString(2, "Stahl");
ResultSet result = ps.executeQuery();
Nachdem ein PreparedStatement erst einmal vorbereitet worden ist, kann es sehr einfach nach der Ausführung wiederverwendet werden. Wir müssen einfach nur die Werte der setter-Methoden ändern. Hier ein Beispiel:
String sql = "SELECT * FROM kunden WHERE id > ? AND kategorie = ?;";
PreparedStatement ps = con.prepareStatement(sql);
ps.setInt(1, 100);
ps.setString(2, "Stahl");
ResultSet result = ps.executeQuery();
ps.setInt(1, 50);
ps.setString(2, "Blech");
ResultSet result2 = ps.executeQuery();
Nicht zuletzt erhöhen PreparedStatements die Sicherheit, indem Sie sog. "SQL-Injections" verhindern können. Bei SQL-Injections handelt es sich um einen Missbrauch von SQL-Abfragen.
Hier ein Beispiel:
public void del(String name) throws SQLException{
String sql = "DELETE FROM kunden WHERE name = '" + name + "';";
Statement stm = con.createStatement();
stm.execute(sql);
}
Diese Methode nimmt einen String-Parameter an setzt daraus den SQL-DELETE-String zusammen. Der passende Datensatz wird gelöscht. Was geschieht aber, wenn der Methode statt eines gültigen Namens "EGAL' OR 1 = 1 OR name='EGAL" als Argument übergeben wird, der SQL-String also so zusammengesetzt wird:
DELETE FROM kunden WHERE name = 'EGAL' OR 1 = 1 OR name='EGAL';
In diesem Fall würden alle Datenätze gelöscht werden! Die eingeschleuste (injected) ODER-Verknüpfung lässt die WHERE-Prüfung nämlich für alle Zeilen wahr werden.
Glücklicherweise gibt es unser PreparedStatement. Damit ist ein solches "Hacking" nicht mehr möglich:
String sql = "DELETE FROM kunden WHERE name = ?;";
PreparedStatement ps = con.prepareStatement(sql);
ps.setString(1, name);
ps.execute();
Besonders bei grafischen Benutzeroberflächen (GUIs), wo der Benutzer Eingabefelder zur Datenbank-Abfrage vorfindet, sollte unbedingt mit PreparedStatements gearbeitet werden. Sonst wäre es ein Leichtes für Unbefugte, die Datenbank zu hacken und Kundendaten auszulesen, zu manipulieren oder zu löschen.
Java Basics
[Java einrichten] [Variablen] [Primitive Datentypen] [Operatoren] [if else] [switch-case] [Arrays] [Schleifen]
Objektorientierung
[Einstieg] [Variablen ] [Konstruktor] [Methoden] [Rekursion] [Statische Member] [Initializer] [Pass-by-value] [Objektsammlungen] [Objektinteraktion] [Objekte löschen]
Klassenbibliothek
[Allgemeines] [String ] [Math] [Wrapper] [Scanner] [java.util.Arrays] [Date-Time-API]
Vererbung
[Einstieg Vererbung] [Konstruktoren bei Vererbung ] [Der protected Zugriffsmodifikator] [Abstrakte Klassen und Methoden] [Polymorphie in Java] [Typumwandlung] [Die Klasse Object] [Die toString()-Methode] [Objekte vergleichen] [Was ist ein Interface?]