Files
work/notes/nextcloud/Notes/IT-Know-How/datenbanken/ORM.md
T

13 KiB
Executable File
Raw Blame History

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:

SELECT * FROM konto WHERE id = 1;

zu schreiben, machst du in Python z.B.:

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:

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.
# 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:

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:

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:

pip install sqlalchemy

Ein minimaler Aufbau mit Kunde und Konto:

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:

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.).