Files
it-know-how/datenbanken/ORM.md
T
2026-03-27 12:58:01 +01:00

476 lines
13 KiB
Markdown
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 1. Grundidee: Was ist ein ORM?
ORM steht für **Object-Relational Mapping**.
Im Kontext von Python und Datenbankmodellen bedeutet das:
> Ein ORM ist eine Bibliothek, die es dir erlaubt, mit einer relationalen Datenbank (z.B. PostgreSQL, MySQL, SQLite) zu arbeiten, indem du **Python-Klassen und -Objekte** verwendest statt SQL-Strings.
Anstatt also SQL wie:
```sql
SELECT * FROM konto WHERE id = 1;
```
zu schreiben, machst du in Python z.B.:
```python
konto = session.get(Konto, 1)
```
Das ORM übersetzt deine Python-Anweisung im Hintergrund in SQL, führt sie aus und gibt dir Python-Objekte zurück.
---
## 2. Wichtige Begriffe und Bausteine (intuitiv erklärt)
Nehmen wir ein vereinfachtes Bank-Beispiel:
- **Kunde** (Customer)
- **Konto** (Account)
- **Transaktion** (Transaction)
In einer relationalen Datenbank wären das Tabellen:
- `kunde`
- `konto`
- `transaktion`
Mit einem ORM legst du dafür **Modelle** als Python-Klassen an:
```python
class Kunde:
...
class Konto:
...
class Transaktion:
...
```
Die wichtigsten Bausteine:
- **Modell / Entity**:
Eine Klasse, die eine Tabelle repräsentiert (z.B. `Konto` → Tabelle `konto`).
- **Felder / Spalten**:
Attribute der Klasse (z.B. `saldo`, `kunde_id`).
- **Beziehungen**:
- „Ein Kunde hat viele Konten“ (One-to-Many)
- „Ein Konto gehört zu genau einem Kunden“ (Many-to-One)
- **Session / EntityManager**:
Ein Objekt, über das du mit der Datenbank sprichst (z.B. Datensätze abfragen, speichern, löschen).
---
## 3. Abgrenzung zu verwandten Begriffen
### 3.1 ORM vs. direkte SQL-Nutzung
**Direkte SQL-Nutzung:**
- Du schreibst selbst SQL-Statements.
- Du verwendest z.B. `psycopg2` (PostgreSQL) oder `sqlite3` (Standard in Python).
- Du bekommst Resultate als Tupel / Dictionaries zurück und baust selbst deine Objekte.
**Mit ORM:**
- Du arbeitest mit Python-Klassen und -Objekten.
- Das ORM generiert und führt SQL für dich aus.
- Du bekommst direkt Instanzen deiner Klassen.
```python
# Direkte SQL-Nutzung
cursor.execute("SELECT id, name FROM kunde WHERE id = %s", (1,))
row = cursor.fetchone()
kunde = {"id": row[0], "name": row[1]}
# ORM
kunde = session.get(Kunde, 1)
print(kunde.name)
```
ORM nimmt dir also den „manuellen“ Teil des Mappings ab.
---
### 3.2 ORM vs. Datenbankmodellierung (ER-Modell)
- **Datenbankmodellierung** (ER-Diagramme, Normalisierung usw.) ist der Schritt, in dem du **konzipierst**, wie deine Daten strukturiert sind.
- **ORM** ist ein **Werkzeug**, um mit dieser Struktur in Python zu arbeiten.
Du kannst ein gutes Datenbankmodell haben **mit oder ohne** ORM.
ORM ersetzt nicht das Nachdenken über ein sinnvolles Datenmodell.
---
### 3.3 ORM vs. Migrations-Tools
- **Migrations-Tools** (z.B. Alembic für SQLAlchemy, Django-Migrations) verwalten die **Versionierung und Änderungen** am Schema (z.B. neue Spalte, geänderte Spalte).
- Ein **ORM** arbeitet primär zur **Laufzeit** mit Daten; viele ORMs bringen allerdings Tools mit, um aus den Modellen Migrationen zu erzeugen.
---
### 3.4 ORM vs. Query-Builder
- **Query-Builder**: Bibliothek, die das Schreiben von SQL erleichtert, aber nicht unbedingt Objekte modelliert (z.B. SQLAlchemy Core).
- **ORM**: baut auf einem Query-Builder auf und bringt zusätzlich:
- Klassen→Tabellen-Mapping
- Objekte→Zeilen-Mapping
- Beziehungen als Attribute
---
### 3.5 ORM vs. [[Pydantic]] / Dataclasses
- **[[Pydantic]] / `dataclasses`**: Modellieren **In-Memory-Daten** (z.B. Input aus einer API), inkl. Validation und Typen.
- **ORM**: Modelliert **persistente Daten** in einer relationalen Datenbank.
Man kombiniert das oft:
- [[Pydantic]]-Modelle für API-Ein-/Ausgaben
- ORM-Modelle für Speicherung in der Datenbank
---
## 4. Welche Probleme löst ein ORM?
### 4.1 „Impedance Mismatch“: Objekte vs. Tabellen
Python arbeitet mit **Objekten**:
```python
kunde.name
konto.saldo
konto.kunde.name
```
Datenbanken arbeiten mit **Tabellen**, **Zeilen** und **Fremdschlüsseln**.
ORMs „übersetzen“ zwischen diesen Welten.
Beispiel:
- `konto.kunde` ist in Python einfach ein Attribut.
- Intern bedeutet das: Joins über `konto.kunde_id = kunde.id`.
---
### 4.2 Weniger Boilerplate, mehr Fokus auf Fachlogik
Ohne ORM schreibst du viel repetiven Code:
- SQL-Strings
- Parameter-Bindung
- Zeilen in Python-Objekte umwandeln
Mit ORM schreibst du einmal deine Modelle und konzentrierst dich dann auf:
- „Neue Transaktion buchen“
- „Saldo prüfen“
- „Kontoauszug generieren“
Anstatt: „Wie formuliere ich nochmal den SQL-Join...“.
---
### 4.3 Typisierung, Autocomplete, Konsistenz
Durch Python-Klassen hast du:
- **Typhinweise** (z.B. `saldo: float`)
- Unterstützung durch IDE (Autocomplete, Refactoring)
- Klar definierte Beziehungen (z.B. `konto.kunde`)
---
### 4.4 Datenbank-Agnostik
Viele ORMs unterstützen mehrere Datenbanken:
- Du kannst z.B. in Tests SQLite verwenden,
- in Produktion PostgreSQL,
- ohne deinen ganzen Code umzuschreiben (meist nur Konfiguration).
---
### 4.5 Testbarkeit
Du kannst:
- einfacher Unit-Tests schreiben, indem du In-Memory-SQLite nutzt,
- oder sogar nur mit „Fake-Repositories“ arbeitest, die sich wie das ORM verhalten.
---
## 5. Welche Herausforderungen bringen ORMs mit sich?
### 5.1 Performance-Fallen („N+1-Problem“, zu viele Queries)
Beispiel: Du lädst 100 Konten und für jedes Konto den zugehörigen Kunden:
Naiv:
```python
konten = session.query(Konto).all()
for konto in konten:
print(konto.kunde.name)
```
Kann bedeuten:
- 1 Query für alle Konten
- + 100 Queries für jeden einzelnen Kunden
→ Insgesamt 101 Queries (= N+1-Problem).
Mit ORM musst du lernen, wie man:
- **Eager Loading** / `join` / `selectinload` etc. nutzt,
- um nur **12 Queries** zu erzeugen.
---
### 5.2 Man darf SQL nicht völlig „vergessen“
ORM nimmt viel Arbeit ab, aber:
- du solltest **verstehen**, was für SQL generiert wird,
- und Grundbegriffe wie `JOIN`, `WHERE`, `GROUP BY`, Indexe kennen.
Ohne grundlegende SQL-Kenntnisse tappst du schnell in Performance-Probleme.
---
### 5.3 Komplexe Abfragen
Für einfache Abfragen ist ORM sehr angenehm.
Bei sehr komplexen Auswertungen (z.B. Bank-Reporting mit vielen Aggregationen, Window-Funktionen) kann:
- die ORM-Syntax unübersichtlich werden,
- rohe SQL-Statements manchmal klarer und effizienter sein.
Viele ORMs erlauben gemischt:
- 90 % ORM,
- 10 % direktes SQL für Spezialfälle.
---
### 5.4 Migrationen und Schema-Änderungen
- Du musst im Blick behalten: **Modelle in Python** und **Schema in Datenbank** dürfen nicht auseinanderlaufen.
- Migrations-Tools sind notwendig, aber auch ein eigener Lernbereich.
---
### 5.5 „Lock-In“ und Komplexität
- Große ORMs (z.B. SQLAlchemy ORM, Django ORM) sind mächtig, aber haben Lernkurve.
- Wenn du einmal dein ganzes Projekt auf ein bestimmtes ORM gebaut hast, ist ein Wechsel auf ein anderes ORM oder „plain SQL“ aufwendig.
---
## 6. Praxisnahe Beispiele mit Python (SQLAlchemy ORM)
### 6.1 Setup: einfache Bank-Domain mit SQLAlchemy
Installation:
```bash
pip install sqlalchemy
```
Ein minimaler Aufbau mit `Kunde` und `Konto`:
```python
from sqlalchemy import (
create_engine, Column, Integer, String, Float, ForeignKey
)
from sqlalchemy.orm import (
declarative_base, relationship, Session
)
# Basis-Klasse für alle ORM-Modelle
Base = declarative_base()
class Kunde(Base):
__tablename__ = "kunde"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
# Beziehung: Ein Kunde hat viele Konten
konten = relationship("Konto", back_populates="kunde")
def __repr__(self):
return f"<Kunde id={self.id} name={self.name!r}>"
class Konto(Base):
__tablename__ = "konto"
id = Column(Integer, primary_key=True)
kontonummer = Column(String, unique=True, nullable=False)
saldo = Column(Float, default=0.0)
kunde_id = Column(Integer, ForeignKey("kunde.id"), nullable=False)
# Beziehung: Konto gehört zu genau einem Kunden
kunde = relationship("Kunde", back_populates="konten")
def __repr__(self):
return f"<Konto id={self.id} konto={self.kontonummer!r} saldo={self.saldo}>"
# Engine und Session einrichten (hier: SQLite-Datei)
engine = create_engine("sqlite:///bank.db", echo=True) # echo=True zeigt SQL an
Base.metadata.create_all(engine) # Tabellen aus den Modellen erzeugen
# Session erstellen
session = Session(engine)
# Beispiel-Daten anlegen
kunde = Kunde(name="Max Mustermann")
konto1 = Konto(kontonummer="DE123", saldo=1000.0, kunde=kunde)
konto2 = Konto(kontonummer="DE456", saldo=2500.0, kunde=kunde)
session.add(kunde) # reicht, da konten via Beziehung mit hinzugefügt werden
session.commit()
# Abfragen
alle_kunden = session.query(Kunde).all()
print(alle_kunden)
# Zugriff auf Beziehungen
for k in alle_kunden:
print(f"Kunde: {k.name}")
for konto in k.konten:
print(f" Konto {konto.kontonummer}, Saldo: {konto.saldo}")
# Einzelnes Konto laden und zugehörigen Kunden ausgeben
konto = session.query(Konto).filter_by(kontonummer="DE123").one()
print(konto.kunde.name)
session.close()
```
Wichtige Punkte im Beispiel:
- `Kunde` und `Konto` sind Python-Klassen, die Tabellen repräsentieren.
- Die Beziehung `Kunde.konten` und `Konto.kunde` erlaubt dir objektorientierten Zugriff.
- Das ORM generiert automatisch SQL (sichtbar durch `echo=True`).
---
### 6.2 Gleiche Logik mit „plain SQL“ (zum Vergleich)
Zum Vergleich ein stark vereinfachendes Beispiel mit `sqlite3`:
```python
import sqlite3
conn = sqlite3.connect("bank_plain.db")
cursor = conn.cursor()
# Tabellen anlegen
cursor.execute("""
CREATE TABLE IF NOT EXISTS kunde (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS konto (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kontonummer TEXT NOT NULL UNIQUE,
saldo REAL DEFAULT 0.0,
kunde_id INTEGER NOT NULL,
FOREIGN KEY(kunde_id) REFERENCES kunde(id)
)
""")
# Kunde anlegen
cursor.execute("INSERT INTO kunde (name) VALUES (?)", ("Max Mustermann",))
kunde_id = cursor.lastrowid
# Konten anlegen
cursor.execute(
"INSERT INTO konto (kontonummer, saldo, kunde_id) VALUES (?, ?, ?)",
("DE123", 1000.0, kunde_id)
)
cursor.execute(
"INSERT INTO konto (kontonummer, saldo, kunde_id) VALUES (?, ?, ?)",
("DE456", 2500.0, kunde_id)
)
conn.commit()
# Kunden mit Konten abfragen
cursor.execute("""
SELECT k.name, ko.kontonummer, ko.saldo
FROM kunde k
JOIN konto ko ON ko.kunde_id = k.id
""")
rows = cursor.fetchall()
for row in rows:
name, kontonummer, saldo = row
print(name, kontonummer, saldo)
conn.close()
```
Hier musst du:
- SQL selbst schreiben,
- `rows` manuell in Python-Strukturen übersetzen.
Mit ORM übernimmt der ORM-Layer:
- das Mapping,
- das Auflösen der Beziehungen,
- einiges an Boilerplate.
---
## 7. Speziell im Bank-/Finanz-Kontext
Gerade im Bankumfeld ist ORM attraktiv, weil:
- es viele **fachliche Entitäten** gibt (Konto, Kunde, Vertrag, Produkt, Transaktion, Wertpapier, Order, …),
- du komplexe **Geschäftslogik** in Python implementieren willst,
- und dabei konsistent auf persistente Daten zugreifen musst.
Typische Muster:
- **Transaktions-Logs** (jede Buchung als eigener Datensatz),
- **Audit-Trails** (wer hat wann was geändert),
- **Referenzdaten** (Währungen, Länder, Produktstammdaten).
ORMs helfen:
- diese Entitäten sauber als Klassen zu modellieren,
- Beziehungen explizit zu machen,
- und fachliche Operationen wie „Buchung durchführen“ objektorientiert abzubilden.
---
## 8. Wann lohnt sich ein ORM und wann eher nicht?
**Sinnvoll:**
- Du baust eine mittlere bis große Anwendung mit vielen Entitäten.
- Du willst langfristig Wartbarkeit, Tests und Refactoring erleichtern.
- Du hast Standard-CRUD-Operationen (create/read/update/delete) und „normale“ fachliche Logik.
**Eher nicht sinnvoll:**
- Kleine Skripte, die nur ein paar einfache SQL-Abfragen ausführen.
- Hochoptimierte Reporting- oder Analytics-Abfragen, die ohnehin spezielle SQL-Funktionen nutzen.
- Wenn dein Team sehr SQL-affin ist und kein Interesse an der zusätzlichen Abstraktionsschicht hat.
---
## 9. Zusammenfassung
- **ORM** ist eine Technik (und meist eine Bibliothek), um **relationalen Datenbanken** über **Objekte** und **Klassen** zu begegnen.
- Sie löst das Mapping zwischen Tabellen/Zeilen und Klassen/Objekten und reduziert manuellen SQL-Boilerplate.
- Sie bietet Vorteile in Wartbarkeit, Typisierung, Testbarkeit gerade bei komplexeren Domänen wie Banking.
- Gleichzeitig bringt ein ORM neue Herausforderungen mit sich: Performance-Fallen, Lernaufwand, Verständnis der generierten SQL-Abfragen bleibt wichtig.
- In Python sind gängige ORMs: **[[SQLAlchemy]] ORM**, **Django ORM**, (in moderneren Stacks oft in Kombination mit [[Pydantic]], [[FastAPI]] etc.).