Datenbank und Migrationen

Goat verwendet PostgreSQL, das in Docker ausgeführt wird, sodass du keinen Datenbankserver direkt auf dem Host installieren oder konfigurieren musst.

Die lokale Umgebung speichert PostgreSQL-Daten im Verzeichnis:

.cache/postgres

Dadurch gehen die Daten nicht verloren, wenn der Container gestoppt oder neu gestartet wird.

Dieser Ansatz ermöglicht komfortables lokales Arbeiten und sorgt gleichzeitig für eine reproduzierbare Umgebung auf verschiedenen Computern und Betriebssystemen.

Datenbank starten

Um nur die Datenbank zu starten, führe Folgendes aus:

go run ./scripts run:script --path=herd/db/run.goat

Nach dem Start ist PostgreSQL auf folgendem Port verfügbar:

5433

Die Verbindungsparameter werden aus der lokalen Datei .env gelesen.

Goat verwendet Werte mit folgendem Präfix:

GOAT_DB_MAIN_*

Dort werden unter anderem die für die Datenbankverbindung benötigten Angaben definiert, etwa Datenbankname, Benutzer, Passwort und Host.

Lege keine echten Produktionsgeheimnisse im Repository ab.

Persistenz lokaler Daten

Der PostgreSQL-Container kann ohne Datenverlust gestoppt und neu gestartet werden, da sich die eigentlichen Datenbankdateien außerhalb seines temporären Dateisystems befinden.

In der lokalen Umgebung werden die Daten gespeichert in:

.cache/postgres

Das bedeutet:

  • Ein Neustart des Containers löscht die Datenbank nicht.
  • Ein Neustart der Entwicklungsumgebung bewahrt die Daten.
  • Migrationen können auf dem bestehenden lokalen Zustand ausgeführt werden.
  • Du kannst am Projekt arbeiten, ohne die Datenbank jedes Mal von Grund auf neu erstellen zu müssen.

Wenn du eine saubere Umgebung benötigst, verwende das dafür vorgesehene Bereinigungsskript, statt PostgreSQL-Dateien manuell zu löschen.

Migrationen

Migrationen führst du mit folgendem Befehl aus:

goat run:script --path=herd/db/migrate.goat

Das Skript bereitet die für die Ausführung der Migrationen erforderliche Umgebung vor.

Der typische Ablauf umfasst:

PostgreSQL starten
        ↓
Anwendung generieren
        ↓
CLI bauen
        ↓
db:migrate ausführen

Dadurch hängen Migrationen nicht davon ab, dass Entwickler die einzelnen Schritte manuell ausführen.

Initiales Datenbankschema

Das Basisschema der Anwendung wird anhand des Modells generiert, das definiert ist in:

herd/_model.goat

Das generierte SQL befindet sich in:

app/static/raw/data/sql/migrations/0001_schema.sql

Die Datei enthält die anfängliche Datenbankstruktur, die sich aus dem Anwendungsmodell ergibt.

Sie kann unter anderem Folgendes umfassen:

  • Tabellen,
  • Spalten,
  • Datentypen,
  • Schlüssel,
  • Beziehungen,
  • Indizes,
  • aus dem Modell abgeleitete Einschränkungen.

Dadurch bleibt die Konsistenz zwischen dem Domänenmodell der Anwendung und dem grundlegenden PostgreSQL-Schema erhalten.

Modell und Migrationen

Eine Änderung von:

herd/_model.goat

kann sich auf die Struktur des generierten Schemas auswirken.

Beispielsweise:

Feld zu einer Entität hinzufügen
        ↓
goat re
        ↓
Änderung des generierten Modells
        ↓
Änderung des SQL

Das bedeutet jedoch nicht, dass jede Modelländerung automatisch eine sichere Migration einer bestehenden Datenbank ist.

Dies ist besonders wichtig, wenn die Datenbank bereits Daten enthält oder von anderen Benutzern verwendet wird.

Beispiele für Änderungen, die zusätzliche Aufmerksamkeit erfordern:

  • Löschen einer Spalte,
  • Ändern eines Datentyps,
  • Hinzufügen eines Feldes NOT NULL,
  • Ändern einer Beziehung,
  • Löschen einer Tabelle,
  • Ändern von Schlüsseln oder Einschränkungen,
  • Umstrukturieren von bereits in der Datenbank gespeicherten Daten.

Der Generator kann die Zielstruktur beschreiben, aber der sichere Übergang vom aktuellen Datenzustand zum neuen Schema kann eine manuell vorbereitete Migration erfordern.

Migrationen prüfen

Bevor du Schemaänderungen auf einer Datenbank mit wichtigen Daten anwendest, prüfe das generierte oder vorbereitete SQL.

Insbesondere sollte überprüft werden:

  • ob die Migration keine Daten löscht,
  • ob die Änderung des Spaltentyps für vorhandene Werte möglich ist,
  • ob neue Felder geeignete Standardwerte besitzen,
  • ob neue Einschränkungen von vorhandenen Datensätzen erfüllt werden,
  • ob die Migration keine kostspielige Sperre einer großen Tabelle verursacht,
  • ob ein sicheres Zurückrollen der Änderung möglich ist.

Gehe besonders vorsichtig mit Operationen wie den folgenden um:

