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