1. Datenbankschema nach app + postgres
Wenn Sie zum ersten Mal app + postgres hochgefahren und gesehen haben, dass der PostgreSQL-Container healthy ist, stellt sich ein trügerisches Gefühl des Sieges ein: "Na gut, die Datenbank ist da, also muss die Anwendung funktionieren." Das ist etwa so, als ob man sich freut, einen Kühlschrank zu haben, bevor man überprüft hat, ob Essen darin ist und ob er nicht ausgesteckt ist. Für das Backend ist "die Datenbank ist hochgefahren" nur die halbe Geschichte.
Lassen Sie uns zwei ähnliche, aber grundlegend unterschiedliche Gedanken trennen. Erstens: PostgreSQL als Prozess läuft und ist bereit, TCP-Verbindungen auf Port 5432 anzunehmen. Das betrifft das Netzwerk und die Readiness. Zweitens: PostgreSQL als Datenbank enthält die benötigten Tabellen, Indizes, Constraints, und das alles entspricht den Erwartungen unserer Anwendung. Das betrifft das Schema und seinen Zustand.
In unserem durchgehenden Projekt Container-Ready Catalog Service verwendet die Anwendung im Profil postgres JPA und erwartet, dass es in der Datenbank Tabellen wie catalog_item und (höchstwahrscheinlich) eine Tabelle für Export-Jobs gibt. Wenn diese Tabellen nicht existieren, wird selbst ein perfekt healthy PostgreSQL für die Anwendung zu einem "sehr selbstbewussten leeren Notizbuch": Eine Verbindung besteht, aber es gibt nichts, womit man arbeiten kann.
Und hier kommt der erwachsene Ingenieursgedanke: Der Zustand des Datenbankschemas sollte Teil des reproduzierbaren Starts sein, genauso wie compose.yaml Teil des reproduzierbaren Hochfahrens der Container ist. Sonst löst Docker zwar das Problem "Wie starte ich einen Dienst?", hinterlässt aber eine Lücke: "Und wie bringe ich die Daten in die richtige Form?"
2. Schema-Migrationen ohne "DevOps-Magie"
Wenn Anfänger das Wort "Migrationen" hören, schaltet sich oft der Alarmmodus ein: "Oh, das ist etwas aus der Welt der Datenbankadministratoren, dahin darf ich nicht." Tatsächlich sind Migrationen in unserem Kontext eine extrem entwicklerfreundliche Sache. Sie sind eine Möglichkeit, Schemaänderungen als Code zu speichern, direkt neben Ihrer Anwendung, damit jeder Entwickler mit einem einzigen Startbefehl den korrekten Datenbankzustand erhält.
Das Datenbankschema ist, vereinfacht gesagt, "wie die Tabellen aufgebaut sind": welche Tabellen es gibt, welche Felder sie haben, welche Constraints, welche Indizes. Wenn Sie in Java-Code ein neues Feld zu einer Entität hinzugefügt haben (oder einfach das Geschäft sagt: "Wir müssen einen Status beim Produkt speichern"), muss in der Datenbank die entsprechende Änderung erscheinen. Und genau das ist eine Migration: ein kleines SQL-Skript (oder anderes Format, aber wir bleiben bei SQL), das eine konkrete Änderung beschreibt: Tabelle erstellen, Spalte hinzufügen, Index erstellen, minimale Startdaten befüllen.
Der entscheidende Punkt: Migrationen sind keine "einmalige Datenbankvorbereitung durch den Autoren des Kurses". Migrationen sind die Quelle der Wahrheit darüber, wie das Schema aussehen soll. Sie liegen im Repository, reisen mit dem Code und werden automatisch beim Start der Anwendung ausgeführt (in unserem Fall über Flyway). Ein neuer Entwickler (oder Sie selbst in einer Woche, wenn Sie alles vergessen haben) wird also nicht im Chat suchen müssen: "Wer erinnert sich, welche Tabellen erstellt werden mussten?"
Ja, Migrationen sind keine "schöne Theorie". Es geht um schlichte Nervenersparnis. Denn das manuelle Erstellen von Tabellen in der lokalen Datenbank endet fast immer damit, dass jeder Entwickler ein etwas anderes Schema hat. Und "ein kleines bisschen anders" ist bei einer Datenbank wie "ein kleines bisschen anderes Passwort": Es funktioniert nicht.
3. Flyway im Start von Spring Boot
Nach dem Wort "Migrationen" kommt meist eine zweite Frage: "Okay, es gibt Migrationen. Aber wer führt sie aus?" Und hier tritt Flyway als ein sehr disziplinierter Manager auf: Er kommt früher als alle anderen zur Arbeit, prüft, was bereits erledigt ist, führt die fehlenden Änderungen aus, und erst danach erlaubt er der Anwendung, "richtig zu starten". Und das tut er nicht als separates Tool irgendwo daneben, sondern als Teil des normalen Starts einer Spring-Boot-Anwendung.
In unserem Projekt ist Flyway eine Bibliothek. Sie fügen eine Abhängigkeit hinzu, Spring Boot sieht sie im Classpath, sieht, dass ein DataSource vorhanden ist, und schaltet automatisch den Migrationsschritt in die Startup-Pipeline ein. Das ähnelt der Autokonfiguration, an die wir bereits gewöhnt sind: Sie schreiben nicht von Hand "Erstelle mir einen Flyway und rufe migrate() auf", sondern fügen einfach die benötigte Abhängigkeit hinzu und geben eine korrekte Konfiguration an.
Es ist sehr wichtig, die Reihenfolge festzuhalten. In der Containerwelt gilt eine Anwendung als "lebendig", solange der Hauptprozess lebt. Für Spring Boot ist das der JVM-Prozess. Wenn Flyway beim Start keine Migrationen anwenden konnte, erreicht Spring Boot normalerweise nicht den Zustand "Started … in N seconds", der Kontext startet nicht, der Prozess wird beendet, und der Anwendungscontainer stoppt entweder oder startet neu (wenn Sie eine Restart-Richtlinie aktiviert haben, aber in unserem grundlegenden Lernsetup konzentrieren wir uns nicht darauf).
Es ist praktisch, den Start als eine Kette von Schritten zu betrachten, bei der jeder nächste vom vorherigen abhängt:
flowchart TD
%% Wichtig: Der Healthcheck von Postgres betrifft das Netzwerk und die Prozessverfügbarkeit, nicht das fertige Schema
A["docker compose up"] --> B["PostgreSQL container startet"]
B --> C["Healthcheck (pg_isready) = healthy"]
%% Flyway ist in den Anwendungsstart eingebaut und wird vor dem finalen Started ausgeführt
C --> D["App container startet"]
D --> E["Spring Boot startet ApplicationContext"]
E --> F["DataSource wird erstellt (Verbindung zu postgres)"]
F --> G["Flyway wendet Migrationen an"]
G --> H["Anwendung erreicht Started + beginnt, Port zu hören"]
Beachten Sie die Feinheit: Die Readiness der Datenbank (pg_isready) garantiert nur, dass die Datenbank bereit ist, Verbindungen anzunehmen, aber nicht, dass sich das Schema im korrekten Zustand befindet. Flyway ist die nächste "Kontrollstation", die eine einfach verfügbare Datenbank in eine für die Anwendung nutzbare Datenbank verwandelt.
Bisher gab es lokal möglicherweise einfachere Wege: Jemand erstellt Tabellen von Hand, jemand lässt spring.jpa.hibernate.ddl-auto stehen, damit Hibernate das Schema selbst anpasst. Für schnelle Experimente ist das manchmal in Ordnung, aber im postgres-Modus des Kurses wird Flyway zum Eigentümer der Schemaevolution. Hibernate sollte hier nicht parallel mit den Migrationsdateien konkurrieren, sonst hat das Projekt zwei Quellen der Wahrheit: eine lebt in SQL-Dateien, die andere im Laufzeitverhalten des ORM.
4. Einbindung von Flyway und das Profil postgres
Lassen Sie uns die Idee nun auf unseren Container-Ready Catalog Service anwenden. Hier ist es wichtig zu sehen, dass Flyway standardmäßig nicht "in Docker Compose als separater Container lebt", kein "Tool ist, das man manuell startet" und keinen separaten Dienst in compose.yaml benötigt. In unserem Lern-Baseline ist Flyway Teil der Anwendung und lebt daher dort, wo auch der Code lebt.
Auf Gradle-Ebene sieht das sehr unspektakulär aus: Abhängigkeit hinzugefügt – und fertig. In build.gradle.kts könnte das etwa so aussehen (stark vereinfacht, nur das, was jetzt wichtig ist):
dependencies {
// JPA: im Profil postgres arbeiten wir über ORM und DataSource
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
// Flyway: bringt das Schema beim Start in den erwarteten Zustand
implementation("org.flywaydb:flyway-core")
// Postgres-Treiber: ohne ihn kann die Anwendung keine Verbindung zur Datenbank herstellen
runtimeOnly("org.postgresql:postgresql")
}
Der Sinn ist einfach: JPA benötigen wir für die Arbeit mit Postgres im Profil postgres, der PostgreSQL-Treiber wird benötigt, um überhaupt eine Verbindung herstellen zu können, und Flyway wird benötigt, um das Schema beim Start in den erwarteten Zustand zu versetzen. Spring Boot verbindet dies durch seine Autokonfiguration zu einem einheitlichen Mechanismus.
Weiterhin muss Flyway eine Verbindung zur selben Datenbank herstellen wie die Anwendung. Daher steht Flyway im Profil postgres (z. B. in application-postgres.yml) neben den Einstellungen für datasource, und das ist sehr logisch. Ungefähr so:
spring:
datasource:
# JDBC-URL zu Postgres
url: jdbc:postgresql://postgres:5432/catalog
# Zugangsdaten, unter denen die Anwendung eine Verbindung zur Datenbank herstellt
username: catalog
password: catalog
flyway:
# Im Lernprojekt sind Migrationen Teil des Starts (standardmäßig true, aber explizit ist beruhigender)
enabled: true
Beachten Sie, dass hier der Host postgres lautet, also der Name des Dienstes innerhalb von Compose. Das ist dasselbe Prinzip, das wir bereits verinnerlicht haben: Innerhalb von Compose bedeutet localhost fast immer "der eigene Container", nicht "der benachbarte Dienst".
Und schließlich das "Containermäßigste": Flyway ergibt nur dann Sinn, wenn die Anwendung tatsächlich im postgres-Modus läuft, d. h. wenn das Profil postgres aktiviert ist. In compose.yaml machen wir dies über eine Environment-Variable. In minimaler Form sieht das so aus:
services:
app:
environment:
# Aktiviert das Profil, in dem es eine externe DataSource und folglich Flyway gibt
SPRING_PROFILES_ACTIVE: postgres
depends_on:
postgres:
# Warten, bis Postgres bereit ist, TCP-Verbindungen anzunehmen
condition: service_healthy
Diese Stelle ist sehr bezeichnend: Compose garantiert die Reihenfolge und eine minimale Bereitschaft der Abhängigkeit, das Spring-Boot-Profil schaltet den Speichermodus um, und Flyway wird genau in diesem Modus Teil des Startup-Pfades der Anwendung.
5. Readiness-Signale und Bereitschaft der Anwendung
Als wir das Readiness-Modell gelernt haben, bekämpften wir den Effekt "manchmal startet es, manchmal nicht". Wir haben einen Healthcheck hinzugefügt, depends_on.condition: service_healthy gesetzt, und jetzt stürzt die Anwendung nicht mehr nur ab, weil die Datenbank "noch nicht aufgewacht ist". Das war ein wichtiger Schritt. Aber er löst immer noch nur die Netzwerkseite der Bereitschaft.
Mit dem Auftauchen von Flyway kommt eine zweite Bereitschaftsebene hinzu – die logische. Die Anwendung kann sich hervorragend mit der Datenbank verbinden, aber dann abstürzen, weil die Migrationen nicht angewandt wurden. Und das ist kein Fehler von Docker und keine "Laune von Spring", sondern ein ehrliches Signal: Die Datenbank ist verfügbar, aber es war nicht möglich, sie in den erwarteten Zustand zu versetzen.
Um Verwirrung zu vermeiden, ist es hilfreich, eine einfache Tabelle der Unterschiede im Kopf zu behalten. Sie ist sehr "nicht akademisch", rettet aber hervorragend vor Chaos im Kopf:
| Signal | Worüber es spricht | Was es nicht garantiert |
|---|---|---|
| postgres = healthy | PostgreSQL ist bereit, Verbindungen anzunehmen | Dass das Schema bereits erstellt wurde und den Erwartungen der Anwendung entspricht |
| Container app gestartet | Der JVM-Prozess wurde gestartet | Dass die Anwendung den Arbeitszustand erreicht hat und auf HTTP zu antworten beginnt |
| In den Logs gibt es Successfully applied ... migrations | Flyway hat die Migrationen erfolgreich angewandt | Dass es danach keine Probleme geben wird (aber immerhin ist das Schema hochgefahren) |
| In den Logs gibt es Started ... | Spring Boot hat den Zustand "gestartet" erreicht | Dass ein bestimmter Geschäfts-Endpunkt funktioniert (aber das ist eine andere Prüfebene) |
Aus praktischer Sicht bedeutet dies, dass man ab heute das Wort "gestartet" vorsichtiger verwenden sollte. "Postgres gestartet" ist eines. "Die Anwendung gestartet" ist ein zweites. Und "die Anwendung gestartet und die Migrationen durchgelaufen" ist bereits eine dritte, erwachsenere Vertrauensebene.
Und ja, das ist genau der Moment, in dem ein Entwickler aufhört, den Container als "magische Box" zu betrachten, und beginnt, eine Kette von Ursachen und Wirkungen zu sehen. Manchmal ist das ein wenig traurig, weil wir jetzt mehr Verantwortung haben. Aber es ist erfreulich, weil wir jetzt weniger Magie und mehr Kontrolle haben.
6. Logs und Workflow von Flyway
Erfolgreicher Start: Flyway-Logs
Wir haben uns bereits daran gewöhnt, Logs nicht als "Rauschen" zu betrachten, sondern als lebenswichtiges Signal. Mit Flyway wird das noch wichtiger, weil Migrationen Teil des Starts sind. Daher hat ein erfolgreicher Anwendungsstart im postgres-Modus nun ein charakteristisches Muster in den Logs: Zuerst beginnt Spring Boot hochzufahren, dann erscheint ein Flyway-Block, dann erreicht die Anwendung "Started".
In der Realität können die Zeilen abweichen (Versionen, Logger-Format, Details), aber der Sinn ist normalerweise erkennbar. Ein Beispiel dafür, wie ein erfolgreicher Start logisch aussieht:
# Start der JVM/Anwendung
app-1 | Starting CatalogApplication
# Flyway wird aktiviert und beginnt mit der Prüfung/Anwendung von Migrationen
app-1 | Flyway Community Edition ...
# Schlüsselsignal: Migrationen erfolgreich angewandt
app-1 | Successfully applied 2 migrations
# Endgültiges Signal: Spring Boot hat "Started" erreicht
app-1 | Started CatalogApplication
In diesem Ausschnitt ist nicht wichtig, dass es "2 Migrationen" sind (diese Zahl ändert sich, wenn das Projekt wächst), sondern dass die Reihenfolge zeigt: Die Migrationen befinden sich zwischen dem Startbeginn und dem finalen "Started".
Und hier bricht bei Anfängern oft der alte Reflex: "Ich habe gesehen, dass der Container app im Status Up ist – also ist alles in Ordnung." Jetzt stimmt das nicht mehr ganz. Der Container kann tatsächlich in den ersten Sekunden den Status Up haben, aber wenn Flyway abstürzt, startet Spring Boot nicht, der Prozess wird beendet, und der Container wird zu Exited. Daher ist es kein Zeichen von "wenn etwas schlecht läuft", sondern ein normaler Teil des Starts, in die Logs zu schauen.
Für Compose ist das besonders praktisch, weil man den Log-Stream für den gesamten Stack gleichzeitig lesen kann. Zum Beispiel werden Sie in einem Moment sehen, dass Postgres hochgefahren ist, und dann die Anwendung die Migrationen verarbeitet hat. (Die Befehle haben wir bereits früher gelernt, hier festigen wir nur den Gedanken, dass sie täglich benötigt werden.)
Flyway im Arbeitsprozess
Irgendwann möchte man fragen: "Kann ich nicht einfach einmal die Datenbank hochfahren, von Hand die Tabellen erstellen und mich dann nicht mehr damit quälen?" Kann man. Genau so haben es tausende Projekte gemacht, und genau deshalb haben tausende Projekte später gelitten. Denn der manuelle Zustand einer Datenbank ist wie "Ich erinnere mich genau an das Passwort, habe es nicht aufgeschrieben". Bis zum ersten Urlaub, Laptop-Wechsel oder neuen Kollegen.
Docker und Compose haben unseren Umgebungsstart auf Containerebene reproduzierbar gemacht: gleiche Postgres-Versionen, gleiche Ports, gleiche Env-Vars, gleiche Befehle. Flyway macht das reproduzierbar, was innerhalb der Datenbank ist: Tabellen, ihre Struktur, ein minimaler Startzustand und die Reihenfolge der Änderungen.
Besonders wichtig ist dieser Gedanke in Verbindung mit dem named volume von PostgreSQL. Wir wissen bereits, dass die Datenbankdaten länger leben als der Container. Das bedeutet, dass die Datenbank Dutzende von up/down-Vorgängen überleben kann, und mit ihr auch das Schema und die Migrationshistorie. Flyway wird beim nächsten Start nicht "jedes Mal alles neu erstellen", sondern versteht, welche Migrationen bereits angewandt wurden und welche nicht, und führt nur die fehlenden aus. Dadurch wird die lokale Umgebung nicht nur "startbar", sondern "lebendig in einer verständlichen Evolution".
Und genau deshalb nennen wir Flyway heute einen Teil des Anwendungsstarts. Denn auf der Gewohnheitsebene sollte es so aussehen: Sie haben den Stack hochgefahren – Flyway hat seine Arbeit erledigt – das Schema entspricht der Code-Version – Sie kümmern sich um die Aufgabe, nicht um Schamanentum rund um die Datenbank.
Hier reicht ein einziger Arbeitsgedanke: Migrationen sind ein genauso gültiges Artefakt des Projekts wie Dockerfile oder compose.yaml, und Flyway ist der Mechanismus, der dieses Artefakt beim Start ausführbar macht.
7. Typische Fehler bei der Arbeit mit Flyway
Bei diesem Thema passieren Fehler oft nicht, weil es "schwierig" ist, sondern weil im Kopf verschiedene Bereitschaftsebenen vermischt werden: Container, Netzwerk, Datenbank als Prozess und Datenbank als Schema. Hinzu kommt die Gewohnheit, "schnell von Hand zu reparieren", was auf kurze Distanz wie eine Rettung wirkt, auf lange Sicht aber zu einem ständigen Rätsel wird. Lassen Sie uns die häufigsten Fallstricke durchgehen.
Fehler Nr. 1: Flyway als separaten manuellen Schritt "daneben" betrachten.
Anfänger denken manchmal, dass Migrationen separat gestartet werden müssen: mit einem separaten Befehl, einem separaten Tool, einem separaten Container. In echten Teams wird das manchmal so gemacht, aber in unserem Lern-Baseline ist Flyway in die Spring-Boot-Anwendung eingebaut und arbeitet als Teil des Starts. Das ist methodisch wichtig: Sie müssen die Migrationen in den Logs zusammen mit dem Start sehen und verstehen, dass die Anwendung ohne sie nicht "bereit" ist.
Fehler Nr. 2: Denken, dass die Anwendung starten muss, wenn PostgreSQL healthy ist.
Der Healthcheck der Datenbank löst nur die Readiness der Datenbank als Dienst. Aber die Anwendung kann abstürzen, weil die Migrationen nicht angewandt oder nicht vollständig angewandt wurden. Das ist unangenehm, aber ehrlich: Die Datenbank ist verfügbar, aber das Schema ist nicht fertig. Ab heute gilt: "healthy postgres" bedeutet "man kann mit dem Anwendungsstart beginnen", nicht "alles ist schon bereit".
Fehler Nr. 3: Migrationen in jedem Profil erwarten, auch im standalone-Modus.
Im standalone-Modus haben wir keine externe Datenbank: In-Memory-Speicherung, schneller Start, minimale Abhängigkeiten. Daher ist Flyway eine Geschichte des Profils postgres, in dem es einen echten DataSource gibt. Wenn Sie standalone starten und "Flyway nicht in den Logs sehen", ist das kein Defekt, sondern korrektes Verhalten.
Fehler Nr. 4: Das Migrationsproblem durch Deaktivieren von Flyway "beheben".
Manchmal möchte man spring.flyway.enabled=false setzen, damit die Anwendung "wenigstens startet". Das ähnelt der Idee "Die Glühbirne ist durchgebrannt – ich klebe sie mit Klebeband zu". Die Anwendung kommt vielleicht weiter, aber dann beginnt sie bereits bei der Arbeit mit den Tabellen zu scheitern, und Sie verlieren die Hauptquelle der Wahrheit über das Schema. Wenn Migrationen brechen, ist das ein Signal, die Quelle der Wahrheit (die Migrationen) zu korrigieren, nicht den Mechanismus zu deaktivieren.
Fehler Nr. 5: Das Schema von Hand direkt in der Datenbank ändern und vergessen, die Migrationen zu aktualisieren.
Manuelle Änderungen in der lokalen Datenbank erzeugen eine "Zustandsdrift": Sie haben ein Schema, der Kollege ein anderes, CI ein drittes (falls es überhaupt existiert) und Ihr zukünftiges Ich in einer Woche ein viertes. Flyway wurde entwickelt, um das Schema versioniert und reproduzierbar zu halten. Sobald Sie eine "kleine manuelle Korrektur" vornehmen und sie nicht als Migration festhalten, kehren Sie zurück in die Welt des Chaos, vor der Docker/Compose uns eigentlich bewahren sollten.
GO TO FULL VERSION