Datenbank-Migrationen schreiben, die bei wiederholter Ausführung nicht brechen
Du deployst eine neue Funktion, die eine zusätzliche Spalte in der Users-Tabelle benötigt. Du schreibst das Migrationsskript, führst es im Staging aus, alles sieht gut aus. Dann führt jemand im Team dasselbe Skript versehentlich erneut aus – bei einem Pipeline-Retry. Jetzt bekommst du einen Fehler: „Spalte existiert bereits.“ Das Deployment schlägt fehl, jemand muss die Datenbank manuell reparieren, und der Release verzögert sich.
Dieses Szenario passiert häufiger, als die meisten Teams zugeben. Die Lösung ist nicht kompliziert, erfordert aber ein Umdenken bei der Erstellung von Migrationsskripten. Du musst sie so schreiben, dass sie mehrfach ausgeführt werden können, ohne Probleme zu verursachen.
Was eine Migration sicher macht
Das Kernprinzip ist Idempotenz. Eine Operation ist idempotent, wenn ihre einmalige oder hundertfache Ausführung denselben Endzustand erzeugt. Es geht nicht darum, die doppelte Ausführung zu verhindern. Es geht darum, sicherzustellen, dass eine doppelte Ausführung keinen Schaden anrichtet.
Überlege, warum ein Skript mehr als einmal laufen könnte. Eine Pipeline bricht mittendrin ab und wird wiederholt. Jemand führt die Migration im Staging aus, vergisst es und führt sie später erneut aus. Zwei Entwickler wenden dieselbe Änderung in verschiedenen Umgebungen zu unterschiedlichen Zeitpunkten an. Ohne Idempotenz kann jedes dieser Szenarien deine Daten korrumpieren oder das Deployment zerstören.
Das einfache Muster: Prüfen vor dem Handeln
Der einfachste Weg, eine Migration idempotent zu machen, ist zu prüfen, ob die Änderung bereits existiert, bevor du sie anwendest. SQL-Datenbanken machen das mit bedingten Anweisungen einfach.
Hier ein konkretes Beispiel. Angenommen, du musst eine last_login_at-Spalte hinzufügen, um Benutzeraktivitäten zu verfolgen:
-- Nicht idempotent: schlägt fehl, wenn die Spalte bereits existiert
ALTER TABLE users ADD COLUMN last_login_at TIMESTAMP;
-- Idempotent: funktioniert, egal ob die Spalte existiert oder nicht
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMP;
Die erste Version wirft einen Fehler, wenn die Spalte bereits existiert. Die zweite Version läuft jedes Mal sicher durch.
Statt zu schreiben:
ALTER TABLE users ADD COLUMN phone_number VARCHAR(20);
Schreibe:
ALTER TABLE users ADD COLUMN IF NOT EXISTS phone_number VARCHAR(20);
Die IF NOT EXISTS-Klausel sorgt dafür, dass das Skript erfolgreich ist, egal ob die Spalte bereits vorhanden ist oder nicht. Derselbe Ansatz funktioniert für Indizes, Constraints und neue Tabellen. PostgreSQL, MySQL und die meisten modernen Datenbanken unterstützen diese bedingten Konstrukte.
Beim Entfernen von Objekten gilt dieselbe Logik. Das Löschen einer nicht existierenden Spalte schlägt fehl. Verwende IF EXISTS, um die Operation sicher zu machen:
ALTER TABLE users DROP COLUMN IF EXISTS old_phone_number;
Datenmigrationen handhaben
Das Hinzufügen und Entfernen von Spalten sind die einfachen Fälle. Die wirkliche Komplexität entsteht, wenn du vorhandene Daten verschieben oder transformieren musst. Angenommen, du teilst eine full_name-Spalte in first_name und last_name auf. Die Migration muss:
- Die beiden neuen Spalten hinzufügen
- Sie aus den vorhandenen Daten befüllen
- Den Fall behandeln, dass das Skript erneut ausgeführt wird
Ein naiver Ansatz würde Daten kopieren, ohne zu prüfen, ob sie bereits kopiert wurden. Eine zweimalige Ausführung erzeugt doppelte Daten oder überschreibt gültige Werte. Ein sichereres Muster sieht so aus:
-- Spalten hinzufügen, falls sie nicht existieren
ALTER TABLE users ADD COLUMN IF NOT EXISTS first_name VARCHAR(100);
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_name VARCHAR(100);
-- Nur befüllen, wenn die Zielspalten leer sind
UPDATE users
SET first_name = SPLIT_PART(full_name, ' ', 1),
last_name = SPLIT_PART(full_name, ' ', 2)
WHERE first_name IS NULL AND last_name IS NULL;
Die WHERE-Klausel stellt sicher, dass das Update nur für Zeilen ausgeführt wird, die noch nicht verarbeitet wurden. Wenn das Skript erneut läuft, sind diese Zeilen bereits befüllt, und das Update tut nichts.
Für komplexere Szenarien musst du möglicherweise Quell- und Zieldaten vergleichen oder eine Prüfsumme verwenden, um zu überprüfen, ob die Transformation das korrekte Ergebnis geliefert hat. Das Prinzip bleibt dasselbe: Gehe niemals davon aus, dass die Daten sich im ursprünglichen Zustand befinden.
Schrittweises Löschen reduziert Risiken
Das Löschen von Spalten oder Tabellen ist riskant, weil du es nicht einfach rückgängig machen kannst. Ein sichererer Ansatz ist, es in Phasen durchzuführen:
- Erste Migration: Spalte in
spaltenname_deprecatedumbenennen - Einige Release-Zyklen warten, um sicherzustellen, dass nichts kaputt geht
- Zweite Migration: Die veraltete Spalte löschen
Dieses Muster gibt deinem Team Zeit, Code zu finden, der noch auf die alte Spalte verweist. Wenn etwas schiefgeht, ist das Umbenennen umkehrbar. Das Löschen nicht.
Führe ein Protokoll über die ausgeführten Migrationen
Idempotenz behandelt den Fall, dass ein Skript mehrfach ausgeführt wird. Du musst aber auch wissen, welche Skripte gelaufen sind, wann sie gelaufen sind und ob sie erfolgreich waren. Das ist deine Prüfspur.
Die meisten Migrationsframeworks wie Flyway oder Liquibase erledigen das automatisch. Sie erstellen eine Tabelle, die jedes Migrationsskript nach Name, Prüfsumme und Ausführungszeitstempel verfolgt. Wenn du rohe SQL-Skripte ohne Framework schreibst, erstelle deine eigene Tracking-Tabelle:
CREATE TABLE IF NOT EXISTS migration_log (
script_name VARCHAR(255) PRIMARY KEY,
started_at TIMESTAMP,
completed_at TIMESTAMP,
status VARCHAR(20),
script_hash VARCHAR(64)
);
Füge vor dem Ausführen einer Migration eine Zeile mit dem Skriptnamen und dem Status „running“ ein. Nach Abschluss aktualisiere den Status auf „success“ oder „failed“. Wenn das Skript fehlschlägt, kann die Pipeline wiederholt werden, und der Migrations-Runner kann prüfen, ob das Skript bereits erfolgreich abgeschlossen wurde.
Dieses Protokoll dient nicht nur dem Debugging. Es ist dein Nachweis für Compliance und Governance. Wenn jemand fragt: „Wer hat wann die Users-Tabelle geändert?“, sollte die Antwort aus dem Protokoll kommen, nicht aus der Erinnerung von jemandem.
Praktische Checkliste für Migrationsskripte
Bevor du eine Migration in der Produktion ausführst, überprüfe diese Punkte:
- Kann das Skript zweimal ohne Fehler ausgeführt werden? Teste es, indem du es zweimal hintereinander auf einer Kopie deiner Datenbank ausführst.
- Prüft das Skript, ob Spalten, Indizes oder Constraints bereits existieren, bevor es sie erstellt?
- Behandelt das Skript bei Datentransformationen bereits transformierte Zeilen sicher?
- Werden Löschungen in Phasen durchgeführt, mit einer Entfernungsfrist vor der endgültigen Löschung?
- Gibt es einen Protokolleintrag für jede Skriptausführung, einschließlich Zeitstempel und Status?
- Kannst du die Änderung rückgängig machen, wenn etwas schiefgeht? Falls nicht, hast du einen Plan?
Das Fazit
Ein Migrationsskript, das bei zweimaliger Ausführung fehlschlägt, ist kein Migrationsskript. Es ist eine Zeitbombe, die darauf wartet, dass jemand eine Pipeline wiederholt. Schreibe jede Migration so, als würde sie mehrfach ausgeführt werden – denn in der Praxis wird sie das wahrscheinlich auch. Prüfe, bevor du erstellst, verifiziere, bevor du transformierst, und protokolliere alles. Dein zukünftiges Ich, das um 2 Uhr morgens ein Deployment debuggt, wird es dir danken.