Boring Stack


3 Min. Lesezeit

Das Binary migriert seine Datenbank selbst

goose, einfache SQL-Dateien, eingebettet und beim Start ausgeführt

Teil sechs des Boring Stack. Die App schreibt ihr SQL selbst. Die Tabellen, gegen die dieses SQL läuft, ändern sich mit der Zeit, und diese Änderungen brauchen einen Ablauf.

Der klassische Ablauf hat einen eigenen Schritt: Vor oder nach dem Deploy lässt jemand das Migrationswerkzeug gegen die Produktionsdatenbank laufen. Manchmal vergisst dieser Jemand es. Dann läuft neuer Code gegen ein altes Schema oder alter Code gegen ein neues.

In meinen Apps gibt es diesen Schritt nicht. Das Binary migriert seine Datenbank beim Start selbst.

Nummerierte SQL-Dateien

Migrationen sind einfache SQL-Dateien in einem Ordner, der Reihe nach nummeriert:

internal/db/migrations/001_users.sql
internal/db/migrations/002_items.sql
internal/db/migrations/003_items_archived_at.sql

Jede Datei hat einen «Up»-Teil und, wo es Sinn ergibt, einen «Down»-Teil, markiert mit Kommentaren, die goose versteht. Eine neue Migration bekommt die nächste Nummer nach der höchsten bestehenden. Sonst nichts: keine generierten Zeitstempel, keine eigene Sprache, keine Migrationsklassen.

goose merkt sich in einer Tabelle in der Datenbank selbst, welche Nummern schon gelaufen sind.

Eingebettet und beim Start ausgeführt

Go kann mit //go:embed Dateien ins Binary einbetten. Der Migrationsordner ist eingebettet, also trägt das Binary jede Migration mit sich, die es braucht. Beim Start, bevor es eine einzige Anfrage beantwortet, führt die App jede Migration aus, die noch nicht gelaufen ist.

Daraus folgt einiges, was ich mag:

  • Ein Deploy ist aus Sicht des Schemas atomar. Das neue Binary startet, bringt das Schema auf den neuen Stand und antwortet erst dann. Es gibt kein Zeitfenster, in dem Code und Schema nicht zusammenpassen.
  • Eine neue Umgebung richtet sich selbst ein. Datenordner löschen, App starten, und man hat eine leere, vollständig migrierte Datenbank. So setze ich meinen lokalen Stand zurück.
  • Tests nutzen dieselben Migrationen. Jede Testdatenbank entsteht durch die echten Migrationen, also laufen die Tests gegen das Schema, das auch die Produktion hat.

Migrationen schreiben, die nicht schaden können

Wenn Migrationen automatisch laufen, müssen sie unbeaufsichtigt sicher sein. Ein paar Regeln:

  • Hinzufügen, nicht umbenennen. Eine Spalte hinzuzufügen ist sicher. Eine umzubenennen bricht das alte Binary, falls man je zurückrollt.
  • Nie eine Migration ändern, die schon gelaufen ist. War eine Migration falsch, schreib eine neue, die es korrigiert. Geänderte Geschichte heisst, dass eine Datenbank eine andere Version ausgeführt hat als die andere.
  • Datenänderungen klein halten. Eine Migration, die beim Start eine Million Zeilen umschreibt, verzögert den Start. Bei SQLite und den Apps, die ich baue, ist das selten ein Problem, aber man sollte daran denken, bevor es eines wird.

Was es kostet

  • Zurückrollen heisst vorwärts korrigieren. Ein älteres Binary macht eine Migration nicht rückgängig. Ich korrigiere stattdessen mit einer neuen Migration, was in der Praxis ohnehin alle tun.
  • Eine fehlgeschlagene Migration stoppt die App. Das ist Absicht. Lieber eine App, die nicht startet, als eine, die gegen ein halb migriertes Schema läuft.
  • Eine App pro Datenbank. Automatische Migrationen setzen voraus, dass die Datenbank der App gehört. Teilten sich mehrere Services eine Datenbank, bräuchte es einen koordinierten Ablauf. Meine tun das nicht.

Wann ich anders entscheiden würde

Wenn mehrere Instanzen einer App gleichzeitig gegen eine geteilte Datenbank starten, würde ich die Migrationen als eigenen Deploy-Schritt laufen lassen, damit sie sich nicht in die Quere kommen. Mit einem Server und einer SQLite-Datei ist das Binary der beste Ort dafür.

Als Nächstes: Tests, und warum sie keine Mocks verwenden.

Mehr in Boring Stack

Teilen