DROP TABLE
DROP COLUMN
ALTER COLUMN

da sie zu einem irreversiblen Datenverlust führen können.

Lokale Datenbank bereinigen

Um die lokale Datenbank zu bereinigen, führe Folgendes aus:

goat run:script --path=herd/db/clean.goat

Das Skript löscht die Daten und die Struktur der lokalen Datenbank, sodass die Arbeit von einem sauberen Zustand aus begonnen werden kann.

Dies kann unter anderem nützlich sein, wenn:

  • du die Initialisierung des Projekts erneut testen möchtest,
  • du das Modell auf eine Weise geändert hast, die nicht mit der lokalen Datenbank kompatibel ist,
  • du Migrationen ausgehend von einem leeren Schema testest,
  • du Fixture-Daten wiederherstellen möchtest,
  • die lokalen Daten nicht mehr der aktuellen Version der Anwendung entsprechen.

Vorsicht bei destruktiven Operationen

herd/db/clean.goat führt destruktive Operationen aus.

Es kann dauerhaft Folgendes löschen:

  • Tabellen,
  • Daten,
  • den lokalen Datenbankstatus.

Verwende es daher nur, wenn du sicher bist, dass die Konfiguration auf die richtige Umgebung verweist.

Vor der Ausführung solltest du die Werte von GOAT_DB_MAIN_* in .env überprüfen, insbesondere wenn das Projekt sich mit mehr als einer Datenbank verbinden kann.

Betrachte clean.goat nicht als Werkzeug zur Verwaltung einer Produktionsumgebung.

Typischer Workflow bei Modelländerungen

In einer Entwicklungsumgebung kann eine Änderung der Datenstruktur wie folgt aussehen:

# Modell ändern
vim herd/_model.goat

# Änderungen generieren
goat re

# generiertes SQL und Code prüfen
git diff

# Migrationen ausführen
goat run:script --path=herd/db/migrate.goat

# Tests ausführen
goat run:script --path=herd/test.goat

Wenn sich die lokale Datenbank in einem Zustand befindet, der nicht mit dem aktuellen Modell kompatibel ist, und die Daten nicht erhalten werden müssen, kannst du sie bereinigen:

goat run:script --path=herd/db/clean.goat
goat run:script --path=herd/db/migrate.goat

Migrationen in gemeinsam genutzten Umgebungen

Ein Ansatz, der während der lokalen Entwicklung bequem ist, sollte nicht automatisch auf Test-, Staging- oder Produktionsumgebungen übertragen werden.

Bei einer von anderen genutzten Datenbank reicht eine reine Modelländerung nicht aus.

Vor der Bereitstellung einer Schemaänderung:

  1. prüfe den Unterschied zwischen dem aktuellen und dem Zielschema,
  2. überprüfe das von der Migration ausgeführte SQL,
  3. bewerte die Auswirkungen der Migration auf vorhandene Daten,
  4. erstelle ein aktuelles Backup,
  5. teste die Migration auf einer Kopie realer Daten,
  6. schätze die Ausführungszeit und mögliche Sperren ab,
  7. bereite eine Möglichkeit zum Zurückrollen oder zur Reparatur einer fehlgeschlagenen Migration vor,
  8. führe die Änderung erst dann in der Zielumgebung aus.

Besonders wichtig ist das Testen von Migrationen mit Daten, deren Struktur und Umfang der Produktion ähneln. Eine Migration, die auf einer leeren lokalen Datenbank korrekt funktioniert, kann sich bei einer großen Tabelle mit Millionen von Datensätzen völlig anders verhalten.

Der Generator ersetzt keine Strategie für Datenmigrationen

Goat kann die aus dem Modell resultierende Struktur generieren, sollte jedoch nicht als automatische Antwort auf jedes Problem im Zusammenhang mit Datenänderungen betrachtet werden.

Es besteht ein wesentlicher Unterschied zwischen:

Zielschema

und:

sicherem Weg für den Übergang
vom aktuellen Schema
zum Zielschema

Beispielsweise kann das Hinzufügen einer erforderlichen Spalte mehrere Schritte erfordern:

Hinzufügen einer optionalen Spalte
        ↓
Auffüllen bestehender Daten
        ↓
Bereitstellung von Code, der das neue Feld verwendet
        ↓
Hinzufügen der NOT-NULL-Einschränkung

Ebenso kann eine Änderung des Datentyps oder der Beziehungsstruktur eine schrittweise durchgeführte Datenmigration erfordern.

Der Generator hilft dabei, die Struktur der Anwendung beizubehalten, aber die Verantwortung für die Datensicherheit liegt weiterhin beim Migrationsprozess.

Bewährte Praxis

Betrachte herd/_model.goat als Beschreibung des Anwendungsmodells und nicht als Garantie für eine sichere Migration bestehender Daten.

Bei lokalen Änderungen kannst du die Anwendung schnell neu generieren und die Datenbank neu aufbauen.

Bei Änderungen, die gemeinsam genutzte oder Produktionsumgebungen betreffen, analysiere stets die Auswirkungen auf das bestehende Schema und die Daten.

Die wichtigste Regel lautet:

Der Generator kann die Zielstruktur der Datenbank beschreiben, aber der sichere Übergang zwischen aufeinanderfolgenden Datenversionen erfordert eine bewusst geplante Migration.