Start / Seminare / FastAPI Architektur in der Praxis
Modul
Asynchrone Persistenz
4 Kapitel in diesem Modul-Video · Laufzeit
Transkript
Der gesprochene Text dieses Moduls zum Mitlesen, Überfliegen und Durchsuchen. Ein Klick auf einen Zeitstempel springt an die Stelle im Video.
Asynchrone Persistenz
0:00 Unsere Angebote leben bisher in einem Dictionary im Arbeitsspeicher. Beim nächsten Neustart sind sie weg. Das ändern wir jetzt — und stoßen dabei auf eine Eigenheit, die viele überrascht: Asynchroner Datenbankzugriff verzeiht weniger als synchroner. Was in einer klassischen Anwendung nur langsam wäre, wirft hier eine Ausnahme.
0:18 Das klingt nach einem Nachteil und ist in Wahrheit ein Geschenk: Fehler, die sonst erst unter Last als schlechte Antwortzeit auffallen, fallen sofort auf. In diesem Modul bauen wir die Speicherschicht auf — typsicher, asynchron und mit einer klaren Grenze zu dem, was wir im ersten Modul gebaut haben.
Die Persistenzschicht einordnen
0:36 Bevor wir die erste Tabelle anlegen, klären wir eine Frage, die den ganzen Rest bestimmt. Wir haben jetzt nämlich zwei Sorten von Modellen im Projekt, die einander zum Verwechseln ähnlich sehen und völlig Verschiedenes tun. Wer diese Trennung von Anfang an sauber zieht, spart sich später viel Ärger. Wer sie verwischt, merkt es erst, wenn die erste Schemaänderung eine Kundenschnittstelle mitreißt.
0:59 Ein Bild macht es greifbar: Denken Sie an ein Restaurant. Die Speisekarte ist der Vertrag mit dem Gast — sie beschreibt, was er bestellen kann und was er bekommt. Das Lager im Keller ist etwas völlig anderes; dort stehen Kisten, sortiert nach Haltbarkeit und Platz. Beide beschreiben dieselben Zutaten und sind trotzdem grundverschieden organisiert, weil sie verschiedenen Zwecken dienen.
1:22 Genauso hier: Das Pydantic-Modell ist die Speisekarte, das SQLAlchemy-Modell ist das Lager. Und niemand käme auf die Idee, die Speisekarte nach der Regalordnung zu gestalten. Die interessanteste Zeile ist die zweite: Woran ändern sich die beiden Familien? Das Vertragsmodell ändert sich, wenn die Clients etwas anderes brauchen.
1:42 Das Speichermodell ändert sich, wenn die Datenhaltung es verlangt — ein neuer Index, eine ausgelagerte Tabelle. Diese beiden Anlässe haben nichts miteinander zu tun, und sie treten zu verschiedenen Zeiten auf. Deshalb brauchen sie eigene Klassen. Verbunden werden sie über eine einzige Einstellung, die Pydantic erlaubt, Felder direkt von einem Objekt abzulesen statt aus einem Dictionary. Eine Brücke, kein Verschmelzen.
2:08 Die Versuchung ist groß, und sie ist verständlich: Die beiden Klassen sehen zu achtzig Prozent gleich aus, und niemand tippt gern dasselbe zweimal. Der Preis zeigt sich erst später. Eine verschmolzene Klasse bedeutet, dass jede Spaltenänderung potenziell die öffentliche Schnittstelle verändert — und umgekehrt, dass jeder Wunsch eines Clients bis in die Tabelle durchschlägt.
2:28 Sie haben dann zwei Interessengruppen, die an derselben Datei ziehen. Das funktioniert eine Weile erstaunlich gut und wird dann sehr schnell sehr unangenehm. Der dritte Punkt ist der, der am ehesten unbemerkt passiert: ORM-Objekte direkt aus einem Endpunkt zurückzugeben. Das funktioniert nämlich — FastAPI serialisiert das brav — und genau deshalb fällt es nicht auf. Nur schickt Ihr Dienst damit jede Spalte hinaus, die zufällig im Modell steht.
2:55 Ein internes Bearbeitungskennzeichen, ein Fremdschlüssel, vielleicht irgendwann ein Feld, das dort wirklich nichts zu suchen hat. Der Vertrag existiert dann nur noch auf dem Papier. Deshalb: Immer über das Lesemodell hinaus, auch wenn es sich im Moment nach unnötiger Arbeit anfühlt.
ORM-Modelle mit Mapped
3:12 Kommen wir zum Handwerk. SQLAlchemy hat mit der Version 2.0 die Art, wie man Tabellen beschreibt, deutlich verändert — zum Besseren. Wenn Sie ältere Beispiele im Netz finden, sehen die anders aus, und es lohnt sich zu wissen, warum. Der neue Stil bringt nämlich etwas mit, das der alte nicht konnte: Ihr Editor weiß, welchen Typ eine Spalte hat, bevor der Code je gelaufen ist.
3:36 Früher schrieb man die Spalte und ihren Typ zweimal hin: einmal als Python-Attribut, einmal als Datenbanktyp. Die beiden konnten auseinanderlaufen, und der Editor wusste von nichts. Im neuen Stil leitet SQLAlchemy den Spaltentyp aus der ganz normalen Python-Typannotation ab. Sie schreiben also einmal hin, dass die Kennung eine ganze Zahl ist, und beide Welten wissen Bescheid.
3:58 Das ist mehr als Kosmetik: Die Autovervollständigung funktioniert, Tippfehler fallen vor dem Start auf, und ein Typprüfer kann Ihnen sagen, dass Sie gerade einen Text mit einer Zahl vergleichen. Worauf es hier ankommt, sind zwei Muster. Erstens: Manche Zeilen brauchen nur die Annotation. Der Titel ist einfach ein Text, mehr gibt es nicht zu sagen.
4:20 Andere brauchen einen Zusatz — ein Primärschlüssel, ein Fremdschlüssel, ein Vorgabewert. Die Faustregel lautet, den Zusatz nur zu schreiben, wenn er wirklich etwas mitteilt. Zweitens die letzte Zeile: Dort steht, dass die Positionen mit dem Angebot verschwinden. Das ist eine fachliche Aussage, keine technische. Eine Position ohne ihr Angebot ergibt keinen Sinn — also darf sie es auch nicht überleben.
4:46 Diese Tabelle beantwortet die Frage, die man beim Schreiben andauernd hat. Das Prinzip dahinter ist einfach: Die Annotation trägt den Typ, der Zusatz trägt alles, was darüber hinausgeht. Wenn Sie nichts weiter zu sagen haben, sagen Sie auch nichts weiter. Das hält die Modelle lesbar — und lesbare Modelle sind bei Datenbanken besonders wertvoll, weil sie oft das Einzige sind, woraus man den tatsächlichen Aufbau der Tabellen noch ablesen kann.
5:12 Beachten Sie außerdem die Fußnote: Die Basisklasse hat sich geändert. Wo früher eine Fabrikfunktion aufgerufen wurde, erbt man heute schlicht. Fünf Schritte, und der rote Faden ist ein Trichter: von der Struktur zum Detail. Erst die Basis, dann der Tabellenname, dann die Spalten in ihrer einfachsten Form. Zusätze kommen erst danach, und nur dort, wo sie gebraucht werden.
5:34 Ganz am Ende stehen die Beziehungen — sie sind der Teil, den man am besten schreibt, wenn beide Seiten schon existieren. Achten Sie beim letzten Schritt auf die gegenseitige Verknüpfung: Beide Seiten müssen voneinander wissen, sonst pflegt SQLAlchemy die eine Richtung und die andere bleibt veraltet, bis Sie neu laden. Zwei Dinge sollen Sie nach dieser Aufgabe sehen können.
5:56 Das erste ist fachlich: Ein gelöschtes Angebot lässt keine verwaisten Positionen zurück — prüfen Sie das ruhig direkt in der Datenbank nach, das ist überzeugender als jeder Test. Das zweite ist die Typsicherheit: Tippen Sie in Ihrem Editor einen Punkt hinter die Positionsliste. Wenn dort die Felder der Position vorgeschlagen werden, haben Sie die Annotation richtig gesetzt.
6:17 Wenn nicht, fehlt der Typ in der eckigen Klammer — und Sie hätten den Fehler sonst erst zur Laufzeit bemerkt.
Async-Sessions mit Kontextmanagern
6:24 Die Modelle stehen. Jetzt brauchen wir eine Verbindung zur Datenbank — und hier lauert die erste echte Falle des asynchronen Arbeitens. Es geht um zwei Objekte, die leicht verwechselt werden, und um eine Einstellung, die harmlos aussieht und ohne die im asynchronen Betrieb praktisch nichts funktioniert. Nehmen wir uns dafür etwas Zeit, es zahlt sich aus.
6:45 Die Unterscheidung, um die es geht, ist die zwischen Engine und Session. Denken Sie an ein Taxiunternehmen: Die Engine ist das Unternehmen mit seinem Fuhrpark — es gibt genau eines, es wird morgens hochgefahren und verwaltet die Wagen. Die Session ist eine einzelne Fahrt. Sie leiht sich einen Wagen, bringt einen Fahrgast von A nach B und gibt den Wagen zurück. Niemand gründet für jede Fahrt ein neues Unternehmen.
7:08 Genau das passiert aber, wenn man eine Engine pro Anfrage erzeugt — mit entsprechenden Folgen unter Last. Drei Dinge passieren hier, und das Wichtigste steht in der Mitte. Die Engine entsteht einmal, auf Modulebene, beim Start des Prozesses. Die Fabrik hängt daran und produziert bei Bedarf Sessions. Und dann ist da die Einstellung, die verhindert, dass Objekte nach dem Speichern als veraltet gelten — dazu gleich mehr, denn sie ist wichtiger, als sie aussieht.
7:36 Die letzte Funktion legt beim Start die Tabellen an. Beachten Sie den Umweg über eine Hilfsfunktion: Das Anlegen von Tabellen ist eine synchrone Angelegenheit, die hier auf einer asynchronen Verbindung ausgeführt wird. Hier lohnt es sich, die Kette einmal ganz durchzudenken. Standardmäßig markiert SQLAlchemy nach dem Speichern alle Attribute als veraltet — aus gutem Grund, denn die Datenbank könnte sie inzwischen verändert haben.
8:02 Berühren Sie danach ein Attribut, lädt SQLAlchemy es klammheimlich nach. In synchronem Code klappt das. In asynchronem Code gibt es keinen sicheren Ort für so eine versteckte Abfrage, also scheitert der Zugriff. Das Tückische: Der Fehler erscheint nicht beim Speichern, sondern erst dort, wo Sie das Objekt zurückgeben wollen — weit weg von der Ursache.
8:24 Der erste Punkt ist der teuerste. Eine Engine pro Anfrage sieht im Code fast identisch aus wie eine Engine pro Prozess — eine Zeile, an einer anderen Stelle. Der Unterschied zeigt sich erst unter Last: Jede Anfrage baut dann einen eigenen Verbindungspool auf, und die Datenbank sieht plötzlich Hunderte von Verbindungen statt einer Handvoll.
8:42 Das ist einer der Fehler, die in der Entwicklung nie auffallen, weil dort immer nur eine Anfrage gleichzeitig läuft. Prüfen Sie deshalb einmal bewusst nach, wo Ihre Engine tatsächlich entsteht.
Der Lebenszyklus der Session
8:54 Wir haben die Session jetzt benutzt, ohne genau hinzuschauen, was sie eigentlich tut. Das holen wir nach, und zwar gründlich. Denn erst wenn man versteht, wann SQL tatsächlich an die Datenbank geht und was dabei garantiert ist, wird aus dem Kontextmanager-Muster ein Werkzeug statt eines Rituals, das man abschreibt, weil es alle so machen.
9:12 Es sind im Kern nur drei Begriffe, aber zwei davon werden regelmäßig verwechselt — und diese Verwechslung ist teuer, wenn sie erst in Produktion auffällt. Das Muster dahinter heißt Unit of Work, und ein Bild macht es klar: Die Session ist ein Notizblock. Sie schreiben auf, was sich ändern soll — dieses Objekt ist neu, jenes hat sich geändert, das dort soll weg. Nichts davon geht sofort an die Datenbank.
9:36 Erst am Ende wird der ganze Block auf einmal abgearbeitet. Das ist der Grund, warum Sie ein Angebot samt Veranstalter und allen Positionen in einem einzigen Schritt übergeben können. Die Session sortiert die Reihenfolge selbst und schickt alles in einem Rutsch. Die beiden ersten Zeilen werden ständig verwechselt, dabei ist der Unterschied groß.
9:57 Das eine schickt das SQL los, aber innerhalb der offenen Transaktion — nach außen ist noch nichts sichtbar, und alles ist noch zurücknehmbar. Das andere beendet die Transaktion und macht sie dauerhaft. Praktisch heißt das: Nach dem ersten Schritt haben Ihre neuen Objekte ihre Kennung von der Datenbank, aber die Entscheidung, ob das Ganze bleiben soll, ist noch offen.
10:17 Die letzte Zeile ist ebenfalls wichtig: Sie müssen keine Transaktion eröffnen — die erste Anweisung tut das von selbst. Das Muster, um das es hier geht, kommt in fast jeder Anwendung vor: Sie legen etwas an und brauchen sofort dessen Kennung — für eine Antwort, für einen Verweis, für einen Folgeschritt. Die Datenbank vergibt diese Kennung erst beim Schreiben. Also schreiben Sie, ohne die Transaktion zu beenden.
10:41 Danach steht die Kennung im Objekt, und die Entscheidung über das Ganze liegt weiterhin beim Aufrufer. Dieses Zusammenspiel ist der Grund, warum die Repository-Funktion nicht selbst speichert: Sie weiß nicht, ob sie Teil einer größeren Operation ist. Alles oder nichts — das klingt selbstverständlich und ist es nicht. Stellen Sie sich vor, eine Anfrage legt ein Angebot an, und beim Schreiben der Positionen geht etwas schief.
11:06 Ohne Transaktion stünde jetzt ein Angebot ohne Positionen in der Datenbank. Kein Fehler, keine Meldung, nur ein Datensatz, der fachlich Unsinn ist und den irgendwann jemand mühsam aufräumen muss. Mit Transaktion verschwindet alles gemeinsam. Die Datenbank steht danach exakt so da wie vorher, als hätte die Anfrage nie stattgefunden. Das ist der eigentliche Grund, warum es Transaktionen gibt.
11:30 Der zweite Punkt betrifft eine sehr konkrete Situation. Ihre Tabelle für Veranstalter hat eine eindeutige Kontaktadresse — legt jemand denselben Veranstalter zweimal an, wirft die Datenbank einen Fehler. Was der Aufrufer jetzt zu sehen bekommt, entscheidet Ihre Anwendung. Lassen Sie den Fehler durchschlagen, meldet der Dienst einen internen Serverfehler, und es sieht so aus, als sei er kaputt.
11:53 Fangen Sie ihn ab, können Sie ehrlich antworten: Diese Adresse gibt es schon. Der Unterschied ist eine Zeile Code und eine völlig andere Erfahrung auf der anderen Seite. Diese Aufgabe hat einen zweiten Teil, den man leicht übersieht. Das Erste ist klar: Der doppelte Veranstalter soll eine ehrliche Konfliktmeldung ergeben, nicht einen Serverfehler.
12:14 Das Zweite ist subtiler — die Session muss danach wieder benutzbar sein. Nach einem Datenbankfehler ist sie nämlich blockiert, bis Sie sie zurückrollen. Vergessen Sie das, funktioniert die erste Anfrage scheinbar korrekt, und die nächste in derselben Transaktion scheitert unerklärlich. Prüfen Sie also beides: die richtige Antwort und die Arbeitsfähigkeit danach.
Was jetzt trägt
12:36 Die Angebote haben jetzt ein Zuhause. Vertrag und Speicher sind zwei getrennte Familien mit je eigener Freiheit. Der Python-Typ trägt die Spalte, und der Editor prüft mit. Eine Engine je Prozess, eine Session je Anfrage, eine Transaktion je Session — und Fehler enden im Rollback statt im halb geschriebenen Angebot. Was wir bisher nicht gefragt haben: wie oft unsere Endpunkte die Datenbank eigentlich ansprechen.
13:01 Die Antwort ist unangenehmer, als man denkt, und sie ist das Thema des nächsten Moduls.
Lieber mit Trainer? Dieses Modul ist Teil unserer Team-Schulungen — mit Übungen, Ihrem eigenen Code und Fragen, die ein Video nicht beantwortet. Mehr erfahren →