Datenbankmigrationen mit Flyway in Spring Boot
1. Überblick
Wenn sich Ihre Anwendung ändert, muss sich auch das Datenbankschema ändern. Wenn Sie jedes ALTER TABLE manuell in jeder Umgebung ausführen, werden die Schemas von Entwicklung, Staging und Production früher oder später auseinanderlaufen. Flyway löst dieses Problem: Jede Schemaänderung ist eine versionierte Migrationsdatei, die automatisch und in der richtigen Reihenfolge angewendet wird.
In diesem Artikel konfigurieren Sie Flyway für eine Spring Boot 3-Anwendung, schreiben Migrationen in SQL und Java, testen sie mit JUnit 5 und erfahren, wie Sie häufige Fehler wie Checksum-Nichtübereinstimmungen beheben.
2. Abhängigkeiten installieren
Spring Boot verwaltet die Version von Flyway bereits, daher müssen Sie keine Version angeben. Im folgenden Beispiel wird PostgreSQL verwendet:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Ab Flyway 10 (im Lieferumfang von Spring Boot 3.3+) wird die Unterstützung für verschiedene Datenbanktypen in separate Module aufgeteilt. Ohne flyway-database-postgresql startet die Anwendung nicht und meldet einen Fehler:
org.flywaydb.core.api.FlywayException: Unsupported Database: PostgreSQL 16.4
H2 ist bereits in flyway-core enthalten, daher benötigt der Test-Bereich kein zusätzliches Modul.
3. Grundkonfiguration
Mit flyway-core im Classpath erstellt Spring Boot automatisch das Flyway-Bean und führt Migrationen aus, bevor die EntityManagerFactory initialisiert wird. Minimale Konfiguration in application.properties:
spring.datasource.url=jdbc:postgresql://localhost:5432/shop
spring.datasource.username=shop
spring.datasource.password=secret
spring.jpa.hibernate.ddl-auto=validate
spring.flyway.locations=classpath:db/migration
spring.flyway.validate-on-migrate=true
Wichtige Zeilen:
spring.jpa.hibernate.ddl-auto=validate: Hibernate überprüft nur, ob Entities dem Schema entsprechen. Es ändert das Schema nicht automatisch. Flyway ist die einzige Quelle der Wahrheit für das Schema.spring.flyway.locations: Verzeichnis mit den Migrationen. Der Standardwert ist bereitsclasspath:db/migration. Hier wird er zur Verdeutlichung angegeben.spring.flyway.validate-on-migrate=true: Vor der Migration überprüft Flyway die Checksum der bereits ausgeführten Migrationen. Dies ist auch der Standardwert.
Hinweis: Verwenden Sie nicht
ddl-auto=updateodercreategleichzeitig mit Flyway. Hibernate und Flyway würden beide das Schema ändern, was zu unvorhersehbaren Ergebnissen führt.
4. SQL-Migrationen schreiben
4.1. Benennungskonventionen
Flyway erkennt Migrationen an ihrem Dateinamen:
V1__create_customer_table.sql
│ │ └── Beschreibung (Unterstriche werden als Leerzeichen interpretiert)
│ └── zwei Unterstriche als Trennzeichen
└── V = versionierte Migration, gefolgt von der Versionsnummer
Die Version kann 1, 2, 1.1 oder 20261016.1 sein. Flyway vergleicht Versionen numerisch, nicht alphabetisch. Daher wird V10 nach V9 ausgeführt.
4.2. Erste Migration
Erstellen Sie die Datei src/main/resources/db/migration/V1__create_customer_table.sql:
CREATE TABLE customer (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
full_name VARCHAR(100) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Danach folgt V2__create_orders_table.sql:
CREATE TABLE orders (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
customer_id BIGINT NOT NULL REFERENCES customer (id),
total NUMERIC(12, 2) NOT NULL,
status VARCHAR(20) NOT NULL
);
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
Beim Starten der Anwendung wird das Log etwa so aussehen:
Flyway Community Edition 10.x by Redgate
Database: jdbc:postgresql://localhost:5432/shop (PostgreSQL 16.4)
Successfully validated 2 migrations (execution time 00:00.012s)
Creating Schema History table "public"."flyway_schema_history" ...
Current version of schema "public": << Empty Schema >>
Migrating schema "public" to version "1 - create customer table"
Migrating schema "public" to version "2 - create orders table"
Successfully applied 2 migrations to schema "public", now at version v2
4.3. Die flyway_schema_history-Tabelle
Flyway zeichnet jede ausgeführte Migration in der Tabelle flyway_schema_history auf. Sie können diese direkt abfragen:
SELECT installed_rank, version, description, checksum, success
FROM flyway_schema_history
ORDER BY installed_rank;
installed_rank | version | description | checksum | success
----------------+---------+-----------------------+-------------+---------
1 | 1 | create customer table | -1253721480 | t
2 | 2 | create orders table | 873416502 | t
Bei der nächsten Anwendung sieht Flyway, dass das Schema bereits auf Version 2 ist, und führt nichts erneut aus. Die checksum-Spalte ist der Schlüssel zu Abschnitt 7.
4.4. Wiederholbare Migrationen
Dateien, die mit R__ beginnen, sind wiederholbare Migrationen. Sie haben keine Versionsnummer. Flyway führt diese Datei jedes Mal aus, wenn sich die Checksum ändert, und führt sie immer nach allen versionierten Migrationen aus. Dieser Typ ist ideal für Views, Funktionen oder gespeicherte Prozeduren:
-- R__order_summary_view.sql
CREATE OR REPLACE VIEW order_summary AS
SELECT c.id AS customer_id, c.full_name, COUNT(o.id) AS order_count
FROM customer c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.full_name;
5. Java-Migrationen
Einige Änderungen lassen sich nur schwer mit reinem SQL schreiben, zum Beispiel das Backfill von Daten, die String-Verarbeitungslogik erfordern. In diesem Fall schreiben Sie eine Java-Migration, die von BaseJavaMigration erbt.
Fügen Sie zunächst eine Spalte über SQL in V3__add_customer_email.sql hinzu:
ALTER TABLE customer ADD COLUMN email VARCHAR(150);
Platzieren Sie dann die folgende Klasse im Package db.migration unter src/main/java/db/migration/V4__Backfill_customer_email.java:
package db.migration;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
import java.text.Normalizer;
public class V4__Backfill_customer_email extends BaseJavaMigration {
@Override
public void migrate(Context context) {
var jdbc = new JdbcTemplate(
new SingleConnectionDataSource(context.getConnection(), true));
jdbc.query("SELECT id, full_name FROM customer WHERE email IS NULL", rs -> {
long id = rs.getLong("id");
String email = toSlug(rs.getString("full_name")) + "." + id + "@example.com";
jdbc.update("UPDATE customer SET email = ? WHERE id = ?", email, id);
});
}
private static String toSlug(String name) {
String noAccent = Normalizer.normalize(name, Normalizer.Form.NFD)
.replaceAll("\\p{M}", "")
.replace('đ', 'd').replace('Đ', 'D');
return noAccent.toLowerCase().trim().replaceAll("\\s+", ".");
}
}
Erklärung:
- Der Klassenname folgt den gleichen Regeln wie die SQL-Datei:
V4__Backfill_customer_email. context.getConnection()gibt die von Flyway verwendete Verbindung zurück, die sich in der gleichen Transaktion der Migration befindet. Der ParametertruevonSingleConnectionDataSourceverhindert, dassJdbcTemplatediese Verbindung schließt.- Flyway scannt das Package
db.migration, da es dem Pfadclasspath:db/migrationentspricht.
Für einen Kunden Nguyễn Văn An mit der ID 1 ist das Ergebnis nguyen.van.an.1@example.com.
Spring Boot registriert auch automatisch alle Beans vom Typ JavaMigration bei Flyway. Dadurch können Sie die Migration als @Component deklarieren. Sie sollten jedoch keine Repositories oder Services in Migrationen injizieren. Geschäftscode ändert sich mit der Zeit, aber Migrationen müssen immer genau gleich ablaufen.
6. Testen von Migrationen
Sie sollten mindestens einen Test haben, der bestätigt, dass alle Migrationen von einem leeren Schema aus ausgeführt werden können und Hibernate erfolgreich validiert. Erstellen Sie src/test/resources/application-test.properties:
spring.datasource.url=jdbc:h2:mem:shop;MODE=PostgreSQL;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=
Test:
import org.flywaydb.core.Flyway;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@ActiveProfiles("test")
class FlywayMigrationTest {
@Autowired
private Flyway flyway;
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
void whenContextStarts_thenAllMigrationsAreApplied() {
var current = flyway.info().current();
assertThat(current).isNotNull();
assertThat(current.getVersion().getVersion()).isEqualTo("4");
assertThat(flyway.info().pending()).isEmpty();
}
@Test
void givenNewCustomer_whenInsert_thenEmailColumnExists() {
jdbcTemplate.update(
"INSERT INTO customer (full_name, email) VALUES (?, ?)",
"Trần Thị Bình", "binh@example.com");
String email = jdbcTemplate.queryForObject(
"SELECT email FROM customer WHERE full_name = ?",
String.class, "Trần Thị Bình");
assertThat(email).isEqualTo("binh@example.com");
}
}
Der erste Test überprüft, dass das Schema auf Version 4 ist und keine ausstehenden Migrationen vorhanden sind. Wenn eine SQL-Datei Syntaxfehler enthält, kann der Context nicht gestartet werden und der Test schlägt sofort fehl. Der zweite Test bestätigt, dass die email-Spalte aus V3 tatsächlich existiert.
Hinweis: H2 im
MODE=PostgreSQLemuliert nur einen Teil der PostgreSQL-Syntax. Wenn Ihre Migration spezifische Features wieJSONB, Partial Indexes oderCREATE INDEX CONCURRENTLYnutzt, sollten Sie Tests gegen echtes PostgreSQL mit Testcontainers ausführen.
7. Häufige Fehler und deren Behandlung
7.1. Versionsmigration nach der Ausführung bearbeiten
Dies ist der häufigste Fehler. Angenommen, Sie bearbeiten V2__create_orders_table.sql, nachdem sie bereits auf Staging ausgeführt wurde. Der nächste Start wird folgenden Fehler melden:
Validate failed: Migrations have failed validation
Migration checksum mismatch for migration version 2
-> Applied to database : 873416502
-> Resolved locally : -402981177
Die Regel ist: Migrationen, die bereits in einer gemeinsamen Umgebung ausgeführt wurden, dürfen nicht verändert werden. Um Änderungen vorzunehmen, erstellen Sie eine neue Migration, zum Beispiel V5__alter_orders_status_length.sql.
Wenn die Änderung nur ein Kommentar oder Whitespace ist und Sie sicher sind, dass sich das Schema nicht ändert, können Sie die Checksum mit dem Befehl repair aktualisieren. Beispiel mit dem Flyway Maven Plugin:
mvn flyway:repair -Dflyway.url=jdbc:postgresql://localhost:5432/shop \
-Dflyway.user=shop -Dflyway.password=secret
repair löscht auch fehlgeschlagene Migrationsdatensätze aus flyway_schema_history. Dies ist wichtig für MySQL, da MySQL keine DDL-Rollbacks innerhalb einer Transaktion unterstützt.
7.2. Doppelte Versionen bei der Teamarbeit
Wenn zwei Entwickler auf verschiedenen Branches beide V5__... erstellen und diese gemergt werden, erhalten Sie den Fehler Found more than one migration with version 5. Es gibt zwei Möglichkeiten, dies zu vermeiden:
- Verwenden Sie Versionen basierend auf Timestamps, zum Beispiel
V20261016_0930__add_order_note.sql. - Vereinbaren Sie, Versionsnummern zu reservieren, bevor Sie Dateien erstellen.
Ein anderes Szenario: Branch A merged V7 zuerst, dann merged Branch B V6. Flyway meldet, dass V6 übersprungen (ignored) wird. Sie können spring.flyway.out-of-order=true aktivieren, aber es ist besser, die Version der neuen Datei höher als die aktuelle Version zu setzen.
7.3. Flyway für vorhandene Datenbanken
Wenn eine Production-Datenbank bereits Tabellen hat, aber noch kein flyway_schema_history, weigert sich Flyway auszuführen, da das Schema nicht leer ist. Sie verwenden Baseline:
spring.flyway.baseline-on-migrate=true
spring.flyway.baseline-version=1
Flyway markiert das aktuelle Schema als Version 1. Danach werden nur Migrationen mit einer Version größer als 1 ausgeführt. V1 sollte dann ein Dump des aktuellen Schemas sein, damit neue Umgebungen von Grund auf aufgebaut werden können.
Hinweis: Aktivieren Sie
baseline-on-migratenur beim ersten Mal. Wenn Sie dies über längere Zeit aktiviert lassen und versehentlich auf die falsche Datenbank mit vorhandenen Tabellen verweisen, wird Flyway baseline durchführen statt einen Fehler zu melden.
7.4. Der clean-Befehl
flyway clean löscht alle Objekte im Schema. Spring Boot 3 setzt standardmäßig spring.flyway.clean-disabled=true. Deaktivieren Sie dieses Flag nicht in Profilen, die Production erreichen können.
7.5. Lange laufende Migrationen
Spring Boot führt Migrationen beim Starten aus. Ein ALTER TABLE auf einer Tabelle mit Dutzenden von Millionen Zeilen kann die Tabelle sperren und Health Checks fehlschlagen lassen. In diesem Fall sollten Sie:
- Teilen Sie die Änderung in mehrere Schritte auf: Fügen Sie eine nullable Spalte hinzu, backfill in Batches und fügen Sie dann
NOT NULLhinzu. - Deaktivieren Sie automatische Migration mit
spring.flyway.enabled=falseund führen Sie Flyway als separaten Schritt in der Deployment-Pipeline aus, bevor Sie die Anwendung ausrollen.
8. Wann man Flyway nicht wie gewohnt verwenden sollte
Flyway ist für die meisten Spring Boot-Anwendungen sehr gut geeignet. Jedoch hat der Ansatz „Migration beim Start” Grenzen:
- Mehrere Instanzen gleichzeitig ausführen: Flyway verwendet eine Datenbanksperre, sodass keine doppelten Migrationen ausgeführt werden. Aber die verbleibenden Instanzen müssen warten, und die Startzeit erhöht sich.
- Anwendungskonto hat keine DDL-Berechtigungen: Dies ist eine häufige Sicherheitsanforderung in Production. Sie führen Flyway dann mit einem separaten Konto in der Pipeline aus.
- Automatisches Rollback erforderlich: Die Community-Version hat kein Undo für Migrationen. Sie müssen Migration „vorwärts” schreiben, um rückgängig zu machen.
9. Fazit
In diesem Artikel haben Sie Flyway in Spring Boot 3 integriert. Sie haben versionierte Migrationen, wiederholbare Migrationen und Java-Migrationen geschrieben, diese mit JUnit 5 und AssertJ getestet und gelernt, wie man häufige Fehler wie Checksum-Nichtübereinstimmungen, doppelte Versionen oder das Baseline älterer Datenbanken handhabt.
Es gibt drei Grundsätze, die Sie sich merken sollten:
- Flyway ist der einzige Ort, wo sich das Schema ändert, Hibernate nur
validate. - Bereits ausgeführte Migrationen sollten niemals bearbeitet werden.
- Sie sollten immer Tests haben, die alle Migrationen von einem leeren Schema aus ausführen.