Migration et versioning de base de données dans une application Spring Boot - Flyway vs Liquibase

Publié le 13/02/2025 Source : sfeir.dev

Qui n’est jamais arrivé en cours de route sur un projet et a découvert une base de données avec une quinzaine de tables, chacune avec un certain nombre de colonnes. Mais comment cette base de données en est-elle arrivée à ce stade ? Quels ont été les besoins qui l’ont fait évoluer ?
C’est ce que nous allons voir dans cet article.

Migration et versioning

Pour bien commencer, posons les bases de ce que nous entendons par migration et versioning :

Flyway : un versioning basé sur des scripts SQL

Présentation de Flyway

Flyway est un outil de migration de base de données qui fonctionne en appliquant des scripts SQL versionnés dans un ordre déterminé. Il suit une approche simple :

Installation et configuration dans Spring Boot

Pour ajouter Flyway à une application Spring Boot, il suffit d’inclure la dépendance suivante dans pom.xml :

<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>

Spring Boot auto-configureFlyway dès qu’il détecte cette dépendance. Ensuite, dans application.properties :

spring.flyway.enabled=true
spring.flyway.baseline-on-migrate=true
spring.flyway.validate-on-migrate=true

Enfin il suffit de placer les scripts SQL dans le répertoire src/main/resources/db/migration/ pour que ces derniers soient exécutés au démarrage de l’application.

Schema history table "PUBLIC"."flyway_schema_history" does not exist yet
Successfully validated 3 migrations (execution time 00:00.017s)
Creating Schema History table "PUBLIC"."flyway_schema_history" ...
Current version of schema "PUBLIC": << Empty Schema >>
Migrating schema "PUBLIC" to version "1 - init"
Migrating schema "PUBLIC" to version "2 - table-user"
Migrating schema "PUBLIC" to version "3 - ajout colonne email"
Successfully applied 3 migrations to schema "PUBLIC", now at version v3 (execution time 00:00.020s)

Si en cours de développement j’ajoute un nouveau script, ce dernier sera détecté au prochain démarrage de l’application et exécuté :

Successfully validated 4 migrations (execution time 00:00.026s)
Current version of schema "PUBLIC": 3
Migrating schema "PUBLIC" to version "4 - table-books"
Successfully applied 1 migration to schema "PUBLIC", now at version v4 (execution time 00:00.009s)

Et si je vais voir en base de données, je retrouve la table flyway_schema_history qui garde l’historique des scripts joués :

Gestion du rollback avec Flyway

Flyway ne supporte pas le rollback automatique. Si une migration a été appliquée, elle ne peut pas être annulée directement.
Il faut écrire un script correctif et l’exécuter avec une nouvelle version.

Liquibase : un système de migration basé sur des changelogs

Présentation de Liquibase

Contrairement à Flyway, Liquibase fonctionne en utilisant des fichiers de changelog au format XML, YAML, JSON ou SQL. Chaque changement est défini sous forme d’un changeSet, qui peut inclure :

Liquibase suit une approche similaire à Flyway :

  1. Il détecte les nouveaux changeSets non encore appliqués.
  2. Il exécute ces changements sur la base de données.
  3. Il enregistre l’historique dans une table DATABASECHANGELOG pour éviter les doublons.

Installation et configuration dans Spring Boot

Pour ajouter Liquibase à une application Spring Boot, il suffit d’inclure la dépendance suivante dans pom.xml :

<dependency>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-core</artifactId>
</dependency>

Ensuite dans application.properties :

spring.liquibase.enabled=true
spring.liquibase.change-log=classpath:db/changelog/changelog-master.xml

Exemple de fichier changelog-master.xml :

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
        http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">

    <changeSet id="001-create-book-table" author="admin">
        <sqlFile path="db/changelog/changesets/001_create_tables.sql" />
    </changeSet>
    <include file="db/changelog/changesets/002_add_users_table.xml"/>

</databaseChangeLog>

Il s’agit ici d’un mauvais exemple car j’ai mélangé des changeSet sql et xml, mais c’est avant tout pour montrer que c’est possible. Dans un projet, il sera préférable de rester sur le même format pour avoir quelque chose d’uniforme.

Au démarrage de l’application, Liquibase jouera les scripts dans l’ordre dans lequel ils sont décrits dans le fichier master.

Creating database history table with name: PUBLIC.DATABASECHANGELOG
Reading from PUBLIC.DATABASECHANGELOG
Successfully acquired change log lock
Using deploymentId: 8225847034
Reading from PUBLIC.DATABASECHANGELOG
Running Changeset: db/changelog/changelog-master.xml::001-create-book-table::admin
SQL in file db/changelog/changesets/001_create_tables.sql executed
ChangeSet db/changelog/changelog-master.xml::001-create-book-table::admin ran successfully in 22ms
Running Changeset: db/changelog/changesets/002_add_users_table.xml::002-create-users-table::admin
Table users created
ChangeSet db/changelog/changesets/002_add_users_table.xml::002-create-users-table::admin ran successfully in 9ms
UPDATE SUMMARY
Run:                          2
Previously run:               0
Filtered out:                 0
-------------------------------
Total change sets:            2
Update summary generated
Update command completed successfully.
Liquibase: Update has been successful. Rows affected: 2
Successfully released change log lock
Command execution complete

