Datenbanken
Kurz gesagt: Eine Datenbank speichert Daten in Tabellen und findet sie mit einer SQL-Abfrage schnell; in Python sprichst du über sqlite3 mit ihr, und ?-Parameter schützen vor SQL-Injection.
Problem
Ein Katalog in einer Datei
Der Katalog des Shops ist eine CSV-Datei, wie im Modul über Dateien. Solange es hundert Produkte sind, geht das. Dann werden es hunderttausend, und es gibt Probleme:
- um ein Produkt zu finden, liest du die ganze Datei;
- „alles unter 5 €, nach Preis sortiert“ ist eine Schleife, ein
ifund eine Sortierung, alles von Hand geschrieben; - zwei Kassen schreiben gleichzeitig in die Datei, und ein Eintrag geht verloren oder die Datei wird beschädigt;
- mitten beim Schreiben fällt der Strom aus, und die halbe Datei ist weg.
Deshalb liegen die Daten fast jedes echten Programms in einer Datenbank: einem Programm, dessen Aufgabe es ist, Daten aufzubewahren, sie schnell zu finden und nie zu verlieren.
Die Idee
Tabellen und SQL
Eine relationale Datenbank speichert Daten in Tabellen. Eine Tabelle hat Spalten (Name, Typ) und Zeilen (eine für jedes Produkt). Die Spalte id ist der Primärschlüssel: Ihr Wert ist in jeder Zeile anders, deshalb findet man mit ihm genau eine Zeile.
Mit der Datenbank sprichst du in SQL. Die fünf häufigsten Befehle:
CREATE TABLE products ( id INT PRIMARY KEY, name VARCHAR(50) NOT NULL, price DECIMAL(8, 2));
INSERT INTO products VALUES (1, 'Kaffee', 7.50);SELECT name, price FROM products WHERE price < 5 ORDER BY price;UPDATE products SET price = 6.90 WHERE id = 1;DELETE FROM products WHERE id = 3;CREATE TABLElegt eine Tabelle an,INSERTfügt eine Zeile ein,UPDATEändert Zeilen undDELETElöscht sie.SELECTliest: welche Spalten, aus welcher Tabelle, unter welcher Bedingung (WHERE) und in welcher Reihenfolge (ORDER BY).
Du sagst, was du willst, und die Datenbank entscheidet, wie sie es schnell findet.
Genauer, für Neugierige
- Bekannte relationale Datenbanken sind PostgreSQL, MySQL, Oracle und SQL Server. Sie laufen als eigene Server. Hier benutzen wir SQLite: eine kleine Datenbank, die in einer Datei oder ganz im Speicher des Programms lebt. Das Modul
sqlite3ist in der Standardbibliothek von Python. Die Adresse":memory:"ist eine Datenbank im Speicher: Jeder Lauf beginnt mit einer leeren. - Text steht in SQL in einfachen Anführungszeichen:
'Kaffee'. In PostgreSQL und MySQL istDECIMAL(8, 2)eine exakte Zahl mit 2 Nachkommastellen, das passt zu Geld. SQLite speichert keine exakten Dezimalzahlen: Dort ist7.50eine gewöhnliche Gleitkommazahl.
Sieh es dir an
sqlite3: Verbindung, Abfrage, Ergebnis
sqlite3 folgt dem Standard von Python für Datenbanken (DB-API, PEP 249): PostgreSQL und MySQL haben dieselben connect, execute, commit und close. Die Verbindung ist eine Connection, und das Ergebnis einer Abfrage ist ein Cursor. Die Funktion setup legt die Tabelle an und fügt drei Produkte ein:def setup(conn): conn.execute("CREATE TABLE products (id INT PRIMARY KEY, name VARCHAR(50) NOT NULL, price DECIMAL(8, 2))") conn.execute("INSERT INTO products VALUES (1, 'Kaffee', 7.50), (2, 'Tee', 3.20), (3, 'Schokolade', 2.40)") conn.commit()Product mit den Feldern id, name und price:@dataclassclass Product: id: int name: str price: floatconn = sqlite3.connect(":memory:")setup(conn)products = []for product_id, name, price in conn.execute("SELECT id, name, price FROM products ORDER BY price"): products.append(Product(product_id, name, price))for p in products: print(p)conn.close()Product(id=3, name='Schokolade', price=2.4)Product(id=2, name='Tee', price=3.2)Product(id=1, name='Kaffee', price=7.5)sqlite3.connect(address)öffnet die Verbindung. Die Adresse sagt, welche Datenbank: hier":memory:", oder ein Dateiname wie"shop.db".conn.execute(sql)schickt einen beliebigen Befehl und gibt einen Cursor zurück. Einefor-Schleife über den Cursor gibt dir eine Zeile nach der anderen, jede als Tupel der Spalten in der Reihenfolge desSELECT. Hier wird das Tupel in drei Variablen entpackt.- Vor
INSERT,UPDATEundDELETEbeginnt sqlite3 von selbst eine Transaktion, undconn.commit()speichert die Änderungen. Ohne das gehen Änderungen an einer Datenbank in einer Datei beim Schließen verloren. conn.close()schließt die Verbindung. (with conn:schließt sie nicht: Es macht eine Transaktion, wie du weiter unten siehst.)- Fehler sind Exceptions von
sqlite3.Error, zum BeispielOperationalErrorfür falsches SQL undIntegrityErrorfür eine verletzte Regel der Tabelle. Python hat keine geprüften Exceptions.
conn = sqlite3.connect(":memory:")setup(conn)products = []for product_id, name, price in conn.execute("SELECT id, name, price FROM products ORDER BY price"): products.append(Product(product_id, name, price))for p in products: print(p)conn.close()Schritt für Schritt, in Worten
- Eine Verbindung zur Datenbank im Speicher. setup legt die Tabelle products an und fügt 3 Zeilen ein.
- Eine leere Liste, in die die Objekte kommen.
- SELECT mit ORDER BY price: conn.execute gibt einen Cursor über die Zeilen zurück, die günstigste zuerst.
- Die Schleife nimmt Zeile 1 als Tupel und entpackt sie in drei Variablen, und Product(...) macht daraus ein Objekt: Schokolade.
- Die Schleife nimmt Zeile 2 als Tupel und entpackt sie in drei Variablen, und Product(...) macht daraus ein Objekt: Tee.
- Die Schleife nimmt Zeile 3 als Tupel und entpackt sie in drei Variablen, und Product(...) macht daraus ein Objekt: Kaffee.
- Der Cursor hat keine Zeilen mehr, und die Schleife hört auf.
- Die Liste enthält gewöhnliche Objekte. Die Ausgabe zeigt die Dataclass mit ihren Feldern.
Genauer, für Neugierige
- Bei einer echten Datenbank auf einem Server ist die Adresse anders, und es gibt einen Benutzer und ein Passwort. Außerdem brauchst du den Treiber der Datenbank als Abhängigkeit im Projekt (für MySQL: das Paket
mysql-connector-python):
import mysql.connector
conn = mysql.connector.connect(host="localhost", port=3306, database="shop", user="shop_app", password="geheim")print("Mit der Datenbank verbunden!")conn.close()- Das Passwort steht nicht im Code. Es wird aus den Einstellungen oder aus einer Umgebungsvariable gelesen (
os.environ).
- Die Preise sind hier
float, damit es kurz bleibt. In einem echten Shop wird Geld exakt gespeichert: alsDECIMALin PostgreSQL oder MySQL (in Python kommt es alsdecimal.Decimalan) und in SQLite in ganzen Cent. - Mit
conn.row_factory = sqlite3.Rowkannst du jede Zeile auch nach dem Namen der Spalte lesen:row["name"].
Probier es aus
Was wird ausgegeben?
Die Produkte sind Kaffee 7,50 €, Tee 3,20 € und Schokolade 2,40 €. Was gibt das Programm aus?
conn = sqlite3.connect(":memory:")setup(conn)for row in conn.execute("SELECT name FROM products WHERE price < 5 ORDER BY price DESC"): print(row[0])conn.close()WHERE price < 5 behält Tee und Schokolade. ORDER BY price DESC sortiert sie nach Preis von oben nach unten (DESC ist absteigend, ASC ist der Standard): zuerst Tee für 3,20 €, dann Schokolade für 2,40 €.
Problem
Die Suche, die alles zeigt
Die Website sucht ein Produkt nach dem Namen, den der Nutzer eintippt. Die Abfrage wird mit einem f-String zusammengesetzt:
def search(conn, name): sql = f"SELECT name, price FROM products WHERE name = '{name}'" for found, price in conn.execute(sql): print(f" {found}: {price} €")conn = sqlite3.connect(":memory:")setup(conn)print("Suche: Tee")search(conn, "Tee")print("Suche: x' OR '1'='1")search(conn, "x' OR '1'='1")conn.close()Suche: Tee Tee: 3.2 €Suche: x' OR '1'='1 Kaffee: 7.5 € Tee: 3.2 € Schokolade: 2.4 €Mit „Tee“ funktioniert es. Mit x' OR '1'='1 wird die Abfrage zu ... WHERE name = 'x' OR '1'='1'. Die Bedingung ist immer wahr, und die ganze Tabelle kommt heraus. Die Eingabe des Nutzers ist Teil des SQL geworden. Das ist SQL-Injection, einer der häufigsten Angriffe auf Websites. Mit demselben Trick kann sich jemand ohne Passwort anmelden oder eine Tabelle löschen.
Die Idee
Parameter mit ?
? an den Stellen der Werte, und du übergibst sie als Tupel, das zweite Argument von execute. (name,) ist ein Tupel mit einem Wert: Das Komma ist Pflicht.def search(conn, name): sql = "SELECT name, price FROM products WHERE name = ?" for found, price in conn.execute(sql, (name,)): print(f" {found}: {price} €")Suche: Tee Tee: 3.2 €Suche: x' OR '1'='1Jetzt ist x' OR '1'='1 nur Text, der als Name gesucht wird. Ein solches Produkt gibt es nicht, und nichts kommt heraus. Was der Nutzer auch eintippt, es kann nicht zu SQL werden.
?.Probier es aus
Kaffee mit Apostroph
Kaffee 'Arabica' bricht das Programm mit sqlite3.OperationalError: near "Arabica": syntax error ab. In welcher Zeile ist der Fehler?Wähle die Zeile mit dem Fehler.
'Arabica' schließt den Text zu früh, und der Rest ist sinnloses SQL (near "Arabica" zeigt auf die Stelle). Ein Angreifer würde dort seine eigene Abfrage hinschreiben. Die Korrektur sind Parameter: conn.execute("UPDATE products SET name = ? WHERE id = ?", (new_name, product_id)). Dann wird der Name genau so gespeichert, wie er ist, mit den Apostrophen.Die Idee
Transaktion: alles oder nichts
Maria überweist 150 € an Jonas. Das sind zwei Abfragen: bei Jonas hinzufügen, bei Maria abziehen. Die Tabelle hat die Bedingung CHECK (balance >= 0), deshalb schlägt die zweite Abfrage fehl: Maria hat nur 100 €.
def open_accounts(conn): conn.execute("CREATE TABLE accounts (id INT PRIMARY KEY, owner VARCHAR(50), balance INT CONSTRAINT no_debt CHECK (balance >= 0))") conn.execute("INSERT INTO accounts VALUES (1, 'Maria', 100), (2, 'Jonas', 50)") conn.commit()
def move(conn, account_id, amount): conn.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", (amount, account_id))
def print_balances(conn): for owner, balance in conn.execute("SELECT owner, balance FROM accounts ORDER BY id"): print(f" {owner}: {balance} €")def transfer(conn, source, target, amount): move(conn, target, amount) conn.commit() move(conn, source, -amount) conn.commit()
conn = sqlite3.connect(":memory:")open_accounts(conn)try: transfer(conn, 1, 2, 150)except sqlite3.Error: print("Die Überweisung von 150 € wurde abgelehnt.")print_balances(conn)conn.close()Die Überweisung von 150 € wurde abgelehnt. Maria: 100 € Jonas: 200 €commit() gespeichert. Die zwei Abfragen müssen eine Transaktion sein: Entweder gehen beide durch, oder keine. In Python macht das with conn:.def transfer(conn, source, target, amount): with conn: move(conn, target, amount) move(conn, source, -amount)Die Überweisung von 150 € wurde abgelehnt. Maria: 100 € Jonas: 50 €- Das erste
UPDATEbeginnt eine Transaktion: Die Änderungen sind noch nicht endgültig. - Wenn der Block
with conn:ohne Fehler endet, ruft erconn.commit()auf und speichert alles auf einmal. - Wenn darin eine Exception auftritt, ruft er
conn.rollback()auf und macht alles rückgängig. Die Exception geht weiter nach oben, damit auch der Aufrufer davon erfährt. open_accountsruft am Endecommit()auf. Ohne das wäre das Anlegen der Konten in derselben Transaktion, undrollback()würde auch das rückgängig machen.
Genauer, für Neugierige
- Die Exception für die fehlgeschlagene Abfrage ist ein
IntegrityErrormit der MeldungCHECK constraint failed: no_debt.no_debtist der Name der Bedingung: In der Tabelle stehtbalance INT CONSTRAINT no_debt CHECK (balance >= 0).
- Datenbanken versprechen bei Transaktionen vier Dinge, bekannt als ACID: alles oder nichts (Atomarität), die Daten bleiben nach den Regeln der Tabelle korrekt (Konsistenz), gleichzeitig laufende Transaktionen stören sich nicht gegenseitig (Isolation), und was gespeichert ist, geht nicht verloren, auch wenn der Strom ausfällt (Dauerhaftigkeit).
Die Idee
ORM: Objekte statt Zeilen
sqlalchemy), und Webprojekte mit Django benutzen das eingebaute ORM von Django.from sqlalchemy import String, create_engine, selectfrom sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column
class Base(DeclarativeBase): pass
class Product(Base): __tablename__ = "products"
id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(50)) price: Mapped[float]- Eine Unterklasse von
Base(ausDeclarativeBase) ist eine Tabelle, und__tablename__gibt ihren Namen an. - Jedes Feld mit dem Typ
Mapped[...]ist eine Spalte.primary_key=Trueist der Primärschlüssel: SQLAlchemy lässt die Datenbank ihm einen Wert geben. mapped_columnlegt Details der Spalte fest: hierString(50). Ein Feld nur mitMapped[float]wird auch zu einer Spalte.- Der Konstruktor wird für dich erzeugt:
Product(name="Kaffee", price=7.50).
Speichern und Lesen gehen über die Session:
engine = create_engine("sqlite://")Base.metadata.create_all(engine)
with Session(engine) as session: session.add(Product(name="Kaffee", price=7.50)) session.add(Product(name="Tee", price=3.20)) session.commit()
cheap = session.scalars(select(Product).where(Product.price < 5).order_by(Product.price)) for p in cheap: print(f"{p.name}: {p.price} €")Tee: 3.2 €create_engine("sqlite://") sagt, welche Datenbank: hier SQLite im Speicher. create_all legt die Tabellen an. session.add fügt das Objekt hinzu, und commit() speichert es in einer Transaktion, wie bei sqlite3. Die Abfrage wird mit Code gebaut: select(Product).where(Product.price < 5) spricht von der Klasse und ihrem Feld price, nicht von der Tabelle. session.scalars gibt fertige Objekte zurück.
SQLAlchemy ist ein Paket, und die Prüfung der Website hat es nicht, deshalb läuft dieser Code hier nicht. Die Ausgabe stammt aus einem Lauf mit installiertem SQLAlchemy 2.1 (pip install sqlalchemy).
Genauer, für Neugierige
- Eine Abfrage mit
selectist Code, deshalb lassen sich die Bedingungen leicht aus der Auswahl des Nutzers zusammensetzen. Eine Funktion, die nach dem Namen sucht:
def named(session, name): query = select(Product).where(Product.name == name) return session.scalars(query).all()- Eine Transaktion, die bei einem Fehler rückgängig gemacht wird:
session.begin()in einemwithspeichert am Ende alles und macht bei einer Exception alles rückgängig:
with Session(engine) as session, session.begin(): session.add(Product(name="Schokolade", price=2.40)) # weitere Änderungen: bei einer Exception wird alles rückgängig gemacht, sonst wird alles gespeichert- Mit
create_engine(address, echo=True)gibt SQLAlchemy das SQL aus, das es schickt. So siehst du, dass darunter weiterhin SQL steckt. - In älterem Code siehst du
session.query(Product).filter(...). Das ist der Stil von SQLAlchemy 1.x, undselect(...)gibt es ab 2.0.
Übung
Ein Shop mit Datenbank
sqlite3 und der Datenbank ":memory:".1. Katalog. Lege eine Tabelle products an und die Funktionen add, set_price, remove und show (list ist der Name eines eingebauten Typs), alle außer show mit ?. Füge Kaffee (7,50 €), Tee (3,20 €) und Schokolade (2,40 €) hinzu, gib sie aus, ändere den Preis des Kaffees auf 6,90 €, lösche die Schokolade und gib sie wieder aus:1. Kaffee: 7.5 €2. Tee: 3.2 €3. Schokolade: 2.4 €---1. Kaffee: 6.9 €2. Tee: 3.2 €find(conn, text) findet alle Produkte, deren Name den Text enthält, ohne auf Groß- und Kleinschreibung zu achten. LOWER ändert in SQLite nur die Buchstaben A bis Z, deshalb bleibt É, wie es ist. Gib der Datenbank eine eigene Funktion: conn.create_function("lower_text", 1, str.lower), dann WHERE lower_text(name) LIKE ? mit dem Wert f"%{text.lower()}%". Probiere „KA“, „éclair“ und einen Injection-Versuch:Suche nach „KA“: Kaffee: 7.5 € Kakao: 4.8 €Suche nach „éclair“: Éclair: 2.4 €Suche nach „' OR '1'='1“: keine Produkte gefundentransfer(conn, source, target, amount) zieht zuerst von source ab und fügt dann bei target hinzu. Wenn cursor.rowcount 0 ist (kein solches Konto), wirf ValueError(f"Kein Konto {account_id}"). Alles ist in with conn:. Überweise 30 € von 1 an 2, dann 40 € von 1 an das Konto 99, das es nicht gibt:Überwiesen: 30 € von 1 an 2Abgelehnt: Kein Konto 99 Maria: 70 € Jonas: 80 €Die zweite Überweisung hat Maria 40 € abgezogen, bevor sie fehlschlug, aber with conn (mit rollback()) hat die 40 € zurückgebucht.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Daten in Dateien · Listen: append, remove und sort · Lambdas · dataclass, match und Muster
- Geht weiter in: Injektionen: wenn Daten zum Befehl werden · Monolith, modularer Monolith, Services · Transactional Outbox · Eine Transaktion über Services hinweg (Saga)
- Aufgaben zum Thema: Programme in der echten Welt