Migration de base de données avec Flyway dans Spring Boot
1. Vue d’ensemble
Lorsque l’application change, le schéma de la base de données doit changer aussi. Si vous exécutez manuellement chaque ALTER TABLE sur chaque environnement, dev, staging et production finiront par dériver. Flyway résout ce problème : chaque modification du schéma est un fichier de migration versionné, appliqué automatiquement et dans le bon ordre.
Dans cet article, vous configurerez Flyway pour une application Spring Boot 3, écrirez des migrations en SQL et en Java, les testerez avec JUnit 5, et apprendrez à gérer les erreurs courantes comme les incompatibilités de checksum.
2. Installation des dépendances
Spring Boot gère déjà la version de Flyway, vous n’avez donc pas besoin de la spécifier. L’exemple ci-dessous utilise PostgreSQL :
<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>
À partir de Flyway 10 (fourni avec Spring Boot 3.3+), le support de chaque base de données est scindé en modules séparés. Si flyway-database-postgresql manque, l’application ne démarrera pas et affichera une erreur :
org.flywaydb.core.api.FlywayException: Unsupported Database: PostgreSQL 16.4
H2 est toujours inclus dans flyway-core, donc le côté test n’a besoin d’aucun module supplémentaire.
3. Configuration de base
Avec juste flyway-core sur le classpath, Spring Boot crée automatiquement un bean Flyway et exécute les migrations avant que EntityManagerFactory soit initialisé. Configuration minimale dans 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
Les lignes importantes :
spring.jpa.hibernate.ddl-auto=validate: Hibernate vérifie seulement que les entités correspondent au schéma. Il ne modifie pas le schéma automatiquement. Flyway est la source de vérité unique pour le schéma.spring.flyway.locations: le répertoire contenant les migrations. La valeur par défaut est déjàclasspath:db/migration. C’est spécifié ici pour plus de clarté.spring.flyway.validate-on-migrate=true: avant de migrer, Flyway vérifie le checksum des migrations déjà exécutées. C’est aussi la valeur par défaut.
Remarque : N’utilisez pas
ddl-auto=updateoucreateen même temps que Flyway. Hibernate et Flyway modifieraient tous les deux le schéma, et le résultat serait imprévisible.
4. Écrire des migrations en SQL
4.1. Convention de nommage
Flyway identifie les migrations par le nom du fichier :
V1__create_customer_table.sql
│ │ └── description (les _ sont interprétés comme des espaces)
│ └── deux traits de soulignement séparateur
└── V = migration versionnée, suivi du numéro de version
La version peut être 1, 2, 1.1 ou 20261016.1. Flyway compare les versions numériquement, pas alphabétiquement. Donc V10 s’exécute après V9.
4.2. Première migration
Créez le fichier 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
);
Ensuite 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);
Au démarrage de l’application, le journal affichera :
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. Tableau flyway_schema_history
Flyway enregistre toutes les migrations exécutées dans la table flyway_schema_history. Vous pouvez l’interroger directement :
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
Au prochain démarrage, Flyway voit que le schéma est déjà à la version 2 et ne réexécute rien. La colonne checksum est clé pour la section 7.
4.4. Migrations répétables
Les fichiers commençant par R__ sont des migrations répétables. Elles n’ont pas de version. Flyway réexécute ce fichier à chaque fois que le checksum change, et s’exécute toujours après toutes les migrations versionnées. Ce type convient aux vues, fonctions ou procédures stockées :
-- 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. Migrations en Java
Certaines modifications sont difficiles à exprimer en SQL pur, par exemple le remplissage des données qui nécessite une logique de traitement de chaînes. Dans ce cas, écrivez une migration en Java en héritage de BaseJavaMigration.
D’abord, ajoutez une colonne avec SQL dans V3__add_customer_email.sql :
ALTER TABLE customer ADD COLUMN email VARCHAR(150);
Ensuite, placez la classe suivante dans le package db.migration, à 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+", ".");
}
}
Explication :
- Le nom de la classe suit la même convention que les fichiers SQL :
V4__Backfill_customer_email. context.getConnection()retourne la connexion que Flyway utilise, dans la même transaction de migration. Le paramètretruedeSingleConnectionDataSourceempêcheJdbcTemplatede fermer cette connexion.- Flyway scanne le package
db.migrationcar il correspond àclasspath:db/migration.
Pour un client nommé Nguyễn Văn An avec l’id 1, le résultat est nguyen.van.an.1@example.com.
Spring Boot enregistre également automatiquement tous les beans de type JavaMigration auprès de Flyway. Grâce à cela, vous pouvez déclarer une migration comme @Component. Cependant, vous ne devriez pas injecter de repository ou de service dans une migration. Le code métier change au fil du temps, tandis que les migrations doivent s’exécuter exactement de la même façon pour toujours.
6. Tester les migrations
Vous devriez avoir au moins un test confirmant que toutes les migrations s’exécutent à partir d’un schéma vide et que la validation Hibernate réussit. Créez 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");
}
}
Le premier test vérifie que le schéma est à la version 4 et qu’aucune migration n’est en attente. Si un fichier SQL a une erreur de syntaxe, le contexte ne démarrera pas et le test échouera immédiatement. Le second test confirme que la colonne email de V3 existe réellement.
Remarque : H2 en
MODE=PostgreSQLn’émule qu’une partie de la syntaxe PostgreSQL. Si votre migration utilise des fonctionnalités spécifiques commeJSONB, les index partiels ouCREATE INDEX CONCURRENTLY, vous devriez exécuter les tests sur une vraie PostgreSQL à l’aide de Testcontainers.
7. Erreurs courantes et solutions
7.1. Modifier une migration qui a été exécutée
C’est l’erreur la plus courante. Supposons que vous modifiez V2__create_orders_table.sql après qu’il ait été exécuté en staging. Au prochain démarrage, vous verrez :
Validate failed: Migrations have failed validation
Migration checksum mismatch for migration version 2
-> Applied to database : 873416502
-> Resolved locally : -402981177
La règle est : une migration qui s’est exécutée dans tout environnement partagé ne doit jamais être modifiée. Pour apporter des modifications, créez une nouvelle migration, par exemple V5__alter_orders_status_length.sql.
Si la modification est juste un commentaire ou un espace blanc et que vous êtes certain que le schéma n’a pas changé, vous pouvez mettre à jour le checksum avec la commande repair. Par exemple, avec le plugin Maven Flyway :
mvn flyway:repair -Dflyway.url=jdbc:postgresql://localhost:5432/shop \
-Dflyway.user=shop -Dflyway.password=secret
repair supprime aussi les enregistrements de migration échoués de flyway_schema_history. C’est important pour MySQL, car MySQL ne peut pas faire de rollback sur les DDL dans une transaction.
7.2. Conflit de version lors du travail en équipe
Si deux développeurs créent chacun V5__... sur deux branches différentes, après fusion vous rencontrerez l’erreur Found more than one migration with version 5. Il y a deux façons de prévenir cela :
- Utilisez les versions basées sur timestamp, par exemple
V20261016_0930__add_order_note.sql. - Coordonnez-vous pour « réserver » les numéros de version avant de créer les fichiers.
Il y a un autre cas : la branche A fusionne V7 d’abord, puis la branche B fusionne V6. Flyway signalera que V6 est ignorée (ignored). Vous pouvez activer spring.flyway.out-of-order=true, mais il est préférable de renommer le fichier pour une version supérieure à la version actuelle.
7.3. Appliquer Flyway à une base de données existante
Avec une base de données production qui a déjà des tables mais pas de flyway_schema_history, Flyway refusera de s’exécuter car le schéma n’est pas vide. Vous utilisez le baseline :
spring.flyway.baseline-on-migrate=true
spring.flyway.baseline-version=1
Flyway marque le schéma actuel comme la version 1. Ensuite, il exécute seulement les migrations avec une version supérieure à 1. À ce stade, V1 doit être un dump du schéma actuel, pour que les nouveaux environnements puissent toujours être construits à partir de zéro.
Remarque : N’activez
baseline-on-migrateque lors de la première conversion. Si vous le laissez activé longtemps et pointez accidentellement vers une base de données existante avec des tables, Flyway basélignera quand même au lieu de signaler une erreur.
7.4. Commande clean
flyway clean supprime tous les objets dans le schéma. Spring Boot 3 définit par défaut spring.flyway.clean-disabled=true. Ne désactivez jamais cet indicateur sur un profil qui pourrait accéder à la production.
7.5. Migrations longues
Spring Boot exécute les migrations au démarrage. Une instruction ALTER TABLE sur une table de dizaines de millions de lignes peut verrouiller la table et faire échouer les vérifications de santé. Dans ce cas, vous devriez :
- Fractionner la modification en plusieurs étapes : ajouter une colonne nullable, faire le remplissage par lots, puis ajouter la contrainte
NOT NULL. - Désactiver la migration automatique avec
spring.flyway.enabled=falseet exécuter Flyway comme une étape séparée dans le pipeline de déploiement, avant le lancement de l’application.
8. Quand ne pas utiliser Flyway par défaut
Flyway convient bien à la plupart des applications Spring Boot. Cependant, l’approche « migrer au démarrage » a ses limites :
- Exécuter plusieurs instances simultanément : Flyway utilise un verrou sur la base de données donc ne s’exécute pas en doublons. Mais les autres instances doivent attendre, ce qui augmente le temps de démarrage.
- Le compte d’application n’a pas de permissions DDL : c’est une exigence de sécurité courante en production. Dans ce cas, vous exécutez Flyway avec un compte séparé dans le pipeline.
- Besoin de rollback automatique : l’édition Community n’a pas d’undo de migration. Vous devez écrire des migrations « forward » pour annuler les changements.
9. Conclusion
Dans cet article, vous avez intégré Flyway dans Spring Boot 3. Vous avez écrit des migrations versionnées, des migrations répétables et des migrations Java, les avez testées avec JUnit 5 et AssertJ, et avez appris à gérer les erreurs courantes comme les incompatibilités de checksum, les doublons de version ou le baseline d’une ancienne base de données.
Il y a trois principes à retenir :
- Flyway est le seul endroit qui modifie le schéma, Hibernate fait juste
validate. - Une migration qui s’est exécutée ne doit jamais être modifiée.
- Ayez toujours un test qui exécute toutes les migrations à partir d’un schéma vide.