Kapitel 21 • Data Engineering • SQL & Persistenz

Relationale Datenbanken & SQLite 🗄️⚡

Lerne die Grundlagen relationaler Datenbanken mit Pythons integriertem sqlite3-Modul, meistere CRUD-Operationen und schütze deine Daten vor SQL-Injections.

📂 Dateien dieser Einheit:
💻 In Web-IDE öffnen → 📄 aufgabe.py (Startercode) 🧪 test_aufgabe.py (Tests) 💡 musterloesung.py (Lösung) 📖 README.md

1. Was ist eine relationale Datenbank? (Die Aktenschrank-Analogie)

In vorherigen Kapiteln haben wir Daten in einfachen JSON- oder CSV-Dateien gespeichert. Bei größeren Anwendungen stoßen Textdateien jedoch schnell an ihre Grenzen: Man kann nicht blitzschnell nach einzelnen Datensätzen suchen, ohne die gesamte Datei einzulesen, und gleichzeitige Schreibzugriffe führen zu Datenverlust. Hier kommen relationale Datenbanksysteme (RDBMS) ins Spiel!

🗄️ Die Aktenschrank-Analogie:
Stell dir eine relationale Datenbank wie einen soliden Büroschrank vor:
  • Die Datenbankdatei (.db): Der gesamte Aktenschrank im Büro.
  • Die Tabelle (Table): Eine einzelne ausziehbare Schublade (z.B. Schublade „Kunden“).
  • Die Spalte (Column / Field): Ein vorgedrucktes Formularfeld auf den Karteikarten (z.B. „Name“, „E-Mail“, „Guthaben“).
  • Die Zeile (Row / Record): Eine einzelne, ausgefüllte Karteikarte für genau einen Kunden.
  • Der Primärschlüssel (PRIMARY KEY): Die eindeutige Kundennummer oben rechts auf jeder Karteikarte.

Der Lebenszyklus einer SQLite-Verbindung in Python

Python bringt mit sqlite3 eine vollständige, vollwertige SQL-Datenbank direkt in der Standardbibliothek mit – ohne Server-Installation!

Grundgerüst mit sqlite3
import sqlite3

# 1. Verbindung herstellen (erstellt die Datei automatisch, falls nicht vorhanden)
conn = sqlite3.connect("kunden.db")

# 2. row_factory aktivieren: Erlaubt Dict-artigen Spaltenzugriff wie row["name"]
conn.row_factory = sqlite3.Row

# 3. Cursor erzeugen (der 'Stift', mit dem wir Befehle an die DB schicken)
cursor = conn.cursor()

# 4. SQL-Befehl ausführen
cursor.execute("""
    CREATE TABLE IF NOT EXISTS kunden (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        email TEXT NOT NULL UNIQUE,
        guthaben REAL DEFAULT 0.0
    )
""")

# 5. Transaktion dauerhaft speichern (Commit)
conn.commit()

# 6. Verbindung sauber schließen
conn.close()

2. Die 4 Säulen von SQL: CRUD im Überblick

In relationalen Datenbanken dreht sich alles um die vier elementaren CRUD-Operationen:

Operation SQL-Befehl Python-Beispiel mit Platzhaltern (?)
Create (Erstellen) INSERT INTO ... cursor.execute("INSERT INTO kunden (name, email, guthaben) VALUES (?, ?, ?)", (name, email, 50.0))
Read (Lesen / Suchen) SELECT ... WHERE cursor.execute("SELECT * FROM kunden WHERE id = ?", (kunden_id,))
row = cursor.fetchone()
Update (Aktualisieren) UPDATE ... SET cursor.execute("UPDATE kunden SET guthaben = guthaben + ? WHERE id = ?", (betrag, kunden_id))
Delete (Löschen) DELETE FROM ... cursor.execute("DELETE FROM kunden WHERE id = ?", (kunden_id,))
💡 Cursor-Methoden im Detail:
  • cursor.fetchone(): Liefert die nächste einzelne Ergebniszeile (oder None).
  • cursor.fetchall(): Liefert eine Liste aller passenden Ergebniszeilen (oder []).
  • cursor.lastrowid: Die automatisch generierte ID des zuletzt eingefügten Datensatzes.
  • cursor.rowcount: Die Anzahl der Zeilen, die durch das letzte UPDATE oder DELETE verändert wurden.

3. 🚨 Sicherheit: Schutz vor SQL-Injections mit ?-Parametern

Eine der häufigsten und gefährlichsten Sicherheitslücken in der Software-Entwicklung ist die SQL-Injection. Sie entsteht, wenn Benutzereingaben unbedacht per String-Formatierung (z.B. f-Strings) direkt in den SQL-Befehl eingebettet werden!

❌ GEFÄHRLICH: String-Formatierung

# Angreifer gibt als Name ein:
# Robert'); DROP TABLE kunden;--

query = f"INSERT INTO kunden (name) VALUES ('{user_name}')"
cursor.execute(query)
# BUMM! Die Tabelle kunden wird gelöscht!

✅ SICHER: Parametrisierte Abfrage

# Das sqlite3-Modul maskiert alle Zeichen:

query = "INSERT INTO kunden (name) VALUES (?)"
cursor.execute(query, (user_name,))

# Der Name wird harmlos als Text gespeichert!

4. In-Memory Datenbanken mit :memory:

Für automatisierte Unit Tests oder temporäre Berechnungen bietet SQLite ein geniales Feature: die In-Memory-Datenbank.

conn = sqlite3.connect(":memory:")

Die Datenbank existiert ausschließlich im flüchtigen RAM. Sobald conn.close() aufgerufen wird oder der Test endet, ist alles rückstandslos bereinigt – ideal für blitzschnelle, voneinander isolierte Tests ohne lästige temporäre Testdateien auf der Festplatte!

5. 🎯 Deine Aufgaben in aufgabe.py

Öffne die Datei aufgabe.py und implementiere die Klasse KundenDatenbank:

💡 Lösungshinweise (Aufklappen bei Bedarf)

🔍 Tipp zur Tabellenerstellung
self.cursor.execute("""
    CREATE TABLE IF NOT EXISTS kunden (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        email TEXT NOT NULL UNIQUE,
        guthaben REAL DEFAULT 0.0
    )
""")
self.conn.commit()
🔍 Tipp zu row_factory & Dict-Rückgabe
self.cursor.execute("SELECT id, name, email, guthaben FROM kunden WHERE id = ?", (kunden_id,))
row = self.cursor.fetchone()
if row is None:
    return None
return {
    "id": row["id"],
    "name": row["name"],
    "email": row["email"],
    "guthaben": float(row["guthaben"])
}

6. 🧪 Lösung überprüfen

Führe im Terminal im Ordner 21_datenbanken_und_sqlite folgenden Befehl aus:

Terminal
python3 test_aufgabe.py