SilentChat
Zurück zum BlogEngineering

Über 200 Migrationen: was wir über Datenbank-Änderungen gelernt haben

Marc Wagner16. September 20263 Min. lesen
TL;DR

SilentChat hat seit März 2026 über 200 Datenbank-Migrationen angesammelt. Die wichtigste Lehre: Eine Migration, die bei jedem Deploy erneut läuft, muss selbst wissen, ob sie schon gewirkt hat — und ein Seeder, der „Erfolg“ meldet, hat nicht unbedingt etwas getan.

Wie Migrationen bei uns laufen

Die Tabellenstruktur legt das ORM an (GORM AutoMigrate). Datenänderungen — Tarife, Texte, Korrekturen — sind SQL-Dateien, die ein Seeder bei jedem Deploy einzeln und namentlich ausführt. Eine Tabelle, die sich merkt, was schon gelaufen ist, gibt es nicht. Jede Datei muss deshalb idempotent sein: Beim zweiten Lauf darf sie nichts mehr ändern.

Lehre 1: Jedes UPDATE braucht eine Bedingung

Ein UPDATE ohne Bedingung überschreibt bei jedem Deploy, was ein Administrator inzwischen geändert hat — ohne Fehlermeldung. Bei Tarifen ist genau das passiert: Ein großer Teil der Migrationen auf die Tarif-Tabelle schrieb bedingungslos. Seitdem prüft ein Wächter, dass jedes UPDATE auf Tarife eine echte Bedingung hat.

„Nur nicht gelöschte Zeilen“ zählt dabei ausdrücklich nicht als Bedingung: Das schützt vor Leichen, nicht vor dem zweiten Lauf.

Lehre 2: Die Reihenfolge ist Teil der Korrektur

Vier Korrekturen an Blogtexten standen im Seeder vor dem Schritt, der die Artikel anlegt. Auf laufenden Installationen fiel das nie auf, denn dort gab es die Artikel längst. Auf einer frischen Installation liefen die Korrekturen ins Leere, und danach kam der unkorrigierte Text. Ein Wächter prüft seitdem, dass jede Korrektur nach ihrem Anlegeschritt steht.

Lehre 3: „Erfolg“ heißt nicht „gewirkt“

  • Mehrere Anweisungen in einer Datei: Der Datenbanktreiber führte ohne Platzhalter nur die erste Anweisung aus — ein Blog-Seed meldete eine betroffene Zeile statt sieben. Der Seeder teilt Dateien seitdem selbst auf.
  • Temporäre Tabellen: Eine Migration legte eine temporäre Tabelle an, die folgenden Anweisungen liefen auf anderen Verbindungen aus dem Pool und fanden sie nicht. Der Seeder meldete trotzdem Erfolg, und doppelte Einträge überlebten drei Läufe. Dateien mit eigener Transaktion laufen seitdem auf einer einzigen Verbindung.
  • Nicht eingetragen: Migrationsdateien lagen im Repository, waren aber im Seeder nicht aufgerufen — beim Deploy lief nichts, gemeldet wurde Erfolg. Ein Wächter prüft seitdem, dass jede Datei eingetragen ist, und seit August bricht der Seeder mit Fehlercode ab, wenn ein Schritt scheitert.

Lehre 4: Das ORM baut keine Zusagen

AutoMigrate legt Tabellen und Spalten an — partielle eindeutige Indizes baut es nicht nach. Eine Zusage wie „es gibt genau einen Standardtarif“ galt deshalb nur dort, wo jemand die zugehörige Migration einmal von Hand gefahren hatte. Eine eigene Migration sichert diese Strukturzusagen heute ab und warnt bei Datenkonflikten, statt den Deploy abzubrechen.

Lehre 5: Seeds, die nichts überschreiben, erreichen niemanden

Inhalts-Seeds schreiben mit ON CONFLICT DO NOTHING, damit Bearbeitungen im Admin erhalten bleiben. Die Kehrseite: Eine Korrektur am Seed-Text erreicht eine laufende Installation nie. Bei den Hilfe-Artikeln betraf das zeitweise 121 von 147. Korrekturen bekommen deshalb eine eigene Migration, die auf den alten Text prüft — so wie die, mit der dieser Artikel eingespielt wurde.

Kleinigkeiten, die Zeit gekostet haben

  • Spaltenname: Ein Go-Feld heißt MetaDesc, sein JSON-Name meta_description. GORM leitet den Spaltennamen vom Feld ab — meta_desc. Wer ihn aus einer API-Antwort abliest, liest den falschen.
  • Fragezeichen: Das ? des JSONB-Operators ist zugleich GORMs Platzhalter. Wir schreiben stattdessen jsonb_exists.
  • Nummern: Zwei Nummern sind doppelt vergeben, eine fehlt. Unsere eigene Regel verlangt Zeitstempel als Präfix — der Bestand nummeriert fortlaufend.
  • Umkehrungen: Zu fast jeder Migration gibt es eine Down-Datei. Automatisch ausgeführt wird keine davon; sie sind ein Notfallplan für Handarbeit.
postgresmigrationschemaengineeringdatabase

Passende Artikel zum Thema