Ajoutons maintenant un nouveau script pour ajouter une colonne à la table users dans le fichier master :

{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "003-add-mail-column",
        "author": "admin",
        "changes": [
          {
            "addColumn": {
              "tableName": "users",
              "columns": [
                {
                  "name": "mail",
                  "type": "VARCHAR(255)",
                  "constraints": {
                    "nullable": false,
                    "unique": true
                  }
                }
              ]
            }
          }
        ]
      }
    }
  ]
}

Et l’ajouter dans le ficher master

<include file="db/changelog/changesets/003_add_mail_column.json"/>

Au prochain démarrage de l’application j’aurais le résultat suivant dans les traces de log :

Running Changeset: db/changelog/changesets/003_add_mail_column.json::003-add-mail-column::admin
Columns mail(JSON) added to users
ChangeSet db/changelog/changesets/003_add_mail_column.json::003-add-mail-column::admin ran successfully in 19ms
UPDATE SUMMARY
Run:                          1
Previously run:               2
Filtered out:                 0
-------------------------------
Total change sets:            3
Update summary generated
Update command completed successfully.
Liquibase: Update has been successful. Rows affected: 1
Successfully released change log lock
Command execution complete

Et si je vais voir en base de données, je retrouve une table avec l’historique des changeSet qui ont été joués :

Gestion du rollback avec Liquibase

Contrairement à Flyway, Liquibase supporte les rollbacks automatiques. Chaque changeSet peut être annulé grâce à un rollback.

{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "003-add-mail-column",
        "author": "admin",
        "changes": [
          {
            "addColumn": {
              "tableName": "users",
              "columns": [
                {
                  "name": "mail",
                  "type": "VARCHAR(255)",
                  "constraints": {
                    "nullable": false,
                    "unique": true
                  }
                }
              ]
            }
          }
        ],
        "rollback": [
          {
            "dropColumn": {
              "tableName": "users",
              "columnName": "mail"
            }
          }
        ]
      }
    }
  ]
}

Pour cela, il faut de nouveau modifier notre fichier pom.xml pour lui ajouter le plugin Liquibase

<build>
    <plugins>
        <plugin>
            <groupId>org.liquibase</groupId>
            <artifactId>liquibase-maven-plugin</artifactId>
            <version>4.31.0</version> 
            <configuration>
                <changeLogFile>db/changelog/changelog-master.xml</changeLogFile>
                <url>jdbc:h2:file:~/data/liquibase</url>
                <driver>org.h2.Driver</driver>
                <username>sa</username>
                <password>password</password>
            </configuration>
        </plugin>
    </plugins>
</build>

et de jouer la commande suivante :

liquibase:rollback -Dliquibase.rollbackCount=1

Ici, nous indiquons avec rollbackCount combien de rollback nous voulons jouer à partir du changeSet le plus récent.

####################################################
##   _     _             _ _                      ##
##  | |   (_)           (_) |                     ##
##  | |    _  __ _ _   _ _| |__   __ _ ___  ___   ##
##  | |   | |/ _` | | | | | '_ \ / _` / __|/ _ \  ##
##  | |___| | (_| | |_| | | |_) | (_| \__ \  __/  ##
##  \_____/_|\__, |\__,_|_|_.__/ \__,_|___/\___|  ##
##              | |                               ##
##              |_|                               ##
##                                                ## 
##  Get documentation at docs.liquibase.com       ##
##  Get certified courses at learn.liquibase.com  ## 
##                                                ##
####################################################
Starting Liquibase at 10:07:14 using Java 21.0.2 (version 4.29.2 #3683 built at 2024-08-29 16:45+0000)
Set default schema name to PUBLIC
Executing on Database: jdbc:h2:file:~/data/liquibase
Reading from DATABASECHANGELOG
Successfully acquired change log lock
Reading from DATABASECHANGELOG
Rolling Back Changeset: db/changelog/changesets/003_add_mail_column.json::003-add-mail-column::admin
Rollback command completed successfully.
Successfully released change log lock
Command execution complete

Comparaison entre Flyway et Liquibase

Critère Flyway Liquibase
Type de migrations SQL XML, YAML, JSON ou SQL
Facilité d’utilisation Très simple Plus flexible mais plus complexe
Gestion des rollbacks Script correctif Native
Apprentissage Très rapide Plus long à maîtriser

Conclusion

Dans un projet Spring Boot, il est crucial de versionner et gérer les migrations de base de données.

Le choix entre Flyway et Liquibase dépend donc des besoins du projet :
✔ Pour un projet simple, Flyway est suffisant.
✔ Pour un projet évolutif et complexe, Liquibase est préférable.