diff --git a/Vim Cheatsheet.md b/Vim Cheatsheet.md new file mode 100755 index 0000000..b26fb32 --- /dev/null +++ b/Vim Cheatsheet.md @@ -0,0 +1,214 @@ +## Modi +```text +i In den Insert-Modus (vor Cursor) +I Insert am Zeilenanfang +a Insert nach Cursor +A Insert am Zeilenende +o Neue Zeile unterhalb + Insert +O Neue Zeile oberhalb + Insert + + Zurück in Normalmodus + +v Visueller Modus (Zeichenweise) +V Visueller Modus (Zeilenweise) + Visueller Blockmodus +``` +## Navigation (Normalmodus) +```text +h j k l Links / runter / hoch / rechts + +0 Zeilenanfang +^ Erstes nicht-leer Zeichen +$ Zeilenende + +w Zum nächsten Wortanfang +b Zum vorherigen Wortanfang +e Zum Wortende +W B E Wie oben, aber Worte durch Leerzeichen getrennt + +gg Erste Zeile +G Letzte Zeile +ngg / nG Zu Zeile n + +H Oberer Bildschirmrand +M Mittlerer Bildschirmrand +L Unterer Bildschirmrand + +Ctrl-u Halbe Seite hoch +Ctrl-d Halbe Seite runter +Ctrl-b Seite hoch +Ctrl-f Seite runter +``` +## Basis-Bearbeitung +```text +x Zeichen unter Cursor löschen +X Zeichen links vom Cursor löschen +r Zeichen ersetzen + +dd Zeile löschen (cut) +D Bis zum Zeilenende löschen +cc Zeile ändern (delete + insert) +C Bis zum Zeilenende ändern + +yy Zeile kopieren +Y Alias für yy +p Einfügen NACH Cursor/Zeile +P Einfügen VOR Cursor/Zeile + +u Undo +Ctrl-r Redo + +J Nächste Zeile an aktuelle anhängen +>> Zeile einrücken (indent) +<< Zeile ausrücken (unindent) +``` +## Mit Counts & Bewegungen +Viele Befehle funktionieren mit einer Anzahl (count) und einer Bewegung: +```text +3w 3 Wörter vor +5j 5 Zeilen runter + +d3w 3 Wörter löschen +c$ Bis Zeilenende ändern +y0 Bis Zeilenanfang kopieren +``` +## Textobjekte (im Normal- / Visuellen Modus) +```text +aw A word (inkl. Leerzeichen) +iw Inner word +as A sentence +is Inner sentence +ap A paragraph +ip Inner paragraph + +a" / i" Text in Anführungszeichen +a' / i' Text in einfachen Quotes +a) / i) Text in Klammern (usw. für {}, [], <>) + +Beispiele: +ci" Inhalt von "..." ändern +da( Klammerausdruck inklusive Klammern löschen +viw Wort unter Cursor markieren +``` +## Suchen & Ersetzen +```text +/word Vorwärts nach „word“ suchen +?word Rückwärts suchen +n Nächster Treffer +N Vorheriger Treffer + +* Wort unter Cursor vorwärts suchen +# Wort unter Cursor rückwärts suchen + +:%s/alt/neu/g In gesamter Datei ersetzen +:%s/alt/neu/gc Mit Bestätigung +:.,$s/alt/neu/g Von aktueller Zeile bis Ende +:10,20s/alt/neu/g In Zeilen 10–20 +``` +## Dateien, Buffers, Splits & Tabs +```text +:e datei Datei öffnen +:w Speichern +:w name Unter anderem Namen speichern +:q Beenden +:q! Beenden ohne Speichern +:wq / :x Speichern und beenden +:qa Alle Fenster schließen +:qa! Alle schließen ohne Speichern + +" Buffer (Dateien im Speicher) +:ls Buffer-Liste +:buffer n Buffer n öffnen +:bnext / :bn Nächster Buffer +:bprev / :bp Vorheriger Buffer +:bdelete Buffer schließen + +" Splits +:split datei Horizontaler Split +:vsplit datei Vertikaler Split +Ctrl-w s Split horizontal +Ctrl-w v Split vertikal +Ctrl-w w Zum nächsten Split +Ctrl-w h/j/k/l Split wechseln +Ctrl-w q Split schließen + +" Tabs +:tabnew Neuer Tab +:tabclose Tab schließen +:tabnext / :tn Nächster Tab +:tabprev / :tp Vorheriger Tab +``` +## Visual Mode Aktionen +```text +v / V / Ctrl-v Auswahl starten +y Auswahl kopieren +d Auswahl löschen +c Auswahl ändern + +> Einrücken +< Ausrücken += Auto-Indent (z.B. für Code) + +: Befehl auf Auswahl anwenden + (Bereich wird automatisch eingetragen) +``` +## Makros & Wiederholung +```text +. Letzte Änderung wiederholen + +q Macro in Register a aufzeichnen + (z.B. qa) +... Aktionen ausführen +q Aufzeichnung beenden + +@a Macro a ausführen +3@a Macro 3x ausführen +@@ Letztes Macro wiederholen +``` +## Register +```text +"0 Zuletzt kopiertes (y) ohne delete +"1–"9 Verlaufsregister +"" Default-Register + +"+ System-Clipboard (kopieren/einfügen mit OS) +"* Auswahl-Clipboard (unter Linux/X11) + +Beispiele: +"ayw Wort in Register a kopieren +"ap Inhalt von Register a einfügen +"+y In System-Clipboard kopieren +"+p Aus System-Clipboard einfügen +``` +## Kommandomodus (Ex-Befehle) +```text +:!cmd Externen Befehl ausführen +:r !cmd Ausgabe von cmd einfügen +:r datei Datei unter Cursor-Zeile einfügen + +:set nu Zeilennummern an +:set nonu Zeilennummern aus +:set relativenumber Relative Nummern +:set tabstop=4 shiftwidth=4 expandtab +``` +## Einfache .vimrc-Beispiele +```vim +" Zeilennummern +set number +set relativenumber + +" Einrückung +set tabstop=4 +set shiftwidth=4 +set expandtab +set smartindent + +" Suche +set ignorecase +set smartcase +set hlsearch +set incsearch + +" Maus +set mouse=a +``` diff --git a/datenbanken/Identity Columns.md b/datenbanken/Identity Columns.md new file mode 100755 index 0000000..bcffa01 --- /dev/null +++ b/datenbanken/Identity Columns.md @@ -0,0 +1,497 @@ +#datenbank + +➡️ [[#Zusammenfassung]] + +--- +## 1. Grundidee: Was ist eine Identity Column? + +Stell dir eine Tabelle „Kunden“ vor. Jeder Kunde soll eine eindeutige Nummer bekommen: + +- Kunde 1 +- Kunde 2 +- Kunde 3 +- … + +Du willst diese Nummern **nicht selbst vergeben**, sondern die Datenbank soll das **automatisch** machen, wenn du einen neuen Datensatz einfügst. + +Genau das ist eine **Identity Column**: + +> Eine Identity Column ist eine Spalte, deren Wert von der Datenbank automatisch erzeugt wird, meist als laufende Nummer (1, 2, 3, …). Sie wird oft als Primärschlüssel benutzt. + +Typische Eigenschaften: + +- numerisch (z. B. `INT`, `BIGINT`) +- beim `INSERT` wird kein Wert angegeben – die DB füllt ihn selbst +- der Wert ist pro Tabelle eindeutig +- er steigt (meist) monoton an (1,2,3,…), kann aber Lücken haben + +--- + +## 2. Ein einfaches Beispiel + +Beispiel in **SQL Server**: + +```sql +CREATE TABLE Kunde ( + KundeID INT IDENTITY(1,1) PRIMARY KEY, -- Start bei 1, Inkrement 1 + Name NVARCHAR(100) NOT NULL, + Email NVARCHAR(200) NOT NULL +); + +-- Einfügen ohne KundeID anzugeben: +INSERT INTO Kunde (Name, Email) +VALUES ('Anna Beispiel', 'anna@example.com'), + ('Max Muster', 'max@muster.de'); + +-- Auslesen: +SELECT * FROM Kunde; +``` + +Ergebnis (vereinfacht): + +```text +KundeID | Name | Email +--------+----------------+------------------- +1 | Anna Beispiel | anna@example.com +2 | Max Muster | max@muster.de +``` + +Du hast `KundeID` nie selbst gesetzt – die Datenbank hat sie erzeugt. + +--- + +## 3. Abgrenzung zu verwandten Begriffen + +### 3.1 Identity Column vs. Primärschlüssel + +- **Primärschlüssel (Primary Key)**: + Konzept: eine oder mehrere Spalten, die **eindeutig** einen Datensatz identifizieren. +- **Identity Column**: + Mechanismus: eine Spalte, deren Werte automatisch generiert werden. + +Eine Identity Column **kann** der Primärschlüssel sein, muss aber nicht. Man kann auch: + +- Identity Column + separater fachlicher Schlüssel (z. B. Kundennummer im ERP) +- Primärschlüssel auf einer anderen Spalte (z. B. E-Mail) ohne Identity Column + +In der Praxis: Sehr häufig ist die Identity Column = Primärschlüssel. + +--- + +### 3.2 Identity Column vs. Auto-Increment + +Viele Systeme benutzen unterschiedliche Begriffe: + +- SQL Server: `IDENTITY` +- PostgreSQL: `GENERATED AS IDENTITY` (früher `SERIAL`) +- MySQL: `AUTO_INCREMENT` +- Oracle: Identity-Spalten oder separate `SEQUENCE` + +**Inhaltlich** ist „Identity Column“ oft gleichbedeutend mit „Auto-Increment-Spalte“: eine automatisch hochzählende Spalte. + +PostgreSQL-Beispiel: + +```sql +CREATE TABLE kunde ( + kunde_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name TEXT NOT NULL, + email TEXT NOT NULL +); +``` + +MySQL-Beispiel: + +```sql +CREATE TABLE kunde ( + kunde_id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(200) NOT NULL +); +``` + +--- + +### 3.3 Identity Column vs. Sequence + +**Sequence (Sequenz)** ist ein **eigenständiges Datenbankobjekt**, das Zahlen generiert, etwa so: + +```sql +CREATE SEQUENCE kunde_seq START WITH 1 INCREMENT BY 1; + +SELECT nextval('kunde_seq'); -- gibt 1 zurück +SELECT nextval('kunde_seq'); -- gibt 2 zurück +``` + +Eine Identity Column ist oft intern an eine Sequence gekoppelt. +Unterschied: + +- Sequence: unabhängig von Tabellen, du kannst sie überall verwenden +- Identity Column: an genau eine Spalte gebunden, automatischer Einsatz bei `INSERT` + +In einigen DB-Systemen (z. B. Oracle, PostgreSQL) sind Identity Columns technisch eine bequeme Hülle um eine Sequence. + +--- + +### 3.4 Identity Column vs. natürlicher/fachlicher Schlüssel + +- **Natürlicher/fachlicher Schlüssel**: basiert auf echten Geschäfts-Daten + Beispiele: + - E-Mail-Adresse als eindeutiger Schlüssel für Benutzer + - ISBN für Bücher + - Personalnummer aus dem HR-System + +- **Surrogate Key** (hier passt die Identity Column): technischer, künstlicher Schlüssel, ohne fachliche Bedeutung. + +Identity Columns sind **Surrogate Keys**: +- `KundeID = 42` sagt fachlich nichts über den Kunden aus +- sie existiert nur, um Zeilen eindeutig zu identifizieren + +In vielen Projekten arbeitet man mit: + +- Identity Column als Primärschlüssel +- dazu Unique-Constraints auf fachlichen Spalten (z. B. `Email UNIQUE`) + +--- + +### 3.5 Identity Column vs. UUID / GUID + +Statt einer Identity Column (INT) kann man auch **UUIDs** (z. B. `UUID`, `uniqueidentifier`) als Primärschlüssel verwenden. + +Unterschiede: + +- **Identity (INT/BIGINT)**: + - kleiner, effizienter Index + - gut lesbar (`KundeID = 123`) + - nicht global eindeutig über mehrere Systeme, nur innerhalb der Tabelle + - lässt leicht Rückschlüsse auf Anzahl/Abfolge (z. B. „wir sind bei Kunde 10.000“) + +- **UUID**: + - sehr große, „zufällig“ wirkende Zeichenfolge + - global eindeutig (sehr hoch wahrscheinlich) + - schwerer zu merken/lesen + - Indexe sind meist größer, Einfügen kann teurer sein + +Z. B. in PostgreSQL: + +```sql +CREATE TABLE kunde ( + kunde_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL +); +``` + +Identity Column und UUID sind also alternative Strategien zur ID-Erzeugung. + +--- + +## 4. Welche Probleme löst eine Identity Column? + +### 4.1 Eindeutige Identifikation von Datensätzen + +Jede Zeile bekommt automatisch eine eindeutige ID: + +- kein Risiko, dass zwei Datensätze „versehentlich“ dieselbe ID haben +- Voraussetzung für saubere Joins, Fremdschlüssel, Referenzen + +Beispiel: Fremdschlüssel von Auftrag auf Kunde: + +```sql +CREATE TABLE Kunde ( + KundeID INT IDENTITY(1,1) PRIMARY KEY, + Name NVARCHAR(100) NOT NULL +); + +CREATE TABLE Auftrag ( + AuftragID INT IDENTITY(1,1) PRIMARY KEY, + KundeID INT NOT NULL, + Betrag DECIMAL(10,2) NOT NULL, + FOREIGN KEY (KundeID) REFERENCES Kunde(KundeID) +); +``` + +Beim Anlegen eines Auftrags verweist `Auftrag.KundeID` auf `Kunde.KundeID`. + +--- + +### 4.2 Vereinfachung beim Einfügen von Daten + +Ohne Identity Column müsstest du bei jedem `INSERT` einen neuen eindeutigen Wert berechnen und eintragen – in Mehrbenutzer-Umgebungen ist das fehleranfällig. + +Mit Identity Column: + +```sql +INSERT INTO Kunde (Name) VALUES ('Lisa Test'); -- ID macht die DB +``` + +Du musst dir keine Gedanken machen über: + +- die nächste freie Nummer +- Race Conditions (zwei Nutzer generieren zufällig dieselbe ID) +- Sperren von Tabellen, etc. + +--- + +### 4.3 Unterstützung von Mehrbenutzerbetrieb + +In einer Datenbank arbeiten meist viele Nutzer oder Prozesse parallel. Identity Columns sind so implementiert, dass: + +- gleichzeitig eingefügte Zeilen **ohne Kollision** IDs bekommen +- du dich nicht um Synchronisation kümmern musst + +Die Datenbank regelt intern: + +- Sperren der Sequenz +- Transaktionssicherheit + +--- + +### 4.4 Gute Performance und einfache Indizierung + +Numerische Identity-Spalten: + +- sind kompakt (z. B. 4 oder 8 Byte) +- lassen sich effizient indexieren +- wachsen meist monoton -> B-Tree-Indizes funktionieren sehr performant + +Im Vergleich: +- komplexe Primärschlüssel aus mehreren Textspalten sind größer, langsamer +- Identity-Spalte als „technischer Schlüssel“ macht viele Operationen schneller + +--- + +## 5. Herausforderungen und typische Fallstricke + +### 5.1 Lücken in der Nummernfolge + +Anfänger erwarten oft: „Wenn ich einen Datensatz lösche oder eine Transaktion zurückrolle, füllt die DB die Nummer wieder auf.“ + +Das **passiert nicht** (und sollte auch nicht passieren). + +Gründe für Lücken: + +- Transaktion wird zurückgerollt nach dem Erzeugen einer ID +- Datensatz mit ID 10 wird gelöscht +- Parallel eingefügte Datensätze + +Beispiel: +- du fügst einen Datensatz ein -> bekommt ID 10 +- im gleichen Moment ein zweiter -> ID 11 +- deine Transaktion schlägt fehl -> Datensatz 10 existiert nicht +- Datensatz 11 bleibt erhalten + +Ergebnis: Es gibt jetzt mitunter keinen Datensatz mit ID 10. Das ist normal. + +**Wichtige Praxisregel**: +Eine Identity Column ist eine **technische ID**, keine „lückenlose Rechnungsnummer“, keine „fortlaufende Kundennummer“ im rechtlichen Sinn. + +Wenn du wirklich lückenlose Nummern brauchst (z. B. für Rechnungen), wird das typischerweise anders gelöst (spezialisierte Logik, Sperren, eigene Tabellen). + +--- + +### 5.2 „ID = Reihenfolge“ ist gefährlich + +Menschen neigen dazu zu denken: + +> „ID 100 wurde nach ID 99 erstellt.“ + +In der Praxis ist das oft, aber **nicht garantiert**: + +- Backups/Restores +- Replikation +- Imports aus anderen Systemen +- unterschiedliche Identity-Strategien + +Besser: Wenn du die zeitliche Reihenfolge brauchst, verwende eine **Zeitstempel-Spalte**: + +```sql +CREATE TABLE Kunde ( + KundeID INT IDENTITY(1,1) PRIMARY KEY, + Name NVARCHAR(100), + CreatedAt DATETIME2 NOT NULL DEFAULT SYSDATETIME() +); +``` + +Dann sortierst du nach `CreatedAt`, nicht nach `KundeID`. + +--- + +### 5.3 Limits des Datentyps (Überlauf) + +Wenn du `INT` (32-Bit) verwendest, ist irgendwann Schluss (ca. 2,1 Milliarden positive Werte). Bei sehr großen Tabellen oder sehr intensiver Nutzung kann das relevant werden. + +Praktischer Tipp: + +- lieber direkt `BIGINT` für Identity-Spalten verwenden – die Grenze ist so hoch, dass du in normalen Anwendungen nicht anstößt. + +Beispiel: + +```sql +CREATE TABLE Bestellung ( + BestellungID BIGINT IDENTITY(1,1) PRIMARY KEY, + ... +); +``` + +--- + +### 5.4 Datenmigrationen, Importe, Reseeding + +Problemfälle: + +- du willst Daten von einem System ins andere migrieren +- dort gibt es schon Datensätze mit Identity-Werten +- beim Import dürfen keine IDs kollidieren + +Strategien: + +1. **Identity-Werte mit übernehmen** + In vielen DB-Systemen kann man temporär eigene Werte setzen: + + SQL Server: + ```sql + SET IDENTITY_INSERT Kunde ON; + + INSERT INTO Kunde (KundeID, Name, Email) + VALUES (1001, 'Imported User', 'import@example.com'); + + SET IDENTITY_INSERT Kunde OFF; + ``` + +2. **Nach dem Import die Identity „neu starten“ (reseeden)** + SQL Server: + ```sql + DBCC CHECKIDENT ('Kunde', RESEED, 2000); -- nächste ID = 2001 + ``` + +Diese Themen werden wichtig, wenn du Daten zwischen Systemen hin- und herschiebst. + +--- + +### 5.5 Verteilte Systeme / Sharding + +In modernen Architekturen gibt es manchmal: + +- mehrere Datenbankserver (Shards) +- die später zusammengeführt werden sollen + +Wenn jede Tabelle eine Identity-Spalte bei 1 beginnen lässt, kann es bei Merge-Vorgängen Kollisionen geben (z. B. `KundeID = 100` existiert auf zwei Servern mit unterschiedlichen Kunden). + +Lösungen: + +- pro Server unterschiedliche Startwerte und Inkremente: + - Server A: `IDENTITY(1, 3)` → 1,4,7,… + - Server B: `IDENTITY(2, 3)` → 2,5,8,… + - Server C: `IDENTITY(3, 3)` → 3,6,9,… +- oder Nutzung von UUIDs statt Identity-Spalten + +--- + +### 5.6 Informationsleck (Datenschutz, Sicherheit) + +Außen sichtbare IDs (z. B. in URLs) können Informationen verraten: + +- wenn dein Kunde eine URL wie `/bestellung/1000` sieht, kann er ahnen, dass es ~1000 Bestellungen gibt +- er kann versuchen, `/bestellung/999` aufzurufen (ID-Raten) + +Das ist kein Problem der Identity-Spalte an sich, sondern der Entscheidung, **diese ID außerhalb der Anwendung sichtbar** zu machen. + +Lösungen: + +- Zugriffskontrollen („zeige nur eigene Bestellungen“) +- andere, „nicht-erratbare“ IDs für externe Darstellung (z. B. UUIDs, Hashes) + +--- + +### 5.7 Portabilität zwischen Datenbanksystemen + +Jede Datenbank hat leicht andere Syntax: + +- SQL Server: `INT IDENTITY(1,1)` +- PostgreSQL: `GENERATED ALWAYS AS IDENTITY` +- MySQL: `AUTO_INCREMENT` +- Oracle: `GENERATED BY DEFAULT AS IDENTITY` oder Sequences + +Wenn du Wert auf **Portabilität** legst (dass dein Schema in mehreren DB-Systemen läuft), musst du darauf achten: + +- eher Standard-SQL (`GENERATED [ALWAYS|BY DEFAULT] AS IDENTITY`) +- oder ID-Generierung in der Anwendung selbst + +--- + +## 6. Praxisnahe Gesamtbeispiele + +### 6.1 Einfache Kunden- und Bestellverwaltung (SQL Server-Variante) + +```sql +CREATE TABLE Kunde ( + KundeID INT IDENTITY(1,1) PRIMARY KEY, + Name NVARCHAR(100) NOT NULL, + Email NVARCHAR(200) NOT NULL UNIQUE, + CreatedAt DATETIME2 NOT NULL DEFAULT SYSDATETIME() +); + +CREATE TABLE Bestellung ( + BestellungID INT IDENTITY(1,1) PRIMARY KEY, + KundeID INT NOT NULL, + Datum DATETIME2 NOT NULL DEFAULT SYSDATETIME(), + Betrag DECIMAL(10, 2) NOT NULL, + FOREIGN KEY (KundeID) REFERENCES Kunde(KundeID) +); + +-- Neuen Kunden anlegen: +INSERT INTO Kunde (Name, Email) +VALUES ('Anna Beispiel', 'anna@example.com'); + +-- Die ID des eben eingefügten Kunden holen: +SELECT SCOPE_IDENTITY() AS NeueKundeID; +``` + +`SCOPE_IDENTITY()` gibt dir in SQL Server die zuletzt erzeugte Identity in der aktuellen Session und dem aktuellen Scope. Damit kannst du *direkt danach* eine Bestellung für genau diesen Kunden anlegen. + +--- + +### 6.2 Beispiel in PostgreSQL (moderner Standard) + +```sql +CREATE TABLE kunde ( + kunde_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE bestellung ( + bestellung_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + kunde_id BIGINT NOT NULL REFERENCES kunde(kunde_id), + datum TIMESTAMPTZ NOT NULL DEFAULT now(), + betrag NUMERIC(10,2) NOT NULL +); + +-- Einfügen: +INSERT INTO kunde (name, email) +VALUES ('Max Muster', 'max@muster.de') +RETURNING kunde_id; -- PostgreSQL-spezifisch, um die ID direkt zu bekommen +``` + +--- + +## Zusammenfassung + +- Eine **Identity Column** ist eine Spalte, deren Wert die Datenbank automatisch erzeugt (typisch: laufende Nummer). +- Sie wird häufig als **Primärschlüssel** verwendet, ist aber **nicht dasselbe** wie der Begriff „Primärschlüssel“. +- Sie löst v. a. diese Probleme: + - eindeutige Identifikation von Zeilen + - einfache Inserts, ohne selbst IDs zu erzeugen + - sichere Nutzung in Mehrbenutzer-Umgebungen + - gute Performance bei Indexen und Joins +- Sie ist verwandt mit: + - Auto-Increment-Spalten (`AUTO_INCREMENT`, `SERIAL`) + - Sequences (`SEQUENCE`) + - Surrogate Keys im Gegensatz zu natürlichen Schlüsseln + - Alternativen wie UUID/GUID +- Typische Herausforderungen: + - Lücken in der Nummernfolge (normal und erwünscht) + - ID ist nicht zuverlässig die zeitliche Reihenfolge + - Datenmigration, Reseeding, Sharding + - potenzielles Informationsleck, wenn IDs öffentlich sichtbar sind + - Unterschiede in der Implementierung zwischen Datenbanksystemen \ No newline at end of file diff --git a/datenbanken/ORM.md b/datenbanken/ORM.md new file mode 100755 index 0000000..c5c7730 --- /dev/null +++ b/datenbanken/ORM.md @@ -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"" + +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.). + diff --git a/datenbanken/PostgreSQL/PostgreSQL Umstieg von Oracle SQL.md b/datenbanken/PostgreSQL/PostgreSQL Umstieg von Oracle SQL.md new file mode 100755 index 0000000..09eff2d --- /dev/null +++ b/datenbanken/PostgreSQL/PostgreSQL Umstieg von Oracle SQL.md @@ -0,0 +1,346 @@ +Hier eine kompakte, praxisorientierte Übersicht zu PostgreSQL mit Fokus auf „Was ist anders als in Oracle?“ und „Was sollte ich vor dem Umstieg lernen?“. + +--- + +## 1. Kurzüberblick & Gemeinsamkeiten + +Gemeinsamkeiten (nur kurz, da dir das meiste vertraut ist): + +- Relationales DBMS, SQL-basiert, ANSI-konform +- [[ACID-Transaktionen]], [[MVCC]], Isolation Levels +- Sequences, Views, [[Materialized Views]], Trigger, Stored Procedures/Functions +- Joins, Subselects, Window Functions, [[CTEs]] (`WITH`), etc. +- Rolle-/Rechtemodell, Schemas + +--- + +## 2. Zentrale Unterschiede auf einen Blick + +Die wichtigsten Lernfelder im Übergang von Oracle zu PostgreSQL: + +1. **Namensräume & `search_path` statt Synonyme** +2. **Datentypen & Funktionalität (z.B. `NUMERIC`, `SERIAL`, `JSONB`, Arrays)** +3. **Sequences & Identity-Spalten** +4. **PL/pgSQL vs. PL/SQL (kein Paketkonzept)** +5. **MVCC-Implementierung & VACUUM/Autovacuum** +6. **DDL in Transaktionen, Auto-Commit-Verhalten** +7. **Index-Typen & Besonderheiten (GIN/GiST, Partial-, Expression-Indexe)** +8. **Partitionierung (deutlich anders als ältere Oracle-Partitioning-Konzepte)** +9. **Admin & Tools (psql, Konfigurationsparameter, Monitoring)** + +--- + +## 3. Schemata, Namensauflösung & Synonyme + +### Schemata & `search_path` + +In PostgreSQL sind Schemata ähnlich wie in Oracle. Es gibt zusätzlich einen **`search_path`**, der festlegt, in welcher Reihenfolge Schemata nach Objekten durchsucht werden. + +```sql +SHOW search_path; +SET search_path TO app, public; +``` + +Statt Synonymen (Oracle) wird oft mit `search_path` gearbeitet: + +- Kein `CREATE SYNONYM` in PostgreSQL. +- Alternative: Views in einem „zentrale“ Schema, `search_path` setzen, oder `CREATE VIEW` als Abstraktionsschicht. + +**Lernpunkt:** Überleg dir, wie du Synonyme ablöst – meistens durch eine Kombination aus `search_path`, Views und ggf. konsistenter Schema-Namenskonvention. + +--- + +## 4. Datentypen & SQL-Dialekt + +### Wichtige Unterschiede bei Datentypen + +- `NUMBER` → in der Regel `NUMERIC(p,s)` oder `INTEGER`, `BIGINT`. +- `VARCHAR2` → `VARCHAR(n)` oder `TEXT`. PostgreSQL hat keinen Unterschied zwischen `VARCHAR` und `VARCHAR2`. +- `DATE` in PostgreSQL enthält Datum **und Uhrzeit** (wie Oracle `DATE`). Für „reine“ Datumswerte: `date`, für Zeitpunkte mit Zeitzone: `timestamptz`. +- `CLOB`/`BLOB` → meist `TEXT` bzw. `BYTEA`. +- Zusätzliche Typen: + - `JSON`/`JSONB` + - Arrays (`integer[]`, `text[]` etc.) + - Geodaten (via Extension PostGIS) + - `UUID`, `Inet`, `CIDR` etc. + +```sql +CREATE TABLE kunde ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(200), + email TEXT, + erstellt_am timestamptz DEFAULT now(), + daten JSONB +); +``` + +### Funktionen und Syntax-Details + +- String-Konkatenation: `||` (wie Oracle). +- `NVL` → `COALESCE` (Standard); es gibt auch `NULLIF`. +- `DECODE` → `CASE WHEN ... THEN ... ELSE ... END`. +- `ROWNUM` → `LIMIT` / `OFFSET` oder Window Functions (`row_number()`). +- Kein „`FROM dual`“ nötig, du kannst einfach: + ```sql + SELECT 1; + ``` + +**Lernpunkt:** Zuordnen der wichtigsten Oracle-Funktionen zu PostgreSQL-Äquivalenten (`DECODE` → `CASE`, `NVL` → `COALESCE`, `ROWNUM` → `LIMIT`/Window). + +--- + +## 5. Sequences & Identity-Spalten + +In Oracle: `CREATE SEQUENCE ...` + Trigger oder `IDENTITY` in neueren Versionen. + +In PostgreSQL gibt es mehrere Varianten: + +- Klassische Sequence: + ```sql + CREATE SEQUENCE myseq; + SELECT nextval('myseq'); + ``` + +- „Legacy“ Auto-Increment: + ```sql + CREATE TABLE t ( + id SERIAL PRIMARY KEY, + ... + ); + ``` + +- Standardkonforme [[Identity Columns]] (empfohlen): + ```sql + CREATE TABLE t ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + ... + ); + ``` + +**Lernpunkt:** Umstieg auf `GENERATED AS IDENTITY` planen, `SERIAL` verstehen (ist syntaktischer Zucker für Sequence + Default). + +--- + +## 6. PL/pgSQL vs. PL/SQL (Packages, Prozeduren, Funktionen) + +### Kein Paketkonzept + +PostgreSQL hat keine Packages wie Oracle (`package spec/body`). Stattdessen: + +- Funktionen stehen „flach“ in einem Schema. +- Ähnliches Strukturieren über Namenskonventionen, Schemata, Extensions. + +### PL/pgSQL + +Syntax ähnlich PL/SQL, aber mit einigen Unterschieden: + +```sql +CREATE OR REPLACE FUNCTION addiere(a int, b int) +RETURNS int +LANGUAGE plpgsql +AS $$ +DECLARE + res int; +BEGIN + res := a + b; + RETURN res; +END; +$$; +``` + +Ab PostgreSQL 11 gibt es auch **Stored Procedures** (ohne Rückgabewert, aufrufbar mit `CALL`) – im Unterschied zu Funktionen, die in SQL-Ausdrücke eingebettet werden können. + +**Lernpunkte:** + +- Unterschiede in Ausnahmebehandlung und Cursor-Syntax im Detail. +- Kein Paket-Overloading wie in Oracle – Overloading geht, aber ohne Packages. +- Migration von Package-Variablen: oft durch Tabellen, Konfigurationstabellen oder `SET LOCAL` + GUC-Parameter ersetzen. + +--- + +## 7. Transaktionen, MVCC & Locks + +### MVCC-Implementierung + +Beide nutzen MVCC, aber die Implementierung ist anders: + +- PostgreSQL speichert mehrere Versionen von Zeilen (Tuples) im Heap. +- Gelöschte/veraltete Versionen werden nicht sofort entfernt, sondern durch **VACUUM** aufgeräumt. +- Standard: Autovacuum kümmert sich darum. Manchmal Tuning nötig (`autovacuum_*`-Parameter). + +### Isolation Levels + +Standard-Level in PostgreSQL ist `READ COMMITTED`, `REPEATABLE READ` ist nicht exakt wie Oracle `SERIALIZABLE`. Es gibt zusätzlich ein echtes `SERIALIZABLE` via SSI (Serializable Snapshot Isolation). + +```sql +SHOW default_transaction_isolation; +SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ; +``` + +### DDL in Transaktionen + +- PostgreSQL erlaubt DDL **innerhalb** von Transaktionen und kann diese zurückrollen: + ```sql + BEGIN; + CREATE TABLE test (id int); + ROLLBACK; -- Tabelle existiert danach nicht + ``` + +- Standard-Clients (z.B. `psql`) arbeiten mit Auto-Commit = ON, aber du kannst das Verhalten steuern. + +**Lernpunkte:** + +- VACUUM/Autovacuum verstehen: wann notwendig, wie überwachen, wie konfigurieren. +- Unterschiedliche Semantik von Isolation Levels im Detail prüfen (z.B. bei Portierung von Code, der sich auf Oracle-Sperrverhalten verlässt). + +--- + +## 8. Indizes & Partitionierung + +### Index-Typen + +PostgreSQL bietet mehr verschiedene Index-Methoden: + +- `btree` (Standard) +- `hash` (seltener) +- `GIN` (für Volltext, JSONB-Keys, Arrays) +- `GiST` (Geodaten, Range-Typen) +- `BRIN` (große append-only Tabellen, z.B. Logs) + +Zusätzlich: + +- **Expression Indexes**: + ```sql + CREATE INDEX idx_lower_name ON kunde (lower(name)); + ``` + +- **Partial Indexes**: + ```sql + CREATE INDEX idx_active_kunden ON kunde (id) WHERE aktiv = true; + ``` + +### Partitionierung + +Neuere PostgreSQL-Versionen haben native Partitionierung (Range, List, Hash). Unterschied zu Oracle: + +- Implementierung anders, kein identisches Interface. +- Viele Operationen laufen „Partition-transparent“, aber es gibt noch Ecken (z.B. bestimmte DDL-Operationen). + +```sql +CREATE TABLE messwerte ( + id BIGINT GENERATED ALWAYS AS IDENTITY, + ts timestamptz NOT NULL, + wert numeric +) PARTITION BY RANGE (ts); + +CREATE TABLE messwerte_2024 PARTITION OF messwerte +FOR VALUES FROM ('2024-01-01') TO ('2025-01-01'); +``` + +**Lernpunkte:** + +- Eignung und Grenzen von GIN/GiST verstehen, wenn du JSONB oder Volltext planst. +- Partitionierungsstrategie neu durchdenken, statt 1:1 die Oracle-Logik zu kopieren. + +--- + +## 9. Materialized Views + +PostgreSQL hat Materialized Views, aber: + +- Kein `ON COMMIT REFRESH`. +- Refresh ist explizit: + ```sql + REFRESH MATERIALIZED VIEW my_mv; + ``` +- Optional mit `CONCURRENTLY` (mit Einschränkungen), um Downtime zu minimieren. + +**Lernpunkt:** Wenn du in Oracle stark auf automatische Refresh-Mechanismen setzt, brauchst du in PostgreSQL einen eigenen Refresh-Workflow (z.B. Cron, Scheduler, Applikationslogik). + +--- + +## 10. Rechte & Rollenmodell + +Ähnlich, aber mit eigenen Begriffen/Details: + +- Nur **Rollen** (kein getrenntes Konzept von „User“ und „Role“ wie in Oracle; ein User ist eine Login-fähige Rolle). +- Rechte auf Objekte (`GRANT SELECT ON table TO role;` etc.) +- Kein systemweites `PUBLIC SYNONYM`, aber es gibt das Schema `public` und das `PUBLIC`-Rolle/Privileges-Konzept. + +```sql +CREATE ROLE app_user LOGIN PASSWORD '...'; +GRANT CONNECT ON DATABASE mydb TO app_user; +GRANT USAGE ON SCHEMA app TO app_user; +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app TO app_user; +``` + +**Lernpunkt:** Mapping deiner bisherigen Oracle-Rollen/Profiles auf PostgreSQL-Rollen und -Rechte. + +--- + +## 11. Administration, Tools & Ökosystem + +### Wichtige Tools + +- `psql` (Kommandozeile, sehr mächtig) +- Admin-Tools: `pgAdmin`, `DBeaver`, `DataGrip` etc. +- Verbindungs-Pooler (wichtig, da PostgreSQL pro Verbindung einen Prozess startet): + - `pgbouncer`, `pgpool-II` + +### Wichtige Admin-Konzepte + +- **Konfiguration** über `postgresql.conf` und `ALTER SYSTEM`: + z.B. `shared_buffers`, `work_mem`, `maintenance_work_mem`, `max_connections`. +- **VACUUM/ANALYZE**: + - `VACUUM` räumt auf. + - `ANALYZE` aktualisiert Statistiken. +- **EXPLAIN/EXPLAIN ANALYZE**: zwingend für Performance-Tuning. + +```sql +EXPLAIN ANALYZE +SELECT ... +FROM ... +WHERE ...; +``` + +- **Backup/Recovery**: + - Logische Backups: `pg_dump`, `pg_dumpall`. + - Physische Backups & PITR: Base-Backups + WAL-Archiving (oder Tools wie `pgBackRest`). + +**Lernpunkte:** + +- Verbindungspooling ist viel wichtiger als in Oracle. +- Autovacuum-Monitoring (z.B. via `pg_stat_*`-Views) und Basiskonfiguration. + +--- + +## 12. Typische Stolpersteine beim Umstieg + +1. **Fehlende Synonyme** → Lösung über `search_path`, Views, klare Schema-Strategie. +2. **Paketkonzept fehlt** → Namespacing neu denken. +3. **Materialized View Refresh** → eigene Scheduler-Logik. +4. **ROWNUM/`dual`** → an `LIMIT`/`OFFSET`, Window Functions, `SELECT 1;` gewöhnen. +5. **Datenbanklinks (`DBLINK`)** → Extension `dblink` oder `postgres_fdw` (Foreign Data Wrapper) verwenden. +6. **Autoincrement** → `IDENTITY`/`SERIAL` + `currval()/nextval()`-Verwendung lernen. +7. **Optimizer-Verhalten** → Statistiken, Parameter und Query-Pläne sind anders; alte „Index-Hints-Gewohnheiten“ greifen nicht 1:1. + +--- + +## 13. Sinnvolle Lernreihenfolge + +1. Basiskonzepte: + - Schemata + `search_path` + - Datentyp-Mapping (NUMBER, DATE, CLOB/BLOB → NUMERIC, TIMESTAMP, TEXT/BYTEA) + - Sequences & Identity +2. SQL-Dialekt: + - Funktionale Unterschiede (`DECODE`, `NVL`, `ROWNUM`, `dual`, Subqueries) +3. Server-seitige Logik: + - PL/pgSQL, Unterschiede zu PL/SQL, kein Package-Konzept +4. MVCC & Performance: + - VACUUM/Autovacuum, EXPLAIN ANALYZE, Index-Typen +5. Admin & Betrieb: + - Rollen, Backups, Konfiguration, Verbindungspooling +6. Spezielle Features: + - JSONB, Arrays, GIN-Indizes, Partitionierung, FDWs + +Wenn du magst, kannst du mir ein paar typische Oracle-Features nennen, die du intensiv nutzt (z.B. bestimmte Partitionierungsarten, Advanced Queuing, bestimmte PL/SQL-Patterns). Dann kann ich dir eine gezielte „Mapping-Tabelle“ für genau diese Themen in PostgreSQL machen. \ No newline at end of file diff --git a/glossar/ACID-Transaktionen.md b/glossar/ACID-Transaktionen.md new file mode 100755 index 0000000..a5a6d6c --- /dev/null +++ b/glossar/ACID-Transaktionen.md @@ -0,0 +1,384 @@ +#datenbank +Kontext: Datenbanken + +--- +ACID-Transaktionen sind ein zentrales Konzept in der Datenbankwelt – vor allem dort, wo Daten „kritisch“ sind (z.B. Banking, Buchhaltung, Bestellungen). Ich führe dich Schritt für Schritt ein, ohne Vorwissen vorauszusetzen. + +--- + +## 1. Grundidee: Was ist eine Transaktion? + +Stell dir eine Transaktion als **logische Einheit von mehreren Datenbankoperationen** vor, die entweder **komplett** oder **gar nicht** ausgeführt werden soll. + +Beispiel (Banküberweisung): +- Konto A: 100 € → 80 € (10 € werden abgebucht) +- Konto B: 50 € → 70 € (10 € werden gutgeschrieben) + +Diese beiden Schritte gehören zusammen: +- Es darf nicht passieren, dass bei A 10 € abgebucht werden, ohne dass sie bei B ankommen. +- Oder dass B 10 € bekommt, ohne dass sie bei A abgezogen werden. + +Beide Aktionen zusammen bilden eine **Transaktion**. + +--- + +## 2. Was bedeutet ACID? + +**ACID** steht für vier Eigenschaften, die Transaktionen in klassischen relationalen Datenbanken gewährleisten sollen: + +1. **A – Atomicity (Atomarität)** +2. **C – Consistency (Konsistenz)** +3. **I – Isolation** +4. **D – Durability (Dauerhaftigkeit)** + +Ich erkläre jede mit einfachen Beispielen. + +--- + +### 2.1 Atomicity (Atomarität) + +**Definition:** +Eine Transaktion wird **entweder vollständig oder gar nicht** ausgeführt. Es gibt keinen „halben“ Zustand. + +Beispiel: +Wieder unsere Banküberweisung: +1. 10 € von Konto A abbuchen +2. 10 € Konto B gutschreiben + +Passiert in der Mitte ein Fehler (z.B. Stromausfall nach Schritt 1), sorgt Atomicity dafür: +- Entweder werden **beide** Schritte ausgeführt +- oder **beide rückgängig gemacht** (Rollback) + +Es darf nicht der Zustand entstehen: +- Konto A: -10 € +- Konto B: unverändert + +Das wäre ein inkonsistenter Zustand – Atomicity verhindert das. + +--- + +### 2.2 Consistency (Konsistenz) + +**Definition (aus Sicht der Datenbankregeln):** +Eine Transaktion bringt die Datenbank von einem **gültigen Zustand** in einen **anderen gültigen Zustand**, gemäß den definierten Regeln (Constraints, Geschäftsregeln). + +Wichtig: Konsistenz bezieht sich nicht nur auf „richtige Daten“, sondern auf die **Einhaltung von Regeln**, z.B.: + +- Fremdschlüssel-Beziehungen (z.B. jede Bestellung gehört zu einem existierenden Kunden) +- Eindeutigkeit (z.B. E-Mail-Adressen sind eindeutig) +- Wertebereiche (z.B. Kontostand darf nicht negativ sein, wenn das System das verbietet) + +Beispiel: +- Regel: Kontostand darf nicht unter 0 fallen. +- Transaktion: Konto mit 5 € soll 10 € abbuchen. +- Datenbank oder Geschäftslogik verhindern, dass diese Transaktion „erfolgreich bestätigt“ wird. +- Nach der Transaktion ist die Datenbank immer noch in einem Zustand, in dem alle Regeln gelten. + +Konsistenz sagt also: +„Wenn du eine Transaktion beginnst, die gültige Daten voraussetzt, dann endet sie (falls sie commitet wird) wieder mit gültigen Daten.“ + +--- + +### 2.3 Isolation + +**Definition:** +Parallele Transaktionen **beeinflussen sich nicht gegenseitig** in einer Art und Weise, die zu inkonsistenten Ergebnissen führt. +Jede Transaktion „fühlt sich so an“, als wäre sie die einzige, die gerade läuft. + +Isolation wird in der Praxis über **Isolation Levels** gesteuert (z.B. READ COMMITTED, REPEATABLE READ, SERIALIZABLE), aber fürs Grundverständnis reicht: + +Beispiel (gleichzeitige Transaktionen): + +- T1: Bucht 10 € von Konto A zu Konto B um. +- T2: Prüft den Kontostand von Konto A für eine Kreditprüfung. + +Ohne Isolation könnte T2 gerade dann den Kontostand lesen, wenn T1 mitten in der Überweisung ist (z.B. A: 90 €, B: noch nicht aktualisiert). +Je nach Isolationsebene verhindert das System: +- „schmutzige“ Reads (lesen von unbestätigten Zwischenständen), +- instabile Wiederholungen (derselbe SELECT liefert in einer Transaktion plötzlich andere Ergebnisse), +- Phantom Reads (neue Datensätze tauchen „plötzlich“ in einer laufenden Transaktion auf). + +Einfach gesagt: Isolation schützt davor, dass sich parallele Transaktionen gegenseitig „in die Quere kommen“. + +--- + +### 2.4 Durability (Dauerhaftigkeit) + +**Definition:** +Wenn eine Transaktion **erfolgreich abgeschlossen** (committed) ist, bleiben die Änderungen **dauerhaft** gespeichert – auch bei: + +- Stromausfall +- Absturz des Servers +- Neustart der Datenbank + +Technisch wird das oft über: +- Write-Ahead-Log (Transaktionslog), +- Journaling, +- redundante Speicherung +umgesetzt. + +Beispiel: +- Überweisung wurde committed. +- Direkt danach fällt der Strom aus. +- Nach Neustart der Datenbank sind die Kontostände **so**, wie sie nach der Überweisung sein sollen. + +Keine „zufällige Rücksetzung“ auf den Stand davor. + +--- + +## 3. Abgrenzung zu verwandten Begriffen + +### 3.1 Transaktion vs. einzelne SQL-Operation + +- Eine **Transaktion** kann aus einer oder mehreren SQL-Anweisungen (z.B. mehreren INSERT, UPDATE, DELETE) bestehen. +- Ohne explizite Transaktion wird oft jede Anweisung automatisch als eigene, kleine Transaktion behandelt (Auto-Commit-Modus). + +Beispiel: +- Zwei separate UPDATE-Befehle ohne Transaktion: + - Es kann passieren, dass nur der erste erfolgreich ist und der zweite fehlschlägt – dann hast du einen halben Zustand. +- In einer Transaktion: + - Beides wird zusammen behandelt – im Fehlerfall wird alles zurückgerollt. + +--- + +### 3.2 ACID vs. BASE (in verteilten / NoSQL-Systemen) + +In vielen verteilten Systemen (z.B. große NoSQL-Datenbanken) wird statt ACID eher das Prinzip **BASE** verfolgt: + +- **B**asically **A**vailable – Das System ist grundsätzlich verfügbar. +- **S**oft-state – Der Zustand kann sich ändern, auch ohne explizite Transaktion. +- **E**ventual consistency – Daten werden **irgendwann konsistent**, nicht unbedingt sofort. + +Vergleich: +- ACID: Strenge Garantien, dafür manchmal langsamer oder schwerer skalierbar. +- BASE: Weniger strenge Garantien, dafür oft leicht horizontal skalierbar (über viele Server verteilt). + +Beispiel: +- Social-Media-Likes: + - Es ist ok, wenn du für ein paar Sekunden oder Minuten leicht unterschiedliche Like-Zahlen auf verschiedenen Geräten siehst. +- Bankkonto: + - Es ist **nicht** ok, wenn der Kontostand „irgendwann mal“ stimmt. Hier will man ACID. + +--- + +### 3.3 Konsistenz (ACID) vs. Konsistenz (CAP-Theorem) + +Der Begriff „Konsistenz“ wird auch im **CAP-Theorem** verwendet (bei verteilten Systemen). Dort bedeutet er etwas anderes: + +- ACID-Konsistenz: Einhaltung von Datenbankregeln (Constraints, Geschäftslogik). +- CAP-Konsistenz: Alle Knoten eines verteilten Systems sehen dieselben Daten zur selben Zeit. + +Es ist wichtig, diese beiden Bedeutungen nicht zu verwechseln. + +--- + +## 4. Welche Probleme lösen ACID-Transaktionen? + +### 4.1 Vermeidung halbfertiger Zustände + +Ohne Atomicity: +- Stromausfall mitten in einer Update-Sequenz → Daten inkonsistent. + +Mit ACID: +- Entweder alles oder nichts → Daten bleiben konsistent. + +--- + +### 4.2 Schutz vor parallelen Zugriffskonflikten + +Typische Probleme ohne Isolation: + +1. **Lost Update (verlorenes Update)** + - Zwei Benutzer lesen denselben Datenstand, ändern ihn unabhängig und speichern beide. + - Die zweite Speicherung überschreibt die erste, ohne es zu merken. + + Beispiel: + - Lagerbestand: 10 Stück + - Benutzer A: Reserviert 3 (denkt: 10 → 7) + - Benutzer B: Reserviert 4 (denkt: 10 → 6) + - Ohne Schutz: Endergebnis könnte 6 sein (Reservierung von A geht „verloren“). + - Mit Isolation/Locks: Das System stellt sicher, dass die Updates korrekt nacheinander verarbeitet werden. + +2. **Dirty Read (schmutziges Lesen)** + - Eine Transaktion liest Änderungen, die eine andere Transaktion noch gar nicht committed hat. + - Wenn die zweite Transaktion zurückgerollt wird, hat man auf falschen Daten gearbeitet. + +3. **Non-repeatable Read & Phantom Read** + - Eine Transaktion liest dieselben Daten mehrmals, aber bekommt unterschiedliche Ergebnisse, weil andere Transaktionen dazwischen geschrieben haben. + - Oder es tauchen plötzlich zusätzliche/fehlende Zeilen auf. + +ACID + passende Isolationsebene reduziert oder verhindert solche Probleme. + +--- + +### 4.3 Sicherung kritischer Geschäftsprozesse + +Wo ACID typisch ist: + +- Bank- und Finanzsysteme +- Flugsitz-Reservierungssysteme +- Warenwirtschaft / Bestellabwicklung +- Ticketverkauf (Konzert, Bahn, Flug) + +Beispiel Tickets: +- Nur 1 Ticket übrig. +- 2 Leute versuchen gleichzeitig, es zu kaufen. +- ACID-Transaktionen sorgen dafür, dass am Ende nur eine Person das Ticket bekommt, nicht beide. + +--- + +## 5. Welche Herausforderungen bringen ACID-Transaktionen mit sich? + +ACID klingt perfekt, hat aber trade-offs. + +### 5.1 Performance und Skalierbarkeit + +- Starke Isolation und Konsistenz können **langsamer** sein, weil: + - Locks (Sperren) auf Zeilen/Tabellen gesetzt werden, + - viele Logs geschrieben werden müssen, + - parallele Zugriffe begrenzt werden. + +Beispiel: +- In einem stark frequentierten Online-Shop könnten lange laufende Transaktionen zu Warteschlangen führen: + - Kunden müssen warten, bevor ihre Updates durchgeführt werden können. + +--- + +### 5.2 Deadlocks (Verklemmungen) + +Wenn mehrere Transaktionen sich gegenseitig sperren, kann es zu einem **Deadlock** kommen: + +Beispiel: + +- T1: + - Sperrt Zeile X + - Will dann Zeile Y sperren (die aber schon von T2 gesperrt ist) +- T2: + - Sperrt Zeile Y + - Will dann Zeile X sperren (die aber schon von T1 gesperrt ist) + +Beide warten aufeinander → Niemand kommt weiter. + +Die Datenbank muss Deadlocks erkennen und eine Transaktion abbrechen (Rollback), damit sich das System wieder erholen kann. + +--- + +### 5.3 Lange laufende Transaktionen + +- Je länger eine Transaktion läuft, desto länger hält sie Sperren. +- Das kann viele andere Benutzer blockieren. + +Beispiel: +- Ein Report, der mehrere Minuten läuft, wird innerhalb einer Transaktion ausgeführt. +- In dieser Zeit können andere nicht ordentlich auf dieselben Daten zugreifen (je nach Isolationsebene). + +Deshalb: +In der Praxis versucht man, Transaktionen so **kurz wie möglich** zu halten. + +--- + +### 5.4 Verteilte Transaktionen (über mehrere Systeme) + +Wenn eine Transaktion **über mehrere Datenbanken oder Services** geht, wird es komplex: + +Beispiel: +- Bestellsystem: + - Reserviert Ware in Lager-Datenbank A + - Erstellt Rechnung in Finanz-Datenbank B + +Um ACID über beide Systeme zu garantieren, braucht man z.B.: +- 2-Phase-Commit (2PC), +- spezielle Transaktionskoordinatoren. + +Probleme: +- Komplex, fehleranfällig, +- schlechte Performance, +- schlechte Skalierbarkeit in großen, verteilten Systemen. + +Darum verzichten moderne Microservice-Architekturen oft auf verteilte ACID-Transaktionen und nutzen stattdessen: +- Eventual Consistency, +- Sagas (geschäftliche, verteilte Abläufe mit Kompensationsaktionen). + +--- + +### 5.5 Komplexität in der Anwendungslogik + +- Wenn Entwickler die Eigenschaften von Isolation, Locks, Deadlocks etc. nicht verstehen, können: + - unerwartete Blockaden, + - merkwürdige Nebenwirkungen, + - Performanceprobleme + auftreten. + +Deshalb ist ein grundlegendes Verständnis von ACID und Transaktionen auch für Anwendungsentwickler wichtig, nicht nur für Datenbankadministratoren. + +--- + +## 6. Praxisnahe Beispiele im Überblick + +### Beispiel 1: Bestellprozess im Online-Shop + +Schritte, die typischerweise in einer Transaktion laufen können: + +1. Kundenbestellung speichern. +2. Lagerbestand reduzieren. +3. Reservierte Waren markieren. +4. Zahlstatus erfassen (z.B. „Bezahlung ausstehend“). + +Mit ACID: +- Wenn in Schritt 2 der Lagerbestand negativ würde → Transaktion fehlschlägt → nichts wird dauerhaft gespeichert. +- Kein Zustand „Bestellung existiert, aber Lagerbestand stimmt nicht“. + +--- + +### Beispiel 2: Benutzerregistrierung + +Schritte: + +1. Benutzerkonto anlegen (INSERT in `users`). +2. Authentifizierungsdaten anlegen (INSERT in `credentials`). +3. Willkommensguthaben in ein Bonuskonto einzahlen (INSERT in `bonus_accounts`). + +Mit ACID: +- Entweder alle drei Inserts werden erfolgreich committed +- oder keiner. +- Kein Benutzer ohne Credentials, kein Bonuskonto ohne Benutzer. + +--- + +### Beispiel 3: Rechnungsstellung + +Schritte: + +1. Rechnung anlegen. +2. Rechnungsposten anlegen. +3. Offenen Posten im Finanzsystem erzeugen. + +Mit ACID: +- Wenn das Anlegen der Rechnungsposten fehlschlägt, wird auch die Rechnung selbst zurückgerollt. +- Es gibt keine „leere Rechnung“. + +--- + +## 7. Zusammenfassung + +- **Transaktionen** fassen mehrere Operationen zu einer Einheit zusammen. +- **ACID**-Eigenschaften sorgen dafür, dass diese Einheiten: + - **A**tomar sind (alles oder nichts), + - **C**onsistent bleiben (Regeln bleiben gültig), + - **I**soliert laufen (parallele Transaktionen stören sich nicht „unsichtbar“), + - **D**auerhaft gespeichert werden (Commit ist endgültig, auch bei Abstürzen). + +**Sie lösen vor allem:** +- Teilweise Updates, +- Inkonsistenzen durch Fehler, +- Probleme durch parallele Zugriffe. + +**Herausforderungen sind:** +- Performance, +- Skalierung, +- Deadlocks, +- Komplexität (vor allem in verteilten Systemen). + +Wenn du möchtest, können wir als nächsten Schritt: +- konkrete SQL-Beispiele (z.B. in PostgreSQL oder MySQL) durchgehen, oder +- tiefer in Isolation Levels (READ COMMITTED, REPEATABLE READ, SERIALIZABLE) einsteigen. \ No newline at end of file diff --git a/glossar/CTEs.md b/glossar/CTEs.md new file mode 100755 index 0000000..9f61892 --- /dev/null +++ b/glossar/CTEs.md @@ -0,0 +1,549 @@ +#datenbank +Kontext: Datenbanken + += ==Common Table Expression== + +>`WITH`-Block in SQL-Abfrage + +➡️[[#Zusammenfassung]] + +--- +Im Folgenden bekommst du eine umfassende, aber einsteigerfreundliche Einführung in CTEs („Common Table Expressions“) im Kontext von Datenbanken – mit Definition, Abgrenzung, Nutzen, Herausforderungen und praxisnahen Beispielen. + +--- + +## 1. Grundidee: Was ist eine CTE? + +**CTE** steht für **Common Table Expression**. +Vereinfacht gesagt ist eine CTE: + +> Eine **temporäre, benannte Ergebnismenge**, die du in einer SQL-Abfrage definierst und im Anschluss in derselben Abfrage wie eine Tabelle verwenden kannst. + +Man kann sich das vorstellen wie: + +- „Ich speichere mir ein Zwischenergebnis unter einem Namen…“ +- „…und benutze dieses Zwischenergebnis dann in der eigentlichen Anfrage.“ + +Grundform: + +```sql +WITH name_der_cte AS ( + -- irgendeine SELECT-Abfrage + SELECT ... + FROM ... + WHERE ... +) +SELECT * +FROM name_der_cte; +``` + +Wichtig: + +- Die CTE existiert **nur für diese eine Abfrage**. +- Sie wird **innerhalb der Abfrage** definiert und direkt **danach** genutzt. +- Man kann CTEs **mehrfach referenzieren**, als wären es echte Tabellen. + +--- + +## 2. Ein einfaches Beispiel + +Stell dir vor, du hast eine Tabelle `bestellungen`: + +- `id` +- `kunde_id` +- `betrag` +- `datum` + +Du möchtest: + +1. Alle Bestellungen des Jahres 2024 herausfiltern. +2. Dann auf Basis dieser gefilterten Daten den Gesamtumsatz pro Kunde berechnen. + +Ohne CTE könnte man das mit einer verschachtelten Abfrage lösen; mit CTE sieht es lesbarer aus: + +```sql +WITH bestellungen_2024 AS ( + SELECT * + FROM bestellungen + WHERE datum >= '2024-01-01' + AND datum < '2025-01-01' +) +SELECT + kunde_id, + SUM(betrag) AS umsatz_2024 +FROM bestellungen_2024 +GROUP BY kunde_id; +``` + +Die CTE `bestellungen_2024` ist hier: +„Alle Bestellungen aus 2024“, und wird anschließend in der Hauptabfrage verwendet. + +--- + +## 3. Abgrenzung zu ähnlichen oder verwandten Begriffen + +### 3.1 CTE vs. Unterabfrage (Subquery / Derived Table) + +**Unterabfrage**: Eine Abfrage innerhalb einer anderen Abfrage, z. B.: + +```sql +SELECT + kunde_id, + SUM(betrag) AS umsatz_2024 +FROM ( + SELECT * + FROM bestellungen + WHERE datum >= '2024-01-01' + AND datum < '2025-01-01' +) AS b2024 +GROUP BY kunde_id; +``` + +Unterschiede: + +- **CTE**: Wird am Anfang mit `WITH` definiert, trägt einen Namen und kann **mehrfach** benutzt werden. +- **Subquery**: Steht direkt im `FROM` oder `WHERE`, ist meist **anonymer** und schwerer zu lesen, vor allem bei komplexen Konstrukten. + +Funktional können CTEs und Subqueries oft das Gleiche – CTEs verbessern eher **Struktur und Lesbarkeit**. + +--- + +### 3.2 CTE vs. View (Sicht) + +**View** (Sicht) ist wie eine **gespeicherte Abfrage** in der Datenbank: + +```sql +CREATE VIEW bestellungen_2024 AS +SELECT * +FROM bestellungen +WHERE datum >= '2024-01-01' + AND datum < '2025-01-01'; +``` + +Dann kannst du schreiben: + +```sql +SELECT kunde_id, SUM(betrag) +FROM bestellungen_2024 +GROUP BY kunde_id; +``` + +Unterschiede: + +- **View**: + - Wird **dauerhaft** in der Datenbank definiert. + - Hat einen Namen, kann von **vielen Abfragen** und auch anderen Nutzer:innen verwendet werden. + - Ändert sich nur über `CREATE OR REPLACE VIEW` / `ALTER VIEW`. +- **CTE**: + - Gilt **nur für eine einzige Abfrage**. + - Ist daher ideal für **einmalige** oder **sehr spezialisierte** Zwischenschritte. + - Erfordert keine Rechte zum Erstellen von Objekten in der Datenbank (kein `CREATE VIEW`). + +Kurz: +Views = dauerhafte, wiederverwendbare Bausteine. +CTEs = temporäre, einmalige Bausteine innerhalb einer Abfrage. + +--- + +### 3.3 CTE vs. temporäre Tabelle + +Viele Datenbanken kennen **temporäre Tabellen**, z. B. `#temp_tab` in SQL Server oder `CREATE TEMP TABLE` in PostgreSQL: + +```sql +CREATE TEMP TABLE bestellungen_2024 AS +SELECT * +FROM bestellungen +WHERE datum >= '2024-01-01' + AND datum < '2025-01-01'; + +SELECT kunde_id, SUM(betrag) +FROM bestellungen_2024 +GROUP BY kunde_id; +``` + +Unterschiede: + +- **Temporäre Tabelle**: + - Wird physisch (zumindest logisch) in der Datenbank angelegt. + - Existiert für die Dauer einer Session oder Transaktion. + - Kann **indiziert** werden (Index hinzufügen) und so bei großen Datenmengen Performancevorteile bringen. +- **CTE**: + - Keine echte Tabelle, eher eine „logische Abfragekomponente“. + - Keine eigenen Indexe. + - Gilt nur innerhalb einer Abfrage. + +--- + +### 3.4 CTE vs. Stored Procedure / Function + +**Stored Procedures** und **Functions** sind Programmierbausteine auf Datenbankseite, z. B.: + +- Prozeduren: führen mehrere Schritte, ggf. mit Kontrollstrukturen aus (IF, WHILE,…). +- Funktionen: geben einen Wert oder eine Tabelle zurück. + +Unterschiede: + +- CTE ist **Teil einer einzelnen SQL-Select/Insert/Update/Delete-Abfrage**. +- Stored Proc / Function ist **Code**, den man **speichert, versioniert und immer wieder aufrufen** kann. + +CTEs können innerhalb von Stored Procedures verwendet werden – sie sind eher Bausteine **auf Abfrage-Ebene**, nicht auf Programm-Ebene. + +--- + +## 4. Welche Probleme werden durch CTEs gelöst? + +### 4.1 Bessere Lesbarkeit und Struktur + +Statt eine riesige, komplexe Abfrage mit vielen verschachtelten Unterabfragen zu schreiben, kann man sich die Abfrage in **logische Schritte** zerlegen: + +1. `WITH schritt1 AS (...)` +2. `, schritt2 AS (...)` +3. `SELECT ... FROM schritt2 ...` + +Beispiel: +Du möchtest erst „aktive Kunden“ bestimmen und dann nur deren Bestellungen summieren: + +```sql +WITH aktive_kunden AS ( + SELECT id, name + FROM kunden + WHERE status = 'aktiv' +), +bestellungen_aktive_kunden AS ( + SELECT + b.kunde_id, + SUM(b.betrag) AS umsatz + FROM bestellungen b + JOIN aktive_kunden k ON k.id = b.kunde_id + GROUP BY b.kunde_id +) +SELECT + k.name, + bak.umsatz +FROM bestellungen_aktive_kunden bak +JOIN aktive_kunden k ON k.id = bak.kunde_id; +``` + +Jeder CTE beschreibt einen **klaren Teilschritt**, das erleichtert Verstehen und Warten des Codes. + +--- + +### 4.2 Wiederverwendung innerhalb einer Abfrage + +Oft brauchst du ein bestimmtes Zwischenergebnis **mehrmals in derselben Abfrage**. +Ohne CTE müsstest du: + +- Dieselbe Unterabfrage mehrfach schreiben (redundant, fehleranfällig) +- Oder du machst Temp-Tabellen / Views, die aber mehr Setup erfordern. + +Mit CTE definierst du das einmal und verwendest es mehrfach: + +```sql +WITH bestellungen_2024 AS ( + SELECT * + FROM bestellungen + WHERE datum >= '2024-01-01' + AND datum < '2025-01-01' +) +SELECT + (SELECT COUNT(*) FROM bestellungen_2024) AS anzahl_gesamt, + (SELECT SUM(betrag) FROM bestellungen_2024) AS umsatz_gesamt; +``` + +--- + +### 4.3 Schrittweise Transformationen (ETL-artige Abläufe) + +Man kann komplexe Datenverarbeitungen in **mehrere CTE-Schritte** aufteilen, z. B.: + +1. Rohdaten bereinigen (ungültige Werte rausfiltern) +2. Daten anreichern (Join mit Lookup-Tabellen) +3. Aggregationen berechnen +4. Ergebnis selektieren + +Schema: + +```sql +WITH raw_data AS ( + SELECT * FROM import_tabelle +), +cleaned_data AS ( + SELECT * + FROM raw_data + WHERE wert IS NOT NULL +), +enriched_data AS ( + SELECT + c.*, + l.beschreibung + FROM cleaned_data c + LEFT JOIN lookup l ON c.code = l.code +), +aggregated AS ( + SELECT + beschreibung, + COUNT(*) AS anzahl, + AVG(wert) AS durchschnitt + FROM enriched_data + GROUP BY beschreibung +) +SELECT * +FROM aggregated +ORDER BY anzahl DESC; +``` + +Solche „linearen“ CTE-Ketten sind sehr hilfreich, um komplexe ETL-Schritte innerhalb einer SQL-Abfrage klar zu definieren. + +--- + +### 4.4 Rekursive Abfragen (Hierarchien, Bäume, Graphen) + +Ein **besonderer Typ** von CTE ist die **rekursive CTE**. Damit kann man Strukturen abfragen, die **hierarchisch** sind, z. B.: + +- Mitarbeiter und ihre Vorgesetzten (Organigramm) +- Kategorien und Unterkategorien (Baumstrukturen) +- Stücklisten (Bauteil besteht aus Unterteilen, die wiederum Unterteile haben) +- Graphen/Netzwerke mit Verbindungen + +Grundidee: + +- Es gibt eine **Anker-Abfrage** (Startpunkt). +- Und eine **rekursive Abfrage**, die sich immer wieder selbst referenziert. + +Beispiel: Mitarbeiter-Hierarchie +Tabelle `mitarbeiter`: + +- `id` +- `name` +- `chef_id` (Verweis auf `id` in derselben Tabelle, NULL für Chef ganz oben) + +```sql +WITH RECURSIVE hierarchie AS ( + -- 1) Anker: Chef (oberste Ebene) + SELECT + id, + name, + chef_id, + 0 AS ebene + FROM mitarbeiter + WHERE chef_id IS NULL + + UNION ALL + + -- 2) Rekursiver Teil: alle Mitarbeiter, die einem bereits gefundenen Mitarbeiter unterstellt sind + SELECT + m.id, + m.name, + m.chef_id, + h.ebene + 1 AS ebene + FROM mitarbeiter m + JOIN hierarchie h ON m.chef_id = h.id +) +SELECT * +FROM hierarchie +ORDER BY ebene, id; +``` + +Ergebnis: +Alle Mitarbeiter mit einer Spalte `ebene`, die angibt, wie weit sie vom obersten Chef entfernt sind. + +**Ohne rekursive CTEs** wäre das sehr umständlich oder gar nicht (standardkonform) direkt in SQL möglich. Manche Systeme haben dafür eigene Syntax (z. B. `CONNECT BY` in Oracle), rekursive CTEs sind der **SQL-Standard**-Weg. + +--- + +## 5. Weitere praxisnahe Beispiele + +### 5.1 Alltagsszenario: „Top-Produkte pro Monat“ + +Gegeben: + +- `verkaeufe` mit Spalten: + - `produkt_id` + - `datum` + - `menge` + - `umsatz` + +Ziel: +Pro Monat die **Top 3 Produkte nach Umsatz** anzeigen. + +Ein möglicher Weg mit CTE: + +```sql +WITH monatliche_umsaetze AS ( + SELECT + DATE_TRUNC('month', datum) AS monat, + produkt_id, + SUM(umsatz) AS umsatz_monat + FROM verkaeufe + GROUP BY DATE_TRUNC('month', datum), produkt_id +), +ranking AS ( + SELECT + monat, + produkt_id, + umsatz_monat, + ROW_NUMBER() OVER ( + PARTITION BY monat + ORDER BY umsatz_monat DESC + ) AS rang_im_monat + FROM monatliche_umsaetze +) +SELECT + monat, + produkt_id, + umsatz_monat, + rang_im_monat +FROM ranking +WHERE rang_im_monat <= 3 +ORDER BY monat, rang_im_monat; +``` + +Die Logik wird nachvollziehbar in zwei Schritte aufgeteilt: + +1. summieren pro Monat und Produkt +2. pro Monat ranken und Top 3 auswählen + +--- + +### 5.2 Kalender-CTE: fehlende Tage auffüllen + +Manchmal hat man z. B. nur Daten für Tage, an denen Verkäufe stattfanden, möchte aber **jeden Tag im Zeitraum** sehen (auch wenn Umsatz 0 ist). +Mit einer rekursiven CTE kann man einen Kalender erzeugen. + +Beispiel (PostgreSQL-Syntax): + +```sql +WITH RECURSIVE kalender AS ( + SELECT DATE '2024-01-01' AS tag + UNION ALL + SELECT tag + INTERVAL '1 day' + FROM kalender + WHERE tag < DATE '2024-01-31' +) +SELECT + k.tag, + COALESCE(SUM(v.umsatz), 0) AS umsatz +FROM kalender k +LEFT JOIN verkaeufe v ON DATE(v.datum) = k.tag +GROUP BY k.tag +ORDER BY k.tag; +``` + +So erhältst du für **jeden Tag im Januar 2024** einen Umsatzwert – bei Tagen ohne Verkäufe eben 0. + +--- + +## 6. Welche Herausforderungen und Stolpersteine gibt es? + +### 6.1 Performance (Leistungsfähigkeit) + +CTEs sind primär ein **Lesbarkeits-Feature**, aber sie können die Performance beeinflussen – je nach Datenbank: + +- Einige Datenbanken „materialisieren“ CTEs: + - Das Zwischenergebnis wird wirklich berechnet und zwischengespeichert. + - Mehrfachzugriff ist dann ggf. schneller, aber die Initialberechnung kann teurer sein. +- Andere optimieren CTEs ähnlich wie Unterabfragen: + - Sie werden im Optimizer „eingefaltet“, d. h. wie eine direkte Teilabfrage behandelt. + +Typische Punkte: + +- **Mehrfache Verwendung großer CTEs**: + - Wenn eine CTE sehr groß ist und du mehrfach darauf zugreifst, kann das teuer werden. + - In manchen Systemen ist dann eine temporäre Tabelle mit Index effizienter. +- **Rekursive CTEs**: + - Können viel CPU und Zeit brauchen, wenn die Hierarchie sehr tief oder stark verzweigt ist. + - Es gibt oft eine maximale Rekursionstiefe (z. B. 100 oder 32767), die begrenzt, wie tief die Rekursion geht. + +Praxis-Tipp: + +- CTEs sind super für mittlere Komplexität und moderate Datenmengen. +- Bei sehr großen Datenmengen und Performanceproblemen: + - Ausführungsplan anschauen. + - Mit Temp-Tabellen, Indizes oder Views experimentieren. + +--- + +### 6.2 Übermäßige Verschachtelung + +Wenn man CTEs zu exzessiv nutzt, z. B. 20 oder mehr CTEs in einer Abfrage, wird es: + +- schwer zu lesen +- schwierig zu debuggen +- komplex zu warten + +Hier gilt: +**Abstraktion, wo sinnvoll, aber nicht übertreiben.** + +Manchmal ist es besser: + +- Teile der Logik in eine View zu legen, +- oder in eine Stored Procedure / Function, +- oder die Logik in mehrere kleinere Abfragen aufzuteilen. + +--- + +### 6.3 Datenbankspezifische Unterschiede + +Nicht jede Datenbank: + +- unterstützt CTEs gleich +- oder hat genau dieselbe Syntax. + +Beispiele: + +- **PostgreSQL**: Unterstützt `WITH` und `WITH RECURSIVE`. +- **SQL Server**: Unterstützt CTEs, Rekursion ohne extra `RECURSIVE`-Keyword. +- **MySQL**: Ab Version 8.0 gibt es CTEs, rekursiv mit `WITH RECURSIVE`. +- **Oracle**: Unterstützt CTEs, hatte aber historisch eigene Hierarchie-Syntax (`CONNECT BY`). + +Wenn du in mehreren Systemen arbeitest, lohnt ein Blick in die Dokumentation der jeweiligen Datenbank. + +--- + +### 6.4 CTEs haben keine eigenen Indizes + +Da CTEs keine physischen Tabellen sind, kannst du: + +- keine Indexe darauf erstellen +- keine Statistiken direkt auf CTE-Ebene pflegen + +D. h.: + +- Der Optimizer muss die Abfrage so gut wie möglich auf Grundlage der zugrundeliegenden Tabellen optimieren. +- Bei Performanceproblemen kann eine echte (temporäre) Tabelle mit Index manchmal besser sein. + +--- + +## 7. Wann sollte man CTEs einsetzen? – Orientierung + +**Geeignet für CTEs:** + +- Wenn eine Abfrage **schwer lesbar** ist, weil sie viele Unterabfragen enthält. +- Wenn du **dasselbe Zwischenergebnis mehrfach** in einer Abfrage benötigst. +- Wenn du **schrittweise Transformationen** ausdrücken möchtest („Step 1, Step 2, …“). +- Wenn du **Hierarchien** oder rekursive Strukturen mit SQL abbilden willst. + +**Eher nicht ideal:** + +- Für dauerhafte, von vielen Personen genutzte Logik → eher **Views** oder **Stored Procedures**. +- Für sehr große Zwischenergebnisse, die oft wiederverwendet werden → evtl. **temporäre Tabellen** mit Index. +- Wenn Performance schon kritisch ist → CTE-Einsatz testen und Ausführungspläne prüfen. + +--- + +## Zusammenfassung + +- **Definition**: CTE = „Common Table Expression“ = eine **temporäre, benannte Ergebnismenge** innerhalb einer Abfrage, definiert mit `WITH`. +- **Nutzen**: + - Klarere Struktur und bessere Lesbarkeit komplexer SQL-Abfragen. + - Wiederverwendbarkeit eines Zwischenergebnisses innerhalb derselben Abfrage. + - Möglichkeit, komplexe Transformationen sequenziell auszudrücken. + - Unterstützung rekursiver Abfragen (Hierarchien/Bäume). +- **Abgrenzung**: + - Gegenüber Subqueries: funktional ähnlich, aber meist lesbarer. + - Gegenüber Views: CTEs sind **temporär**, Views **dauerhaft**. + - Gegenüber Temp-Tabellen: CTEs sind logisch, Temp-Tabellen physisch und indexierbar. +- **Herausforderungen**: + - Performanceabhängig vom Datenbanksystem und vom Aufbau der Abfrage. + - Übermäßiger Gebrauch kann Abfragen unübersichtlich machen. + - Rekursive CTEs können komplex und teuer werden, brauchen sorgfältige Bedingungen. \ No newline at end of file diff --git a/glossar/MVCC.md b/glossar/MVCC.md new file mode 100755 index 0000000..cb4993f --- /dev/null +++ b/glossar/MVCC.md @@ -0,0 +1,384 @@ +#datenbank +➡️[[#Zusammenfassung]] + +--- +MVCC („==Multi-Version Concurrency Control==“) ist ein Verfahren, mit dem Datenbanken gleichzeitige Zugriffe (Concurrency) von vielen Benutzern/Programmen steuern, ohne dass sie sich ständig gegenseitig blockieren. + +Ich gehe Schritt für Schritt vor: + +1. Ausgangspunkt: Was ist das Problem überhaupt? +2. Grunddefinition von MVCC (für Einsteiger) +3. Wie funktioniert das intern grob? +4. Abgrenzung zu verwandten Konzepten +5. Welche Probleme löst MVCC konkret? +6. Welche Herausforderungen/Nachteile bringt MVCC mit sich? +7. Praxisnahe Beispiele + +--- + +## 1. Ausgangspunkt: Wozu brauche ich so etwas? + +In einer typischen Anwendung greifen viele Nutzer gleichzeitig auf dieselbe Datenbank zu: + +- Nutzer A liest Daten (z.B. Produktliste im Shop) +- Nutzer B aktualisiert Daten (z.B. ändert Preise) +- Nutzer C führt einen großen Report aus (z.B. tägliche Umsatzauswertung) + +Ziel: +Alle sollen gleichzeitig arbeiten können, **ohne Fehler** wie: + +- „Dirty Reads“ (jemand liest halbfertige Änderungen) +- Inkonsistente Sicht auf die Daten +- Datenverlust, weil gleichzeitige Änderungen sich überschreiben +- Dauerhafte Sperren und Deadlocks, die das System verlangsamen oder blockieren + +Dazu gibt es das Konzept der **Transaktionen** (ACID): + +- **A**tomicity: Alles oder nichts. +- **C**onsistency: Regeln der Datenbank bleiben erfüllt. +- **I**solation: Transaktionen beeinflussen sich möglichst wenig. +- **D**urability: Einmal bestätigte Daten gehen nicht verloren. + +Die Isolation ist das Kernfeld, in dem MVCC ins Spiel kommt: +Wie sorgt man dafür, dass viele Transaktionen **gleichzeitig** laufen können, ohne sich ständig zu blockieren? + +--- + +## 2. Grunddefinition von MVCC (für Einsteiger) + +**MVCC (Multi-Version Concurrency Control)** bedeutet: + +- **Jede Zeile (Row) in einer Tabelle existiert in mehreren Versionen.** +- Jede Transaktion sieht die Daten „so, wie sie zu einem bestimmten Zeitpunkt waren“ – sie arbeitet mit einem **Snapshot** (Momentaufnahme). +- Wenn jemand etwas **ändert**, wird **nicht** die existierende Zeile überschrieben, sondern eine **neue Version** der Zeile angelegt. +- Für jede Transaktion gibt es Regeln, **welche Version** einer Zeile sie sehen darf. + +Anschaulich: + +- Stell dir eine Tabelle als Tabelle in Excel vor. +- MVCC macht von einigen Zeilen „Kopien“, wenn sie geändert werden. +- Du (deine Transaktion) siehst eine konsistente Version des Tabellenblatts zu dem Zeitpunkt, als du angefangen hast – auch wenn andere währenddessen schon neue Versionen erzeugt haben. + +--- + +## 3. Wie funktioniert MVCC grob intern? + +Vereinfachtes Modell (z.B. ähnlich wie in PostgreSQL/InnoDB): + +### 3.1. Versionen von Zeilen + +Statt einer Zeile gibt es: + +- Version 1: erstellt von Transaktion T1 +- Version 2: später erstellt von Transaktion T2 (z.B. nach einem UPDATE) +- Version 3: später erstellt von T3, usw. + +Jede Version hat Metadaten, z.B.: + +- „ab Transaktion X gültig“ +- „bis Transaktion Y gültig“ + (oder „gelöscht ab Transaktion Y“) + +### 3.2. Was sieht eine Transaktion? + +Wenn eine neue Transaktion T liest: + +- Sie merkt sich einen „Zeitpunkt“ (z.B. eine Transaktions-ID oder Timestamp). +- Beim Lesen prüft sie pro Zeile: + + - Wurde diese Version **vor** meinem Start erzeugt und war zu dem Zeitpunkt schon committed? + - Wurde sie danach erzeugt? Dann darf ich sie nicht sehen. + - Ist sie bereits gelöscht oder von einer laufenden Transaktion verändert, die noch nicht committed ist? Dann gilt sie für mich ggf. als nicht existent. + +Das Ergebnis: +Du bekommst einen **konsistenten Snapshot** der Daten – so, als ob alle Änderungen nach deinem Start noch nicht existieren. + +### 3.3. Schreiben mit MVCC + +Beim **UPDATE** einer Zeile: + +1. Alte Version bleibt im Speicher (zunächst noch sichtbar für alte Transaktionen). +2. Neue Version wird erzeugt, mit „gültig ab Transaktion T“. +3. Für neu startende Transaktionen ist **nach Commit** nur noch die neue Version relevant. + +Beim **DELETE**: + +- Die Zeile bekommt sozusagen ein „Todes-Zeitstempel“ (gültig bis T). +- Alte Transaktionen sehen sie noch, neue nicht mehr. + +### 3.4. Aufräumen („Vacuum“, Garbage Collection) + +Alte Versionen, die **keine** Transaktion mehr sehen kann, sind Müll: + +- Es gibt Prozess(e), die diese alten Versionen von Zeit zu Zeit **physisch entfernen**. +- Beispiel-Begriff: in PostgreSQL „VACUUM“. + +--- + +## 4. Abgrenzung zu verwandten Konzepten + +### 4.1. MVCC vs. „klassische“ Sperren (Locking) + +Früher (oder in einfachen Systemen): + +- Jede Transaktion, die liest oder schreibt, setzt **Sperren** (Locks) auf Zeilen oder ganze Tabellen. +- Wenn du eine Zeile liest, kann ein strenges System diese Zeile sperren, sodass andere sie nicht ändern dürfen, bis du fertig bist. +- Das kann zu Blockaden und Deadlocks führen. + +**MVCC-Ansatz:** + +- Leser blockieren Schreiber **nicht** (oder nur minimal). +- Schreiber blockieren Leser **nicht** (solange es nur um das Lesen bereits existierender, „alter“ Versionen geht). +- Es gibt zwar weiterhin Sperren, aber sie sind oft nur für Konfliktfälle relevant (z.B. zwei Transaktionen, die **dieselbe** Zeile gleichzeitig ändern wollen). + +MVCC ersetzt also nicht jede Form von Locking, sondern **reduziert** deren Notwendigkeit für Lesezugriffe stark. + +### 4.2. MVCC vs. Snapshot Isolation + +- **MVCC** ist ein **Mechanismus**: mehrere Versionen von Zeilen, Snapshots, Sichtbarkeitsregeln. +- **Snapshot Isolation** ist eine **Isolationsebene** (ein bestimmter Garantiesatz für Transaktionen), die **auf MVCC aufbaut**. + +Viele Systeme verwenden MVCC, um Snapshot Isolation umzusetzen: + +- Jede Transaktion sieht den Datenbestand so, wie er beim Start (oder beim Start des Statements) war. +- Aber: Snapshot Isolation ist oft **nicht vollständig serialisierbar** (siehe Write Skew Problem weiter unten). + +Kurz: +MVCC = Wie die Datenbank Versionen verwaltet. +Snapshot Isolation = Welche Sicht und Garantien Transaktionen bekommen. + +### 4.3. MVCC vs. Optimistic Concurrency Control (OCC) + +- **Optimistic Concurrency Control** geht davon aus: Konflikte sind selten, man lässt Transaktionen frei operieren und prüft erst beim Commit, ob es Konflikte gab. +- Viele MVCC-Systeme verwenden **Elemente von OCC**: Sie lassen Transaktionen auf einem Snapshot arbeiten und prüfen erst beim Commit, ob sie z.B. eine Zeile verändert haben, die inzwischen jemand anders schon geändert hat. + +MVCC ist also kein direkter Gegensatz zu OCC, sondern eher eine Technik, mit der man OCC effizient implementieren kann. + +--- + +## 5. Welche Probleme werden durch MVCC gelöst? + +### 5.1. Leser blockieren Schreiber nicht (und umgekehrt) + +**Problem ohne MVCC:** +Großer Report läuft lange und sperrt viele Zeilen oder Tabellen. Schreibende Transaktionen können nicht mehr weiterarbeiten. + +**Mit MVCC:** + +- Der Report bekommt einen Snapshot. +- Während der Report läuft: + - Andere Transaktionen können neue Versionen schreiben. + - Der Report liest weiterhin die alten Versionen, die für ihn gültig sind. +- Ergebnis: lange lesende Transaktionen blockieren das System weit weniger. + +**Praxisbeispiel:** + +- Ein Online-Shop: + - Ein Reporting-Job zählt alle Bestellungen des letzten Monats. + - Gleichzeitig legen Kunden neue Bestellungen an. +- Mit MVCC: + - Reporting-Job sieht einen stabilen Stand („Stand 10:00 Uhr“). + - Kunden können weiter bestellen; diese Bestellungen erscheinen evtl. nicht im aktuellen Report, aber im nächsten. + +### 5.2. Konsistente Lese-Sichten + +Lesende Transaktionen sehen eine **konsistente Momentaufnahme**, nicht einen Mix aus „halb alt, halb neu“. + +Beispiel: + +- Du willst sicherstellen, dass Summe aller Kontosalden = 0 (Bilanz). +- Ohne MVCC könntest du beim Lesen ein Konto schon nach der neuen Buchung sehen, ein anderes aber noch vor der Buchung – Ergebnis: inkonsistente Summe. +- Mit MVCC liest du alles so, wie es zu deinem Startzeitpunkt war. + +### 5.3. Hohe Skalierbarkeit für viele Leser + +MVCC ist besonders stark in Systemen mit: + +- hohem Leseanteil (Reports, Dashboards, APIs) +- vielen parallelen Nutzern + +Weil Leser nicht permanent Schreiblocks blockieren, kann das System mehr parallele Anfragen verarbeiten. + +### 5.4. Reduzierte Deadlocks + +Deadlocks entstehen oft, wenn zwei Transaktionen sich gegenseitig Sperren wegnehmen wollen. +Da MVCC Leser weitgehend von Locks entkoppelt, reduziert sich die Deadlock-Wahrscheinlichkeit, insbesondere bei reinen Lesezugriffen. + +--- + +## 6. Welche Herausforderungen und Nachteile hat MVCC? + +### 6.1. Speicher-Overhead durch viele Versionen + +Da alte Versionen zunächst liegen bleiben, kann es passieren: + +- Tabellen werden „aufgebläht“ (Bloat). +- Disk-Verbrauch steigt, Indexe werden größer, Performance sinkt. + +Darum braucht man: + +- Mechanismen zur Bereinigung (VACUUM, Garbage Collection) +- Tuning (z.B. wie aggressiv wird gereinigt?) + +**Praxisproblem:** +Ein System mit dauernd vielen Updates/Deletes, aber schlecht eingestelltem Vacuum, kann plötzlich deutlich langsamer werden, weil die Tabellen intern riesig sind. + +### 6.2. Lang laufende Transaktionen halten alte Versionen fest + +Wenn eine Transaktion sehr lange offen bleibt (z.B. ein Report, der 2 Stunden läuft): + +- Alle Versionen, die **für diese Transaktion sichtbar sein könnten**, müssen erhalten bleiben. +- Die Datenbank kann diese Versionen nicht löschen, weil sie noch gebraucht werden. +- Folge: Bloat, mehr Speicherverbrauch, mehr I/O, langsamere Zugriffe. + +Praxisnahe Konsequenz: + +- Man versucht in produktiven Systemen: + - Lange Transaktionen zu vermeiden oder + - Sie auf Replikas/Read-Only-Kopien auszulagern. + +### 6.3. Anomalien unter Snapshot Isolation (Write Skew) + +Snapshot Isolation (häufig via MVCC umgesetzt) ist oft **nicht vollständig serialisierbar**. + +Beispiel (vereinfacht): + +- Zwei Ärzte müssen gleichzeitig im Krankenhaus Dienst haben. +- Regel: Es dürfen nie **beide** Ärzte gleichzeitig „Off-Duty“ sein. +- Tabelle `dienstplan` mit Einträgen pro Arzt. + +Transaktion T1: + +```sql +BEGIN; +SELECT COUNT(*) FROM dienstplan WHERE on_duty = TRUE; +-- Ergebnis: 2 (Arzt A und B sind im Dienst) + +-- T1 setzt Arzt A auf off-duty +UPDATE dienstplan SET on_duty = FALSE WHERE arzt = 'A'; +COMMIT; +``` + +Transaktion T2 (parallel, mit eigenem Snapshot): + +```sql +BEGIN; +SELECT COUNT(*) FROM dienstplan WHERE on_duty = TRUE; +-- Ergebnis: 2 (sieht noch alten Stand, beide on duty) + +-- T2 setzt Arzt B auf off-duty +UPDATE dienstplan SET on_duty = FALSE WHERE arzt = 'B'; +COMMIT; +``` + +Beide Transaktionen sehen **denselben alten Stand** („2 Ärzte im Dienst“). +Beide halten es für zulässig, je einen Arzt auf off-duty zu setzen. +Ergebnis nach beiden Commits: **0 Ärzte im Dienst**, Regel verletzt. + +Das ist ein typischer **Write Skew** – möglich unter Snapshot Isolation, obwohl MVCC sauber arbeitet. + +Lösung: + +- Strengere Isolationsebene: **SERIALIZABLE** (die DB verhindert durch zusätzliche Checks solche Fälle), + oder +- Explizite Sperren / zusätzliche Constraints durch den Entwickler. + +### 6.4. Komplexere Implementierung und Tuning + +MVCC ist intern relativ komplex: + +- Verwaltung von Versionen +- Sichtbarkeitslogik +- Garbage Collection +- Umgang mit Hot-Spot-Tabellen (sehr häufig aktualisierte Zeilen) + +Für Administratoren/Entwickler bedeutet das: + +- Man muss verstehen, wie die eigene DB MVCC implementiert (PostgreSQL, MySQL InnoDB, Oracle, SQL Server etc. machen es leicht unterschiedlich). +- Man muss Parameter fürs Aufräumen und für Isolationsebenen sinnvoll setzen. + +--- + +## 7. Praxisnahe Beispiele + +### Beispiel 1: Webshop – Produktpreise ändern + +Situation: + +- Tabelle `produkte` mit Spalte `preis`. +- Viele Nutzer sehen gleichzeitig das Produkt im Shop. +- Ein Admin ändert den Preis. + +Ohne MVCC (vereinfachte, pessimistische Sperrung): + +- Admin setzt Lock auf Zeile/Produkt. +- Solange Admin noch nicht committed hat: + - Kunden könnten blockiert werden oder + - sie sehen unklare Zwischenzustände. + +Mit MVCC: + +- Admin-Transaktion erzeugt eine **neue Version** der Produktzeile mit neuem Preis. +- Kunden, die vor dem Commit lesen: + - sehen die **alte Version** (alten Preis). +- Kunden, die nach dem Commit lesen: + - sehen die **neue Version** (neuen Preis). +- Es gibt keine Lese-Blockade während der Preisänderung. + +### Beispiel 2: Reporting vs. Online-Transaktionen + +- Ein Finance-Report soll **Stand Tagesende** alle Buchungen auswerten. +- Parallel buchen die Nutzer weiter. + +Mit MVCC: + +- Report bekommt einen Snapshot (z.B. Stand 23:59). +- Er arbeitet vielleicht 30 Minuten oder länger. +- Währenddessen können Buchungen von 00:00–00:30 weiter eingehen. +- Report sieht einen stabilen Zustand; neue Buchungen tauchen erst im nächsten Report auf. + +Das ist ideal für: + +- Data Warehouse Light +- tägliche Berichte +- Audits (reproduzierbare Sicht) + +### Beispiel 3: Problemfall lange Transaktion + +- Ein Entwickler startet in der Entwicklungsumgebung eine Transaktion: + +```sql +BEGIN; +SELECT * FROM grosse_tabelle; -- dauert lange +-- Entwickler vergisst COMMIT oder ROLLBACK +``` + +- Diese Transaktion bleibt offen. +- Alle Versionen, die seit Beginn dieser Transaktion geändert wurden, müssen aufgehoben werden. +- Vacuum/Garbage Collector kann viele alte Zeilen nicht löschen. +- Nach Stunden/Tagen: + - Tabellen aufgebläht, + - Performance sinkt, + - Admin muss offene Sessions identifizieren und beenden. + +Lernpunkt: +Mit MVCC muss man auf **offene, vergessene Transaktionen** achten. + +--- + +## Zusammenfassung + +- **MVCC** bedeutet: Zeilen werden nicht einfach überschrieben, sondern es gibt mehrere Versionen derselben Zeile. +- Leser und Schreiber können gleichzeitig arbeiten: + - Leser sehen einen konsistenten Snapshot, + - Schreiber erzeugen neue Versionen, ohne Leser direkt zu blockieren. +- MVCC ist der Mechanismus, auf dem Isolationsebenen wie **Snapshot Isolation** aufbauen. +- Vorteile: + - Hohe Parallelität, + - Weniger Sperrkonflikte, + - Konsistente Sichten für Reports. +- Nachteile/Herausforderungen: + - Mehr Speicherbedarf durch alte Versionen, + - Notwendigkeit von Aufräumprozessen, + - Vorsicht bei langen Transaktionen, + - bestimmte Anomalien (z.B. Write Skew) bei Snapshot Isolation. diff --git a/glossar/Materialized Views.md b/glossar/Materialized Views.md new file mode 100755 index 0000000..8095313 --- /dev/null +++ b/glossar/Materialized Views.md @@ -0,0 +1,360 @@ +#datenbank + +➡️ [[#Zusammenfassung]] + +--- +### 1. Grundidee: Was ist eine Materialized View? + +Eine **Materialized View** (materialisierte Sicht) ist vereinfacht gesagt: + +> **Eine gespeicherte Ergebnistabelle einer Abfrage**, die regelmäßig aktualisiert wird. + +Im Unterschied zu einer „normalen“ View, die bei jedem Zugriff die zugrunde liegende Abfrage neu ausführt, werden bei einer Materialized View: + +- die Daten der Abfrage **physisch gespeichert** (wie in einer Tabelle), +- und später **wiederverwendet**, ohne jedes Mal die komplette Abfrage neu berechnen zu müssen. + +Du kannst dir das vorstellen wie einen **vorgeberechneten Bericht**, der als echte Tabelle im System liegt, aber technisch aus einer oder mehreren anderen Tabellen abgeleitet ist. + +Typische Verwendung: +- Große Tabellen +- Aufwendige Joins +- Aggregationen (SUM, COUNT, AVG, …) +- Reporting & Analytics + +--- + +### 2. Abgrenzung zu verwandten Begriffen + +#### 2.1 Normale View vs. Materialized View + +**View (logische Sicht)**: +- Ist nur eine **gespeicherte Abfrage**. +- Speichert selbst **keine Daten**. +- Beim SELECT auf die View wird die zugrunde liegende Abfrage jedes Mal **neu ausgeführt**. +- Vorteil: Immer **aktuell**, kein Speicheraufwand. +- Nachteil: Kann bei komplexen Abfragen **langsam** sein. + +**Materialized View**: +- Ist eine **physisch gespeicherte Tabelle**, die aus einer Abfrage berechnet wurde. +- Beim SELECT werden die **vorgehaltenen Daten** gelesen (schnell). +- Muss **explizit aktualisiert** („refreshed“) werden. +- Vorteil: Schnelle Abfragen, vor allem bei komplexen Berechnungen. +- Nachteil: Daten können **veraltet** sein, zusätzlicher Speicher & Wartung. + +#### 2.2 Materialized View vs. Tabelle + +**Normale Tabelle**: +- Daten werden direkt in diese Tabelle geschrieben (INSERT, UPDATE, DELETE). +- Struktur (Schema) ist unabhängig; die Tabelle „gehört sich selbst“. + +**Materialized View**: +- Wird **aus anderen Tabellen abgeleitet** (Definition durch SELECT). +- Man schreibt normalerweise **nicht direkt** hinein, sondern nur in die Basistabellen. +- Die Materialized View wird über einen **Refresh-Mechanismus** aktualisiert. + +#### 2.3 Materialized View vs. Cache + +Ein **Cache** (z. B. im Application Server): +- Hält Daten im Speicher, typischerweise kurzfristig. +- Wird meist von der Anwendung gesteuert. +- Ist oft flüchtig (z. B. bei Neustart weg). + +**Materialized View**: +- Ist Teil der Datenbank, persistent auf Disk. +- Wird durch die Datenbank verwaltet. +- Kann komplexere Konsistenz- und Refresh-Strategien nutzen. + +#### 2.4 Materialized View vs. Index + +**Index**: +- Beschleunigt den Zugriff auf bestehende Daten einer Tabelle. +- Speichert typischerweise Schlüsselwerte und Zeiger auf die Tabelle. +- Berechnet **keine neuen Inhalte**, sondern hilft, vorhandene schneller zu finden. + +**Materialized View**: +- Enthält **eigene Daten**, die Ergebnis einer Abfrage sind (z. B. Summen, Gruppierungen). +- Kann zusätzlich selbst Indexe haben. + +#### 2.5 Datenbanken und Begriffe + +Verschiedene Systeme nutzen leicht unterschiedliche Begriffe: + +- **PostgreSQL**: `MATERIALIZED VIEW` +- **Oracle**: `MATERIALIZED VIEW` (sehr ausgereifte Funktionen) +- **SQL Server**: Kein direkter Begriff, aber **Indexed Views** sind sehr ähnlich +- **MySQL**: Keine echte Materialized View, aber man kann das Verhalten nachbauen (z. B. mit Triggern, geplanten Jobs, Tabellen) + +--- + +### 3. Welche Probleme werden durch Materialized Views gelöst? + +#### 3.1 Performance bei komplexen Abfragen + +Wenn du häufig dieselbe **komplexe Abfrage** ausführst, kostet das jedes Mal viel Rechenzeit: + +- Viele Joins über große Tabellen +- Aggregationen (SUM, COUNT, MAX, …) +- Filter auf komplizierten Kombinationen + +Eine Materialized View **berechnet diese Abfrage einmal** (oder in festen Intervallen) und speichert das Ergebnis. Spätere Abfragen greifen nur noch auf die **fertige Ergebnismenge** zu. + +**Beispiel:** + +Du hast einen Online-Shop mit Tabellen: + +- `orders` (Bestellungen) +- `order_items` (Bestellpositionen) +- `products` (Produkte) +- `customers` (Kunden) + +Du willst regelmäßig wissen: **Wie viel Umsatz pro Kunde pro Monat?** + +Ohne Materialized View: + +```sql +SELECT + c.customer_id, + date_trunc('month', o.order_date) AS month, + SUM(oi.quantity * oi.unit_price) AS revenue +FROM customers c +JOIN orders o ON o.customer_id = c.customer_id +JOIN order_items oi ON oi.order_id = o.order_id +GROUP BY c.customer_id, date_trunc('month', o.order_date); +``` + +Diese Abfrage kann bei Millionen Zeilen **sehr langsam** sein, besonders wenn sie viele Nutzer gleichzeitig ausführen. + +Mit Materialized View: + +```sql +CREATE MATERIALIZED VIEW mv_customer_monthly_revenue AS +SELECT + c.customer_id, + date_trunc('month', o.order_date) AS month, + SUM(oi.quantity * oi.unit_price) AS revenue +FROM customers c +JOIN orders o ON o.customer_id = c.customer_id +JOIN order_items oi ON oi.order_id = o.order_id +GROUP BY c.customer_id, date_trunc('month', o.order_date); +``` + +Einfache Abfrage darauf: + +```sql +SELECT * FROM mv_customer_monthly_revenue +WHERE month = '2025-01-01'; +``` + +Das ist meist **sehr schnell**, da nur noch fertige Zeilen gelesen werden. + +#### 3.2 Entlastung der Primärtables + +Wenn viele Nutzer komplexe Analysen auf den operativen Tabellen ausführen, kann das: + +- den Datenbankserver stark belasten, +- Transaktionen verlangsamen, +- das Online-System (z. B. Shop) spürbar ausbremsen. + +Materialized Views dienen hier als eine Art **vorgefertigtes Reporting-Layer**, das: + +- weniger Schreiboperationen hat, +- stark komprimiert / aggregiert sein kann, +- unabhängig indexiert werden kann. + +#### 3.3 Zugriff auf entfernte Daten (z. B. Data Warehouse) + +In manchen Systemen kann eine Materialized View Daten aus **anderen Datenbanken** oder **externen Quellen** einbinden. Damit können z. B.: + +- Daten aus mehreren Systemen +- in einer lokal gespeicherten, performanten Sicht +zusammengefasst werden. + +--- + +### 4. Welche Herausforderungen und Nachteile gibt es? + +#### 4.1 Datenaktualität (Freshness) + +Materialized Views sind **nicht automatisch immer aktuell**. + +Typische Arten des Refresh: + +1. **Maneller Refresh** + Du rufst selbst etwas auf wie: + + ```sql + REFRESH MATERIALIZED VIEW mv_customer_monthly_revenue; + ``` + +2. **Geplanter Refresh (z. B. jede Stunde/Tageswechsel)** + Über Scheduler/Jobs: „Führe jede Nacht um 3 Uhr einen Refresh aus“. + +3. **On Commit / nahezu in Echtzeit** (je nach DB) + Bei bestimmten Systemen können Materialized Views nach Änderungen an den Basistabellen automatisch aktualisiert werden. + +**Konsequenz:** +Es gibt immer einen Trade-off zwischen: + +- **Aktualität** (häufig refresht → näher an „Echtzeit“) +- **Performance/Belastung** (jeder Refresh ist teils teuer) + +#### 4.2 Konsistenz und Komplexität + +Bei vielen Materialized Views, die sich überlappen oder von anderen Views abhängen, kann es kompliziert werden: + +- In welcher Reihenfolge refresht man? +- Was passiert, wenn eine Quelle fehlerhaft ist? +- Wie geht man mit teilweisen Refreshes um? + +#### 4.3 Speicherbedarf + +Materialized Views brauchen **zusätzlichen Speicher**, da sie Daten duplizieren: + +- Daten sind in den Basistabellen vorhanden +- plus noch einmal in den Materialized Views + +Je nach Anzahl und Detailgrad kann das signifikant sein. + +#### 4.4 Schreibaufwand auf Basistabellen (indirekt) + +Wenn Materialized Views sehr häufig aktualisiert werden: + +- können Inserts/Updates auf Basistabellen indirekt langsamer werden, +- weil der Refresh-Prozess Ressourcen frisst (z. B. CPU, I/O, Locks). + +Bei Systemen mit „Refresh on Commit“ müssen evtl. zusätzliche Metadaten gepflegt werden, um Änderungen nachvollziehbar zu machen. + +#### 4.5 Komplexität im Design und Betrieb + +- Man muss Entscheidungen treffen: + - Welche Abfragen lohnen sich als Materialized View? + - Wie oft sollten sie aktualisiert werden? + - Wer ist verantwortlich für Monitoring und Fehlerbehandlung? + +- Bei Änderungen am Schema (z. B. Spalten hinzufügen) muss oft: + - die Materialized View angepasst + - oder neu aufgebaut werden. + +--- + +### 5. Praxisnahe Beispiele + +#### 5.1 Reporting im E-Commerce + +Stell dir einen Online-Shop vor, der täglich Tausende Bestellungen hat. Das Management möchte im Dashboard sehen: + +- Umsatz pro Tag +- Top-10-Produkte pro Woche +- Anzahl neuer Kunden pro Monat + +Ohne Materialized Views würden diese Reports bei jedem Aufruf heavy Queries auf großen Tabellen ausführen. + +Mit Materialized Views: + +1. `mv_daily_revenue` + Umsätze pro Tag + + ```sql + CREATE MATERIALIZED VIEW mv_daily_revenue AS + SELECT + date_trunc('day', o.order_date) AS day, + SUM(oi.quantity * oi.unit_price) AS revenue + FROM orders o + JOIN order_items oi ON oi.order_id = o.order_id + GROUP BY date_trunc('day', o.order_date); + ``` + +2. `mv_weekly_top_products` + Top-Produkte je Woche + + ```sql + CREATE MATERIALIZED VIEW mv_weekly_top_products AS + SELECT + date_trunc('week', o.order_date) AS week, + oi.product_id, + SUM(oi.quantity) AS total_quantity + FROM orders o + JOIN order_items oi ON oi.order_id = o.order_id + GROUP BY date_trunc('week', o.order_date), oi.product_id; + ``` + +3. `mv_monthly_new_customers` + Neue Kunden pro Monat + + ```sql + CREATE MATERIALIZED VIEW mv_monthly_new_customers AS + SELECT + date_trunc('month', c.created_at) AS month, + COUNT(*) AS new_customers + FROM customers c + GROUP BY date_trunc('month', c.created_at); + ``` + +Diese Views kannst du z. B. **jede Nacht** aktualisieren, da sich historische Daten nicht mehr ändern. + +#### 5.2 Data Warehouse / BI + +In einem Data Warehouse gibt es oft: + +- große „Faktentabellen“ (z. B. `fact_sales` mit hunderten Millionen Zeilen), +- Dimensionstabellen (Kunde, Produkt, Region). + +Materialized Views können hier als **„Summary Tables“** dienen, z. B.: + +- Verkäufe pro Region und Monat +- Verkäufe pro Produktkategorie und Quartal + +Analysten müssen dann nicht mehr auf die komplette Faktentabelle zugreifen, sondern nur noch auf relativ kleine, aggregierte Materialized Views. + +#### 5.3 Teilweise Aktualisierung (Incremental Refresh) + +Fortgeschrittene Systeme (z. B. Oracle) können Materialized Views **inkrementell** aktualisieren: Es werden nur die Änderungen seit dem letzten Stand eingerechnet, statt alles komplett neu zu berechnen. Das spart enorm Zeit und Ressourcen, ist aber vom Setup her komplexer. + +--- + +### 6. Typische Strategien und Best Practices + +1. **Nur für „schwere“ Abfragen nutzen** + Materialized Views lohnen sich insbesondere, wenn: + - die Abfrage sehr teuer ist, + - die Daten sich nicht „jede Sekunde“ ändern, + - die Ergebnisse häufig angefragt werden. + +2. **Refresh-Frequenz bewusst wählen** + - Nahe Echtzeit notwendig? → Häufigerer Refresh, mehr Last + - Reine Reports (z. B. täglich) → nächtlicher Refresh reicht oft + +3. **Materialized Views benennen und dokumentieren** + - Sinnvolle Namen (`mv_...`) + - Dokumentation: Welche Abfrage? Wie wird refresht? Wer nutzt sie? + +4. **Indexe auf Materialized Views setzen** + - Genau wie bei normalen Tabellen können Indexe Abfragen weiter beschleunigen: + ```sql + CREATE INDEX idx_mv_daily_revenue_day + ON mv_daily_revenue (day); + ``` + +5. **Monitoring** + - Refresh-Zeiten messen + - Fehler beim Refresh protokollieren + - Speicherverbrauch im Blick behalten + +--- + +### Zusammenfassung + +- **Materialized Views** sind gespeicherte (materialisierte) Ergebnisse von Abfragen, die wie Tabellen genutzt werden können. +- Sie dienen vor allem zur **Performance-Steigerung** bei komplexen oder häufig ausgeführten Abfragen und zur **Entlastung** der operativen Tabellen. +- Gegenüber normalen Views: + - **schneller** beim Lesen, + - aber **nicht automatisch aktuell** – sie müssen refresht werden. +- Herausforderungen: + - Datenaktualität (wie oft refresht man?), + - zusätzlicher Speicherverbrauch, + - Komplexität in Design und Wartung. +- Typische Einsatzgebiete: + - Reporting, Analytics, Dashboards, + - Data Warehousing, + - Aggregationen über große Datenmengen. diff --git a/python/Asynchron in Python.md b/python/Asynchron in Python.md new file mode 100755 index 0000000..8d61787 --- /dev/null +++ b/python/Asynchron in Python.md @@ -0,0 +1,411 @@ +Hier eine Einführung in „asynchron“ in der Python-Programmierung, für jemanden ohne Vorkenntnisse in diesem Bereich. + +--- + +## 1. Intuitive Vorstellung: Was heißt „asynchron“? + +Stell dir vor, du kochst: + +- Du stellst Wasser auf den Herd (es braucht Zeit, bis es kocht). +- Während du wartest, schneidest du Gemüse, bereitest Sauce vor usw. +- Du **blockierst nicht** deine Zeit, indem du nur auf den Topf starrst. + +**Asynchron** bedeutet in der Programmierung: +Dein Programm kann etwas starten, das länger dauert (z.B. eine Netzwerkabfrage), und während es darauf wartet, **andere Dinge erledigen**, statt „untätig“ zu blockieren. + +--- + +## 2. Grundbegriffe: synchron vs. asynchron + +### 2.1 Synchron (blockierend) + +Synchroner, „klassischer“ Code: + +- Befehl A wird ausgeführt. +- Erst wenn A fertig ist, wird B ausgeführt. +- Wenn A lange wartet (z.B. auf eine Antwort aus dem Internet), **steht das ganze Programm an dieser Stelle still**. + +Beispiel (synchron, blockierend): + +```python +import time + +print("Starte") +time.sleep(5) # wartet 5 Sekunden – Programm ist blockiert +print("Fertig") +``` + +Während `time.sleep(5)` läuft, kann das Programm nichts anderes tun. + +### 2.2 Asynchron (nicht-blockierend innerhalb eines Ablaufs) + +Asynchroner Code versucht: + +- Langsame Operationen (z.B. Netzwerk, Festplatte, Datenbank) so zu starten, +- und während sie „laufen“, andere Aufgaben zu bearbeiten. + +In Python machst du das typischerweise mit `async` und `await`. + +Sehr vereinfachtes Beispiel: + +```python +import asyncio + +async def aufgabe(name, dauer): + print(f"{name} gestartet") + await asyncio.sleep(dauer) # nicht-blockierend warten + print(f"{name} fertig") + +async def main(): + # Zwei Aufgaben (Tasks) gleichzeitig laufen lassen + task1 = asyncio.create_task(aufgabe("A", 2)) + task2 = asyncio.create_task(aufgabe("B", 2)) + + await task1 + await task2 + +asyncio.run(main()) +``` + +Typischer Ablauf: +- „A gestartet“ +- „B gestartet“ +- (2 Sekunden vergehen) +- „A fertig“ +- „B fertig“ + +Beide Aufgaben „warten gleichzeitig“, und die Zeit überlappt sich. + +--- + +## 3. Wichtige Abgrenzungen: Begriffe, die oft durcheinandergehen + +### 3.1 Nebenläufigkeit (Concurrency) vs. Parallelität + +- **Nebenläufigkeit (Concurrency)**: Mehrere Aufgaben werden so organisiert, dass sie *scheinbar gleichzeitig* laufen, indem man schnell zwischen ihnen hin- und herschaltet. +- **Parallelität (Parallelism)**: Mehrere Aufgaben laufen *wirklich gleichzeitig* auf mehreren CPU-Kernen. + +Asynchrones Programmieren in Python (`asyncio`) ist in erster Linie ein Werkzeug für **Nebenläufigkeit**, nicht zwingend für echte Parallelität. + +### 3.2 Threads vs. Async + +- **Threads**: + - Betriebssystem-Fäden (OS-Threads). + - Können an verschiedenen CPU-Kernen parallel laufen. + - Schwerer zu testen, zu debuggen (Race Conditions, Deadlocks). +- **Async (z.B. asyncio in Python)**: + - Läuft typischerweise in **einem** Thread. + - Nutzt einen **Event Loop**, um zwischen Aufgaben zu wechseln, wenn sie gerade warten. + - Sehr gut geeignet, wenn viele Aufgaben hauptsächlich **I/O-lastig** sind (Netzwerk, Dateien). + +**Kurz:** +- Viele Netzwerk-Anfragen gleichzeitig? → Async kann ideal sein. +- Viel CPU-Berechnung (z.B. Bildverarbeitung)? → Threads oder Prozesse (Multiprocessing) sind oft sinnvoller. + +### 3.3 Blocking vs. Non-blocking I/O + +- **Blockierend**: „Lies aus dem Netzwerk“ – der Code bleibt stehen, bis Daten da sind. +- **Non-blockierend**: „Lies aus dem Netzwerk, aber wenn gerade nichts da ist, mach solange andere Aufgaben.“ + +Asynchrones Python nutzt non-blocking I/O und einen Event Loop, um viele solcher Operationen gleichzeitig zu verwalten. + +--- + +## 4. Asynchron in Python konkret: `asyncio`, `async`, `await` + +### 4.1 Historischer Kontext + +- Vor Python 3.4 gab es `asyncio` nur als externes Paket. +- Ab Python 3.5 wurden die Schlüsselwörter `async` und `await` eingeführt und haben das Arbeiten mit Async deutlich angenehmer gemacht. + +### 4.2 Zentrale Begriffe + +- **Coroutine**: eine Funktion, die „angehalten“ und später fortgesetzt werden kann. In Python: definiert mit `async def`. +- **Event Loop**: eine Schleife, die: + - Aufgaben plant, + - sie laufen lässt, bis sie warten müssen (z.B. auf I/O), + - dann anderen Aufgaben CPU-Zeit gibt. +- **Task**: eine geplante Coroutine, die vom Event Loop verwaltet wird. +- **Future**: ein Platzhalter für ein Ergebnis, das noch nicht fertig ist. + +### 4.3 Einfaches Beispiel: Event Loop und Coroutines + +```python +import asyncio + +async def hallo(): + print("Hallo...") + await asyncio.sleep(1) # simuliert I/O-Wartezeit + print("...Welt!") + +async def main(): + await hallo() + +asyncio.run(main()) +``` + +- `hallo()` ist eine Coroutine. +- `await asyncio.sleep(1)` bedeutet: „warte 1 Sekunde, aber blockiere nicht den Event Loop“. + +--- + +## 5. Welche Probleme werden durch asynchrones Programmieren gelöst? + +### 5.1 Viele gleichzeitige I/O-Aufgaben + +Typische Beispiele: + +- Webserver, die viele gleichzeitige HTTP-Anfragen beantworten. +- Web-Scraper oder Clients, die viele HTTP-Anfragen an andere Server stellen. +- Chat-Server, WebSockets, Streaming. +- Programme, die gleichzeitig: + - Dateien lesen/schreiben, + - mit einer Datenbank kommunizieren, + - HTTP-Anfragen senden. + +**Synchroner Ansatz**: +Jede Anfrage blockiert einen Thread/Prozess, solange sie auf Antwort wartet → sehr viele Threads/Prozesse nötig. + +**Asynchroner Ansatz**: +Ein Event Loop verwaltet viele tausend Verbindungen in einem oder wenigen Threads, indem er immer dort weiterarbeitet, wo gerade Daten verfügbar sind. + +### 5.2 Bessere Ressourcennutzung bei I/O-lastigen Programmen + +Wenn dein Programm hauptsächlich: + +- Daten lädt (HTTP, DB), +- auf Antworten wartet, +- nicht viel rechnet, + +dann ist asynchroner Code oft **effizienter** (weniger Overhead, weniger Threads, bessere Skalierung). + +### 5.3 Responsivere Anwendungen (z.B. GUIs) + +In grafischen Anwendungen (oder auch CLI-Tools) willst du: + +- Nicht, dass die Oberfläche „einfriert“, während eine Anfrage ans Internet läuft. +- Stattdessen nutzt du asynchrone oder nebenläufige Mechanismen, damit der Haupt-Thread weiterhin Eingaben entgegennimmt. + +--- + +## 6. Praxisnahe Beispiele + +### 6.1 Vergleich: synchron vs. asynchron HTTP-Anfragen + +#### Synchron: nacheinander mit `requests` + +```python +import requests + +urls = [ + "https://example.com", + "https://httpbin.org/delay/2", + "https://httpbin.org/delay/3", +] + +def fetch(url): + print(f"Rufe {url} ab...") + response = requests.get(url) + print(f"{url}: Status {response.status_code}") + +def main(): + for url in urls: + fetch(url) + +if __name__ == "__main__": + main() +``` + +- Jede Anfrage wartet, bis sie fertig ist. +- Gesamtzeit ≈ Summe aller Wartezeiten. + +#### Asynchron: gleichzeitig mit `aiohttp` und `asyncio` + +```python +import asyncio +import aiohttp + +urls = [ + "https://example.com", + "https://httpbin.org/delay/2", + "https://httpbin.org/delay/3", +] + +async def fetch(session, url): + print(f"Rufe {url} ab...") + async with session.get(url) as response: + print(f"{url}: Status {response.status}") + +async def main(): + async with aiohttp.ClientSession() as session: + tasks = [fetch(session, url) for url in urls] + await asyncio.gather(*tasks) # starte alle gleichzeitig + +if __name__ == "__main__": + asyncio.run(main()) +``` + +- Alle Anfragen werden „gleichzeitig“ gestartet. +- Gesamtzeit ≈ maximale Einzeldauer, nicht Summe. + +### 6.2 Viele „Schlaf-Aufgaben“ parallel (Simulation von I/O) + +```python +import asyncio +import random + +async def simulierte_io_aufgabe(n): + dauer = random.uniform(0.5, 2.0) + print(f"Aufgabe {n} startet, Dauer ~{dauer:.2f}s") + await asyncio.sleep(dauer) + print(f"Aufgabe {n} fertig") + +async def main(): + tasks = [simulierte_io_aufgabe(i) for i in range(5)] + await asyncio.gather(*tasks) + +asyncio.run(main()) +``` + +Output (ähnlich): + +- Mehrere Aufgaben starten schnell hintereinander. +- Sie enden in anderer Reihenfolge, je nach Dauer. +- Die Gesamtzeit liegt in etwa bei der **längsten** Wartezeit, nicht bei der Summe aller. + +--- + +## 7. Typische Herausforderungen und Stolperfallen + +### 7.1 Denken in „async“ ist ungewohnt + +Für Einsteiger: +- Man kann `await` **nur** in `async def`-Funktionen verwenden. +- Asynchrone Funktionen verhalten sich anders als normale: + +```python +async def foo(): + return 42 + +# Aufruf: +result = foo() # das ist KEINE 42, sondern eine Coroutine! +``` + +Du musst sie ausführen: + +```python +import asyncio + +async def foo(): + return 42 + +async def main(): + result = await foo() + print(result) + +asyncio.run(main()) +``` + +### 7.2 Blockierender Code in asynchronem Kontext + +Problem: +- Du hast eine `async`-Funktion, benutzt darin aber eine **blockierende** Bibliothek (z.B. `requests`, `time.sleep`). +- Dann blockierst du trotzdem den Event Loop, obwohl du „async“ verwendest. + +Beispiel (so besser nicht): + +```python +import asyncio +import time + +async def schlecht(): + print("Blockiere Event Loop...") + time.sleep(5) # blockiert den Event Loop komplett! + print("Weiter geht's") + +asyncio.run(schlecht()) +``` + +Lösung: +- Entweder eine **asynchrone Alternative** verwenden (z.B. `aiohttp` statt `requests`). +- Oder blockierende Funktion in einem Thread/Prozess auslagern (z.B. `asyncio.to_thread`). + +```python +import asyncio +import time + +def blockierende_funktion(): + time.sleep(5) + return "fertig" + +async def gut(): + print("Starte blockierende Funktion in Thread...") + result = await asyncio.to_thread(blockierende_funktion) + print("Ergebnis:", result) + +asyncio.run(gut()) +``` + +### 7.3 Debugging und Fehlersuche + +- Fehler in asynchronen Programmen können schwerer nachzuvollziehen sein. +- Stack-Traces sehen anders aus, weil Coroutines, Tasks und der Event Loop beteiligt sind. +- Es kann passieren, dass Tasks „stillschweigend“ fehlschlagen, wenn man sie nicht korrekt awaited oder Fehler nicht abfängt. + +### 7.4 Testen von asynchronem Code + +- Unit-Tests brauchen meist auch einen Event Loop. +- Viele Testframeworks bieten dafür Mechanismen (`pytest` mit `pytest-asyncio`). + +Beispiel mit `pytest-asyncio`: + +```python +# test_example.py +import pytest +import asyncio + +async def verdoppeln(x): + await asyncio.sleep(0.1) + return x * 2 + +@pytest.mark.asyncio +async def test_verdoppeln(): + assert await verdoppeln(21) == 42 +``` + +--- + +## 8. Wann lohnt sich asynchrones Programmieren (und wann nicht)? + +**Sinnvoll bei:** + +- Web-APIs, Microservices, Webserver. +- Chat-Server, WebSocket-Anwendungen. +- Web-Scraping vieler Seiten gleichzeitig. +- I/O-lastigen Programmen mit vielen Netzwerk- oder Datenbankzugriffen. + +**Weniger sinnvoll bei:** + +- Reinen CPU-lastigen Aufgaben (z.B. numerische Berechnungen, Bildverarbeitung). + - Hier helfen eher: mehrere Prozesse (`multiprocessing`) oder spezialisierte Bibliotheken (NumPy, Numba, etc.). +- Kleinen Scripts, die nur wenige, einfache Schritte nacheinander machen – da ist synchroner Code oft einfacher und ausreichend. + +--- + +## 9. Zusammenfassung in einfachen Worten + +- **Asynchron** in Python bedeutet: + Du kannst zeitaufwändige, I/O-lastige Aufgaben starten und in der Zwischenzeit andere Aufgaben erledigen, statt auf jede einzelne zu warten. + +- Die Mechanismen dafür sind: + - `async def` (Coroutines), + - `await` (warten, ohne zu blockieren), + - ein **Event Loop** (z.B. in `asyncio`). + +- Es löst besonders gut Probleme mit **vielen gleichzeitigen I/O-Operationen** (Netzwerk, Datenbanken), wie bei Webservern und Web-Scrapern. + +- Herausforderungen: + - Umdenken gegenüber normalem, synchronem Code. + - Aufpassen, keine blockierenden Funktionen im Event Loop zu verwenden. + - Debugging und Testen sind etwas komplexer. + diff --git a/python/FastAPI.md b/python/FastAPI.md new file mode 100755 index 0000000..df89f13 --- /dev/null +++ b/python/FastAPI.md @@ -0,0 +1,499 @@ +Im Folgenden bekommst du eine umfassende, aber einsteigerfreundliche Einführung in FastAPI. + +--- + +## 1. Grundidee: Was ist FastAPI? + +**FastAPI** ist ein modernes, schnelles Web-Framework für Python, mit dem du **Web-APIs** (Schnittstellen) bauen kannst. +Eine API ist eine „Schnittstelle“, über die andere Programme mit deinem Programm sprechen können – z. B. eine Web-App, ein Mobile-App-Backend oder interne Services in einem Unternehmen. + +Kernpunkte von FastAPI: + +- **Schwerpunkt:** Aufbau von **HTTP-APIs** (REST-APIs, JSON-basierte APIs). +- **Geschwindigkeit:** Sehr performant durch Nutzung von **asynchronem Python** (`async`/`await`), basierend auf **ASGI**. +- **Typisierung:** Starke Nutzung von **Python-Typannotationen** (z. B. `str`, `int`, eigene Klassen). + → Daraus entstehen automatisch: + - Validierung von Daten, + - automatische Dokumentation (Swagger / OpenAPI), + - bessere IDE-Unterstützung (Autovervollständigung, Fehlererkennung). +- **Auto-Dokumentation:** FastAPI generiert automatisch eine **interaktive API-Dokumentation** im Browser. + +Ein typisches „Hello World“ mit FastAPI sieht so aus: + +```python +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/") +def read_root(): + return {"message": "Hello World"} +``` + +Starten kannst du das z. B. mit: + +```bash +uvicorn main:app --reload +``` + +Dann ist die API z. B. unter `http://127.0.0.1:8000` erreichbar. + +--- + +## 2. Abgrenzung: FastAPI vs. verwandte Begriffe und Frameworks + +### 2.1 FastAPI vs. „API“ / REST / HTTP allgemein + +- **HTTP**: Das zugrunde liegende Protokoll, über das Browser oder andere Dienste kommunizieren. +- **REST-API**: Eine Art, HTTP-APIs zu strukturieren (z. B. `GET /users`, `POST /orders`). +- **FastAPI**: Ein **Framework**, das dir hilft, solche HTTP/REST-APIs in Python zu bauen. + +FastAPI „spricht“ also HTTP, baut REST-APIs, ist aber selbst das **Werkzeug**, kein Protokoll. + +--- + +### 2.2 FastAPI vs. Flask + +**Flask** ist ein sehr bekanntes, minimalistisches Python-Webframework. + +**Ähnlichkeiten:** + +- Beide erlauben es, mit wenig Code HTTP-Endpunkte zu definieren. +- Beide sind relativ leichtgewichtig und flexibel. + +**Unterschiede:** + +- **Asynchronität**: + - Flask: traditionell synchron (WSGI), Async ist erst neuerdings und eingeschränkt verfügbar. + - FastAPI: von Anfang an für **async** gebaut (ASGI). +- **Typen & Validierung**: + - Flask: Kein eingebautes System für automatische Validierung – du machst das selbst oder mit Erweiterungen. + - FastAPI: Nutzt **[[Pydantic]]**-Modelle und Typannotationen → automatische Validierung. +- **Dokumentation**: + - Flask: Kein automatisches API-Dokumentations-UI. + - FastAPI: Automatisch generierte OpenAPI/Swagger-UI unter `/docs` und `/redoc`. + +Praxisbeispiel Vergleich: + +**Flask:** + +```python +from flask import Flask, request, jsonify + +app = Flask(__name__) + +@app.route("/items", methods=["POST"]) +def create_item(): + data = request.get_json() + name = data.get("name") + price = data.get("price") + if not isinstance(name, str) or not isinstance(price, (int, float)): + return jsonify({"error": "Invalid data"}), 400 + return jsonify({"name": name, "price": price}) +``` + +**FastAPI:** + +```python +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + +class Item(BaseModel): + name: str + price: float + +@app.post("/items") +def create_item(item: Item): + # item ist schon validiert + return item +``` + +FastAPI übernimmt hier die Validierung automatisch. + +--- + +### 2.3 FastAPI vs. Django (und Django REST Framework) + +**Django** ist ein „Full-Stack“-Framework: + +- liefert Templates, ORM (Datenbankzugriff), Admin-Interface, Auth-System, Formulare etc. +- ideal für klassische Webanwendungen mit HTML-Seiten. + +Für APIs nutzt man meist **Django REST Framework (DRF)** als Erweiterung. + +**FastAPI** dagegen ist: + +- stärker auf **APIs** fokussiert, +- nicht „alles aus einer Hand“, sondern: + - Web-Layer: `Starlette`, + - Datenvalidierung: `Pydantic`, + - Datenbank: du wählst selbst z. B. SQLAlchemy, Tortoise ORM etc. + +Faustregel: + +- Wenn du eine klassische Website mit HTML-Rendering brauchst → Django. +- Wenn du primär eine performant API bauen willst (z. B. für SPA, Microservices) → FastAPI ist sehr attraktiv. + +--- + +### 2.4 FastAPI vs. Node.js / Express + +**Node.js + Express** ist eine sehr verbreitete Lösung für APIs in JavaScript/TypeScript. + +- **Sprache:** Node → JavaScript/TypeScript, FastAPI → Python. +- **Typen:** TypeScript kann Typen bieten, FastAPI nutzt Python-Typen + [[Pydantic]]. +- **Ökosystem:** Node sehr stark im Web-/Frontend-nahen Bereich, Python stark bei Data Science, Machine Learning und Backend-Services. + +FastAPI ist besonders interessant, wenn du sowieso Python nutzt (z. B. wegen ML/AI) und dafür eine passende Web-API brauchst. + +--- + +## 3. Welche Probleme löst FastAPI? + +### 3.1 Saubere, valide Eingabedaten + +Problem ohne Framework-Unterstützung: + +- Du bekommst z. B. einen JSON-Body und musst: + - alle Felder prüfen (Typ, Pflichtfelder, Wertebereiche), + - Fehler verständlich zurückgeben, + - alles manuell machen. + +FastAPI + [[Pydantic]] lösen das: + +```python +from fastapi import FastAPI +from pydantic import BaseModel, Field + +app = FastAPI() + +class User(BaseModel): + name: str = Field(..., min_length=3) + age: int = Field(..., ge=0, le=120) # 0 <= age <= 120 + +@app.post("/users") +def create_user(user: User): + # Wenn name zu kurz oder age negativ ist, liefert FastAPI automatisch 422 mit Fehlerdetails + return {"message": "User created", "user": user} +``` + +Vorteil: + +- Weniger Fehleranfälligkeit. +- Konsistente Fehlerantworten. +- Gute Developer-Erfahrung. + +--- + +### 3.2 Automatische Dokumentation und Testbarkeit + +FastAPI erzeugt automatisch eine OpenAPI-Spezifikation und UI: + +- `http://localhost:8000/docs` → Swagger UI (interaktive Oberfläche, du kannst Requests direkt aus dem Browser abschicken). +- `http://localhost:8000/redoc` → ReDoc, alternative Dokumentationsansicht. + +Das hilft: + +- Dir selbst beim Testen. +- Frontend-Entwicklern oder anderen Teams, die deine API nutzen. +- Beim automatisierten Generieren von Client-SDKs (z. B. TypeScript-Client). + +--- + +### 3.3 Performance und asynchrones I/O + +Problem: + +- In „klassischen“ synchronen Webframeworks blockiert jeder Request, der z. B. auf eine externe API oder langsame DB wartet. +- Bei vielen gleichzeitigen Anfragen leiden Durchsatz und Antwortzeit. + +FastAPI setzt auf **ASGI** (Asynchronous Server Gateway Interface) und `async def`: + +```python +from fastapi import FastAPI +import httpx # asynchroner HTTP-Client + +app = FastAPI() + +@app.get("/external") +async def call_external_api(): + async with httpx.AsyncClient() as client: + response = await client.get("https://httpbin.org/get") + return response.json() +``` + +Vorteil: + +- Viele I/O-lastige Requests können parallel abgewickelt werden. +- Besonders sinnvoll bei Microservices, die viel mit anderen Services kommunizieren. + +--- + +### 3.4 Abhängigkeiten und Wiederverwendbarkeit (Dependency Injection) + +FastAPI bietet ein eingebautes **Dependency Injection**-System. +Beim Entwickeln von APIs brauchst du häufig: + +- Datenbankverbindungen, +- Authentifizierungslogik, +- Konfigurationsobjekte. + +Ohne System würdest du das überall wiederholen oder global speichern. +Mit FastAPI: + +```python +from fastapi import Depends, FastAPI + +app = FastAPI() + +def get_settings(): + # z. B. Konfiguration laden + return {"app_name": "Meine App"} + +@app.get("/info") +def read_info(settings = Depends(get_settings)): + return {"app_name": settings["app_name"]} +``` + +Das verbessert: + +- Testbarkeit (du kannst Dependencies im Test austauschen), +- Struktur deines Codes (klarere Trennung von Zuständigkeiten). + +--- + +## 4. Typische Einsatzszenarien (praxisnah) + +### Beispiel 1: Einfaches CRUD für ein „Item“ + +```python +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from typing import List + +app = FastAPI() + +class Item(BaseModel): + id: int + name: str + price: float + +# „Fake-Datenbank“ im Speicher +items_db: List[Item] = [] + +@app.post("/items", response_model=Item) +def create_item(item: Item): + # einfache Prüfung: ID darf nicht doppelt sein + if any(existing.id == item.id for existing in items_db): + raise HTTPException(status_code=400, detail="Item ID already exists") + items_db.append(item) + return item + +@app.get("/items", response_model=List[Item]) +def list_items(): + return items_db + +@app.get("/items/{item_id}", response_model=Item) +def get_item(item_id: int): + for item in items_db: + if item.id == item_id: + return item + raise HTTPException(status_code=404, detail="Item not found") +``` + +Du bekommst: + +- JSON-APIs für CRUD, +- automatische Dokumentation, +- automatische Validierung für `Item`. + +--- + +### Beispiel 2: Path-Parameter, Query-Parameter, Body + +```python +from fastapi import FastAPI +from pydantic import BaseModel +from typing import Optional + +app = FastAPI() + +class SearchFilters(BaseModel): + min_price: Optional[float] = None + max_price: Optional[float] = None + +@app.get("/products/{category}") +def search_products( + category: str, + q: Optional[str] = None, # Query-Parameter ?q=Text + filters: SearchFilters = None # Body JSON +): + return { + "category": category, + "query": q, + "filters": filters + } +``` + +Beispiel-Request: + +- `GET /products/books?q=python` mit JSON-Body: + ```json + { + "min_price": 10, + "max_price": 50 + } + ``` + +FastAPI erkennt: + +- `category` als Pfadparameter, +- `q` als Query-Parameter, +- `filters` als JSON-Body und validiert ihn. + +--- + +### Beispiel 3: Einfache Authentifizierung per Token + +```python +from fastapi import Depends, FastAPI, HTTPException, Header + +app = FastAPI() + +def get_current_user(x_token: str = Header(...)): + if x_token != "secrettoken123": + raise HTTPException(status_code=401, detail="Invalid or missing token") + return {"username": "alice"} + +@app.get("/profile") +def read_profile(current_user = Depends(get_current_user)): + return {"message": f"Hello, {current_user['username']}"} +``` + +Hier: + +- Der Endpunkt `/profile` verlangt einen HTTP-Header `X-Token`. +- FastAPI übernimmt das Zusammenspiel von Header → Dependency → Endpoint-Logik. + +--- + +## 5. Herausforderungen und typische Stolpersteine + +FastAPI nimmt dir viel ab, aber es gibt einige Themen, die für Einsteiger Hürden sein können: + +### 5.1 Asynchrones Programmieren (`async` / `await`) + +- Wenn du noch nie mit Async gearbeitet hast, ist es ungewohnt: + - Wann nutze ich `async def`? + - Wo brauche ich `await`? + - Was ist „Blocking I/O“? +- Du musst darauf achten, dass du **asynchrone Bibliotheken** verwendest, wenn du im Handler `async` einsetzt + (z. B. `httpx` statt `requests`, `asyncpg` statt `psycopg2`). + +Wenn du erst mal einsteigst, kannst du auch erst **synchron** (ohne `async`) starten und später umstellen. + +--- + +### 5.2 Typannotationen und Pydantic verstehen + +FastAPI baut stark auf Typen auf: + +- Für jemanden ohne Erfahrung mit Typannotationen in Python ist das anfangs ungewohnt. +- Du musst verstehen: + - Wie du eigene Modelle mit `BaseModel` definierst. + - Wie optionale Felder mit `Optional[...]` und Standardwerten funktionieren. + - Wie Validierung und Fehlernachrichten von [[Pydantic]] aussehen. + +Aber: +Der Lerneffekt lohnt sich, weil du insgesamt saubereren, stabileren Code bekommst. + +--- + +### 5.3 Datenbankintegration + +FastAPI selbst bringt keinen [[ORM]] mit. Du musst wählen: + +- z. B. **[[SQLAlchemy]]**, Tortoise-ORM, Prisma, Gino etc. + +Dabei stellen sich Fragen wie: + +- Wie verwalte ich Datenbank-Sessions pro Request? +- Nutze ich die sync- oder async-Variante meiner ORM? +- Wie realisiere ich Migrations (alembic, etc.)? + +Es gibt viele Beispielprojekte, aber es ist ein zusätzlicher Schritt im Vergleich zu Django, wo ein [[ORM]] „eingebaut“ ist. + +--- + +### 5.4 Deployment / Betrieb + +Für Einsteiger ist der Weg von „läuft lokal“ zu „läuft im Internet“ oft herausfordernd: + +- FastAPI-Anwendung läuft typischerweise mit: + - [[uvicorn]] oder [[hypercorn]] (ASGI-Server), + - oft hinter einem Reverse Proxy wie [[Nginx]]. +- Themen: + - Logging konfigurieren, + - Umgebungsvariablen (Konfiguration), + - HTTPS/SSL (z. B. via [[Nginx]]/Let’s Encrypt), + - Skalierung (mehrere Worker, z. B. `gunicorn` + `uvicorn.workers.UvicornWorker`). + +Für den Anfang kannst du auch auf Plattformen wie Render, Railway, fly.io, oder Docker + Cloud setzen. + +--- + +### 5.5 Versionierung und Wartung großer Projekte + +Bei größeren APIs: + +- Wie strukturiere ich meinen Code? + - z. B. mit **Routern** (`APIRouter`) und Modulen. +- Wie versioniere ich die API? (`/v1/user`, `/v2/user`, …) +- Wie halte ich die Dokumentation aktuell? + - FastAPI hilft zwar, aber bei vielen Endpunkten braucht man Konventionen und ggf. zusätzliche Dokumentation. + +Beispiel mit Router: + +```python +from fastapi import FastAPI, APIRouter + +app = FastAPI() +items_router = APIRouter(prefix="/items", tags=["items"]) + +@items_router.get("/") +def list_items(): + return [{"id": 1, "name": "Item 1"}] + +app.include_router(items_router) +``` + +So kannst du größere Projekte modular strukturieren. + +--- + +## 6. Zusammenfassung + +- **FastAPI** ist ein modernes Framework für **Web-APIs in Python**, fokussiert auf: + - hohe **Performance** (async), + - **Typen** + automatische Validierung ([[Pydantic]]), + - automatische **OpenAPI-/Swagger-Dokumentation**, + - gute Developer Experience. + +- Es unterscheidet sich von: + - **Flask**: moderner, stärker typisiert, async-first, integrierte Validierung & Doku. + - **Django**: kein Full-Stack-Framework, sondern eher API-fokussiert; du kombinierst es mit eigenen Tools für DB, Templates etc. + - Node/Express: andere Sprache, andere Ökosysteme; FastAPI besonders stark, wenn du ohnehin Python nutzt. + +- Es löst typische Probleme beim API-Bau: + - Validierung von Eingaben, + - Dokumentation & Testbarkeit, + - Performance bei vielen gleichzeitigen Anfragen, + - saubere Struktur durch Dependency Injection. + +- Herausforderungen: + - Einstieg in asynchrones Programmieren, + - Verständnis von Typannotationen & [[Pydantic]], + - separate Auswahl & Integration einer Datenbanklösung, + - Deployment & Betrieb. diff --git a/python/Pydantic.md b/python/Pydantic.md new file mode 100755 index 0000000..7106bdd --- /dev/null +++ b/python/Pydantic.md @@ -0,0 +1,470 @@ +Pydantic ist ein zentrales Werkzeug im heutigen Python-Ökosystem, vor allem im Umfeld von APIs (z.B. [[FastAPI]]), Konfiguration und Datenvalidierung. +Im Folgenden bekommst du eine systematische Einführung mit praxisnahen Beispielen. + +**Hinweis:** Die Beispiele orientieren sich an **Pydantic v2** (aktuelle Hauptversion). In v1 ist die Syntax ähnlich, aber es gibt einige Unterschiede (z.B. `@validator` vs. `@field_validator`). + +--- + +## 1. Grundsätzliche Definition: Was ist Pydantic? + +**Kurz:** +Pydantic ist eine Bibliothek für **Datenmodelle mit Validierung und Parsing** auf Basis von **Python-Typannotationen**. + +Du beschreibst deine Datenstruktur wie bei einer Klasse mit Typen: +- Pydantic prüft zur Laufzeit, ob eingehende Daten diese Struktur erfüllen. +- Es konvertiert (parst) Werte soweit wie möglich in die gewünschten Typen. +- Es gibt strukturierte Fehlermeldungen aus, wenn etwas nicht passt. +- Es kann aus deinen Modellen u.a. **JSON-Schemas** generieren. + +Beispiel – ein einfaches Datenmodell: + +```python +from pydantic import BaseModel, ValidationError +from typing import List + +class User(BaseModel): + id: int + name: str + tags: List[str] = [] + +# Daten aus einer externen Quelle (z.B. JSON) +payload = { + "id": "123", # wird zu int konvertiert + "name": "Alice", + "tags": ["admin", "beta"] +} + +user = User(**payload) +print(user) +print(user.id, type(user.id)) + +# Fehlvalidierung +try: + User(id="abc", name=123) +except ValidationError as e: + print(e.errors()) +``` + +Wichtige Punkte: +- `id` ist als `int` deklariert, ein String `"123"` wird automatisch konvertiert. +- Wenn Konvertierung scheitert (z.B. `"abc"` → `int`), erzeugt Pydantic eine **ValidationError** mit detailierten Fehlerinfos. + +--- + +## 2. Grundkonzepte von Pydantic (v2) + +### 2.1 BaseModel und Felder + +Alle Modelle erben typically von `BaseModel`: + +```python +from pydantic import BaseModel, Field +from typing import Optional + +class Product(BaseModel): + id: int + name: str = Field(..., min_length=3, description="Produktname") + price: float = Field(ge=0) + description: Optional[str] = None +``` + +- `Field(...)` bedeutet „Pflichtfeld“ mit zusätzlichen Metadaten/Constraints. +- `ge=0` = „greater or equal 0“. +- `Optional[str] = None` = optionales Feld, default `None`. + +### 2.2 Validierung & Parsing + +Pydantic führt **Validierung und Parsing beim Erstellen** des Modells durch. +Man kann explizit „parsen“: + +```python +from pydantic import TypeAdapter +from typing import List + +# Einzelnes Modell: meistens direkt Model(**data) +product = Product(id="1", name="TV", price="999.90") + +# Sammlung von Modellen validieren: +ta = TypeAdapter(List[Product]) + +data = [ + {"id": 1, "name": "TV", "price": 999.90}, + {"id": "2", "name": "Laptop", "price": "1299.50"}, +] + +products = ta.validate_python(data) +print(products) +``` + +`TypeAdapter` in v2 ersetzt viele frühere `parse_obj_as`-Usecases. + +### 2.3 Serialisierung + +Pydantic-Modelle lassen sich leicht in z.B. JSON-kompatible Strukturen umwandeln: + +```python +product = Product(id=1, name="TV", price=999.90) +print(product.model_dump()) # dict +print(product.model_dump_json()) # JSON-String +``` + +Man kann steuern: +- welche Felder inkludiert/exkludiert werden, +- wie verschachtelte Modelle serialisiert werden, +- ob Alias-Namen verwendet werden sollen etc. + +--- + +## 3. Abgrenzung zu verwandten Konzepten / Bibliotheken + +### 3.1 Pydantic vs. `dataclasses` + +Python `dataclasses`: + +```python +from dataclasses import dataclass + +@dataclass +class UserDC: + id: int + name: str +``` + +- `dataclasses` stellen nur **strukturelle Container** bereit. +- Keine automatische Validierung oder Typkonvertierung. +- Typannotationen sind rein informativ (für IDE, mypy), nicht enforced. + +Pydantic: + +```python +class UserModel(BaseModel): + id: int + name: str +``` + +- Führt **Validierung & Parsing** beim Erstellen durch. +- Gibt strukturierte Fehler aus. +- Generiert optional JSON-Schemas. +- Basiert auch auf Typannotationen, aber **wertet sie zur Laufzeit aus**. + +Kurz: +- `dataclasses`: leichtgewichtige Container. +- Pydantic: Container + Validierung + Parsing + Schema. + +### 3.2 Pydantic vs. Marshmallow / Cerberus u.ä. + +- **Marshmallow** ist ebenfalls eine Validierungs-/Serialisierungsbibliothek. + - Du definierst Schemas explizit über Felder (z.B. `fields.Int()`) statt über Typannotationen. + - Skill: starke Serialisierung/Deserialisierung, aber andere API. + +- **Pydantic**: + - Nutzt standardmäßige Python-Typannotationen (nativer für moderne Python-Code). + - Sehr eng mit Typing-Ökosystem (mypy, IDEs). + - Performance-fokussiert, in v2 mit `pydantic-core` in Rust. + +### 3.3 Pydantic vs. Typing-Features (`TypedDict`, `Protocol`, …) + +- `TypedDict` definiert nur statische Typinformationen für Dictionaries. +- Pydantic-Modelle sind **richtige Klassen** mit Methoden, Validierung und Verhalten. + +### 3.4 Pydantic vs. ORMs (z.B. Django Models, SQLAlchemy Models) + +- [[ORM]]-Modelle repräsentieren **Datenbanktabellen** und kümmern sich um **Persistenz** (CRUD, Queries). +- Pydantic-Modelle sind **reine Daten- und Validierungsmodelle**, ohne DB-Anbindung. + +In der Praxis: +- Du kannst Pydantic-Modelle nutzen, um **Requests/Responses** zu validieren und zu dokumentieren. +- ORMs nutzen, um die Daten in der Datenbank zu speichern. + +[[FastAPI]] macht genau das: +- Pydantic-Modelle für Request/Response, +- SQLAlchemy/SQLModel/etc. für DB. + +--- + +## 4. Welche Probleme löst Pydantic? + +### 4.1 Validierung externer Daten (APIs, Formulare, Message Queues) + +Externe Daten sind oft: +- unvollständig, +- im falschen Typ, +- fehlerhaft strukturiert. + +Pydantic sorgt für: +- Zentral definierte Datenstruktur. +- Automatische Validierung bei jedem Eingang. +- Konvertierung (z.B. `"123"` → `int`, `"2024-01-01"` → `datetime`). + +Beispiel: Request-Daten einer (pseudo) API: + +```python +from pydantic import BaseModel, HttpUrl +from typing import List + +class Article(BaseModel): + title: str + url: HttpUrl + tags: List[str] = [] + +payload = { + "title": "Pydantic Einführung", + "url": "https://example.com/pydantic", + "tags": ["python", "validation"] +} + +article = Article(**payload) +print(article) +``` + +Wenn `url` kein gültiger URL-String ist, kommt eine strukturierte Fehlermeldung. + +### 4.2 Konfiguration und Umgebungsvariablen + +Pydantic kann Konfiguration aus: +- Umgebungsvariablen, +- `.env`-Dateien, +- kwargs, +- etc. +laden und validieren. + +In v2 nutzt man `pydantic-settings`: + +```python +from pydantic_settings import BaseSettings + +class AppSettings(BaseSettings): + debug: bool = False + database_url: str + port: int = 8000 + + model_config = { + "env_file": ".env", + "env_prefix": "APP_", + } + +settings = AppSettings() +print(settings.database_url, settings.debug) +``` + +- `APP_DATABASE_URL` in der Umgebung oder `.env` wird gelesen. +- Falsche Typen werden validiert/konvertiert (z.B. `"true"` → `bool`). +- Fehlende Pflichtwerte (z.B. `database_url`) führen zu Fehlern. + +### 4.3 Saubere Domain-Modelle und Business-Logik + +Du kannst Pydantic-Modelle verwenden, um deine Domain-Objekte zu modellieren, inklusive: +- Validierung von Invarianten (z.B. Preis > 0, Datum in der Zukunft/ Vergangenheit), +- Standardwerte, +- abgeleitete Felder. + +```python +from pydantic import BaseModel, field_validator +from datetime import datetime + +class Event(BaseModel): + name: str + start: datetime + end: datetime + + @field_validator("end") + def end_must_be_after_start(cls, v, info): + start = info.data.get("start") + if start and v <= start: + raise ValueError("end must be after start") + return v +``` + +--- + +## 5. Herausforderungen & Stolpersteine + +### 5.1 Performance und Overhead + +- Pydantic führt bei **jedem Instanziieren** eines Modells Validierung/Parsing durch. +- Bei sehr großen Datenmengen oder sehr häufigen Instanziierungen kann das Performance kosten. +- *Lösung*: gezielt einsetzen, ggf. `model_validate` mit `from_attributes=True` o.Ä., Caching, oder an bestimmten Stellen auf „raw“ Datenstrukturen ausweichen. + +### 5.2 Lax vs. Strict Typen + +Standardmäßig ist Pydantic recht **„freundlich“**: +- `"123"` → `int(123)` +- `"true"` → `bool(True)` (bei Settings) +- `"1.23"` → `float(1.23)` + +Das ist praktisch, kann aber auch unerwartete Effekte haben. + +Du kannst **strict**-Typen verwenden oder striktere Konfiguration: + +```python +from pydantic import BaseModel, StrictInt + +class Model(BaseModel): + value: StrictInt + +# Model(value="1") -> ValidationError (keine Autokonvertierung) +``` + +Oder über `model_config`: + +```python +class Model(BaseModel): + value: int + + model_config = { + "strict": True, + } +``` + +### 5.3 Umgang mit Optional, Defaults, Required + +Typische Stolperfallen: + +```python +from typing import Optional +from pydantic import BaseModel, Field + +class Example(BaseModel): + a: int # Pflichtfeld + b: Optional[int] # „darf None sein“, aber kein Default → ebenfalls Pflichtfeld + c: int = 0 # optional, default = 0 + d: Optional[int] = None # optional, default = None + e: int = Field(..., description="explizit required") # Pflichtfeld +``` + +- `Optional[int]` heißt nur „`int` oder `None`“, nicht automatisch optional im Sinne von „nicht im Input vorhanden“. +- „Required“ bedeutet: Feld muss im Input vorhanden sein, außer es gibt einen Default. + +### 5.4 Migration v1 → v2 + +Wenn du Codebeispiele im Netz findest, sind viele noch Pydantic v1: +- `@validator` wurde größtenteils zu `@field_validator`. +- `parse_obj_as` → `TypeAdapter`. +- `Config`-Inner-Class → `model_config` oder `ConfigDict`. + +Beim Einstieg: gleich v2-Doku lesen und wählen. + +### 5.5 Komplexe verschachtelte Strukturen + +Pydantic kann sehr komplexe Strukturen validieren (verschachtelte Modelle, Union-Typen, generische Modelle). +Herausforderung ist eher das **Verständnis** der Typen und Validierungsreihenfolge. + +--- + +## 6. Praxisnahe Beispiele + +### 6.1 Verschachtelte Modelle + +```python +from pydantic import BaseModel +from typing import List + +class Address(BaseModel): + street: str + city: str + zip_code: str + +class Customer(BaseModel): + id: int + name: str + addresses: List[Address] + +data = { + "id": "1", + "name": "Bob", + "addresses": [ + {"street": "Main St 1", "city": "Berlin", "zip_code": "10115"}, + {"street": "Side St 2", "city": "Hamburg", "zip_code": "20095"}, + ] +} + +customer = Customer(**data) +print(customer) +``` + +Fehler in einer Adresse werden detailliert auf der jeweiligen „Pfad“-Ebene ausgegeben. + +### 6.2 Feld-Constraints & Metadaten + +```python +from pydantic import BaseModel, Field +from typing import Literal + +class Order(BaseModel): + id: int + status: Literal["open", "paid", "shipped"] + quantity: int = Field(gt=0, description="Muss > 0 sein") + customer_email: str = Field(pattern=r"[^@]+@[^@]+\.[^@]+") + +order = Order( + id=1, + status="open", + quantity=5, + customer_email="test@example.com" +) +``` + +- `Literal` beschränkt mögliche Werte (Enum-artig). +- `pattern` (Regex) validiert z.B. einfache E-Mail-Formate. + +### 6.3 Custom Validierung mit `field_validator` und `model_validator` + +```python +from pydantic import BaseModel, field_validator, model_validator + +class User(BaseModel): + username: str + password: str + password_repeat: str + + @field_validator("username") + def username_not_empty(cls, v): + if not v.strip(): + raise ValueError("username must not be empty") + return v + + @model_validator(mode="after") + def passwords_match(self): + if self.password != self.password_repeat: + raise ValueError("passwords do not match") + return self +``` + +- `field_validator` prüft einzelne Felder. +- `model_validator` (v2) hat Zugriff auf das ganze Modell (z.B. um zwei Felder zu vergleichen). + +### 6.4 JSON-Schema / OpenAPI-Integration + +Pydantic kann JSON-Schemas erzeugen, die u.a. von [[FastAPI]] genutzt werden, um automatisch Doku (OpenAPI/Swagger) zu generieren: + +```python +from pydantic import BaseModel + +class Item(BaseModel): + name: str + price: float + +print(Item.model_json_schema()) +``` + +Das ausgegebene Schema beschreibt die Struktur, Typen und Constraints – ideal für API-Dokumentation. + +--- + +## 7. Zusammenfassung + +- **Definition:** Pydantic ist eine Python-Bibliothek für **Datenmodelle mit Validierung, Parsing und Serialisierung** auf Basis von Typannotationen. +- **Abgrenzung:** + - Mehr als `dataclasses` (mit Validierung & Parsing). + - Nutzt Python-Typing natürlicher als Marshmallow & Co. + - Kein ORM, sondern ergänzt diese (oft für API-Schicht). +- **Probleme, die gelöst werden:** + - Validierung externer Daten (APIs, Config, User-Input). + - Typ-sichere Domain-Modelle. + - Konfiguration aus Umgebungsvariablen/Dateien inkl. Typenprüfung. + - Automatische Generierung von JSON-Schemas (z.B. für APIs). +- **Herausforderungen:** + - Performance bei massiver Nutzung. + - Verständnis von strict vs. lax Typen. + - Stolperfallen bei Optional/Defaults. + - Versionsunterschiede (v1 vs. v2). \ No newline at end of file diff --git a/python/SQLAlchemy.md b/python/SQLAlchemy.md new file mode 100755 index 0000000..3da02a0 --- /dev/null +++ b/python/SQLAlchemy.md @@ -0,0 +1,1170 @@ +SQLAlchemy ist eines der zentralen Datenbank-Tools im Python-Ökosystem. Es bietet sowohl ein mächtiges ORM als auch ein flexibles, SQL-nahes Core-API. + +Im Folgenden: + +1. Grundidee von SQLAlchemy +2. Abgrenzung zu verwandten Ansätzen/Frameworks +3. Welche Probleme SQLAlchemy löst +4. Typische Herausforderungen / Stolpersteine +5. Praxisnahe Beispiele (Core & ORM) + +--- + +## 1. Grundsätzliche Idee von SQLAlchemy + +### Zwei große Säulen: Core und [[ORM]] + +**a) SQLAlchemy Core** + +- Bietet ein **relationales Abstraktions-Framework** nahe an SQL. +- Du definierst Tabellen und baust SQL-Ausdrücke mit Python-Objekten, statt Strings zu schreiben. +- Fokus: **Kontrolle über SQL**, Portabilität zwischen Datenbanken, aber kein “Objekt-Graph” wie beim ORM. + +Beispiel: Tabelle und Query mit Core: + +```python +from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, select + +engine = create_engine("sqlite:///example.db", echo=True) +metadata = MetaData() + +users = Table( + "users", + metadata, + Column("id", Integer, primary_key=True), + Column("name", String), + Column("email", String), +) + +metadata.create_all(engine) + +with engine.connect() as conn: + # Insert + conn.execute(users.insert().values(name="Alice", email="alice@example.com")) + conn.commit() + + # Select + stmt = select(users).where(users.c.name == "Alice") + result = conn.execute(stmt) + for row in result: + print(row.id, row.name, row.email) +``` + +**b) SQLAlchemy ORM** + +- Setzt auf dem Core auf und bietet: + - **Objekt-Relationales Mapping**: Tabellen ↔ Python-Klassen. + - **Unit-of-Work / Session**: Änderungen an Objekten werden gesammelt und in Transaktionen geschrieben. + - Navigation über **Beziehungen** (z.B. `user.addresses` statt Joins manuell zu schreiben). + +Beispiel: ORM-Modelle und Nutzung: + +```python +from sqlalchemy import create_engine, Integer, String, ForeignKey +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session + +engine = create_engine("sqlite:///example.db", echo=True) + + +class Base(DeclarativeBase): + pass + + +class User(Base): + __tablename__ = "users" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + name: Mapped[str] = mapped_column(String) + email: Mapped[str] = mapped_column(String) + + addresses: Mapped[list["Address"]] = relationship(back_populates="user") + + +class Address(Base): + __tablename__ = "addresses" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + email_address: Mapped[str] = mapped_column(String) + user_id: Mapped[int] = mapped_column(ForeignKey("users.id")) + + user: Mapped[User] = relationship(back_populates="addresses") + + +Base.metadata.create_all(engine) + +# Arbeiten mit Objekten +with Session(engine) as session: + user = User(name="Bob", email="bob@example.com") + user.addresses.append(Address(email_address="bob@work.com")) + session.add(user) + session.commit() + + # Query + bob = session.query(User).filter_by(name="Bob").first() + print(bob.email, [addr.email_address for addr in bob.addresses]) +``` + +**Kernidee:** +SQLAlchemy bietet dir die Wahl: + +- Du willst **präzises, datenbanknahes SQL**? Nimm **Core**. +- Du willst **Objekte** und **Abstraktion** über SQL? Nimm das **ORM** (intern nutzt es Core). + +--- + +## 2. Abgrenzung zu ähnlichen Vorgehensweisen / Paketen / Paradigmen + +### a) Raw SQL / DB-API (z.B. mit `psycopg2`, `sqlite3`) + +- Du schreibst SQL-Strings direkt: + ```python + import sqlite3 + + conn = sqlite3.connect("example.db") + cursor = conn.cursor() + cursor.execute("SELECT id, name FROM users WHERE name = ?", ("Alice",)) + rows = cursor.fetchall() + ``` +- **Vorteile**: + - Vollständige Kontrolle, keine zusätzliche Abstraktion. + - Einfach für sehr kleine Projekte / Skripte. +- **Nachteile**: + - Manuelles Mappen in Python-Objekte. + - Kein einheitliches, portables Abstraktionslevel. + - Redundante String-SQLs; schwer wartbar bei großen Codebasen. + +**SQLAlchemy Core** liegt hier zwischen „Raw SQL” und „ORM”: +- Bietet Typsicherheit (bis zu einem gewissen Grad), Query-Building, Portabilität. +- Keine Magic, aber viel Komfort. + +### b) Andere ORMs (z.B. Django ORM, peewee, ponyORM) + +**Django ORM:** + +- Stark mit Django-Framework integriert, nicht generisch. +- Typisch: `Model.objects.filter(...)`. +- **SQLAlchemy vs Django ORM**: + - SQLAlchemy ist **Framework-agnostisch**, kann in [[FastAPI]], Flask, CLI-Skripten etc. genutzt werden. + - ORM-Funktionen sind **umfangreicher** (z.B. komplexes Relationship-Handling, polymorphe Mappings, etc.). + - Django ORM ist einfacher für typische Web-CRUD-Anwendungen, weniger flexibel bei exotischer DB-Logik. + +**peewee, ponyORM etc.:** + +- Meist leichtergewichtig, weniger boilerplate. +- Oft weniger mächtig in Sonderfällen (komplexe Joins, exotische DB-Features). +- SQLAlchemy ist tendenziell der „Power-User-Ansatz”: breit, tief, konfigurierbar. + +### c) Active Record vs Data Mapper + +- Viele ORMs (Rails ActiveRecord, Django) folgen dem **Active-Record-Pattern**: + - Das Model-Objekt kennt seine Datenzugriffe (`save()`, `delete()`). +- SQLAlchemy ORM folgt eher dem **Data-Mapper-Pattern**: + - Domain-Objekte sind relativ „rein“. + - Persistenz wird über die Session/Mapper hergestellt. +- Folge: stärkere **Trennung von Domänenlogik und Persistence** – gut für komplexe Business-Logik. + +--- + +## 3. Welche Probleme werden gelöst? + +### a) Impedance Mismatch: Objektwelt vs Relationale Welt + +- Python: Objekte, Vererbung, Beziehungen über Attribute/Listen. +- Datenbank: Tabellen, Fremdschlüssel, Joins, Normalisierung. + +SQLAlchemy ORM löst u.a.: + +- **Mapping von Tabellen auf Klassen** +- **Mapping von Zeilen auf Objekte** +- **Beziehungen als Python-Attribute** (`user.addresses` statt manueller Join). +- **Vererbung**: Single-Table-Inheritance, Joined-Table-Inheritance etc. + +Beispiel-Vererbung (Joined-Table): + +```python +class Employee(Base): + __tablename__ = "employees" + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String) + type: Mapped[str] = mapped_column(String) # Discriminator + + __mapper_args__ = { + "polymorphic_on": type, + "polymorphic_identity": "employee", + } + + +class Manager(Employee): + __tablename__ = "managers" + id: Mapped[int] = mapped_column(ForeignKey("employees.id"), primary_key=True) + department: Mapped[str] = mapped_column(String) + + __mapper_args__ = {"polymorphic_identity": "manager"} +``` + +### b) Transaktionen und Unit-of-Work + +- Die **Session** verwaltet ein Set von Objekten: + - Änderungen werden verfolgt (dirty tracking). + - `session.commit()` bündelt alles in einer Transaktion. +- Du musst nicht jede Insert/Update/Delete-Anweisung selbst schreiben. + +Beispiel: + +```python +with Session(engine) as session: + user = session.get(User, 1) + user.email = "new@example.com" # Änderung am Objekt + session.commit() # ORM generiert UPDATE users SET email=... WHERE id=1 +``` + +### c) Portabilität zwischen Datenbanken + +- Einmal definierte Modelle funktionieren (meistens) auf SQLite, PostgreSQL, MySQL, Oracle usw. +- Migration von einer DB-Engine zur anderen ist deutlich einfacher, solange du SQL-Standard-Funktionalitäten nutzt. +- Im Zusammenspiel mit **Alembic** (Migrations-Tool von SQLAlchemy) sind Schema-Änderungen integrierbar. + +### d) Komplexe Queries in kontrollierbarer Form + +- SQLAlchemy Core/ORM erlaubt den **systematischen Aufbau** von SQL-Ausdrücken: + - Zusammensetzen von Filtern / Joins / Subqueries abhängig von Parametern zur Laufzeit. + - Vermeidet Fehler bei String-SQL-Konkatenation. + +Beispiel: dynamischer Query-Aufbau mit ORM: + +```python +from sqlalchemy import select, or_ + +def search_users(session: Session, name=None, email=None): + stmt = select(User) + conditions = [] + if name: + conditions.append(User.name.ilike(f"%{name}%")) + if email: + conditions.append(User.email.ilike(f"%{email}%")) + + if conditions: + stmt = stmt.where(or_(*conditions)) + + return session.scalars(stmt).all() +``` + +--- + +## 4. Herausforderungen und typische Stolpersteine + +### a) Lernkurve und Komplexität + +- SQLAlchemy ist **umfangreich**: + - Core, ORM, Mappings, Loader-Options, Eager/Lazy Loading, Events, etc. +- Für einfache CRUD-Apps wirkt es am Anfang „overkill“. +- Viele Wege führen zum Ziel (Core vs ORM, Declarative vs klassische Mappings, Query-APIs). + +**Praxis-Tipp:** +Für neue Projekte: +- Starte mit **Declarative ORM** und einfachen Patterns. +- Lern bei Bedarf zusätzlich Core für spezielle SQL-Fälle. + +### b) Session-Management und Lebenszyklus + +Falscher Umgang mit Sessions ist eine häufige Fehlerquelle: + +- Zu lange lebende Sessions, die viele Objekte halten → hoher Memory-Footprint. +- Session in Web-Apps: + - Typischer Pattern: **“Session-per-request”**: + - Am Anfang eines Requests Session erstellen. + - Am Ende commit/rollback und schließen. + +Beispiel mit [[FastAPI]]: + +```python +from fastapi import Depends, FastAPI +from sqlalchemy.orm import Session + +app = FastAPI() + +def get_session(): + with Session(engine) as session: + yield session + +@app.get("/users/{user_id}") +def read_user(user_id: int, session: Session = Depends(get_session)): + return session.get(User, user_id) +``` + +### c) Lazy Loading vs Eager Loading + +- Beziehungen sind per Default oft **lazy geladen**: + - Zugriff auf `user.addresses` löst eine neue Query aus. +- Probleme: + - In Schleifen kann das zu **N+1 Query Problem** führen. +- Lösung: + - Eager Loading über Optionen (`joinedload`, `selectinload`). + +Beispiel Eager Loading: + +```python +from sqlalchemy.orm import joinedload + +with Session(engine) as session: + users = ( + session.query(User) + .options(joinedload(User.addresses)) + .all() + ) + + # Kein weiterer Query für .addresses nötig + for user in users: + print(user.name, len(user.addresses)) +``` + +### d) Performance-Tuning + +- ORM-Komfort kann SQL „verstecken“ – was gut und schlecht sein kann: + - Es können ineffiziente Queries entstehen, ohne dass du es sofort merkst. +- Tools: + - `echo=True` im Engine zum SQL-Loggen. + - Profiler, EXPLAIN-Analyse in der DB. +- Manchmal ist es besser, für komplexe Performance-kritische Queries: + - **Explizites Core-Statement** oder sogar **Raw SQL** zu verwenden. + +### e) Migrations / Schema-Änderungen + +- SQLAlchemy selbst bietet **keine Migrationen**, das ist die Aufgabe von **Alembic**. +- Herausforderung: + - Konsistentes Management von Modellen (Python) und Schema (DB). + - Migration-Skripte schreiben, testen, deployen. + +Trotzdem ist das Zusammenspiel SQLAlchemy + Alembic ein de-facto Standard in vielen Projekten. + +--- + +## 5. Praxisnahe Beispiele + +### a) Typisches Setup in einer kleinen Web-Anwendung + +Struktur: +- `database.py` – Engine und Session-Factory +- `models.py` – ORM-Modelle +- `crud.py` – Datenbank-Operationen +- `main.py` – FastAPI/Flask-Endpoints + +`database.py`: + +```python +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +DATABASE_URL = "sqlite:///./app.db" + +engine = create_engine(DATABASE_URL, echo=False, future=True) +SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False) +``` + +`models.py`: + +```python +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column +from sqlalchemy import Integer, String + +class Base(DeclarativeBase): + pass + +class User(Base): + __tablename__ = "users" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) + name: Mapped[str] = mapped_column(String, index=True) + email: Mapped[str] = mapped_column(String, unique=True, index=True) +``` + +`crud.py`: + +```python +from sqlalchemy.orm import Session +from .models import User + +def get_user_by_email(db: Session, email: str) -> User | None: + return db.query(User).filter(User.email == email).first() + +def create_user(db: Session, name: str, email: str) -> User: + user = User(name=name, email=email) + db.add(user) + db.commit() + db.refresh(user) + return user +``` + +`main.py` (FastAPI): + +```python +from fastapi import FastAPI, Depends +from sqlalchemy.orm import Session +from .database import SessionLocal, engine +from .models import Base +from . import crud + +app = FastAPI() +Base.metadata.create_all(bind=engine) + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +@app.post("/users/") +def create_user(name: str, email: str, db: Session = Depends(get_db)): + return crud.create_user(db, name, email) + +@app.get("/users/{email}") +def read_user(email: str, db: Session = Depends(get_db)): + return crud.get_user_by_email(db, email) +``` + +### b) Mischung aus Core und ORM + +Beispiel: Du nutzt ORM-Models, aber für eine bestimmte, komplexe Aggregation lieber Core: + +```python +from sqlalchemy import select, func +from sqlalchemy.orm import Session +from .models import User + +def count_users_by_first_letter(db: Session): + stmt = ( + select(func.substr(User.name, 1, 1).label("initial"), func.count()) + .group_by("initial") + .order_by("initial") + ) + return db.execute(stmt).all() +``` + +--- + +## Zusammenfassung + +- **Grundidee:** + SQLAlchemy ist ein flexibles Toolkit, das sowohl eine SQL-nahe Abstraktion (Core) als auch ein mächtiges ORM (Data-Mapper-basiert) bereitstellt. Es überbrückt den Graben zwischen Objektwelt (Python) und relationalen Datenbanken. + +- **Abgrenzung:** + Im Vergleich zu Raw SQL: mehr Komfort, Typsicherheit, Portabilität. + Im Vergleich zu anderen ORMs: framework-agnostisch, sehr flexibel, eher für komplexere Anforderungen optimiert. + +- **Gelöste Probleme:** + Objekt-relationales Mapping, Transaktionsverwaltung (Unit-of-Work), DB-Portabilität, systematischer Query-Aufbau, Integration mit Migrations-Tooling (Alembic). + +- **Herausforderungen:** + Relativ hohe Lernkurve, Session-Handling, Lazy/Eager Loading, Performance-Tuning, Zusammenspiel mit Migrationen. +# # Data Management With Python, SQLite, and SQLAlchemy +[Data Management With Python, SQLite, and SQLAlchemy – Real Python](https://realpython.com/python-sqlite-sqlalchemy/) + +All programs process data in one form or another, and many need to be able to save and retrieve that data from one invocation to the next. Python, [SQLite](https://www.sqlite.org/index.html), and [SQLAlchemy](https://www.sqlalchemy.org/) give your programs database functionality, allowing you to store data in a single file without the need for a database server. + +You can achieve similar results using [flat files](https://en.wikipedia.org/wiki/Flat-file_database) in any number of formats, including CSV, JSON, XML, and even custom formats. Flat files are often human-readable text files—though they can also be binary data—with a structure that can be parsed by a computer program. Below, you’ll explore using SQL databases and flat files for data storage and manipulation and learn how to decide which approach is right for your program. + +**In this tutorial, you’ll learn how to use:** + +- **Flat files** for data storage +- **SQL** to improve access to persistent data +- **SQLite** for data storage +- **SQLAlchemy** to work with data as Python objects + +You can get all of the code and data you’ll see in this tutorial by clicking on the link below: + +**Download the sample code:** [Click here to get the code you’ll use](https://realpython.com/bonus/sqlite-sqlalchemy-code/) to learn about data management with SQLite and SQLAlchemy in this tutorial. + +## Using Flat Files for Data Storage[](https://realpython.com/python-sqlite-sqlalchemy/#using-flat-files-for-data-storage "Permanent link") + +A **flat file** is a file containing data with no internal hierarchy and usually no references to external files. Flat files contain human-readable characters and are very useful for creating and reading data. Because they don’t have to use fixed field widths, flat files often use other structures to make it possible for a program to parse text. + +For example, [comma-separated value (CSV)](https://realpython.com/python-csv/) files are lines of plain text in which the comma character separates the data elements. Each line of text represents a row of data, and each comma-separated value is a field within that row. The comma character delimiter indicates the boundary between data values. + +Python excels at [reading from and saving to files](https://realpython.com/read-write-files-python/). Being able to read data files with Python allows you to restore an application to a useful state when you rerun it at a later time. Being able to save data in a file allows you to share information from the program between users and sites where the application runs. + +Before a program can read a data file, it has to be able to understand the data. Usually, this means the data file needs to have some structure that the application can use to read and parse the text in the file. + +Below is a CSV file named `author_book_publisher.csv`, used by the first example program in this tutorial: + +`first_name,last_name,title,publisher Isaac,Asimov,Foundation,Random House Pearl,Buck,The Good Earth,Random House Pearl,Buck,The Good Earth,Simon & Schuster Tom,Clancy,The Hunt For Red October,Berkley Tom,Clancy,Patriot Games,Simon & Schuster Stephen,King,It,Random House Stephen,King,It,Penguin Random House Stephen,King,Dead Zone,Random House Stephen,King,The Shining,Penguin Random House John,Le Carre,"Tinker, Tailor, Soldier, Spy: A George Smiley Novel",Berkley Alex,Michaelides,The Silent Patient,Simon & Schuster Carol,Shaben,Into The Abyss,Simon & Schuster` + +The first line provides a comma-separated list of fields, which are the column names for the data that follows in the remaining lines. The rest of the lines contain the data, with each line representing a single record. + +**Note:** Though the authors, books, and publishers are all real, the relationships between books and publishers are fictional and were created for the purposes of this tutorial. + +Next, you’ll take a look at some of the advantages and disadvantages of using flat files like the above CSV to work with your data. + +[Remove ads](https://realpython.com/account/join/) + +### Advantages of Flat Files[](https://realpython.com/python-sqlite-sqlalchemy/#advantages-of-flat-files "Permanent link") + +Working with data in flat files is manageable and straightforward to implement. Having the data in a human-readable format is helpful not only for creating the data file with a text editor but also for examining the data and looking for any inconsistencies or problems. + +Many applications can export flat-file versions of the data generated by the file. For example, [Excel](https://realpython.com/openpyxl-excel-spreadsheets-python/) can import or export a CSV file to and from a spreadsheet. Flat files also have the advantage of being self-contained and transferable if you want to share the data. + +Almost every programming language has tools and libraries that make working with CSV files easier. Python has the built-in `csv` module and the powerful [pandas](https://realpython.com/pandas-read-write-files/) module available, making working with CSV files a potent solution. + +### Disadvantages of Flat Files[](https://realpython.com/python-sqlite-sqlalchemy/#disadvantages-of-flat-files "Permanent link") + +The advantages of working with flat files start to diminish as the data becomes larger. Large files are still human-readable, but editing them to create data or look for problems becomes a more difficult task. If your application will change the data in the file, then one solution would be to [read the entire file into memory](https://realpython.com/read-write-files-python/), make the changes, and write the data out to another file. + +Another problem with using flat files is that you’ll need to explicitly create and maintain any relationships between parts of your data and the application program within the file syntax. Additionally, you’ll need to generate code in your application to use those relationships. + +A final complication is that people you want to share your data file with will also need to know about and act on the structures and relationships you’ve created in the data. To access the information, those users will need to understand not only the structure of the data but also the programming tools necessary for accessing it. + +### Flat File Example[](https://realpython.com/python-sqlite-sqlalchemy/#flat-file-example "Permanent link") + +The example program `examples/example_1/main.py` uses the `author_book_publisher.csv` file to get the data and relationships in it. This CSV file maintains a list of authors, the books they’ve published, and the publishers for each of the books. + +**Note:** The data files used in the examples are available in the `project/data` directory. There’s also a program file in the `project/build_data` directory that generates the data. That application is useful if you change the data and want to get back to a known state. + +To get access to the data files used in this section and throughout the tutorial, click on the link below: + +**Download the sample code:** [Click here to get the code you’ll use](https://realpython.com/bonus/sqlite-sqlalchemy-code/) to learn about data management with SQLite and SQLAlchemy in this tutorial. + +The CSV file presented above is a pretty small data file containing only a few authors, books, and publishers. You should also notice some things about the data: + +- The authors Stephen King and Tom Clancy appear more than once because multiple books they’ve published are represented in the data. + +- The authors Stephen King and Pearl Buck have the same book published by more than one publisher. + + +These duplicated data fields create relationships between other parts of the data. One author can write many books, and one publisher can work with multiple authors. Authors and publishers share relationships with individual books. + +The relationships in the `author_book_publisher.csv` file are represented by fields that appear multiple times in different rows of the data file. Because of this data redundancy, the data represents more than a single two-dimensional table. You’ll see more of this when you use the file to create an SQLite database file. + +The example program `examples/example_1/main.py` uses the relationships embedded in the `author_book_publisher.csv` file to generate some data. It first presents a list of the authors and the number of books each has written. It then shows a list of publishers and the number of authors for which each has published books. + +It also uses the [`treelib`](https://treelib.readthedocs.io/en/latest/) module to display a tree hierarchy of the authors, books , and publishers. + +Lastly, it adds a new book to the data and redisplays the tree hierarchy with the new book in place. Here’s the [`main()`](https://realpython.com/courses/python-main-function/) entry-point function for this program: + +`def main(): """The main entry point of the program""" # Get the resources for the program with resources.path( "project.data", "author_book_publisher.csv" ) as filepath: data = get_data(filepath) # Get the number of books printed by each publisher books_by_publisher = get_books_by_publisher(data, ascending=False) for publisher, total_books in books_by_publisher.items(): print(f"Publisher: {publisher}, total books: {total_books}") print() # Get the number of authors each publisher publishes authors_by_publisher = get_authors_by_publisher(data, ascending=False) for publisher, total_authors in authors_by_publisher.items(): print(f"Publisher: {publisher}, total authors: {total_authors}") print() # Output hierarchical authors data output_author_hierarchy(data) # Add a new book to the data structure data = add_new_book( data, author_name="Stephen King", book_title="The Stand", publisher_name="Random House", ) # Output the updated hierarchical authors data output_author_hierarchy(data)` + +The Python code above takes the following steps: + +- **Lines 4 to 7** read the `author_book_publisher.csv` file into a pandas DataFrame. +- **Lines 10 to 13** print the number of books published by each publisher. +- **Lines 16 to 19** print the number of authors associated with each publisher. +- **Line 22** outputs the book data as a hierarchy sorted by authors. +- **Lines 25 to 30** add a new book to the in-memory structure. +- **Line 33** outputs the book data as a hierarchy sorted by authors, including the newly added book. + +Running this program generates the following output: + +`$ python main.py Publisher: Simon & Schuster, total books: 4 Publisher: Random House, total books: 4 Publisher: Penguin Random House, total books: 2 Publisher: Berkley, total books: 2 Publisher: Simon & Schuster, total authors: 4 Publisher: Random House, total authors: 3 Publisher: Berkley, total authors: 2 Publisher: Penguin Random House, total authors: 1 Authors ├── Alex Michaelides │ └── The Silent Patient │ └── Simon & Schuster ├── Carol Shaben │ └── Into The Abyss │ └── Simon & Schuster ├── Isaac Asimov │ └── Foundation │ └── Random House ├── John Le Carre │ └── Tinker, Tailor, Soldier, Spy: A George Smiley Novel │ └── Berkley ├── Pearl Buck │ └── The Good Earth │ ├── Random House │ └── Simon & Schuster ├── Stephen King │ ├── Dead Zone │ │ └── Random House │ ├── It │ │ ├── Penguin Random House │ │ └── Random House │ └── The Shining │ └── Penguin Random House └── Tom Clancy ├── Patriot Games │ └── Simon & Schuster └── The Hunt For Red October └── Berkley` + +The author hierarchy above is presented twice in the output, with the addition of Stephen King’s _The Stand_, published by Random House. The actual output above has been edited and shows only the first hierarchy output to save space. + +`main()` calls other functions to perform the bulk of the work. The first function it calls is `get_data()`: + +`def get_data(filepath): """Get book data from the csv file""" return pd.read_csv(filepath)` + +This function takes in the file path to the CSV file and uses pandas to read it into a [pandas DataFrame](https://realpython.com/pandas-dataframe/), which it then passes back to the caller. The return value of this function becomes the data structure passed to the other functions that make up the program. + +`get_books_by_publisher()` calculates the number of books published by each publisher. The resulting pandas [Series](https://realpython.com/pandas-python-explore-dataset/#understanding-series-objects) uses the pandas [GroupBy](https://realpython.com/pandas-groupby/) functionality to group by publisher and then [sort](https://realpython.com/pandas-sort-python/) based on the `ascending` flag: + +`def get_books_by_publisher(data, ascending=True): """Return the number of books by each publisher as a pandas series""" return data.groupby("publisher").size().sort_values(ascending=ascending)` + +`get_authors_by_publisher()` does essentially the same thing as the previous function, but for authors: + +`def get_authors_by_publisher(data, ascending=True): """Returns the number of authors by each publisher as a pandas series""" return ( data.assign(name=data.first_name.str.cat(data.last_name, sep=" ")) .groupby("publisher") .nunique() .loc[:, "name"] .sort_values(ascending=ascending) )` + +`add_new_book()` creates a new book in the pandas DataFrame. The code checks to see if the author, book, or publisher already exists. If not, then it creates a new book and appends it to the pandas DataFrame: + +`def add_new_book(data, author_name, book_title, publisher_name): """Adds a new book to the system""" # Does the book exist? first_name, _, last_name = author_name.partition(" ") if any( (data.first_name == first_name) & (data.last_name == last_name) & (data.title == book_title) & (data.publisher == publisher_name) ): return data # Add the new book return data.append( { "first_name": first_name, "last_name": last_name, "title": book_title, "publisher": publisher_name, }, ignore_index=True, )` + +`output_author_hierarchy()` uses nested [`for` loops](https://realpython.com/python-for-loop/) to iterate through the levels of the data structure. It then uses the `treelib` module to output a hierarchical listing of the authors, the books they’ve published, and the publishers who’ve published those books: + +`def output_author_hierarchy(data): """Output the data as a hierarchy list of authors""" authors = data.assign( name=data.first_name.str.cat(data.last_name, sep=" ") ) authors_tree = Tree() authors_tree.create_node("Authors", "authors") for author, books in authors.groupby("name"): authors_tree.create_node(author, author, parent="authors") for book, publishers in books.groupby("title")["publisher"]: book_id = f"{author}:{book}" authors_tree.create_node(book, book_id, parent=author) for publisher in publishers: authors_tree.create_node(publisher, parent=book_id) # Output the hierarchical authors data authors_tree.show()` + +This application works well and illustrates the power available to you with the pandas module. The module provides excellent functionality for reading a CSV file and interacting with the data. + +Let’s push on and create an identically functioning program using Python, an SQLite database version of the author and publication data, and SQLAlchemy to interact with that data. + +[Remove ads](https://realpython.com/account/join/) + +## Using SQLite to Persist Data[](https://realpython.com/python-sqlite-sqlalchemy/#using-sqlite-to-persist-data "Permanent link") + +As you saw earlier, there’s redundant data in the `author_book_publisher.csv` file. For example, all information about Pearl Buck’s _The Good Earth_ is listed twice because two different publishers have published the book. + +Imagine if this data file contained more related data, like the author’s address and phone number, publication dates and ISBNs for books, or addresses, phone numbers, and perhaps yearly revenue for publishers. This data would be duplicated for each root data item, like author, book, or publisher. + +It’s possible to create data this way, but it would be exceptionally unwieldy. Think about the problems keeping this data file current. What if [Stephen King wanted to change his name](https://en.wikipedia.org/wiki/Richard_Bachman)? You’d have to update multiple records containing his name and make sure there were no typos. + +Worse than the data duplication would be the complexity of adding other relationships to the data. What if you decided to add phone numbers for the authors, and they had phone numbers for home, work, mobile, and perhaps more? Every new relationship that you’d want to add for any root item would multiply the number of records by the number of items in that new relationship. + +This problem is one reason that relationships exist in database systems. An important topic in database engineering is **database normalization**, or the process of breaking apart data to reduce redundancy and increase integrity. When a database structure is extended with new types of data, having it normalized beforehand keeps changes to the existing structure to a minimum. + +The SQLite database is available in Python, and according to the [SQLite home page](https://www.sqlite.org/index.html), it’s used more than all other database systems combined. It offers a full-featured [relational database management system (RDBMS)](https://en.wikipedia.org/wiki/Relational_database#Relations_or_tables) that works with a single file to maintain all the database functionality. + +It also has the advantage of not requiring a separate database server to function. The database file format is cross-platform and accessible to any programming language that supports SQLite. + +All of this is interesting information, but how is it relevant to the use of flat files for data storage? You’ll find out below! + +### Creating a Database Structure[](https://realpython.com/python-sqlite-sqlalchemy/#creating-a-database-structure "Permanent link") + +The brute force approach to getting the `author_book_publisher.csv` data into an SQLite database would be to create a single table matching the structure of the CSV file. Doing this would ignore a good deal of SQLite’s power. + +**Relational databases** provide a way to store structured data in tables and establish relationships between those tables. They usually use [Structured Query Language (SQL)](https://en.wikipedia.org/wiki/SQL) as the primary way to interact with the data. This is an oversimplification of what RDBMSs provide, but it’s sufficient for the purposes of this tutorial. + +An SQLite database provides support for interacting with the data table using SQL. Not only does an SQLite database file contain the data, but it also has a standardized way to interact with the data. This support is embedded in the file, meaning that any programming language that can use an SQLite file can also use SQL to work with it. + +### Interacting With a Database With SQL[](https://realpython.com/python-sqlite-sqlalchemy/#interacting-with-a-database-with-sql "Permanent link") + +SQL is a **declarative language** used to create, manage, and query the data contained in a database. A declarative language describes _what_ is to be accomplished rather than _how_ it should be accomplished. You’ll see examples of SQL statements later when you get to creating database tables. + +## Structuring a Database With SQL[](https://realpython.com/python-sqlite-sqlalchemy/#structuring-a-database-with-sql "Permanent link") + +To take advantage of the power in SQL, you’ll need to apply some database normalization to the data in the `author_book_publisher.csv` file. To do this, you’ll separate the authors, books, and publishers into separate database tables. + +Conceptually, data is stored in the database in two-dimensional table structures. Each table consists of rows of **records**, and each record consists of columns, or **fields**, containing data. + +The data contained in the fields is of pre-defined types, including text, [integers, floats](https://realpython.com/python-numbers/#integers-and-floating-point-numbers), and more. CSV files are different because all the fields are text and must be parsed by a program to have a data type assigned to them. + +Each record in the table has a **primary key** defined to give a record a unique identifier. The primary key is similar to the key in a [Python dictionary](https://realpython.com/python-dicts/). The database engine itself often generates the primary key as an incrementing integer value for every record inserted into the database table. + +Though the primary key is often automatically generated by the database engine, it doesn’t have to be. If the data stored in a field is unique across all other data in the table in that field, then it can be the primary key. For example, a table containing data about books could use the book’s ISBN as the primary key. + +[Remove ads](https://realpython.com/account/join/) + +### Creating Tables With SQL[](https://realpython.com/python-sqlite-sqlalchemy/#creating-tables-with-sql "Permanent link") + +Here’s how you can create the three tables representing the authors, books, and publishers in the CSV file using SQL statements: + +`CREATE TABLE author ( author_id INTEGER NOT NULL PRIMARY KEY, first_name VARCHAR, last_name VARCHAR ); CREATE TABLE book ( book_id INTEGER NOT NULL PRIMARY KEY, author_id INTEGER REFERENCES author, title VARCHAR ); CREATE TABLE publisher ( publisher_id INTEGER NOT NULL PRIMARY KEY, name VARCHAR );` + +Notice there are no file operations, no variables created, and no structures to hold them. The statements describe only the desired result: the creation of a table with particular attributes. The database engine determines how to do this. + +Once you’ve created and populated this table with author data from the `author_book_publisher.csv` file, you can access it using SQL statements. The following statement (also called a **query**) uses the wildcard character (`*`) to get all the data in the `author` table and output it: + +`SELECT * FROM author;` + +You can use the [`sqlite3`](https://sqlite.org/cli.html) command-line tool to interact with the `author_book_publisher.db` database file in the `project/data` directory: + +`$ sqlite3 author_book_publisher.db` + +Once the SQLite command-line tool is running with the database open, you can enter SQL commands. Here’s the above SQL command and its output, followed by the `.q` command to exit the program: + +`sqlite> SELECT * FROM author; 1|Isaac|Asimov 2|Pearl|Buck 3|Tom|Clancy 4|Stephen|King 5|John|Le Carre 6|Alex|Michaelides 7|Carol|Shaben sqlite> .q` + +Notice that each author exists only once in the table. Unlike the CSV file, which had multiple entries for some of the authors, here, only one unique record per author is necessary. + +### Maintaining a Database With SQL[](https://realpython.com/python-sqlite-sqlalchemy/#maintaining-a-database-with-sql "Permanent link") + +SQL provides ways to work with existing databases and tables by inserting new data and updating or deleting existing data. Here’s an example SQL statement for inserting a new author into the `author` table: + +`INSERT INTO author (first_name, last_name) VALUES ('Paul', 'Mendez');` + +This SQL statement inserts the values ‘`Paul`’ and ‘`Mendez`’ into the respective columns `first_name` and `last_name` of the `author` table. + +Notice that the `author_id` column isn’t specified. Because that column is the primary key, the database engine generates the value and inserts it as part of the statement execution. + +Updating records in a database table is an uncomplicated process. For instance, suppose Stephen King wanted to be known by his pen name, Richard Bachman. Here’s an the SQL statement to update the database record: + +`UPDATE author SET first_name = 'Richard', last_name = 'Bachman' WHERE first_name = 'Stephen' AND last_name = 'King';` + +The SQL statement locates the single record for `'Stephen King'` using the conditional statement `WHERE first_name = 'Stephen' AND last_name = 'King'` and then updates the `first_name` and `last_name` fields with the new values. SQL uses the equals sign (`=`) as both the comparison operator and [assignment operator](https://realpython.com/python-assignment-operator/). + +You can also delete records from a database. Here’s an example SQL statement to delete a record from the `author` table: + +`DELETE FROM author WHERE first_name = 'Paul' AND last_name = 'Mendez';` + +This SQL statement deletes a single row from the `author` table where the `first_name` is equal to `'Paul'` and the `last_name` is equal to `'Mendez'`. + +Be careful when deleting records! The conditions you set must be as specific as possible. A conditional that’s too broad can lead to deleting more records than you intend. For example, if the condition were based only on the line `first_name = 'Paul'`, then all authors with a first name of Paul would be deleted from the database. + +**Note:** To avoid the accidental deletion of records, many applications don’t allow deletions at all. Instead, the record has another column to indicate if it’s in use or not. This column might be named `active` and contain a value that evaluates to either True or False, indicating whether the record should be included when querying the database. + +For example, the SQL query below would get all columns for all active records in `some_table`: + +`SELECT * FROM some_table WHERE active = 1;` + +SQLite doesn’t have a [Boolean data type](https://realpython.com/python-boolean/), so the `active` column is represented by an integer with a value of `0` or `1` to indicate the state of the record. Other database systems may or may not have native Boolean data types. + +It’s entirely possible to build database applications in Python using SQL statements directly in the code. Doing so returns data to the application as a list of [lists](https://realpython.com/python-lists-tuples/) or list of [dictionaries](https://realpython.com/courses/dictionaries-python/). + +Using raw SQL is a perfectly acceptable way to work with the data returned by queries to the database. However, rather than doing that, you’re going to move directly into using SQLAlchemy to work with databases. + +[Remove ads](https://realpython.com/account/join/) + +## Building Relationships[](https://realpython.com/python-sqlite-sqlalchemy/#building-relationships "Permanent link") + +Another feature of database systems that you might find even more powerful and useful than data persistence and retrieval is **relationships**. Databases that support relationships allow you to break up data into multiple tables and establish connections between them. + +The data in the `author_book_publisher.csv` file represents the data and relationships by duplicating data. A database handles this by breaking the data up into three tables—`author`, `book`, and `publisher`—and establishing relationships between them. + +After getting all the data you want into one place in the CSV file, why would you want to break it up into multiple tables? Wouldn’t it be more work to create and put back together again? That’s true to an extent, but the advantages of breaking up the data and putting it back together using SQL could win you over! + +### One-to-Many Relationships[](https://realpython.com/python-sqlite-sqlalchemy/#one-to-many-relationships "Permanent link") + +A **one-to-many** relationship is like that of a customer ordering items online. One customer can have many orders, but each order belongs to one customer. The `author_book_publisher.db` database has a one-to-many relationship in the form of authors and books. Each author can write many books, but each book is written by one author. + +As you saw in the table creation above, the implementation of these separate entities is to place each into a database table, one for authors and one for books. But how does the one-to-many relationship between these two tables get implemented? + +Remember, each table in a database has a field designated as the primary key for that table. Each table above has a primary key field named using this pattern: `_id`. + +The `book` table shown above contains a field, `author_id`, that references the `author` table. The `author_id` field establishes a one-to-many relationship between authors and books that looks like this: + +[![ERD diagram for the author_book relationship produced with JetBrains DataGrip application](https://files.realpython.com/media/author_book.e9c86f34967d.png)](https://files.realpython.com/media/author_book.e9c86f34967d.png) + +The diagram above is a simple [entity-relationship diagram (ERD)](https://en.wikipedia.org/wiki/Entity%E2%80%93relationship_model) created with the [JetBrains DataGrip](https://www.jetbrains.com/datagrip/features/) application showing the tables `author` and `book` as boxes with their respective primary key and data fields. Two graphical items add information about the relationship: + +1. **The small yellow and blue key icons** indicate the primary and foreign keys for the table, respectively. + +2. **The arrow connecting `book` to `author`** indicates the relationship between the tables based on the `author_id` foreign key in the `book` table. + + +When you add a new book to the `book` table, the data includes an `author_id` value for an existing author in the `author` table. In this way, all the books written by an author have a lookup relationship back to that unique author. + +Now that you have separate tables for authors and books, how do you use the relationship between them? SQL supports what’s called a [`JOIN`](https://realpython.com/python-sql-libraries/#join) operation, which you can use to tell the database how to connect two or more tables. + +The SQL query below joins the `author` and `book` table together using the SQLite command-line application: + +`sqlite> SELECT ...> a.first_name || ' ' || a.last_name AS author_name, ...> b.title AS book_title ...> FROM author a ...> JOIN book b ON b.author_id = a.author_id ...> ORDER BY a.last_name ASC; Isaac Asimov|Foundation Pearl Buck|The Good Earth Tom Clancy|The Hunt For Red October Tom Clancy|Patriot Games Stephen King|It Stephen King|Dead Zone Stephen King|The Shining John Le Carre|Tinker, Tailor, Soldier, Spy: A George Smiley Novel Alex Michaelides|The Silent Patient Carol Shaben|Into The Abyss` + +The SQL query above gathers information from both the author and book table by joining the tables using the relationship established between the two. SQL [string concatenation](https://realpython.com/python-string-concatenation/) assigns the author’s full name to the alias `author_name`. The data gathered by the query are sorted in ascending order by the `last_name` field. + +There are a few things to notice in the SQL statement. First, authors are presented by their full names in a single column and sorted by their last names. Also, authors appear in the output multiple times because of the one-to-many relationship. An author’s name is duplicated for each book they’ve written in the database. + +By creating separate tables for authors and books and establishing the relationship between them, you’ve reduced redundancy in the data. Now you only have to edit an author’s data in one place, and that change appears in any SQL query accessing the data. + +### Many-to-Many Relationships[](https://realpython.com/python-sqlite-sqlalchemy/#many-to-many-relationships "Permanent link") + +**Many-to-many** relationships exist in the `author_book_publisher.db` database between authors and publishers as well as between books and publishers. One author can work with many publishers, and one publisher can work with many authors. Similarly, one book can be published by many publishers, and one publisher can publish many books. + +Handling this situation in the database is more involved than a one-to-many relationship because the relationship goes both ways. Many-to-many relationships are created by an **association table** acting as a bridge between the two related tables. + +The association table contains at least two foreign key fields, which are the primary keys of each of the two associated tables. This SQL statement creates the association table relating the `author` and `publisher` tables: + +`CREATE TABLE author_publisher ( author_id INTEGER REFERENCES author, publisher_id INTEGER REFERENCES publisher );` + +The SQL statements create a new `author_publisher` table referencing the primary keys of the existing `author` and `publisher` tables. The `author_publisher` table is an association table establishing relationships between an author and a publisher. + +Because the relationship is between two primary keys, there’s no need to create a primary key for the association table itself. The combination of the two related keys creates a unique identifier for a row of data. + +As before, you use the `JOIN` keyword to connect two tables together. Connecting the `author` table to the `publisher` table is a two-step process: + +1. `JOIN` the `author` table with the `author_publisher` table. +2. `JOIN` the `author_publisher` table with the `publisher` table. + +The `author_publisher` association table provides the bridge through which the `JOIN` connects the two tables. Here’s an example SQL query returning a list of authors and the publishers publishing their books: + +`sqlite> SELECT ...> a.first_name || ' ' || a.last_name AS author_name, ...> p.name AS publisher_name ...> FROM author a ...> JOIN author_publisher ap ON ap.author_id = a.author_id ...> JOIN publisher p ON p.publisher_id = ap.publisher_id ...> ORDER BY a.last_name ASC; Isaac Asimov|Random House Pearl Buck|Random House Pearl Buck|Simon & Schuster Tom Clancy|Berkley Tom Clancy|Simon & Schuster Stephen King|Random House Stephen King|Penguin Random House John Le Carre|Berkley Alex Michaelides|Simon & Schuster Carol Shaben|Simon & Schuster` + +The statements above perform the following actions: + +- **Line 1** starts a `SELECT` statement to get data from the database. + +- **Line 2** selects the first and last name from the `author` table using the `a` alias for the `author` table and concatenates them together with a space character. + +- **Line 3** selects the publisher’s name aliased to `publisher_name`. + +- **Line 4** uses the `author` table as the first source from which to retrieve data and assigns it to the alias `a`. + +- **Line 5** is the first step of the process outlined above for connecting the `author` table to the `publisher` table. It uses the alias `ap` for the `author_publisher` association table and performs a `JOIN` operation to connect the `ap.author_id` foreign key reference to the `a.author_id` primary key in the `author` table. + +- **Line 6** is the second step in the two-step process mentioned above. It uses the alias `p` for the `publisher` table and performs a `JOIN` operation to relate the `ap.publisher_id` foreign key reference to the `p.publisher_id` primary key in the `publisher` table. + +- **Line 7** sorts the data by the author’s last name in ascending alphabetical order and ends the SQL query. + +- **Lines 8 to 17** are the output of the SQL query. + + +Note that the data in the source `author` and `publisher` tables are normalized, with no data duplication. Yet the returned results have duplicated data where necessary to answer the SQL query. + +The SQL query above demonstrates how to make use of a relationship using the SQL `JOIN` keyword, but the resulting data is a partial re-creation of the `author_book_publisher.csv` CSV data. What’s the win for having done the work of creating a database to separate the data? + +Here’s another SQL query to show a little bit of the power of SQL and the database engine: + +`sqlite> SELECT ...> a.first_name || ' ' || a.last_name AS author_name, ...> COUNT(b.title) AS total_books ...> FROM author a ...> JOIN book b ON b.author_id = a.author_id ...> GROUP BY author_name ...> ORDER BY total_books DESC, a.last_name ASC; Stephen King|3 Tom Clancy|2 Isaac Asimov|1 Pearl Buck|1 John Le Carre|1 Alex Michaelides|1 Carol Shaben|1` + +The SQL query above returns the list of authors and the number of books they’ve written. The list is sorted first by the number of books in descending order, then by the author’s name in alphabetical order: + +- **Line 1** begins the SQL query with the `SELECT` keyword. + +- **Line 2** selects the author’s first and last names, separated by a space character, and creates the alias `author_name`. + +- **Line 3** counts the number of books written by each author, which will be used later by the `ORDER BY` clause to sort the list. + +- **Line 4** selects the `author` table to get data from and creates the `a` alias. + +- **Line 5** connects to the related `book` table through a `JOIN` to the `author_id` and creates the `b` alias for the `book` table. + +- **Line 6** generates the aggregated author and total number of books data by using the `GROUP BY` keyword. `GROUP BY` is what groups each `author_name` and controls what books are tallied by `COUNT()` for that author. + +- **Line 7** sorts the output first by number of books in descending order, then by the author’s last name in ascending alphabetical order. + +- **Lines 8 to 14** are the output of the SQL query. + + +In the above example, you take advantage of SQL to perform aggregation calculations and sort the results into a useful order. Having the database perform calculations based on its built-in data organization ability is usually faster than performing the same kinds of calculations on raw data sets in Python. SQL offers the advantages of using [set theory](https://www.sqlshack.com/mathematics-sql-server-fast-introduction-set-theory/) embedded in RDBMS databases. + +[Remove ads](https://realpython.com/account/join/) + +### Entity Relationship Diagrams[](https://realpython.com/python-sqlite-sqlalchemy/#entity-relationship-diagrams "Permanent link") + +An [entity-relationship diagram (ERD)](https://en.wikipedia.org/wiki/Entity%E2%80%93relationship_model) is a visual depiction of an entity-relationship model for a database or part of a database. The `author_book_publisher.db` SQLite database is small enough that the entire database can be visualized by the diagram shown below: + +[![ERD diagram for the author_book_publisher Sqlite database produced with JetBrains DataGrip application](https://files.realpython.com/media/author_book_publisher.fbc88687deeb.png)](https://files.realpython.com/media/author_book_publisher.fbc88687deeb.png) + +This diagram presents the table structures in the database and the relationships between them. Each box represents a table and contains the fields defined in the table, with the primary key indicated first if it exists. + +The arrows show the relationships between the tables connecting a foreign key field in one table to a field, often the primary key, in another table. The table `book_publisher` has two arrows, one connecting it to the `book` table and another connecting it to the `publisher` table. The arrow indicates the many-to-many relationship between the `book` and `publisher` tables. The `author_publisher` table provides the same relationship between `author` and `publisher`. + +## Working With SQLAlchemy and Python Objects[](https://realpython.com/python-sqlite-sqlalchemy/#working-with-sqlalchemy-and-python-objects "Permanent link") + +[SQLAlchemy](https://www.sqlalchemy.org/) is a powerful database access tool kit for Python, with its [object-relational mapper (ORM)](https://en.wikipedia.org/wiki/Object-relational_mapping) being one of its most famous components, and the one discussed and used here. + +When you’re working in an [object-oriented](https://realpython.com/python3-object-oriented-programming/) language like Python, it’s often useful to think in terms of objects. It’s possible to map the results returned by SQL queries to objects, but doing so works against the grain of how the database works. Sticking with the scalar results provided by SQL works against the grain of how Python developers work. This problem is known as [object-relational impedance mismatch](https://en.wikipedia.org/wiki/Object-relational_impedance_mismatch). + +The ORM provided by SQLAlchemy sits between the SQLite database and your Python program and transforms the data flow between the database engine and Python objects. SQLAlchemy allows you to think in terms of objects and still retain the powerful features of a database engine. + +### The Model[](https://realpython.com/python-sqlite-sqlalchemy/#the-model "Permanent link") + +One of the fundamental elements to enable connecting SQLAlchemy to a database is creating a **model**. The model is a [Python class](https://realpython.com/python-classes/) defining the data mapping between the Python objects returned as a result of a database query and the underlying database tables. + +The entity-relationship diagram displayed earlier shows boxes connected with arrows. The boxes are the tables built with the SQL commands and are what the Python classes will model. The arrows are the relationships between the tables. + +The models are Python classes inheriting from an SQLAlchemy `Base` class. The `Base` class provides the interface operations between instances of the model and the database table. + +Below is the `models.py` file that creates the models to represent the `author_book_publisher.db` database: + +`from sqlalchemy import Column, Integer, String, ForeignKey, Table from sqlalchemy.orm import relationship, backref from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() author_publisher = Table( "author_publisher", Base.metadata, Column("author_id", Integer, ForeignKey("author.author_id")), Column("publisher_id", Integer, ForeignKey("publisher.publisher_id")), ) book_publisher = Table( "book_publisher", Base.metadata, Column("book_id", Integer, ForeignKey("book.book_id")), Column("publisher_id", Integer, ForeignKey("publisher.publisher_id")), ) class Author(Base): __tablename__ = "author" author_id = Column(Integer, primary_key=True) first_name = Column(String) last_name = Column(String) books = relationship("Book", backref=backref("author")) publishers = relationship( "Publisher", secondary=author_publisher, back_populates="authors" ) class Book(Base): __tablename__ = "book" book_id = Column(Integer, primary_key=True) author_id = Column(Integer, ForeignKey("author.author_id")) title = Column(String) publishers = relationship( "Publisher", secondary=book_publisher, back_populates="books" ) class Publisher(Base): __tablename__ = "publisher" publisher_id = Column(Integer, primary_key=True) name = Column(String) authors = relationship( "Author", secondary=author_publisher, back_populates="publishers" ) books = relationship( "Book", secondary=book_publisher, back_populates="publishers" )` + +Here’s what’s going on in this module: + +- **Line 1** imports the `Column`, `Integer`, `String`, `ForeignKey`, and `Table` classes from SQLAlchemy, which are used to help define the model attributes. + +- **Line 2** imports the `relationship()` and `backref` objects, which are used to create the relationships between objects. + +- **Line 3** imports the `declarative_base` object, which connects the database engine to the SQLAlchemy functionality of the models. + +- **Line 5** creates the `Base` class, which is what all models inherit from and how they get SQLAlchemy ORM functionality. + +- **Lines 7 to 12** create the `author_publisher` association table model. + +- **Lines 14 to 19** create the `book_publisher` association table model. + +- **Lines 21 to 29** define the `Author` class model to the `author` database table. + +- **Lines 31 to 38** define the `Book` class model to the `book` database table. + +- **Lines 40 to 49** define the `Publisher` class model to the `publisher` database table. + + +The description above shows the mapping of the five tables in the `author_book_publisher.db` database. But it glosses over some SQLAlchemy ORM features, including `Table`, `ForeignKey`, `relationship()`, and `backref`. Let’s get into those now. + +### `Table` Creates Associations[](https://realpython.com/python-sqlite-sqlalchemy/#table-creates-associations "Permanent link") + +`author_publisher` and `book_publisher` are both instances of the `Table` class that create the many-to-many association tables used between the `author` and `publisher` tables and the `book` and `publisher` tables, respectively. + +The SQLAlchemy `Table` class creates a unique instance of an ORM mapped table within the database. The first parameter is the table name as defined in the database, and the second is `Base.metadata`, which provides the connection between the SQLAlchemy functionality and the database engine. + +The rest of the parameters are instances of the `Column` class defining the table fields by name, their type, and in the example above, an instance of a `ForeignKey`. + +[Remove ads](https://realpython.com/account/join/) + +### `ForeignKey` Creates a Connection[](https://realpython.com/python-sqlite-sqlalchemy/#foreignkey-creates-a-connection "Permanent link") + +The SQLAlchemy **`ForeignKey`** class defines a dependency between two `Column` fields in different tables. A `ForeignKey` is how you make SQLAlchemy aware of the relationships between tables. For example, this line from the `author_publisher` instance creation establishes a foreign key relationship: + +`Column("author_id", Integer, ForeignKey("author.author_id"))` + +The statement above tells SQLAlchemy that there’s a column in the `author_publisher` table named `author_id`. The type of that column is `Integer`, and `author_id` is a foreign key related to the primary key in the `author` table. + +Having both `author_id` and `publisher_id` defined in the `author_publisher` `Table` instance creates the connection from the `author` table to the `publisher` table and vice versa, establishing a many-to-many relationship. + +### `relationship()` Establishes a Collection[](https://realpython.com/python-sqlite-sqlalchemy/#relationship-establishes-a-collection "Permanent link") + +Having a `ForeignKey` defines the existence of the relationship between tables but not the collection of books an author can have. Take a look at this line in the `Author` class definition: + +`books = relationship("Book", backref=backref("author"))` + +The code above defines a parent-child collection. The `books` attribute being plural (which is not a requirement, just a convention) is an indication that it’s a collection. + +The first parameter to `relationship()`, the class name `Book` (which is _not_ the table name `book`), is the class to which the `books` attribute is related. The `relationship` informs SQLAlchemy that there’s a relationship between the `Author` and `Book` classes. SQLAlchemy will find the relationship in the `Book` class definition: + +`author_id = Column(Integer, ForeignKey("author.author_id"))` + +SQLAlchemy recognizes that this is the `ForeignKey` connection point between the two classes. You’ll get to the `backref` parameter in `relationship()` in a moment. + +The other relationship in `Author` is to the `Publisher` class. This is created with the following statement in the `Author` class definition: + +`publishers = relationship( "Publisher", secondary=author_publisher, back_populates="authors" )` + +Like `books`, the attribute `publishers` indicates a collection of publishers associated with an author. The first parameter, `"Publisher"`, informs SQLAlchemy what the related class is. The second and third parameters are `secondary=author_publisher` and `back_populates="authors"`: + +- **`secondary`** tells SQLAlchemy that the relationship to the `Publisher` class is through a secondary table, which is the `author_publisher` association table created earlier in `models.py`. The `secondary` parameter makes SQLAlchemy find the `publisher_id` `ForeignKey` defined in the `author_publisher` association table. + +- **`back_populates`** is a convenience configuration telling SQLAlchemy that there’s a complementary collection in the `Publisher` class called `authors`. + + +### `backref` Mirrors Attributes[](https://realpython.com/python-sqlite-sqlalchemy/#backref-mirrors-attributes "Permanent link") + +The **`backref`** parameter of the `books` collection `relationship()` creates an `author` attribute for each `Book` instance. This attribute refers to the parent `Author` that the `Book` instance is related to. + +For example, if you executed the following Python code, then a `Book` instance would be returned from the SQLAlchemy query. The `Book` instance has attributes that can be used to print out information about the book: + +`book = session.query(Book).filter_by(Book.title == "The Stand").one_or_none() print(f"Authors name: {book.author.first_name} {book.author.last_name}")` + +The existence of the `author` attribute in the `Book` above is because of the `backref` definition. A `backref` can be very handy to have when you need to refer to the parent and all you have is a child instance. + +[Remove ads](https://realpython.com/account/join/) + +### Queries Answer Questions[](https://realpython.com/python-sqlite-sqlalchemy/#queries-answer-questions "Permanent link") + +You can make a basic query like `SELECT * FROM author;` in SQLAlchemy like this: + +`results = session.query(Author).all()` + +The **`session`** is an SQLAlchemy object used to communicate with SQLite in the Python example programs. Here, you tell the session you want to execute a query against the `Author` model and return all records. + +At this point, the advantages of using SQLAlchemy instead of plain SQL might not be obvious, especially considering the setup required to create the models representing the database. The `results` returned by the query is where the magic happens. Instead of getting back a list of lists of scalar data, you’ll get back a list of instances of `Author` objects with attributes matching the column names you defined. + +The `books` and `publishers` collections maintained by SQLAlchemy create a hierarchical list of authors and the books they’ve written as well as the publishers who’ve published them. + +Behind the scenes, SQLAlchemy turns the object and method calls into SQL statements to execute against the SQLite database engine. SQLAlchemy transforms the data returned by SQL queries into Python objects. + +With SQLAlchemy, you can perform the more complex aggregation query shown earlier for the list of authors and the number of books they’ve written like this: + +`author_book_totals = ( session.query( Author.first_name, Author.last_name, func.count(Book.title).label("book_total") ) .join(Book) .group_by(Author.last_name) .order_by(desc("book_total")) .all() )` + +The query above gets the author’s first and last name, along with a count of the number of books that the author has written. The aggregating `count` used by the `group_by` clause is based on the author’s last name. Finally, the results are sorted in descending order based on the aggregated and aliased `book_total`. + +### Example Program[](https://realpython.com/python-sqlite-sqlalchemy/#example-program "Permanent link") + +The example program `examples/example_2/main.py` has the same functionality as `examples/example_1/main.py` but uses SQLAlchemy exclusively to interface with the `author_book_publisher.db` SQLite database. The program is broken up into the `main()` function and the functions it calls: + +`def main(): """Main entry point of program""" # Connect to the database using SQLAlchemy with resources.path( "project.data", "author_book_publisher.db" ) as sqlite_filepath: engine = create_engine(f"sqlite:///{sqlite_filepath}") Session = sessionmaker() Session.configure(bind=engine) session = Session() # Get the number of books printed by each publisher books_by_publisher = get_books_by_publishers(session, ascending=False) for row in books_by_publisher: print(f"Publisher: {row.name}, total books: {row.total_books}") print() # Get the number of authors each publisher publishes authors_by_publisher = get_authors_by_publishers(session) for row in authors_by_publisher: print(f"Publisher: {row.name}, total authors: {row.total_authors}") print() # Output hierarchical author data authors = get_authors(session) output_author_hierarchy(authors) # Add a new book add_new_book( session, author_name="Stephen King", book_title="The Stand", publisher_name="Random House", ) # Output the updated hierarchical author data authors = get_authors(session) output_author_hierarchy(authors)` + +This program is a modified version of `examples/example_1/main.py`. Let’s go over the differences: + +- **Lines 4 to 7** first initialize the `sqlite_filepath` variable to the database file path. Then they create the `engine` variable to communicate with SQLite and the `author_book_publisher.db` database file, which is SQLAlchemy’s access point to the database. + +- **Line 8** creates the `Session` class from the SQLAlchemy’s `sessionmaker()`. + +- **Line 9** binds the `Session` to the engine created in line 8. + +- **Line 10** creates the `session` instance, which is used by the program to communicate with SQLAlchemy. + + +The rest of the function is similar, except for the replacement of `data` with `session` as the first parameter to all the functions called by `main()`. + +`get_books_by_publisher()` has been refactored to use SQLAlchemy and the models you defined earlier to get the data requested: + +`def get_books_by_publishers(session, ascending=True): """Get a list of publishers and the number of books they've published""" if not isinstance(ascending, bool): raise ValueError(f"Sorting value invalid: {ascending}") direction = asc if ascending else desc return ( session.query( Publisher.name, func.count(Book.title).label("total_books") ) .join(Publisher.books) .group_by(Publisher.name) .order_by(direction("total_books")) )` + +Here’s what the new function, `get_books_by_publishers()`, is doing: + +- **Line 6** creates the `direction` variable and sets it equal to the SQLAlchemy `desc` or `asc` function depending on the value of the `ascending` parameter. + +- **Lines 9 to 11** query the `Publisher` table for data to return, which in this case are `Publisher.name` and the aggregate total of `Book` objects associated with an author, aliased to `total_books`. + +- **Line 12** joins to the `Publisher.books` collection. + +- **Line 13** aggregates the book counts by the `Publisher.name` attribute. + +- **Line 14** sorts the output by the book counts according to the operator defined by `direction`. + +- **Line 15** closes the object, executes the query, and returns the results to the caller. + + +All the above code expresses what is wanted rather than how it’s to be retrieved. Now instead of using SQL to describe what’s wanted, you’re using Python objects and methods. What’s returned is a list of Python objects instead of a list of tuples of data. + +`get_authors_by_publisher()` has also been modified to work exclusively with SQLAlchemy. Its functionality is very similar to the previous function, so a function description is omitted: + +`def get_authors_by_publishers(session, ascending=True): """Get a list of publishers and the number of authors they've published""" if not isinstance(ascending, bool): raise ValueError(f"Sorting value invalid: {ascending}") direction = asc if ascending else desc return ( session.query( Publisher.name, func.count(Author.first_name).label("total_authors"), ) .join(Publisher.authors) .group_by(Publisher.name) .order_by(direction("total_authors")) )` + +`get_authors()` has been added to get a list of authors sorted by their last names. The result of this query is a list of `Author` objects containing a collection of books. The `Author` objects already contain hierarchical data, so the results don’t have to be reformatted: + +`def get_authors(session): """Get a list of author objects sorted by last name""" return session.query(Author).order_by(Author.last_name).all()` + +Like its previous version, `add_new_book()` is relatively complex but straightforward to understand. It determines if a book with the same title, author, and publisher exists in the database already. + +If the search query finds an exact match, then the function returns. If no book matches the exact search criteria, then it searches to see if the author has written a book using the passed in title. This code exists to prevent duplicate books from being created in the database. + +If no matching book exists, and the author hasn’t written one with the same title, then a new book is created. The function then retrieves or creates an author and publisher. Once instances of the `Book`, `Author` and `Publisher` exist, the relationships between them are created, and the resulting information is saved to the database: + +`def add_new_book(session, author_name, book_title, publisher_name): """Adds a new book to the system""" # Get the author's first and last names first_name, _, last_name = author_name.partition(" ") # Check if book exists book = ( session.query(Book) .join(Author) .filter(Book.title == book_title) .filter( and_( Author.first_name == first_name, Author.last_name == last_name ) ) .filter(Book.publishers.any(Publisher.name == publisher_name)) .one_or_none() ) # Does the book by the author and publisher already exist? if book is not None: return # Get the book by the author book = ( session.query(Book) .join(Author) .filter(Book.title == book_title) .filter( and_( Author.first_name == first_name, Author.last_name == last_name ) ) .one_or_none() ) # Create the new book if needed if book is None: book = Book(title=book_title) # Get the author author = ( session.query(Author) .filter( and_( Author.first_name == first_name, Author.last_name == last_name ) ) .one_or_none() ) # Do we need to create the author? if author is None: author = Author(first_name=first_name, last_name=last_name) session.add(author) # Get the publisher publisher = ( session.query(Publisher) .filter(Publisher.name == publisher_name) .one_or_none() ) # Do we need to create the publisher? if publisher is None: publisher = Publisher(name=publisher_name) session.add(publisher) # Initialize the book relationships book.author = author book.publishers.append(publisher) session.add(book) # Commit to the database session.commit()` + +The code above is relatively long. Let’s break the functionality down to manageable sections: + +- **Lines 7 to 18** set the `book` variable to an instance of a `Book` if a book with the same title, author, and publisher is found. Otherwise, they set `book` to `None`. + +- **Lines 20 and 21** determine if the book already exists and return if it does. + +- **Lines 24 to 37** set the `book` variable to an instance of a `Book` if a book with the same title and author is found. Otherwise, they create a new `Book` instance. + +- **Lines 40 to 52** set the `author` variable to an existing author, if found, or create a new `Author` instance based on the passed-in author name. + +- **Lines 55 to 63** set the `publisher` variable to an existing publisher, if found, or create a new `Publisher` instance based on the passed-in publisher name. + +- **Line 66** sets the `book.author` instance to the `author` instance. This creates the relationship between the author and the book, which SQLAlchemy will create in the database when the session is committed. + +- **Line 67** adds the `publisher` instance to the `book.publishers` collection. This creates the many-to-many relationship between the `book` and `publisher` tables. SQLAlchemy will create references in the tables as well as in the `book_publisher` association table that connects the two. + +- **Line 68** adds the `Book` instance to the session, making it part of the session’s unit of work. + +- **Line 71** commits all the creations and updates to the database. + + +There are a few things to take note of here. First, there’s is no mention of the `author_publisher` or `book_publisher` association tables in either the queries or the creations and updates. Because of the work you did in `models.py` setting up the relationships, SQLAlchemy can handle connecting objects together and keeping those tables in sync during creations and updates. + +Second, all the creations and updates happen within the context of the `session` object. None of that activity is touching the database. Only when the `session.commit()` statement executes does the session then go through its [unit of work](https://www.martinfowler.com/eaaCatalog/unitOfWork.html) and commit that work to the database. + +For example, if a new `Book` instance is created (as in line 37 above), then the book has its attributes initialized except for the `book_id` primary key and `author_id` foreign key. Because no database activity has happened yet, the `book_id` is unknown, and nothing was done in the instantiation of `book` to give it an `author_id`. + +When `session.commit()` is executed, one of the things it will do is insert `book` into the database, at which point the database will create the `book_id` primary key. The session will then initialize the `book.book_id` value with the primary key value created by the database engine. + +`session.commit()` is also aware of the insertion of the `Book` instance in the `author.books` collection. The `author` object’s `author_id` primary key will be added to the `Book` instance appended to the `author.books` collection as the `author_id` foreign key. + +[Remove ads](https://realpython.com/account/join/) + +## Providing Access to Multiple Users[](https://realpython.com/python-sqlite-sqlalchemy/#providing-access-to-multiple-users "Permanent link") + +To this point, you’ve seen how to use pandas, SQLite, and SQLAlchemy to access the same data in different ways. For the relatively straightforward use case of the author, book, and publisher data, it could still be a toss-up whether you should use a database. + +One deciding factor when choosing between using a flat file or a database is data and relationship complexity. If the data for each entity is complicated and contains many relationships between the entities, then creating and maintaining it in a flat file might become more difficult. + +Another factor to consider is whether you want to share the data between multiple users. The solution to this problem might be as simple as using a [sneakernet](https://en.wikipedia.org/wiki/Sneakernet) to physically move data between users. Moving data files around this way has the advantage of ease of use, but the data can quickly get out of sync when changes are made. + +The problem of keeping the data consistent for all users becomes even more difficult if the users are remote and want to access the data across networks. Even when you’re limited to a single language like Python and using pandas to access the data, network file locking isn’t sufficient to ensure the data doesn’t get corrupted. + +Providing the data through a server application and a user interface alleviates this problem. The server is the only application that needs file-level access to the database. By using a database, the server can take advantage of SQL to access the data using a consistent interface no matter what programming language the server uses. + +The last example program demonstrates this by providing a web application and user interface to the [Chinook](https://www.sqlitetutorial.net/sqlite-sample-database/) sample SQLite database. Peter Stark generously maintains the Chinook database as part of the [SQLite Tutorial](https://www.sqlitetutorial.net/) site. If you’d like to learn more about SQLite and SQL in general, then the site is a great resource. + +The Chinook database provides artist, music, and playlist information along the lines of a simplified [Spotify](https://www.spotify.com/). The database is part of the example code project in the `project/data` folder. + +## Using Flask With Python, SQLite, and SQLAlchemy[](https://realpython.com/python-sqlite-sqlalchemy/#using-flask-with-python-sqlite-and-sqlalchemy "Permanent link") + +The `examples/example_3/chinook_server.py` program creates a [Flask](https://flask.palletsprojects.com/en/1.1.x/) application that you can interact with using a browser. The application makes use of the following technologies: + +- [**Flask Blueprint**](https://flask.palletsprojects.com/en/1.1.x/blueprints/) is part of Flask and provides a good way to follow the [separation of concerns](https://realpython.com/flask-blueprint/) design principle and create distinct modules to contain functionality. + +- [**Flask SQLAlchemy**](https://pypi.org/project/Flask-SQLAlchemy/) is an extension for Flask that adds support for SQLAlchemy in your web applications. + +- [**Flask_Bootstrap4**](https://pypi.org/project/Flask-Bootstrap4/) packages the [Bootstrap](https://getbootstrap.com/) front-end tool kit, integrating it with your Flask web applications. + +- [**Flask_WTF**](https://pypi.org/project/Flask-WTF/) extends Flask with [WTForms](https://wtforms.readthedocs.io/en/2.3.x/), giving your web applications a useful way to generate and validate web forms. + +- [**python_dotenv**](https://pypi.org/project/python-dotenv/) is a Python module that an application uses to read environment variables from a file and keep sensitive information out of program code. + + +Though not necessary for this example, a `.env` file holds the environment variables for the application. The `.env` file exists to contain sensitive information like passwords, which you should keep out of your code files. However, the content of the project `.env` file is shown below since it doesn’t contain any sensitive data: + +`SECRET_KEY = "you-will-never-guess" SQLALCHEMY_TRACK_MODIFICATIONS = False SQLAlCHEMY_ECHO = False DEBUG = True` + +The example application is fairly large, and only some of it is relevant to this tutorial. For this reason, examining and learning from the code is left as an exercise for the reader. That said, you can take a look at an animated screen capture of the application below, followed by the HTML that renders the home page and the Python Flask route that provides the dynamic data. + +Here’s the application in action, navigating through various menus and features: + +[![The chinook database web application in action as an animated GIF](https://files.realpython.com/media/python-sqlite-sqlalchemy-in-action.8fcee355cc31.gif)](https://files.realpython.com/media/python-sqlite-sqlalchemy-in-action.8fcee355cc31.gif) + +The animated screen capture starts on the application home page, styled using [Bootstrap 4](https://getbootstrap.com/). The page displays the artists in the database, sorted in ascending order. The remainder of the screen capture presents the results of clicking on the displayed links or navigating around the application from the top-level menu. + +Here’s the [Jinja2](https://realpython.com/primer-on-jinja-templating/) HTML template that generates the home page of the application: + +`{% extends "base.html" %} {% block content %}
Create New Artist
{{ form.csrf_token }} {{ render_field(form.name, placeholder=form.name.label.text) }}
{% for artist in artists %} {% endfor %}
List of Artists
Artist Name
{{ artist.name }}
{% endblock %}` + +Here’s what’s going on in this Jinja2 template code: + +- **Line 1** uses Jinja2 template inheritance to build this template from the `base.html` template. The `base.html` template contains all the HTML5 boilerplate code as well as the Bootstrap navigation bar consistent across all pages of the site. + +- **Lines 3 to 37** contain the block content of the page, which is incorporated into the Jinja2 macro of the same name in the `base.html` base template. + +- **Lines 9 to 13** render the form to create a new artist. This uses the features of [Flask-WTF](https://pypi.org/project/Flask-WTF/) to generate the form. + +- **Lines 24 to 32** create a `for` loop that renders the table of artist names. + +- **Lines 27 to 29** render the artist name as a link to the artist’s album page showing the songs associated with a particular artist. + + +Here’s the Python route that renders the page: + +`from flask import Blueprint, render_template, redirect, url_for from flask_wtf import FlaskForm from wtforms import StringField from wtforms.validators import InputRequired, ValidationError from app import db from app.models import Artist # Set up the blueprint artists_bp = Blueprint( "artists_bp", __name__, template_folder="templates", static_folder="static" ) def does_artist_exist(form, field): artist = ( db.session.query(Artist) .filter(Artist.name == field.data) .one_or_none() ) if artist is not None: raise ValidationError("Artist already exists", field.data) class CreateArtistForm(FlaskForm): name = StringField( label="Artist's Name", validators=[InputRequired(), does_artist_exist] ) @artists_bp.route("/") @artists_bp.route("/artists", methods=["GET", "POST"]) def artists(): form = CreateArtistForm() # Is the form valid? if form.validate_on_submit(): # Create new artist artist = Artist(name=form.name.data) db.session.add(artist) db.session.commit() return redirect(url_for("artists_bp.artists")) artists = db.session.query(Artist).order_by(Artist.name).all() return render_template("artists.html", artists=artists, form=form,)` + +Let’s go over what the above code is doing: + +- **Lines 1 to 6** import all the modules necessary to render the page and initialize forms with data from the database. + +- **Lines 9 to 11** create the blueprint for the artists page. + +- **Lines 13 to 20** create a custom validator function for the Flask-WTF forms to make sure a request to create a new artist doesn’t conflict with an already existing artist. + +- **Lines 22 to 25** create the form class to handle the artist form rendered in the browser and provide validation of the form field inputs. + +- **Lines 27 to 28** connect two routes to the `artists()` function they decorate. + +- **Line 30** creates an instance of the `CreateArtistForm()` class. + +- **Line 33** determines if the page was requested through the HTTP methods GET or POST (submit). If it was a POST, then it also validates the fields of the form and informs the user if the fields are invalid. + +- **Lines 35 to 37** create a new artist object, add it to the SQLAlchemy session, and commit the artist object to the database, persisting it. + +- **Line 38** redirects back to the artists page, which will be rerendered with the newly created artist. + +- **Line 40** runs an SQLAlchemy query to get all the artists in the database and sort them by `Artist.name`. + +- **Line 41** renders the artists page if the HTTP request method was a GET. + + +You can see that a great deal of functionality is created by a reasonably small amount of code. + +[Remove ads](https://realpython.com/account/join/) + +## Creating a REST API Server[](https://realpython.com/python-sqlite-sqlalchemy/#creating-a-rest-api-server "Permanent link") + +You can also create a web server providing a [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) API. This kind of server offers URL endpoints responding with data, often in [JSON](https://en.wikipedia.org/wiki/JSON) format. A server providing REST API endpoints can be used by JavaScript single-page web applications through the use of AJAX HTTP requests. + +Flask is an excellent tool for creating REST applications. For a multi-part series of tutorials about using Flask, Connexion, and SQLAlchemy to create REST applications, check out [Python REST APIs With Flask, Connexion, and SQLAlchemy](https://realpython.com/flask-connexion-rest-api/). + +If you’re a fan of Django and are interested in creating REST APIs, then check out [Django Rest Framework – An Introduction](https://realpython.com/django-rest-framework-quick-start/) and [Create a Super Basic REST API with Django Tastypie](https://realpython.com/create-a-super-basic-rest-api-with-django-tastypie/). + +**Note:** It’s reasonable to ask if SQLite is the right choice as the database backend to a web application. The [SQLite website](https://www.sqlite.org/whentouse.html) states that SQLite is a good choice for sites that serve around 100,000 hits per day. If your site gets more daily hits, the first thing to say is congratulations! + +Beyond that, if you’ve implemented your website with SQLAlchemy, then it’s possible to move the data from SQLite to another database such as [MySQL](https://realpython.com/python-mysql/) or PostgreSQL. For a comparison of SQLite, MySQL, and PostgreSQL that will help you make decisions about which one will serve your application best, check out [Introduction to Python SQL Libraries](https://realpython.com/python-sql-libraries/). + +It’s well worth considering SQLite for your Python application, no matter what it is. Using a database gives your application versatility, and it might create surprising opportunities to add additional features. + +## Conclusion[](https://realpython.com/python-sqlite-sqlalchemy/#conclusion "Permanent link") + +You’ve covered a lot of ground in this tutorial about databases, SQLite, SQL, and SQLAlchemy! You’ve used these tools to move data contained in flat files to an SQLite database, access the data with SQL and SQLAlchemy, and provide that data through a web server. + +**In this tutorial, you’ve learned:** + +- Why an **SQLite database** can be a compelling alternative to flat-file data storage +- How to **normalize data** to reduce data redundancy and increase data integrity +- How to use **SQLAlchemy** to work with databases in an object-oriented manner +- How to build a **web application** to serve a database to multiple users + +Working with databases is a powerful abstraction for working with data that adds significant functionality to your Python programs and allows you to ask interesting questions of your data. + +You can get all of the code and data you saw in this tutorial at the link below: + +**Download the sample code:** [Click here to get the code you’ll use](https://realpython.com/bonus/sqlite-sqlalchemy-code/) to learn about data management with SQLite and SQLAlchemy in this tutorial. + +## Further Reading[](https://realpython.com/python-sqlite-sqlalchemy/#further-reading "Permanent link") + +This tutorial is an introduction to using databases, SQL, and SQLAlchemy, but there’s much more to learn about these subjects. These are powerful, sophisticated tools that no single tutorial can cover adequately. Here are some resources for additional information to expand your skills: + +- If your application will expose the database to users, then avoiding SQL injection attacks is an important skill. For more information, check out [Preventing SQL Injection Attacks With Python](https://realpython.com/prevent-python-sql-injection/). + +- Providing web access to a database is common in web-based single-page applications. To learn how, check out [Python REST APIs With Flask, Connexion, and SQLAlchemy – Part 2](https://realpython.com/flask-connexion-rest-api-part-2/). + +- Preparing for [data engineering](https://realpython.com/python-data-engineer/) job interviews gives you a leg up in your career. To get started, check out [Data Engineer Interview Questions With Python](https://realpython.com/data-engineer-interview-questions-python/). + +- Migrating data and being able to roll back using Flask with Postgres and SQLAlchemy is an integral part of the Software Development Life Cycle (SDLC). You can learn more about it by checking out [Flask by Example – Setting up Postgres, SQLAlchemy, and Alembic](https://realpython.com/flask-by-example-part-2-postgres-sqlalchemy-and-alembic/). + +--- +# Anhang: Promt +```text +Gib mir eine umfassende und detaillierte Übersicht zum Python Paket "SQLAlchemy". +* Wie ist die grundsätzliche Idee? +* Welche Abgrenzung zu ähnlichen oder verwandten Vorgehensweisen, Paketen oder Pradigmen gibt es +* Welche Probleme werden gelöst? +* Welche Herausforderungen gibt es? +* Ergänze Erläuterungen mit praxisnahen Beispielen. +``` \ No newline at end of file diff --git a/python/argpase.md b/python/argpase.md new file mode 100755 index 0000000..e86d645 --- /dev/null +++ b/python/argpase.md @@ -0,0 +1,368 @@ +## 1. Grundidee von `argparse` + +`argparse` ist das Standardmodul in Python, um Kommandozeilen-Argumente zu definieren, zu parsen und automatisch Hilfe-/Usage-Texte zu erzeugen. + +Minimalbeispiel: + +```python +import argparse + +parser = argparse.ArgumentParser(description="Ein kleines Beispiel-CLI") +parser.add_argument("datei", help="Pfad zur Eingabedatei") +args = parser.parse_args() + +print(args.datei) +``` + +Aufruf: +```bash +python script.py meine_datei.txt +``` + +--- + +## 2. Argumente definieren: `ArgumentParser.add_argument` + +### 2.1 Positionsargumente + +- Werden ohne führende `-` oder `--` angegeben. +- Reihenfolge ist relevant. + +```python +parser.add_argument("quelle", help="Quellpfad") +parser.add_argument("ziel", help="Zielpfad") +``` + +Aufruf: +```bash +python script.py input.txt output.txt +``` + +Use-Case: +- Pflichtwerte, die immer gebraucht werden (z.B. Eingabe- und Ausgabedatei). + +--- + +### 2.2 Optionale Argumente (Flags / Optionen) + +- Beginnen mit `-` bzw. `--`. +- Reihenfolge ist egal. +- Können Standardwerte haben. + +```python +parser.add_argument( + "-v", "--verbose", + action="store_true", + help="Ausführliche Ausgabe aktivieren" +) + +parser.add_argument( + "-n", "--anzahl", + type=int, + default=10, + help="Anzahl der Elemente (Standard: 10)" +) +``` + +Aufruf: +```bash +python script.py input.txt --verbose --anzahl 5 +# oder kurz: +python script.py input.txt -v -n 5 +``` + +Use-Case: +- Konfiguration, optionales Verhalten, Debug/Verbose-Flags, Parameter mit Default. + +--- + +## 3. Wichtige Parameter von `add_argument` + +### 3.1 `name` / `flags` + +- Beispiel: + - Positional: `"datei"` + - Optional: `"-v", "--verbose"` + +```python +parser.add_argument("datei") +parser.add_argument("-v", "--verbose") +``` + +--- + +### 3.2 `type` + +- Convertiert Eingabe in Typ. +- Validiert automatisch (bei falschem Typ Fehler + Hilfe). + +```python +parser.add_argument("--port", type=int, default=8080) +parser.add_argument("--faktor", type=float) +``` + +Use-Case: +- Numerische Werte, Pfade, eigene Typen (z.B. `Path` aus `pathlib`). + +--- + +### 3.3 `default` + +- Standardwert, wenn Argument nicht übergeben wird. + +```python +parser.add_argument("--log-level", default="INFO") +``` + +Use-Case: +- Sinnvolle Defaults, um CLI kompakt zu halten. + +--- + +### 3.4 `required` + +- Macht optionale Argumente zwingend erforderlich. + +```python +parser.add_argument("--config", required=True) +``` + +Use-Case: +- Flags/Optionen, die zwingend gesetzt werden müssen (z.B. API-Key, Konfigdatei). + +--- + +### 3.5 `help` + +- Beschreibung für die `--help`-Ausgabe. + +```python +parser.add_argument("--mode", help="Betriebsmodus: fast oder safe") +``` + +Use-Case: +- Dokumentation der Optionen (sehr wichtig für Benutzerfreundlichkeit). + +--- + +### 3.6 `choices` + +- Schränkt erlaubte Werte ein. + +```python +parser.add_argument( + "--mode", + choices=["fast", "safe"], + default="safe", + help="fast = schneller, safe = sicherer" +) +``` + +Use-Case: +- Enum-ähnliche Optionen (z.B. `debug/info/warn/error`, `json/text`). + +--- + +### 3.7 `action` + +Steuert, was passiert, wenn das Argument gesetzt wird. + +Häufige Actions: + +1. `store` (Standard) + Speichert den Wert (z.B. `--port 8000` → `args.port = 8000`). + +2. `store_true` / `store_false` + Boolean-Flag, das `True`/`False` setzt. + + ```python + parser.add_argument("-v", "--verbose", action="store_true") + ``` + +3. `append` + Fügt mehrere Werte in eine Liste ein. + + ```python + parser.add_argument( + "-t", "--tag", + action="append", + help="Kann mehrfach verwendet werden" + ) + # Aufruf: --tag a --tag b -> args.tag = ["a", "b"] + ``` + +4. `count` + Zählt, wie oft ein Flag verwendet wurde. + + ```python + parser.add_argument( + "-v", "--verbose", + action="count", + default=0, + help="Mehrfach verwenden für mehr Details" + ) + # -v -> 1, -vv -> 2 ... + ``` + +Use-Case: +- Flags (bool), Mehrfachangaben (Listen), Verbosity-Level etc. + +--- + +### 3.8 `nargs` + +Gibt an, wie viele Werte zu einem Argument gehören. + +Typische Varianten: + +- `nargs=1` → eine Liste mit einem Element +- `nargs=2` → genau 2 Werte +- `nargs="+"` → mindestens ein Wert +- `nargs="*"` → beliebig viele (auch 0) + +```python +parser.add_argument("dateien", nargs="+", help="Eine oder mehrere Dateien") +parser.add_argument("--koordinaten", nargs=2, type=float, help="x y") +``` + +Use-Case: +- Mehrere Dateien, Koordinaten, Listen von Werten. + +--- + +### 3.9 `metavar` + +- Steuert, wie das Argument im Help-Text angezeigt wird. + +```python +parser.add_argument( + "--output", + metavar="DATEI", + help="Ausgabedatei" +) +``` + +Use-Case: +- Schöner formatierte Hilfe (statt generischer Namen). + +--- + +### 3.10 `dest` + +- Name des Attributes in `args`. + +```python +parser.add_argument("-o", "--output", dest="ausgabedatei") +# args.ausgabedatei +``` + +Use-Case: +- Lesbare/konfliktfreie Python-Bezeichner, wenn CLI-Namen nicht ideal sind. + +--- + +## 4. Subkommandos: `subparsers` + +Für CLI-Tools mit mehreren Befehlen (ähnlich `git commit`, `git status`). + +```python +import argparse + +parser = argparse.ArgumentParser(prog="tool") +subparsers = parser.add_subparsers(dest="command", required=True) + +# Subkommando: "run" +run_parser = subparsers.add_parser("run", help="Job ausführen") +run_parser.add_argument("job_id", type=int) + +# Subkommando: "list" +list_parser = subparsers.add_parser("list", help="Jobs auflisten") +list_parser.add_argument("--status", choices=["open", "done"]) + +args = parser.parse_args() + +if args.command == "run": + print(f"Starte Job {args.job_id}") +elif args.command == "list": + print(f"Liste Jobs mit Status {args.status}") +``` + +Aufrufe: +```bash +tool run 42 +tool list --status open +``` + +Use-Case: +- Umfangreiche Tools mit verschiedenen Befehlen (z.B. Admin-Tools, Deployment-CLI). + +--- + +## 5. Automatische Hilfe und Usage + +`argparse` erzeugt automatisch `-h` / `--help`: + +```bash +python script.py --help +``` + +Du bekommst: + +- Beschreibung (`description`) +- Liste aller Argumente +- Default-Werte (wenn konfiguriert) +- Subkommandos (falls vorhanden) + +Beispiel: + +```python +parser = argparse.ArgumentParser( + description="Konvertiert Dateien in andere Formate." +) +``` + +--- + +## 6. Minimaler „Best-Practice“-Skeleton + +```python +import argparse + +def parse_args(): + parser = argparse.ArgumentParser( + description="Beispiel-Tool für argparse" + ) + + # Positionsargumente + parser.add_argument("eingabe", help="Eingabedatei") + + # Optionale Argumente + parser.add_argument( + "-o", "--output", + help="Ausgabedatei (Standard: stdout)" + ) + parser.add_argument( + "-v", "--verbose", + action="store_true", + help="Ausführliche Ausgabe" + ) + parser.add_argument( + "--mode", + choices=["fast", "safe"], + default="safe", + help="Verarbeitungsmodus (Standard: safe)" + ) + + return parser.parse_args() + +def main(): + args = parse_args() + if args.verbose: + print(f"Starte in Modus {args.mode} mit Eingabe {args.eingabe}") + # weitere Logik… + +if __name__ == "__main__": + main() +``` + +--- diff --git a/python/black.md b/python/black.md new file mode 100755 index 0000000..20c6cec --- /dev/null +++ b/python/black.md @@ -0,0 +1,525 @@ +Hier eine umfassende, aber für Einsteiger verständliche Einführung in **black**, den „opinionated“ Python-Code-Formatter. + +--- + +## 1. Grundidee von *black* + +**Was ist black?** + +- *black* ist ein **automatisches Formatierungs-Tool** für Python. +- Es ändert **nur die Formatierung**, nicht die Logik deines Codes. +- Es ist **„opinionated“**: Es gibt nur sehr wenige Einstellungen – black entscheidet den Stil für dich. + +**Zentrale Idee:** + +> „*You are not your code style.*“ +> Statt darüber zu diskutieren, ob ein Leerzeichen hier oder dort besser ist, überlässt du das black. +> +> Ziel: +> - einheitlicher Stil +> - weniger Diskussionen in Code Reviews +> - Fokus auf inhaltliche Fehler, nicht auf Formatierung + +--- + +## 2. Was macht black konkret? + +Black nimmt deinen Python-Code, parst ihn und schreibt ihn nach festen Regeln neu. Beispiele: + +### 2.1 Installation + +```bash +pip install black +``` + +### 2.2 Einfache Nutzung + +Eine einzelne Datei formatieren: + +```bash +black main.py +``` + +Ein ganzes Projekt: + +```bash +black . +``` + +Nur anzeigen, was geändert würde (ohne zu schreiben): + +```bash +black --diff --check . +``` + +--- + +## 3. Praxisnahe Formatierungsbeispiele + +### 3.1 Zeilenumbrüche und Einrückung + +**Vorher:** + +```python +def very_long_function_name(arg1,arg2,arg3,arg4,arg5,arg6,arg7=False,arg8=None): + return (arg1+arg2+arg3+arg4+arg5+arg6) +``` + +**Nachher (black):** + +```python +def very_long_function_name( + arg1, + arg2, + arg3, + arg4, + arg5, + arg6, + arg7=False, + arg8=None, +): + return arg1 + arg2 + arg3 + arg4 + arg5 + arg6 +``` + +Was passiert? + +- Argumente werden sauber untereinander geschrieben. +- Operatoren (+) werden mit Leerzeichen versehen. +- Abschluss-Komma nach dem letzten Argument (hilft bei späteren Änderungen). + +--- + +### 3.2 Strings und Anführungszeichen + +Black bevorzugt fast immer **doppelte Anführungszeichen**. + +**Vorher:** + +```python +name = 'Alice' +message = 'Hello, ' + name + '!' +``` + +**Nachher:** + +```python +name = "Alice" +message = "Hello, " + name + "!" +``` + +Ausnahmen: +- Wenn ein String doppelte Anführungszeichen enthält, kann black einzelne beibehalten, um weniger zu escapen. + +--- + +### 3.3 Leerzeichen und Klammern + +**Vorher:** + +```python +result=(1+2)*3 +if x==42: + print( 'Answer',x ) +``` + +**Nachher:** + +```python +result = (1 + 2) * 3 +if x == 42: + print("Answer", x) +``` + +Black: +- fügt Leerzeichen um Operatoren hinzu (`1 + 2`, `x == 42`), +- entfernt unnötige Leerzeichen (`print( 'Answer',x )` → `print("Answer", x)`). + +--- + +### 3.4 Collections (Listen, Dicts, Sets) + +**Vorher:** + +```python +config = {"host":"localhost","port":5432,"debug":True} +``` + +**Nachher:** + +```python +config = { + "host": "localhost", + "port": 5432, + "debug": True, +} +``` + +Vorteile: +- Bessere Lesbarkeit +- Leicht, neue Einträge hinzuzufügen (wegen Abschluss-Komma). + +--- + +### 3.5 Lange Ausdrücke + +**Vorher:** + +```python +query = session.query(User).filter(User.is_active==True, User.signup_date>=start_date, User.signup_date<=end_date).order_by(User.signup_date.desc()) +``` + +**Nachher:** + +```python +query = ( + session.query(User) + .filter( + User.is_active == True, + User.signup_date >= start_date, + User.signup_date <= end_date, + ) + .order_by(User.signup_date.desc()) +) +``` + +Black bricht lange Zeilen so um, dass: +- sie unter der vorgegebenen Maximalbreite bleiben (standard: 88 Zeichen), +- die Struktur des Codes klarer sichtbar wird. + +--- + +## 4. Abgrenzung zu verwandten Tools + +### 4.1 Formatter vs. Linter vs. Typprüfer + +- **Formatter** (Formatierer): + Passen das **Aussehen** deines Codes an – z. B. black, autopep8, yapf. +- **Linter**: + Finden mögliche **Fehler, Stilprobleme oder unschöne Konstrukte** – z. B. flake8, pylint, ruff. +- **Typprüfer**: + Prüfen, ob Typen konsistent sind (z. B. mit `typing`) – z. B. mypy, pyright. + +Black ist **nur** ein Formatter. + +--- + +### 4.2 Black vs. autopep8 + +**autopep8**: + +- Ziel: Code so anpassen, dass er PEP8-konform ist. +- Orientierung direkt an den PEP8-Regeln. +- Viele Optionen (z. B. bestimmte Checks an- oder abschalten). + +**black**: + +- Ziel: **konsequenter, einheitlicher Stil** – nicht nur PEP8, sondern zusätzliche strenge Regeln. +- Sehr wenige Konfigurationsmöglichkeiten (bewusst!). +- Output ist oft deutlich „strenger” und einheitlicher als autopep8. + +--- + +### 4.3 Black vs. yapf + +**yapf**: + +- Google-Tool zur Formatierung von Python. +- Sehr konfigurierbar: du kannst deinen Style stark beeinflussen (ähnlich wie bei C++/clang-format). +- Mehr Freiheit, aber dadurch auch mehr Diskussionen möglich. + +**black**: + +- „Meine Regeln oder gar nicht“. +- Ziel: Diskussionen vermeiden, daher kaum Konfigurationsoptionen. +- Sehr stabiler, vorhersehbarer Output. + +--- + +### 4.4 Black vs. isort + +**isort** ist ein Tool, um **Imports** zu sortieren und zu gruppieren. + +- Sortiert `import`-Zeilen alphabetisch und nach Gruppen: + - Standardbibliothek + - Third-Party + - Projektinterne Module + +Black: + +- Formatiert auch Imports (Zeilenumbrüche, Leerzeichen), +- sortiert sie aber **nicht** nach Paketnamen. + +Typischerweise nutzt man: + +```bash +isort . +black . +``` + +Oder beides zusammen über Tools wie `ruff` oder `pre-commit`. + +--- + +## 5. Welche Probleme löst black? + +### 5.1 Konsistenter Stil in Teams + +Ohne Tool: + +- Jeder schreibt „sein“ Python. +- Unterschiedliche Leerzeichen, Umbrüche, String-Stile, etc. +- Code wirkt „bunt“ und uneinheitlich. + +Mit black: + +- Jedes Commit, jede Datei, jede Funktion hat denselben Stil. +- Neue Teammitglieder lernen schneller, was „üblich“ ist – es ist einfach: das, was black macht. + +--- + +### 5.2 Weniger Diskussionen in Code Reviews + +Vor black: + +- Kommentare wie: „Bitte hier ein Leerzeichen einfügen.“ +- „Kannst du die Argumente untereinander schreiben?“ +- „Wir verwenden eigentlich doppelte Anführungszeichen.“ + +Mit black: + +- Reviewer sagen: „Bitte einmal black drüber laufen lassen.“ +- Fokus liegt auf: + - Ist der Algorithmus korrekt? + - Sind die Funktionen gut benannt? + - Sind Tests vorhanden? + +--- + +### 5.3 Bessere Lesbarkeit & Wartbarkeit + +- Lange Zeilen werden sinnvoll umgebrochen. +- Verschachtelte Ausdrücke werden strukturiert. +- Datensammlungen (Listen, Dicts) werden mehrzeilig und übersichtlich dargestellt. + +Beispiel: Eine unübersichtliche Dict-Liste wird automatisch gut lesbar formatiert. + +**Vorher:** + +```python +users=[{"id":1,"name":"Alice","active":True},{"id":2,"name":"Bob","active":False}] +``` + +**Nachher:** + +```python +users = [ + {"id": 1, "name": "Alice", "active": True}, + {"id": 2, "name": "Bob", "active": False}, +] +``` + +--- + +### 5.4 Weniger „Rauschen“ in Git-Diffs + +Manuell Änderungen + Formatierung: + +- Du änderst eine Zeile, formatierst etwas, +- der Diff zeigt viele Änderungen, obwohl nur wenig Logik geändert wurde. + +Mit black: + +- Wenn alle Dateien bereits formatiert sind, entstehen bei späteren Änderungen klarere Diffs: + - Format ist überall gleich, + - nur die wirklich geänderte Logik fällt auf. + +--- + +## 6. Herausforderungen und typische Stolpersteine + +### 6.1 Einstieg in ein bestehendes Projekt + +Problem: + +- Du führst black in einem **alten, großen Projekt** ein. +- Beim ersten Durchlauf ändert black Hunderte/tausende Dateien. +- Git-Diff ist riesig. + +Lösungen / Best Practices: + +- Einmaliger „Formatting-Commit“ (nur Style): + - In einem eigenen Commit alle Dateien mit black formatieren. + - Danach neue Commits nur mit funktionalen Anpassungen. +- Oder schrittweise: + - Nur neue/aktuell bearbeitete Module mit black formatieren. + - z. B. mit `pre-commit`-Hook nur geänderte Dateien behandeln. + +--- + +### 6.2 „Mir gefällt der Stil nicht!“ + +Black ist sehr strikt: +- Du kannst nicht „mal eben“ sagen: + - „Ich möchte lieber 120 statt 88 Zeichen pro Zeile“ (ok, das **geht** als Option) + - Aber: Du kannst nicht festlegen, wie exakt bestimmte Konstrukte formatiert werden. + +Beispiel: Viele stören sich zunächst an: +- doppelten Anführungszeichen, +- „ungewöhnlichen“ Zeilenumbrüchen. + +Wichtig: +- black ist ein **Team-Tool**. +- Man einigt sich darauf, den Stil zu akzeptieren, um Diskussionen zu vermeiden. + +--- + +### 6.3 Integration mit anderen Tools (z. B. isort, flake8, ruff) + +Typische Stolperfallen: + +- `isort` und `black` können sich widersprechen, wenn sie unterschiedliche Maximalzeilenlängen nutzen. +- Linter können sich über Formatierung beschweren, wenn sie anders konfiguriert sind als black. + +Best Practice: + +- Einheitliche Konfiguration, z. B. in `pyproject.toml`: + +```toml +[tool.black] +line-length = 88 + +[tool.isort] +profile = "black" +line_length = 88 +``` + +So passen Formatierung und Importsortierung zusammen. + +--- + +### 6.4 Änderungen zwischen Black-Versionen + +Black entwickelt sich weiter. + +- In seltenen Fällen ändert eine neue Version den Stil leicht. +- Dann kann ein erneuter Durchlauf große Diffs erzeugen. + +Abhilfe: + +- Black-Version in `pyproject.toml` oder `requirements.txt` fest pinnen: + +```text +black==24.4.2 +``` + +- Gelegentlich bewusst aktualisieren und einmaliger Reformatting-Commit. + +--- + +### 6.5 Performance bei sehr großen Projekten + +Für normale Projekte ist black schnell genug. +Bei **sehr großen Repositories** kann einmaliges Formatieren aber dauern. + +Lösungen: + +- Nur geänderte Dateien formatieren (z. B. über `pre-commit`). +- In CI nur `black --check .` laufen lassen (prüft, ob alles formatiert ist, ohne neu zu schreiben). + +--- + +## 7. Black in der Praxis: Workflows + +### 7.1 Integration in den Editor/IDE + +Die meisten Editoren können black beim Speichern ausführen: + +- **VS Code**: + - Erweiterung „Python“ installieren. + - In `settings.json`: + +```json +{ + "python.formatting.provider": "black", + "editor.formatOnSave": true +} +``` + +- **PyCharm**: + - Externes Tool konfigurieren oder über Plugins. + +Vorteil: +- Du denkst nicht mehr über Formatierung nach: Speichern = fertig formatiert. + +--- + +### 7.2 Verwendung mit `pre-commit` + +`pre-commit` ist ein Tool, das Skripte ausführt, bevor ein Git-Commit akzeptiert wird. + +Installieren: + +```bash +pip install pre-commit +``` + +In `.pre-commit-config.yaml`: + +```yaml +repos: + - repo: https://github.com/psf/black + rev: 24.4.2 # Version anpassen + hooks: + - id: black +``` + +Aktivieren: + +```bash +pre-commit install +``` + +Jetzt wird bei jedem `git commit` black auf die geänderten Dateien angewandt. +Das stellt sicher, dass alles im Repo immer formatiert ist. + +--- + +### 7.3 Black in der CI-Pipeline + +In CI-Systemen (GitHub Actions, GitLab CI, etc.) kannst du prüfen, ob der Code schon formatiert ist. + +Beispiel (CLI): + +```bash +black --check . +``` + +- Exit-Code 0: alles ok. +- Exit-Code ≠ 0: irgendetwas müsste formatiert werden. + +Das verhindert, dass unformatierter Code in `main` oder `master` landet. + +--- + +## 8. Zusammenfassung + +- **black** ist ein **sehr strenger, kaum konfigurierbarer Formatter** für Python-Code. +- Ziel: + - **einheitlicher Stil**, + - **keine Diskussionen** über Formatierung, + - Fokus auf Logik und Qualität statt auf Leerzeichen. +- Abgrenzung: + - black formatiert nur – es ist kein Linter und kein Typprüfer. + - Im Vergleich zu autopep8/yapf: weniger konfigurierbar, dafür klarer, einheitlicher Stil. +- Es löst: + - Stil-Inkonsistenz, Formatierungs-Diskussionen, „Rauschen“ in Diffs. +- Herausforderungen: + - Einstieg in ältere Projekte, + - wenig Einfluss auf Stil, + - Abstimmung mit anderen Tools, Versionsänderungen. +- In der Praxis: + - Nutze black im Editor (Format-on-save), + - im `pre-commit`-Hook, + - und als `--check` in der CI. + +Wenn du möchtest, kann ich dir im nächsten Schritt ein kleines Beispielprojekt konstruieren (mit `pyproject.toml`, `pre-commit`, isort/black-Konfiguration), damit du siehst, wie man black von Anfang an sauber einrichtet. \ No newline at end of file diff --git a/python/mypy.md b/python/mypy.md new file mode 100755 index 0000000..e35f98c --- /dev/null +++ b/python/mypy.md @@ -0,0 +1,640 @@ +Mypy ist ein statischer Typprüfer („Type Checker“) für Python. Er hilft dir, Fehler schon beim Schreiben bzw. vor dem Ausführen des Codes zu finden – ähnlich wie ein Compiler in streng typisierten Sprachen – ohne dass du Python als dynamische Sprache „aufgibst“. + +Ich gehe Schritt für Schritt durch: + +1. Grundidee (für Einsteiger verständlich) +2. Kurzer praktischer Einstieg (Installation, erste Checks, einfache Beispiele) +3. Welche Probleme mypy löst +4. Abgrenzung zu ähnlichen / verwandten Tools +5. Herausforderungen und typische Stolpersteine +6. Praxisnahe Beispiele und Patterns + + + +--- + +## 1. Grundidee: Was macht mypy? + +Python ist dynamisch typisiert: Variablen haben zur Laufzeit Typen, aber der Interpreter prüft sie nicht im Voraus. Viele Fehler sieht man erst, wenn der entsprechende Code ausgeführt wird. + +Mypy ändert daran nichts zur Laufzeit – aber es analysiert deinen Code **statisch** (also ohne ihn auszuführen) und prüft, ob die verwendeten **Typannotationen** konsistent sind. + +### Typannotationen – ein Beispiel + +Ohne Typen: + +```python +def add(a, b): + return a + b +``` + +Das ist legal, aber du kannst aus dem Code nicht erkennen, ob `a` und `b` Zahlen, Strings oder etwas anderes sein sollen. Python lässt vieles zu: + +```python +print(add(1, 2)) # 3 +print(add("a", "b")) # "ab" +print(add(1, "b")) # TypeError zur Laufzeit +``` + +Mit Typannotationen: + +```python +def add(a: int, b: int) -> int: + return a + b +``` + +Damit sagst du: `add` nimmt zwei `int` und gibt einen `int` zurück. + +Mypy überprüft jetzt: + +```bash +mypy mein_code.py +``` + +und meldet z.B.: + +```text +mein_code.py:10: error: Argument 2 to "add" has incompatible type "str"; expected "int" +``` + +– wenn du irgendwo `add(1, "b")` aufrufst. + +**Grundidee**: +Du schreibst „Verträge“ (Typen) in deinen Code, und mypy überprüft, ob du dich überall daran hältst. Das verbessert Lesbarkeit, Robustheit und macht refactoring sicherer. + +--- + +## 2. Kurzer praktischer Einstieg + +### Installation + +```bash +pip install mypy +``` + +(Je nach Setup evtl. in einer virtuellen Umgebung.) + +### Minimalbeispiel + +`calculator.py`: + +```python +def add(a: int, b: int) -> int: + return a + b + +def main() -> None: + x = add(1, 2) + y = add("a", "b") # Fehler + + print(x, y) +``` + +Mypy ausführen: + +```bash +mypy calculator.py +``` + +Ausgabe: + +```text +calculator.py:6: error: Argument 1 to "add" has incompatible type "str"; expected "int" +Found 1 error in 1 file (checked 1 source file) +``` + +Obwohl Python den Code ausführen würde (und bei `add("a", "b")` sogar ein „korrektes“ Ergebnis liefern würde: `"ab"`), sagt mypy: Du hast gegen deinen eigenen Typvertrag verstoßen. + +### Gradual Typing + +Du musst nicht alles von Anfang an typisieren. Du kannst Schritt für Schritt anfangen: + +```python +def add(a, b): # keine Typen hier + return a + b + +def use_add() -> int: + result = add(1, 2) # mypy lässt das oft durchgehen (je nach Konfiguration) + return result +``` + +Mypy arbeitet „gradual“: + +- Ungetypte Bereiche werden als `Any` betrachtet (unsicher, aber flexibel). +- Getypte Bereiche werden überprüft. +- Du kannst nach und nach mehr Typen hinzufügen und die Strenge erhöhen. + +--- + +## 3. Welche Probleme werden durch mypy gelöst? + +### 3.1. Typbezogene Fehler früh erkennen + +Typische Klassen von Bugs: + +1. **Falsche Argumenttypen**: + +```python +def send_email(to: str, subject: str, body: str) -> None: + ... + +send_email(["user@example.com"], "Hi", "Text") # Bug: Liste statt String +``` + +Mypy: + +```text +error: Argument 1 to "send_email" has incompatible type "List[str]"; expected "str" +``` + +2. **Verfügbare Attribute/Methoden**: + +```python +def greet(name: str) -> None: + print(name.upper()) + +user_name: int = 42 +greet(user_name) +``` + +Mypy: + +```text +error: Argument 1 to "greet" has incompatible type "int"; expected "str" +``` + +3. **Optionale Werte vergessen zu prüfen** (`None`): + +```python +from typing import Optional + +def get_user_name(user_id: int) -> Optional[str]: + ... + +def print_name(user_id: int) -> None: + name = get_user_name(user_id) + print(name.upper()) # Bug: name kann None sein! +``` + +Mypy: + +```text +error: Item "None" of "Optional[str]" has no attribute "upper" +``` + +Du wirst gezwungen, zuerst auf `None` zu prüfen: + +```python +def print_name(user_id: int) -> None: + name = get_user_name(user_id) + if name is None: + print("User not found") + return + print(name.upper()) # jetzt ok +``` + +### 3.2. Sicherere Refactorings + +Wenn du Funktionensignaturen änderst, Parameter umbenennst oder Rückgabetypen anpasst, kann mypy dir helfen, alle Stellen zu finden, die du anpassen musst. + +Beispiel: + +```python +# vorher +def get_price(product_id: int) -> float: + ... + +# nachher +def get_price(product_id: int) -> int: # Rückgabetyp geändert! + ... +``` + +Wenn irgendwo angenommen wird, dass `float` zurückkommt: + +```python +price_cents: float = get_price(123) # jetzt inkonsistent +``` + +meldet mypy das. Das verringert das Risiko von subtilen Bugs nach Refactorings. + +### 3.3. Bessere Dokumentation & IDE-Unterstützung + +Typannotationen sind lebende Dokumentation: + +```python +def load_config(path: str) -> dict[str, str]: + ... +``` + +Du siehst sofort, was die Funktion erwartet und liefert – ohne lange Kommentare. IDEs nutzen die Typen für: + +- Autovervollständigung +- Inlay Hints +- Navigation („go to definition“) +- Inline-Fehlermeldungen + +--- + +## 4. Abgrenzung zu ähnlichen / verwandten Tools + +### 4.1. Mypy vs. Linter (z.B. flake8, pylint) + +**Linter** prüfen v.a.: + +- Stil (PEP 8) +- potenziell problematische Patterns (unbenutzte Variablen, Schatten von Builtins, zu komplexe Funktionen) +- gewisse Logikfehler (z.B. nie erreichte Codezweige) + +**Mypy** fokussiert auf **Typkonsistenz**: + +- Stimmen die deklarierten Typen mit den tatsächlichen Verwendungen überein? +- Können bestimmte Codezweige überhaupt erreicht werden, wenn Typen berücksichtigt werden? +- Sind Operationen auf bestimmten Typen erlaubt? + +Beispiel: + +```python +x = [] +x.append(1) +x.append("a") +``` + +Linter: meistens kein Problem. +Mypy (je nach Typinferenz) könnte sagen: + +```text +List item 1 has incompatible type "str"; expected "int" +``` + +Fazit: +Linter und mypy ergänzen sich – sie ersetzen sich nicht. + +### 4.2. Mypy vs. Testframeworks (pytest, unittest) + +**Tests**: + +- prüfen Laufzeitverhalten für konkrete Eingaben. +- stellen sicher, dass Funktionen das tun, was fachlich / funktional gewünscht ist. + +**Mypy**: + +- prüft nur Typkonsistenz – keine fachliche Korrektheit. +- findet z.B. nicht, ob du die falsche mathematische Formel verwendest, solange die Typen passen. + +Beispiel: + +```python +def calculate_discount(price: float) -> float: + return price * 2 # fachlich falsch, aber typgerecht +``` + +Mypy ist zufrieden. Ein Unit-Test würde diesen Fehler finden. + +Fazit: +Mypy ergänzt Tests, ersetzt sie aber nicht. + +### 4.3. Mypy vs. andere Typechecker (Pyright, Pyre, pytype) + +Es gibt mehrere Typchecker für Python: + +- **mypy** – der „Klassiker“, in Python geschrieben, von vielen Projekten verwendet. +- **pyright** – sehr schneller Typechecker (Microsoft), in TypeScript geschrieben. +- **pyre** – von Meta (Facebook), mit Fokus auf große Codebasen. +- **pytype** – von Google. + +Sie verfolgen alle eine ähnliche Idee: statische Typprüfung für Python. Unterschiede gibt es bei: + +- Performance +- Genauigkeit / Strenge in bestimmten Bereichen +- Tooling-Integration (z.B. VS Code nutzt intern Pyright) + +Für den Einstieg ist mypy völlig ausreichend und weit verbreitet. + +### 4.4. Mypy vs. Laufzeit-Typprüfung (pydantic, marshmallow) + +**pydantic** & Co.: + +- validieren und konvertieren Daten zur **Laufzeit** (z.B. JSON-Input in API). +- werfen Exceptions, wenn Daten nicht passen. +- nutzen Typannotationen als Basis, sind aber nicht auf Compile-/Check-Zeit beschränkt. + +**Mypy**: + +- prüft nur zur Analysezeit, ändert das Laufzeitverhalten nicht. +- „merkt nicht“, ob zur Laufzeit echte Validierung stattfindet. + +Beispiel mit pydantic: + +```python +from pydantic import BaseModel + +class User(BaseModel): + id: int + name: str + +user = User(id="123", name="Alice") # zur Laufzeit wird "123" zu int geparst +``` + +Mypy würde melden: + +```text +Argument "id" to "User" has incompatible type "str"; expected "int" +``` + +– obwohl pydantic das zur Laufzeit akzeptiert und konvertiert. +Hier musst du entscheiden, ob du dich eher am statischen Vertrag (Typ) oder am dynamischen Verhalten orientieren willst. + +--- + +## 5. Herausforderungen und typische Stolpersteine + +### 5.1. Legacy Code ohne Typen + +In bestehendem Code fehlen oft Typannotationen, und vieles ist dynamisch. + +Strategien: + +- Zuerst nur neue Modules/Funktionen typisieren. +- Mypy mit „lockereren“ Einstellungen starten. +- Langsam „strictness“ erhöhen. + +Beispiel-Konfiguration (`mypy.ini`): + +```ini +[mypy] +python_version = 3.11 +ignore_missing_imports = True +disallow_untyped_defs = False +disallow_incomplete_defs = False +``` + +Später kannst du verschärfen: + +```ini +disallow_untyped_defs = True +disallow_incomplete_defs = True +warn_unused_ignores = True +strict_optional = True +``` + +### 5.2. Dynamische Features von Python + +Dinge wie: + +- dynamisches Hinzufügen von Attributen +- `setattr`, `getattr` +- Metaklassen-Magie +- Monkey-Patching + +sind schwer für statische Analyser. + +Beispiel: + +```python +class Dynamic: + pass + +obj = Dynamic() +obj.name = "Alice" # dynamisches Attribut +print(obj.name) +``` + +Mypy weiß nicht, dass `name` existiert, und meldet: + +```text +error: "Dynamic" has no attribute "name" +``` + +Workarounds: + +- Attribut im Klassendefinitionskörper deklarieren: + + ```python + class Dynamic: + name: str + ``` +- oder `# type: ignore` an problematischen Stellen nutzen. + +### 5.3. Komplexe Typen und Verbosität + +Generics, `Union`, `Optional`, `TypedDict`, `Protocol` etc. können komplex werden. Das kostet Einarbeitung. + +Beispiel für generische Funktion: + +```python +from typing import TypeVar, Iterable, List + +T = TypeVar("T") + +def first(items: Iterable[T]) -> T: + for item in items: + return item + raise ValueError("Empty iterable") +``` + +Mypy hilft hier, allgemeingültige, typsichere Utilities zu schreiben, aber das Typ-System wird relativ mächtig (und gelegentlich inelegant). + +### 5.4. False Positives und `# type: ignore` + +Manchmal **weißt du mehr** als mypy. Dann musst du mit mypy kommunizieren: + +```python +from typing import cast, Any + +def get_from_json(json_obj: dict[str, Any]) -> int: + return cast(int, json_obj["value"]) +``` + +oder: + +```python +some_weird_library_call() # type: ignore[arg-type] +``` + +Zu viele `# type: ignore` können aber wieder die Sicherheit untergraben. Es lohnt sich, sie sparsam und begründet einzusetzen. + +### 5.5. Performance bei großen Codebasen + +Für wirklich große Projekte kann mypy langsamer werden, vor allem bei vielen Imports und tiefen Typstrukturen. +Es gibt Optionen wie `--incremental` und `dmypy` (Daemon-Modus), um das zu beschleunigen. + +--- + +## 6. Praxisnahe Beispiele & Patterns + +### 6.1. Basic Typannotationen + +```python +def greet(name: str, times: int = 1) -> None: + for _ in range(times): + print(f"Hello, {name}!") +``` + +Sammlungstypen: + +```python +from typing import List, Dict + +def total_length(names: List[str]) -> int: + length = 0 + for n in names: + length += len(n) + return length + +def invert_mapping(mapping: Dict[int, str]) -> Dict[str, int]: + return {v: k for k, v in mapping.items()} +``` + +Ab Python 3.9 kannst du oft die Kurzform nutzen: + +```python +def total_length(names: list[str]) -> int: + ... +def invert_mapping(mapping: dict[int, str]) -> dict[str, int]: + ... +``` + +### 6.2. Optional und Union + +```python +from typing import Optional, Union + +def parse_int(value: str) -> Optional[int]: + try: + return int(value) + except ValueError: + return None + +def stringify(value: Union[int, float]) -> str: + return f"{value:.2f}" +``` + +Aufruf: + +```python +result = parse_int("123") +if result is not None: + print(result + 1) +``` + +Mypy zwingt dich, mit dem `None`-Fall umzugehen. + +### 6.3. Typen für Klassen + +```python +class User: + def __init__(self, user_id: int, name: str) -> None: + self.user_id = user_id + self.name = name + + def greet(self) -> str: + return f"Hello, {self.name}!" +``` + +### 6.4. Dataclasses mit Typen + +```python +from dataclasses import dataclass + +@dataclass +class Product: + id: int + name: str + price_cents: int + +def apply_discount(product: Product, percent: float) -> Product: + discount = int(product.price_cents * percent / 100) + return Product( + id=product.id, + name=product.name, + price_cents=product.price_cents - discount + ) +``` + +Mypy prüft, ob du `Product` überall korrekt verwendest. + +### 6.5. Typen für „Dictionary-Objekte“: TypedDict + +Wenn du viele Dictionaries mit immer der gleichen Struktur verwendest (z.B. JSON): + +```python +from typing import TypedDict + +class UserData(TypedDict): + id: int + name: str + email: str + +def send_welcome_mail(user: UserData) -> None: + print(f"Sending mail to {user['email']}") +``` + +Mypy prüft: + +- fehlen Keys? +- stimmen die Typen der Werte? + +### 6.6. Duck Typing mit Protocol + +Du willst „etwas, das wie eine Datei ist“, nicht unbedingt eine konkrete Klasse: + +```python +from typing import Protocol + +class Writer(Protocol): + def write(self, data: str) -> int: + ... + +def log_to(writer: Writer, message: str) -> None: + writer.write(message + "\n") +``` + +Jede Klasse, die `write(self, str) -> int` implementiert, passt – ganz im Sinne von „Duck Typing“, aber statisch überprüfbar. + +### 6.7. Mypy-Konfiguration in der Praxis + +`mypy.ini` im Projektverzeichnis: + +```ini +[mypy] +python_version = 3.11 +strict = True # aktiviert viele strenge Checks + +# Beispiele für häufige Optionen: +# disallow_untyped_defs = True +# disallow_incomplete_defs = True +# no_implicit_optional = True +# warn_unused_ignores = True +# warn_return_any = True +``` + +Du kannst auch per Modulbereich Regeln setzen: + +```ini +[mypy-myproject.legacy.*] +ignore_errors = True # Legacy-Code vorerst ignorieren + +[mypy-myproject.core.*] +strict = True +``` + +--- + +## Zusammenfassung + +- **Grundidee**: mypy ist ein statischer Typprüfer für Python. Du annotierst deinen Code mit Typen, und mypy überprüft, ob der Code diese Verträge einhält. +- **Probleme, die gelöst werden**: + - Frühes Finden von typbezogenen Fehlern + - Sicherere Refactorings + - Bessere Dokumentation und Tooling/IDE-Unterstützung +- **Abgrenzung**: + - Linter → Stil & allgemeine Codequalität; mypy → Typkonsistenz + - Tests → fachliche Korrektheit zur Laufzeit; mypy → statische Typkonsistenz + - Andere Typchecker (Pyright etc.) verfolgen die gleiche Grundidee + - Runtime-Validatoren (pydantic) prüfen zur Laufzeit, mypy nur zur Analysezeit +- **Herausforderungen**: + - Einstieg in bestehende, dynamische Codebasen + - Dynamische Python-Features sind schwer statisch zu erfassen + - Komplexe Typen können verbose sein + - Manchmal sind `cast` und `# type: ignore` nötig + +Wenn du magst, kann ich dir als nächsten Schritt ein kleines Beispielprojekt skizzieren (z.B. eine Mini-API oder ein CLI-Tool) und zeigen, wie man mypy dort konkret einführt und schrittweise verschärft. \ No newline at end of file diff --git a/python/pytest.md b/python/pytest.md new file mode 100755 index 0000000..e84a7d2 --- /dev/null +++ b/python/pytest.md @@ -0,0 +1,1212 @@ +# Chatbot Output +... nach [[#Anhang Prompt|diesem Promt]] + +Hier ein ausführliches, praxisnahes Tutorial zu `pytest`, inkl. Vergleich zu `unittest` und vielen Beispielen. + +--- +## 1. Kurzer Einstieg: Was ist `pytest`? + +`pytest` ist ein beliebtes Test-Framework für Python, das gegenüber `unittest` vor allem: + +- weniger Boilerplate benötigt, +- deutlich mächtigere Fixture-Mechanismen bietet, +- umfangreiche Parametrisierung unterstützt, +- ein großes Plugin-Ökosystem hat. + +### Installation + +```bash +pip install pytest +``` + +### Minimaler Test mit `pytest` + +Datei: `test_example.py` + +```python +def test_addition(): + assert 1 + 1 == 2 +``` + +Ausführen: + +```bash +pytest +``` + +`pytest` findet automatisch alle Dateien, die mit `test_` beginnen oder auf `_test.py` enden, und darin Funktionen/Klassen, deren Namen mit `test` beginnen. + +--- + +## 2. Wichtigste Features von `pytest` (mit Beispielen) + +### 2.1 Einfache Test-Schreibweise (statt `unittest.TestCase`) + +**unittest-Version:** + +```python +import unittest + +class TestMath(unittest.TestCase): + def test_addition(self): + self.assertEqual(1 + 1, 2) + +if __name__ == '__main__': + unittest.main() +``` + +**pytest-Version:** + +```python +def test_addition(): + assert 1 + 1 == 2 +``` + +Unterschiede: + +- Kein TestCase-Subclass, keine `self.assert*`-Methoden nötig. +- normale `assert`-Statements, die von `pytest` „umgeschrieben“ werden, um bei Fehlern sehr gute Fehlermeldungen zu erzeugen. + +--- + +### 2.2 Intelligente Fehlermeldungen durch Assert-Rewriting + +```python +def test_list_contains(): + items = [1, 2, 3] + assert 4 in items +``` + +Fehlerausgabe (vereinfacht): + +```text +> assert 4 in items +E assert 4 in [1, 2, 3] +``` + +Bei komplexeren Ausdrücken zeigt `pytest` oft die einzelnen Teilausdrücke und deren Werte. + +--- + +### 2.3 Fixtures: Wiederverwendbare Setup-/Teardown-Logik + +Fixtures sind einer der größten Vorteile von `pytest` im Vergleich zu `unittest.setUp/tearDown`. + +**Beispiel: einfache Fixture** + +```python +import pytest + +@pytest.fixture +def sample_user(): + return {"id": 1, "name": "Alice"} + +def test_user_has_name(sample_user): + assert sample_user["name"] == "Alice" +``` + +- Fixture `sample_user` wird automatisch per Parameter in den Test injiziert. +- Kein explizites Aufrufen, kein Erben von Klassen notwendig. + +**Mit Setup & Teardown (yield-Fixture)** + +```python +import pytest +import sqlite3 + +@pytest.fixture +def db_connection(): + conn = sqlite3.connect(":memory:") + # Setup: Tabelle anlegen + conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)") + yield conn + # Teardown: Verbindung schließen + conn.close() + +def test_insert_user(db_connection): + db_connection.execute("INSERT INTO users (name) VALUES (?)", ("Alice",)) + cursor = db_connection.execute("SELECT COUNT(*) FROM users") + count = cursor.fetchone()[0] + assert count == 1 +``` + +--- + +### 2.4 Fixture-Scopes: Performance optimieren + +Scope-Optionen: `function`, `class`, `module`, `package`, `session`. + +```python +import pytest + +@pytest.fixture(scope="session") +def expensive_resource(): + print("Init expensive resource once per session") + return {"data": "big thing"} + +def test_one(expensive_resource): + assert expensive_resource["data"] == "big thing" + +def test_two(expensive_resource): + assert "data" in expensive_resource +``` + +Die Fixture wird nur einmal pro Testlauf erstellt, nicht pro Testfunktion. + +--- + +### 2.5 Parametrisierung: Viele Testfälle mit wenig Code + +```python +import pytest + +@pytest.mark.parametrize( + "a, b, expected", + [ + (1, 2, 3), + (0, 0, 0), + (-1, 1, 0), + ], +) +def test_add(a, b, expected): + assert a + b == expected +``` + +`pytest` erzeugt automatisch drei Testfälle: + +- `test_add[1-2-3]` +- `test_add[0-0-0]` +- `test_add[-1-1-0]` + +Im Vergleich dazu ist Parametrisierung in `unittest` umständlicher (z. B. mit `subTest` oder Metaklassen). + +--- + +### 2.6 Marker: Tests kategorisieren, skippen, xfail + +**skip/xfail** + +```python +import pytest +import sys + +@pytest.mark.skipif(sys.platform == "win32", reason="Does not run on Windows") +def test_only_on_unix(): + assert True + +@pytest.mark.xfail(reason="Bug #123 not fixed yet") +def test_known_bug(): + assert 1 / 0 == 1 # absichtlich falsch +``` + +**eigene Marker (z. B. „slow“, „integration“)** +`pytest.ini`: + +```ini +[pytest] +markers = + slow: marks tests as slow (deselect with '-m "not slow"') +``` + +Verwendung: + +```python +import pytest + +@pytest.mark.slow +def test_long_running(): + ... +``` + +Ausführen nur schneller Tests: + +```bash +pytest -m "not slow" +``` + +--- + +### 2.7 Testauswahl & Filter + +- `pytest path/to/tests` – Ordner/Datei wählen +- `pytest -k "name_part"` – filtern nach Testnamen + +```bash +pytest -k "addition" +``` + +führt nur Tests aus, deren Name „addition“ enthält. + +--- + +### 2.8 Plugins & Ecosystem + +Beispiele: + +- `pytest-cov` – Coverage-Reports +- `pytest-xdist` – parallele Testausführung +- `pytest-django`, `pytest-flask` – Framework-spezifische Fixtures +- `pytest-mock` – einfachere Nutzung von `unittest.mock` +- `pytest-rerunfailures` – flaky Tests neu starten + +Integration (z. B. Coverage): + +```bash +pip install pytest-cov +pytest --cov=mein_package --cov-report=term-missing +``` + +--- + +### 2.9 Integration mit `unittest` + +`pytest` findet und führt auch `unittest.TestCase`-Klassen aus: + +```python +import unittest + +class TestMath(unittest.TestCase): + def test_addition(self): + self.assertEqual(1 + 1, 2) +``` + +Einfach mit `pytest` laufen lassen – keine Änderung nötig. Das erleichtert die schrittweise Migration. + +--- + +## 3. Typische Use-Cases für `pytest` + +### 3.1 Unit-Tests (Funktionen, Klassen, Methoden) + +```python +def is_even(n: int) -> bool: + return n % 2 == 0 + +def test_is_even(): + assert is_even(2) + assert not is_even(3) +``` + +--- + +### 3.2 Integrationstests (z. B. Datenbank + Service) + +```python +@pytest.fixture +def user_service(db_connection): + # Imagine a service that uses the DB connection + class UserService: + def create_user(self, name): + db_connection.execute("INSERT INTO users (name) VALUES (?)", (name,)) + def count_users(self): + return db_connection.execute("SELECT COUNT(*) FROM users").fetchone()[0] + return UserService() + +def test_user_service_integration(user_service): + user_service.create_user("Alice") + user_service.create_user("Bob") + assert user_service.count_users() == 2 +``` + +--- + +### 3.3 API-Tests (REST, HTTP) + +```python +import requests + +@pytest.mark.integration +def test_public_api(): + resp = requests.get("https://httpbin.org/get") + assert resp.status_code == 200 + data = resp.json() + assert "url" in data +``` + +--- + +### 3.4 CLI-Tests + +```python +import subprocess + +def test_cli_help(): + result = subprocess.run(["mytool", "--help"], capture_output=True, text=True) + assert result.returncode == 0 + assert "Usage:" in result.stdout +``` + +--- + +### 3.5 Property-based Testing (mit Hypothesis) + +```bash +pip install hypothesis pytest +``` + +```python +from hypothesis import given, strategies as st + +def my_add(a, b): + return a + b + +@given(st.integers(), st.integers()) +def test_my_add_commutative(a, b): + assert my_add(a, b) == my_add(b, a) +``` + +--- + +## 4. Was gehört zu einem kompletten & effizienten Testframework / Test-Suite? + +### 4.1 Struktur des Projekts + +Typische Struktur: + +```text +myproject/ + mypackage/ + __init__.py + core.py + ... + tests/ + __init__.py (optional) + conftest.py + test_core.py + test_api.py + integration/ + test_db_integration.py +``` + +- Tests in separatem `tests/`-Ordner +- `conftest.py` für gemeinsame Fixtures/Konfiguration + +--- + +### 4.2 Gemeinsame Fixtures in `conftest.py` + +`tests/conftest.py`: + +```python +import pytest +from mypackage.app import create_app + +@pytest.fixture(scope="session") +def app(): + return create_app(testing=True) + +@pytest.fixture +def client(app): + return app.test_client() # Beispiel: Flask +``` + +Tests können `client` direkt verwenden: + +```python +def test_health_check(client): + resp = client.get("/health") + assert resp.status_code == 200 +``` + +--- + +### 4.3 Konfigurationsdatei `pytest.ini` / `pyproject.toml` + +`pytest.ini`: + +```ini +[pytest] +addopts = -ra -q +testpaths = tests +markers = + slow: slow tests + integration: integration tests +``` + +Damit: + +- Standardoptionen (`-ra`: Zusammenfassung von Skips/Xfails, `-q`: quiet) +- Testpfade +- Marker-Definitionen, damit keine Warnungen kommen + +--- + +### 4.4 Coverage-Integration + +```bash +pip install pytest-cov +``` + +`pytest.ini`: + +```ini +[pytest] +addopts = --cov=mypackage --cov-report=term-missing +``` + +Ausführen: + +```bash +pytest +``` + +Coverage ist immer aktiviert. + +--- + +### 4.5 Testtypen trennen (schnell vs. langsam, Unit vs. Integration) + +Beispiel mit Markern: + +```python +import pytest + +@pytest.mark.slow +def test_big_data_processing(): + ... + +@pytest.mark.integration +def test_external_service(): + ... +``` + +- In CI: Unit-Tests ohne `slow`/`integration` häufig laufen lassen +- Nacht-Build: alle Tests + +```bash +pytest -m "not slow and not integration" +``` + +--- + +### 4.6 CI/CD-Integration (GitHub Actions Beispiel) + +`.github/workflows/tests.yml`: + +```yaml +name: Tests + +on: [push, pull_request] + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install -r requirements.txt + - run: pytest +``` + +--- + +### 4.7 Testdaten-Strategie + +- Factory-Funktionen/Fixtures statt harte Testdaten +- z. B. `factory_boy` oder einfache Fixtures: + +```python +import itertools +import pytest + +_id_counter = itertools.count(1) + +@pytest.fixture +def user_factory(): + def _create_user(name="User", active=True): + return { + "id": next(_id_counter), + "name": name, + "active": active, + } + return _create_user + +def test_create_multiple_users(user_factory): + u1 = user_factory(name="Alice") + u2 = user_factory(name="Bob", active=False) + assert u1["id"] != u2["id"] +``` + +--- + +### 4.8 Mocking / Patching + +Mit `unittest.mock` direkt oder über `pytest-mock`: + +```bash +pip install pytest-mock +``` + +```python +def send_email(address, subject): + # send email ... + ... + +def notify_user(user): + send_email(user.email, "Welcome!") + +def test_notify_user(mocker): + mock_send = mocker.patch(__name__ + ".send_email") + class User: + email = "test@example.com" + notify_user(User()) + mock_send.assert_called_once_with("test@example.com", "Welcome!") +``` + +--- + +## 5. Herausforderungen mit `pytest` (und wie man damit umgeht) + +### 5.1 „Magie“ und versteckte Abhängigkeiten bei Fixtures + +Problem: + +- Viele Fixtures in `conftest.py` +- Testfunktionen haben lange Parameterlisten +- Schwer nachzuvollziehen, woher Werte kommen + +Ansätze: + +- Fixtures möglichst lokal definieren, wenn sie nur in wenigen Tests gebraucht werden +- Namespacing durch mehrere `conftest.py` in Unterordnern +- Gute Benennung + Docstrings der Fixtures + +--- + +### 5.2 Komplexe Fixture-Hierarchien + +Z. B. Fixture A hängt von B, B von C, etc. – schwer zu verstehen. + +Empfehlungen: + +- Komplexe Abhängigkeiten dokumentieren +- ggf. in Helper-Funktionen auslagern, statt in Fixture-Graph zu pressen +- fixere Struktur pro Layer (Unit vs. Integration) trennen + +--- + +### 5.3 Performance + +Typische Ursachen: + +- Fixtures mit teurem Setup ohne größeren Scope (`function` statt `session`) +- Datenbankzugriffe in vielen Tests +- Externe Dienste (Netzwerk) + +Lösungen: + +- Fixture-Scopes gezielt einsetzen (`module`, `session`) +- Datenbank pro Testlauf statt pro Test zurücksetzen (z. B. via Transaktionen) +- Externe Dienste mocken +- Parallele Ausführung mit `pytest-xdist`: + +```bash +pip install pytest-xdist +pytest -n auto +``` + +--- + +### 5.4 Flaky Tests + +Tests, die manchmal grün, manchmal rot sind (Zeit, Zufall, externe Abhängigkeiten). + +Maßnahmen: + +- Zeitabhängigkeiten mocken (`freezegun`, eigene Time-Adapter) +- Zufall mit deterministischem Seed versehen +- Externe Dienste mocken oder stubben +- Falls gar nicht vermeidbar: Plugins wie `pytest-rerunfailures` nutzen (aber das ist eher die letzte Option) + +--- + +### 5.5 Migration von `unittest` zu `pytest` + +Herausforderungen: + +- Gewohnheit (xUnit-Stil vs. funktionaler Stil) +- Mischung aus alten `unittest`-Tests und neuen `pytest`-Tests + +Strategie: + +1. Bestehende `unittest`-Tests unverändert lassen; nur `pytest` statt `python -m unittest` verwenden. +2. Neue Tests im `pytest`-Stil schreiben. +3. Schrittweise Klassen in einfache Testfunktionen migrieren, wo sinnvoll. +4. Gemeinsame Setups aus `setUp/tearDown` in Fixtures extrahieren. + +--- + +## 6. Vergleich `pytest` vs. `unittest` im Detail + +### 6.1 Syntax und Stil + +**unittest:** + +```python +import unittest + +class TestCalculator(unittest.TestCase): + def setUp(self): + self.calc = Calculator() + + def test_add(self): + self.assertEqual(self.calc.add(1, 2), 3) + + def test_subtract(self): + self.assertEqual(self.calc.subtract(5, 3), 2) +``` + +**pytest:** + +```python +import pytest + +@pytest.fixture +def calc(): + return Calculator() + +def test_add(calc): + assert calc.add(1, 2) == 3 + +def test_subtract(calc): + assert calc.subtract(5, 3) == 2 +``` + +Gemeinsamkeiten: + +- Beide Frameworks unterstützen Setup/Teardown, Assertions, Testdiscovery. + +Unterschiede: + +- `unittest`: OOP/xUnit-Stil, Methoden in Klassen, `self.assert*`. +- `pytest`: funktionaler Stil, freie Funktionen, normale `assert`-Statements, Fixtures statt `setUp/tearDown`. + +--- + +### 6.2 Assertions + +**unittest:** + +```python +self.assertEqual(a, b) +self.assertTrue(cond) +self.assertRaises(ValueError, func, arg1) +``` + +**pytest:** + +```python +assert a == b +assert cond +import pytest +with pytest.raises(ValueError): + func(arg1) +``` + +Vorteile bei `pytest`: + +- Weniger Boilerplate. +- Ausführlichere Fehlermeldungen bei komplexen Ausdrücken. + +--- + +### 6.3 Setup/Teardown vs. Fixtures + +**unittest:** + +```python +class TestSomething(unittest.TestCase): + def setUp(self): + self.db = setup_db() + + def tearDown(self): + self.db.close() +``` + +**pytest mit Fixture:** + +```python +@pytest.fixture +def db(): + connection = setup_db() + yield connection + connection.close() +``` + +Die Fixture kann von beliebig vielen Tests wiederverwendet werden – auch über verschiedene Dateien hinweg (via `conftest.py`). + +--- + +### 6.4 Parametrisierung + +**unittest mit `subTest`:** + +```python +class TestAdd(unittest.TestCase): + def test_add(self): + cases = [(1, 2, 3), (0, 0, 0), (-1, 1, 0)] + for a, b, expected in cases: + with self.subTest(a=a, b=b): + self.assertEqual(a + b, expected) +``` + +**pytest:** + +```python +@pytest.mark.parametrize("a, b, expected", [ + (1, 2, 3), + (0, 0, 0), + (-1, 1, 0), +]) +def test_add(a, b, expected): + assert a + b == expected +``` + +`pytest` erzeugt separate Testfälle, was Reporting und Debugging erleichtert. + +--- + +### 6.5 Plugins vs. Standardbibliothek + +- `unittest` ist Teil der Standardbibliothek – keine zusätzliche Abhängigkeit. +- `pytest` ist ein Drittanbieterpaket, aber: + - deutlich mehr Plugins und Erweiterungen, + - in der Praxis weit verbreitet (De-Facto-Standard in vielen Python-Projekten). + +--- + +### 6.6 Interoperabilität + +- `pytest` kann `unittest`-Tests ausführen. +- `unittest` weiß nichts von `pytest`-Tests. + +Dadurch eignet sich `pytest` gut für eine schrittweise Migration. + +--- + +## 7. Fazit & Empfehlung fürs Vorgehen + +Wenn du bisher mit `unittest` gearbeitet hast, ist eine typische Übergangsstrategie: + +1. **Start einfach:** Installiere `pytest`, führe deine bestehenden `unittest`-Tests mit `pytest` aus. +2. **Neue Tests mit pytest-Stil:** Für neue Features nur noch `pytest`-Tests (Funktionen, Fixtures, Parametrisierung). +3. **SetUp/Teardown migrieren:** Häufige Setups in `unittest` schrittweise in `pytest`-Fixtures nach `conftest.py` überführen. +4. **Plugins nutzen:** + - `pytest-cov` für Coverage, + - optional `pytest-xdist` für Speed, + - `pytest-mock` für Mocking. +1. **Marker & Struktur etablieren:** Tests in schnelle/ langsame, Unit/Integration gliedern; sinnvolle Projektstruktur und `pytest.ini` anlegen. +# Pytest Official: Get Started +[Get Started — pytest documentation](https://docs.pytest.org/en/7.1.x/getting-started.html) + +# Real Python: Effective Python Testing With pytest +[Effective Python Testing With pytest – Real Python](https://realpython.com/pytest-python-testing/) + +`pytest` is a popular testing framework for Python that simplifies the process of writing and executing tests. To start using `pytest`, install it with `pip` in a virtual environment. `pytest` offers several advantages over `unittest` that ships with Python, such as less boilerplate code, more readable output, and a rich plugin ecosystem. + +**By the end of this tutorial, you’ll understand that:** + +- Using `pytest` requires **installing it with `pip`** in a virtual environment to set up the `pytest` command. +- `pytest` allows for less code, easier readability, and more features **compared to `unittest`**. +- Managing **test dependencies** and **state** with `pytest` is made efficient through the use of **fixtures**, which provide explicit dependency declarations. +- **Parametrization** in `pytest` helps avoid redundant test code by allowing multiple test scenarios from a single test function. +- **Assertion introspection** in `pytest` provides detailed information about failures in the test report. + +**Free Bonus:** [5 Thoughts On Python Mastery](https://realpython.com/bonus/python-mastery-course/), a free course for Python developers that shows you the roadmap and the mindset you’ll need to take your Python skills to the next level. + + ==**Take the Quiz:**== Test your knowledge with our interactive “Effective Testing with Pytest” quiz. You’ll receive a score upon completion to help you track your learning progress: + +--- + +[ + +![Effective Python Testing With Pytest](https://files.realpython.com/media/Intermediate-Advanced-PyTest-Features_Watermarked.43fb169e7121.jpg) + + + +](https://realpython.com/quizzes/effective-testing-with-pytest/) + +**Interactive Quiz** + +[Effective Testing with Pytest](https://realpython.com/quizzes/effective-testing-with-pytest/) + +In this quiz, you'll test your understanding of pytest, a Python testing tool. With this knowledge, you'll be able to write more efficient and effective tests, ensuring your code behaves as expected. + +## How to Install `pytest`[](https://realpython.com/pytest-python-testing/#how-to-install-pytest "Permanent link") + +To follow along with some of the examples in this tutorial, you’ll need to install `pytest`. As most [Python packages](https://realpython.com/python-modules-packages/), `pytest` is available on [PyPI](https://realpython.com/pypi-publish-python-package/). You can install it in a [virtual environment](https://realpython.com/python-virtual-environments-a-primer/) using [`pip`](https://realpython.com/what-is-pip/): + +- [Windows](https://realpython.com/pytest-python-testing/#windows-1) +- [Linux + macOS](https://realpython.com/pytest-python-testing/#linux-macos-1) + +`PS> python -m venv venv PS> .\venv\Scripts\activate (venv) PS> python -m pip install pytest` + +The `pytest` command will now be available in your installation environment. + +[Remove ads](https://realpython.com/account/join/) + +## What Makes `pytest` So Useful?[](https://realpython.com/pytest-python-testing/#what-makes-pytest-so-useful "Permanent link") + +If you’ve written unit [tests](https://realpython.com/python-testing/) for your Python code before, then you may have used Python’s built-in **`unittest`** module. `unittest` provides a solid base on which to build your test suite, but it has a few shortcomings. + +A number of third-party testing frameworks attempt to address some of the issues with `unittest`, and `pytest` has proven to be one of the most popular. `pytest` is a feature-rich, plugin-based ecosystem for testing your Python code. + +If you haven’t had the pleasure of using `pytest` yet, then you’re in for a treat! Its philosophy and features will make your testing experience more productive and enjoyable. With `pytest`, common tasks require less code and advanced tasks can be achieved through a variety of time-saving commands and plugins. It’ll even run your existing tests out of the box, including those written with `unittest`. + +As with most frameworks, some development patterns that make sense when you first start using `pytest` can start causing pains as your test suite grows. This tutorial will help you understand some of the tools `pytest` provides to keep your testing efficient and effective even as it scales. + +### Less Boilerplate[](https://realpython.com/pytest-python-testing/#less-boilerplate "Permanent link") + +Most functional tests follow the Arrange-Act-Assert model: + +1. **Arrange**, or set up, the conditions for the test +2. **Act** by calling some function or method +3. **Assert** that some end condition is true + +Testing frameworks typically hook into your test’s [assertions](https://realpython.com/python-assert-statement/) so that they can provide information when an assertion fails. `unittest`, for example, provides a number of helpful assertion utilities out of the box. However, even a small set of tests requires a fair amount of [boilerplate code](https://en.wikipedia.org/wiki/Boilerplate_code). + +Imagine you’d like to write a test suite just to make sure that `unittest` is working properly in your project. You might want to write one test that always passes and one that always fails: + +`test_with_unittest.py` + +`from unittest import TestCase class TryTesting(TestCase): def test_always_passes(self): self.assertTrue(True) def test_always_fails(self): self.assertTrue(False)` + +You can then run those tests from the command line using the `discover` option of `unittest`: + +`(venv) $ python -m unittest discover F. ====================================================================== FAIL: test_always_fails (test_with_unittest.TryTesting) ---------------------------------------------------------------------- Traceback (most recent call last): File "...\effective-python-testing-with-pytest\test_with_unittest.py", line 10, in test_always_fails self.assertTrue(False) AssertionError: False is not true ---------------------------------------------------------------------- Ran 2 tests in 0.006s FAILED (failures=1)` + +As expected, one test passed and one failed. You’ve proven that `unittest` is working, but look at what you had to do: + +1. Import the `TestCase` class from `unittest` +2. Create `TryTesting`, a [subclass](https://realpython.com/python3-object-oriented-programming/) of `TestCase` +3. Write a method in `TryTesting` for each test +4. Use one of the `self.assert*` methods from `unittest.TestCase` to make assertions + +That’s a significant amount of code to write, and because it’s the minimum you need for _any_ test, you’d end up writing the same code over and over. `pytest` simplifies this workflow by allowing you to use normal functions and Python’s `assert` keyword directly: + +`test_with_pytest.py` + +`def test_always_passes(): assert True def test_always_fails(): assert False` + +That’s it. You don’t have to deal with any imports or classes. All you need to do is include a function with the `test_` prefix. Because you can use the `assert` keyword, you don’t need to learn or remember all the different `self.assert*` methods in `unittest`, either. If you can write an expression that you expect to evaluate to `True`, and then `pytest` will test it for you. + +Not only does `pytest` eliminate a lot of boilerplate, but it also provides you with a much more detailed and easy-to-read output. + +### Nicer Output[](https://realpython.com/pytest-python-testing/#nicer-output "Permanent link") + +You can run your test suite using the `pytest` command from the top-level folder of your project: + +`(venv) $ pytest ============================= test session starts ============================= platform win32 -- Python 3.10.5, pytest-7.1.2, pluggy-1.0.0 rootdir: ...\effective-python-testing-with-pytest collected 4 items test_with_pytest.py .F [ 50%] test_with_unittest.py F. [100%] ================================== FAILURES =================================== ______________________________ test_always_fails ______________________________ def test_always_fails(): > assert False E assert False test_with_pytest.py:7: AssertionError ________________________ TryTesting.test_always_fails _________________________ self = def test_always_fails(self): > self.assertTrue(False) E AssertionError: False is not true test_with_unittest.py:10: AssertionError =========================== short test summary info =========================== FAILED test_with_pytest.py::test_always_fails - assert False FAILED test_with_unittest.py::TryTesting::test_always_fails - AssertionError:... ========================= 2 failed, 2 passed in 0.20s =========================` + +`pytest` presents the test results differently than `unittest`, and the `test_with_unittest.py` file was also automatically included. The report shows: + +1. The system state, including which versions of Python, `pytest`, and any plugins you have installed +2. The `rootdir`, or the directory to search under for configuration and tests +3. The number of tests the runner discovered + +These items are presented in the first section of the output: + +`============================= test session starts ============================= platform win32 -- Python 3.10.5, pytest-7.1.2, pluggy-1.0.0 rootdir: ...\effective-python-testing-with-pytest collected 4 items` + +The output then indicates the status of each test using a syntax similar to `unittest`: + +- **A dot (`.`)** means that the test passed. +- **An `F`** means that the test has failed. +- **An `E`** means that the test raised an unexpected exception. + +The special characters are shown next to the name with the overall progress of the test suite shown on the right: + +`test_with_pytest.py .F [ 50%] test_with_unittest.py F. [100%]` + +For tests that fail, the report gives a detailed breakdown of the failure. In the example, the tests failed because `assert False` always fails: + +`================================== FAILURES =================================== ______________________________ test_always_fails ______________________________ def test_always_fails(): > assert False E assert False test_with_pytest.py:7: AssertionError ________________________ TryTesting.test_always_fails _________________________ self = def test_always_fails(self): > self.assertTrue(False) E AssertionError: False is not true test_with_unittest.py:10: AssertionError` + +This extra output can come in extremely handy when debugging. Finally, the report gives an overall status report of the test suite: + +`=========================== short test summary info =========================== FAILED test_with_pytest.py::test_always_fails - assert False FAILED test_with_unittest.py::TryTesting::test_always_fails - AssertionError:... ========================= 2 failed, 2 passed in 0.20s =========================` + +When compared to unittest, the `pytest` output is much more informative and readable. + +In the next section, you’ll take a closer look at how `pytest` takes advantage of the existing `assert` keyword. + +[Remove ads](https://realpython.com/account/join/) + +### Less to Learn[](https://realpython.com/pytest-python-testing/#less-to-learn "Permanent link") + +Being able to use the [`assert`](https://realpython.com/python-assert-statement/) keyword is also powerful. If you’ve used it before, then there’s nothing new to learn. Here are a few assertion examples so you can get an idea of the types of test you can make: + +`test_assert_examples.py` + +`def test_uppercase(): assert "loud noises".upper() == "LOUD NOISES" def test_reversed(): assert list(reversed([1, 2, 3, 4])) == [4, 3, 2, 1] def test_some_primes(): assert 37 in { num for num in range(2, 50) if not any(num % div == 0 for div in range(2, num)) }` + +They look very much like normal Python functions. All of this makes the learning curve for `pytest` shallower than it is for `unittest` because you don’t need to learn new constructs to get started. + +Note that each test is quite small and self-contained. This is common—you’ll see long function names and not a lot going on within a function. This serves mainly to keep your tests isolated from each other, so if something breaks, you know exactly where the problem is. A nice side effect is that the labeling is much better in the output. + +To see an example of a project that creates a test suite along with the main project, check out the [Build a Hash Table in Python With TDD](https://realpython.com/python-hash-table/) tutorial. Additionally, you can work on Python practice problems to try test-driven development yourself while you [get ready for your next interview](https://realpython.com/python-practice-problems/) or [parse CSV files](https://realpython.com/python-interview-problem-parsing-csv-files/). + +In the next section, you’re going to be examining fixtures, a great pytest feature to help you manage test input values. + +### Easier to Manage State and Dependencies[](https://realpython.com/pytest-python-testing/#easier-to-manage-state-and-dependencies "Permanent link") + +Your tests will often depend on types of data or [test doubles](https://en.wikipedia.org/wiki/Test_double) that mock objects your code is likely to encounter, such as [dictionaries](https://realpython.com/python-dicts/) or [JSON](https://realpython.com/python-json/) files. + +With `unittest`, you might extract these dependencies into `.setUp()` and `.tearDown()` methods so that each test in the class can make use of them. Using these special methods is fine, but as your test classes get larger, you may inadvertently make the test’s dependence entirely **implicit**. In other words, by looking at one of the many tests in isolation, you may not immediately see that it depends on something else. + +Over time, implicit dependencies can lead to a complex tangle of code that you have to unwind to make sense of your tests. Tests should help to make your code more understandable. If the tests themselves are difficult to understand, then you may be in trouble! + +`pytest` takes a different approach. It leads you toward **explicit** dependency declarations that are still reusable thanks to the availability of [fixtures](https://docs.pytest.org/en/latest/fixture.html). `pytest` fixtures are functions that can create data, test doubles, or initialize system state for the test suite. Any test that wants to use a fixture must explicitly use this fixture function as an argument to the test function, so dependencies are always stated up front: + +`fixture_demo.py` + +`import pytest @pytest.fixture def example_fixture(): return 1 def test_with_fixture(example_fixture): assert example_fixture == 1` + +Looking at the test function, you can immediately tell that it depends on a fixture, without needing to check the whole file for fixture definitions. + +**Note:** You usually want to put your tests into their own folder called `tests` at the root level of your project. + +For more information about structuring a Python application, check out the [video course](https://realpython.com/courses/structuring-python-application/) on that very topic. + +Fixtures can also make use of other fixtures, again by declaring them explicitly as dependencies. That means that, over time, your fixtures can become bulky and modular. Although the ability to insert fixtures into other fixtures provides enormous flexibility, it can also make managing dependencies more challenging as your test suite grows. + +Later in this tutorial, you’ll learn [more about fixtures](https://realpython.com/pytest-python-testing/#fixtures-managing-state-and-dependencies) and try a few techniques for handling these challenges. + +### Easy to Filter Tests[](https://realpython.com/pytest-python-testing/#easy-to-filter-tests "Permanent link") + +As your test suite grows, you may find that you want to run just a few tests on a feature and save the full suite for later. `pytest` provides a few ways of doing this: + +- **Name-based filtering**: You can limit `pytest` to running only those tests whose fully qualified names match a particular expression. You can do this with the `-k` parameter. +- **Directory scoping**: By default, `pytest` will run only those tests that are in or under the current directory. +- **Test categorization**: `pytest` can include or exclude tests from particular categories that you define. You can do this with the `-m` parameter. + +Test categorization in particular is a subtly powerful tool. `pytest` enables you to create **marks**, or custom labels, for any test you like. A test may have multiple labels, and you can use them for granular control over which tests to run. Later in this tutorial, you’ll see an example of [how `pytest` marks work](https://realpython.com/pytest-python-testing/#marks-categorizing-tests) and learn how to make use of them in a large test suite. + +[Remove ads](https://realpython.com/account/join/) + +### Allows Test Parametrization[](https://realpython.com/pytest-python-testing/#allows-test-parametrization "Permanent link") + +When you’re testing functions that process data or perform generic transformations, you’ll find yourself writing many similar tests. They may differ only in the [input or output](https://realpython.com/python-input-output/) of the code being tested. This requires duplicating test code, and doing so can sometimes obscure the behavior that you’re trying to test. + +`unittest` offers a way of collecting several tests into one, but they don’t show up as individual tests in result reports. If one test fails and the rest pass, then the entire group will still return a single failing result. `pytest` offers its own solution in which each test can pass or fail independently. You’ll see [how to parametrize tests](https://realpython.com/pytest-python-testing/#parametrization-combining-tests) with `pytest` later in this tutorial. + +### Has a Plugin-Based Architecture[](https://realpython.com/pytest-python-testing/#has-a-plugin-based-architecture "Permanent link") + +One of the most beautiful features of `pytest` is its openness to customization and new features. Almost every piece of the program can be cracked open and changed. As a result, `pytest` users have developed a rich ecosystem of helpful plugins. + +Although some `pytest` plugins focus on specific frameworks like [Django](https://www.djangoproject.com/), others are applicable to most test suites. You’ll see [details on some specific plugins](https://realpython.com/pytest-python-testing/#useful-pytest-plugins) later in this tutorial. + +## Fixtures: Managing State and Dependencies[](https://realpython.com/pytest-python-testing/#fixtures-managing-state-and-dependencies "Permanent link") + +`pytest` fixtures are a way of providing data, test doubles, or state setup to your tests. Fixtures are functions that can return a wide range of values. Each test that depends on a fixture must explicitly accept that fixture as an argument. + +### When to Create Fixtures[](https://realpython.com/pytest-python-testing/#when-to-create-fixtures "Permanent link") + +In this section, you’ll simulate a typical [test-driven development](https://realpython.com/courses/test-driven-development-pytest/) (TDD) workflow. + +Imagine you’re writing a function, `format_data_for_display()`, to process the data returned by an API endpoint. The data represents a list of people, each with a given name, family name, and job title. The function should output a list of strings that include each person’s full name (their `given_name` followed by their `family_name`), a colon, and their `title`: + +`format_data.py` + +`def format_data_for_display(people): ... # Implement this!` + +In good TDD fashion, you’ll want to first write a test for it. You might write the following code for that: + +`test_format_data.py` + +`def test_format_data_for_display(): people = [ { "given_name": "Alfonsa", "family_name": "Ruiz", "title": "Senior Software Engineer", }, { "given_name": "Sayid", "family_name": "Khan", "title": "Project Manager", }, ] assert format_data_for_display(people) == [ "Alfonsa Ruiz: Senior Software Engineer", "Sayid Khan: Project Manager", ]` + +While writing this test, it occurs to you that you may need to write another function to transform the data into comma-separated values for use in [Excel](https://realpython.com/openpyxl-excel-spreadsheets-python/): + +`format_data.py` + +`def format_data_for_display(people): ... # Implement this! def format_data_for_excel(people): ... # Implement this!` + +Your to-do list grows! That’s good! One of the advantages of TDD is that it helps you plan out the work ahead. The test for the `format_data_for_excel()` function would look awfully similar to the `format_data_for_display()` function: + +`test_format_data.py` + +`def test_format_data_for_display(): # ... def test_format_data_for_excel(): people = [ { "given_name": "Alfonsa", "family_name": "Ruiz", "title": "Senior Software Engineer", }, { "given_name": "Sayid", "family_name": "Khan", "title": "Project Manager", }, ] assert format_data_for_excel(people) == """given,family,title Alfonsa,Ruiz,Senior Software Engineer Sayid,Khan,Project Manager """` + +Notably, both the tests have to repeat the definition of the `people` variable, which is quite a few lines of code. + +If you find yourself writing several tests that all make use of the same underlying test data, then a fixture may be in your future. You can pull the repeated data into a single function decorated with `@pytest.fixture` to indicate that the function is a `pytest` fixture: + +`test_format_data.py` + +`import pytest @pytest.fixture def example_people_data(): return [ { "given_name": "Alfonsa", "family_name": "Ruiz", "title": "Senior Software Engineer", }, { "given_name": "Sayid", "family_name": "Khan", "title": "Project Manager", }, ] # ...` + +You can use the fixture by adding the function reference as an argument to your tests. Note that you don’t call the fixture function. `pytest` takes care of that. You’ll be able to use the return value of the fixture function as the name of the fixture function: + +`test_format_data.py` + +`# ... def test_format_data_for_display(example_people_data): assert format_data_for_display(example_people_data) == [ "Alfonsa Ruiz: Senior Software Engineer", "Sayid Khan: Project Manager", ] def test_format_data_for_excel(example_people_data): assert format_data_for_excel(example_people_data) == """given,family,title Alfonsa,Ruiz,Senior Software Engineer Sayid,Khan,Project Manager """` + +Each test is now notably shorter but still has a clear path back to the data it depends on. Be sure to name your fixture something specific. That way, you can quickly determine if you want to use it when writing new tests in the future! + +When you first discover the power of fixtures, it can be tempting to use them all the time, but as with all things, there’s a balance to be maintained. + +[Remove ads](https://realpython.com/account/join/) + +### When to Avoid Fixtures[](https://realpython.com/pytest-python-testing/#when-to-avoid-fixtures "Permanent link") + +Fixtures are great for extracting data or objects that you use across multiple tests. However, they aren’t always as good for tests that require slight variations in the data. Littering your test suite with fixtures is no better than littering it with plain data or objects. It might even be worse because of the added layer of indirection. + +As with most abstractions, it takes some practice and thought to find the right level of fixture use. + +Nevertheless, fixtures will likely be an integral part of your test suite. As your project grows in scope, the challenge of scale starts to come into the picture. One of the challenges facing any kind of tool is how it handles being used at scale, and luckily, `pytest` has a bunch of useful features that can help you manage the complexity that comes with growth. + +### How to Use Fixtures at Scale[](https://realpython.com/pytest-python-testing/#how-to-use-fixtures-at-scale "Permanent link") + +As you extract more fixtures from your tests, you might see that some fixtures could benefit from further abstraction. In `pytest`, fixtures are **modular**. Being modular means that fixtures can be [imported](https://realpython.com/python-import/), can import other modules, and they can depend on and import other fixtures. All this allows you to compose a suitable fixture abstraction for your use case. + +For example, you may find that fixtures in two separate files, or [modules](https://realpython.com/python-modules-packages/), share a common dependency. In this case, you can move fixtures from test modules into more general fixture-related modules. That way, you can import them back into any test modules that need them. This is a good approach when you find yourself using a fixture repeatedly throughout your project. + +If you want to make a fixture available for your whole project without having to import it, a special configuration module called [`conftest.py`](https://docs.pytest.org/en/6.2.x/fixture.html#conftest-py-sharing-fixtures-across-multiple-files) will allow you to do that. + +`pytest` looks for a `conftest.py` module in each directory. If you add your general-purpose fixtures to the `conftest.py` module, then you’ll be able to use that fixture throughout the module’s parent directory and in any subdirectories without having to import it. This is a great place to put your most widely used fixtures. + +Another interesting use case for fixtures and `conftest.py` is in guarding access to resources. Imagine that you’ve written a test suite for code that deals with [API calls](https://realpython.com/api-integration-in-python/). You want to ensure that the test suite doesn’t make any real network calls even if someone accidentally writes a test that does so. + +`pytest` provides a [`monkeypatch`](https://docs.pytest.org/en/latest/monkeypatch.html) fixture to replace values and behaviors, which you can use to great effect: + +`conftest.py` + +`import pytest import requests @pytest.fixture(autouse=True) def disable_network_calls(monkeypatch): def stunted_get(): raise RuntimeError("Network access not allowed during testing!") monkeypatch.setattr(requests, "get", lambda *args, **kwargs: stunted_get())` + +By placing `disable_network_calls()` in `conftest.py` and adding the `autouse=True` option, you ensure that network calls will be disabled in every test across the suite. Any test that executes code calling `requests.get()` will raise a `RuntimeError` indicating that an unexpected network call would have occurred. + +Your test suite is growing in numbers, which gives you a great feeling of confidence to make changes and not break things unexpectedly. That said, as your test suite grows, it might start taking a long time. Even if it doesn’t take that long, perhaps you’re focusing on some core behavior that trickles down and breaks most tests. In these cases, you might want to limit the test runner to only a certain category of tests. + +## Marks: Categorizing Tests[](https://realpython.com/pytest-python-testing/#marks-categorizing-tests "Permanent link") + +In any large test suite, it would be nice to avoid running _all_ the tests when you’re trying to iterate quickly on a new feature. Apart from the default behavior of `pytest` to run all tests in the current working directory, or the [filtering](https://docs.pytest.org/en/7.1.x/example/markers.html#using-k-expr-to-select-tests-based-on-their-name) functionality, you can take advantage of **markers**. + +`pytest` enables you to define categories for your tests and provides options for including or excluding categories when you run your suite. You can mark a test with any number of categories. + +Marking tests is useful for categorizing tests by subsystem or dependencies. If some of your tests require access to a database, for example, then you could create a `@pytest.mark.database_access` mark for them. + +**Pro tip**: Because you can give your marks any name you want, it can be easy to mistype or misremember the name of a mark. `pytest` will warn you about marks that it doesn’t recognize in the test output. + +You can use the `--strict-markers` flag to the `pytest` command to ensure that all marks in your tests are registered in your `pytest` configuration file, `pytest.ini`. It’ll prevent you from running your tests until you register any unknown marks. + +For more information on registering marks, check out the [`pytest` documentation](https://docs.pytest.org/en/latest/mark.html#registering-marks). + +When the time comes to run your tests, you can still run them all by default with the `pytest` command. If you’d like to run only those tests that require database access, then you can use `pytest -m database_access`. To run all tests _except_ those that require database access, you can use `pytest -m "not database_access"`. You can even use an `autouse` fixture to limit database access to those tests marked with `database_access`. + +Some plugins expand on the functionality of marks by adding their own guards. The [`pytest-django`](https://pytest-django.readthedocs.io/en/latest/) plugin, for instance, provides a `django_db` mark. Any tests without this mark that try to access the database will fail. The first test that tries to access the database will trigger the creation of Django’s test database. + +The requirement that you add the `django_db` mark nudges you toward stating your dependencies explicitly. That’s the `pytest` philosophy, after all! It also means that you can much more quickly run tests that don’t rely on the database, because `pytest -m "not django_db"` will prevent the test from triggering database creation. The time savings really add up, especially if you’re diligent about running your tests frequently. + +`pytest` provides a few marks out of the box: + +- **`skip`** skips a test unconditionally. +- **`skipif`** skips a test if the expression passed to it evaluates to `True`. +- **`xfail`** indicates that a test is expected to fail, so if the test _does_ fail, the overall suite can still result in a passing status. +- **`parametrize`** creates multiple variants of a test with different values as arguments. You’ll learn more about this mark shortly. + +You can see a list of all the marks that `pytest` knows about by running `pytest --markers`. + +On the topic of parametrization, that’s coming up next. + +[Remove ads](https://realpython.com/account/join/) + +## Parametrization: Combining Tests[](https://realpython.com/pytest-python-testing/#parametrization-combining-tests "Permanent link") + +You saw earlier in this tutorial how `pytest` fixtures can be used to reduce code duplication by extracting common dependencies. Fixtures aren’t quite as useful when you have several tests with slightly different inputs and expected outputs. In these cases, you can [**parametrize**](http://doc.pytest.org/en/latest/example/parametrize.html) a single test definition, and `pytest` will create variants of the test for you with the parameters you specify. + +Imagine you’ve written a function to tell if a string is a [palindrome](https://en.wikipedia.org/wiki/Palindrome). An initial set of tests could look like this: + +`def test_is_palindrome_empty_string(): assert is_palindrome("") def test_is_palindrome_single_character(): assert is_palindrome("a") def test_is_palindrome_mixed_casing(): assert is_palindrome("Bob") def test_is_palindrome_with_spaces(): assert is_palindrome("Never odd or even") def test_is_palindrome_with_punctuation(): assert is_palindrome("Do geese see God?") def test_is_palindrome_not_palindrome(): assert not is_palindrome("abc") def test_is_palindrome_not_quite(): assert not is_palindrome("abab")` + +All of these tests except the last two have the same shape: + +`def test_is_palindrome_(): assert is_palindrome("")` + +This is starting to smell a lot like boilerplate. `pytest` so far has helped you get rid of boilerplate, and it’s not about to let you down now. You can use `@pytest.mark.parametrize()` to fill in this shape with different values, reducing your test code significantly: + +`@pytest.mark.parametrize("palindrome", [ "", "a", "Bob", "Never odd or even", "Do geese see God?", ]) def test_is_palindrome(palindrome): assert is_palindrome(palindrome) @pytest.mark.parametrize("non_palindrome", [ "abc", "abab", ]) def test_is_palindrome_not_palindrome(non_palindrome): assert not is_palindrome(non_palindrome)` + +The first argument to `parametrize()` is a comma-delimited string of parameter names. You don’t have to provide more than one name, as you can see in this example. The second argument is a [list](https://realpython.com/python-list/) of either [tuples](https://realpython.com/python-tuple/) or single values that represent the parameter value(s). You could take your parametrization a step further to combine all your tests into one: + +`@pytest.mark.parametrize("maybe_palindrome, expected_result", [ ("", True), ("a", True), ("Bob", True), ("Never odd or even", True), ("Do geese see God?", True), ("abc", False), ("abab", False), ]) def test_is_palindrome(maybe_palindrome, expected_result): assert is_palindrome(maybe_palindrome) == expected_result` + +Even though this shortened your code, it’s important to note that in this case you actually lost some of the more descriptive nature of the original functions. Make sure you’re not parametrizing your test suite into incomprehensibility. You can use parametrization to separate the test data from the test behavior so that it’s clear what the test is testing, and also to make the different test cases easier to read and maintain. + +## Durations Reports: Fighting Slow Tests[](https://realpython.com/pytest-python-testing/#durations-reports-fighting-slow-tests "Permanent link") + +Each time you switch contexts from implementation code to test code, you incur some [overhead](https://en.wikipedia.org/wiki/Overhead_\(computing\)). If your tests are slow to begin with, then overhead can cause friction and frustration. + +You read earlier about using marks to filter out slow tests when you run your suite, but at some point you’re going to need to run them. If you want to improve the speed of your tests, then it’s useful to know _which_ tests might offer the biggest improvements. `pytest` can automatically record test durations for you and report the top offenders. + +Use the `--durations` option to the `pytest` command to include a duration report in your test results. `--durations` expects an integer value `n` and will report the slowest `n` number of tests. A new section will be included in your test report: + +`(venv) $ pytest --durations=5 ... ============================= slowest 5 durations ============================= 3.03s call test_code.py::test_request_read_timeout 1.07s call test_code.py::test_request_connection_timeout 0.57s call test_code.py::test_database_read (2 durations < 0.005s hidden. Use -vv to show these durations.) =========================== short test summary info =========================== ...` + +Each test that shows up in the durations report is a good candidate to speed up because it takes an above-average amount of the total testing time. Note that short durations are hidden by default. As spelled out in the report, you can increase the report verbosity and show these by passing `-vv` together with `--durations`. + +Be aware that some tests may have an invisible setup overhead. You read earlier about how the first test marked with `django_db` will trigger the creation of the Django test database. The `durations` report reflects the time it takes to set up the database in the test that triggered the database creation, which can be misleading. + +You’re well on your way to full test coverage. Next, you’ll be taking a look at some of the plugins that are part of the rich `pytest` plugin ecosystem. + +## Useful `pytest` Plugins[](https://realpython.com/pytest-python-testing/#useful-pytest-plugins "Permanent link") + +You learned about a few valuable `pytest` plugins earlier in this tutorial. In this section, you’ll be exploring those and a few others in more depth—everything from utility plugins like `pytest-randomly` to library-specific ones, like those for Django. + +[Remove ads](https://realpython.com/account/join/) + +### `pytest-randomly`[](https://realpython.com/pytest-python-testing/#pytest-randomly "Permanent link") + +Often the order of your tests is unimportant, but as your codebase grows, you may inadvertently introduce some side effects that could cause some tests to fail if they were run out of order. + +[`pytest-randomly`](https://github.com/pytest-dev/pytest-randomly) forces your tests to run in a random order. `pytest` always collects all the tests it can find before running them. `pytest-randomly` just shuffles that list of tests before execution. + +This is a great way to uncover tests that depend on running in a specific order, which means they have a **stateful dependency** on some other test. If you built your test suite from scratch in `pytest`, then this isn’t very likely. It’s more likely to happen in test suites that you migrate to `pytest`. + +The plugin will print a seed value in the configuration description. You can use that value to run the tests in the same order as you try to fix the issue. + +### `pytest-cov`[](https://realpython.com/pytest-python-testing/#pytest-cov "Permanent link") + +If you want to measure how well your tests cover your implementation code, then you can use the [coverage](https://coverage.readthedocs.io/) package. [`pytest-cov`](https://pytest-cov.readthedocs.io/en/latest/) integrates coverage, so you can run `pytest --cov` to see the test coverage report and boast about it on your project front page. + +### `pytest-django`[](https://realpython.com/pytest-python-testing/#pytest-django "Permanent link") + +[`pytest-django`](https://pytest-django.readthedocs.io/en/latest/) provides a handful of useful fixtures and marks for dealing with Django tests. You saw the `django_db` mark earlier in this tutorial. The `rf` fixture provides direct access to an instance of Django’s [`RequestFactory`](https://docs.djangoproject.com/en/3.0/topics/testing/advanced/#django.test.RequestFactory). The `settings` fixture provides a quick way to set or override Django settings. These plugins are a great boost to your Django testing productivity! + +If you’re interested in learning more about using `pytest` with Django, then check out [How to Provide Test Fixtures for Django Models in Pytest](https://realpython.com/django-pytest-fixtures/). + +### `pytest-bdd`[](https://realpython.com/pytest-python-testing/#pytest-bdd "Permanent link") + +`pytest` can be used to run tests that fall outside the traditional scope of unit testing. [Behavior-driven development](https://en.wikipedia.org/wiki/Behavior-driven_development) (BDD) encourages writing plain-language descriptions of likely user actions and expectations, which you can then use to determine whether to implement a given feature. [pytest-bdd](https://pytest-bdd.readthedocs.io/en/latest/) helps you use [Gherkin](http://docs.behat.org/en/v2.5/guides/1.gherkin.html) to write feature tests for your code. + +You can see which other plugins are available for `pytest` with this extensive [list of third-party plugins](https://docs.pytest.org/en/latest/reference/plugin_list.html). + +## Conclusion[](https://realpython.com/pytest-python-testing/#conclusion "Permanent link") + +`pytest` offers a core set of productivity features to filter and optimize your tests along with a flexible plugin system that extends its value even further. Whether you have a huge legacy `unittest` suite or you’re starting a new project from scratch, `pytest` has something to offer you. + +In this tutorial, you learned how to use: + +- **Fixtures** for handling test dependencies, state, and reusable functionality +- **Marks** for categorizing tests and limiting access to external resources +- **Parametrization** for reducing duplicated code between tests +- **Durations** to identify your slowest tests +- **Plugins** for integrating with other frameworks and testing tools + +Install `pytest` and give it a try. You’ll be glad you did. Happy testing! + +If you’re looking for an example project built with `pytest`, then check out the tutorial on [building a hash table with TDD](https://realpython.com/python-hash-table/), which will not only get you up to speed with `pytest`, but also help you master hash tables! + +--- +# Anhang: Prompt +```text +Kannst Du mir einen ausführlichen Überblick und ein Tutorial zu `pytest` geben. +- was sind die wichtigsten Features? +- Welche Use-Cases werden abgedeckt? +- Was gehört typischerweise zu einem kompletten und effizeinten Testframework oder Test-Suite +- Welche Herausforderungen gibt es? +- Ich habe bisher immer mit `unittest` gearbeitet. Gib gerne Hinweise auf Gemeinsamkeiten und Unterschiede zwischen `pytest` und `unittest`. +- Ergänze alle Erläuterungen mit praxisnahen Beispielen +``` \ No newline at end of file diff --git a/python/ruff.md b/python/ruff.md new file mode 100755 index 0000000..1011cb1 --- /dev/null +++ b/python/ruff.md @@ -0,0 +1,540 @@ +Ruff ist ein relativ neues, sehr schnelles Tool zur Verbesserung der Code-Qualität in Python. Es kombiniert Funktionen, für die man früher mehrere Werkzeuge brauchte (Linter, Formatter, Import-Sortierer, teilweise Sicherheits-Checks), in einem Programm. + +Ich gehe Schritt für Schritt durch: + +1. Grundidee und Zielgruppe +2. Was macht ruff konkret? +3. Abgrenzung zu ähnlichen Tools +4. Welche Probleme löst ruff? +5. Typische Herausforderungen im Einsatz +6. Praxisnahe Beispiele (inkl. Konfiguration) + +--- + +## 1. Grundidee von ruff – in einfachen Worten + +Stell dir vor, mehrere Entwickler:innen arbeiten an einem Python-Projekt. Ohne Regeln sieht der Code schnell sehr unterschiedlich aus: + +- Mal sind Leerzeichen anders gesetzt, +- mal werden nicht genutzte Variablen liegen gelassen, +- vielleicht wird an manchen Stellen unsicherer Code geschrieben, +- im schlimmsten Fall fallen bestimmte Fehler erst spät auf. + +Bisher nutzte man typischerweise mehrere Tools: + +- ein **Linter** (z. B. *flake8* oder *pylint*), +- einen **Formatter** (z. B. *black*), +- einen **Import-Sortierer** (z. B. *isort*), +- evtl. ein **Security-Tool** (z. B. *bandit*), +- plus diverse Plugins. + +**Ruff will das bündeln:** + +- Ein Tool für viele Code-Qualitätsaspekte +- Sehr schnell (geschrieben in Rust, nicht in Python) +- Einfache Konfiguration (alles typischerweise in `pyproject.toml`) +- Viele Checks, die sich an bekannten Tools orientieren (flake8-Plugins, isort-Regeln, bandit-Regeln usw.) + +--- + +## 2. Was macht ruff konkret? + +### 2.1. Linting (Fehler und Stilprobleme finden) + +Ruff untersucht deinen Python-Code und meldet z. B.: + +- Syntax-Fehler +- Ungenutzte Variablen oder Importe +- Logische Stolperfallen (z. B. `== None` statt `is None`) +- Stil-Regeln (z. B. maximale Zeilenlänge) +- Potenziell unsichere Konstruktionen + +Beispiel: + +```python +def calculate(a, b): + result = a + b + unused = 42 + if a == None: + print("a is None") + return result +``` + +Ruff würde in etwa melden (vereinfacht): + +- `unused` wird nie verwendet +- `a == None` – besser `a is None` + +Viele dieser Probleme kann ruff auch **automatisch beheben** (`--fix`). + +--- + +### 2.2. Formatierung (Code automatisch „schön“ machen) + +Ruff hat inzwischen einen **eigenen Formatter**, der ähnlich wie *black* funktioniert: + +- Einheitliche Einrückungen und Zeilenumbrüche +- Klammer-Formatierung +- Konsistente Verwendung von Anführungszeichen (je nach Einstellung) +- Entfernung überflüssiger Leerzeilen usw. + +Beispiel (unformatierter Code): + +```python +def foo( x:int,y:int )->int: + return x+y +``` + +Nach `ruff format` könnte das so aussehen: + +```python +def foo(x: int, y: int) -> int: + return x + y +``` + +--- + +### 2.3. Imports sortieren und aufräumen + +Ruff kann ähnlich wie *isort*: + +- Importe sortieren (alphabetisch und nach Gruppen: Standardbibliothek, Drittanbieter, Projektcode) +- Unbenutzte Importe entfernen + +Beispiel: + +```python +import myproject.utils +import os +import sys +import requests + +from math import sqrt +from math import ceil +``` + +Nach ruff (vereinfacht): + +```python +import os +import sys +from math import ceil, sqrt + +import requests + +import myproject.utils +``` + +Und wenn `sys` gar nicht verwendet wird, kann ruff es auch entfernen. + +--- + +### 2.4. Ein Tool – mehrere Regel-Sammlungen + +Ruff bringt rule sets mit, die vielen bekannten Tools entsprechen, z. B.: + +- **E/F/W**: Pycodestyle/Pyflakes-ähnlich (via flake8) +- **I**: isort-Regeln +- **N**: pep8-naming (Namenskonventionen) +- **S**: bandit (Sicherheitsregeln) +- **UP**: pyupgrade (veraltete Syntax, modernisieren) +- u. v. m. + +Du kannst über die Konfiguration steuern, welche Regel-Gruppen du aktivierst oder deaktivierst. + +--- + +## 3. Abgrenzung zu ähnlichen Tools + +### 3.1. Ruff vs. flake8 (+ Plugins) + +**flake8** ist ein Linter; für viele Extras braucht man Plugins: + +- z. B. `flake8-bugbear`, `flake8-import-order`, `pep8-naming` usw. + +**Ruff:** + +- Bietet die Funktionalität vieler flake8-Plugins „eingebaut“. +- Ist deutlich **schneller** (insbesondere bei großen Projekten). +- Wird oft als Drop-in-Ersatz für flake8 verwendet. + +Aber: + +- flake8 ist schon lange etabliert; manche Teams haben stark angepasste flake8-Setups, die man nicht 1:1 nach ruff übertragen kann. +- Einige Spezial-Plugins existieren ggf. nur für flake8. + +--- + +### 3.2. Ruff vs. pylint + +**pylint**: + +- Sehr umfangreicher Linter (viele komplexe Regeln, u. a. über Projektstruktur, OOP-Patterns usw.) +- Langsam im Vergleich zu ruff +- Detaillierte Reports und Scores + +**Ruff**: + +- Fokus auf **Geschwindigkeit** und auf Regeln, die sich gut automatisieren/auto-fixen lassen. +- Viele „klassische“ Lint-Regeln, Naming, Imports, Security-Basics, aber nicht alle tiefgehenden Analysen von pylint. +- Für komplexe Architektur-Regeln wird weiterhin oft pylint oder andere Tools genutzt. + +--- + +### 3.3. Ruff vs. black + +**black**: + +- Reiner **Formatter** – macht nur Formatierung, keine Lint-Fehler (bis auf ganz wenige Ausnahmen). +- Sehr stabile, strikte Formatierung (Meinung: „The uncompromising code formatter“). + +**Ruff**: + +- Eigenständiger Formatter, der in vielen Projekten Black ersetzen kann. +- Zusätzlich: Linting, Import-Sortierung, Security-Regeln usw. +- Du kannst: + - nur ruff als Formatter nutzen, oder + - ruff als Linter + black als Formatter (dann `ruff format` nicht verwenden), wenn dein Team bereits stark auf black setzt. + +--- + +### 3.4. Ruff vs. isort + +**isort**: + +- Spezialisiert auf das Sortieren von Imports. + +**Ruff**: + +- Hat eine integrierte Import-Sortierung (Regelgruppe `I`). +- Für die meisten Fälle reicht ruff völlig aus. +- Wenn ihr sehr spezielle Import-Sortierregeln braucht, ist isort manchmal noch flexibler, aber das wird immer weniger relevant. + +--- + +### 3.5. Ruff vs. mypy/pyright (Typprüfung) + +**mypy/pyright**: + +- Statische Typprüfer: sie prüfen, ob die Typannotationen sinnvoll zusammenpassen. +- Finden z. B. Fehler wie: „Funktion gibt laut Typ `str` zurück, tatsächlich aber `int`“. + +**Ruff**: + +- Enthält Regeln, die mit Typannotationen arbeiten (z. B. Style, Safety), aber **kein vollwertiger Typprüfer**. +- Typfehler (im Sinne von mypy) sollten weiterhin mit mypy oder pyright geprüft werden. + +--- + +### 3.6. Ruff vs. bandit (Security) + +**bandit**: + +- Spezialisiertes Security-Tool für Python. + +**Ruff**: + +- Hat viele bandit-Regeln integriert (Regelgruppe `S`). +- Deckt gängige Sicherheitsfallen ab (z. B. `eval` auf untrusted Input, hartkodierte Passwörter etc.). +- Für tiefgehende Security-Audits kann ein spezialisiertes Tool trotzdem sinnvoll sein. + +--- + +## 4. Welche Probleme löst ruff? + +### 4.1. Performance-Probleme in großen Projekten + +Früher: + +- flake8 + black + isort + bandit + mypy + → viele Tools, mehrfaches Einlesen des Codes, CI dauert lange. + +Mit ruff: + +- Ein Tool übernimmt Linting, Formatierung, Importe, einen großen Teil der Security-Regeln. +- Deutlich weniger Laufzeit, besonders in CI-Pipelines oder bei großen Repositories. + +--- + +### 4.2. Zu viele Tools, komplizierte Konfiguration + +Problem: + +- Unterschiedliche Konfigurationsdateien (`.flake8`, `pyproject.toml`, `setup.cfg`, `.isort.cfg`, `pyproject.toml` für black…) +- Mehr Aufwand beim Onboarding neuer Teammitglieder. + +Ruff: + +- Typischerweise alles in **einer** Konfiguration (`pyproject.toml`). +- Weniger bewegliche Teile, einfachere Wartung. + +--- + +### 4.3. Inkonsequente Codequalität im Team + +Ohne einheitliche Tools: + +- Jede:r schreibt etwas anders. +- Diskussionen in Code-Reviews drehen sich um Stil statt Inhalte. +- Fehler (z. B. ungenutzte Variablen, potentielle Bugs) werden erst spät bemerkt. + +Mit ruff: + +- Gemeinsame, automatisierte Regeln. +- Automatische Fixes für vieles (z. B. im Editor oder Pre-Commit-Hooks). +- Code-Reviews können sich auf Architektur und Logik konzentrieren. + +--- + +### 4.4. Technische Schulden reduzieren + +In älteren Projekten: + +- Viele kleine Stil- und Qualitätsprobleme haben sich angesammelt. +- Niemand möchte „alles mal eben aufräumen“, weil Tools zu langsam sind oder es zu viel ist. + +Mit ruff: + +- Durch die Geschwindigkeit kann man auch große Codebasen lintern. +- Schrittweise Verbesserung möglich: z. B. zunächst nur wichtige Regelgruppen aktivieren, später mehr. + +--- + +## 5. Herausforderungen bei der Nutzung von ruff + +### 5.1. Zu viele Meldungen am Anfang + +Wenn du ruff das erste Mal auf ein älteres Projekt loslässt, bekommst du oft hunderte oder tausende Meldungen. + +Strategie: + +- Nur einen Teil der Regeln aktivieren (z. B. nur „kritische“ oder klar hilfreiche). +- Bestehende Verstöße einmalig ignorieren (per `--ignore` oder `--per-file-ignores`) und neue Verstöße blocken. +- Nach und nach alte Stellen aufräumen. + +--- + +### 5.2. Regeln verstehen und anpassen + +Ruff hat sehr viele Regeln. Nicht alle passen zu jedem Projekt. + +- Manche Regeln sind sehr streng (z. B. bestimmte Naming- oder Docstring-Regeln). +- Du musst überlegen: Welche Regeln sind für unser Team sinnvoll? + +Lösung: + +- Regeln gezielt aktivieren/deaktivieren. +- Dokumentieren, warum bestimmte Regeln aktiv oder abgeschaltet sind. + +--- + +### 5.3. Wechsel von bestehender Tool-Landschaft + +Wenn ihr schon flake8, black, isort etc. nutzt: + +- Müssen Einstellungen in ruff nachgebaut werden. +- Manche Teams werden black nicht sofort durch ruff format ersetzen wollen. + +Ein pragmatischer Weg: + +1. Zuerst ruff als **Linter** einführen (Formatierung bleibt bei black). +2. Wenn gewünscht, später ruff format testen und ggf. black ersetzen. + +--- + +### 5.4. Auto-Fixes mit Vorsicht genießen + +Ruff kann sehr viel automatisch reparieren: + +- In der Regel gut, aber: + - Bei manchen Regeln sollte man prüfen, ob die Änderung wirklich die Absicht trifft. + - In kritischen Bereichen (z. B. Security-sensible Logik) ggf. Auto-Fixes nicht blind akzeptieren. + +Empfehlung: + +- Auto-Fixes lokal ausführen, dann diff anschauen. +- In CI eher nur prüfen, nicht fixen. + +--- + +### 5.5. Editor-Integration + +Ruff hat gute Unterstützung in vielen Editoren (VS Code, PyCharm, Neovim usw.), aber: + +- Man muss oft ein Plugin oder eine Extension installieren. +- Manchmal überschneidet sich das mit vorhandenen Tools (z. B. Black-Extension vs. Ruff-Formatter); das muss sauber konfiguriert werden. + +--- + +## 6. Praxisnahe Beispiele + +### 6.1. Installation von ruff + +Typisch über `pip`: + +```bash +pip install ruff +``` + +Oder als dev-Abhängigkeit in `pyproject.toml` (Poetry/uv/pip-tools etc.). + +--- + +### 6.2. Einfacher Aufruf + +Linting: + +```bash +ruff check . +``` + +Mit automatischen Fixes: + +```bash +ruff check . --fix +``` + +Formatierung: + +```bash +ruff format . +``` + +--- + +### 6.3. Minimale Konfiguration in `pyproject.toml` + +```toml +[tool.ruff] +target-version = "py311" # welche Python-Version ihr nutzt +line-length = 88 # maximale Zeilenlänge +exclude = ["venv", ".venv", "build", "dist"] + +[tool.ruff.lint] +select = ["E", "F", "I", "B", "UP"] # Regelgruppen: Errors, Flake, Imports, Bugbear, pyupgrade +ignore = ["E501"] # Beispiel: lange Zeilen ignoriere ich (wenn ich black nutze) + +[tool.ruff.format] +# Beispiel: wenn ihr black-kompatible Einstellungen wollt +quote-style = "double" +indent-style = "space" +line-ending = "auto" +``` + +- `select` gibt an, welche Regelgruppen aktiv sind. +- `ignore` hebt einzelne Regelcodes auf (z. B. `E501` für Zeilenlänge). + +--- + +### 6.4. Beispiel: Unbenutzte Variable und schlechtes `None`-Checking + +Code: + +```python +def process(items): + count = len(items) + unused = 0 + + if items == None: + print("No items") + return count +``` + +Ruff meldet u. a.: + +- `F841` (unused variable `unused`) +- `E711` (Vergleich mit `None` mit `==` statt `is`) + +Mit `ruff check . --fix` könnte daraus werden: + +```python +def process(items): + count = len(items) + + if items is None: + print("No items") + return count +``` + +--- + +### 6.5. Beispiel: Imports aufräumen + +Code: + +```python +import os +import sys +import json +import requests + +def get_data(url): + r = requests.get(url) + return r.json() +``` + +Hier wird `os`, `sys` und `json` nicht genutzt. + +Ruff meldet: + +- `F401` (unused imports: os, sys, json) + +Nach `ruff check . --fix`: + +```python +import requests + + +def get_data(url): + r = requests.get(url) + return r.json() +``` + +--- + +### 6.6. Beispiel: Einsatz mit pre-commit + +Viele Teams nutzen [pre-commit](https://pre-commit.com/), damit Checks automatisch vor jedem Git-Commit laufen. + +`.pre-commit-config.yaml`: + +```yaml +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.5 # Beispielversion, ggf. aktualisieren + hooks: + - id: ruff + args: [--fix] + - id: ruff-format +``` + +Damit werden: + +- Linting + Auto-Fix vor jedem Commit ausgeführt, +- Formatierung automatisch angewendet. + +--- + +## 7. Zusammenfassung + +- **Ruff** ist ein modernes, sehr schnelles Tool zur Verbesserung der Code-Qualität in Python. +- Es kombiniert: + - Linting (Fehler und Stil), + - Formatierung, + - Import-Sortierung, + - viele Sicherheits- und Modernisierungsregeln. +- Es ersetzt in vielen Projekten: + - flake8 (+ viele Plugins), + - isort, + - teilweise black, + - teilweise bandit. +- Es löst Probleme wie: + - lange Tool-Laufzeiten, + - fragmentierte Konfiguration, + - inkonsistente Codequalität. +- Herausforderungen sind v. a.: + - Auswahl und Anpassung der Regeln, + - Umstieg von bestehenden Tools, + - Umgang mit großen Mengen an Anfangs-Warnungen. + +Wenn du möchtest, kann ich dir im nächsten Schritt eine **konkrete Beispielkonfiguration für ein kleines oder ein großes Projekt** vorschlagen (inkl. welche Regelgruppen sinnvoll sind und welche man am Anfang lieber auslässt). \ No newline at end of file diff --git a/python/uv.md b/python/uv.md new file mode 100755 index 0000000..0ec6bb6 --- /dev/null +++ b/python/uv.md @@ -0,0 +1,94 @@ +`uv` ist ein neues, sehr schnelles All-in-one-Tool für das Python-Ökosystem, entwickelt von Astral (den Machern von `ruff`). +Es ist im Kern ein **extrem schneller Paket- und Environment-Manager**, der mehrere bekannte Tools ersetzt bzw. bündelt: + +- `pip` / `pip-tools` (Installation & Lockfiles) +- `virtualenv` / `venv` (virtuelle Umgebungen) +- `pyenv` (verschiedene Python-Versionen verwalten) +- Teile von `poetry` / `pipx` (Projekt- & Tool-Management) + +`pip install uv` installiert dabei nur einen Bootstrapper, der das eigentliche `uv`-Binary nachlädt. + +--- + +## Wofür ist `uv` gut? + +### 1. Pakete extrem schnell installieren +`uv` ist in Rust geschrieben und deutlich schneller als `pip` oder `pip-tools` beim Auflösen und Installieren von Abhängigkeiten. + +Beispiel (Drop-in für pip): + +```bash +uv pip install requests +``` + +Auch mit `requirements.txt`: + +```bash +uv pip install -r requirements.txt +``` + +--- + +### 2. Virtuelle Umgebungen verwalten + +```bash +# Neue venv erstellen +uv venv .venv + +# venv aktivieren (z. B. in bash) +source .venv/bin/activate +``` + +--- + +### 3. Python-Versionen verwalten + +Ähnlich wie `pyenv`: + +```bash +# Bestimmte Python-Version installieren +uv python install 3.12 + +# Projekt mit bestimmter Version starten +uv run --python 3.12 main.py +``` + +--- + +### 4. Projekte initialisieren & reproduzierbare Builds + +`uv` unterstützt `pyproject.toml` und Lockfiles (`uv.lock`) für reproduzierbare Umgebungen: + +```bash +# Neues Projekt anlegen +uv init my-project + +cd my-project +uv sync # Installiert Abhängigkeiten gemäß pyproject + uv.lock +``` + +--- + +### 5. Skripte/Tools direkt ausführen + +Ähnlich wie `pipx`: + +```bash +uv run black . +uv run pytest +``` + +`uv` kümmert sich im Hintergrund um die passende Umgebung und Abhängigkeiten. + +--- + +## Kurzfassung + +`uv` ist sinnvoll, wenn du: + +- Python-Pakete **deutlich schneller** installieren willst, +- **alles rund um Python-Umgebungen** (Packages, venvs, Python-Versionen, Tools) mit **einem** konsistenten Tool verwalten möchtest, +- reproduzierbare Builds mit `pyproject.toml` und Lockfile brauchst, +- eine moderne Alternative zu `pip`, `virtualenv`, `pyenv`, `pip-tools`, teilweise `poetry`/`pipx` suchst. + +Wenn du aktuell `pip`, `virtualenv` & Co nutzt, kannst du mittelfristig sehr viel davon durch `uv` ersetzen. \ No newline at end of file diff --git a/python/uvicorn.md b/python/uvicorn.md new file mode 100755 index 0000000..d16836b --- /dev/null +++ b/python/uvicorn.md @@ -0,0 +1,469 @@ +## 1. Grundsätzliche Definition: Was ist **uvicorn**? + +**uvicorn** ist ein extrem performanter, asynchroner Web-Server für Python, der das **ASGI**‑Protokoll unterstützt. + +- **ASGI** = *Asynchronous Server Gateway Interface* +- uvicorn ist also das Bindeglied zwischen: + - dem Web (HTTP, WebSockets) + - und deiner Python‑Applikation (z.B. [[FastAPI]], Starlette, Django mit ASGI) + +Uvicorn basiert intern auf sehr schnellen C‑Bibliotheken: +- **uvloop** (schneller Event Loop, Ersatz für `asyncio`‑Loop) +- **httptools** (schnelles HTTP‑Parsing) + +Du verwendest uvicorn typischerweise, um eine ASGI‑App „zu starten“: + +```bash +uvicorn main:app --reload +``` + +--- + +## 2. Wichtige Begriffe: ASGI, WSGI und Web-Frameworks + +### 2.1 ASGI vs. WSGI + +- **WSGI** (älterer Standard, z.B. für Django (klassisch), Flask): + - synchron + - kein natives WebSocket‑Support + - typische Server: `gunicorn`, `uWSGI`, `mod_wsgi` + +- **ASGI** (moderner Standard): + - unterstützt **async/await** + - kann **HTTP** und **WebSockets** und Background Tasks + - typische Server: `uvicorn`, `hypercorn`, `daphne` + +uvicorn ist also ein **ASGI-Server**, nicht WSGI. + +### 2.2 uvicorn vs. Web-Frameworks ([[FastAPI]], Starlette, Django, Flask) + +- **Framework** ([[FastAPI]], Starlette, Django, Flask): + - definiert, wie du Routen, Views, Models, etc. schreibst. + - kümmert sich um Request/Response‑Logik + +- **uvicorn**: + - kümmert sich um das Annehmen von Verbindungen, HTTP‑Parsing, Event‑Loop‑Handling. + - ruft deine Applikation nur gemäß dem ASGI‑Protokoll auf. + +Bildlich: +**Browser** → (HTTP) → **uvicorn** → (ASGI) → **deine App** (z.B. [[FastAPI]]) + +--- + +## 3. Abgrenzung zu ähnlichen oder verwandten Begriffen + +### 3.1 uvicorn vs. Gunicorn + +- **gunicorn**: + - klassischer **WSGI**-Server (für sync‑Apps wie Flask oder Django ohne ASGI). + - kann aber mithilfe von Workern wie `uvicorn.workers.UvicornWorker` auch ASGI-App starten. + +Beispiel: [[FastAPI]]‑App mit gunicorn + uvicorn worker: + +```bash +gunicorn -k uvicorn.workers.UvicornWorker main:app -b 0.0.0.0:8000 +``` + +Hier ist: +- gunicorn = Prozessmanager und Worker‑Spawner +- uvicorn = eigentlicher ASGI‑Server pro Worker + +### 3.2 uvicorn vs. Hypercorn / Daphne + +- **hypercorn**: + - anderer ASGI‑Server (unterstützt z.B. HTTP/2, verschiedene Event Loops) +- **daphne**: + - ASGI‑Server aus dem Django‑Channels‑Ökosystem + +Alle drei (uvicorn, hypercorn, daphne) machen im Kern das Gleiche: +**ASGI‑Apps ausführen**, unterscheiden sich aber in Features, Performance und Konfigurationsmöglichkeiten. + +### 3.3 uvicorn vs. „eingebauter Development-Server“ + +Viele Frameworks haben eingebaute Dev-Server, z.B.: + +- Flask: `app.run(debug=True)` +- Django: `python manage.py runserver` + +Diese sind: +- für **Entwicklung** gedacht +- nicht für **Produktion** (Performance, Stabilität, Security) + +uvicorn ist ein **richtiger** Webserver, der für **Produktion** geeignet ist (oft zusammen mit einem Reverse Proxy wie [[Nginx]]). + +--- + +## 4. Welche Probleme löst uvicorn? + +### 4.1 Asynchrone Web‑Backends performant betreiben + +Mit ASGI kannst du: + +- `async def` Endpoints schreiben +- WebSockets nutzen +- viele gleichzeitige Requests mit einem Event Loop bedienen + +uvicorn ermöglicht dir, diese **asynchronen** Apps performant auszuliefern. + +Praxisnahes Beispiel ([[FastAPI]]): + +```python +# main.py +from fastapi import FastAPI +import asyncio + +app = FastAPI() + +@app.get("/items/{item_id}") +async def read_item(item_id: int): + await asyncio.sleep(1) # simuliert eine I/O-Operation + return {"item_id": item_id} +``` + +Starten mit uvicorn: + +```bash +uvicorn main:app --reload +``` + +uvicorn kümmert sich darum, dass mehrere Requests gleichzeitig abgearbeitet werden können, während `asyncio.sleep` nicht blockiert. + +### 4.2 WebSockets und Long-Lived Connections + +ASGI (und damit uvicorn) unterstützt **WebSockets** nativ, was mit WSGI nicht geht. + +Beispiel mit Starlette: + +```python +# main.py +from starlette.applications import Starlette +from starlette.responses import JSONResponse +from starlette.websockets import WebSocket +from starlette.routing import Route, WebSocketRoute + +async def homepage(request): + return JSONResponse({"hello": "world"}) + +async def websocket_endpoint(websocket: WebSocket): + await websocket.accept() + await websocket.send_text("Willkommen!") + while True: + data = await websocket.receive_text() + await websocket.send_text(f"Du hast gesendet: {data}") + +routes = [ + Route("/", endpoint=homepage), + WebSocketRoute("/ws", endpoint=websocket_endpoint), +] + +app = Starlette(routes=routes) +``` + +Start: + +```bash +uvicorn main:app +``` + +Mit WSGI wäre so ein WebSocket‑Endpoint nicht möglich. + +### 4.3 Produktionstauglicher Server gegenüber Entwicklungsservern + +- Stabilität bei hoher Last +- Steuerung von: + - Anzahl Worker‑Prozesse + - Timeouts + - Logging +- Start via CLI, systemd, Docker, Kubernetes etc. + +--- + +## 5. Grundlegende Verwendung von uvicorn + +### 5.1 Installation + +```bash +pip install uvicorn +# optional: schnellere Variante mit C-Extensions +pip install "uvicorn[standard]" +``` + +`[standard]` installiert u.a. `uvloop` und `httptools`. + +### 5.2 Minimalbeispiel: Plain-ASGI-App + +Du kannst eine ASGI‑App auch ohne Framework schreiben: + +```python +# app.py +async def app(scope, receive, send): + assert scope["type"] == "http" + + # Request body lesen (vereinfachter Fall) + await receive() + + body = b"Hello, world" + headers = [(b"content-type", b"text/plain")] + + await send({ + "type": "http.response.start", + "status": 200, + "headers": headers, + }) + await send({ + "type": "http.response.body", + "body": body, + }) +``` + +Starten: + +```bash +uvicorn app:app --reload +``` + +Erklärung: +- `app:app` = Modul `app.py`, Variable `app` +- `--reload` = automatischer Neustart bei Codeänderung (nur dev) + +### 5.3 Beispiel mit [[FastAPI]] + +```python +# main.py +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/") +async def root(): + return {"message": "Hello from uvicorn + FastAPI"} +``` + +Start: + +```bash +uvicorn main:app --reload --host 0.0.0.0 --port 8000 +``` + +Wichtige CLI‑Optionen: +- `--reload`: Auto-Reload bei Codeänderungen (Dev) +- `--host`: z.B. `0.0.0.0` um von außen erreichbar zu sein +- `--port`: Port, z.B. `8000` +- `--workers`: Anzahl der Prozesse (für Produktion) + +### 5.4 Starten aus Python heraus + +```python +# run.py +import uvicorn + +if __name__ == "__main__": + uvicorn.run( + "main:app", + host="0.0.0.0", + port=8000, + reload=True, + ) +``` + +Start: + +```bash +python run.py +``` + +--- + +## 6. Typische Konfigurationen und Szenarien + +### 6.1 Entwicklung + +- ein Worker +- `--reload` aktiviert +- Logging auf `debug` + +```bash +uvicorn main:app --reload --host 0.0.0.0 --port 8000 --log-level debug +``` + +### 6.2 Produktion (einfach) + +- mehrere Worker-Prozesse +- kein `--reload` +- Logging eher `info` oder `warning` + +```bash +uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 --log-level info +``` + +Richtwert für Worker: +`Anzahl CPU-Kerne * 2` (abhängig von App und Last; immer testen). + +### 6.3 Produktion hinter einem Reverse Proxy (z.B. [[Nginx]]) + +Typischer Aufbau: + +``` +Internet → Nginx (TLS, gzip, etc.) → uvicorn → FastAPI/Starlette/Django +``` + +- [[Nginx]] übernimmt TLS/SSL, Load Balancing, Static Files +- uvicorn macht die Application-Logik + +[[Nginx]]-Konfig (stark vereinfacht) könnte so aussehen: + +```nginx +location / { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; +} +``` + +uvicorn wird weiterhin wie oben gestartet. + +--- + +## 7. Herausforderungen und typische Stolpersteine + +### 7.1 Async/Synchron-Mix und Blockierungen + +**Problem:** +Du verwendest uvicorn (ASGI, async), aber in deinen Endpoints gibt es blockierende Operationen: + +- große CPU‑Aufgaben +- synchrones Warten auf externe APIs (z.B. `requests.get(...)`) +- schwere Datenbank‑Queries, die nicht async sind + +Beispiel: + +```python +@app.get("/slow") +async def slow(): + import time + time.sleep(5) # BLOCKIERT den Event-Loop + return {"status": "ok"} +``` + +Folge: +- Ein Request blockiert den Event Loop → alle anderen Requests warten mit. + +Lösungen: +- I/O: async Libraries benutzen (z.B. `httpx` statt `requests`, `asyncpg` statt sync‑DB‑Client) +- CPU‑lastig: in Thread‑Pool oder Process‑Pool auslagern (`run_in_threadpool` etc.) + +### 7.2 Gemeinsamer Zustand über Worker-Prozesse + +Wenn du `--workers > 1` nutzt, hast du **mehrere Prozesse**. +Globaler Zustand in Python wird **nicht** zwischen Prozessen geteilt. + +Beispiel (Problem): + +```python +counter = 0 + +@app.get("/count") +def count(): + global counter + counter += 1 + return {"counter": counter} +``` + +Mit mehreren Workern: +- jeder Worker hat seinen eigenen `counter` +- Ergebnisse sind inkonsistent + +Lösung: +- geteilten Zustand über externe Systeme (Redis, Datenbank, etc.) +- oder nur einen Worker nutzen, wenn globaler In-Memory-State unvermeidbar ist (aber meist unsauber). + +### 7.3 Datenbankverbindungen und Lebenszyklus + +uvicorn unterstützt ASGI‑`lifespan`‑Events (startup/shutdown). +Frameworks wie [[FastAPI]]/Starlette nutzen das, um z.B. DB‑Connections zu öffnen/schließen. + +Stolpersteine: +- Verbindungspools pro Worker korrekt initialisieren +- bei Shutdown sauber schließen +- nicht „pro Request“ neue Connections aufmachen + +Beispiel mit [[FastAPI]] (vereinfacht): + +```python +from fastapi import FastAPI + +app = FastAPI() +db = None + +@app.on_event("startup") +async def startup(): + global db + db = await some_async_db_connect() + +@app.on_event("shutdown") +async def shutdown(): + await db.close() +``` + +### 7.4 Logging und Error-Handling + +uvicorn hat eigenes Logging; dein Framework ebenso. +Typische Themen: + +- Log-Format in Produktion standardisieren +- Fehler-Logs im Zusammenspiel mit Reverse Proxy +- Ausführliche Logs in Dev, weniger in Prod + +Beispiel (JSON-Logging in Produktion, nur angedeutet): + +```bash +uvicorn main:app \ + --host 0.0.0.0 \ + --port 8000 \ + --log-config logging_config.yaml +``` + +In `logging_config.yaml` kannst du detailliert das Logging steuern. + +### 7.5 Plattformunterschiede (Windows vs. Linux) + +- `--reload` nutzt File-Watcher und Signale → unter Linux sehr stabil; unter Windows kann es ein paar Besonderheiten geben. +- In Produktion läuft uvicorn meist auf Linux‑Servern oder in Docker‑Containern. + +--- + +## 8. Kurze Checkliste für den praktischen Einstieg + +1. **Framework wählen** + - [[FastAPI]] oder Starlette, wenn du intensiv async nutzen willst. +1. **App schreiben** + - `app = FastAPI()`, Endpoints definieren. +3. **In Entwicklung starten** + ```bash + uvicorn main:app --reload + ``` +4. **Vor Produktion** + - Blocking‑Code prüfen (CPU, I/O) + - Datenbankzugriff sauber konfigurieren (Pools, async‑Client) + - Logging und Error‑Handling aufräumen +5. **In Produktion starten** (einfach) + ```bash + uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 + ``` +6. **Optional**: + - vor uvicorn einen [[Nginx]] oder Traefik setzen (TLS, Load Balancing) + +--- + +## 9. Zusammenfassung + +- **uvicorn** ist ein **ASGI‑Webserver** für Python, optimiert für **asynchrone** Web‑Apps. +- Er ist **kein Web‑Framework**, sondern die Laufzeitumgebung für Frameworks wie **[[FastAPI]]**, **Starlette** oder moderne **Django**‑Konfigurationen. +- Es löst die Probleme klassischer WSGI‑Server in Bezug auf **Async**, **WebSockets** und **Performance**. +- Typische Herausforderungen liegen im Bereich: + - korrektes Async‑Design + - Umgang mit mehreren Workern und gemeinsamem Zustand + - saubere Integration von Datenbanken, Logging, Deployment +- Für dich als Python‑Entwickler ist uvicorn im Alltag vor allem: + **das Kommando, mit dem du deine moderne Web‑API startest.** + diff --git a/sync-at-home.sh b/sync-at-home.sh old mode 100644 new mode 100755 diff --git a/sync-at-work.sh b/sync-at-work.sh new file mode 100755 index 0000000..3ad2a39 --- /dev/null +++ b/sync-at-work.sh @@ -0,0 +1 @@ +unison "$HOME/links/mathias_dfine_vault/Know-How/" "$HOME/it-know-how/" diff --git a/typescript/TYPESCRIPT_INTRO.md b/typescript/TYPESCRIPT_INTRO.md new file mode 100755 index 0000000..a8bfad4 --- /dev/null +++ b/typescript/TYPESCRIPT_INTRO.md @@ -0,0 +1,606 @@ +# TypeScript Introduction for Python Developers + +A practical guide to TypeScript, written for Python developers. + +--- + +## Table of Contents + +1. [What is TypeScript?](#what-is-typescript) +2. [Strengths & Weaknesses](#strengths--weaknesses) +3. [Typical Use Cases](#typical-use-cases) +4. [Basic Syntax](#basic-syntax) + - [Variables: `const` and `let`](#variables-const-and-let) + - [Functions](#functions) + - [Type Annotations](#type-annotations) +5. [Arrays](#arrays) + - [Array Methods: `find`, `filter`, `map`, `flatMap`](#array-methods-find-filter-map-flatmap) + - [Slicing and Indexing](#slicing-and-indexing) +6. [Handling Null and Undefined](#handling-null-and-undefined) + - [Optional Chaining `?.`](#optional-chaining-) + - [Nullish Coalescing `??`](#nullish-coalescing-) +7. [Strings and Template Literals](#strings-and-template-literals) +8. [Guard Clauses and Validation](#guard-clauses-and-validation) +9. [Date Handling](#date-handling) +10. [Code Style](#code-style) +11. [Quick Reference: Python to TypeScript](#quick-reference-python-to-typescript) + +--- + +## What is TypeScript? + +TypeScript is a superset of JavaScript that adds **static type checking**. Your TypeScript code compiles (transpiles) to plain JavaScript, which then runs in browsers, Node.js, or anywhere JavaScript runs. + +```ts +// TypeScript (what you write) +function greet(name: string): string { + return `Hello, ${name}`; +} +``` + +```js +// JavaScript (what runs) +function greet(name) { + return "Hello, " + name; +} +``` + +The key difference from Python: TypeScript checks types **at compile time**, while Python checks types at **runtime**. This means many bugs are caught before your code runs. + +--- + +## Strengths & Weaknesses + +### Strengths + +| Benefit | Description | +|---------|-------------| +| **Early bug detection** | Type errors are caught during development, not in production | +| **Better IDE support** | Autocomplete, inline docs, and refactoring work reliably | +| **Self-documenting code** | Types serve as living documentation | +| **Safer refactoring** | Rename a function and the compiler finds all call sites | +| **Gradual adoption** | Add TypeScript to existing JavaScript projects incrementally | + +### Weaknesses + +| Drawback | Description | +|----------|-------------| +| **Compilation step** | Requires a build process (though often simple) | +| **Learning curve** | Advanced types can be complex | +| **Boilerplate** | Type annotations add extra syntax | +| **Type system limits** | Complex runtime patterns may not fit static typing easily | + +### Comparison with Python + +| Aspect | TypeScript | Python | +|--------|-----------|--------| +| Typing | Static (compile time) | Dynamic (runtime) | +| Type inference | Yes (often inferrable) | Yes (via type hints) | +| Null safety | Optional (strict mode) | Via type hints | +| Null representation | `null`, `undefined` | `None` | +| Execution | Compiles to JS | Interpreted | + +--- + +## Typical Use Cases + +TypeScript shines in: + +- **Frontend web applications** (React, Vue, Angular all support TypeScript) +- **Node.js backends** (APIs, microservices) +- **Large codebases** where refactoring and maintenance matter +- **Teams** where code review and shared understanding are important +- **Projects needing stability** (banking, healthcare, enterprise software) + +Python still leads in data science, ML/AI, scripting, and rapid prototyping. + +--- + +## Basic Syntax + +### Variables: `const` and `let` + +TypeScript uses `const` for variables that won't be reassigned, and `let` for those that will. Avoid `var`. + +```ts +const apiBase = 'https://api.example.com'; // Cannot be reassigned +let count = 0; // Can be reassigned +count = count + 1; +``` + +**Important:** `const` prevents reassignment but doesn't make objects immutable: + +```ts +const config = { retries: 2 }; +config.retries = 3; // Allowed: mutating the object +// config = {}; // Error: reassigning the variable +``` + +**Python comparison:** +```python +API_BASE = 'https://api.example.com' # Convention only, not enforced +count = 0 +``` + +### Functions + +Basic function declaration with type annotations: + +```ts +function greet(name: string): string { + return `Hello, ${name}`; +} +``` + +With optional parameters and defaults: + +```ts +function buildWeeklyPeriods( + dateRange?: DateRange, + now = new Date(), +): string[] { + // ... +} +``` + +- `?:` marks a parameter as optional +- `= value` provides a default +- `: type` declares the return type + +**Python comparison:** +```python +def greet(name: str) -> str: + return f"Hello, {name}" + +def build_weekly_periods(date_range=None, now=None): + if now is None: + now = datetime.now() +``` + +### Type Annotations + +Type annotations come **after** the variable/parameter name (opposite of Python): + +```ts +const name: string = 'Alice'; +const age: number = 30; +const isActive: boolean = true; +``` + +Common basic types: + +| TypeScript | Python | Description | +|------------|--------|-------------| +| `string` | `str` | Text | +| `number` | `int` / `float` | All numbers | +| `boolean` | `bool` | True / False | +| `undefined` | — | Uninitialized | +| `null` | `None` | Intentional absence | +| `string[]` | `List[str]` | Array of strings | + +--- + +## Arrays + +TypeScript arrays are typed and support the same operations as Python lists. + +```ts +const parts = [ + { type: 'year', value: '2026' }, + { type: 'month', value: '03' }, + { type: 'day', value: '24' }, +]; + +// Access by index +const first = parts[0]; + +// Array length +const count = parts.length; +``` + +### Array Methods: `find`, `filter`, `map`, `flatMap` + +**`find`** — Get the first matching element: + +```ts +const yearPart = parts.find((part) => part.type === 'year'); +console.log(yearPart); // { type: 'year', value: '2026' } +``` + +Returns `undefined` if no match found. + +**Python comparison:** +```python +year_part = next((p for p in parts if p['type'] == 'year'), None) +``` + +**`filter`** — Keep matching elements: + +```ts +const numbers = [1, 2, 3, 4, 5]; +const evens = numbers.filter((n) => n % 2 === 0); +console.log(evens); // [2, 4] +``` + +**`map`** — Transform each element: + +```ts +const doubled = numbers.map((n) => n * 2); +console.log(doubled); // [2, 4, 6, 8, 10] +``` + +**`flatMap`** — Filter and transform in one pass: + +```ts +const result = periodStarts.flatMap((start, index) => { + if (!isInRange(start)) { + return []; // Drop this element + } + return `${start.toISOString()}/${end.toISOString()}`; // Transform +}); +``` + +Returning `[]` removes the element; returning a value keeps it. + +**Python comparison:** +```python +filtered = [s for s in period_starts if in_range(s)] +result = [make_interval(s) for s in filtered] +``` + +### Slicing and Indexing + +```ts +const alignedStarts: Date[] = []; + +// Slice: from start to before last element +const periodStarts = alignedStarts.slice(0, -1); + +// Index access +const start = alignedStarts[index]; +const end = alignedStarts[index + 1]; +``` + +**Python comparison:** +```python +period_starts = aligned_starts[:-1] +start = aligned_starts[index] +end = aligned_starts[index + 1] +``` + +--- + +## Handling Null and Undefined + +TypeScript has two "nothing" values: `null` (explicitly set) and `undefined` (not yet assigned). Python only has `None`. + +### Optional Chaining `?.` + +Safely access properties that might not exist: + +```ts +const user: { profile?: { city?: string } } = {}; +const city = user.profile?.city; // undefined, no crash +``` + +Without `?.`, accessing `user.profile.city` when `profile` is `undefined` would throw an error. + +**Python comparison:** +```python +city = user.profile.city if user and user.profile else None +``` + +### Nullish Coalescing `??` + +Provide a fallback only when the value is `null` or `undefined`: + +```ts +const name = maybeName ?? 'anonymous'; +``` + +Key difference from `||`: + +```ts +'' || 'fallback'; // 'fallback' (empty string is falsy) +'' ?? 'fallback'; // '' (only null/undefined trigger fallback) +0 || 'fallback'; // 'fallback' +0 ?? 'fallback'; // 0 +``` + +**Python comparison:** +```python +name = x if x is not None else 'fallback' +``` + +### Combining Operators for Robust Code + +These operators work great together: + +```ts +const year = parts.find((part) => part.type === 'year')?.value ?? ''; +``` + +Breaking it down: +1. `find(...)` returns `undefined` if no match +2. `?.value` safely accesses `value` (or returns `undefined`) +3. `?? ''` provides a fallback string + +This pattern is **extremely common** in TypeScript code. + +--- + +## Strings and Template Literals + +### String Literals + +```ts +const a = 'single quotes'; +const b = "double quotes"; // Both are valid +const c = ''; // Empty string +``` + +### Template Literals + +Use backticks for string interpolation: + +```ts +const year = '2026'; +const month = '03'; +const day = '24'; + +const label = `${year}-${month}-${day}`; +console.log(label); // '2026-03-24' +``` + +Template literals also support multi-line strings: + +```ts +const html = ` +
+

Title

+
+`; +``` + +**Python comparison:** +```python +label = f"{year}-{month}-{day}" +html = """ +
+

Title

+
+""" +``` + +--- + +## Guard Clauses and Validation + +Guard clauses exit early when conditions are not met: + +```ts +if (rangeStart !== undefined && rangeEnd !== undefined && rangeStart > rangeEnd) { + return []; +} +``` + +- `!==` — strict "not equal" (use this, not `!=`) +- `===` — strict "equal" (use this, not `==`) + +```ts +1 === 1; // true +1 === '1'; // false (different types) +0 === false; // false (different types) +``` + +Always prefer strict equality (`===` / `!==`) to avoid subtle type coercion bugs. + +**Python comparison:** +```python +if range_start is not None and range_end is not None and range_start > range_end: + return [] +``` + +--- + +## Date Handling + +### Getting Timestamps + +```ts +const startTime = start.getTime(); +``` + +Returns milliseconds since Unix epoch (1970-01-01). + +Compare timestamps directly: + +```ts +if (startTime >= rangeStart && startTime <= rangeEnd) { + // ... +} +``` + +### Converting to ISO Strings + +```ts +const interval = `${start.toISOString()}/${end.toISOString()}`; +``` + +Output: +``` +2026-03-23T23:00:00.000Z/2026-03-30T22:00:00.000Z +``` + +The `Z` indicates UTC timezone. + +**Python comparison:** +```python +start_ts_ms = int(start_dt.timestamp() * 1000) +interval = f"{start.isoformat()}/{end.isoformat()}" +``` + +--- + +## Code Style + +### Semicolons + +TypeScript/JavaScript allows optional semicolons. Most projects choose one style and stick with it: + +```ts +// With semicolons (common in TypeScript) +const x = 1; +const y = 2; + +// Without semicolons (also valid) +const x = 1 +const y = 2 +``` + +**Follow your project's convention.** Most TypeScript projects use semicolons. + +### Equality + +| Operator | Use case | +|----------|----------| +| `===` | Always use for comparisons (strict equality) | +| `!==` | Always use for comparisons (strict inequality) | +| `==` | Avoid (allows type coercion) | +| `!=` | Avoid (allows type coercion) | + +### Robust Patterns + +The idiomatic way to safely extract values: + +```ts +function safePart(parts: Part[], wanted: string): string { + return parts.find((p) => p.type === wanted)?.value ?? ''; +} +``` + +This function: +- Returns `undefined` if no matching part exists +- Uses `?.` to safely access `.value` +- Uses `?? ''` to ensure a string is always returned + +--- + +## Quick Reference: Python to TypeScript + +### Functions + +```python +def fn(x: int) -> str: + return str(x) +``` + +```ts +function fn(x: number): string { + return String(x); +} +``` + +### Lambda / Arrow Functions + +```python +lambda x: x + 1 +``` + +```ts +(x) => x + 1 +``` + +### Fallback for Missing Values + +```python +value = x if x is not None else 'fallback' +``` + +```ts +const value = x ?? 'fallback'; +``` + +### String Interpolation + +```python +f"{year}-{month}-{day}" +``` + +```ts +`${year}-${month}-${day}` +``` + +### Find First Match + +```python +next((p for p in parts if p['type'] == 'year'), None) +``` + +```ts +parts.find((p) => p.type === 'year') +``` + +### Array Filtering + +```python +filtered = [x for x in items if x.active] +``` + +```ts +const filtered = items.filter((x) => x.active); +``` + +### Array Mapping + +```python +mapped = [x.name for x in items] +``` + +```ts +const mapped = items.map((x) => x.name); +``` + +### None Checks + +```python +if user and user.profile: + city = user.profile.city +else: + city = None +``` + +```ts +const city = user.profile?.city; +``` + +### Return Type Annotations + +```python +from typing import List + +def get_names() -> List[str]: + return ['Alice', 'Bob'] +``` + +```ts +function getNames(): string[] { + return ['Alice', 'Bob']; +} +``` + +--- + +## Next Steps + +Now that you understand the fundamentals, explore: + +- **Interfaces and Types** — Define custom shapes for your data +- **Generics** — Write reusable functions that work with any type +- **Enums** — Define fixed sets of values +- **Modules** — Organize code across files +- **TypeScript with React/Vue/Node** — Apply these concepts in real frameworks + +--- + +*Based on TypeScript learning notes from the d-fine vault.* diff --git a/typescript/bits/00-typescript-learning-index.md b/typescript/bits/00-typescript-learning-index.md new file mode 100755 index 0000000..a8c1a9d --- /dev/null +++ b/typescript/bits/00-typescript-learning-index.md @@ -0,0 +1,29 @@ +# TypeScript Learning Index (Python -> TypeScript) + +## Foundations + +- `01-function-declaration-syntax.md` +- `02-const-and-assignment.md` +- `03-dot-access-and-method-calls.md` +- `04-array-find-and-arrow-functions.md` +- `05-strict-equality-operator.md` +- `06-optional-chaining-operator.md` +- `07-nullish-coalescing-operator.md` +- `08-string-literals.md` +- `09-template-literals.md` +- `10-semicolons.md` +- `11-robust-formatting-style.md` +- `12-python-to-typescript-mini-map.md` + +## Date/Interval Logic from your code + +- `13-optional-params-and-default-values.md` +- `14-return-type-array-strings.md` +- `15-array-slice-and-indexing.md` +- `16-timestamps-with-gettime.md` +- `17-guard-clauses-and-range-validation.md` +- `18-flatmap-filter-and-map-pattern.md` +- `19-iso-strings-and-template-literals.md` +- `20-alignedstarts-concept.md` + +Recommended order: start at `01`, then jump to `13-20` while reading `jsonDownload.ts`. diff --git a/typescript/bits/01-function-declaration-syntax.md b/typescript/bits/01-function-declaration-syntax.md new file mode 100755 index 0000000..a4d5a88 --- /dev/null +++ b/typescript/bits/01-function-declaration-syntax.md @@ -0,0 +1,28 @@ +# Function Declaration Syntax in TypeScript + +A function declaration defines a reusable block of logic with typed inputs and output. + +## Basic Shape + +```ts +function greet(name: string): string { + return `Hello, ${name}`; +} +``` + +## Parts Explained + +- `function`: keyword to declare a function. +- `greet`: function name. +- `(name: string)`: parameter list with a type annotation. +- `: string`: return type annotation. +- `{ ... }`: function body. + +## Python Comparison + +```py +def greet(name: str) -> str: + return f"Hello, {name}" +``` + +TypeScript puts type annotations after variable names as `name: string`, similar to Python type hints. diff --git a/typescript/bits/02-const-and-assignment.md b/typescript/bits/02-const-and-assignment.md new file mode 100755 index 0000000..462fe03 --- /dev/null +++ b/typescript/bits/02-const-and-assignment.md @@ -0,0 +1,31 @@ +# `const` and Assignment + +TypeScript uses `const`, `let`, and `var` for variable declarations. Modern code usually prefers `const` and `let`. + +## `const` + +```ts +const apiBase = 'https://example.com'; +``` + +- `const` means the binding cannot be reassigned. +- You can still mutate object contents unless frozen. + +```ts +const config = { retries: 2 }; +config.retries = 3; // allowed +// config = {}; // not allowed +``` + +## Assignment Operator `=` + +```ts +let count = 0; +count = count + 1; +``` + +- `=` assigns a new value to a variable. + +## Python Comparison + +Python has no enforced `const`; TypeScript enforces no-reassign when `const` is used. diff --git a/typescript/bits/03-dot-access-and-method-calls.md b/typescript/bits/03-dot-access-and-method-calls.md new file mode 100755 index 0000000..aa0761c --- /dev/null +++ b/typescript/bits/03-dot-access-and-method-calls.md @@ -0,0 +1,31 @@ +# Dot Access and Method Calls + +Dot notation accesses properties and methods on objects. + +## Property Access + +```ts +const user = { name: 'Mathias', age: 30 }; +console.log(user.name); // 'Mathias' +``` + +## Method Call + +```ts +const text = 'hello'; +console.log(text.toUpperCase()); // 'HELLO' +``` + +In your code: + +```ts +backendPeriodLabelFormatter.formatToParts(date) +``` + +- `backendPeriodLabelFormatter` is an object. +- `formatToParts` is a method. +- `(date)` passes the argument. + +## Python Comparison + +Equivalent idea to `obj.attr` and `obj.method(arg)`. diff --git a/typescript/bits/04-array-find-and-arrow-functions.md b/typescript/bits/04-array-find-and-arrow-functions.md new file mode 100755 index 0000000..5fdb99e --- /dev/null +++ b/typescript/bits/04-array-find-and-arrow-functions.md @@ -0,0 +1,36 @@ +# Array `find` and Arrow Functions + +`Array.prototype.find` returns the first element that matches a condition. + +## Example + +```ts +const parts = [ + { type: 'year', value: '2026' }, + { type: 'month', value: '03' }, + { type: 'day', value: '24' }, +]; + +const yearPart = parts.find((part) => part.type === 'year'); +console.log(yearPart); // { type: 'year', value: '2026' } +``` + +## Arrow Function Syntax + +```ts +(part) => part.type === 'year' +``` + +- `(part)`: parameter. +- `=>`: arrow token. +- `part.type === 'year'`: expression result (`true` or `false`). + +If no item matches, `find` returns `undefined`. + +## Python Comparison + +Similar to: + +```py +year_part = next((p for p in parts if p['type'] == 'year'), None) +``` diff --git a/typescript/bits/05-strict-equality-operator.md b/typescript/bits/05-strict-equality-operator.md new file mode 100755 index 0000000..c493121 --- /dev/null +++ b/typescript/bits/05-strict-equality-operator.md @@ -0,0 +1,23 @@ +# Strict Equality Operator `===` + +TypeScript/JavaScript have both `==` and `===`. + +Use `===` for predictable behavior. + +## Examples + +```ts +1 === 1; // true +1 === '1'; // false +0 === false; // false +``` + +`===` compares both value and type, and avoids implicit coercion. + +## Why it matters + +Using `===` prevents subtle bugs caused by automatic conversions. + +## Python Comparison + +Closest to Python `==`, which does not coerce strings/numbers the JavaScript way. diff --git a/typescript/bits/06-optional-chaining-operator.md b/typescript/bits/06-optional-chaining-operator.md new file mode 100755 index 0000000..37478b2 --- /dev/null +++ b/typescript/bits/06-optional-chaining-operator.md @@ -0,0 +1,29 @@ +# Optional Chaining Operator `?.` + +Optional chaining safely accesses properties/methods when a value might be `null` or `undefined`. + +## Property Access + +```ts +const user: { profile?: { city?: string } } = {}; +const city = user.profile?.city; +console.log(city); // undefined +``` + +## Method Call + +```ts +const maybeFn: undefined | (() => string) = undefined; +const value = maybeFn?.(); +console.log(value); // undefined +``` + +Without `?.`, these would throw runtime errors. + +## Python Comparison + +Similar intent to: + +```py +city = user.profile.city if user and user.profile else None +``` diff --git a/typescript/bits/07-nullish-coalescing-operator.md b/typescript/bits/07-nullish-coalescing-operator.md new file mode 100755 index 0000000..9836ce0 --- /dev/null +++ b/typescript/bits/07-nullish-coalescing-operator.md @@ -0,0 +1,23 @@ +# Nullish Coalescing Operator `??` + +`??` provides a fallback only when the left side is `null` or `undefined`. + +## Example + +```ts +const maybeName: string | undefined = undefined; +const name = maybeName ?? 'anonymous'; +console.log(name); // 'anonymous' +``` + +## Difference from `||` + +```ts +'' || 'fallback'; // 'fallback' +'' ?? 'fallback'; // '' +``` + +- `||` treats many falsy values as missing (`''`, `0`, `false`). +- `??` treats only `null` and `undefined` as missing. + +In your formatter code, `?? ''` is used as a safe fallback. diff --git a/typescript/bits/08-string-literals.md b/typescript/bits/08-string-literals.md new file mode 100755 index 0000000..6e76c7c --- /dev/null +++ b/typescript/bits/08-string-literals.md @@ -0,0 +1,23 @@ +# String Literals + +String literals are text values written directly in code. + +## Examples + +```ts +const a = 'year'; +const b = "month"; +const c = ''; +``` + +`''` is an empty string. + +## Typical Uses + +- Labels and constants. +- Comparisons. +- Fallback values. + +## Good Practice + +Keep quote style consistent with project conventions. diff --git a/typescript/bits/09-template-literals.md b/typescript/bits/09-template-literals.md new file mode 100755 index 0000000..31fcf6b --- /dev/null +++ b/typescript/bits/09-template-literals.md @@ -0,0 +1,28 @@ +# Template Literals + +Template literals are strings enclosed by backticks and support interpolation. + +## Syntax + +```ts +const year = '2026'; +const month = '03'; +const day = '24'; + +const label = `${year}-${month}-${day}`; +console.log(label); // '2026-03-24' +``` + +## Why use them + +- Easier than concatenation. +- More readable for multi-part strings. +- Supports multiline text. + +## Python Comparison + +Equivalent concept to Python f-strings: + +```py +label = f"{year}-{month}-{day}" +``` diff --git a/typescript/bits/10-semicolons.md b/typescript/bits/10-semicolons.md new file mode 100755 index 0000000..2d5cd35 --- /dev/null +++ b/typescript/bits/10-semicolons.md @@ -0,0 +1,17 @@ +# Semicolons in TypeScript + +Semicolons terminate statements. + +## Example + +```ts +const x = 1; +const y = 2; +const z = x + y; +``` + +JavaScript has automatic semicolon insertion, but many teams still use explicit semicolons for consistency and fewer edge-case surprises. + +## Recommendation + +Follow the style already used in your repository. diff --git a/typescript/bits/11-robust-formatting-style.md b/typescript/bits/11-robust-formatting-style.md new file mode 100755 index 0000000..4ab2b0d --- /dev/null +++ b/typescript/bits/11-robust-formatting-style.md @@ -0,0 +1,23 @@ +# Why This Formatting Pattern Is Robust + +The pattern in your code combines `find`, `?.`, and `??`: + +```ts +const year = parts.find((part) => part.type === 'year')?.value ?? ''; +``` + +## Why this is robust + +- `find(...)` may return `undefined`. +- `?.value` prevents a crash when no part exists. +- `?? ''` guarantees a string fallback. + +This keeps `formatPeriodStartLabel(...)` stable even when input parts are incomplete. + +## End-to-End Example + +```ts +function safePart(parts: Intl.DateTimeFormatPart[], wanted: string): string { + return parts.find((p) => p.type === wanted)?.value ?? ''; +} +``` diff --git a/typescript/bits/12-python-to-typescript-mini-map.md b/typescript/bits/12-python-to-typescript-mini-map.md new file mode 100755 index 0000000..62fe237 --- /dev/null +++ b/typescript/bits/12-python-to-typescript-mini-map.md @@ -0,0 +1,56 @@ +# Python to TypeScript Mini Map + +Quick syntax map for common constructs. + +## Function typing + +```py +def fn(x: int) -> str: + return str(x) +``` + +```ts +function fn(x: number): string { + return String(x); +} +``` + +## Lambda / Arrow + +```py +lambda x: x + 1 +``` + +```ts +(x) => x + 1 +``` + +## Fallback for missing values + +```py +value = x if x is not None else 'fallback' +``` + +```ts +const value = x ?? 'fallback'; +``` + +## String interpolation + +```py +f"{year}-{month}-{day}" +``` + +```ts +`${year}-${month}-${day}` +``` + +## Searching first match + +```py +next((p for p in parts if p['type'] == 'year'), None) +``` + +```ts +parts.find((p) => p.type === 'year') +``` diff --git a/typescript/bits/13-optional-params-and-default-values.md b/typescript/bits/13-optional-params-and-default-values.md new file mode 100755 index 0000000..1708e0a --- /dev/null +++ b/typescript/bits/13-optional-params-and-default-values.md @@ -0,0 +1,32 @@ +# Optional Parameters and Default Values in TypeScript + +In TypeScript, a function parameter can be optional and can also have a default value. + +## Example from your code + +```ts +function buildWeeklyDownloadPeriods( + dateRange?: DownloadDateRange, + now = new Date(), +): string[] { + // ... +} +``` + +## What this means + +- `dateRange?`: + - The `?` means this argument is optional. + - The caller can omit it. +- `now = new Date()`: + - If caller does not pass `now`, TypeScript uses `new Date()`. + +## Python comparison + +```py +def build_weekly_download_periods(date_range=None, now=None): + if now is None: + now = datetime.now() +``` + +TypeScript default parameters are cleaner because the default is declared directly in the signature. diff --git a/typescript/bits/14-return-type-array-strings.md b/typescript/bits/14-return-type-array-strings.md new file mode 100755 index 0000000..d797618 --- /dev/null +++ b/typescript/bits/14-return-type-array-strings.md @@ -0,0 +1,27 @@ +# Return Type `string[]` + +TypeScript can declare exactly what a function returns. + +## Example + +```ts +function buildWeeklyDownloadPeriods(...): string[] { + return ['2026-03-23T23:00:00.000Z/2026-03-30T22:00:00.000Z']; +} +``` + +## Meaning + +- `string[]` means "array of strings". +- Each array item must be a `string`. + +## Python comparison + +```py +from typing import List + +def build_weekly_download_periods(...) -> List[str]: + return ['a/b'] +``` + +TypeScript enforces this statically, so returning non-strings will be flagged by the type checker. diff --git a/typescript/bits/15-array-slice-and-indexing.md b/typescript/bits/15-array-slice-and-indexing.md new file mode 100755 index 0000000..fbf801b --- /dev/null +++ b/typescript/bits/15-array-slice-and-indexing.md @@ -0,0 +1,30 @@ +# Array `slice` and Indexing + +Your snippet uses `slice` and index-based access to create weekly intervals. + +## `slice(0, -1)` + +```ts +const periodStarts = alignedStarts.slice(0, -1); +``` + +- Start at index `0`. +- Stop before the last item (`-1` means from the end). +- Useful when each `start` needs a following `end` item. + +## Index access + +```ts +const end = alignedStarts[index + 1]; +``` + +- Gets the next boundary after the current `start`. + +## Python comparison + +```py +period_starts = aligned_starts[:-1] +end = aligned_starts[index + 1] +``` + +This is the same concept as Python slicing and list indexing. diff --git a/typescript/bits/16-timestamps-with-gettime.md b/typescript/bits/16-timestamps-with-gettime.md new file mode 100755 index 0000000..88c195f --- /dev/null +++ b/typescript/bits/16-timestamps-with-gettime.md @@ -0,0 +1,28 @@ +# Timestamps with `getTime()` + +`Date.getTime()` returns a timestamp in milliseconds since Unix epoch. + +## Example + +```ts +const startTime = start.getTime(); +``` + +## Why this is useful + +Numeric timestamps are easy to compare: + +```ts +startTime >= rangeStart +startTime <= rangeEnd +``` + +Comparing numbers is usually simpler and safer than comparing date strings directly. + +## Python comparison + +```py +start_ts_ms = int(start_dt.timestamp() * 1000) +``` + +Both represent an absolute moment in time. diff --git a/typescript/bits/17-guard-clauses-and-range-validation.md b/typescript/bits/17-guard-clauses-and-range-validation.md new file mode 100755 index 0000000..4692ef4 --- /dev/null +++ b/typescript/bits/17-guard-clauses-and-range-validation.md @@ -0,0 +1,30 @@ +# Guard Clauses and Range Validation + +A guard clause exits early when input is invalid. + +## Example from your snippet + +```ts +if (rangeStart !== undefined && rangeEnd !== undefined && rangeStart > rangeEnd) { + return []; +} +``` + +## Why this is good + +- Fails fast. +- Prevents harder-to-debug logic later. +- Keeps the main flow cleaner. + +## Operator notes + +- `!==`: strict "not equal" comparison. +- `&&`: logical AND (all conditions must be true). +- `>`: greater-than comparison. + +## Python comparison + +```py +if range_start is not None and range_end is not None and range_start > range_end: + return [] +``` diff --git a/typescript/bits/18-flatmap-filter-and-map-pattern.md b/typescript/bits/18-flatmap-filter-and-map-pattern.md new file mode 100755 index 0000000..368bab6 --- /dev/null +++ b/typescript/bits/18-flatmap-filter-and-map-pattern.md @@ -0,0 +1,38 @@ +# `flatMap` as Filter + Map Pattern + +`flatMap` can both remove items and transform remaining items. + +## Pattern in your snippet + +```ts +return periodStarts.flatMap((start, index) => { + if (!matchesRange) { + return []; + } + + return `${start.toISOString()}/${end.toISOString()}`; +}); +``` + +## How it works + +- Return `[]` to drop an element. +- Return a value to keep/transform it. +- `flatMap` flattens one level automatically. + +## Equivalent with `filter` + `map` + +```ts +return periodStarts + .filter((start) => isInRange(start)) + .map((start, index) => makeInterval(start, index)); +``` + +## Python comparison + +Usually done as separate steps: + +```py +filtered = [s for s in period_starts if in_range(s)] +result = [make_interval(s, i) for i, s in enumerate(filtered)] +``` diff --git a/typescript/bits/19-iso-strings-and-template-literals.md b/typescript/bits/19-iso-strings-and-template-literals.md new file mode 100755 index 0000000..e624225 --- /dev/null +++ b/typescript/bits/19-iso-strings-and-template-literals.md @@ -0,0 +1,23 @@ +# ISO Strings and Template Literals + +Your code builds interval strings using `toISOString()` and a template literal. + +## Example + +```ts +const interval = `${start.toISOString()}/${end.toISOString()}`; +``` + +## Why this format is good + +- ISO format is unambiguous. +- Easy for backend APIs to parse. +- Includes timezone info (`Z` for UTC). + +Example value: + +```text +2026-03-23T23:00:00.000Z/2026-03-30T22:00:00.000Z +``` + +This describes one weekly period as `start/end`. diff --git a/typescript/bits/20-alignedstarts-concept.md b/typescript/bits/20-alignedstarts-concept.md new file mode 100755 index 0000000..4e2663d --- /dev/null +++ b/typescript/bits/20-alignedstarts-concept.md @@ -0,0 +1,26 @@ +# Understanding `alignedStarts` + +`alignedStarts` is an array of date boundaries that match your backend schedule rule. + +## Concept + +```ts +const alignedStarts: Date[] = []; +``` + +Each item is a valid period boundary (for example Monday 00:00 in backend-local schedule terms). + +Later, you form intervals by pairing adjacent items: + +```ts +const start = alignedStarts[index]; +const end = alignedStarts[index + 1]; +``` + +So if `alignedStarts` has `N` items, you can make up to `N-1` intervals. + +## Why this is useful + +- Keeps schedule boundaries consistent. +- Makes interval generation deterministic. +- Handles DST-safe boundaries when generated correctly. diff --git a/vue/00-Vue Tutorial.md b/vue/00-Vue Tutorial.md new file mode 100755 index 0000000..3bedbe4 --- /dev/null +++ b/vue/00-Vue Tutorial.md @@ -0,0 +1,747 @@ +# Vue.js Introduction for Python Developers + +A practical guide to Vue.js, written for developers who know Python. + +--- + +## Table of Contents + +1. [What is Vue?](#what-is-vue) +2. [The Big Idea: Reactivity](#the-big-idea-reactivity) +3. [Your First Vue Component](#your-first-vue-component) +4. [Reactive State with `ref`](#reactive-state-with-ref) +5. [Derived State with `computed`](#derived-state-with-computed) +6. [Templates: HTML with Vue Features](#templates-html-with-vue-features) +7. [Two-Way Binding with `v-model`](#two-way-binding-with-v-model) +8. [Events and User Interaction](#events-and-user-interaction) +9. [Components and Props](#components-and-props) +10. [Lifecycle Hooks](#lifecycle-hooks) +11. [Composables: Reusable Logic](#composables-reusable-logic) +12. [A Real-World Example: Filter Flow](#a-real-world-example-filter-flow) +13. [Vue Strengths, Weaknesses, and Use Cases](#vue-strengths-weaknesses-and-use-cases) +14. [Quick Reference](#quick-reference) + +--- + +## What is Vue? + +Vue.js is a JavaScript framework for building user interfaces. It focuses on the **view layer** - what the user sees and interacts with. + +### Vue 2 vs Vue 3 + +This guide covers **Vue 3**, which introduced the Composition API (using ` + + +``` + +When `count.value` changes, Vue re-renders the button text. You never call `update_view()`. + +--- + +## Your First Vue Component + +A Vue **single-file component** (`.vue` file) has three sections: + +```vue + + + + + +``` + +### Mental Model + +When reading Vue code, ask four questions: + +1. **What values are reactive state?** → Look for `ref()` and `computed()` +2. **Which template elements use that state?** → Look for `{{ variable }}` and `v-model` +3. **Which functions change that state?** → Look for functions that modify `.value` +4. **Which computed values depend on that state?** → Look for `computed()` + +--- + +## Reactive State with `ref` + +### What is `ref`? + +`ref()` wraps a value so Vue can track changes to it. + +```ts +import { ref } from 'vue'; + +const count = ref(0); // reactive number +const username = ref('alice'); // reactive string +const isLoading = ref(false); // reactive boolean +const items = ref([]); // reactive array +``` + +### Reading and Writing + +In ` +``` + +The button label and disabled state both react to `isLoading`. + +--- + +## Derived State with `computed` + +Use `computed()` for values that are calculated from other reactive values. + +```ts +import { ref, computed } from 'vue'; + +const firstName = ref('Ada'); +const lastName = ref('Lovelace'); + +const fullName = computed(() => { + return `${firstName.value} ${lastName.value}`; +}); +``` + +### Python Comparison + +This is like a Python `@property`: + +```python +class Person: + @property + def full_name(self): + return f"{self.first_name} {self.last_name}" +``` + +### Rule of Thumb + +| Use `ref` when | Use `computed` when | +|---------------|---------------------| +| The value changes over time | The value is calculated from other values | +| The value is primary state | You don't want to duplicate state | +| Examples: user input, API data | Examples: full name from first + last | + +### Common Mistake: Storing What Can Be Derived + +```ts +// Bad: duplicate state, must update manually +const firstName = ref('Ada'); +const lastName = ref('Lovelace'); +const fullName = ref('Ada Lovelace'); // must keep in sync! + +// Good: computed handles it automatically +const fullName = computed(() => `${firstName.value} ${lastName.value}`); +``` + +### Real Example: Validation + +```ts +const selectedStart = ref(null); +const selectedEnd = ref(null); + +const hasValidRange = computed(() => { + if (!selectedStart.value || !selectedEnd.value) { + return true; // missing values are valid (no filter) + } + return new Date(selectedStart.value) <= new Date(selectedEnd.value); +}); +``` + +--- + +## Templates: HTML with Vue Features + +### Interpolation: `{{ }}` + +Print values into HTML: + +```vue +

{{ username }}

+

{{ 2 + 3 }}

+

{{ isLoading ? 'Loading...' : 'Done' }}

+

{{ items.length }} items

+``` + +### Conditional Rendering: `v-if` / `v-else` + +Show elements based on conditions: + +```vue +

{{ error }}

+

No errors

+ +
+

Welcome, {{ username }}!

+
+
+

Please log in.

+
+``` + +### Loops: `v-for` + +Repeat elements for each item: + +```vue +
    +
  • + {{ user.name }} +
  • +
+``` + +**Important:** Always use `:key` with `v-for` for proper list rendering. + +### Attribute Binding: `:` + +Bind HTML attributes to JavaScript expressions: + +```vue + + +
View Profile +``` + +Short for `v-bind:disabled`. + +### Class Binding + +```vue +
+ Content +
+``` + +--- + +## Two-Way Binding with `v-model` + +`v-model` connects a form field and a reactive variable bidirectionally. + +### Basic Input + +```vue + + + +``` + +- User types → `email` updates +- Code changes `email` → input display updates + +### Select Dropdown + +```vue + +``` + +```ts +const countries = [ + { title: 'Germany', value: 'DE' }, + { title: 'France', value: 'FR' }, + { title: 'Spain', value: 'ES' }, +]; +const selectedCountry = ref(null); +``` + +- `item-title`: what's shown to users +- `item-value`: what's stored in the variable + +--- + +## Events and User Interaction + +### Click Events: `@click` + +```vue + +``` + +```ts +function save() { + console.log('saving...'); +} +``` + +### Other Common Events + +```vue + +
+ +``` + +If the user types into the input, `username` changes. +If `username` changes in code, the input display changes. + +## Example: Filter Form + +```vue + + + +``` + +This is very similar to your date filter selects in FlexibilityTable. + +## Good Beginner Rule + +When reading Vue code, ask these four questions: + +1. What values are reactive state? +2. Which template elements use that state? +3. Which functions change that state? +4. Which computed values or API calls depend on that state? + +If you answer those four, most Vue components become understandable. + +## Related Notes + +- [[01-Refs, Computed, and Reactivity]] +- [[02-Templates, v-model, and Events]] +- [[03-Components, Props, Lifecycle, and Composables]] +- [[04-FlexibilityTable Filter Flow]] diff --git a/vue/01-Refs, Computed, and Reactivity.md b/vue/01-Refs, Computed, and Reactivity.md new file mode 100755 index 0000000..2afec57 --- /dev/null +++ b/vue/01-Refs, Computed, and Reactivity.md @@ -0,0 +1,252 @@ +# Refs, Computed, and Reactivity + +This note explains the most important Vue state concepts for a Python developer. + +## `ref`: reactive storage for one value + +A `ref` wraps a value so Vue can track it. + +```ts +import { ref } from 'vue'; + +const count = ref(0); +const username = ref('mathias'); +const isLoading = ref(false); +``` + +In script code, you read and write using `.value`. + +```ts +count.value += 1; +username.value = 'new name'; +isLoading.value = true; +``` + +In templates, Vue unwraps refs automatically. + +```vue + +``` + +You write `count`, not `count.value`, in the template. + +## Python comparison + +Python: + +```python +count = 0 +count += 1 +``` + +Vue script: + +```ts +const count = ref(0); +count.value += 1; +``` + +The extra `.value` exists because `count` is a reactive wrapper object, not the raw number. + +## Example: loading state + +```vue + + + +``` + +The button label and disabled state both react to `isLoading`. + +## `computed`: derived state + +A `computed` value is calculated from other reactive values. + +```ts +import { ref, computed } from 'vue'; + +const firstName = ref('Ada'); +const lastName = ref('Lovelace'); + +const fullName = computed(() => { + return `${firstName.value} ${lastName.value}`; +}); +``` + +Use `computed` when a value can be derived instead of stored manually. + +Bad pattern: + +```ts +const firstName = ref('Ada'); +const lastName = ref('Lovelace'); +const fullName = ref('Ada Lovelace'); +``` + +Now you must remember to update `fullName` yourself every time. + +Better: + +```ts +const fullName = computed(() => `${firstName.value} ${lastName.value}`); +``` + +## Python comparison + +This is similar to a property. + +```python +class Person: + def __init__(self, first_name, last_name): + self.first_name = first_name + self.last_name = last_name + + @property + def full_name(self): + return f"{self.first_name} {self.last_name}" +``` + +Vue `computed` plays a similar role. + +## Real example from your app + +In FlexibilityTable, this pattern appears: + +```ts +const hasValidPeriodRange = computed(() => { + if (!selectedPeriodStartFrom.value || !selectedPeriodStartUntil.value) { + return true; + } + + return new Date(selectedPeriodStartFrom.value).getTime() <= new Date(selectedPeriodStartUntil.value).getTime(); +}); +``` + +This means: + +- if one boundary is missing, the range is treated as valid +- if both are set, start must be before or equal to end + +The component does not store `hasValidPeriodRange` manually. It derives it from the two selected dates. + +## Another example: filtered list + +```ts +const searchText = ref(''); +const users = ref(['Alice', 'Bob', 'Charlie']); + +const filteredUsers = computed(() => { + return users.value.filter((user) => + user.toLowerCase().includes(searchText.value.toLowerCase()), + ); +}); +``` + +If `searchText` changes, `filteredUsers` updates automatically. + +## Rule of Thumb + +Use `ref` when: + +- the value changes over time +- the UI should react to that change +- the value is primary state + +Use `computed` when: + +- the value is calculated from other reactive values +- you do not want to duplicate state + +## Common Beginner Mistakes + +### forgetting `.value` in script + +Wrong: + +```ts +count += 1; +``` + +Right: + +```ts +count.value += 1; +``` + +### using `computed` for side effects + +Bad: + +```ts +const result = computed(() => { + console.log('side effect'); + return count.value * 2; +}); +``` + +A computed should mainly calculate and return a value. + +### storing what can be derived + +Bad: + +```ts +const selectedFirst = ref('2026-01-01'); +const selectedSecond = ref('2026-01-10'); +const isValid = ref(true); +``` + +Better: + +```ts +const isValid = computed(() => selectedFirst.value <= selectedSecond.value); +``` + +## Tiny Exercise + +What should be `ref` and what should be `computed`? + +Scenario: + +- user types first name +- user types last name +- screen shows full name +- submit button disabled when first name is empty + +Answer: + +```ts +const firstName = ref(''); +const lastName = ref(''); +const fullName = computed(() => `${firstName.value} ${lastName.value}`.trim()); +const isSubmitDisabled = computed(() => !firstName.value.trim()); +``` + +## Related Notes + +- [[00-Vue for Python Developers]] +- [[02-Templates, v-model, and Events]] +- [[04-FlexibilityTable Filter Flow]] diff --git a/vue/02-Templates, v-model, and Events.md b/vue/02-Templates, v-model, and Events.md new file mode 100755 index 0000000..939e794 --- /dev/null +++ b/vue/02-Templates, v-model, and Events.md @@ -0,0 +1,252 @@ +# Templates, v-model, and Events + +This note explains how Vue templates talk to your script code. + +## Templates are HTML with Vue features + +Example: + +```vue + + + +``` + +The same variable is: + +- shown inside `{{ username }}` +- edited by `` +- changed by the `reset()` function + +## `{{ ... }}`: show a value + +```vue +

{{ username }}

+

{{ isLoading ? 'Loading...' : 'Done' }}

+

{{ 2 + 3 }}

+``` + +This is interpolation. It prints a value into the rendered HTML. + +## `v-model`: two-way binding + +`v-model` connects a form field and a reactive variable. + +```vue + +``` + +```ts +const email = ref(''); +``` + +Two-way binding means: + +- if the user types, `email` changes +- if code changes `email`, the input display changes + +## Example with a select + +```vue + +``` + +```ts +const countries = ['Germany', 'France', 'Spain']; +const selectedCountry = ref(null); +``` + +When the user picks France, `selectedCountry.value` becomes `'France'`. + +## Your real example + +From FlexibilityTable: + +```vue + +``` + +This means: + +- the dropdown shows options from `periodStartOptions` +- each option is an object +- `title` is shown to the user +- `value` is stored in `selectedPeriodStartFrom` + +If one item is: + +```ts +{ title: '2026-03-23', value: '2026-03-23T23:00:00.000Z' } +``` + +then the user sees: + +```text +2026-03-23 +``` + +but the stored value becomes: + +```ts +selectedPeriodStartFrom.value = '2026-03-23T23:00:00.000Z'; +``` + +## Events with `@click` + +`@click` means: call a function when the element is clicked. + +```vue + +``` + +```ts +function increment() { + count.value += 1; +} +``` + +Other common events: + +```vue + + +