init at work
This commit is contained in:
Executable
+475
@@ -0,0 +1,475 @@
|
||||
## 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 **1–2 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.).
|
||||
|
||||
Reference in New Issue
Block a user