Aus dem Code mit der Datenbank verbinden
Kurz gesagt: Ein Programm spricht mit einer Datenbank über einen Treiber: Es öffnet eine Verbindung zur Adresse der Datenbank, schickt die Abfrage, liest die Zeilen eine nach der anderen und schließt die Verbindung.
Problem
Die Seite „Unter 5 €“
Die Website des Shops hat eine Seite mit den günstigen Produkten. Die Abfrage dafür kennst du schon:
Aber niemand auf der Website tippt Abfragen in ein Feld. Wenn jemand die Seite öffnet, führt der Server des Shops ein Programm aus. Das Programm schickt diese Abfrage an die Datenbank, bekommt die 17 Zeilen als Werte seiner eigenen Sprache zurück (ein String, eine Zahl) und setzt sie ins HTML. Wie genau spricht ein Programm mit einer Datenbank?
Die Idee
Die fünf Schritte
In jeder Sprache ist es dasselbe:
- Treiber. Eine Bibliothek, die die Sprache einer bestimmten Datenbank spricht (SQLite, PostgreSQL, MySQL).
- Verbindung. Das Programm sagt, wo die Datenbank ist (ihre Adresse), und verbindet sich.
- Abfrage. Der SQL-Text geht an die Datenbank.
- Zeilen. Das Ergebnis kommt Zeile für Zeile zurück, und das Programm liest seine Spalten als Werte.
- Schließen. Die Verbindung wird freigegeben, auch wenn mittendrin ein Fehler passiert ist.
Hier ist dieselbe Abfrage mit einer kleinen Kopie des Shops im Speicher (:memory:, sechs Produkte, die setup am Anfang anlegt):
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) { setup(conn); String sql = "SELECT name, price FROM products WHERE price < 5 ORDER BY price"; try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery(sql)) { while (rs.next()) { System.out.printf("%s: %.2f €%n", rs.getString("name"), rs.getDouble("price")); } }}Butterbrötchen: 0.80 €Weißbrot: 1.20 €Frischmilch: 1.60 €Butter: 3.20 €using var conn = new SqliteConnection("Data Source=:memory:");conn.Open();Setup(conn);var cmd = conn.CreateCommand();cmd.CommandText = "SELECT name, price FROM products WHERE price < 5 ORDER BY price";using var reader = cmd.ExecuteReader();while (reader.Read()){ Console.WriteLine($"{reader.GetString(0)}: {reader.GetDouble(1):F2} €");}Butterbrötchen: 0.80 €Weißbrot: 1.20 €Frischmilch: 1.60 €Butter: 3.20 €conn = sqlite3.connect(":memory:")setup(conn)sql = "SELECT name, price FROM products WHERE price < 5 ORDER BY price"for name, price in conn.execute(sql): print(f"{name}: {price:.2f} €")conn.close()Butterbrötchen: 0.80 €Weißbrot: 1.20 €Frischmilch: 1.60 €Butter: 3.20 €const db = new DatabaseSync(":memory:");setup(db);const sql = "SELECT name, price FROM products WHERE price < 5 ORDER BY price";for (const row of db.prepare(sql).all()) { console.log(`${row.name}: ${row.price.toFixed(2)} €`);}db.close();Butterbrötchen: 0.80 €Weißbrot: 1.20 €Frischmilch: 1.60 €Butter: 3.20 €Um den fünften Schritt kümmert sich die Sprache: try (…) in Java, using in C#, close() in Python und JavaScript. In Python beendet with die Transaktion, nicht die Verbindung, deshalb steht close() hier ausdrücklich da.
Genauer, für Neugierige
setup führt den SQL-Text aus, der die Tabelle mit den sechs Produkten anlegt:
static void setup(Connection conn) throws SQLException { try (Statement st = conn.createStatement()) { st.executeUpdate(""" CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL NOT NULL, discount INTEGER); INSERT INTO products VALUES (1, 'Weißbrot', 1.20, NULL), (3, 'Butterbrötchen', 0.80, 10), (5, 'Frischmilch', 1.60, NULL), (7, 'Weißkäse', 7.90, 15), (9, 'Butter', 3.20, NULL), (17, 'Kaffeebohnen', 14.90, 0); """); }}static void Setup(SqliteConnection conn){ var cmd = conn.CreateCommand(); cmd.CommandText = """ CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL NOT NULL, discount INTEGER); INSERT INTO products VALUES (1, 'Weißbrot', 1.20, NULL), (3, 'Butterbrötchen', 0.80, 10), (5, 'Frischmilch', 1.60, NULL), (7, 'Weißkäse', 7.90, 15), (9, 'Butter', 3.20, NULL), (17, 'Kaffeebohnen', 14.90, 0); """; cmd.ExecuteNonQuery();}def setup(conn): conn.executescript(""" CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL NOT NULL, discount INTEGER); INSERT INTO products VALUES (1, 'Weißbrot', 1.20, NULL), (3, 'Butterbrötchen', 0.80, 10), (5, 'Frischmilch', 1.60, NULL), (7, 'Weißkäse', 7.90, 15), (9, 'Butter', 3.20, NULL), (17, 'Kaffeebohnen', 14.90, 0); """)function setup(db) { db.exec(` CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL NOT NULL, discount INTEGER); INSERT INTO products VALUES (1, 'Weißbrot', 1.20, NULL), (3, 'Butterbrötchen', 0.80, 10), (5, 'Frischmilch', 1.60, NULL), (7, 'Weißkäse', 7.90, 15), (9, 'Butter', 3.20, NULL), (17, 'Kaffeebohnen', 14.90, 0); `);}Die Treiber in den Beispielen: sqlite-jdbc in Java (das Paket org.xerial:sqlite-jdbc), Microsoft.Data.Sqlite in C# (NuGet), und in Python und JavaScript ist SQLite eingebaut (sqlite3, und node:sqlite seit Node 22). Jedes Beispiel wurde ausgeführt, und seine Ausgabe ist genau diese.
Sieh es dir an
Die Adresse der Datenbank und das Passwort
SQLite ist eine Datei, und die Adresse ist der Pfad dazu. Wenn du in der Lektion „SQL und echte Datenbanken“ eine shop.db angelegt hast (sqlite3 shop.db < shop-de.sql), ersetze :memory: durch den Pfad und lass setup weg: In Java ist es jdbc:sqlite:shop.db, in C# Data Source=shop.db, und in Python und JavaScript einfach "shop.db". Dasselbe Programm zeigt dann alle 17 Produkte.
Eine Server-Datenbank (PostgreSQL, MySQL, SQL Server) lebt in einem anderen Programm, oft auf einem anderen Computer. Ihre Adresse hat einen Host, einen Port, einen Datenbanknamen, einen Benutzer und ein Passwort:
String url = "jdbc:postgresql://localhost:5432/shop";String password = System.getenv("SHOP_DB_PASSWORD");try (Connection conn = DriverManager.getConnection(url, "shop_app", password)) { // dieselben Abfragen}var password = Environment.GetEnvironmentVariable("SHOP_DB_PASSWORD");var url = $"Host=localhost;Port=5432;Database=shop;Username=shop_app;Password={password}";using var conn = new NpgsqlConnection(url);conn.Open();import osimport psycopg
conn = psycopg.connect(host="localhost", port=5432, dbname="shop", user="shop_app", password=os.environ["SHOP_DB_PASSWORD"])import pg from "pg";
const db = new pg.Client({ host: "localhost", port: 5432, database: "shop", user: "shop_app", password: process.env.SHOP_DB_PASSWORD });await db.connect();Das Passwort steht nicht im Code. Der Code kommt in git, wo jeder mit Zugriff ihn sieht, und das Passwort der echten Seite ist ein anderes als das auf deinem Computer. Es kommt also von außen: aus einer Umgebungsvariablen wie hier oder aus dem Secrets-Speicher des Hosting-Anbieters.
Diese vier Beispiele laufen nicht in den Prüfungen der Seite, weil sie einen PostgreSQL-Server brauchen. Die Treiber sind org.postgresql, Npgsql, psycopg und pg.
Probier es aus
Die Abfrage für eine andere Seite
Die Seite „Ausverkauft“ im Admin-Bereich zeigt die Namen der Produkte mit Bestand 0, in alphabetischer Reihenfolge. Schreib die Abfrage, die das Programm schicken wird.
Lösung
SELECT name FROM products WHERE stock = 0 ORDER BY name;Das Programm ändert sich nicht: Nur der Text in sql ändert sich. Deshalb hilft es, die Abfrage erst in einem Feld wie diesem auszuprobieren und sie erst dann in den Code zu setzen.
Die Idee
Von der Spalte zum Wert
Jede Spalte kommt als Wert eines Typs der Sprache an. INTEGER wird zu int (oder long), REAL wird zu double, TEXT wird zu String / string / str. Interessant ist NULL. In Python ist es None, in JavaScript null, und in Java und C# hängt es davon ab, wie du es liest:
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) { setup(conn); try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery("SELECT name, discount FROM products WHERE id IN (1, 3, 17)")) { while (rs.next()) { int discount = rs.getInt("discount"); String text = rs.wasNull() ? "kein Rabatt" : discount + "%"; System.out.println(rs.getString("name") + ": getInt liefert " + discount + ", also " + text); } }}Weißbrot: getInt liefert 0, also kein RabattButterbrötchen: getInt liefert 10, also 10%Kaffeebohnen: getInt liefert 0, also 0%using var conn = new SqliteConnection("Data Source=:memory:");conn.Open();Setup(conn);var cmd = conn.CreateCommand();cmd.CommandText = "SELECT name, discount FROM products WHERE id IN (1, 3, 17)";using var reader = cmd.ExecuteReader();while (reader.Read()){ var text = reader.IsDBNull(1) ? "kein Rabatt" : $"{reader.GetInt32(1)}%"; Console.WriteLine($"{reader.GetString(0)}: {text}");}Weißbrot: kein RabattButterbrötchen: 10%Kaffeebohnen: 0%conn = sqlite3.connect(":memory:")setup(conn)for name, discount in conn.execute("SELECT name, discount FROM products WHERE id IN (1, 3, 17)"): text = "kein Rabatt" if discount is None else f"{discount}%" print(f"{name}: {discount!r}, also {text}")conn.close()Weißbrot: None, also kein RabattButterbrötchen: 10, also 10%Kaffeebohnen: 0, also 0%const db = new DatabaseSync(":memory:");setup(db);for (const row of db.prepare("SELECT name, discount FROM products WHERE id IN (1, 3, 17)").all()) { const text = row.discount === null ? "kein Rabatt" : `${row.discount}%`; console.log(`${row.name}: ${row.discount}, also ${text}`);}db.close();Weißbrot: null, also kein RabattButterbrötchen: 10, also 10%Kaffeebohnen: 0, also 0%Weißbrot hat keinen Rabatt (NULL), während der Kaffee einen Rabatt von 0 % hat. Das ist nicht dasselbe. In Java liefert getInt in beiden Fällen 0, und nur wasNull() unterscheidet sie. In C# bricht GetInt32 bei NULL mit einem Fehler ab, deshalb fragst du zuerst mit IsDBNull.
Übung
Die neuen Produkte
Die Startseite zeigt die neuen Produkte: Name und Preis der Produkte, die ab dem 1. Juni 2025 hinzugekommen sind (added_at), die neuesten zuerst, und bei gleichem Datum nach Name.
Lösung
SELECT name, price FROM products WHERE added_at >= '2025-06-01' ORDER BY added_at DESC, name;Mit zwei Schlüsseln in ORDER BY: added_at DESC, name. Ohne den zweiten Schlüssel können die beiden Produkte vom 1. August jedes Mal in anderer Reihenfolge herauskommen, und auf der Seite sieht man das.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: SELECT: Spalten auswählen · WHERE: Zeilen filtern · NULL: kein Wert · ORDER BY, LIMIT und DISTINCT
- Geht weiter in: SQL-Injection und Parameter · ORM und das N+1-Problem · Schema-Migrationen · Verbindungspools, Backups und Berechtigungen
- Aufgaben zum Thema: Die Datenbank im Programm