init at work
This commit is contained in:
Executable
+214
@@ -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
|
||||
|
||||
<Esc> Zurück in Normalmodus
|
||||
|
||||
v Visueller Modus (Zeichenweise)
|
||||
V Visueller Modus (Zeilenweise)
|
||||
<C-v> 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<char> 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<a> 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
|
||||
```
|
||||
Executable
+497
@@ -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
|
||||
Executable
+475
@@ -0,0 +1,475 @@
|
||||
## 1. Grundidee: Was ist ein ORM?
|
||||
|
||||
ORM steht für **Object-Relational Mapping**.
|
||||
Im Kontext von Python und Datenbankmodellen bedeutet das:
|
||||
|
||||
> Ein ORM ist eine Bibliothek, die es dir erlaubt, mit einer relationalen Datenbank (z. B. PostgreSQL, MySQL, SQLite) zu arbeiten, indem du **Python-Klassen und -Objekte** verwendest statt SQL-Strings.
|
||||
|
||||
Anstatt also SQL wie:
|
||||
|
||||
```sql
|
||||
SELECT * FROM konto WHERE id = 1;
|
||||
```
|
||||
|
||||
zu schreiben, machst du in Python z. B.:
|
||||
|
||||
```python
|
||||
konto = session.get(Konto, 1)
|
||||
```
|
||||
|
||||
Das ORM übersetzt deine Python-Anweisung im Hintergrund in SQL, führt sie aus und gibt dir Python-Objekte zurück.
|
||||
|
||||
---
|
||||
|
||||
## 2. Wichtige Begriffe und Bausteine (intuitiv erklärt)
|
||||
|
||||
Nehmen wir ein vereinfachtes Bank-Beispiel:
|
||||
|
||||
- **Kunde** (Customer)
|
||||
- **Konto** (Account)
|
||||
- **Transaktion** (Transaction)
|
||||
|
||||
In einer relationalen Datenbank wären das Tabellen:
|
||||
|
||||
- `kunde`
|
||||
- `konto`
|
||||
- `transaktion`
|
||||
|
||||
Mit einem ORM legst du dafür **Modelle** als Python-Klassen an:
|
||||
|
||||
```python
|
||||
class Kunde:
|
||||
...
|
||||
class Konto:
|
||||
...
|
||||
class Transaktion:
|
||||
...
|
||||
```
|
||||
|
||||
Die wichtigsten Bausteine:
|
||||
|
||||
- **Modell / Entity**:
|
||||
Eine Klasse, die eine Tabelle repräsentiert (z. B. `Konto` → Tabelle `konto`).
|
||||
- **Felder / Spalten**:
|
||||
Attribute der Klasse (z. B. `saldo`, `kunde_id`).
|
||||
- **Beziehungen**:
|
||||
- „Ein Kunde hat viele Konten“ (One-to-Many)
|
||||
- „Ein Konto gehört zu genau einem Kunden“ (Many-to-One)
|
||||
- **Session / EntityManager**:
|
||||
Ein Objekt, über das du mit der Datenbank sprichst (z. B. Datensätze abfragen, speichern, löschen).
|
||||
|
||||
---
|
||||
|
||||
## 3. Abgrenzung zu verwandten Begriffen
|
||||
|
||||
### 3.1 ORM vs. direkte SQL-Nutzung
|
||||
|
||||
**Direkte SQL-Nutzung:**
|
||||
|
||||
- Du schreibst selbst SQL-Statements.
|
||||
- Du verwendest z. B. `psycopg2` (PostgreSQL) oder `sqlite3` (Standard in Python).
|
||||
- Du bekommst Resultate als Tupel / Dictionaries zurück und baust selbst deine Objekte.
|
||||
|
||||
**Mit ORM:**
|
||||
|
||||
- Du arbeitest mit Python-Klassen und -Objekten.
|
||||
- Das ORM generiert und führt SQL für dich aus.
|
||||
- Du bekommst direkt Instanzen deiner Klassen.
|
||||
|
||||
```python
|
||||
# Direkte SQL-Nutzung
|
||||
cursor.execute("SELECT id, name FROM kunde WHERE id = %s", (1,))
|
||||
row = cursor.fetchone()
|
||||
kunde = {"id": row[0], "name": row[1]}
|
||||
|
||||
# ORM
|
||||
kunde = session.get(Kunde, 1)
|
||||
print(kunde.name)
|
||||
```
|
||||
|
||||
ORM nimmt dir also den „manuellen“ Teil des Mappings ab.
|
||||
|
||||
---
|
||||
|
||||
### 3.2 ORM vs. Datenbankmodellierung (ER-Modell)
|
||||
|
||||
- **Datenbankmodellierung** (ER-Diagramme, Normalisierung usw.) ist der Schritt, in dem du **konzipierst**, wie deine Daten strukturiert sind.
|
||||
- **ORM** ist ein **Werkzeug**, um mit dieser Struktur in Python zu arbeiten.
|
||||
|
||||
Du kannst ein gutes Datenbankmodell haben – **mit oder ohne** ORM.
|
||||
ORM ersetzt nicht das Nachdenken über ein sinnvolles Datenmodell.
|
||||
|
||||
---
|
||||
|
||||
### 3.3 ORM vs. Migrations-Tools
|
||||
|
||||
- **Migrations-Tools** (z. B. Alembic für SQLAlchemy, Django-Migrations) verwalten die **Versionierung und Änderungen** am Schema (z. B. neue Spalte, geänderte Spalte).
|
||||
- Ein **ORM** arbeitet primär zur **Laufzeit** mit Daten; viele ORMs bringen allerdings Tools mit, um aus den Modellen Migrationen zu erzeugen.
|
||||
|
||||
---
|
||||
|
||||
### 3.4 ORM vs. Query-Builder
|
||||
|
||||
- **Query-Builder**: Bibliothek, die das Schreiben von SQL erleichtert, aber nicht unbedingt Objekte modelliert (z. B. SQLAlchemy Core).
|
||||
- **ORM**: baut auf einem Query-Builder auf und bringt zusätzlich:
|
||||
- Klassen→Tabellen-Mapping
|
||||
- Objekte→Zeilen-Mapping
|
||||
- Beziehungen als Attribute
|
||||
|
||||
---
|
||||
|
||||
### 3.5 ORM vs. [[Pydantic]] / Dataclasses
|
||||
|
||||
- **[[Pydantic]] / `dataclasses`**: Modellieren **In-Memory-Daten** (z. B. Input aus einer API), inkl. Validation und Typen.
|
||||
- **ORM**: Modelliert **persistente Daten** in einer relationalen Datenbank.
|
||||
|
||||
Man kombiniert das oft:
|
||||
|
||||
- [[Pydantic]]-Modelle für API-Ein-/Ausgaben
|
||||
- ORM-Modelle für Speicherung in der Datenbank
|
||||
|
||||
---
|
||||
|
||||
## 4. Welche Probleme löst ein ORM?
|
||||
|
||||
### 4.1 „Impedance Mismatch“: Objekte vs. Tabellen
|
||||
|
||||
Python arbeitet mit **Objekten**:
|
||||
|
||||
```python
|
||||
kunde.name
|
||||
konto.saldo
|
||||
konto.kunde.name
|
||||
```
|
||||
|
||||
Datenbanken arbeiten mit **Tabellen**, **Zeilen** und **Fremdschlüsseln**.
|
||||
ORMs „übersetzen“ zwischen diesen Welten.
|
||||
|
||||
Beispiel:
|
||||
|
||||
- `konto.kunde` ist in Python einfach ein Attribut.
|
||||
- Intern bedeutet das: Joins über `konto.kunde_id = kunde.id`.
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Weniger Boilerplate, mehr Fokus auf Fachlogik
|
||||
|
||||
Ohne ORM schreibst du viel repetiven Code:
|
||||
|
||||
- SQL-Strings
|
||||
- Parameter-Bindung
|
||||
- Zeilen in Python-Objekte umwandeln
|
||||
|
||||
Mit ORM schreibst du einmal deine Modelle und konzentrierst dich dann auf:
|
||||
|
||||
- „Neue Transaktion buchen“
|
||||
- „Saldo prüfen“
|
||||
- „Kontoauszug generieren“
|
||||
|
||||
Anstatt: „Wie formuliere ich nochmal den SQL-Join...“.
|
||||
|
||||
---
|
||||
|
||||
### 4.3 Typisierung, Autocomplete, Konsistenz
|
||||
|
||||
Durch Python-Klassen hast du:
|
||||
|
||||
- **Typhinweise** (z. B. `saldo: float`)
|
||||
- Unterstützung durch IDE (Autocomplete, Refactoring)
|
||||
- Klar definierte Beziehungen (z. B. `konto.kunde`)
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Datenbank-Agnostik
|
||||
|
||||
Viele ORMs unterstützen mehrere Datenbanken:
|
||||
|
||||
- Du kannst z. B. in Tests SQLite verwenden,
|
||||
- in Produktion PostgreSQL,
|
||||
- ohne deinen ganzen Code umzuschreiben (meist nur Konfiguration).
|
||||
|
||||
---
|
||||
|
||||
### 4.5 Testbarkeit
|
||||
|
||||
Du kannst:
|
||||
|
||||
- einfacher Unit-Tests schreiben, indem du In-Memory-SQLite nutzt,
|
||||
- oder sogar nur mit „Fake-Repositories“ arbeitest, die sich wie das ORM verhalten.
|
||||
|
||||
---
|
||||
|
||||
## 5. Welche Herausforderungen bringen ORMs mit sich?
|
||||
|
||||
### 5.1 Performance-Fallen („N+1-Problem“, zu viele Queries)
|
||||
|
||||
Beispiel: Du lädst 100 Konten und für jedes Konto den zugehörigen Kunden:
|
||||
|
||||
Naiv:
|
||||
|
||||
```python
|
||||
konten = session.query(Konto).all()
|
||||
for konto in konten:
|
||||
print(konto.kunde.name)
|
||||
```
|
||||
|
||||
Kann bedeuten:
|
||||
|
||||
- 1 Query für alle Konten
|
||||
- + 100 Queries für jeden einzelnen Kunden
|
||||
|
||||
→ Insgesamt 101 Queries (= N+1-Problem).
|
||||
|
||||
Mit ORM musst du lernen, wie man:
|
||||
|
||||
- **Eager Loading** / `join` / `selectinload` etc. nutzt,
|
||||
- um nur **1–2 Queries** zu erzeugen.
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Man darf SQL nicht völlig „vergessen“
|
||||
|
||||
ORM nimmt viel Arbeit ab, aber:
|
||||
|
||||
- du solltest **verstehen**, was für SQL generiert wird,
|
||||
- und Grundbegriffe wie `JOIN`, `WHERE`, `GROUP BY`, Indexe kennen.
|
||||
|
||||
Ohne grundlegende SQL-Kenntnisse tappst du schnell in Performance-Probleme.
|
||||
|
||||
---
|
||||
|
||||
### 5.3 Komplexe Abfragen
|
||||
|
||||
Für einfache Abfragen ist ORM sehr angenehm.
|
||||
Bei sehr komplexen Auswertungen (z. B. Bank-Reporting mit vielen Aggregationen, Window-Funktionen) kann:
|
||||
|
||||
- die ORM-Syntax unübersichtlich werden,
|
||||
- rohe SQL-Statements manchmal klarer und effizienter sein.
|
||||
|
||||
Viele ORMs erlauben gemischt:
|
||||
|
||||
- 90 % ORM,
|
||||
- 10 % direktes SQL für Spezialfälle.
|
||||
|
||||
---
|
||||
|
||||
### 5.4 Migrationen und Schema-Änderungen
|
||||
|
||||
- Du musst im Blick behalten: **Modelle in Python** und **Schema in Datenbank** dürfen nicht auseinanderlaufen.
|
||||
- Migrations-Tools sind notwendig, aber auch ein eigener Lernbereich.
|
||||
|
||||
---
|
||||
|
||||
### 5.5 „Lock-In“ und Komplexität
|
||||
|
||||
- Große ORMs (z. B. SQLAlchemy ORM, Django ORM) sind mächtig, aber haben Lernkurve.
|
||||
- Wenn du einmal dein ganzes Projekt auf ein bestimmtes ORM gebaut hast, ist ein Wechsel auf ein anderes ORM oder „plain SQL“ aufwendig.
|
||||
|
||||
---
|
||||
|
||||
## 6. Praxisnahe Beispiele mit Python (SQLAlchemy ORM)
|
||||
|
||||
### 6.1 Setup: einfache Bank-Domain mit SQLAlchemy
|
||||
|
||||
Installation:
|
||||
|
||||
```bash
|
||||
pip install sqlalchemy
|
||||
```
|
||||
|
||||
Ein minimaler Aufbau mit `Kunde` und `Konto`:
|
||||
|
||||
```python
|
||||
from sqlalchemy import (
|
||||
create_engine, Column, Integer, String, Float, ForeignKey
|
||||
)
|
||||
from sqlalchemy.orm import (
|
||||
declarative_base, relationship, Session
|
||||
)
|
||||
|
||||
# Basis-Klasse für alle ORM-Modelle
|
||||
Base = declarative_base()
|
||||
|
||||
class Kunde(Base):
|
||||
__tablename__ = "kunde"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String, nullable=False)
|
||||
|
||||
# Beziehung: Ein Kunde hat viele Konten
|
||||
konten = relationship("Konto", back_populates="kunde")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Kunde id={self.id} name={self.name!r}>"
|
||||
|
||||
class Konto(Base):
|
||||
__tablename__ = "konto"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
kontonummer = Column(String, unique=True, nullable=False)
|
||||
saldo = Column(Float, default=0.0)
|
||||
|
||||
kunde_id = Column(Integer, ForeignKey("kunde.id"), nullable=False)
|
||||
# Beziehung: Konto gehört zu genau einem Kunden
|
||||
kunde = relationship("Kunde", back_populates="konten")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Konto id={self.id} konto={self.kontonummer!r} saldo={self.saldo}>"
|
||||
|
||||
# Engine und Session einrichten (hier: SQLite-Datei)
|
||||
engine = create_engine("sqlite:///bank.db", echo=True) # echo=True zeigt SQL an
|
||||
Base.metadata.create_all(engine) # Tabellen aus den Modellen erzeugen
|
||||
|
||||
# Session erstellen
|
||||
session = Session(engine)
|
||||
|
||||
# Beispiel-Daten anlegen
|
||||
kunde = Kunde(name="Max Mustermann")
|
||||
konto1 = Konto(kontonummer="DE123", saldo=1000.0, kunde=kunde)
|
||||
konto2 = Konto(kontonummer="DE456", saldo=2500.0, kunde=kunde)
|
||||
|
||||
session.add(kunde) # reicht, da konten via Beziehung mit hinzugefügt werden
|
||||
session.commit()
|
||||
|
||||
# Abfragen
|
||||
alle_kunden = session.query(Kunde).all()
|
||||
print(alle_kunden)
|
||||
|
||||
# Zugriff auf Beziehungen
|
||||
for k in alle_kunden:
|
||||
print(f"Kunde: {k.name}")
|
||||
for konto in k.konten:
|
||||
print(f" Konto {konto.kontonummer}, Saldo: {konto.saldo}")
|
||||
|
||||
# Einzelnes Konto laden und zugehörigen Kunden ausgeben
|
||||
konto = session.query(Konto).filter_by(kontonummer="DE123").one()
|
||||
print(konto.kunde.name)
|
||||
|
||||
session.close()
|
||||
```
|
||||
|
||||
Wichtige Punkte im Beispiel:
|
||||
|
||||
- `Kunde` und `Konto` sind Python-Klassen, die Tabellen repräsentieren.
|
||||
- Die Beziehung `Kunde.konten` und `Konto.kunde` erlaubt dir objektorientierten Zugriff.
|
||||
- Das ORM generiert automatisch SQL (sichtbar durch `echo=True`).
|
||||
|
||||
---
|
||||
|
||||
### 6.2 Gleiche Logik mit „plain SQL“ (zum Vergleich)
|
||||
|
||||
Zum Vergleich ein stark vereinfachendes Beispiel mit `sqlite3`:
|
||||
|
||||
```python
|
||||
import sqlite3
|
||||
|
||||
conn = sqlite3.connect("bank_plain.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Tabellen anlegen
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS kunde (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS konto (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
kontonummer TEXT NOT NULL UNIQUE,
|
||||
saldo REAL DEFAULT 0.0,
|
||||
kunde_id INTEGER NOT NULL,
|
||||
FOREIGN KEY(kunde_id) REFERENCES kunde(id)
|
||||
)
|
||||
""")
|
||||
|
||||
# Kunde anlegen
|
||||
cursor.execute("INSERT INTO kunde (name) VALUES (?)", ("Max Mustermann",))
|
||||
kunde_id = cursor.lastrowid
|
||||
|
||||
# Konten anlegen
|
||||
cursor.execute(
|
||||
"INSERT INTO konto (kontonummer, saldo, kunde_id) VALUES (?, ?, ?)",
|
||||
("DE123", 1000.0, kunde_id)
|
||||
)
|
||||
cursor.execute(
|
||||
"INSERT INTO konto (kontonummer, saldo, kunde_id) VALUES (?, ?, ?)",
|
||||
("DE456", 2500.0, kunde_id)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
# Kunden mit Konten abfragen
|
||||
cursor.execute("""
|
||||
SELECT k.name, ko.kontonummer, ko.saldo
|
||||
FROM kunde k
|
||||
JOIN konto ko ON ko.kunde_id = k.id
|
||||
""")
|
||||
rows = cursor.fetchall()
|
||||
|
||||
for row in rows:
|
||||
name, kontonummer, saldo = row
|
||||
print(name, kontonummer, saldo)
|
||||
|
||||
conn.close()
|
||||
```
|
||||
|
||||
Hier musst du:
|
||||
|
||||
- SQL selbst schreiben,
|
||||
- `rows` manuell in Python-Strukturen übersetzen.
|
||||
|
||||
Mit ORM übernimmt der ORM-Layer:
|
||||
|
||||
- das Mapping,
|
||||
- das Auflösen der Beziehungen,
|
||||
- einiges an Boilerplate.
|
||||
|
||||
---
|
||||
|
||||
## 7. Speziell im Bank-/Finanz-Kontext
|
||||
|
||||
Gerade im Bankumfeld ist ORM attraktiv, weil:
|
||||
|
||||
- es viele **fachliche Entitäten** gibt (Konto, Kunde, Vertrag, Produkt, Transaktion, Wertpapier, Order, …),
|
||||
- du komplexe **Geschäftslogik** in Python implementieren willst,
|
||||
- und dabei konsistent auf persistente Daten zugreifen musst.
|
||||
|
||||
Typische Muster:
|
||||
|
||||
- **Transaktions-Logs** (jede Buchung als eigener Datensatz),
|
||||
- **Audit-Trails** (wer hat wann was geändert),
|
||||
- **Referenzdaten** (Währungen, Länder, Produktstammdaten).
|
||||
|
||||
ORMs helfen:
|
||||
|
||||
- diese Entitäten sauber als Klassen zu modellieren,
|
||||
- Beziehungen explizit zu machen,
|
||||
- und fachliche Operationen wie „Buchung durchführen“ objektorientiert abzubilden.
|
||||
|
||||
---
|
||||
|
||||
## 8. Wann lohnt sich ein ORM – und wann eher nicht?
|
||||
|
||||
**Sinnvoll:**
|
||||
|
||||
- Du baust eine mittlere bis große Anwendung mit vielen Entitäten.
|
||||
- Du willst langfristig Wartbarkeit, Tests und Refactoring erleichtern.
|
||||
- Du hast Standard-CRUD-Operationen (create/read/update/delete) und „normale“ fachliche Logik.
|
||||
|
||||
**Eher nicht sinnvoll:**
|
||||
|
||||
- Kleine Skripte, die nur ein paar einfache SQL-Abfragen ausführen.
|
||||
- Hochoptimierte Reporting- oder Analytics-Abfragen, die ohnehin spezielle SQL-Funktionen nutzen.
|
||||
- Wenn dein Team sehr SQL-affin ist und kein Interesse an der zusätzlichen Abstraktionsschicht hat.
|
||||
|
||||
---
|
||||
|
||||
## 9. Zusammenfassung
|
||||
|
||||
- **ORM** ist eine Technik (und meist eine Bibliothek), um **relationalen Datenbanken** über **Objekte** und **Klassen** zu begegnen.
|
||||
- Sie löst das Mapping zwischen Tabellen/Zeilen und Klassen/Objekten und reduziert manuellen SQL-Boilerplate.
|
||||
- Sie bietet Vorteile in Wartbarkeit, Typisierung, Testbarkeit – gerade bei komplexeren Domänen wie Banking.
|
||||
- Gleichzeitig bringt ein ORM neue Herausforderungen mit sich: Performance-Fallen, Lernaufwand, Verständnis der generierten SQL-Abfragen bleibt wichtig.
|
||||
- In Python sind gängige ORMs: **[[SQLAlchemy]] ORM**, **Django ORM**, (in moderneren Stacks oft in Kombination mit [[Pydantic]], [[FastAPI]] etc.).
|
||||
|
||||
+346
@@ -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.
|
||||
Executable
+384
@@ -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.
|
||||
Executable
+549
@@ -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.
|
||||
Executable
+384
@@ -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.
|
||||
Executable
+360
@@ -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.
|
||||
Executable
+411
@@ -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.
|
||||
|
||||
Executable
+499
@@ -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.
|
||||
Executable
+470
@@ -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).
|
||||
Executable
+1170
File diff suppressed because it is too large
Load Diff
Executable
+368
@@ -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()
|
||||
```
|
||||
|
||||
---
|
||||
Executable
+525
@@ -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.
|
||||
Executable
+640
@@ -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.
|
||||
Executable
+1212
File diff suppressed because it is too large
Load Diff
Executable
+540
@@ -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).
|
||||
Executable
+94
@@ -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.
|
||||
Executable
+469
@@ -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.**
|
||||
|
||||
Regular → Executable
Executable
+1
@@ -0,0 +1 @@
|
||||
unison "$HOME/links/mathias_dfine_vault/Know-How/" "$HOME/it-know-how/"
|
||||
Executable
+606
@@ -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 = `
|
||||
<div>
|
||||
<h1>Title</h1>
|
||||
</div>
|
||||
`;
|
||||
```
|
||||
|
||||
**Python comparison:**
|
||||
```python
|
||||
label = f"{year}-{month}-{day}"
|
||||
html = """
|
||||
<div>
|
||||
<h1>Title</h1>
|
||||
</div>
|
||||
"""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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.*
|
||||
+29
@@ -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`.
|
||||
+28
@@ -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.
|
||||
Executable
+31
@@ -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.
|
||||
+31
@@ -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)`.
|
||||
+36
@@ -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)
|
||||
```
|
||||
+23
@@ -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.
|
||||
+29
@@ -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
|
||||
```
|
||||
+23
@@ -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.
|
||||
Executable
+23
@@ -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.
|
||||
Executable
+28
@@ -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}"
|
||||
```
|
||||
Executable
+17
@@ -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.
|
||||
+23
@@ -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 ?? '';
|
||||
}
|
||||
```
|
||||
+56
@@ -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')
|
||||
```
|
||||
@@ -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.
|
||||
+27
@@ -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.
|
||||
+30
@@ -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.
|
||||
+28
@@ -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.
|
||||
@@ -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 []
|
||||
```
|
||||
+38
@@ -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)]
|
||||
```
|
||||
+23
@@ -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`.
|
||||
Executable
+26
@@ -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.
|
||||
Executable
+747
@@ -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 `<script setup>`). If you see `ref()` and `computed()`, you're looking at Vue 3.
|
||||
|
||||
### Python Analogy
|
||||
|
||||
Think of Vue components as Python classes that combine:
|
||||
|
||||
| Python | Vue |
|
||||
|--------|-----|
|
||||
| Class with state | `<script setup>` with `ref()` values |
|
||||
| Class methods | Functions defined in `<script setup>` |
|
||||
| `__str__` or template engine | `<template>` section |
|
||||
| Class styling | `<style>` section |
|
||||
|
||||
---
|
||||
|
||||
## The Big Idea: Reactivity
|
||||
|
||||
**Reactive** means the UI automatically updates when your data changes.
|
||||
|
||||
```python
|
||||
# Python: you manually update the view
|
||||
class Counter:
|
||||
def __init__(self):
|
||||
self.count = 0
|
||||
def increment(self):
|
||||
self.count += 1
|
||||
self.update_view() # you must call this manually
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- Vue: UI updates automatically -->
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
const count = ref(0);
|
||||
function increment() {
|
||||
count.value += 1; // UI updates automatically
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button @click="increment">Clicked {{ count }} times</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
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
|
||||
<template>
|
||||
<!-- 1. TEMPLATE: what to display -->
|
||||
<p>{{ greeting }}</p>
|
||||
<button @click="sayHello">Click me</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 2. SCRIPT: data and logic
|
||||
import { ref } from 'vue';
|
||||
|
||||
const greeting = ref('Hello from Vue!');
|
||||
|
||||
function sayHello() {
|
||||
console.log('Button clicked!');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 3. STYLE: how it looks */
|
||||
p { color: blue; }
|
||||
</style>
|
||||
```
|
||||
|
||||
### 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<string[]>([]); // reactive array
|
||||
```
|
||||
|
||||
### Reading and Writing
|
||||
|
||||
In `<script>`, use `.value`:
|
||||
|
||||
```ts
|
||||
count.value += 1; // increment
|
||||
username.value = 'bob'; // change string
|
||||
isLoading.value = true; // set to true
|
||||
items.value.push('apple'); // modify array
|
||||
```
|
||||
|
||||
In `<template>`, Vue unwraps refs automatically—use the variable name without `.value`:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<p>{{ count }}</p> <!-- reads count.value -->
|
||||
<p>{{ username }}</p> <!-- reads username.value -->
|
||||
<button :disabled="isLoading">Save</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Python Comparison
|
||||
|
||||
```python
|
||||
# Python
|
||||
count = 0
|
||||
count += 1
|
||||
```
|
||||
|
||||
```ts
|
||||
// Vue (script)
|
||||
const count = ref(0);
|
||||
count.value += 1;
|
||||
```
|
||||
|
||||
The `.value` exists because `count` is a wrapper object, not the raw value.
|
||||
|
||||
### Example: Loading State
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<button :disabled="isLoading" @click="save">
|
||||
{{ isLoading ? 'Saving...' : 'Save' }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const isLoading = ref(false);
|
||||
|
||||
async function save() {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await fakeRequest();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function fakeRequest() {
|
||||
return new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
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<string | null>(null);
|
||||
const selectedEnd = ref<string | null>(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
|
||||
<p>{{ username }}</p>
|
||||
<p>{{ 2 + 3 }}</p>
|
||||
<p>{{ isLoading ? 'Loading...' : 'Done' }}</p>
|
||||
<p>{{ items.length }} items</p>
|
||||
```
|
||||
|
||||
### Conditional Rendering: `v-if` / `v-else`
|
||||
|
||||
Show elements based on conditions:
|
||||
|
||||
```vue
|
||||
<p v-if="error">{{ error }}</p>
|
||||
<p v-else>No errors</p>
|
||||
|
||||
<div v-if="isLoggedIn">
|
||||
<p>Welcome, {{ username }}!</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>Please log in.</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Loops: `v-for`
|
||||
|
||||
Repeat elements for each item:
|
||||
|
||||
```vue
|
||||
<ul>
|
||||
<li v-for="user in users" :key="user.id">
|
||||
{{ user.name }}
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
**Important:** Always use `:key` with `v-for` for proper list rendering.
|
||||
|
||||
### Attribute Binding: `:`
|
||||
|
||||
Bind HTML attributes to JavaScript expressions:
|
||||
|
||||
```vue
|
||||
<button :disabled="isLoading">Save</button>
|
||||
<img :src="imageUrl" />
|
||||
<a :href="profileUrl">View Profile</a>
|
||||
```
|
||||
|
||||
Short for `v-bind:disabled`.
|
||||
|
||||
### Class Binding
|
||||
|
||||
```vue
|
||||
<div :class="{ active: isSelected, highlight: hasError }">
|
||||
Content
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Two-Way Binding with `v-model`
|
||||
|
||||
`v-model` connects a form field and a reactive variable bidirectionally.
|
||||
|
||||
### Basic Input
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<input v-model="email" />
|
||||
<p>You typed: {{ email }}</p>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
const email = ref('');
|
||||
</script>
|
||||
```
|
||||
|
||||
- User types → `email` updates
|
||||
- Code changes `email` → input display updates
|
||||
|
||||
### Select Dropdown
|
||||
|
||||
```vue
|
||||
<v-select
|
||||
v-model="selectedCountry"
|
||||
:items="countries"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
/>
|
||||
```
|
||||
|
||||
```ts
|
||||
const countries = [
|
||||
{ title: 'Germany', value: 'DE' },
|
||||
{ title: 'France', value: 'FR' },
|
||||
{ title: 'Spain', value: 'ES' },
|
||||
];
|
||||
const selectedCountry = ref<string | null>(null);
|
||||
```
|
||||
|
||||
- `item-title`: what's shown to users
|
||||
- `item-value`: what's stored in the variable
|
||||
|
||||
---
|
||||
|
||||
## Events and User Interaction
|
||||
|
||||
### Click Events: `@click`
|
||||
|
||||
```vue
|
||||
<button @click="save">Save</button>
|
||||
```
|
||||
|
||||
```ts
|
||||
function save() {
|
||||
console.log('saving...');
|
||||
}
|
||||
```
|
||||
|
||||
### Other Common Events
|
||||
|
||||
```vue
|
||||
<input @input="onInput" /> <!-- typing -->
|
||||
<form @submit.prevent="submitForm" /> <!-- form submission -->
|
||||
<select @change="onChange" /> <!-- dropdown change -->
|
||||
<div @mouseover="hovering = true" /> <!-- mouse events -->
|
||||
```
|
||||
|
||||
The `.prevent` modifier calls `event.preventDefault()` automatically.
|
||||
|
||||
### Event with Parameters
|
||||
|
||||
```vue
|
||||
<button @click="deleteItem(id)">Delete</button>
|
||||
```
|
||||
|
||||
```ts
|
||||
function deleteItem(id: number) {
|
||||
console.log('deleting', id);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Components and Props
|
||||
|
||||
### What is a Component?
|
||||
|
||||
A component is a reusable UI unit. Think of it like a Python function that returns HTML, but with its own state and logic.
|
||||
|
||||
### Parent Using Child
|
||||
|
||||
```vue
|
||||
<!-- ParentComponent.vue -->
|
||||
<template>
|
||||
<UserCard :name="username" :age="34" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import UserCard from './UserCard.vue';
|
||||
const username = 'Alice';
|
||||
</script>
|
||||
```
|
||||
|
||||
### Props: Data Passed In
|
||||
|
||||
Props are like function arguments for components.
|
||||
|
||||
```vue
|
||||
<!-- UserCard.vue -->
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
name: string;
|
||||
age: number;
|
||||
mode?: 'compact' | 'full'; // optional prop
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
mode: 'full',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="props.mode">
|
||||
<p>{{ props.name }} is {{ props.age }} years old.</p>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Python Comparison
|
||||
|
||||
```python
|
||||
# Python function
|
||||
def user_card(name: str, age: int, mode: str = 'full'):
|
||||
return f"{name} is {age} years old ({mode})"
|
||||
```
|
||||
|
||||
### Child-to-Parent Communication
|
||||
|
||||
Props go down; events go up.
|
||||
|
||||
```vue
|
||||
<!-- Parent -->
|
||||
<template>
|
||||
<FileUpload @upload="handleUpload" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
function handleUpload(file: File) {
|
||||
console.log('Uploaded:', file.name);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
The child component calls `emit('upload', file)` when something happens. The parent decides what to do.
|
||||
|
||||
---
|
||||
|
||||
## Lifecycle Hooks
|
||||
|
||||
Code that runs at specific moments in a component's life.
|
||||
|
||||
### `onMounted`: When Component Appears
|
||||
|
||||
```ts
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('Component is on screen');
|
||||
await fetchData();
|
||||
});
|
||||
```
|
||||
|
||||
This is like `__post_init__` or a setup function that runs after initialization.
|
||||
|
||||
### Other Common Hooks
|
||||
|
||||
| Hook | When It Runs |
|
||||
|------|-------------|
|
||||
| `onMounted` | Component is added to DOM |
|
||||
| `onUnmounted` | Component is removed from DOM |
|
||||
| `onUpdated` | Component re-renders |
|
||||
| `onBeforeMount` | Just before first render |
|
||||
| `onBeforeUnmount` | Just before removal |
|
||||
|
||||
---
|
||||
|
||||
## Composables: Reusable Logic
|
||||
|
||||
A **composable** is a reusable function containing Vue logic.
|
||||
|
||||
### Example: useCounter
|
||||
|
||||
```ts
|
||||
// useCounter.ts
|
||||
export function useCounter() {
|
||||
const count = ref(0);
|
||||
|
||||
function increment() {
|
||||
count.value += 1;
|
||||
}
|
||||
|
||||
return { count, increment };
|
||||
}
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- Using it -->
|
||||
<script setup lang="ts">
|
||||
import { useCounter } from './useCounter';
|
||||
|
||||
const { count, increment } = useCounter();
|
||||
</script>
|
||||
```
|
||||
|
||||
### Why Composables?
|
||||
|
||||
Without composables, components get too big. Composables let you:
|
||||
|
||||
- Reuse logic across components
|
||||
- Separate data fetching from UI
|
||||
- Keep components readable
|
||||
|
||||
### Real Examples from Projects
|
||||
|
||||
```ts
|
||||
// Separate backend logic from UI
|
||||
const { downloadFlexData } = useJsonDownload();
|
||||
const rows = await downloadFlexData(payload);
|
||||
|
||||
// Authentication logic
|
||||
const { isAuthenticated, login, logout } = useAuth();
|
||||
```
|
||||
|
||||
### Python Comparison
|
||||
|
||||
Composables are like Python modules with business logic:
|
||||
|
||||
```python
|
||||
# Python
|
||||
from downloads import download_flex_data
|
||||
rows = download_flex_data(payload)
|
||||
|
||||
# Vue
|
||||
const { downloadFlexData } = useJsonDownload();
|
||||
const rows = await downloadFlexData(payload);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## A Real-World Example: Filter Flow
|
||||
|
||||
This pattern appears frequently in data-driven applications.
|
||||
|
||||
### The Problem
|
||||
|
||||
User selects filter options, then clicks "Apply" to fetch data. Without separating "selected" and "applied" state, every dropdown change would trigger a backend request.
|
||||
|
||||
### State Structure
|
||||
|
||||
```ts
|
||||
// What the user is currently editing
|
||||
const selectedStart = ref<string | null>(defaultStart);
|
||||
const selectedEnd = ref<string | null>(defaultEnd);
|
||||
|
||||
// What was last applied
|
||||
const appliedStart = ref<string | null>(defaultStart);
|
||||
const appliedEnd = ref<string | null>(defaultEnd);
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
```ts
|
||||
const isValidRange = computed(() => {
|
||||
if (!selectedStart.value || !selectedEnd.value) return true;
|
||||
return new Date(selectedStart.value) <= new Date(selectedEnd.value);
|
||||
});
|
||||
```
|
||||
|
||||
### Apply Action
|
||||
|
||||
```ts
|
||||
async function applyFilters() {
|
||||
if (!isValidRange.value) return;
|
||||
|
||||
appliedStart.value = selectedStart.value;
|
||||
appliedEnd.value = selectedEnd.value;
|
||||
|
||||
await refreshData();
|
||||
}
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
User changes dropdown
|
||||
↓
|
||||
selectedStart updates (draft state)
|
||||
↓
|
||||
Validation recomputes
|
||||
↓
|
||||
User clicks Apply
|
||||
↓
|
||||
selected → applied (commit)
|
||||
↓
|
||||
Build payload
|
||||
↓
|
||||
Fetch from backend
|
||||
↓
|
||||
Update table data
|
||||
↓
|
||||
Table re-renders
|
||||
```
|
||||
|
||||
### Why This Matters
|
||||
|
||||
- **Draft state**: what the user is currently editing (may not be valid)
|
||||
- **Applied state**: what the system is actually using
|
||||
|
||||
This prevents excessive API calls and ensures exports match what's visible on screen.
|
||||
|
||||
---
|
||||
|
||||
## Vue Strengths, Weaknesses, and Use Cases
|
||||
|
||||
### Strengths
|
||||
|
||||
| Strength | Description |
|
||||
|----------|-------------|
|
||||
| **Gentle learning curve** | Approachable syntax; good docs; Vue 3 Composition API feels natural |
|
||||
| **Reactivity system** | Automatic UI updates without manual DOM manipulation |
|
||||
| **Single-file components** | Template, logic, and styles in one file—easy to understand |
|
||||
| **Flexible architecture** | Use it for simple widgets or complex SPAs |
|
||||
| **Great tooling** | Vite dev server, Vue DevTools, excellent TypeScript support |
|
||||
| **Active ecosystem** | Vuetify (Material UI), Nuxt (SSR/SSG), Pinia (state management) |
|
||||
|
||||
### Weaknesses
|
||||
|
||||
| Weakness | Description |
|
||||
|----------|-------------|
|
||||
| **JavaScript ecosystem** | Requires understanding npm, build tools, ES modules |
|
||||
| **Reactivity gotchas** | Objects/arrays need special handling; `.value` confusion |
|
||||
| **Mobile/Web separation** | Vue itself is web-only (consider NativeScript or Capacitor for mobile) |
|
||||
| **Smaller ecosystem than React** | Fewer third-party libraries |
|
||||
|
||||
### Typical Use Cases
|
||||
|
||||
| Use Case | Example |
|
||||
|----------|---------|
|
||||
| **Single-page applications (SPAs)** | Dashboards, admin panels, SaaS products |
|
||||
| **Interactive UI components** | Data tables with filters, dynamic forms, real-time updates |
|
||||
| **Progressive enhancement** | Add Vue to existing apps (like you might use jQuery) |
|
||||
| **Prototyping** | Quick MVPs with Vuetify or Tailwind UI components |
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Template Syntax
|
||||
|
||||
```vue
|
||||
{{ value }} <!-- interpolation -->
|
||||
v-model="variable" <!-- two-way binding -->
|
||||
@click="handler" <!-- click event -->
|
||||
:prop="value" <!-- bind attribute -->
|
||||
v-if="condition" <!-- conditional -->
|
||||
v-for="item in items" <!-- loop -->
|
||||
```
|
||||
|
||||
### Script Basics
|
||||
|
||||
```ts
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
|
||||
const variable = ref(initialValue); // reactive state
|
||||
const derived = computed(() => ...); // derived state
|
||||
onMounted(() => { ... }); // lifecycle hook
|
||||
```
|
||||
|
||||
### Common Patterns
|
||||
|
||||
| Pattern | Code |
|
||||
|---------|------|
|
||||
| Reading a ref | `variable.value` in script, `variable` in template |
|
||||
| Writing a ref | `variable.value = newValue` |
|
||||
| Defining props | `const props = defineProps<Props>()` |
|
||||
| Optional props | `withDefaults(defineProps<Props>(), { optional: 'default' })` |
|
||||
| Emitting events | `emit('eventName', data)` |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Vue combines:
|
||||
|
||||
- **Reactivity**: UI updates automatically when state changes
|
||||
- **Components**: Reusable pieces combining template, logic, and styles
|
||||
- **Composables**: Shared logic extracted into testable functions
|
||||
- **Props and Events**: Clear data flow between components
|
||||
|
||||
The key mental shift from Python:
|
||||
|
||||
| Python Thinking | Vue Thinking |
|
||||
|----------------|--------------|
|
||||
| Call functions to update view | Change state, view updates automatically |
|
||||
| Classes hold state and methods | Components hold reactive refs and functions |
|
||||
| Import modules for logic | Import composables for reusable logic |
|
||||
|
||||
---
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Vue 3 Documentation](https://vuejs.org/guide/)
|
||||
- [Vue School Tutorials](https://vueschool.io/)
|
||||
- [Vuetify Components](https://vuetifyjs.com/) (your project's UI framework)
|
||||
- Project composables: `useJsonDownload`, `useJsonUpload`, `useAuth`
|
||||
Executable
+215
@@ -0,0 +1,215 @@
|
||||
# Vue for Python Developers
|
||||
|
||||
This note gives you a practical mental model for Vue if you already know Python.
|
||||
|
||||
## The Big Idea
|
||||
|
||||
Vue components are small units that combine:
|
||||
|
||||
- template: what should be shown
|
||||
- script: data and logic
|
||||
- style: how it should look
|
||||
|
||||
A Vue single-file component often looks like this:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<p>{{ message }}</p>
|
||||
<button @click="increment">Clicked {{ count }} times</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const message = 'Hello from Vue';
|
||||
const count = ref(0);
|
||||
|
||||
function increment() {
|
||||
count.value += 1;
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Python Analogy
|
||||
|
||||
Think of a Vue component like a Python object plus a small HTML view.
|
||||
|
||||
Python-style thinking:
|
||||
|
||||
```python
|
||||
class CounterPage:
|
||||
def __init__(self):
|
||||
self.message = "Hello from Vue"
|
||||
self.count = 0
|
||||
|
||||
def increment(self):
|
||||
self.count += 1
|
||||
```
|
||||
|
||||
Vue-style thinking:
|
||||
|
||||
```ts
|
||||
const message = 'Hello from Vue';
|
||||
const count = ref(0);
|
||||
|
||||
function increment() {
|
||||
count.value += 1;
|
||||
}
|
||||
```
|
||||
|
||||
Difference:
|
||||
|
||||
- In Python, changing `self.count` changes object state.
|
||||
- In Vue, changing `count.value` changes reactive state and the UI updates automatically.
|
||||
|
||||
## What "Reactive" Means
|
||||
|
||||
Reactive means the UI watches data. When the data changes, Vue updates the screen.
|
||||
|
||||
Example:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<p>{{ name }}</p>
|
||||
<button @click="rename">Rename</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const name = ref('Mathias');
|
||||
|
||||
function rename() {
|
||||
name.value = 'Mathias 2';
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
When `name.value` changes, the paragraph updates.
|
||||
|
||||
## Template vs Script
|
||||
|
||||
The template is not plain HTML. It is HTML with Vue features.
|
||||
|
||||
Example:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<p>{{ username }}</p>
|
||||
<button @click="login">Login</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
- `{{ username }}` means: print a variable into the HTML.
|
||||
- `@click="login"` means: run the function when clicked.
|
||||
|
||||
The script defines the variables and functions used by the template.
|
||||
|
||||
```ts
|
||||
<script setup lang="ts">
|
||||
const username = 'alice';
|
||||
|
||||
function login() {
|
||||
console.log('login clicked');
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Common Vue Building Blocks
|
||||
|
||||
### `ref`
|
||||
|
||||
Used for reactive single values.
|
||||
|
||||
```ts
|
||||
const count = ref(0);
|
||||
const username = ref('alice');
|
||||
const isLoading = ref(false);
|
||||
```
|
||||
|
||||
### `computed`
|
||||
|
||||
Used for values derived from other values.
|
||||
|
||||
```ts
|
||||
const firstName = ref('Ada');
|
||||
const lastName = ref('Lovelace');
|
||||
|
||||
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
|
||||
```
|
||||
|
||||
### event handlers
|
||||
|
||||
Functions triggered by user actions.
|
||||
|
||||
```vue
|
||||
<button @click="save">Save</button>
|
||||
```
|
||||
|
||||
```ts
|
||||
function save() {
|
||||
console.log('saving');
|
||||
}
|
||||
```
|
||||
|
||||
### lifecycle hooks
|
||||
|
||||
Code that runs when a component starts.
|
||||
|
||||
```ts
|
||||
onMounted(async () => {
|
||||
await loadData();
|
||||
});
|
||||
```
|
||||
|
||||
This is roughly like running setup code after the component appears on screen.
|
||||
|
||||
## `v-model` in One Sentence
|
||||
|
||||
`v-model` is two-way binding between a form field and a reactive variable.
|
||||
|
||||
```vue
|
||||
<input v-model="username" />
|
||||
```
|
||||
|
||||
If the user types into the input, `username` changes.
|
||||
If `username` changes in code, the input display changes.
|
||||
|
||||
## Example: Filter Form
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<input v-model="searchText" />
|
||||
<button @click="apply">Apply</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const searchText = ref('');
|
||||
|
||||
function apply() {
|
||||
console.log('User searched for:', searchText.value);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
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]]
|
||||
Executable
+252
@@ -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
|
||||
<template>
|
||||
<p>{{ count }}</p>
|
||||
</template>
|
||||
```
|
||||
|
||||
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
|
||||
<template>
|
||||
<button :disabled="isLoading" @click="save">
|
||||
{{ isLoading ? 'Saving...' : 'Save' }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const isLoading = ref(false);
|
||||
|
||||
async function save() {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await fakeRequest();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function fakeRequest() {
|
||||
return new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
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]]
|
||||
Executable
+252
@@ -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
|
||||
<template>
|
||||
<p>{{ username }}</p>
|
||||
<input v-model="username" />
|
||||
<button @click="reset">Reset</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const username = ref('mathias');
|
||||
|
||||
function reset() {
|
||||
username.value = '';
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
The same variable is:
|
||||
|
||||
- shown inside `{{ username }}`
|
||||
- edited by `<input v-model="username" />`
|
||||
- changed by the `reset()` function
|
||||
|
||||
## `{{ ... }}`: show a value
|
||||
|
||||
```vue
|
||||
<p>{{ username }}</p>
|
||||
<p>{{ isLoading ? 'Loading...' : 'Done' }}</p>
|
||||
<p>{{ 2 + 3 }}</p>
|
||||
```
|
||||
|
||||
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
|
||||
<input v-model="email" />
|
||||
```
|
||||
|
||||
```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
|
||||
<v-select
|
||||
v-model="selectedCountry"
|
||||
:items="countries"
|
||||
label="Country"
|
||||
/>
|
||||
```
|
||||
|
||||
```ts
|
||||
const countries = ['Germany', 'France', 'Spain'];
|
||||
const selectedCountry = ref<string | null>(null);
|
||||
```
|
||||
|
||||
When the user picks France, `selectedCountry.value` becomes `'France'`.
|
||||
|
||||
## Your real example
|
||||
|
||||
From FlexibilityTable:
|
||||
|
||||
```vue
|
||||
<v-select
|
||||
v-model="selectedPeriodStartFrom"
|
||||
:items="periodStartOptions"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
label="Period start from"
|
||||
/>
|
||||
```
|
||||
|
||||
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
|
||||
<button @click="increment">Add</button>
|
||||
```
|
||||
|
||||
```ts
|
||||
function increment() {
|
||||
count.value += 1;
|
||||
}
|
||||
```
|
||||
|
||||
Other common events:
|
||||
|
||||
```vue
|
||||
<input @input="onInput" />
|
||||
<form @submit.prevent="submitForm" />
|
||||
<select @change="onChange" />
|
||||
```
|
||||
|
||||
## Binding attributes with `:`
|
||||
|
||||
The `:` shorthand means "bind this HTML or component prop to JavaScript".
|
||||
|
||||
```vue
|
||||
<button :disabled="isLoading">Save</button>
|
||||
```
|
||||
|
||||
Equivalent long form:
|
||||
|
||||
```vue
|
||||
<button v-bind:disabled="isLoading">Save</button>
|
||||
```
|
||||
|
||||
If `isLoading` is true, the button becomes disabled.
|
||||
|
||||
## Example: disable refresh button
|
||||
|
||||
In your component:
|
||||
|
||||
```vue
|
||||
<v-btn
|
||||
:disabled="loading || !hasValidPeriodRange"
|
||||
@click="applyDateFilters"
|
||||
>
|
||||
Refresh
|
||||
</v-btn>
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
- disable the button if data is loading
|
||||
- also disable it if the selected date range is invalid
|
||||
- when clicked, run `applyDateFilters`
|
||||
|
||||
## Conditional rendering
|
||||
|
||||
Show something only if a condition is true:
|
||||
|
||||
```vue
|
||||
<p v-if="errorMessage">{{ errorMessage }}</p>
|
||||
<p v-else>No errors</p>
|
||||
```
|
||||
|
||||
## Loops with `v-for`
|
||||
|
||||
```vue
|
||||
<li v-for="user in users" :key="user.id">
|
||||
{{ user.name }}
|
||||
</li>
|
||||
```
|
||||
|
||||
This repeats the element for each item.
|
||||
|
||||
## Common beginner confusion: where does a template variable come from?
|
||||
|
||||
If you see this in a template:
|
||||
|
||||
```vue
|
||||
<p>{{ total }}</p>
|
||||
```
|
||||
|
||||
search the script for:
|
||||
|
||||
- `const total = ...`
|
||||
- `let total = ...`
|
||||
- `function total() ...`
|
||||
- `computed(() => ...)`
|
||||
- `defineProps(...)`
|
||||
|
||||
The template only knows values exposed by the script.
|
||||
|
||||
## Mental model for reading templates
|
||||
|
||||
For every template line, ask:
|
||||
|
||||
1. Is it displaying a value?
|
||||
2. Is it binding a prop?
|
||||
3. Is it listening for an event?
|
||||
4. Which script variable or function does it connect to?
|
||||
|
||||
## Mini Example: search box
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<input v-model="searchText" placeholder="Search users" />
|
||||
<button :disabled="!searchText" @click="search">Search</button>
|
||||
<p>You entered: {{ searchText }}</p>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const searchText = ref('');
|
||||
|
||||
function search() {
|
||||
console.log('Searching for', searchText.value);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
Flow:
|
||||
|
||||
- user types into input
|
||||
- `searchText` updates
|
||||
- paragraph updates immediately
|
||||
- button disable state updates immediately
|
||||
- clicking Search uses current `searchText`
|
||||
|
||||
## Related Notes
|
||||
|
||||
- [[00-Vue for Python Developers]]
|
||||
- [[01-Refs, Computed, and Reactivity]]
|
||||
- [[03-Components, Props, Lifecycle, and Composables]]
|
||||
- [[04-FlexibilityTable Filter Flow]]
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
# Components, Props, Lifecycle, and Composables
|
||||
|
||||
This note explains how Vue code is organized into reusable pieces.
|
||||
|
||||
## Components
|
||||
|
||||
A component is a reusable UI unit.
|
||||
|
||||
Examples from your project:
|
||||
|
||||
- `FlexibilityTable.vue`
|
||||
- `FileUpload.vue`
|
||||
- `AppHeader.vue`
|
||||
|
||||
A parent component can render a child component.
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<UserCard />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import UserCard from './UserCard.vue';
|
||||
</script>
|
||||
```
|
||||
|
||||
## Props: inputs passed into a component
|
||||
|
||||
Props are like function arguments for components.
|
||||
|
||||
Example:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<UserCard :name="username" :age="34" />
|
||||
</template>
|
||||
```
|
||||
|
||||
Child component:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p>{{ props.name }} is {{ props.age }} years old.</p>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Python comparison
|
||||
|
||||
This is similar to passing constructor arguments or function arguments.
|
||||
|
||||
```python
|
||||
render_user_card(name=username, age=34)
|
||||
```
|
||||
|
||||
## Your real example
|
||||
|
||||
In FlexibilityTable:
|
||||
|
||||
```ts
|
||||
interface FlexibilityTableProps {
|
||||
title: string;
|
||||
uploadMode?: 'fsp' | 'tso' | 'both' | 'none';
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<FlexibilityTableProps>(), {
|
||||
uploadMode: 'none',
|
||||
});
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
- the parent must give `title`
|
||||
- the parent may give `uploadMode`
|
||||
- if `uploadMode` is missing, use `'none'`
|
||||
|
||||
Then template code can use:
|
||||
|
||||
```vue
|
||||
<v-toolbar-title>
|
||||
{{ title }}
|
||||
</v-toolbar-title>
|
||||
```
|
||||
|
||||
and script code can use:
|
||||
|
||||
```ts
|
||||
props.uploadMode
|
||||
```
|
||||
|
||||
## Lifecycle: when should code run?
|
||||
|
||||
Sometimes you want code to run when the component appears.
|
||||
|
||||
```ts
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
onMounted(() => {
|
||||
console.log('Component is mounted');
|
||||
});
|
||||
```
|
||||
|
||||
In your component:
|
||||
|
||||
```ts
|
||||
onMounted(async () => {
|
||||
await refreshTableDataFromBackend();
|
||||
});
|
||||
```
|
||||
|
||||
That means when the table component is loaded, it immediately fetches backend data.
|
||||
|
||||
## Composables
|
||||
|
||||
A composable is a reusable function that contains Vue logic.
|
||||
|
||||
Common pattern:
|
||||
|
||||
```ts
|
||||
export function useCounter() {
|
||||
const count = ref(0);
|
||||
|
||||
function increment() {
|
||||
count.value += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
count,
|
||||
increment,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Use it inside a component:
|
||||
|
||||
```ts
|
||||
const { count, increment } = useCounter();
|
||||
```
|
||||
|
||||
## Why composables exist
|
||||
|
||||
Without composables, components get too big.
|
||||
|
||||
Instead of putting all API logic, auth logic, upload logic, and export logic directly in one component, you move reusable logic into composables.
|
||||
|
||||
Your project has examples:
|
||||
|
||||
- `useJsonDownload()`
|
||||
- `useJsonUpload()`
|
||||
- `useAuth()`
|
||||
|
||||
## Real example: `useJsonDownload`
|
||||
|
||||
In FlexibilityTable:
|
||||
|
||||
```ts
|
||||
const { downloadFlexData, downloadRawFlexData } = useJsonDownload();
|
||||
```
|
||||
|
||||
That means:
|
||||
|
||||
- this component imports data-loading logic from a composable
|
||||
- the composable handles HTTP and backend transformation details
|
||||
- the component only calls its functions
|
||||
|
||||
This is similar to separating Python business logic into a helper module.
|
||||
|
||||
Python style:
|
||||
|
||||
```python
|
||||
from downloads import download_flex_data
|
||||
|
||||
rows = download_flex_data(payload)
|
||||
```
|
||||
|
||||
Vue composable style:
|
||||
|
||||
```ts
|
||||
const { downloadFlexData } = useJsonDownload();
|
||||
const rows = await downloadFlexData(payload);
|
||||
```
|
||||
|
||||
## Child-to-parent communication
|
||||
|
||||
Props go down from parent to child.
|
||||
|
||||
Events often go up from child to parent.
|
||||
|
||||
Example:
|
||||
|
||||
```vue
|
||||
<FileUpload @upload="onFspUpload" />
|
||||
```
|
||||
|
||||
This means the child component emits an `upload` event, and the parent runs `onFspUpload` when that happens.
|
||||
|
||||
## Example structure
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<SearchBox @search="runSearch" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
function runSearch(query: string) {
|
||||
console.log('search query:', query);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
The child says: "something happened".
|
||||
The parent decides what to do.
|
||||
|
||||
## Rule of Thumb
|
||||
|
||||
Put code in a component when:
|
||||
|
||||
- it is very local to one UI element
|
||||
- it mostly controls rendering
|
||||
|
||||
Put code in a composable when:
|
||||
|
||||
- it will be reused
|
||||
- it handles data fetching
|
||||
- it contains business logic
|
||||
- it makes the component easier to read
|
||||
|
||||
## Reading Strategy
|
||||
|
||||
When you see a component, identify:
|
||||
|
||||
1. props coming in
|
||||
2. local reactive state
|
||||
3. composables being used
|
||||
4. lifecycle hooks
|
||||
5. events going to child components
|
||||
|
||||
## Related Notes
|
||||
|
||||
- [[00-Vue for Python Developers]]
|
||||
- [[02-Templates, v-model, and Events]]
|
||||
- [[04-FlexibilityTable Filter Flow]]
|
||||
Executable
+332
@@ -0,0 +1,332 @@
|
||||
# FlexibilityTable Filter Flow
|
||||
|
||||
This note explains how your `selectedPeriodStartFrom` example interacts with the rest of the code.
|
||||
|
||||
## Goal of the feature
|
||||
|
||||
The table lets the user:
|
||||
|
||||
- choose a start period
|
||||
- choose an end period
|
||||
- click Refresh
|
||||
- load matching backend data
|
||||
- view and export the filtered result
|
||||
|
||||
## The important variables
|
||||
|
||||
From FlexibilityTable:
|
||||
|
||||
```ts
|
||||
type PeriodFilterValue = string | null;
|
||||
|
||||
const periodStartOptions = getDownloadPeriodStartOptions();
|
||||
const defaultPeriodStartFrom = periodStartOptions[0]?.value ?? null;
|
||||
const defaultPeriodStartUntil = periodStartOptions[periodStartOptions.length - 1]?.value ?? null;
|
||||
|
||||
const selectedPeriodStartFrom = ref<PeriodFilterValue>(defaultPeriodStartFrom);
|
||||
const selectedPeriodStartUntil = ref<PeriodFilterValue>(defaultPeriodStartUntil);
|
||||
const appliedPeriodStartFrom = ref<PeriodFilterValue>(defaultPeriodStartFrom);
|
||||
const appliedPeriodStartUntil = ref<PeriodFilterValue>(defaultPeriodStartUntil);
|
||||
```
|
||||
|
||||
There are two groups of state here.
|
||||
|
||||
### selected values
|
||||
|
||||
These track what the user is currently editing in the UI.
|
||||
|
||||
```ts
|
||||
selectedPeriodStartFrom
|
||||
selectedPeriodStartUntil
|
||||
```
|
||||
|
||||
### applied values
|
||||
|
||||
These track what was last confirmed with the Refresh button.
|
||||
|
||||
```ts
|
||||
appliedPeriodStartFrom
|
||||
appliedPeriodStartUntil
|
||||
```
|
||||
|
||||
This separation is very important.
|
||||
|
||||
## Why both selected and applied exist
|
||||
|
||||
Without this split, every dropdown change could immediately trigger a backend request.
|
||||
|
||||
That would mean:
|
||||
|
||||
- too many requests
|
||||
- surprising UI updates while the user is still choosing dates
|
||||
- exports might not match the table state clearly
|
||||
|
||||
So the component uses a draft pattern.
|
||||
|
||||
Python analogy:
|
||||
|
||||
```python
|
||||
form_input = {'start': '2026-03-01', 'end': '2026-03-31'}
|
||||
applied_filters = {'start': '2026-02-01', 'end': '2026-02-28'}
|
||||
```
|
||||
|
||||
The form input is what the user is editing.
|
||||
The applied filters are what the system is currently using.
|
||||
|
||||
## Step 1: build options for the dropdown
|
||||
|
||||
```ts
|
||||
const periodStartOptions = getDownloadPeriodStartOptions();
|
||||
```
|
||||
|
||||
This comes from `jsonDownload.ts`.
|
||||
That composable calculates valid backend week starts.
|
||||
|
||||
Each option looks roughly like this:
|
||||
|
||||
```ts
|
||||
{
|
||||
title: '2026-03-23',
|
||||
value: '2026-03-22T23:00:00.000Z'
|
||||
}
|
||||
```
|
||||
|
||||
- `title` is for humans
|
||||
- `value` is for the program
|
||||
|
||||
## Step 2: connect the dropdown to state
|
||||
|
||||
```vue
|
||||
<v-select
|
||||
v-model="selectedPeriodStartFrom"
|
||||
:items="periodStartOptions"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
label="Period start from"
|
||||
/>
|
||||
```
|
||||
|
||||
When the user chooses an option:
|
||||
|
||||
- Vuetify reads the chosen item
|
||||
- Vue writes its `value` field into `selectedPeriodStartFrom`
|
||||
- any code depending on `selectedPeriodStartFrom` can react
|
||||
|
||||
## Step 3: validate the selected range
|
||||
|
||||
```ts
|
||||
const hasValidPeriodRange = computed(() => {
|
||||
if (!selectedPeriodStartFrom.value || !selectedPeriodStartUntil.value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return new Date(selectedPeriodStartFrom.value).getTime() <= new Date(selectedPeriodStartUntil.value).getTime();
|
||||
});
|
||||
```
|
||||
|
||||
This computed reads the two selected values and checks if the range makes sense.
|
||||
|
||||
The Refresh button uses it:
|
||||
|
||||
```vue
|
||||
<v-btn
|
||||
:disabled="loading || !hasValidPeriodRange"
|
||||
@click="applyDateFilters"
|
||||
>
|
||||
Refresh
|
||||
</v-btn>
|
||||
```
|
||||
|
||||
So if the user chooses an invalid range, the button becomes disabled.
|
||||
|
||||
## Step 4: user clicks Refresh
|
||||
|
||||
```ts
|
||||
async function applyDateFilters() {
|
||||
if (!hasValidPeriodRange.value) {
|
||||
showMessage('Select a valid period range before refreshing.', 'error', true);
|
||||
return;
|
||||
}
|
||||
|
||||
appliedPeriodStartFrom.value = selectedPeriodStartFrom.value;
|
||||
appliedPeriodStartUntil.value = selectedPeriodStartUntil.value;
|
||||
|
||||
await refreshTableDataFromBackend(
|
||||
'Could not load data from backend.',
|
||||
buildSelectedDownloadPayload(),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This does three things:
|
||||
|
||||
1. validate
|
||||
2. copy selected values into applied values
|
||||
3. fetch new backend data
|
||||
|
||||
## Step 5: build the request payload
|
||||
|
||||
```ts
|
||||
function buildSelectedDownloadPayload(): DownloadPayload {
|
||||
return buildDownloadPayload(
|
||||
buildDateRange(selectedPeriodStartFrom.value, selectedPeriodStartUntil.value),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
and:
|
||||
|
||||
```ts
|
||||
function buildDateRange(periodStartFrom: PeriodFilterValue, periodStartUntil: PeriodFilterValue): DownloadDateRange {
|
||||
return {
|
||||
periodStartFrom: periodStartFrom ?? undefined,
|
||||
periodStartUntil: periodStartUntil ?? undefined,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This turns UI values into backend filter input.
|
||||
|
||||
Important detail:
|
||||
|
||||
- `null` from the UI becomes `undefined` for the backend builder
|
||||
- that means "no boundary selected"
|
||||
|
||||
## Step 6: call the composable
|
||||
|
||||
```ts
|
||||
tableData.value = await downloadFlexData(payload);
|
||||
```
|
||||
|
||||
This happens inside `refreshTableDataFromBackend()`.
|
||||
|
||||
The component does not itself know how to talk to the backend in detail. That logic lives in `useJsonDownload()`.
|
||||
|
||||
## Step 7: transform backend data into rows
|
||||
|
||||
Inside `jsonDownload.ts`, the composable:
|
||||
|
||||
- picks the correct endpoint
|
||||
- sends the request
|
||||
- receives backend offer data
|
||||
- converts it into table rows and display rows
|
||||
|
||||
So the data flow is:
|
||||
|
||||
```text
|
||||
Dropdown -> selectedPeriodStartFrom -> payload builder -> downloadFlexData -> tableData -> rendered table
|
||||
```
|
||||
|
||||
## Step 8: render the table
|
||||
|
||||
The table rows come from:
|
||||
|
||||
```ts
|
||||
const rows = computed<FlexDisplayRow[]>(() => tableData.value.displayRows ?? []);
|
||||
```
|
||||
|
||||
Then more filtering happens:
|
||||
|
||||
```ts
|
||||
const filteredRows = computed<FlexDisplayRow[]>(() =>
|
||||
rows.value.filter((row) => {
|
||||
const aggregator = row.senderId ?? row.aggregator;
|
||||
const controlArea = row.receiverId ?? row.controlArea;
|
||||
|
||||
if (selectedAggregators.value.length && (!aggregator || !selectedAggregators.value.includes(aggregator))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedControlArea.value && controlArea !== selectedControlArea.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
Notice something important:
|
||||
|
||||
- date filters affect what is fetched from the backend
|
||||
- aggregator and control area filters affect what is shown locally in the frontend
|
||||
|
||||
That is a useful architectural distinction.
|
||||
|
||||
## Step 9: exports use applied filters
|
||||
|
||||
For JSON export:
|
||||
|
||||
```ts
|
||||
const rawData = await downloadRawFlexData(buildAppliedDownloadPayload());
|
||||
```
|
||||
|
||||
That uses applied values, not selected values.
|
||||
|
||||
Why?
|
||||
Because if the user changed the dropdown but did not click Refresh yet, the table still reflects the old filters. Export should match what the user is actually seeing as the active state.
|
||||
|
||||
## Full mental model
|
||||
|
||||
Use this simple chain:
|
||||
|
||||
```text
|
||||
1. Options are created
|
||||
2. User picks a value
|
||||
3. selectedPeriodStartFrom changes
|
||||
4. Validation recomputes
|
||||
5. User clicks Refresh
|
||||
6. selected values become applied values
|
||||
7. payload is built
|
||||
8. backend data is fetched
|
||||
9. tableData updates
|
||||
10. table re-renders
|
||||
```
|
||||
|
||||
## Tiny simplified example
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<select v-model="selectedColor">
|
||||
<option value="red">Red</option>
|
||||
<option value="blue">Blue</option>
|
||||
</select>
|
||||
|
||||
<button @click="apply">Apply</button>
|
||||
|
||||
<p>Selected: {{ selectedColor }}</p>
|
||||
<p>Applied: {{ appliedColor }}</p>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const selectedColor = ref('red');
|
||||
const appliedColor = ref('red');
|
||||
|
||||
function apply() {
|
||||
appliedColor.value = selectedColor.value;
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
This is the same pattern as your date filters, just simpler.
|
||||
|
||||
## Key beginner lesson
|
||||
|
||||
When reading Vue, do not only ask "what is this variable".
|
||||
Ask:
|
||||
|
||||
- who writes it?
|
||||
- who reads it?
|
||||
- is it draft state or applied state?
|
||||
- does it affect frontend-only filtering or backend requests?
|
||||
|
||||
That is how the whole interaction becomes understandable.
|
||||
|
||||
## Related Notes
|
||||
|
||||
- [[00-Vue for Python Developers]]
|
||||
- [[01-Refs, Computed, and Reactivity]]
|
||||
- [[02-Templates, v-model, and Events]]
|
||||
- [[03-Components, Props, Lifecycle, and Composables]]
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
# Vue Learning Path
|
||||
|
||||
Start here if you are new to Vue and coming from Python.
|
||||
|
||||
## Suggested reading order
|
||||
|
||||
1. [[00-Vue for Python Developers]]
|
||||
2. [[01-Refs, Computed, and Reactivity]]
|
||||
3. [[02-Templates, v-model, and Events]]
|
||||
4. [[03-Components, Props, Lifecycle, and Composables]]
|
||||
5. [[04-FlexibilityTable Filter Flow]]
|
||||
|
||||
## What each note covers
|
||||
|
||||
### Vue for Python Developers
|
||||
|
||||
The big picture:
|
||||
|
||||
- what a Vue component is
|
||||
- template vs script
|
||||
- what reactive means
|
||||
- how to read Vue code without getting lost
|
||||
|
||||
### Refs, Computed, and Reactivity
|
||||
|
||||
State management basics:
|
||||
|
||||
- `ref`
|
||||
- `.value`
|
||||
- `computed`
|
||||
- when to store data vs derive data
|
||||
|
||||
### Templates, v-model, and Events
|
||||
|
||||
Template mechanics:
|
||||
|
||||
- interpolation with `{{ ... }}`
|
||||
- `v-model`
|
||||
- `@click`
|
||||
- `:disabled`
|
||||
- how template code connects to script code
|
||||
|
||||
### Components, Props, Lifecycle, and Composables
|
||||
|
||||
Code organization:
|
||||
|
||||
- parent and child components
|
||||
- props
|
||||
- `onMounted`
|
||||
- composables such as `useJsonDownload()`
|
||||
|
||||
### FlexibilityTable Filter Flow
|
||||
|
||||
Real project walkthrough:
|
||||
|
||||
- `selectedPeriodStartFrom`
|
||||
- selected vs applied filters
|
||||
- backend payload building
|
||||
- table refresh flow
|
||||
- export behavior
|
||||
|
||||
## Best way to study these notes
|
||||
|
||||
For each note:
|
||||
|
||||
1. Read the explanation slowly.
|
||||
2. Copy one code example into a small test component.
|
||||
3. Change one line and predict what the UI will do.
|
||||
4. Run it and compare expectation with result.
|
||||
|
||||
That loop is the fastest way to get comfortable with Vue.
|
||||
Executable
+358
@@ -0,0 +1,358 @@
|
||||
Hier ist eine umfassende Einführung in Argo CD – verständlich, aber fachlich fundiert.
|
||||
|
||||
---
|
||||
|
||||
## 1. Was ist Argo CD?
|
||||
|
||||
Argo CD ist ein **GitOps-Tool** für **Kubernetes**. Es sorgt dafür, dass der Zustand eines Kubernetes-Clusters immer mit dem übereinstimmt, was in einem **Git-Repository** als „Wahrheit“ definiert ist.
|
||||
|
||||
Kerneigenschaften:
|
||||
|
||||
- **Git-zentriert**: Git als Single Source of Truth (Manifest-Dateien, Helm-Charts, Kustomize, etc.)
|
||||
- **Deklarativ**: Der gewünschte Zustand (desired state) ist beschrieben, nicht programmiert
|
||||
- **Kontinuierliche Synchronisierung**: Änderungen in Git werden automatisch oder manuell im Cluster ausgerollt
|
||||
- **Visuell**: Web UI mit Übersicht über Applikationen, Health-Status, Diffs, History
|
||||
- **Mehrcluster-fähig**: Kann mehrere Kubernetes-Cluster verwalten
|
||||
|
||||
---
|
||||
|
||||
## 2. GitOps – der Kontext von Argo CD
|
||||
|
||||
Argo CD implementiert das GitOps-Konzept:
|
||||
|
||||
- **Infrastruktur & Deployments im Git** (Manifest-Dateien, Helm Values, Kustomize Overlays, etc.)
|
||||
- **Pull-basiert**: Argo CD „zieht“ den Zustand regelmäßig aus Git und gleicht ihn mit dem Cluster ab.
|
||||
- **Versioniert & auditiert**: Jede Änderung ist ein Git-Commit, inkl. History, Review, Rollback-Möglichkeit
|
||||
- **Automatisierte Drift-Erkennung**: Unterschiede zwischen Git und Cluster werden erkannt und gemeldet
|
||||
|
||||
Vorteile von GitOps mit Argo CD:
|
||||
|
||||
- Reproduzierbare Deployments
|
||||
- Besseres Change-Management (Git-Workflows, PRs)
|
||||
- Weniger manuelle „[[kubectl]] apply“-Befehle
|
||||
- Klare Trennung von Build (CI) und Deployment (CD)
|
||||
|
||||
---
|
||||
|
||||
## 3. Architektur von Argo CD
|
||||
|
||||
### 3.1 Hauptkomponenten
|
||||
|
||||
- **argocd-server**
|
||||
- Stellt die Web UI und das gRPC/REST-API bereit
|
||||
- Authentifizierung (lokale User, SSO via OIDC, etc.)
|
||||
- Kommunikation mit CLI (`argocd` CLI) und UI
|
||||
|
||||
- **argocd-repo-server**
|
||||
- Greift auf Git-Repositories zu
|
||||
- Rendert Manifeste (z.B. Helm, Kustomize, Jsonnet)
|
||||
- Führt Template-Engines und „Config Management Plugins“ aus
|
||||
|
||||
- **argocd-application-controller**
|
||||
- Herzstück des Sync-Mechanismus
|
||||
- Vergleicht desired state (Git) mit live state (Cluster)
|
||||
- Führt Syncs, Health-Checks und Rollbacks aus
|
||||
|
||||
- **argocd-redis** (optional, in vielen Installationen vorhanden)
|
||||
- Cache für Repositories und Application-Informationen
|
||||
|
||||
Diese Komponenten laufen typischerweise im Namespace `argocd` im Cluster.
|
||||
|
||||
### 3.2 Target Cluster
|
||||
|
||||
Argo CD kann:
|
||||
|
||||
- Den **Cluster, in dem es selbst läuft**, verwalten
|
||||
- **Externe Cluster** (z.B. Staging, Prod) als zusätzliche „Cluster Secrets“ anbinden
|
||||
|
||||
---
|
||||
|
||||
## 4. Zentrale Konzepte
|
||||
|
||||
### 4.1 Application
|
||||
|
||||
Die zentrale Ressource in Argo CD ist eine CRD namens **Application** (`kind: Application`).
|
||||
|
||||
Eine Application beschreibt:
|
||||
|
||||
- **Quelle (source)**: Woher kommen die Manifeste?
|
||||
- Git-Repository / Helm-Repo / Kustomize-Ordner
|
||||
- Pfad im Repo
|
||||
- ggf. Helm-Chart und Values
|
||||
- **Ziel (destination)**:
|
||||
- Zu verwaltender Kubernetes-Cluster
|
||||
- Ziel-Namespace
|
||||
- **Sync-Strategie & -Optionen**:
|
||||
- Automatische Synchronisierung (auto sync) oder manuell
|
||||
- Verhalten bei Fehlern, Prune, Self-Heal etc.
|
||||
|
||||
Ein minimaler `Application`-Beispiel (vereinfachtes YAML):
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: my-app
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: https://github.com/mein-org/mein-repo.git
|
||||
targetRevision: main
|
||||
path: kubernetes/my-app
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: my-app
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
```
|
||||
|
||||
### 4.2 Sync-Status & Health-Status
|
||||
|
||||
Für jede Application zeigt Argo CD zwei wesentliche Zustände:
|
||||
|
||||
- **Sync-Status**:
|
||||
- `Synced`: Cluster-Zustand entspricht Git-Zustand
|
||||
- `OutOfSync`: Unterschiede zwischen Git und Cluster
|
||||
- **Health-Status**:
|
||||
- `Healthy`: Anwendung läuft wie erwartet (z.B. alle Pods bereit)
|
||||
- `Degraded`: Fehlerzustände (CrashLoop, fehlende Ressourcen etc.)
|
||||
- `Progressing`: Deployment läuft noch, noch nicht stabil
|
||||
|
||||
Health-Checks sind ressourcentyp-spezifisch (Deployments, StatefulSets, CRDs etc.).
|
||||
|
||||
### 4.3 Drift-Erkennung
|
||||
|
||||
Argo CD vergleicht:
|
||||
|
||||
- **Desired state**: Manifeste, die aus Git gerendert wurden
|
||||
- **Live state**: Tatsächliche Ressourcen im Cluster
|
||||
|
||||
Wenn jemand manuell `kubectl apply` oder `kubectl edit` verwendet, entsteht „Drift“. Argo CD:
|
||||
|
||||
- Markiert den Status als `OutOfSync`
|
||||
- Kann bei `selfHeal: true` diese Änderungen automatisch wieder rückgängig machen (zurück auf Git-Zustand)
|
||||
|
||||
---
|
||||
|
||||
## 5. Unterstützte Werkzeuge & Formate
|
||||
|
||||
Argo CD unterstützt u.a.:
|
||||
|
||||
- **Plain YAML-Manifeste**
|
||||
- **Helm** (Charts, Values)
|
||||
- **Kustomize**
|
||||
- **Jsonnet**
|
||||
- **Ksonnet (legacy)**
|
||||
|
||||
Außerdem:
|
||||
|
||||
- **Config Management Plugins (CMP)**: Eigene Tools/Pipelines zum Manifest-Rendering integrierbar
|
||||
- **Multiple Sources**: Neuere Versionen unterstützen mehrere Quellen pro Application (z.B. App-Manifeste + separate Helm-Values)
|
||||
|
||||
---
|
||||
|
||||
## 6. Installation & erste Schritte
|
||||
|
||||
### 6.1 Installation
|
||||
|
||||
Typische Wege:
|
||||
|
||||
- **kubectl apply** eines offiziellen Install-Manifests:
|
||||
- z.B. `https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml`
|
||||
- **Helm-Chart** (empfohlen für produktivere Setups)
|
||||
- **Operator-Lösungen** in manchen Distributionen
|
||||
|
||||
Nach der Installation:
|
||||
|
||||
- Argo CD läuft im Namespace `argocd`
|
||||
- UI via `argocd-server` (LoadBalancer, Ingress oder Port-Forward)
|
||||
- Erstlogin meist über das Passwort aus dem `argocd-initial-admin-secret`
|
||||
|
||||
### 6.2 CLI
|
||||
|
||||
Das CLI-Tool `argocd` erlaubt:
|
||||
|
||||
- Login: `argocd login <server>`
|
||||
- Erstellung von Applications: `argocd app create ...`
|
||||
- Manuelles Syncen: `argocd app sync my-app`
|
||||
- Status anzeigen, Diffs, Rollbacks etc.
|
||||
|
||||
---
|
||||
|
||||
## 7. Deployment-Strategien & Sync-Policies
|
||||
|
||||
### 7.1 Manuelles vs. automatisches Syncen
|
||||
|
||||
- **Manuell**:
|
||||
- Änderungen in Git werden erkannt, aber nur als `OutOfSync` markiert
|
||||
- Deploy erfolgt erst nach `argocd app sync` (CLI/UI)
|
||||
- **Automatisch (auto sync)**:
|
||||
- Argo CD führt bei Änderungen in Git automatisch ein Sync aus
|
||||
- Option `prune` entfernt Ressourcen, die in Git gelöscht wurden
|
||||
- Option `selfHeal` korrigiert Drift im Cluster
|
||||
|
||||
### 7.2 Sync-Optionen
|
||||
|
||||
- **Sync-Wellen (Sync Waves)**:
|
||||
- Steuerung der Reihenfolge via `argocd.argoproj.io/sync-wave` Annotation
|
||||
- **Sync-Hooks**:
|
||||
- Vor-/Nachschaltbare Hooks (`PreSync`, `PostSync`, `SyncFail` etc.)
|
||||
- Realisiert mit bestimmten Kubernetes-Resourcen und Annotationen
|
||||
|
||||
---
|
||||
|
||||
## 8. Projekte (AppProjects) & Multi-Tenancy
|
||||
|
||||
Die CRD **AppProject** erlaubt die logische Gruppierung und Begrenzung von Applications.
|
||||
|
||||
Funktionen von AppProjects:
|
||||
|
||||
- Einschränkung von:
|
||||
- Erlaubten Git-Repositories
|
||||
- Erlaubten Ziel-Cluster / Namespaces
|
||||
- Erlaubten Ressourcentypen
|
||||
- Gemeinsame Policies & RBAC für eine Gruppe von Apps
|
||||
- Sinnvoll für **Multi-Tenancy**:
|
||||
- z.B. ein Project pro Team, Produkt oder Mandant
|
||||
|
||||
---
|
||||
|
||||
## 9. Sicherheit & RBAC
|
||||
|
||||
### 9.1 Authentifizierung
|
||||
|
||||
- Lokale Benutzer in Argo CD (Admin, weitere User)
|
||||
- Single Sign-On via:
|
||||
- OIDC (z.B. Keycloak, Dex, Azure AD, Okta)
|
||||
- SAML (über OIDC-Integrationen)
|
||||
- Tokens für Automatisierung (CI-Systeme, Bots)
|
||||
|
||||
### 9.2 Autorisierung (RBAC)
|
||||
|
||||
Argo CD hat ein eigenes RBAC-System:
|
||||
|
||||
- Rollen (z.B. `role:readonly`, `role:admin`, benutzerdefinierte Rollen)
|
||||
- Zuordnung von Rollen zu Subjekten (User, Gruppen)
|
||||
- Rechte auf:
|
||||
- Anwendungen (lesen, syncen, löschen)
|
||||
- Projekte
|
||||
- Cluster, Repos
|
||||
- Granulare Steuerung:
|
||||
- z.B. „Team A darf nur Apps in Project X syncen“
|
||||
|
||||
---
|
||||
|
||||
## 10. Typische Patterns: App-of-Apps & Monorepo/Multirepo
|
||||
|
||||
### 10.1 App-of-Apps Pattern
|
||||
|
||||
Ein häufiges Muster ist die **„Application of Applications“**:
|
||||
|
||||
- Eine „Root-Application“, deren Manifeste nur aus anderen `Application`-Ressourcen bestehen
|
||||
- Diese Root-App definiert alle anderen Applikationen/Umgebungen
|
||||
- Vorteile:
|
||||
- Zentrales Entry-Point
|
||||
- Environments (dev/stage/prod) als Under-Apps
|
||||
- Bessere Struktur bei vielen Services
|
||||
|
||||
### 10.2 Repository-Strukturen
|
||||
|
||||
- **Monorepo**:
|
||||
- Alle Manifeste aller Services und Environments in einem Git-Repo
|
||||
- **Multirepo**:
|
||||
- Pro Service oder Team ein eigenes Repo
|
||||
- Infrastruktur/Plattform in separaten Repos
|
||||
- Argo CD unterstützt beide Modelle – Entscheidung ist organisatorisch/architektonisch.
|
||||
|
||||
---
|
||||
|
||||
## 11. Integration in CI/CD-Pipelines
|
||||
|
||||
Argo CD übernimmt in einem klassischen Setup die **CD-Rolle**:
|
||||
|
||||
- CI-Pipeline (z.B. GitLab CI, GitHub Actions, Jenkins):
|
||||
- Buildet Container-Images
|
||||
- Pusht Images in ein Registry
|
||||
- Aktualisiert Versionsnummern/Tags in den Git-Manifesten (Deployment-Repo)
|
||||
- Argo CD:
|
||||
- Erkennt Änderungen im Git-Repo
|
||||
- Deployt diese in den Cluster
|
||||
|
||||
Dadurch entsteht eine klare Trennung:
|
||||
|
||||
- CI = Builds, Tests, Image-Erzeugung
|
||||
- CD = Versionswechsel im Git + Argo CD Sync
|
||||
|
||||
---
|
||||
|
||||
## 12. Monitoring & Observability
|
||||
|
||||
- Argo CD bietet:
|
||||
- Events & Logs (Application-Controller, Repo-Server etc.)
|
||||
- Metriken (Prometheus-Exporter) für:
|
||||
- Anzahl Apps, Sync-Status, Health
|
||||
- Dauer von Syncs
|
||||
- Fehler, Retries
|
||||
- Integration mit:
|
||||
- **Prometheus/Grafana** (Dashboards für Argo CD)
|
||||
- Alerting auf Drift, Degradation, Sync-Fehler
|
||||
|
||||
---
|
||||
|
||||
## 13. Best Practices
|
||||
|
||||
Einige bewährte Vorgehensweisen:
|
||||
|
||||
1. **Git als einzige Wahrheit**
|
||||
- Keine manuellen Änderungen via `kubectl` in produktiven Namespaces
|
||||
2. **Automatisches Syncen mit Self-Heal** (mindestens in lower Environments)
|
||||
3. **Trennung von Image-Build-Repo und Deployment-Repo**
|
||||
4. **AppProjects pro Team/Domain** zur Strukturierung und Begrenzung
|
||||
5. **RBAC sauber definieren**:
|
||||
- Admin-Rechte nur für wenige
|
||||
- Team-spezifische Rollen
|
||||
6. **Health-Konfigurationen pflegen**:
|
||||
- Custom Health Checks für eigene CRDs, falls nötig
|
||||
7. **Klare Namenskonventionen** für Applications, Projekte und Repos
|
||||
8. **App-of-Apps** für große Landschaften:
|
||||
- z.B. `cluster-bootstrap` Application, die alle anderen Apps initialisiert
|
||||
|
||||
---
|
||||
|
||||
## 14. Typische Fallstricke
|
||||
|
||||
- **Drift durch manuelle Änderungen**:
|
||||
- Ursache für OutOfSync und schwer zu reproduzierende Zustände
|
||||
- **Fehlerhafte oder komplexe Helm-/Kustomize-Setups**:
|
||||
- Render-Probleme im Repo-Server bei sehr komplexen Konfigurationen
|
||||
- **Unzureichende Rechte im Ziel-Cluster**:
|
||||
- Argo CD-Service-Account braucht passende RBAC-Rechte
|
||||
- **Netzwerk-/Firewall-Probleme**:
|
||||
- Argo CD muss das Git-Repo erreichen können (Proxy, SSL, SSH-Keys)
|
||||
- **Stateful-Migrationen**:
|
||||
- Vorsicht bei Datenbank-Migrationen im Sync-Prozess (Hook-Jobs, Migrationsphasen)
|
||||
|
||||
---
|
||||
|
||||
## 15. Argo CD vs. andere GitOps-Tools (z.B. Flux)
|
||||
|
||||
Kurzvergleich zu **Flux** (ein anderes populäres GitOps-Tool):
|
||||
|
||||
- **Argo CD**:
|
||||
- Sehr starke, moderne **UI**
|
||||
- Klare Application-Entität
|
||||
- Häufig intuitiv für Teams, die visuelle Steuerung wünschen
|
||||
- **Flux**:
|
||||
- Sehr „Kubernetes-native“, keine eigene zentrale UI (oft mit zusätzlicher UIs wie Weave GitOps)
|
||||
- Stärker über CRDs und CLI gesteuert, weniger „zentrales“ Dashboard
|
||||
|
||||
Beide implementieren GitOps; die Wahl hängt oft von:
|
||||
|
||||
- UI-Bedarf
|
||||
- Präferenzen des Teams
|
||||
- Vorhandenen Ökosystem-Komponenten
|
||||
|
||||
|
||||
Executable
+255
@@ -0,0 +1,255 @@
|
||||
Im IT-Kontext bedeutet **Ingress** allgemein:
|
||||
> „Eingehender Datenverkehr in ein System, Netzwerk oder einen Dienst.“
|
||||
|
||||
Je nach Kontext wird der Begriff etwas unterschiedlich verwendet:
|
||||
# Allgemein
|
||||
|
||||
### 1. Netzwerk / Security
|
||||
|
||||
In klassischen Netzwerken und Firewalls unterscheidet man:
|
||||
|
||||
- **Ingress-Traffic**: Datenverkehr, der **von außen nach innen** in ein Netzwerk oder System hineinkommt
|
||||
(z. B. HTTP-Anfragen aus dem Internet an Ihren Webserver).
|
||||
- **Egress-Traffic**: Datenverkehr, der **von innen nach außen** geht
|
||||
(z. B. Ihr Server ruft eine externe API im Internet auf).
|
||||
|
||||
Typische Themen rund um Ingress in diesem Kontext:
|
||||
- **Ingress-Filterung**: Regeln, die festlegen, welcher eingehende Traffic erlaubt ist (z. B. Firewall-Regeln).
|
||||
- **Ingress-Schutz**: Maßnahmen gegen Angriffe von außen (DDoS-Schutz, WAF, Rate Limiting).
|
||||
|
||||
### 2. Kubernetes (Spezialfall „Ingress“ Resource)
|
||||
|
||||
In Kubernetes ist **Ingress** ein eigener Begriff und bezeichnet eine **API-Ressource**, die regelt, wie externe Requests in den Cluster hinein zu den internen Services geleitet werden.
|
||||
|
||||
Kurz gesagt:
|
||||
- Ein **Ingress** ist eine Art **Reverse Proxy / Routing-Regel** auf Layer 7 (HTTP/HTTPS).
|
||||
- Er definiert z. B.:
|
||||
- Welche Domain (`host`) auf welchen Service zeigt
|
||||
- Welche Pfade (`/api`, `/app`) zu welchen Services geroutet werden
|
||||
- SSL/TLS-Termination (HTTPS)
|
||||
|
||||
Beispielhafte Ingress-Definition in Kubernetes:
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: my-app-ingress
|
||||
spec:
|
||||
rules:
|
||||
- host: myapp.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: my-api-service
|
||||
port:
|
||||
number: 80
|
||||
```
|
||||
|
||||
Hier sorgt der Ingress dafür, dass Anfragen an `https://myapp.example.com/api` in den Cluster gehen und beim Service `my-api-service` landen.
|
||||
|
||||
---
|
||||
|
||||
Im Kubernetes-Kontext ist „Ingress“ etwas spezieller als nur „eingehender Traffic“. Es gibt drei eng zusammenhängende Dinge:
|
||||
|
||||
1. **Ingress-Ressource** (YAML-Objekt, das Routing-Regeln beschreibt)
|
||||
2. **Ingress-Controller** (Software/Pod, der diese Regeln umsetzt)
|
||||
3. **(Optional) IngressClass** (welcher Controller für welches Ingress zuständig ist)
|
||||
|
||||
Ich gehe der Reihe nach durch.
|
||||
|
||||
---
|
||||
# Kubernetes
|
||||
## 1. Warum braucht man Ingress überhaupt?
|
||||
|
||||
Ohne Ingress hast du im Wesentlichen drei Möglichkeiten, Dienste nach außen verfügbar zu machen:
|
||||
|
||||
- `ClusterIP` (Standard): nur intern im Cluster erreichbar
|
||||
- `NodePort`: öffnet einen Port auf jedem Node; eher low-level / unkomfortabel
|
||||
- `LoadBalancer`: Cloud-Loadbalancer vor einem Service (oft pro Service ein eigener LB)
|
||||
|
||||
Nachteile:
|
||||
- Viele externe IPs / Loadbalancer, wenn du viele Services hast
|
||||
- Keine zentrale Layer‑7-Logik (z. B. Routing per Hostname/URL-Pfad, TLS-Termination)
|
||||
|
||||
**Ingress** löst genau das:
|
||||
|
||||
> Ein (oder wenige) externe Einstiegspunkte, die per Hostname und Pfad auf viele interne Services routen können, inkl. HTTPS/TLS, Auth, Rate-Limiting (je nach Controller).
|
||||
|
||||
---
|
||||
|
||||
## 2. Ingress-Ressource (das YAML-Objekt)
|
||||
|
||||
Die Ingress-Ressource beschreibt **Deklarativ**, wie Anfragen von außen zu Services im Cluster geroutet werden sollen:
|
||||
|
||||
- Auf Basis von **Hostname** (`host`, z. B. `api.example.com`)
|
||||
- Auf Basis von **Pfad** (`path`, z. B. `/api`, `/app`)
|
||||
- Verknüpft mit einem **Backend-Service** (`Service` + Port)
|
||||
- Optional: **TLS**-Konfiguration (Zertifikate)
|
||||
|
||||
Beispiel:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: my-app-ingress
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- myapp.example.com
|
||||
secretName: myapp-tls-secret
|
||||
rules:
|
||||
- host: myapp.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: my-api-service
|
||||
port:
|
||||
number: 80
|
||||
- path: /app
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: my-frontend-service
|
||||
port:
|
||||
number: 80
|
||||
```
|
||||
|
||||
Was hier passiert:
|
||||
|
||||
- Alle Anfragen an `https://myapp.example.com/api...` → Service `my-api-service`
|
||||
- Alle Anfragen an `https://myapp.example.com/app...` → Service `my-frontend-service`
|
||||
- TLS-Zertifikat liegt in `Secret myapp-tls-secret`
|
||||
- `ingressClassName: nginx` sagt: „Dieser Ingress ist für den Ingress-Controller mit Klasse `nginx` gedacht“
|
||||
|
||||
Wichtige Felder:
|
||||
|
||||
- `rules[].host`: auf welchen Host gilt die Regel?
|
||||
- `rules[].http.paths[].path`: URL-Pfad
|
||||
- `pathType`:
|
||||
- `Prefix` → „beginnt mit diesem Pfad“
|
||||
- `Exact` → „genau dieser Pfad“
|
||||
- (früher gab es noch `ImplementationSpecific`)
|
||||
- `backend.service.name` / `port`: zu welchem Service leiten?
|
||||
|
||||
---
|
||||
|
||||
## 3. Ingress-Controller (die „Engine“ dahinter)
|
||||
|
||||
Die YAML-Ressource alleine macht noch nichts. Es braucht einen **Ingress-Controller**:
|
||||
|
||||
- Läuft als Pod/Deployment im Cluster
|
||||
- Beobachtet (watch) Ingress-Ressourcen im API-Server
|
||||
- Übersetzt die Regeln in eine konkrete Konfiguration (z. B. [[Nginx]], Envoy, Traefik …)
|
||||
- Lauscht auf einem (oder mehreren) Node-Ports/LoadBalancer-IPs und routet Traffic in die passende Services
|
||||
|
||||
Typische Implementierungen:
|
||||
|
||||
- **[[Nginx]] Ingress Controller**
|
||||
- **Traefik**
|
||||
- **HAProxy Ingress**
|
||||
- **Envoy-basierte** Controller
|
||||
- Cloud-spezifische:
|
||||
- GCE/GLBC (GKE HTTP(S) Load Balancing)
|
||||
- AWS ALB Ingress Controller
|
||||
- Azure Application Gateway Ingress
|
||||
|
||||
Wichtig:
|
||||
Du kannst mehrere Ingress-Controller im Cluster haben, z. B. einen für öffentliche, einen für interne Services.
|
||||
|
||||
---
|
||||
|
||||
## 4. IngressClass (Zuordnung Controller ↔ Ingress)
|
||||
|
||||
Mit `IngressClass` kann man genau definieren, **welcher** Ingress-Controller für welche Ingress-Ressourcen zuständig ist.
|
||||
|
||||
Beispiel IngressClass:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: IngressClass
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
controller: k8s.io/ingress-nginx
|
||||
```
|
||||
|
||||
Im Ingress verknüpfst du das mit:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
# ...
|
||||
```
|
||||
|
||||
Somit:
|
||||
|
||||
- Nur der Ingress-Controller, der sich als `k8s.io/ingress-nginx` registriert, reagiert auf diesen Ingress.
|
||||
- Andere Controller ignorieren ihn.
|
||||
|
||||
Früher wurde das oft über **Annotations** geregelt (`kubernetes.io/ingress.class`), `ingressClassName` ist der neuere, bevorzugte Weg.
|
||||
|
||||
---
|
||||
|
||||
## 5. Request-Flow: Wie läuft eine Anfrage technisch?
|
||||
|
||||
Typischer Ablauf (z. B. mit [[Nginx]] Ingress Controller in einer Cloud):
|
||||
|
||||
1. Benutzer ruft im Browser `https://myapp.example.com/api/users` auf.
|
||||
2. DNS zeigt `myapp.example.com` auf die externe IP eines Cloud-Loadbalancers.
|
||||
3. Der Cloud-Loadbalancer leitet die Anfrage an einen Node/Port, auf dem der Ingress-Controller erreichbar ist.
|
||||
4. Der Ingress-Controller (Pod) erhält die HTTP(S)-Anfrage.
|
||||
5. Er vergleicht Host (`myapp.example.com`) und Pfad (`/api/users`) mit seinen Ingress-Regeln:
|
||||
- Host passt zu `myapp.example.com`
|
||||
- Pfad passt zum Prefix `/api`
|
||||
6. Er leitet die Anfrage intern an den Kubernetes-Service `my-api-service:80` weiter.
|
||||
7. Kubernetes-Service verteilt die Anfrage per kube-proxy/IPVS/iptables auf einen Pod (`my-api`-Pod).
|
||||
8. Antwort geht den Weg zurück und landet beim Benutzer.
|
||||
|
||||
---
|
||||
|
||||
## 6. Typische Features (Controller-abhängig)
|
||||
|
||||
Je nach Ingress-Controller gibt es zusätzliche Möglichkeiten, meist über **Annotations** oder spezifische CRDs:
|
||||
|
||||
- **TLS-Termination** und optional TLS-Passthrough
|
||||
- **HTTP → HTTPS Redirect**
|
||||
- **Path-Rewrites**, z. B. `/api` außen → `/` im Backend
|
||||
- **Authentifizierung** (z. B. OAuth2, Basic Auth)
|
||||
- **Rate Limiting**, IP-Whitelist/Blacklist
|
||||
- **Canary-Deployments / Traffic-Splitting**
|
||||
- **gRPC-Support**, WebSockets, etc.
|
||||
|
||||
Beispiel (nginx Spezifische Annotation für Redirect):
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Grenzen von Ingress & Gateway API
|
||||
|
||||
Die klassische Ingress-API ist bewusst relativ simpel (Host, Pfad, Backend, TLS).
|
||||
Für komplexere Szenarien (multi-tenant, umfangreiche Routing-Logik, mehrere Gateways) gibt es inzwischen die **Gateway API** (Ressourcen wie `Gateway`, `HTTPRoute` etc.), die Ingress langfristig ergänzen/teilweise ablösen soll.
|
||||
|
||||
Dennoch ist Ingress aktuell (2024) in vielen Clustern der Standardweg, HTTP/HTTPS-Traffic von außen hereinzubringen.
|
||||
|
||||
---
|
||||
|
||||
Wenn du möchtest, kann ich als Nächstes:
|
||||
|
||||
- ein konkretes Setup „von Null an“ durchgehen (z. B. Minikube + [[Nginx]] Ingress + Beispiel-App), oder
|
||||
- spezifisch auf Annotations/Features eines bestimmten Ingress-Controllers eingehen (etwa nginx oder Traefik).
|
||||
Executable
+427
@@ -0,0 +1,427 @@
|
||||
Ein JSON Web Token (JWT) wirkt am Anfang oft etwas „magisch“. Wenn man es einmal verstanden hat, ist es aber ein relativ simples Baustein-konzept für Authentifizierung und Autorisierung in modernen Web‑ und API‑Systemen.
|
||||
|
||||
Ich gehe Schritt für Schritt durch:
|
||||
|
||||
1. Grundidee: Was ist ein JWT?
|
||||
2. Aufbau eines JWT
|
||||
3. Abgrenzung zu verwandten Konzepten
|
||||
4. Welche Probleme werden mit JWT gelöst?
|
||||
5. Typische Anwendungsfälle (praxisnah erklärt)
|
||||
6. Herausforderungen und Fallstricke
|
||||
7. Kurz-Fazit
|
||||
|
||||
---
|
||||
|
||||
## 1. Grundidee: Was ist ein JWT?
|
||||
|
||||
**Definition (vereinfacht):**
|
||||
|
||||
Ein **JWT (JSON Web Token)** ist ein **kompakter, URL-tauglicher, digital signierter Datenblock**, der Informationen über einen Benutzer oder eine Aktion enthält (z.B. „Benutzer X ist eingeloggt und hat Rolle admin“).
|
||||
Diese Informationen sind im JSON-Format strukturiert und werden mit einem kryptografischen Verfahren signiert (und optional verschlüsselt).
|
||||
|
||||
Damit kann ein Server (oder mehrere Server) **prüfen**, ob das Token:
|
||||
- von einem vertrauenswürdigen Aussteller stammt,
|
||||
- nicht manipuliert wurde,
|
||||
- noch gültig ist (nicht abgelaufen).
|
||||
|
||||
Man kann sich einen JWT grob als „fälschungssichereren Ausweis“ für Benutzer (oder Clients) vorstellen, der vom Server ausgestellt wird und den der Client bei weiteren Anfragen vorzeigt.
|
||||
|
||||
---
|
||||
|
||||
## 2. Aufbau eines JWT
|
||||
|
||||
Ein JWT besteht aus **drei Teilen**, jeweils Base64URL-kodiert, durch Punkte getrennt:
|
||||
|
||||
```text
|
||||
HEADER.PAYLOAD.SIGNATURE
|
||||
```
|
||||
|
||||
Ein Beispiel (gekürzt, sieht in der Praxis ähnlich aus):
|
||||
|
||||
```text
|
||||
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
|
||||
.
|
||||
eyJzdWIiOiIxMjM0NTYiLCJlbWFpbCI6Im1heC5tdXN0ZXJAbWFpbC5jb20iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3MzAxNjkzNDN9
|
||||
.
|
||||
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
|
||||
```
|
||||
|
||||
### 2.1 Header
|
||||
|
||||
Der Header beschreibt u.a.:
|
||||
- welches Signaturverfahren genutzt wird,
|
||||
- dass es sich um ein JWT handelt.
|
||||
|
||||
Beispiel:
|
||||
|
||||
```json
|
||||
{
|
||||
"alg": "HS256", // Algorithmus: HMAC-SHA256
|
||||
"typ": "JWT"
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Payload (Claims)
|
||||
|
||||
Die Payload enthält die eigentlichen Informationen („Claims“).
|
||||
Beispiele:
|
||||
|
||||
```json
|
||||
{
|
||||
"sub": "123456", // subject: Benutzer-ID
|
||||
"email": "max.mustermann@example.com",
|
||||
"role": "admin",
|
||||
"iat": 1730169334, // issued at (Unix-Timestamp)
|
||||
"exp": 1730172934, // expiry: Ablaufzeitpunkt
|
||||
"iss": "https://auth.meine-app.de", // issuer
|
||||
"aud": "https://api.meine-app.de" // audience
|
||||
}
|
||||
```
|
||||
|
||||
Wichtige Standard-Claims (nicht verpflichtend, aber üblich):
|
||||
- `iss` – Issuer (Wer hat das Token ausgestellt?)
|
||||
- `sub` – Subject (Auf wen bezieht sich das Token? z.B. Benutzer-ID)
|
||||
- `aud` – Audience (Für welche Anwendung/Services ist das Token bestimmt?)
|
||||
- `exp` – Expiration Time (Wann läuft das Token ab?)
|
||||
- `iat` – Issued At (Wann wurde es ausgestellt?)
|
||||
- `nbf` – Not Before (Ab wann ist es gültig?)
|
||||
|
||||
Dazu kommen **anwendungsspezifische Claims**, z.B.:
|
||||
- `email`, `username`
|
||||
- `roles`: `["admin", "user"]`
|
||||
- `scope`: `"read:orders write:orders"`
|
||||
|
||||
### 2.3 Signature
|
||||
|
||||
Die Signatur wird aus dem Header + Payload + einem Geheimnis (oder Schlüssel) gebildet.
|
||||
Formel grob:
|
||||
|
||||
```text
|
||||
signature = Sign( base64Url(header) + "." + base64Url(payload), secret_or_private_key )
|
||||
```
|
||||
|
||||
Beim Prüfen:
|
||||
- rekonstruiert der Server diese Signatur aus Header+Payload,
|
||||
- vergleicht sie mit der Signatur im Token,
|
||||
- wenn sie übereinstimmen → Token wurde nicht manipuliert.
|
||||
|
||||
---
|
||||
|
||||
## 3. Abgrenzung zu verwandten Begriffen
|
||||
|
||||
### 3.1 JWT vs klassische Session-Cookies
|
||||
|
||||
**Klassische Session:**
|
||||
- Nach Login erzeugt der Server eine **Session-ID** und speichert alle Infos (Benutzer, Rollen usw.) **im Server-Speicher** (in Memory, Redis, DB).
|
||||
- Die Session-ID wird im Browser z.B. als Cookie gespeichert.
|
||||
- Bei jeder Anfrage sendet der Browser die Session-ID; Server schaut im eigenen Speicher nach, welche Daten dazu gehören.
|
||||
|
||||
**JWT:**
|
||||
- Nach Login erzeugt der Server ein **Token**, das die Infos (Benutzer, Rollen, Ablaufzeit) **direkt im Token** enthält.
|
||||
- Das Token wird typischerweise im Browser (Cookie oder Speicher der App) gehalten.
|
||||
- Bei jeder Anfrage schickt der Client das Token mit (z.B. via HTTP-Header `Authorization: Bearer <token>`).
|
||||
- Der Server liest und prüft das Token, braucht aber keinen Session-Speicher.
|
||||
|
||||
Kurz:
|
||||
|
||||
- Session: **zustandsbehaftet (stateful)** auf Server-Seite.
|
||||
- JWT: **zustandslos (stateless)** auf Server-Seite.
|
||||
|
||||
### 3.2 JWT vs OAuth2 / OpenID Connect
|
||||
|
||||
- **OAuth2** ist ein **Protokoll für Autorisierung** (wer darf was). Es definiert Flows, Rollen (Client, Resource Owner, Authorization Server, Resource Server) etc.
|
||||
- **OpenID Connect (OIDC)** baut auf OAuth2 auf und regelt **Authentifizierung** (wer bist du) inklusive ID-Token.
|
||||
|
||||
JWT ist hier eher ein **Datenformat**:
|
||||
- In OAuth2/OIDC werden oft **Access Tokens** und **ID Tokens** als JWT umgesetzt.
|
||||
- Aber OAuth2/OIDC kann theoretisch auch andere Tokenformate verwenden.
|
||||
|
||||
**Merksatz:**
|
||||
OAuth2 / OIDC = das Protokoll;
|
||||
JWT = ein mögliches Format für die Tokens.
|
||||
|
||||
### 3.3 JWT vs JWS / JWE
|
||||
|
||||
- **JWS (JSON Web Signature)**: Standard für signierte JSON-Daten.
|
||||
- **JWE (JSON Web Encryption)**: Standard für verschlüsselte JSON-Daten.
|
||||
|
||||
Ein **typisches JWT ist ein JWS**: signiert, aber nicht verschlüsselt.
|
||||
Es gibt aber auch **JWTs als JWE**: zusätzlich verschlüsselt (seltener in der Praxis).
|
||||
|
||||
Wichtig:
|
||||
**Signiert heißt: Integrität und Echtheit** (nicht manipuliert, vom richtigen Aussteller),
|
||||
**nicht automatisch: Geheimhaltung**.
|
||||
Payload ist bei normalen JWTs **lesbar**, auch wenn sie signiert sind.
|
||||
|
||||
### 3.4 JWT vs API-Schlüssel (API Key)
|
||||
|
||||
- Ein **API-Key** ist meist ein zufälliger String, der einen Client identifiziert.
|
||||
Logik: „Wer den Key kennt, darf die API nutzen.“
|
||||
- Ein **JWT** enthält strukturierte Informationen (Claims) und ist signiert, so dass man viele Details im Token selbst hat (Benutzer, Rollen, Ablauf).
|
||||
|
||||
JWT ist in der Regel **ausdrucksstärker** und besser integrierbar in komplexe Auth-Zusammenhänge; ein API-Key ist eher „simple Zugriffskarte“.
|
||||
|
||||
### 3.5 JWT vs SAML
|
||||
|
||||
- **SAML** ist ein älterer Standard für Single Sign-On (SSO), benutzt XML statt JSON.
|
||||
- JWT (bzw. OIDC mit JWT) ist moderner, leichtergewichtig, JSON-basiert.
|
||||
|
||||
Viele moderne Anwendungen bevorzugen OIDC + JWT statt SAML, vor allem im Web-/API-Bereich.
|
||||
|
||||
---
|
||||
|
||||
## 4. Welche Probleme werden mit JWT gelöst?
|
||||
|
||||
### 4.1 Skalierbare Authentifizierung in verteilten Systemen
|
||||
|
||||
Problem:
|
||||
- Klassische Sessions erfordern zentralen Server-Speicher (Session Store).
|
||||
- In modernen Microservice-Architekturen und skalierenden Web-Apps (viele Instanzen) ist das unpraktisch.
|
||||
|
||||
JWT-Lösung:
|
||||
- **Zustandslose Tokens**: Jeder Service kann das Token **selbst verifizieren**, ohne zentralen Session Store.
|
||||
- Mehrere Instanzen, mehrere Services, sogar andere Systeme (Partner-APIs) können mit dem gleichen Token arbeiten, solange sie den Signaturschlüssel kennen (oder das zugehörige Public Key).
|
||||
|
||||
### 4.2 Einfache Weitergabe von Benutzerinformationen
|
||||
|
||||
Problem:
|
||||
- Mehrere Services müssen wissen, wer der Benutzer ist und welche Rechte er hat.
|
||||
- Man möchte nicht jedes Mal eine zusätzliche Datenbankabfrage machen.
|
||||
|
||||
JWT-Lösung:
|
||||
- Claims im Token: `sub`, `email`, `roles`, `scope`, …
|
||||
- Jeder Service liest diese Claims aus dem Token und kann basierend darauf entscheiden.
|
||||
|
||||
### 4.3 Mobile / Single-Page-Applications (SPAs)
|
||||
|
||||
Problem:
|
||||
- SPAs (z.B. React, Angular, Vue) und Mobile-Apps sprechen meist direkt mit APIs.
|
||||
- Klassische serverseitige Sessions sind dafür unhandlich, weil es keinen klassischen Browser-Request/Response-Cookie-Workflow gibt oder dieser komplexer ist.
|
||||
|
||||
JWT-Lösung:
|
||||
- Client erhält nach Login ein **Access Token (JWT)**.
|
||||
- Bei jedem API-Request wird das JWT im `Authorization`-Header mitgeschickt.
|
||||
- APIs können unabhängig vom Frontend arbeiten (auch mehrere Frontends nutzen die gleiche API).
|
||||
|
||||
---
|
||||
|
||||
## 5. Praxisnahe Anwendungsbeispiele
|
||||
|
||||
### 5.1 Web-App / API-Login mit JWT
|
||||
|
||||
Ablauf:
|
||||
|
||||
1. Benutzer gibt E-Mail + Passwort ein und klickt auf „Login“.
|
||||
2. Frontend schickt diese Daten an `/auth/login` (Backend).
|
||||
3. Backend prüft:
|
||||
- Benutzer existiert?,
|
||||
- Passwort korrekt?
|
||||
4. Wenn ok:
|
||||
- Backend erstellt ein JWT:
|
||||
- `sub = Benutzer-ID`
|
||||
- `email = Benutzer-Email`
|
||||
- `role = "user"`
|
||||
- `exp = in 15 Minuten`
|
||||
- signiert das Token mit einem geheimen Schlüssel (z.B. HS256).
|
||||
5. Backend schickt das Token an das Frontend zurück.
|
||||
6. Frontend speichert das Token:
|
||||
- z.B. in einem **httpOnly Cookie** (empfohlen),
|
||||
- oder in einem sicheren Storage (abhängig vom Setup).
|
||||
7. Bei jeder API-Anfrage:
|
||||
- schickt das Frontend das Token mit, z.B.:
|
||||
|
||||
```http
|
||||
GET /api/orders
|
||||
Authorization: Bearer <JWT_HIER>
|
||||
```
|
||||
|
||||
8. Die API:
|
||||
- prüft die Signatur,
|
||||
- prüft `exp` (nicht abgelaufen?),
|
||||
- liest `sub`, `role`, `scope` aus,
|
||||
- entscheidet, ob Zugriff erlaubt ist.
|
||||
|
||||
### 5.2 Microservices hinter einem API Gateway
|
||||
|
||||
Stell dir eine Plattform mit mehreren Microservices vor:
|
||||
|
||||
- `User-Service`
|
||||
- `Order-Service`
|
||||
- `Billing-Service`
|
||||
|
||||
Es gibt einen **Auth-Service**, der JWTs ausstellt.
|
||||
Alle Services kennen den Public Key (bei asymmetrischer Signatur) oder den Secret Key (bei symmetrischer Signatur).
|
||||
|
||||
Ablauf:
|
||||
|
||||
- Benutzer loggt sich über Auth-Service ein → bekommt JWT.
|
||||
- Frontend ruft `Order-Service` auf mit `Authorization: Bearer <JWT>`.
|
||||
- `Order-Service` prüft JWT:
|
||||
- gültig?
|
||||
- `scope` beinhaltet `read:orders`?
|
||||
- Wenn ja → Bestellung anzeigen.
|
||||
|
||||
Vorteil:
|
||||
Jeder Service muss keinen eigenen Session-Store haben, sondern nur das JWT validieren.
|
||||
|
||||
### 5.3 E-Mail-Bestätigung / Passwort-Reset
|
||||
|
||||
JWTs eignen sich auch für **zeitlich begrenzte Links**:
|
||||
|
||||
Beispiel: Passwort-Reset:
|
||||
|
||||
1. Benutzer klickt „Passwort vergessen“.
|
||||
2. Backend erzeugt ein JWT mit Claims:
|
||||
- `sub = Benutzer-ID`
|
||||
- `exp = in 30 Minuten`
|
||||
- `purpose = "password_reset"`
|
||||
3. Backend verschickt E-Mail mit Link:
|
||||
|
||||
```text
|
||||
https://meine-app.de/reset-password?token=<JWT_HIER>
|
||||
```
|
||||
|
||||
4. Benutzer klickt den Link.
|
||||
5. Frontend schickt den Token an das Backend, z.B. POST `/auth/reset-password`.
|
||||
6. Backend prüft:
|
||||
- Signatur,
|
||||
- `exp` (noch gültig?),
|
||||
- `purpose == "password_reset"`.
|
||||
7. Wenn alles passt → Passwort darf geändert werden.
|
||||
|
||||
---
|
||||
|
||||
## 6. Herausforderungen und Fallstricke
|
||||
|
||||
JWTs lösen einige Probleme, bringen aber eigene Herausforderungen mit sich.
|
||||
|
||||
### 6.1 Sicherheit bei Signatur und Schlüssel
|
||||
|
||||
Wichtige Punkte:
|
||||
|
||||
- **Algorithmuswahl:**
|
||||
Typisch:
|
||||
- Symmetrisch: `HS256` (ein gemeinsames Secret für Signatur & Prüfung)
|
||||
- Asymmetrisch: `RS256` (privater Schlüssel signiert, öffentlicher Schlüssel prüft)
|
||||
- **Keys sicher verwalten:**
|
||||
- nicht im Code hardcoden,
|
||||
- in sicheren Secret-Stores halten,
|
||||
- Schlüssel regelmäßig rotieren.
|
||||
|
||||
Gefährliche Anti-Pattern:
|
||||
- `alg: none` akzeptieren (nie tun),
|
||||
- Server so konfigurieren, dass er das `alg` aus dem Token blind vertraut (statt fest vorzugeben, was erlaubt ist).
|
||||
|
||||
### 6.2 Token-Ablauf und „Logout“
|
||||
|
||||
JWT ist stateless – das macht „Logout“ tricky:
|
||||
|
||||
- Klassische Session:
|
||||
- Session im Server-Speicher löschen → Benutzer abgemeldet.
|
||||
- JWT:
|
||||
- Token ist selbstständig gültig, bis `exp` erreicht ist.
|
||||
- Serverseitig gibt es **keine zentrale Liste** aller aktiven JWTs.
|
||||
|
||||
Mögliche Lösungen:
|
||||
- Tokens **kurzlebig** machen, z.B. 5–15 Minuten.
|
||||
- Für längere Sessions:
|
||||
- **Access Token** kurzlebig,
|
||||
- **Refresh Token** längerlebig und serverseitig verwaltbar (z.B. in DB),
|
||||
- bei Logout → Refresh Token invalidieren, Access Token läuft automatisch bald ab.
|
||||
- Optional: Blacklists/Blocklists oder Token-Revocation-Mechanismen.
|
||||
|
||||
### 6.3 Wo speichert man JWT im Browser?
|
||||
|
||||
Varianten:
|
||||
- **httpOnly Cookie**:
|
||||
- Vorteil: nicht via JavaScript auslesbar → Schutz gegen XSS.
|
||||
- Nachteil: CSRF muss beachtet werden (CSRF-Protection implementieren).
|
||||
- **localStorage / sessionStorage**:
|
||||
- Vorteil: einfache Handhabung im JS-Code.
|
||||
- Nachteil: bei XSS kann ein Angreifer das Token auslesen → sehr riskant.
|
||||
|
||||
In der Praxis:
|
||||
- Oft Kombination aus httpOnly Cookies + CSRF-Schutz,
|
||||
- oder Speziallösungen mit sog. „Double Submit Cookies“, „SameSite“-Cookies etc.
|
||||
|
||||
### 6.4 Token sind nicht geheim (wenn nur signiert)
|
||||
|
||||
Wichtiger Punkt:
|
||||
- Ein **normaler signierter JWT ist nicht verschlüsselt**.
|
||||
- Jeder, der das Token sieht, kann die Payload (Claims) lesen, da sie Base64URL-kodiert ist (was keine Verschlüsselung ist).
|
||||
|
||||
Daraus folgt:
|
||||
- Keine hochsensiblen Daten in die Payload packen (z.B. Passwörter, Zahlungsdaten, interne Geheimnisse).
|
||||
- JWT nur über **HTTPS** übertragen, damit niemand auf dem Weg „mithören“ kann.
|
||||
- Wenn wirklich Geheimhaltung nötig ist → **JWE (verschlüsseltes JWT)** oder andere Verschlüsselungsmechanismen.
|
||||
|
||||
### 6.5 Token-Größe
|
||||
|
||||
Wenn man sehr viele Claims ins Token packt:
|
||||
- wird es groß,
|
||||
- verursacht mehr Traffic,
|
||||
- passt evtl. schlechter in Cookies oder Header.
|
||||
|
||||
Praktisch:
|
||||
Nur wirklich relevante Informationen ins JWT aufnehmen:
|
||||
- Benutzer-ID
|
||||
- Rollen, Rechte oder Scope
|
||||
- wenige zusätzliche Angaben
|
||||
|
||||
Den Rest kann man bei Bedarf aus der Datenbank abfragen.
|
||||
|
||||
### 6.6 Fehler bei der Prüfung des Tokens
|
||||
|
||||
Beim Validieren darf man nicht nur die Signatur prüfen, sondern auch:
|
||||
|
||||
- Ist das Token abgelaufen? (`exp`)
|
||||
- Ist das Token noch nicht gültig? (`nbf`, `iat`)
|
||||
- Stimmt `iss` (Issuer) mit dem erwarteten Aussteller überein?
|
||||
- Stimmt `aud` (Audience) mit der eigenen Anwendung/API überein?
|
||||
|
||||
Beispiel (Node.js mit `jsonwebtoken`):
|
||||
|
||||
```js
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const token = '...'; // vom Client erhalten
|
||||
const publicKey = '...'; // Public Key (bei RS256) oder Secret (bei HS256)
|
||||
|
||||
try {
|
||||
const payload = jwt.verify(token, publicKey, {
|
||||
algorithms: ['RS256'],
|
||||
issuer: 'https://auth.meine-app.de',
|
||||
audience: 'https://api.meine-app.de',
|
||||
});
|
||||
|
||||
console.log('Token gültig, Benutzer-ID:', payload.sub);
|
||||
} catch (err) {
|
||||
console.error('Ungültiges Token:', err.message);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Kurz-Fazit
|
||||
|
||||
- Ein **JWT** ist ein **signierter, kompakter Datenträger im JSON-Format**, der Claims über Benutzer/Clients enthält.
|
||||
- Er wird verwendet für:
|
||||
- Authentifizierung (z.B. „Benutzer X ist eingeloggt“),
|
||||
- Autorisierung (z.B. „Benutzer X hat die Rolle admin“),
|
||||
- zeitlich begrenzte Aktionen (Passwort-Reset, E-Mail-Bestätigung),
|
||||
- verteilte Systeme (Microservices), die ohne zentralen Session-Speicher auskommen sollen.
|
||||
- JWTs sind besonders sinnvoll in:
|
||||
- Web-APIs,
|
||||
- SPAs,
|
||||
- mobilen Apps,
|
||||
- Microservice- und Cloud-Architekturen.
|
||||
- Abgrenzung:
|
||||
- JWT ≠ OAuth2/OIDC (Protokoll vs. Tokenformat),
|
||||
- JWT ≠ Session-Cookie (zustandslos vs. zustandsbehaftet),
|
||||
- JWTs sind meist JWS (signiert, aber nicht verschlüsselt).
|
||||
- Herausforderungen:
|
||||
- Sichere Schlüsselverwaltung, korrekte Token-Prüfung,
|
||||
- Umgang mit Logout/Token-Invalidierung,
|
||||
- sicherer Speicherort im Client (XSS/CSRF),
|
||||
- kein Hineinpacken hochsensibler Daten.
|
||||
|
||||
Executable
+512
@@ -0,0 +1,512 @@
|
||||
## Inhaltsverzeichnis
|
||||
- [Grundkonzepte](#grundkonzepte)
|
||||
- [Schritt 1: Welche Endpoints gibt es?](#schritt-1-welche-endpoints-gibt-es)
|
||||
- [Schritt 2: Endpoint-Konfiguration prüfen](#schritt-2-endpoint-konfiguration-prüfen)
|
||||
- [Schritt 3: Endpoint erreichbar machen](#schritt-3-endpoint-erreichbar-machen)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Grundkonzepte
|
||||
|
||||
### Was ist ein Endpoint?
|
||||
Ein **Endpoint** ist eine URL, über die Sie auf eine Anwendung in Kubernetes zugreifen können (z.B. `https://jupyterhub.example.com` oder `http://localhost:8080`).
|
||||
|
||||
### Wichtige Kubernetes-Komponenten
|
||||
|
||||
#### 1. Pod
|
||||
- Ein **Pod** ist die kleinste Einheit in Kubernetes
|
||||
- Enthält einen oder mehrere Container (z.B. Ihre Anwendung)
|
||||
- Hat eine interne IP-Adresse im Cluster
|
||||
- Befehl: `kubectl get pods -n <namespace>`
|
||||
|
||||
#### 2. Service
|
||||
- Ein **Service** ist eine stabile Zugangsadresse zu einem oder mehreren Pods
|
||||
- Macht Pods innerhalb des Clusters erreichbar
|
||||
- Typen:
|
||||
- `ClusterIP`: Nur innerhalb des Clusters erreichbar
|
||||
- `NodePort`: Von außen über Port am Node erreichbar
|
||||
- `LoadBalancer`: Erstellt einen externen Load Balancer (z.B. bei AWS)
|
||||
- Befehl: `kubectl get svc -n <namespace>`
|
||||
|
||||
#### 3. Ingress
|
||||
- Ein **Ingress** ist die "Haustür" zu Ihren Services von außerhalb des Clusters
|
||||
- Ermöglicht HTTP/HTTPS-Zugriff über Domainnamen (z.B. `app.example.com`)
|
||||
- Benötigt einen Ingress Controller (z.B. [[Nginx]])
|
||||
- Befehl: `kubectl get ingress -n <namespace>`
|
||||
|
||||
#### 4. Namespace
|
||||
- Ein **Namespace** ist wie ein Ordner in Kubernetes
|
||||
- Trennt verschiedene Anwendungen oder Teams
|
||||
- Beispiele: `openmetadata`, `jupyterhub`, `default`
|
||||
|
||||
---
|
||||
|
||||
## Schritt 1: Welche Endpoints gibt es?
|
||||
|
||||
### 1.1 Alle Namespaces auflisten
|
||||
|
||||
```bash
|
||||
kubectl get namespaces
|
||||
```
|
||||
|
||||
**Erklärung:** Zeigt alle Namespaces in Ihrem Cluster. Hier finden Sie, wo Ihre Anwendungen laufen.
|
||||
|
||||
**Beispiel-Output:**
|
||||
```
|
||||
NAME STATUS AGE
|
||||
default Active 30d
|
||||
jupyterhub Active 20d
|
||||
openmetadata Active 5d
|
||||
argocd Active 25d
|
||||
```
|
||||
|
||||
### 1.2 Anwendungen in einem Namespace finden
|
||||
|
||||
```bash
|
||||
# Pods (laufende Anwendungen)
|
||||
kubectl get pods -n openmetadata
|
||||
|
||||
# Services (Zugriffspunkte)
|
||||
kubectl get svc -n openmetadata
|
||||
|
||||
# Ingress (externe Zugänge)
|
||||
kubectl get ingress -n openmetadata
|
||||
```
|
||||
|
||||
**Wichtig:** Ersetzen Sie `openmetadata` mit Ihrem Namespace!
|
||||
|
||||
**Beispiel-Output:**
|
||||
```bash
|
||||
# Pods
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
openmetadata-559bf987f6-gxkdk 1/1 Running 0 4d20h
|
||||
|
||||
# Services
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
openmetadata ClusterIP 172.20.25.201 <none> 8585/TCP,8586/TCP 4d20h
|
||||
|
||||
# Ingress
|
||||
NAME CLASS HOSTS ADDRESS PORTS AGE
|
||||
openmetadata nginx openmetadata.dma.aai-dfine.de xxx.elb.amazonaws.com 80, 443 4d20h
|
||||
```
|
||||
|
||||
### 1.3 Alle Ingress-Endpoints im Cluster
|
||||
|
||||
```bash
|
||||
kubectl get ingress --all-namespaces
|
||||
```
|
||||
|
||||
**Erklärung:** Zeigt ALLE konfigurierten externen Zugänge über alle Namespaces hinweg.
|
||||
|
||||
---
|
||||
|
||||
## Schritt 2: Endpoint-Konfiguration prüfen
|
||||
|
||||
### 2.1 Detaillierte Ingress-Informationen
|
||||
|
||||
```bash
|
||||
kubectl describe ingress <ingress-name> -n <namespace>
|
||||
```
|
||||
|
||||
**Beispiel:**
|
||||
```bash
|
||||
kubectl describe ingress openmetadata -n openmetadata
|
||||
```
|
||||
|
||||
**Was Sie hier sehen:**
|
||||
- **Hosts:** Der Domainname (z.B. `openmetadata.dma.aai-dfine.de`)
|
||||
- **TLS:** Ob HTTPS konfiguriert ist und welches Zertifikat verwendet wird
|
||||
- **Backends:** Zu welchem Service und Port der Ingress weiterleitet
|
||||
- **Address:** Der tatsächliche Load Balancer
|
||||
|
||||
**Beispiel-Output:**
|
||||
```
|
||||
Name: openmetadata
|
||||
Namespace: openmetadata
|
||||
Address: aa83990c7ad3548c4b0e28680b0952ba-1924426268.eu-central-1.elb.amazonaws.com
|
||||
Ingress Class: nginx
|
||||
TLS:
|
||||
aai-dfine-certificate-tls terminates openmetadata.dma.aai-dfine.de
|
||||
Rules:
|
||||
Host Path Backends
|
||||
---- ---- --------
|
||||
openmetadata.dma.aai-dfine.de / openmetadata:8585 (10.10.1.86:8585)
|
||||
```
|
||||
|
||||
### 2.2 Pod-Status überprüfen
|
||||
|
||||
```bash
|
||||
kubectl get pods -n <namespace>
|
||||
```
|
||||
|
||||
**Status-Bedeutungen:**
|
||||
- `Running`: ✅ Pod läuft normal
|
||||
- `Pending`: ⏳ Pod startet gerade
|
||||
- `CrashLoopBackOff`: ❌ Pod startet immer wieder neu (Fehler!)
|
||||
- `Error`: ❌ Pod hat einen Fehler
|
||||
|
||||
### 2.3 Service-Details
|
||||
|
||||
```bash
|
||||
kubectl get svc <service-name> -n <namespace> -o wide
|
||||
```
|
||||
|
||||
**Zeigt:** ClusterIP, Ports, Selectors (welche Pods werden angesprochen)
|
||||
|
||||
---
|
||||
|
||||
## Schritt 3: Endpoint erreichbar machen
|
||||
|
||||
Es gibt **drei Methoden**, um auf einen Kubernetes-Endpoint zuzugreifen:
|
||||
|
||||
### Methode 1: Port-Forwarding (Empfohlen für Tests) ⭐
|
||||
|
||||
**Wann nutzen?**
|
||||
- Schneller Zugriff für Tests
|
||||
- DNS ist nicht konfiguriert
|
||||
- Lokale Entwicklung
|
||||
|
||||
**Vorteile:**
|
||||
- ✅ Keine Cluster-Änderungen
|
||||
- ✅ Sicher (nur für Sie erreichbar)
|
||||
- ✅ Sofort verfügbar
|
||||
- ✅ Keine DNS-Konfiguration nötig
|
||||
|
||||
**Nachteile:**
|
||||
- ❌ Terminal muss offen bleiben
|
||||
- ❌ Nur von Ihrem PC erreichbar
|
||||
|
||||
**Befehl:**
|
||||
```bash
|
||||
kubectl port-forward -n <namespace> svc/<service-name> <lokaler-port>:<service-port>
|
||||
```
|
||||
|
||||
**Beispiel für OpenMetadata:**
|
||||
```bash
|
||||
kubectl port-forward -n openmetadata svc/openmetadata 8585:8585
|
||||
```
|
||||
|
||||
**Was passiert:**
|
||||
1. Kubernetes erstellt einen Tunnel von Ihrem PC zum Service
|
||||
2. Sie können nun auf `http://localhost:8585` zugreifen
|
||||
3. Der Tunnel leitet alle Anfragen an den Service im Cluster weiter
|
||||
|
||||
**Zugriff:**
|
||||
```
|
||||
http://localhost:8585
|
||||
```
|
||||
|
||||
**Beenden:** `Ctrl + C` im Terminal
|
||||
|
||||
**Tipp:** Im Hintergrund laufen lassen:
|
||||
```bash
|
||||
# Windows PowerShell
|
||||
Start-Process kubectl -ArgumentList "port-forward -n openmetadata svc/openmetadata 8585:8585" -WindowStyle Hidden
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Methode 2: Über Ingress mit DNS (Produktiv-Umgebung)
|
||||
|
||||
**Wann nutzen?**
|
||||
- Produktiv-Zugriff
|
||||
- Mehrere Nutzer
|
||||
- HTTPS/TLS erforderlich
|
||||
|
||||
**Voraussetzungen:**
|
||||
1. ✅ Ingress ist konfiguriert (siehe Schritt 2.1)
|
||||
2. ✅ DNS-Eintrag existiert
|
||||
3. ✅ TLS-Zertifikat ist vorhanden
|
||||
|
||||
#### 2.1 DNS-Auflösung testen
|
||||
|
||||
```bash
|
||||
# Windows PowerShell
|
||||
nslookup openmetadata.dma.aai-dfine.de
|
||||
|
||||
# Alternative
|
||||
Resolve-DnsName openmetadata.dma.aai-dfine.de
|
||||
```
|
||||
|
||||
**Wenn DNS funktioniert:**
|
||||
```
|
||||
Server: dns.google
|
||||
Address: 8.8.8.8
|
||||
|
||||
Name: openmetadata.dma.aai-dfine.de
|
||||
Address: 54.xxx.xxx.xxx
|
||||
```
|
||||
|
||||
**Wenn DNS NICHT funktioniert:**
|
||||
```
|
||||
*** dns.google kann openmetadata.dma.aai-dfine.de nicht finden: Non-existent domain
|
||||
```
|
||||
|
||||
#### 2.2 Problem: DNS funktioniert nicht
|
||||
|
||||
**Lösung A: Lokale Hosts-Datei (temporär für Tests)**
|
||||
|
||||
1. **Load Balancer IP/Hostname finden:**
|
||||
```bash
|
||||
kubectl get ingress <ingress-name> -n <namespace>
|
||||
```
|
||||
|
||||
Notieren Sie die `ADDRESS` (z.B. `xxx.elb.amazonaws.com`)
|
||||
|
||||
2. **Load Balancer IP auflösen:**
|
||||
```bash
|
||||
nslookup xxx.elb.amazonaws.com
|
||||
```
|
||||
|
||||
3. **Windows Hosts-Datei bearbeiten:**
|
||||
- Datei öffnen als Administrator: `C:\Windows\System32\drivers\etc\hosts`
|
||||
- Zeile hinzufügen:
|
||||
```
|
||||
<IP-Adresse> openmetadata.dma.aai-dfine.de
|
||||
```
|
||||
- Speichern
|
||||
|
||||
4. **Im Browser öffnen:**
|
||||
```
|
||||
https://openmetadata.dma.aai-dfine.de
|
||||
```
|
||||
|
||||
**Lösung B: DNS-Eintrag erstellen lassen (dauerhaft)**
|
||||
|
||||
Jemand mit AWS Route53-Zugriff muss einen DNS-Eintrag erstellen:
|
||||
- **Typ:** CNAME oder A-Record
|
||||
- **Name:** `openmetadata.dma.aai-dfine.de`
|
||||
- **Ziel:** Load Balancer Hostname/IP
|
||||
|
||||
---
|
||||
|
||||
### Methode 3: Direkt zum Load Balancer (ohne DNS)
|
||||
|
||||
**Wenn vorhanden:**
|
||||
|
||||
1. **Load Balancer URL finden:**
|
||||
```bash
|
||||
kubectl get ingress <ingress-name> -n <namespace>
|
||||
```
|
||||
|
||||
2. **Direkt mit curl testen:**
|
||||
```bash
|
||||
curl -H "Host: openmetadata.dma.aai-dfine.de" http://<load-balancer-url>
|
||||
```
|
||||
|
||||
**Hinweis:** Funktioniert nicht immer im Browser (Host-Header erforderlich)
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: "Hmmm... can't reach this page" / ERR_NAME_NOT_RESOLVED
|
||||
|
||||
**Ursache:** DNS-Name kann nicht aufgelöst werden
|
||||
|
||||
**Lösung:**
|
||||
1. DNS testen: `nslookup <hostname>`
|
||||
2. Falls DNS fehlt → **Port-Forwarding** verwenden (Methode 1)
|
||||
3. Oder: Hosts-Datei editieren (Methode 2, Lösung A)
|
||||
|
||||
---
|
||||
|
||||
### Problem: Port-Forwarding schlägt fehl
|
||||
|
||||
**Fehlermeldung:** `Unable to listen on port 8585`
|
||||
|
||||
**Ursache:** Port ist bereits belegt
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Anderen lokalen Port verwenden
|
||||
kubectl port-forward -n openmetadata svc/openmetadata 9999:8585
|
||||
|
||||
# Dann öffnen: http://localhost:9999
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Problem: Pod läuft nicht (Status: CrashLoopBackOff)
|
||||
|
||||
**Logs anschauen:**
|
||||
```bash
|
||||
kubectl logs <pod-name> -n <namespace>
|
||||
|
||||
# Letzte 100 Zeilen
|
||||
kubectl logs <pod-name> -n <namespace> --tail=100
|
||||
|
||||
# Vorheriger Container (wenn Pod neu gestartet wurde)
|
||||
kubectl logs <pod-name> -n <namespace> --previous
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Problem: 502 Bad Gateway / 503 Service Unavailable
|
||||
|
||||
**Ursache:** Service erreicht den Pod nicht
|
||||
|
||||
**Prüfen:**
|
||||
```bash
|
||||
# 1. Läuft der Pod?
|
||||
kubectl get pods -n <namespace>
|
||||
|
||||
# 2. Sind Endpoints vorhanden?
|
||||
kubectl get endpoints <service-name> -n <namespace>
|
||||
|
||||
# 3. Service-Details
|
||||
kubectl describe svc <service-name> -n <namespace>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Problem: Authentifizierung erforderlich
|
||||
|
||||
**Wenn Login-Seite erscheint:**
|
||||
|
||||
**Standard-Credentials suchen:**
|
||||
```bash
|
||||
# Secrets auflisten
|
||||
kubectl get secrets -n <namespace>
|
||||
|
||||
# Secret anschauen (Werte sind base64-kodiert)
|
||||
kubectl get secret <secret-name> -n <namespace> -o yaml
|
||||
|
||||
# Secret dekodieren
|
||||
kubectl get secret <secret-name> -n <namespace> -o jsonpath='{.data.password}' | base64 -d
|
||||
```
|
||||
|
||||
**Beispiel OpenMetadata:**
|
||||
- Standard-User: `admin`
|
||||
- Standard-Password: `admin`
|
||||
|
||||
---
|
||||
|
||||
## Nützliche Befehle - Cheat Sheet
|
||||
|
||||
### Übersicht verschaffen
|
||||
```bash
|
||||
# Alle Namespaces
|
||||
kubectl get namespaces
|
||||
|
||||
# Alles in einem Namespace
|
||||
kubectl get all -n <namespace>
|
||||
|
||||
# Alle Ingress-Endpoints
|
||||
kubectl get ingress --all-namespaces
|
||||
```
|
||||
|
||||
### Status prüfen
|
||||
```bash
|
||||
# Pods mit mehr Details
|
||||
kubectl get pods -n <namespace> -o wide
|
||||
|
||||
# Pod-Logs live verfolgen
|
||||
kubectl logs -f <pod-name> -n <namespace>
|
||||
|
||||
# In Pod einloggen (für Debugging)
|
||||
kubectl exec -it <pod-name> -n <namespace> -- /bin/bash
|
||||
```
|
||||
|
||||
### Konfiguration anschauen
|
||||
```bash
|
||||
# YAML eines Ingress
|
||||
kubectl get ingress <name> -n <namespace> -o yaml
|
||||
|
||||
# Service-Details
|
||||
kubectl describe svc <name> -n <namespace>
|
||||
|
||||
# ConfigMap auslesen
|
||||
kubectl get configmap <name> -n <namespace> -o yaml
|
||||
```
|
||||
|
||||
### Port-Forwarding
|
||||
```bash
|
||||
# Standard
|
||||
kubectl port-forward -n <namespace> svc/<service-name> <local-port>:<service-port>
|
||||
|
||||
# Zu einem Pod (falls Service nicht existiert)
|
||||
kubectl port-forward -n <namespace> <pod-name> <local-port>:<container-port>
|
||||
|
||||
# Alle Interfaces binden (auch im Netzwerk erreichbar)
|
||||
kubectl port-forward --address 0.0.0.0 -n <namespace> svc/<service> 8585:8585
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Für Tests und Entwicklung
|
||||
✅ **Port-Forwarding** verwenden
|
||||
- Schnell, sicher, keine Änderungen am Cluster
|
||||
|
||||
### Für Produktiv-Zugriff
|
||||
✅ **Ingress mit DNS** verwenden
|
||||
- Korrekte DNS-Einträge erstellen lassen
|
||||
- TLS/HTTPS konfigurieren
|
||||
- Basic Auth oder OAuth für Authentifizierung
|
||||
|
||||
### Sicherheit
|
||||
⚠️ **Nie direkt auf Pods zugreifen** in Produktion
|
||||
⚠️ **Immer über Services** routen
|
||||
⚠️ **Port-Forwarding nicht dauerhaft** laufen lassen
|
||||
|
||||
---
|
||||
|
||||
## Glossar
|
||||
|
||||
| Begriff | Erklärung |
|
||||
|---------|-----------|
|
||||
| **Cluster** | Die gesamte Kubernetes-Umgebung mit allen Maschinen |
|
||||
| **Node** | Eine einzelne Maschine (VM) im Cluster |
|
||||
| **Pod** | Kleinste Einheit, enthält Container mit Ihrer App |
|
||||
| **Service** | Stabiler Zugriffspunkt zu einem oder mehreren Pods |
|
||||
| **Ingress** | HTTP/HTTPS-Zugang von außen, wie ein Router |
|
||||
| **Namespace** | Logische Trennung/Ordner für Anwendungen |
|
||||
| **ConfigMap** | Konfigurationsdaten (nicht-sensitiv) |
|
||||
| **Secret** | Sensible Daten (Passwörter, Tokens) - base64-kodiert |
|
||||
| **Load Balancer** | Verteilt Traffic auf mehrere Pods (z.B. AWS ELB) |
|
||||
| **TLS/SSL** | Verschlüsselung für HTTPS |
|
||||
| **Port-Forward** | Tunnel von lokalem PC zu Kubernetes-Service |
|
||||
|
||||
---
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
### Quick Start: Auf Anwendung zugreifen
|
||||
|
||||
1. **Namespace finden:**
|
||||
```bash
|
||||
kubectl get namespaces
|
||||
```
|
||||
|
||||
2. **Ingress prüfen:**
|
||||
```bash
|
||||
kubectl get ingress -n <namespace>
|
||||
```
|
||||
|
||||
3. **Port-Forwarding starten:**
|
||||
```bash
|
||||
kubectl port-forward -n <namespace> svc/<service-name> 8585:8585
|
||||
```
|
||||
|
||||
4. **Im Browser öffnen:**
|
||||
```
|
||||
http://localhost:8585
|
||||
```
|
||||
|
||||
Das war's! 🎉
|
||||
|
||||
---
|
||||
|
||||
## Weitere Ressourcen
|
||||
|
||||
- [Offizielle Kubernetes Dokumentation](https://kubernetes.io/docs/)
|
||||
- [kubectl Cheat Sheet](https://kubernetes.io/docs/reference/kubectl/cheatsheet/)
|
||||
- [Kubernetes Port-Forwarding](https://kubernetes.io/docs/tasks/access-application-cluster/port-forward-access-application-cluster/)
|
||||
|
||||
---
|
||||
|
||||
*Erstellt am: {{ date:2026-02-10 }}*
|
||||
*Workspace: `gitops`*
|
||||
Executable
+299
@@ -0,0 +1,299 @@
|
||||
#IT #Container #Open-Source #Cloud
|
||||
|
||||
---
|
||||
|
||||
## 1. Was ist Kubernetes – in einem Satz?
|
||||
|
||||
Kubernetes ist ein Open-Source‑System zur **Orchestrierung von Containern** (z. B. Docker), das die **Bereitstellung, Skalierung und Verwaltung** von Anwendungen automatisiert.
|
||||
|
||||
- Entwickelt von Google, heute von der CNCF (Cloud Native Computing Foundation) verwaltet
|
||||
- Läuft in der Cloud (AWS, Azure, GCP), im Rechenzentrum (On‑Prem) oder lokal (z. B. mit kind, minikube)
|
||||
|
||||
---
|
||||
|
||||
## 2. Warum braucht man Kubernetes?
|
||||
|
||||
### 2.1 Ausgangslage ohne Kubernetes
|
||||
|
||||
Bei klassischen Server- oder Container-Setups müssen viele Dinge manuell oder mit Skripten gelöst werden:
|
||||
|
||||
- Container starten/stoppen
|
||||
- Lastverteilung zwischen Instanzen
|
||||
- Neustart fehlerhafter Instanzen
|
||||
- Rollout neuer Versionen
|
||||
- Skalierung bei Lastspitzen
|
||||
- Konfigurationsmanagement und Secrets
|
||||
- Self-Healing bei Fehlern
|
||||
|
||||
### 2.2 Ziele von Kubernetes
|
||||
|
||||
Kubernetes löst diese Probleme, indem es:
|
||||
|
||||
1. **Deklaratives Modell** nutzt
|
||||
- Du beschreibst den „Soll-Zustand“ (z. B. „es sollen 3 Instanzen meiner App laufen“).
|
||||
- Kubernetes sorgt im Hintergrund dafür, dass dieser Zustand erreicht und gehalten wird.
|
||||
|
||||
2. **Automatisierung** von:
|
||||
- Rollouts und Rollbacks
|
||||
- Skalierung (horizontaler Ausbau: mehr Instanzen)
|
||||
- Self-Healing (Pods werden bei Absturz neu gestartet)
|
||||
- Service Discovery & Load Balancing
|
||||
|
||||
3. **Abstraktion von Infrastruktur**:
|
||||
- Egal ob deine Knoten VMs in der Cloud oder bare-metal-Server sind – Kubernetes abstrahiert das weg.
|
||||
|
||||
---
|
||||
|
||||
## 3. Hohe Ebene: Aufbau eines Kubernetes-Clusters
|
||||
|
||||
Ein Kubernetes-Cluster besteht grob aus:
|
||||
|
||||
- **Control Plane** (Master-Komponenten)
|
||||
- **Worker Nodes** (wo die Container wirklich laufen)
|
||||
|
||||
### 3.1 Control Plane (Steuerungsebene)
|
||||
|
||||
Wichtige Komponenten:
|
||||
|
||||
- **kube-apiserver**
|
||||
- Zentraler Einstiegspunkt
|
||||
- Alle Befehle von `kubectl` und anderen Komponenten laufen über die API ([[kubectl]])
|
||||
|
||||
- **etcd**
|
||||
- Verteilter Key-Value-Store
|
||||
- Hält den gesamten Cluster-Zustand (z. B. welche Pods, Deployments, ConfigMaps etc.)
|
||||
|
||||
- **kube-scheduler**
|
||||
- Entscheidet, auf welchem Node neue Pods laufen sollen
|
||||
- Berücksichtigt Ressourcen (CPU, RAM), Constraints, Affinities etc.
|
||||
|
||||
- **kube-controller-manager**
|
||||
- Mehrere Controller (z. B. für Deployments, Nodes, Endpoints)
|
||||
- Stellt sicher, dass der Ist-Zustand dem Soll-Zustand entspricht („Reconciliation Loop“)
|
||||
|
||||
- **cloud-controller-manager** (optional)
|
||||
- Integration mit Cloud-Anbietern (z. B. Load Balancer in AWS/Azure/GCP anlegen)
|
||||
|
||||
### 3.2 Worker Nodes (Arbeitsebene)
|
||||
|
||||
Auf jedem Worker laufen:
|
||||
|
||||
- **kubelet**
|
||||
- Agent auf dem Node
|
||||
- Kommuniziert mit der API und sorgt lokal dafür, dass die Pods wie gewünscht laufen
|
||||
|
||||
- **Container Runtime**
|
||||
- Führt Container aus: z. B. containerd, CRI‑O, Docker (früher direkt, heute über CRI)
|
||||
|
||||
- **kube-proxy**
|
||||
- Implementiert das Kubernetes-Service-Netzwerk
|
||||
- Kümmert sich um Weiterleitung von Netzwerkpaketen zu den richtigen Pods
|
||||
|
||||
---
|
||||
|
||||
## 4. Zentrale Konzepte in Kubernetes
|
||||
|
||||
### 4.1 Pod
|
||||
|
||||
- Kleinste deploybare Einheit in Kubernetes
|
||||
- Ein Pod enthält einen oder mehrere Container, die:
|
||||
- sich IP-Adresse und Ports teilen
|
||||
- sich lokale Volumes teilen
|
||||
- Typischerweise: 1 Container pro Pod (plus evtl. Sidecars)
|
||||
|
||||
Beispiel: Ein Pod mit einem [[Nginx]] Container:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: my-nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- containerPort: 80
|
||||
```
|
||||
|
||||
### 4.2 Deployment
|
||||
|
||||
- Deklarative Beschreibung von Pods + Skalierung
|
||||
- Sagt z. B.: „Es sollen 3 identische Replikate dieses Pods laufen“
|
||||
- Unterstützt Rollouts und Rollbacks (z. B. `kubectl rollout undo`)
|
||||
|
||||
Beispiel-Deployment:
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: my-nginx-deployment
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: my-nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: my-nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.27
|
||||
ports:
|
||||
- containerPort: 80
|
||||
```
|
||||
|
||||
### 4.3 Service
|
||||
|
||||
- Stabile Netzwerk-Adresse (DNS-Name, virtuelle IP) für eine Gruppe von Pods
|
||||
- Entkoppelt Clients von einzelnen Pod-IP-Adressen
|
||||
- Typen:
|
||||
- **ClusterIP**: intern im Cluster
|
||||
- **NodePort**: nach außen freigegeben über einen Port jedes Nodes
|
||||
- **LoadBalancer**: Integration mit einem Cloud-Load-Balancer
|
||||
|
||||
Beispiel-Service:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: my-nginx-service
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: my-nginx
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
```
|
||||
|
||||
### 4.4 Namespace
|
||||
|
||||
- Logische Unterteilung innerhalb eines Clusters
|
||||
- Dient zur:
|
||||
- Trennung von Umgebungen (dev, test, prod)
|
||||
- Multi-Tenancy
|
||||
- Ressourcenkontrolle (ResourceQuotas, Limits)
|
||||
- Standard-Namespace: `default`, daneben z. B. `kube-system`, `kube-public`
|
||||
|
||||
### 4.5 ConfigMap & Secret
|
||||
|
||||
- **ConfigMap**: Konfiguration (nicht sensibel), z. B. Properties, ENV-Variablen
|
||||
- **Secret**: sensible Daten (Passwörter, Tokens, Zertifikate)
|
||||
- Werden in Pods als ENV oder als Files (Volumes) bereitgestellt
|
||||
|
||||
### 4.6 Volume & PersistentVolume
|
||||
|
||||
- **Volumes**: Daten, die nicht verschwinden, wenn ein Container neu startet
|
||||
- **PersistentVolume (PV)**: repräsentiert eine physische/Cloud-Speicher-Ressource
|
||||
- **PersistentVolumeClaim (PVC)**: „Anforderung“ eines Pods an Speicher (Größe, Klasse)
|
||||
|
||||
---
|
||||
|
||||
## 5. Wie arbeitet Kubernetes? – Das „Desired State“-Modell
|
||||
|
||||
1. Du definierst Ressourcen (YAML/JSON), z. B.:
|
||||
|
||||
- „Deployment mit 3 Replikas meiner App“
|
||||
- „Service, der auf Port 80 erreichbar ist“
|
||||
|
||||
2. Du übernimmt diese Definition per:
|
||||
|
||||
```bash
|
||||
kubectl apply -f deployment.yaml
|
||||
```
|
||||
|
||||
3. Die API speichert diese Definition in **etcd**.
|
||||
|
||||
4. Controller und Scheduler beobachten den Zustand und führen eine **Reconciliation Loop** aus:
|
||||
|
||||
- Ist-Zustand (z. B. 2 Pods laufen) vs. Soll-Zustand (3 Pods sollen laufen)
|
||||
- Kubernetes startet einen weiteren Pod
|
||||
- Fällt ein Pod aus → wird automatisch ersetzt
|
||||
|
||||
---
|
||||
|
||||
## 6. Typischer Entwickler-Workflow mit Kubernetes
|
||||
|
||||
1. **Container-Image bauen**
|
||||
- z. B. mit Docker oder Buildpacks
|
||||
|
||||
2. **Image registrieren**
|
||||
- In einer Container Registry (Docker Hub, GitHub Container Registry, ECR, GCR…)
|
||||
|
||||
3. **Kubernetes-Manifest(e) schreiben**
|
||||
- Deployment, Service, ConfigMap, Secret, etc.
|
||||
|
||||
4. **Deployen**
|
||||
- `kubectl apply -f .`
|
||||
- Oder über CI/CD-Pipelines (GitLab CI, GitHub Actions, Argo CD, Jenkins etc.)
|
||||
|
||||
5. **Monitoring & Logging**
|
||||
- Über Tools wie Prometheus, Grafana, ELK/EFK-Stack, Loki, etc.
|
||||
|
||||
---
|
||||
|
||||
## 7. Kubernetes im Ökosystem / Erweiterungen
|
||||
|
||||
Kubernetes ist eine Basis, auf der vieles aufbaut:
|
||||
|
||||
- **Helm**
|
||||
- „Package Manager“ für Kubernetes
|
||||
- Erleichtert das Installieren komplexer Anwendungen (Charts)
|
||||
|
||||
- **Ingress & Ingress Controller**
|
||||
- HTTP/HTTPS-Routing auf Services
|
||||
- Ermöglicht virtuelle Hosts, Pfad-basiertes Routing usw.
|
||||
|
||||
- **Service Mesh (z. B. Istio, Linkerd)**
|
||||
- Feinere Kontrolle über Traffic, Observability, Security (mTLS)
|
||||
|
||||
- **Operators**
|
||||
- Kubernetes-Erweiterungen, die komplexe Anwendungen als „Custom Resources“ managen (z. B. Datenbanken)
|
||||
|
||||
- **GitOps (z. B. Argo CD, Flux)**
|
||||
- Zustände des Clusters werden aus Git-Repositories synchronisiert
|
||||
|
||||
---
|
||||
|
||||
## 8. Vorteile von Kubernetes
|
||||
|
||||
- Hohe **Portabilität** über verschiedene Infrastrukturen
|
||||
- **Automatisierte Skalierung** und Self-Healing
|
||||
- **Standardisierte Schnittstellen** (API-first)
|
||||
- Große **Community** und reiches Ökosystem
|
||||
- Gut geeignet für **Microservices** und verteile Systeme
|
||||
|
||||
---
|
||||
|
||||
## 9. Herausforderungen und Komplexität
|
||||
|
||||
- **Steile Lernkurve**: viele Konzepte (Pods, Deployments, Services, Ingress, RBAC, Storage…)
|
||||
- **Betrieb** eines eigenen Clusters ist nicht trivial
|
||||
- Viele Unternehmen nutzen Managed Services: GKE, AKS, EKS, OpenShift, Rancher etc.
|
||||
- **Observability**: Logs, Metrics, Tracing sind komplexer als bei einer einzigen VM
|
||||
- **Sicherheit** (RBAC, NetworkPolicies, Secrets-Management) erfordert Planung
|
||||
|
||||
---
|
||||
|
||||
## 10. Wie kann man praktisch starten?
|
||||
|
||||
Konkrete Schritte, um anzufangen:
|
||||
|
||||
1. **Lokale Umgebung**:
|
||||
- `kind` (Kubernetes in Docker)
|
||||
- `minikube` oder `k3d`
|
||||
|
||||
2. **Übungen**:
|
||||
- Einen einfachen Webserver als Deployment & Service bereitstellen
|
||||
- Mit `kubectl get`, `describe`, `logs` spielen
|
||||
- Scaling testen: `kubectl scale deployment my-deployment --replicas=5`
|
||||
- Eine neue Version deployen und Rollback ausprobieren
|
||||
|
||||
3. **Doku & Lernressourcen**:
|
||||
- Offizielle Doku: https://kubernetes.io/docs/
|
||||
- Interaktive Tutorials: Katacoda-ähnliche Labs, Kubernetes Bootcamps, z. B. von Cloud-Anbietern
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
„Layer‑7‑Logik“ bezieht sich auf Entscheidungen/Routing auf Basis der **Anwendungsebene** (Layer 7 im OSI-Modell).
|
||||
|
||||
Kurz zum Kontext:
|
||||
|
||||
- **Layer 3 (Netzwerk)**: IP-Adressen
|
||||
- **Layer 4 (Transport)**: TCP/UDP, Ports (z. B. Port 80, 443)
|
||||
- **Layer 7 (Anwendung)**: HTTP, HTTPS, gRPC, SMTP etc. – also „Inhalt“ der Anfrage
|
||||
|
||||
**Layer‑4‑Routing**:
|
||||
Es wird nur nach IP + Port entschieden. Beispiel:
|
||||
- Alles an `10.0.0.5:80` geht zu Service A.
|
||||
Der Proxy/Loadbalancer „sieht“ nicht, ob das `/api` oder `/shop` ist.
|
||||
|
||||
**Layer‑7‑Logik**:
|
||||
Der Proxy versteht das Protokoll (z. B. HTTP) und kann anhand von Anwendungsdaten entscheiden:
|
||||
|
||||
- Hostname: `api.example.com` → Service A, `shop.example.com` → Service B
|
||||
- Pfad: `/api` → Backend 1, `/app` → Backend 2
|
||||
- HTTP-Header: bestimmter `User-Agent`, `X-Feature-Flag` → anderes Routing
|
||||
- Cookies / Session: z. B. „User ist in A/B-Test-Gruppe → anderes Backend“
|
||||
- Authentifizierung / [[JWT token]] prüfen, bevor weitergeleitet wird
|
||||
- TLS-Termination: HTTPS entschlüsseln, weiter innen nur HTTP sprechen
|
||||
|
||||
Im Kubernetes-Ingress-Kontext heißt „Layer‑7‑Logik“ also:
|
||||
Der Ingress-Controller trifft Routing- und Sicherheitsentscheidungen **auf Basis von HTTP(S)-Details** (Host, Pfad, Header, Cookies usw.), nicht nur auf Basis von IPs und Ports.
|
||||
Executable
+430
@@ -0,0 +1,430 @@
|
||||
## 1. Grundidee: Was ist nginx – gerade im Python‑Kontext?
|
||||
|
||||
nginx („engine x“) ist ein **Webserver** und **Reverse Proxy**, der:
|
||||
|
||||
- HTTP(S)-Anfragen aus dem Internet annimmt
|
||||
- statische Dateien (HTML, CSS, Bilder, JS) direkt ausliefert
|
||||
- Anfragen für **dynamische Inhalte** (z.B. deine Python‑App) an einen **Anwendungsserver** weiterleitet (z.B. Gunicorn, uWSGI, [[uvicorn]])
|
||||
|
||||
Im Python‑Kontext wird nginx fast immer als **„Vorderseite“ (Frontend) vor einer Python‑Webanwendung** eingesetzt.
|
||||
|
||||
Typischer Aufbau:
|
||||
|
||||
```text
|
||||
Browser des Nutzers -> nginx -> Gunicorn/uWSGI/uvicorn -> Python-App (Django, Flask, FastAPI, ...)
|
||||
(Port 80/443) (lokaler Port, z.B. 8000)
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 2. Einordnung in die Webwelt: Grundbegriffe
|
||||
|
||||
Damit nginx einzuordnen ist, müssen ein paar Begriffe klar sein:
|
||||
|
||||
### 2.1 Webserver
|
||||
|
||||
Ein **Webserver**:
|
||||
|
||||
- läuft auf einem Rechner (Server)
|
||||
- „lauscht“ auf Port 80 (HTTP) oder 443 (HTTPS)
|
||||
- nimmt HTTP-Anfragen entgegen und schickt HTTP-Antworten zurück
|
||||
|
||||
Beispiele:
|
||||
- **nginx**
|
||||
- **Apache HTTP Server**
|
||||
- (leichtere) Caddy, lighttpd
|
||||
|
||||
### 2.2 Application Server / WSGI-/ASGI-Server
|
||||
|
||||
Ein **Application Server** führt deine **Web-App** aus, also deinen Python-Code.
|
||||
|
||||
- Für klassische synchronen Python-Webapps: **WSGI**-Server
|
||||
- z.B. **[[Gunicorn]]**, **uWSGI**, mod_wsgi
|
||||
- Für moderne, asynchrone Apps: **ASGI**-Server
|
||||
- z.B. **[[uvicorn]]**, **[[hypercorn]]**, Daphne
|
||||
|
||||
Der App-Server:
|
||||
|
||||
- spricht mit deinem Python-Framework (Django, Flask, [[FastAPI]], etc.)
|
||||
- versteht Anfragen im WSGI/ASGI-Format
|
||||
- erzeugt dynamische Antworten (z.B. JSON, HTML aus Templates)
|
||||
|
||||
### 2.3 Python-Webframework (Django, Flask, [[FastAPI]] …)
|
||||
|
||||
Ein **Framework** ist deine Programmierbibliothek, mit der du die Web-App baust:
|
||||
|
||||
- Django: „Vollausstattung“ (ORM, Admin, Auth, etc.)
|
||||
- Flask: minimaler Kern, viel per Erweiterung
|
||||
- [[FastAPI]]: modern, async, stark bei APIs
|
||||
|
||||
**Das Framework ist nicht der Webserver.**
|
||||
Es definiert nur, wie dein Code auf Anfragen reagieren soll.
|
||||
|
||||
---
|
||||
|
||||
## 3. Abgrenzung: nginx vs. verwandte Begriffe
|
||||
|
||||
### 3.1 nginx vs. Apache
|
||||
|
||||
Beide sind **Webserver**, Unterschiede grob:
|
||||
|
||||
- **nginx**
|
||||
- schnell bei vielen gleichzeitigen Verbindungen
|
||||
- ressourcensparend (eventbasiert)
|
||||
- sehr gut als Reverse Proxy, Load Balancer, TLS-Terminator
|
||||
- **Apache**
|
||||
- älter, sehr weit verbreitet
|
||||
- modulbasiert, viel Legacy
|
||||
- kann PHP & Co. direkt ausführen (mod_php, etc.)
|
||||
|
||||
Im modernen Python-Umfeld: **nginx + Gunicorn/[[uvicorn]]** ist ein sehr gängiges Setup.
|
||||
|
||||
### 3.2 nginx vs. [[Gunicorn]]/uWSGI/[[uvicorn]]
|
||||
|
||||
- **nginx**: Webserver / Reverse Proxy
|
||||
- **[[Gunicorn]]/uWSGI/[[uvicorn]]**: Python-Application-Server
|
||||
|
||||
nginx:
|
||||
|
||||
- kümmert sich um HTTP(S), TLS, statische Dateien, Load Balancing
|
||||
- schickt Anfragen an den Application-Server weiter
|
||||
|
||||
[[Gunicorn]]/[[uvicorn]]:
|
||||
|
||||
- startet deine Python-App
|
||||
- verarbeitet die Anfragen logikseitig
|
||||
- rendert Templates, greift auf Datenbanken zu, etc.
|
||||
|
||||
### 3.3 nginx vs. Django/Flask/[[FastAPI]]
|
||||
|
||||
- nginx: Infrastruktur-Komponente (Server)
|
||||
- Django/Flask/[[FastAPI]]: Bibliotheken, mit denen du deinen Code schreibst
|
||||
|
||||
Eine typische Kombi:
|
||||
|
||||
```text
|
||||
nginx + Gunicorn + Django
|
||||
nginx + uvicorn + FastAPI
|
||||
nginx + uWSGI + Flask
|
||||
```
|
||||
|
||||
Jede Komponente hat ihren klaren Aufgabenbereich.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 4. Welche Probleme löst nginx im Python-Setup?
|
||||
|
||||
### 4.1 Sichere und stabile Auslieferung deiner Python-Webapp
|
||||
|
||||
Problem:
|
||||
Die eingebaute Entwicklungsserver in Django/Flask sind nur für **lokale Entwicklung** gedacht:
|
||||
|
||||
- wenig performant
|
||||
- kaum Sicherheitsfeatures
|
||||
- nicht für viele gleichzeitige Nutzer ausgelegt
|
||||
|
||||
Lösung:
|
||||
nginx + App-Server (Gunicorn/uvicorn):
|
||||
|
||||
- nginx nimmt alle Anfragen entgegen
|
||||
- Python-App läuft hinter nginx auf einem internen Port
|
||||
- Anwendung ist stabiler, sicherer, skalierbarer
|
||||
|
||||
### 4.2 Auslieferung statischer Dateien
|
||||
|
||||
Problem:
|
||||
Python-Frameworks sind nicht optimiert, um Millionen von statischen Dateien schnell auszuliefern.
|
||||
|
||||
Lösung:
|
||||
|
||||
- nginx kann statische Dateien extrem effizient ausliefern
|
||||
- Python-App wird entlastet
|
||||
|
||||
**Beispiel:** Django-Setup:
|
||||
|
||||
```text
|
||||
Browser -> nginx -> (statische Dateien: /static/) direkt von nginx
|
||||
-> (dynamische URLs: /api/, /admin/) an Gunicorn/Django
|
||||
```
|
||||
|
||||
### 4.3 HTTPS / TLS-Termination
|
||||
|
||||
Problem:
|
||||
Du willst deine Seite unter **https://deinedomain.de** mit Zertifikat ausliefern.
|
||||
Python-Application-Server kümmern sich eher nicht um Zertifikate.
|
||||
|
||||
Lösung:
|
||||
|
||||
- nginx bietet HTTPS/TLS an (inkl. Let’s Encrypt-Integration)
|
||||
- entschlüsselt eingehende Anfragen
|
||||
- leitet sie dann intern als HTTP an Gunicorn/uvicorn weiter
|
||||
|
||||
So muss sich deine Python-App nicht um Zertifikate kümmern.
|
||||
|
||||
### 4.4 Load Balancing (Lastverteilung)
|
||||
|
||||
Problem:
|
||||
Eine einzelne Instanz deiner Python-App reicht nicht bei hoher Last.
|
||||
|
||||
Lösung:
|
||||
|
||||
- nginx kann Anfragen auf mehrere App-Server verteilen:
|
||||
|
||||
```text
|
||||
Browser -> nginx -> Gunicorn Instanz 1
|
||||
-> Gunicorn Instanz 2
|
||||
-> Gunicorn Instanz 3
|
||||
```
|
||||
|
||||
Damit erreichst du höhere Verfügbarkeit und Kapazität.
|
||||
|
||||
### 4.5 Reverse Proxy / Schutz der App
|
||||
|
||||
Problem:
|
||||
Du willst deine App nicht direkt ins Internet hängen (Sicherheitsrisiko, private Ports).
|
||||
|
||||
Lösung:
|
||||
|
||||
- nginx steht „davor“ und leitet nur definierte Anfragen durch
|
||||
- interne Ports (z.B. 8000) sind von außen nicht erreichbar
|
||||
- nginx kann zusätzliche Security-Header, Ratelimits, IP-Filter etc. einsetzen
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 5. Herausforderungen bei der Arbeit mit nginx
|
||||
|
||||
### 5.1 Konfigurationssyntax & -struktur
|
||||
|
||||
- nginx wird über Textdateien konfiguriert (z.B. `/etc/nginx/nginx.conf` und `sites-available/...`)
|
||||
- die Syntax ist am Anfang ungewohnt:
|
||||
- `http`, `server`, `location`, `upstream`, `listen`, etc.
|
||||
- Fehler in der Konfiguration führen dazu, dass nginx nicht startet oder falsch weiterleitet
|
||||
|
||||
Praxis-Hinweise:
|
||||
|
||||
```bash
|
||||
sudo nginx -t # Konfiguration testen
|
||||
sudo systemctl reload nginx # Konfiguration neu laden
|
||||
```
|
||||
|
||||
### 5.2 Rechte & Pfade (Linux-Dateisystem)
|
||||
|
||||
Probleme:
|
||||
|
||||
- nginx läuft als bestimmter Benutzer (z.B. `www-data`)
|
||||
- Python-App läuft evtl. als anderer Benutzer (z.B. `myapp`)
|
||||
- Zugriffsrechte auf Dateien/Ordner (Logs, statische Dateien) müssen passen
|
||||
|
||||
Beispiel:
|
||||
Wenn statische Dateien unter `/var/www/myapp/static/` liegen, muss der nginx-User Lesezugriff haben.
|
||||
|
||||
### 5.3 Fehlersuche (Debugging)
|
||||
|
||||
- Fehler können an mehreren Stellen auftreten:
|
||||
- DNS / Domain falsch
|
||||
- nginx-Konfiguration fehlerhaft
|
||||
- App-Server (Gunicorn/uvicorn) läuft nicht
|
||||
- Python-App wirft Fehler
|
||||
|
||||
Praxis-Tipp:
|
||||
|
||||
- in nginx-Logs schauen:
|
||||
- `/var/log/nginx/access.log`
|
||||
- `/var/log/nginx/error.log`
|
||||
- in App-Logs schauen
|
||||
- Ports prüfen: `ss -tulpen` oder `netstat -tulpen`
|
||||
|
||||
### 5.4 Performance-Tuning
|
||||
|
||||
Fortgeschrittener, aber wichtig bei größerem Traffic:
|
||||
|
||||
- wie viele Worker-Prozesse nginx nutzt
|
||||
- Keep-Alive-Einstellungen
|
||||
- Buffergrößen
|
||||
- Cache-Konfiguration für statische Inhalte
|
||||
|
||||
Am Anfang reicht meist die Standardkonfiguration, später optimiert man bei Bedarf.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 6. Praxisnahe Beispiele
|
||||
|
||||
### 6.1 Minimalbeispiel: Flask-App hinter nginx + Gunicorn
|
||||
|
||||
**1. Eine einfache Flask-App (Python)**
|
||||
|
||||
Datei: `app.py`
|
||||
|
||||
```python
|
||||
from flask import Flask
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return "Hallo, Welt! Das kommt aus Flask hinter nginx."
|
||||
```
|
||||
|
||||
Start des App-Servers (Gunicorn):
|
||||
|
||||
```bash
|
||||
pip install flask gunicorn
|
||||
gunicorn -w 3 -b 127.0.0.1:8000 app:app
|
||||
# -w 3: 3 Worker
|
||||
# -b 127.0.0.1:8000: lauscht auf Port 8000 nur lokal
|
||||
```
|
||||
|
||||
**2. nginx-Konfiguration (vereinfacht)**
|
||||
|
||||
Datei: z.B. `/etc/nginx/sites-available/myflaskapp`:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.com; # deine Domain oder IP
|
||||
|
||||
# Statische Dateien (wenn du welche hast)
|
||||
# root /var/www/myflaskapp; # falls du HTML/Assets direkt ablegen willst
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8000; # Gunicorn
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Dann:
|
||||
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/myflaskapp /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
Ablauf:
|
||||
|
||||
1. Browser ruft `http://example.com/` auf
|
||||
2. nginx nimmt Anfrage auf Port 80 entgegen
|
||||
3. nginx leitet an `http://127.0.0.1:8000` weiter (Gunicorn/Flask)
|
||||
4. Flask antwortet mit „Hallo, Welt“
|
||||
5. nginx sendet diese Antwort an den Browser zurück
|
||||
|
||||
|
||||
### 6.2 Beispiel: Django mit statischen Dateien
|
||||
|
||||
**1. Django-Setup (stark vereinfacht)**
|
||||
|
||||
In `settings.py`:
|
||||
|
||||
```python
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = "/var/www/mydjangoproject/static/"
|
||||
```
|
||||
|
||||
Danach statische Dateien sammeln:
|
||||
|
||||
```bash
|
||||
python manage.py collectstatic
|
||||
```
|
||||
|
||||
**2. nginx-Konfiguration (Ausschnitt)**
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.com;
|
||||
|
||||
# Statische Dateien direkt von nginx
|
||||
location /static/ {
|
||||
alias /var/www/mydjangoproject/static/;
|
||||
access_log off;
|
||||
expires 30d;
|
||||
}
|
||||
|
||||
# Alles andere an Django via Gunicorn
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8001; # hier läuft Gunicorn
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Vorteil:
|
||||
|
||||
- statische Inhalte kommen extrem schnell von nginx
|
||||
- Django/Gunicorn verarbeitet nur „echte“ App-Logik
|
||||
|
||||
|
||||
### 6.3 Beispiel: HTTPS mit Let’s Encrypt (Konzept)
|
||||
|
||||
Mit Tools wie **certbot** kannst du für nginx kostenlos TLS-Zertifikate holen.
|
||||
|
||||
Grobe Schritte:
|
||||
|
||||
```bash
|
||||
sudo apt install certbot python3-certbot-nginx
|
||||
sudo certbot --nginx -d example.com
|
||||
```
|
||||
|
||||
certbot:
|
||||
|
||||
- holt Zertifikat
|
||||
- passt nginx-Konfiguration an (Port 443, SSL-Parameter)
|
||||
- richtet automatische Erneuerung ein
|
||||
|
||||
nginx-Konfiguration enthält danach z.B.:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name example.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Damit ist deine Python-App per HTTPS erreichbar.
|
||||
|
||||
---
|
||||
|
||||
## 7. Zusammenfassung
|
||||
|
||||
- **nginx** ist ein leistungsfähiger Webserver und Reverse Proxy, der im Python-Kontext typischerweise:
|
||||
- vor deiner Python-Webanwendung sitzt
|
||||
- HTTP(S) abwickelt
|
||||
- statische Dateien effizient ausliefert
|
||||
- Anfragen an einen Python-Application-Server (Gunicorn, uWSGI, uvicorn) weiterleitet
|
||||
- TLS/HTTPS, Load Balancing, Security-Features bereitstellt
|
||||
|
||||
- **Abgrenzung**:
|
||||
- nginx ≠ Python-Framework (Django/Flask/[[FastAPI]])
|
||||
- nginx ≠ Application-Server (Gunicorn/uvicorn)
|
||||
- nginx = Webserver/Reverse Proxy
|
||||
|
||||
- **Gelöste Probleme**:
|
||||
- sichere Produktion anstelle von Entwicklungsservern
|
||||
- Performance für statische Inhalte
|
||||
- TLS/HTTPS, Lastverteilung, Schutz der App
|
||||
|
||||
- **Herausforderungen**:
|
||||
- Konfigurationsdateien verstehen
|
||||
- Rechte & Pfade korrekt setzen
|
||||
- Fehler systematisch debuggen
|
||||
- bei hoher Last Performance tunen
|
||||
|
||||
Executable
+360
@@ -0,0 +1,360 @@
|
||||
Im Folgenden bekommst du eine Einführung in OAuth 2, so dass du danach:
|
||||
|
||||
- weißt, was OAuth 2 grundsätzlich ist,
|
||||
- es von verwandten Konzepten wie „Login mit Google“, OpenID Connect oder reiner Authentifizierung unterscheiden kannst,
|
||||
- verstehst, welche Probleme OAuth 2 löst – und welche neuen Herausforderungen es mit sich bringt,
|
||||
- und ein paar praxisnahe Szenarien im Kopf hast.
|
||||
|
||||
---
|
||||
|
||||
## 1. Grundsätzliche Definition: Was ist OAuth 2?
|
||||
|
||||
**Kurze Definition**:
|
||||
OAuth 2 ist ein *Autorisierungs*-Framework. Es regelt, wie eine Anwendung (Client) **im Namen eines Benutzers** sicher auf Ressourcen (z. B. APIs, Daten) zugreifen kann, **ohne dass die Anwendung das Passwort des Benutzers kennen muss**.
|
||||
|
||||
Es geht also primär um:
|
||||
|
||||
- **Zugriffsrechte (Authorization)**
|
||||
- **Delegation**: „Ich (Nutzer) erlaube App X, auf meine Daten bei Dienst Y zuzugreifen.“
|
||||
|
||||
### Zentrales Prinzip
|
||||
|
||||
Statt:
|
||||
- Benutzer gibt sein Passwort direkt bei jeder Drittanbieter-App ein
|
||||
|
||||
nutzt man:
|
||||
- Benutzer loggt sich beim **vertrauenswürdigen Dienst** ein (z. B. Google, GitHub, dein Unternehmens-Identity-Server)
|
||||
- Dieser Dienst stellt ein **Token** aus (eine Art Eintrittskarte)
|
||||
- Drittanbieter-App nutzt dieses Token, um beim API-Server Zugriff zu bekommen
|
||||
|
||||
Dadurch muss die Drittanbieter-App nie das Passwort sehen – sie bekommt **nur einen begrenzten, kontrollierten Zugriff**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Die wichtigsten Rollen in OAuth 2
|
||||
|
||||
Um Beispiele zu verstehen, braucht man die Standardrollen:
|
||||
|
||||
1. **Resource Owner**
|
||||
Der Besitzer der Daten/Ressourcen – meist der **Benutzer**.
|
||||
|
||||
2. **Client**
|
||||
Die Anwendung, die im Namen des Benutzers zugreifen möchte
|
||||
(z. B. eine mobile App, eine Web-App, ein Backend-Service).
|
||||
|
||||
3. **Resource Server**
|
||||
Die API/das System, das die geschützten Ressourcen bereitstellt
|
||||
(z. B. Google Calendar API, GitHub API, Unternehmens-REST-API).
|
||||
|
||||
4. **Authorization Server**
|
||||
Der Server, der:
|
||||
- den Benutzer authentifiziert (z. B. Login-Formular) und
|
||||
- **Tokens** ausstellt (Access Tokens, optional Refresh Tokens).
|
||||
|
||||
Oft sind Authorization Server und Resource Server derselbe technische Dienst, aber **konzeptionell** werden sie getrennt betrachtet.
|
||||
|
||||
---
|
||||
|
||||
## 3. Grund-Mechanik: Wie läuft ein OAuth-2-Flow ab?
|
||||
|
||||
Stark vereinfacht am typischen Web-Szenario:
|
||||
|
||||
1. Benutzer nutzt eine Drittanbieter-App (Client) und klickt z. B. auf:
|
||||
„Verbinde dich mit meinem Google-Kalender“.
|
||||
|
||||
2. Client leitet den Browser zum **Authorization Server** (z. B. accounts.google.com) weiter:
|
||||
- mit Angaben wie: „Diese App möchte Zugriff auf Kalenderdaten“.
|
||||
|
||||
3. Benutzer loggt sich **beim Authorization Server** ein (falls nicht schon eingeloggt).
|
||||
|
||||
4. Benutzer bekommt eine Seite angezeigt:
|
||||
„Die Anwendung X möchte auf Ihre Kalenderdaten zugreifen. Erlauben? (Ja/Nein)“
|
||||
|
||||
5. Wenn Benutzer zustimmt:
|
||||
- Authorization Server schickt den Benutzer zurück zur App
|
||||
- Die App bekommt einen **Authorization Code** (bei einem gängigen Flow).
|
||||
|
||||
6. Die App tauscht diesen Code im Hintergrund beim Authorization Server gegen ein **Access Token** (und evtl. ein **Refresh Token**) ein.
|
||||
|
||||
7. Die App nutzt das Access Token, um beim **Resource Server** (z. B. Google Calendar API) Daten abzufragen.
|
||||
|
||||
Der Benutzer muss sein **Passwort nur beim Authorization Server** eingeben – nicht bei der Drittanbieter-App.
|
||||
|
||||
---
|
||||
|
||||
## 4. Abgrenzung zu ähnlichen / verwandten Begriffen
|
||||
|
||||
### 4.1 OAuth 2 (Authorization) vs. Authentifizierung (Login)
|
||||
|
||||
**Authentifizierung**: „Wer bist du?“
|
||||
**Autorisierung**: „Worauf darfst du zugreifen?“
|
||||
|
||||
- OAuth 2 löst formal **Autorisierung** (Delegation von Zugriffsrechten).
|
||||
- Viele Systeme nutzen OAuth 2 aber **im Zusammenhang mit Login**, was verwirrend sein kann.
|
||||
|
||||
Ein Login mit Google, bei dem eine Anwendung am Ende weiß „dieser User ist Max Mustermann“, basiert meist auf **OpenID Connect**, das **auf OAuth 2 aufsetzt**.
|
||||
|
||||
### 4.2 OAuth 2 vs. OpenID Connect (OIDC)
|
||||
|
||||
- **OAuth 2**: Spezifiziert, wie Clients **Access Tokens** bekommen, um auf APIs zuzugreifen.
|
||||
- **OpenID Connect**: Erweiterung auf Basis von OAuth 2, um **Identität** zu transportieren.
|
||||
|
||||
OIDC liefert zusätzlich:
|
||||
- ein **ID Token** (meist ein [[JWT token]]), das Informationen wie:
|
||||
- User-ID
|
||||
- E-Mail
|
||||
- Name
|
||||
enthält.
|
||||
|
||||
Praxis-Beispiele:
|
||||
|
||||
- „Mit Google anmelden“ (Login) → **OpenID Connect**
|
||||
- „Google Kalender-API nutzen“ → **OAuth 2** (Autorisierung für API-Zugriff)
|
||||
|
||||
### 4.3 OAuth 2 vs. API-Key
|
||||
|
||||
**API-Key**:
|
||||
- Meist ein statischer Schlüssel, der im Client eingebaut ist.
|
||||
- Unterscheidet nicht zwischen verschiedenen Benutzern.
|
||||
- Hat wenig feingranulare Rechte (oft: „alles oder nichts“).
|
||||
- Sicherheitsrisiko bei Leaks (z. B. im Frontend-Code).
|
||||
|
||||
**OAuth 2**:
|
||||
- Nutzerbezogen (Access Token repräsentiert den Benutzer/das Delegationsrecht).
|
||||
- Token haben meist begrenzte Lebensdauer.
|
||||
- Zugriffsrechte (Scopes) sind feiner steuerbar.
|
||||
|
||||
### 4.4 OAuth 2 vs. SAML
|
||||
|
||||
- **SAML**: Älterer Standard, XML-basiert, häufig in Unternehmensumgebungen für SSO verwendet.
|
||||
- **OAuth 2 / OIDC**: JSON/HTTP-basiert, leichtergewichtig, stark im Web- und Mobile-Bereich verbreitet.
|
||||
|
||||
Grober Merksatz:
|
||||
- SAML: klassische Unternehmen, Single Sign-On im Browser, ältere Infrastruktur.
|
||||
- OAuth 2 + OIDC: moderne Web-/Mobile-Apps, REST-APIs, Microservices.
|
||||
|
||||
---
|
||||
|
||||
## 5. Welche Probleme löst OAuth 2?
|
||||
|
||||
### 5.1 Vermeidung von Passwortweitergabe an Drittanbieter
|
||||
|
||||
Früher:
|
||||
Eine App wollte auf Gmail zugreifen => Benutzer musste **sein Gmail-Passwort** in dieser App eingeben.
|
||||
Risiken:
|
||||
- App könnte Passwort speichern oder missbrauchen.
|
||||
- Bei Passwortänderung müsste der Benutzer alles neu einrichten.
|
||||
- Anbieter (Google) hat keine Kontrolle, welche App das tut.
|
||||
|
||||
Mit OAuth 2:
|
||||
- Benutzer loggt sich nur bei Google ein.
|
||||
- Drittanbieter-App erhält nur ein **Token mit begrenzten Rechten**, kein Passwort.
|
||||
|
||||
**Praxisbeispiel**:
|
||||
Eine To-Do-App möchte deine Google Kalender-Einträge anzeigen:
|
||||
Du klickst „Mit Google verbinden“, wirst zu Google geleitet, bestätigst die Berechtigungen und die App sieht danach deine Kalender – ohne dein Passwort zu kennen.
|
||||
|
||||
### 5.2 Feingranulare Zugriffsrechte (Scopes)
|
||||
|
||||
Mit **Scopes** kann eingeschränkt werden, worauf eine App zugreifen darf.
|
||||
|
||||
Beispiele:
|
||||
- `read_calendar`: nur Leserechte auf Kalender
|
||||
- `write_calendar`: Schreibrechte auf Kalender
|
||||
- `email`: Zugriff auf deine E-Mail-Adresse
|
||||
- `profile`: Zugriff auf deine Basisprofildaten
|
||||
|
||||
So kann der Benutzer (und auch der Resource Server) klar kontrollieren:
|
||||
- Eine App darf z. B. **nur lesen**, nicht schreiben.
|
||||
- Eine App darf **nur auf E-Mail und Profil**, nicht auf Kontakte oder Drive zugreifen.
|
||||
|
||||
### 5.3 Zeitlich begrenzte Tokens
|
||||
|
||||
**Access Tokens** sind typischerweise kurzlebig (z. B. 5–60 Minuten).
|
||||
|
||||
- Wenn ein Token geleakt wird, ist der Schaden zeitlich begrenzt.
|
||||
- Über Revocation-Mechanismen kann der Zugriff vorzeitig beendet werden.
|
||||
|
||||
Optional gibt es **Refresh Tokens**:
|
||||
- Länger gültig
|
||||
- Dienen dazu, neue Access Tokens zu holen
|
||||
- Können vom Authorization Server gezielt deaktiviert werden (z. B. bei Kompromittierung).
|
||||
|
||||
### 5.4 Single Sign-On / bessere User Experience (in Kombination mit OIDC)
|
||||
|
||||
In Kombination mit OIDC und Single-Sign-On-Konzepten:
|
||||
- Benutzer loggt sich einmal beim zentralen Identity Provider ein.
|
||||
- Mehrere Anwendungen können dann in einer Session auf Tokens zugreifen, ohne den Benutzer jedes Mal neu einzuloggen.
|
||||
|
||||
---
|
||||
|
||||
## 6. Typische OAuth-2-Flows (vereinfacht)
|
||||
|
||||
Es gibt verschiedene **„Flows“**, je nach Art des Clients und Nutzungsszenario. Die wichtigsten:
|
||||
|
||||
### 6.1 Authorization Code Flow (heute mit PKCE)
|
||||
|
||||
Typisch für:
|
||||
- Web-Anwendungen mit Backend
|
||||
- Single Page Applications (mit PKCE)
|
||||
- Mobile Apps
|
||||
|
||||
Ablauf (vereinfacht, ohne technische Details):
|
||||
|
||||
1. Anwendung leitet Benutzer zum Authorization Server.
|
||||
2. Benutzer loggt sich ein und stimmt zu.
|
||||
3. Authorization Server schickt **Authorization Code** an die Anwendung (über Redirect).
|
||||
4. Anwendung tauscht Code gegen ein **Access Token** (und optional Refresh Token).
|
||||
|
||||
**PKCE** (Proof Key for Code Exchange) ist eine Sicherheits-Erweiterung, um Code-Diebstahl zu verhindern, insbesondere bei mobilen/Browser-basierten Apps.
|
||||
|
||||
### 6.2 Client Credentials Flow
|
||||
|
||||
Für:
|
||||
- Machine-to-Machine-Kommunikation (kein Benutzer)
|
||||
- Hintergrundprozesse, Microservices, Server-zu-Server
|
||||
|
||||
Hier gibt es keinen Benutzer.
|
||||
Der Client identifiziert sich selbst (z. B. mit Client ID + Client Secret) beim Authorization Server und bekommt ein Access Token, um auf eine API zuzugreifen.
|
||||
|
||||
**Beispiel**:
|
||||
Ein Backend-Service ruft periodisch eine interne Unternehmens-API auf, um Daten zu synchronisieren.
|
||||
|
||||
### 6.3 Device Code Flow
|
||||
|
||||
Für Geräte ohne vollwertigen Browser oder ohne komfortable Eingabemöglichkeit, wie:
|
||||
- Smart-TVs
|
||||
- Konsolen
|
||||
- IoT-Geräte
|
||||
|
||||
Ablauf:
|
||||
- Gerät zeigt einen Code und eine URL an (z. B. „Bitte gehen Sie auf https://example.com/device und geben Sie Code ABCD ein“).
|
||||
- Benutzer geht an seinem Smartphone/PC auf diese URL, loggt sich dort ein und bestätigt.
|
||||
- Gerät bekommt anschließend ein Token und kann auf die Ressource zugreifen.
|
||||
|
||||
---
|
||||
|
||||
## 7. Herausforderungen und typische Stolperfallen
|
||||
|
||||
### 7.1 OAuth 2 ist komplex und flexibel
|
||||
|
||||
OAuth 2 ist sehr flexibel – das ist Stärke und Schwäche zugleich:
|
||||
|
||||
- Viele Optionen & Erweiterungen → schnell unübersichtlich.
|
||||
- Viele Entscheidungen zu treffen: Flows, Token-Formate, Scopes, Lifetimes, PKCE, etc.
|
||||
|
||||
Für Einsteiger wirkt es oft „überdimensioniert“, vor allem wenn man nur „einen einfachen Login“ braucht (wo eigentlich OIDC gefragt ist).
|
||||
|
||||
### 7.2 Verwechslung von OAuth 2 und Login
|
||||
|
||||
Weil viele große Anbieter beides kombiniert haben („Login mit Google“), entsteht oft:
|
||||
|
||||
- „OAuth 2 ist ein Login-Protokoll.“ → **Formal falsch**, es ist ein Autorisierungsframework.
|
||||
- Für „Login“ verwendet man **OpenID Connect**, auch wenn technisch OAuth-2-Mechanismen verwendet werden.
|
||||
|
||||
Konsequenz:
|
||||
Fehlkonzeptionen in Projekten, z. B. versucht man reine OAuth-2-Tokens zu nutzen, um Identität abzuleiten, statt OIDC zu verwenden.
|
||||
|
||||
### 7.3 Sicherheitsrisiken bei falscher Implementierung
|
||||
|
||||
Einige typische Fehler:
|
||||
|
||||
- **Tokens in URLs weitergeben** → landen in Logs, Browser-History, Referrer-Headern.
|
||||
- **Implicit Flow** für Single-Page-Apps ohne ausreichende Sicherheitsmaßnahmen
|
||||
→ gilt inzwischen als überholt; empfohlen wird Authorization Code Flow mit PKCE.
|
||||
- **Keine Einschränkung der Redirect-URIs** → Angreifer können Redirect-URIs manipulieren und Tokens abfangen.
|
||||
- **Zu lange gültige Tokens** → höheres Risiko bei Leaks.
|
||||
- **Speichern von Tokens im unsicheren Speicher** (z. B. localStorage in Browsern) → anfällig für XSS.
|
||||
|
||||
### 7.4 Komplexität für Endnutzer
|
||||
|
||||
Auch für Endnutzer ist die Zustimmung manchmal nicht klar:
|
||||
|
||||
- „Diese App möchte auf Ihr Profil, Ihre Kontakte, Ihre Dateien, Ihre E-Mails zugreifen.“
|
||||
→ Viele klicken aus Gewohnheit auf „Zulassen“, ohne zu verstehen, was das bedeutet.
|
||||
|
||||
Das ist eher ein UX-/Privacy-Thema, aber eng mit OAuth 2 verbunden, da die Darstellung der **Scopes** oft technisch und verwirrend ist.
|
||||
|
||||
---
|
||||
|
||||
## 8. Praxisnahe Beispiele
|
||||
|
||||
### Beispiel 1: „Mit Google anmelden“ in einer Web-App
|
||||
|
||||
**Situation**:
|
||||
Du entwickelst eine Web-App, in der sich Nutzer anmelden können. Statt ein eigenes Benutzerkontensystem aufzubauen, willst du „Login mit Google“ anbieten.
|
||||
|
||||
**In Wahrheit passiert (typisch)**:
|
||||
- Die App nutzt **OpenID Connect**:
|
||||
- OAuth 2 regelt den Token-Flow.
|
||||
- OIDC liefert ein ID Token mit Infos über den Benutzer.
|
||||
- Nutzer sieht: Google-Login, eine Consent-Seite, und wird danach in deine App zurückgeleitet.
|
||||
- Deine App bekommt:
|
||||
- ein **ID Token** (zur Identifikation des Nutzers)
|
||||
- ein **Access Token** (um z. B. zusätzlich auf Google APIs zuzugreifen, falls gewünscht).
|
||||
|
||||
**OAuth-2-Aspekt**:
|
||||
Der eigentliche Mechanismus, wie Access Token ausgestellt werden, ist OAuth 2.
|
||||
|
||||
### Beispiel 2: Drittanbieter-Tool liest GitHub-Repositories
|
||||
|
||||
**Situation**:
|
||||
Ein Build-Tool oder eine CI/CD-Plattform möchte deine privaten GitHub-Repositories lesen, um Builds auszulösen.
|
||||
|
||||
Ablauf:
|
||||
1. In der CI/CD-Plattform klickst du „Mit GitHub verbinden“.
|
||||
2. Du wirst zu GitHub weitergeleitet, loggst dich ein.
|
||||
3. GitHub zeigt dir eine Seite:
|
||||
„Tool X möchte Zugriff auf:
|
||||
- Ihre öffentlichen und privaten Repositories
|
||||
- Ihre Webhooks
|
||||
…“
|
||||
4. Du erlaubst.
|
||||
5. Die CI/CD-Plattform erhält ein Access Token mit bestimmten **Scopes** (z. B. `repo`).
|
||||
|
||||
**OAuth-2-Aspekt**:
|
||||
- Das Access Token erlaubt der CI/CD-Plattform, im Namen deines GitHub-Accounts zu agieren, innerhalb des erlaubten Scopes.
|
||||
|
||||
### Beispiel 3: Unternehmensinternes Microservice-System
|
||||
|
||||
**Situation**:
|
||||
In einer Microservice-Architektur gibt es:
|
||||
- einen zentralen Authorization Server (z. B. Keycloak, Auth0, Azure AD),
|
||||
- viele Services, die interne APIs bereitstellen.
|
||||
|
||||
Szenario:
|
||||
- Ein Frontend bekommt vom Authorization Server (über einen Login) ein Access Token.
|
||||
- Dieses Access Token wird vom Frontend bei Aufrufen an das API-Gateway oder direkt an Microservices mitgeschickt.
|
||||
- Microservices verifizieren das Token und entscheiden anhand von Scopes/Rollen, ob der Zugriff erlaubt ist.
|
||||
|
||||
**OAuth-2-Aspekt**:
|
||||
- Zugriffskontrolle wird zentralisiert.
|
||||
- Jeder Service muss nicht mehr selbst Benutzer verwalten, sondern nur Access Tokens prüfen.
|
||||
|
||||
---
|
||||
|
||||
## 9. Zusammenfassung in Stichpunkten
|
||||
|
||||
- **OAuth 2** ist ein **Autorisierungs-Framework** (nicht direkt ein Login-Protokoll).
|
||||
- Es ermöglicht, dass **Drittanbieter-Apps im Namen eines Nutzers** auf Ressourcen zugreifen können, ohne das Passwort des Nutzers zu kennen.
|
||||
- Wichtige Rollen: **Resource Owner**, **Client**, **Resource Server**, **Authorization Server**.
|
||||
- Es arbeitet mit **Tokens** (Access Tokens, optional Refresh Tokens) und **Scopes** (feingranulare Rechte).
|
||||
- Verwandte Standards:
|
||||
- **OpenID Connect**: für Authentifizierung/Identität (baut auf OAuth 2 auf).
|
||||
- **SAML**: alternative, ältere Lösung v. a. für SSO in Unternehmen.
|
||||
- Es löst Probleme wie:
|
||||
- Passwortweitergabe an Drittanbieter
|
||||
- Fehlende Feingranularität von Zugriffsrechten
|
||||
- Mangelnde Kontrolle über Zugriffsdelegation
|
||||
- Herausforderungen:
|
||||
- Komplexität und viele Optionen
|
||||
- Verwechslungsgefahr mit Authentifizierung
|
||||
- Sicherheitsrisiken bei falscher Implementierung
|
||||
- UX/Verständlichkeit von Zustimmungsdialogen
|
||||
- Praxis:
|
||||
- „Mit Google/GitHub/Facebook anmelden“
|
||||
- Drittanbieter-Apps, die auf APIs zugreifen
|
||||
- Microservice-Systeme mit zentralem Identity & Access Management
|
||||
|
||||
---
|
||||
|
||||
Executable
+530
@@ -0,0 +1,530 @@
|
||||
#infrastructure #Cloud
|
||||
## 1. Was ist Terraform?
|
||||
|
||||
Terraform ist ein Open-Source-Tool (ursprünglich MIT, seit 2023 Business Source License, BSL) von HashiCorp zur **deklarativen** Beschreibung und Verwaltung von Infrastruktur als Code (Infrastructure as Code, IaC).
|
||||
|
||||
Mit Terraform kannst du:
|
||||
|
||||
- Infrastruktur in Cloud-Providern (AWS, Azure, GCP, …), On-Premise und weiteren Systemen **beschreiben** (als Code).
|
||||
- Aus dieser Beschreibung eine **gewünschte Zielkonfiguration** (Desired State) definieren.
|
||||
- Terraform berechnet dann, **welche Änderungen nötig sind**, um vom Ist-Zustand zum Soll-Zustand zu gelangen.
|
||||
- Diese Änderungen werden mit `terraform apply` **automatisch ausgeführt**.
|
||||
|
||||
Terraform ist besonders stark in:
|
||||
|
||||
- **Multi-Cloud- und Hybrid-Szenarien**
|
||||
- **Reproduzierbaren Umgebungen** (z.B. Dev, Test, Prod)
|
||||
- **Versionierbarer Infrastruktur** (Git)
|
||||
- **Team-Kollaboration** an Infrastruktur
|
||||
|
||||
---
|
||||
|
||||
## 2. Grundkonzepte von Terraform
|
||||
|
||||
Die wichtigsten Bausteine:
|
||||
|
||||
### 2.1 Provider
|
||||
|
||||
Provider sind die „Treiber“, mit denen Terraform mit einem externen System spricht, z.B.:
|
||||
|
||||
- `aws` – Amazon Web Services
|
||||
- `azurerm` – Microsoft Azure
|
||||
- `google` – Google Cloud
|
||||
- `kubernetes` – Kubernetes-Cluster
|
||||
- `helm`, `docker`, `vault`, `github`, viele weitere
|
||||
|
||||
In Terraform-Code bindet man einen Provider ein, z.B.:
|
||||
|
||||
```hcl
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = "eu-central-1"
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Ressourcen
|
||||
|
||||
**Ressourcen** (`resource`) sind die eigentlichen Infrastruktur-Objekte, z.B.:
|
||||
|
||||
- `aws_instance` (EC2-VM)
|
||||
- `aws_s3_bucket`
|
||||
- `azurerm_resource_group`
|
||||
- `google_compute_instance`
|
||||
|
||||
Beispiel:
|
||||
|
||||
```hcl
|
||||
resource "aws_s3_bucket" "example" {
|
||||
bucket = "mein-terraform-bucket-12345"
|
||||
acl = "private"
|
||||
}
|
||||
```
|
||||
|
||||
`aws_s3_bucket` ist der Ressourcentyp, `"example"` der Name innerhalb der Terraform-Konfiguration.
|
||||
|
||||
### 2.3 Datenquellen (Data Sources)
|
||||
|
||||
`data`-Blöcke lesen Informationen aus einer bestehenden Umgebung, ohne etwas zu verändern.
|
||||
|
||||
```hcl
|
||||
data "aws_ami" "ubuntu" {
|
||||
most_recent = true
|
||||
|
||||
filter {
|
||||
name = "name"
|
||||
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
|
||||
}
|
||||
|
||||
owners = ["099720109477"] # Canonical
|
||||
}
|
||||
```
|
||||
|
||||
Die Daten können dann in Ressourcen verwendet werden.
|
||||
|
||||
### 2.4 Variablen und Outputs
|
||||
|
||||
**Variablen** (`variable`) erlauben Parametrisierung:
|
||||
|
||||
```hcl
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "eu-central-1"
|
||||
description = "AWS Region"
|
||||
}
|
||||
```
|
||||
|
||||
Aufgerufen z.B.: `var.region`.
|
||||
|
||||
**Outputs** (`output`) geben wichtige Informationen aus (z.B. IP-Adressen):
|
||||
|
||||
```hcl
|
||||
output "instance_ip" {
|
||||
value = aws_instance.web.public_ip
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 Module
|
||||
|
||||
Module sind wiederverwendbare Bausteine von Terraform-Konfigurationen (eine Art „Bibliothek“ von Infrastruktur).
|
||||
|
||||
- Ein Modul ist einfach ein Ordner mit `.tf`-Dateien.
|
||||
- Man kann eigene Module oder Community-Module nutzen (z.B. aus dem Terraform Registry).
|
||||
|
||||
Beispiel Nutzung eines Moduls:
|
||||
|
||||
```hcl
|
||||
module "network" {
|
||||
source = "./modules/network"
|
||||
|
||||
vpc_cidr = "10.0.0.0/16"
|
||||
}
|
||||
```
|
||||
|
||||
Oder:
|
||||
|
||||
```hcl
|
||||
module "vpc" {
|
||||
source = "terraform-aws-modules/vpc/aws"
|
||||
version = "5.0.0"
|
||||
|
||||
name = "main-vpc"
|
||||
cidr = "10.0.0.0/16"
|
||||
azs = ["eu-central-1a", "eu-central-1b"]
|
||||
}
|
||||
```
|
||||
|
||||
### 2.6 Terraform State
|
||||
|
||||
Terraform hält den Zustand der verwalteten Ressourcen in einer **State-Datei** (`terraform.tfstate`) fest.
|
||||
|
||||
- Der State ist die „Wahrheit“, mit der Terraform den Ist-Zustand kennt.
|
||||
- Daran erkennt Terraform, welche Ressource es schon erstellt hat und wie deren IDs, Attribute usw. sind.
|
||||
- Änderungen am Code werden mit dem State abgeglichen → Terraform generiert einen Plan.
|
||||
|
||||
State kann liegen:
|
||||
|
||||
- lokal: `terraform.tfstate` im Projektordner
|
||||
- remote (empfohlen für Teams): z.B. in S3, Azure Storage, GCS, Terraform Cloud, etc.
|
||||
|
||||
Beispiel Backend-Konfiguration (Remote State mit S3):
|
||||
|
||||
```hcl
|
||||
terraform {
|
||||
backend "s3" {
|
||||
bucket = "my-terraform-state-bucket"
|
||||
key = "prod/terraform.tfstate"
|
||||
region = "eu-central-1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Typischer Terraform-Workflow
|
||||
|
||||
### 3.1 Projektstruktur
|
||||
|
||||
Beispiel:
|
||||
|
||||
```text
|
||||
.
|
||||
├── main.tf # Ressourcen, Module
|
||||
├── variables.tf # Variablen
|
||||
├── outputs.tf # Outputs
|
||||
└── providers.tf # Provider / Backend
|
||||
```
|
||||
|
||||
### 3.2 Wichtige Terraform-Kommandos
|
||||
|
||||
1. `terraform init`
|
||||
- Initialisiert das Projekt
|
||||
- Lädt Provider-Plugins
|
||||
- Konfiguriert Backend (State)
|
||||
|
||||
2. `terraform plan`
|
||||
- Zeigt, was Terraform ändern würde (Create/Update/Delete)
|
||||
- Verändert noch nichts
|
||||
- Wichtig für Review (z.B. im CI)
|
||||
|
||||
3. `terraform apply`
|
||||
- Führt den Plan aus (Standard: vorher Anzeige + Bestätigung)
|
||||
- Erzeugt/aktualisiert/löscht Ressourcen
|
||||
|
||||
4. `terraform destroy`
|
||||
- Löscht alle Ressourcen, die Terraform verwaltet
|
||||
- Vorsicht: de facto „Infrastruktur-Abbau“
|
||||
|
||||
Typischer Ablauf:
|
||||
|
||||
```bash
|
||||
terraform init
|
||||
terraform plan
|
||||
terraform apply
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Terraform-Sprache: HCL (HashiCorp Configuration Language)
|
||||
|
||||
Terraform benutzt HCL, eine deklarative, blockbasierte Sprache.
|
||||
|
||||
### 4.1 Syntax-Grundlagen
|
||||
|
||||
Blöcke haben Form:
|
||||
|
||||
```hcl
|
||||
resource "TYP" "NAME" {
|
||||
argument = "value"
|
||||
|
||||
block {
|
||||
nested_arg = 123
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Datentypen:
|
||||
|
||||
- `string` – `"Text"`
|
||||
- `number` – `42`
|
||||
- `bool` – `true` / `false`
|
||||
- Listen – `["a", "b"]`
|
||||
- Maps – `{ key = "value" }`
|
||||
- Objekte/Tuples – komplexere Typen
|
||||
|
||||
### 4.2 Expressions & Referenzen
|
||||
|
||||
Referenzen auf andere Ressourcen:
|
||||
|
||||
```hcl
|
||||
resource "aws_instance" "web" {
|
||||
ami = data.aws_ami.ubuntu.id
|
||||
instance_type = var.instance_type
|
||||
|
||||
tags = {
|
||||
Name = "web-${var.environment}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Referenz-Regel:
|
||||
|
||||
- `ressourcentyp.name.attribut`
|
||||
- z.B. `aws_instance.web.public_ip`
|
||||
|
||||
---
|
||||
|
||||
## 5. Praktisches Beispiel
|
||||
|
||||
Ein minimaler Stack in AWS:
|
||||
|
||||
### `providers.tf`
|
||||
|
||||
```hcl
|
||||
terraform {
|
||||
required_version = ">= 1.6.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = var.region
|
||||
}
|
||||
```
|
||||
|
||||
### `variables.tf`
|
||||
|
||||
```hcl
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "eu-central-1"
|
||||
description = "AWS Region"
|
||||
}
|
||||
|
||||
variable "instance_type" {
|
||||
type = string
|
||||
default = "t3.micro"
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
type = string
|
||||
default = "dev"
|
||||
}
|
||||
```
|
||||
|
||||
### `main.tf`
|
||||
|
||||
```hcl
|
||||
data "aws_ami" "ubuntu" {
|
||||
most_recent = true
|
||||
|
||||
filter {
|
||||
name = "name"
|
||||
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
|
||||
}
|
||||
|
||||
owners = ["099720109477"]
|
||||
}
|
||||
|
||||
resource "aws_instance" "web" {
|
||||
ami = data.aws_ami.ubuntu.id
|
||||
instance_type = var.instance_type
|
||||
|
||||
tags = {
|
||||
Name = "web-${var.environment}"
|
||||
Environment = var.environment
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `outputs.tf`
|
||||
|
||||
```hcl
|
||||
output "web_instance_id" {
|
||||
value = aws_instance.web.id
|
||||
}
|
||||
|
||||
output "web_instance_ami" {
|
||||
value = aws_instance.web.ami
|
||||
}
|
||||
```
|
||||
|
||||
Ablauf:
|
||||
|
||||
```bash
|
||||
terraform init
|
||||
terraform plan
|
||||
terraform apply
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Workspaces, Environments und Modularisierung
|
||||
|
||||
### 6.1 Workspaces
|
||||
|
||||
Terraform Workspaces ermöglichen getrennte States innerhalb desselben Codes (z.B. `default`, `dev`, `prod`).
|
||||
|
||||
```bash
|
||||
terraform workspace list
|
||||
terraform workspace new dev
|
||||
terraform workspace select dev
|
||||
```
|
||||
|
||||
Dann z.B.:
|
||||
|
||||
```hcl
|
||||
variable "environment" {
|
||||
default = terraform.workspace
|
||||
}
|
||||
```
|
||||
|
||||
Viele Teams gehen aber eher über **separate State-Dateien / Ordner / Repos** (z.B. `envs/dev`, `envs/prod`), statt Workspaces intensiv zu nutzen.
|
||||
|
||||
### 6.2 Module für Wiederverwendung
|
||||
|
||||
Struktur-Beispiel:
|
||||
|
||||
```text
|
||||
.
|
||||
├── main.tf
|
||||
├── modules
|
||||
│ ├── network
|
||||
│ │ ├── main.tf
|
||||
│ │ ├── variables.tf
|
||||
│ │ └── outputs.tf
|
||||
│ └── compute
|
||||
│ ├── main.tf
|
||||
│ ├── variables.tf
|
||||
│ └── outputs.tf
|
||||
```
|
||||
|
||||
In `main.tf`:
|
||||
|
||||
```hcl
|
||||
module "network" {
|
||||
source = "./modules/network"
|
||||
vpc_cidr = "10.0.0.0/16"
|
||||
}
|
||||
|
||||
module "compute" {
|
||||
source = "./modules/compute"
|
||||
vpc_id = module.network.vpc_id
|
||||
subnets = module.network.subnet_ids
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Terraform Cloud / Terraform Enterprise
|
||||
|
||||
Neben der CLI gibt es:
|
||||
|
||||
- **Terraform Cloud** (SaaS von HashiCorp)
|
||||
- **Terraform Enterprise** (Self-hosted)
|
||||
|
||||
Features:
|
||||
|
||||
- Remote-State-Storage
|
||||
- Remote-Execution (Plans/Applies)
|
||||
- Policies (Sentinel)
|
||||
- Team & Governance (RBAC)
|
||||
- UI für Runs, Logs, States, Variablen
|
||||
|
||||
Für kleine Teams reicht oft: Git + Remote State (S3/Azure/GCS) + CI/CD Pipeline.
|
||||
|
||||
---
|
||||
|
||||
## 8. Typische Anwendungsfälle
|
||||
|
||||
- **Cloud-Infrastruktur**:
|
||||
- VPCs/Netzwerke, Subnets, Security Groups
|
||||
- Compute (VMs, Managed Kubernetes, Serverless)
|
||||
- Datenbanken, Caches, Messaging
|
||||
- **Kubernetes-Infrastruktur**:
|
||||
- Cluster (EKS/AKS/GKE)
|
||||
- Add-ons via `kubernetes`-Provider, `helm`-Provider
|
||||
- **Multi-Cloud-Szenarien**:
|
||||
- Einheitliches Tool für AWS + Azure + GCP
|
||||
- **On-Prem / Sonstiges**:
|
||||
- VMware, OpenStack, F5, GitHub-Repos, DNS (Cloudflare/Route53), Monitoring (Datadog, New Relic), etc.
|
||||
|
||||
---
|
||||
|
||||
## 9. Vergleich zu anderen Tools
|
||||
|
||||
### Terraform vs. CloudFormation / ARM / Bicep
|
||||
|
||||
- Terraform:
|
||||
- Provider-übergreifend (nicht auf eine Cloud beschränkt)
|
||||
- HCL, verständliche Syntax
|
||||
- CloudFormation (AWS) / ARM/Bicep (Azure) / Deployment Manager (GCP):
|
||||
- Cloud-spezifisch, tief integriert
|
||||
- Gut für reines Single-Cloud-Setup
|
||||
- Multi-Cloud wird komplex
|
||||
|
||||
### Terraform vs. Ansible / Chef / Puppet
|
||||
|
||||
- Terraform:
|
||||
- Fokus: **Provisioning** + Lebenszyklus von Ressourcen
|
||||
- Deklarativ, fokus auf Infrastruktur
|
||||
- Ansible/Chef/Puppet:
|
||||
- Fokus: **Konfiguration** von Servern / Software
|
||||
- IdR. auf bereits existierende Maschinen
|
||||
|
||||
Häufig werden Terraform + Ansible kombiniert:
|
||||
Terraform baut VMs / Netzwerke, Ansible konfiguriert Software im OS.
|
||||
|
||||
### Terraform vs. Pulumi
|
||||
|
||||
- Pulumi: IaC mit **allgemeinen Programmiersprachen** (TypeScript, Python, Go, C#)
|
||||
- Terraform: HCL, deklarativ, stark verbreitete Community
|
||||
- Pulumi bietet mehr „Programmier-Features“ (Loops, Ifs) direkt in Sprache; Terraform hat ähnliche Mechanismen, aber bewusst begrenzt, um die Konfiguration simpel zu halten.
|
||||
|
||||
---
|
||||
|
||||
## 10. Best Practices & Stolperfallen
|
||||
|
||||
### 10.1 Best Practices
|
||||
|
||||
- **State nicht lokal** speichern, sondern Remote (z.B. S3 + DynamoDB Locking, Azure Storage + Locks).
|
||||
- **State schützen**:
|
||||
- Zugriff beschränken (IAM/RBAC)
|
||||
- Backups
|
||||
- **Struktur / Modularisierung**:
|
||||
- Wiederverwendbare Module
|
||||
- Trennung von Environments (dev/stage/prod)
|
||||
- **Git-Workflow**:
|
||||
- Terraform-Code in Repos
|
||||
- PRs/Merge-Requests
|
||||
- `terraform plan` via CI im PR anzeigen
|
||||
- **Versionen pinnen**:
|
||||
- Terraform-Version `required_version`
|
||||
- Provider-Versionen fixieren (z.B. `~> 5.0`)
|
||||
- **Kleine, inkrementelle Änderungen**:
|
||||
- Große Umbauten in mehreren Schritten
|
||||
- Plan gut reviewen
|
||||
|
||||
### 10.2 Typische Probleme
|
||||
|
||||
- **State-Drift**:
|
||||
- Ressourcen werden außerhalb von Terraform geändert (z.B. in der Cloud-Konsole)
|
||||
- Plan zeigt unerwartete Änderungen
|
||||
- Lösung: soweit möglich alle Änderungen via Terraform; bei Drift bewusst entscheiden (import, adopt, ignore)
|
||||
- **Manuelles Löschen von Ressourcen**:
|
||||
- Terraform „denkt“, Ressource existiert noch
|
||||
- Beim nächsten Apply werden sie ggf. neu erstellt
|
||||
- **Zyklen in Abhängigkeiten**:
|
||||
- Falsche Referenzen können zyklische Abhängigkeiten erzeugen
|
||||
- **Große States**:
|
||||
- Sehr viele Ressourcen in einem State → langsamere Plans, unübersichtliche Fehler
|
||||
- Lösung: Aufteilen in mehrere Terraform-Projekte / States (z.B. pro Domäne/Layer)
|
||||
|
||||
---
|
||||
|
||||
## 11. Aktuelle Entwicklungen: Lizenz & OpenTofu
|
||||
|
||||
HashiCorp hat 2023 die Lizenz von Terraform auf die **Business Source License (BSL)** geändert.
|
||||
Daraufhin entstand ein **Community-Fork**:
|
||||
|
||||
- **OpenTofu** (ehemals OpenTF):
|
||||
- Komplett Open Source (MPL 2.0)
|
||||
- CLI & HCL weitgehend kompatibel zu Terraform (Stand heute)
|
||||
- Ziel: drop-in Replacement für viele Terraform-Usecases
|
||||
|
||||
Für einen Einstieg in IaC ist es aber sinnvoll, zuerst die Konzepte anhand von Terraform zu verstehen – die meisten davon sind für OpenTofu nahezu identisch.
|
||||
|
||||
---
|
||||
|
||||
Executable
+358
@@ -0,0 +1,358 @@
|
||||
Hier ist eine kompakte, praxisorientierte Anleitung zu `kubectl` mit den wichtigsten Funktionen und Use Cases.
|
||||
|
||||
---
|
||||
|
||||
## 1. Was ist `kubectl`?
|
||||
|
||||
`kubectl` ist das Kommandozeilen-Tool, um mit einem Kubernetes-Cluster zu kommunizieren.
|
||||
Damit kannst du:
|
||||
|
||||
- Ressourcen erstellen, ändern und löschen (Deployments, Pods, Services, …)
|
||||
- Logs einsehen und Probleme debuggen
|
||||
- Rollouts steuern (Deployments updaten/rollback)
|
||||
- Konfigurationen verwalten (Kontexte, Namespaces, Kubeconfig)
|
||||
|
||||
---
|
||||
|
||||
## 2. Grundlagen: Aufbau von `kubectl`-Befehlen
|
||||
|
||||
Allgemeines Schema:
|
||||
|
||||
```bash
|
||||
kubectl <verb> <ressource> [name] [flags]
|
||||
```
|
||||
|
||||
Beispiele:
|
||||
|
||||
```bash
|
||||
kubectl get pods
|
||||
kubectl describe pod mein-pod
|
||||
kubectl delete service mein-service
|
||||
```
|
||||
|
||||
Wichtige Verben:
|
||||
|
||||
- `get` – Anzeigen
|
||||
- `describe` – Detailinformationen
|
||||
- `create` / `apply` – Ressourcen anlegen/aktualisieren
|
||||
- `delete` – Löschen
|
||||
- `logs` – Logs anzeigen
|
||||
- `exec` – Befehle innerhalb eines Pods ausführen
|
||||
|
||||
---
|
||||
|
||||
## 3. Konfiguration & Kontexte
|
||||
|
||||
### 3.1 Cluster-Zugriff prüfen
|
||||
|
||||
```bash
|
||||
kubectl version
|
||||
kubectl cluster-info
|
||||
```
|
||||
|
||||
### 3.2 Aktuellen Kontext anzeigen und wechseln
|
||||
|
||||
```bash
|
||||
kubectl config get-contexts
|
||||
kubectl config current-context
|
||||
kubectl config use-context mein-context
|
||||
```
|
||||
|
||||
### 3.3 Namespace festlegen
|
||||
|
||||
Temporär per Flag:
|
||||
|
||||
```bash
|
||||
kubectl get pods -n my-namespace
|
||||
```
|
||||
|
||||
Standard-Namespace setzen:
|
||||
|
||||
```bash
|
||||
kubectl config set-context --current --namespace=my-namespace
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Ressourcen anzeigen und inspizieren
|
||||
|
||||
### 4.1 Ressourcen auflisten
|
||||
|
||||
```bash
|
||||
kubectl get pods
|
||||
kubectl get deployments
|
||||
kubectl get services
|
||||
kubectl get all
|
||||
```
|
||||
|
||||
Mit mehr Details:
|
||||
|
||||
```bash
|
||||
kubectl get pods -o wide
|
||||
kubectl get pods -o yaml
|
||||
```
|
||||
|
||||
### 4.2 Detailinformationen zu einer Ressource
|
||||
|
||||
```bash
|
||||
kubectl describe pod mein-pod
|
||||
kubectl describe deployment mein-deployment
|
||||
```
|
||||
|
||||
Use Case: Debugging (Events, Container-Status, Restart-Gründe).
|
||||
|
||||
---
|
||||
|
||||
## 5. Deployments & Anwendungen verwalten
|
||||
|
||||
### 5.1 Deployment aus YAML erstellen
|
||||
|
||||
`deployment.yaml` Beispiel (stark vereinfacht):
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.25
|
||||
ports:
|
||||
- containerPort: 80
|
||||
```
|
||||
|
||||
Anwenden:
|
||||
|
||||
```bash
|
||||
kubectl apply -f deployment.yaml
|
||||
```
|
||||
|
||||
### 5.2 Deployment aktualisieren (Rolling Update)
|
||||
|
||||
Image ändern:
|
||||
|
||||
```bash
|
||||
kubectl set image deployment/nginx-deployment nginx=nginx:1.26
|
||||
```
|
||||
|
||||
Rollout-Status beobachten:
|
||||
|
||||
```bash
|
||||
kubectl rollout status deployment/nginx-deployment
|
||||
```
|
||||
|
||||
Rollback:
|
||||
|
||||
```bash
|
||||
kubectl rollout undo deployment/nginx-deployment
|
||||
```
|
||||
|
||||
### 5.3 Replica-Anzahl skalieren
|
||||
|
||||
```bash
|
||||
kubectl scale deployment nginx-deployment --replicas=5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Services & Zugriff auf Anwendungen
|
||||
|
||||
### 6.1 Services anzeigen
|
||||
|
||||
```bash
|
||||
kubectl get svc
|
||||
```
|
||||
|
||||
Beispiel-Service:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx-service
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: nginx
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
```
|
||||
|
||||
Anwenden:
|
||||
|
||||
```bash
|
||||
kubectl apply -f service.yaml
|
||||
```
|
||||
|
||||
### 6.2 Port-Forwarding (lokaler Zugriff)
|
||||
|
||||
```bash
|
||||
kubectl port-forward svc/nginx-service 8080:80
|
||||
# Aufruf im Browser: http://localhost:8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Logs & Debugging
|
||||
|
||||
### 7.1 Logs eines Pods
|
||||
|
||||
```bash
|
||||
kubectl logs mein-pod
|
||||
kubectl logs mein-pod -c container-name
|
||||
kubectl logs -f mein-pod # -f = "follow"
|
||||
```
|
||||
|
||||
### 7.2 Befehle im Container ausführen
|
||||
|
||||
```bash
|
||||
kubectl exec -it mein-pod -- /bin/sh
|
||||
# oder
|
||||
kubectl exec -it mein-pod -- bash
|
||||
```
|
||||
|
||||
Typischer Use Case: Debugging von laufenden Containern, z.B. Netzwerkprobleme, Dateisystem prüfen.
|
||||
|
||||
### 7.3 Problem-Analyse mit `describe` & Events
|
||||
|
||||
```bash
|
||||
kubectl describe pod mein-pod
|
||||
kubectl get events --sort-by=.metadata.creationTimestamp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Ressourcen ändern & löschen
|
||||
|
||||
### 8.1 Änderungen anwenden
|
||||
|
||||
Konfiguration in einer Datei ändern und dann:
|
||||
|
||||
```bash
|
||||
kubectl apply -f deployment.yaml
|
||||
```
|
||||
|
||||
Um tatsächliche Unterschiede zu sehen:
|
||||
|
||||
```bash
|
||||
kubectl diff -f deployment.yaml
|
||||
```
|
||||
|
||||
### 8.2 Ressourcen löschen
|
||||
|
||||
```bash
|
||||
kubectl delete pod mein-pod
|
||||
kubectl delete -f deployment.yaml
|
||||
kubectl delete deployment nginx-deployment
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Konfigurationen & Secrets
|
||||
|
||||
### 9.1 ConfigMaps
|
||||
|
||||
Erstellen aus einer Datei:
|
||||
|
||||
```bash
|
||||
kubectl create configmap app-config --from-file=config.properties
|
||||
```
|
||||
|
||||
Oder aus Literalwert:
|
||||
|
||||
```bash
|
||||
kubectl create configmap app-config --from-literal=KEY=VALUE
|
||||
```
|
||||
|
||||
ConfigMaps anzeigen:
|
||||
|
||||
```bash
|
||||
kubectl get configmaps
|
||||
kubectl describe configmap app-config
|
||||
```
|
||||
|
||||
### 9.2 Secrets
|
||||
|
||||
```bash
|
||||
kubectl create secret generic db-secret \
|
||||
--from-literal=username=user \
|
||||
--from-literal=password=geheim
|
||||
```
|
||||
|
||||
Achtung: Standard-`kubectl get secret -o yaml` zeigt Base64-codierte, aber nicht verschlüsselte Daten.
|
||||
|
||||
---
|
||||
|
||||
## 10. Weitere nützliche Features
|
||||
|
||||
### 10.1 Autocomplete
|
||||
|
||||
Bash-Beispiel:
|
||||
|
||||
```bash
|
||||
source <(kubectl completion bash)
|
||||
echo 'source <(kubectl completion bash)' >> ~/.bashrc
|
||||
```
|
||||
|
||||
### 10.2 Kustomize (ohne extra Tool, ab neueren kubectl-Versionen)
|
||||
|
||||
```bash
|
||||
kubectl apply -k ./overlays/production
|
||||
```
|
||||
|
||||
Struktur z.B.:
|
||||
|
||||
```text
|
||||
base/
|
||||
deployment.yaml
|
||||
kustomization.yaml
|
||||
overlays/production/
|
||||
kustomization.yaml
|
||||
patches.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Typische Praxis-Workflows
|
||||
|
||||
### 11.1 Neue Version deployen
|
||||
|
||||
```bash
|
||||
git pull
|
||||
kubectl apply -f k8s/
|
||||
kubectl rollout status deployment mein-service
|
||||
kubectl get pods -o wide
|
||||
```
|
||||
|
||||
### 11.2 Fehlerhafte Anwendung debuggen
|
||||
|
||||
```bash
|
||||
kubectl get pods
|
||||
kubectl describe pod fehler-pod
|
||||
kubectl logs fehler-pod
|
||||
kubectl exec -it fehler-pod -- sh
|
||||
kubectl get events --sort-by=.metadata.creationTimestamp
|
||||
```
|
||||
|
||||
### 11.3 Schnell in einen anderen Namespace/Cluster wechseln
|
||||
|
||||
```bash
|
||||
kubectl config use-context staging-cluster
|
||||
kubectl config set-context --current --namespace=team-a
|
||||
kubectl get pods
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Wenn du möchtest, kann ich dir für deinen konkreten Use Case (z.B. “Web-App deployen”, “CI/CD-Pipeline”, “lokales Minikube-Cluster”) eine konkrete Schritt-für-Schritt-Anleitung mit fertigen `kubectl`-Befehlen erstellen.
|
||||
Reference in New Issue
Block a user