Article écrit par Bastien
Par le passé, j’ai eu à travailler sur un projet collaboratif dans lequel aucun outil de versionning de base de données n’avait été mis en place.
Arrivé à un certain degré de maturité sur ce projet, les spécifications fonctionnelles ont été revues et il a fallu faire de grosses modifications dans nos schémas de bases.
Sans surprise : cette refonte du Modèle Conceptuel de Donnée a été catastrophique. La donnée et les schémas de base des différents environnements n’étaient pas cohérents entre eux. Résultat : une partie de la donnée s’est retrouvée inutilisable et les conséquences ont été très lourdes sur ce projet.
Heureusement, il existe des solutions pour éviter ce genre de problème. Dans cet article, je vais vous présenter l’une d’entre elles, Liquibase.
Liquibase, qu’est-ce que c’est ?
Liquibase est une bibliothèque de versionning de bases de données. Elle permet de suivre, de gérer et d’appliquer des changements de schéma de bases de données.
Les raisons qui peuvent nous pousser à utiliser Liquibase sont multiples. Grâce à sa puissante gestion des versions, on peut avoir un meilleur suivi de l’évolution des schémas de base au fil du temps (on a bien évidemment possibilité de monter de versions, mais également de revenir à une version antérieure, avec grâce à un système de rollback). Dans le cas d’un projet collaboratif, les développeurs s’assurent de travailler sur des modèles de bases de données identiques à chaque étape du projet.
On pourrait dire que Liquibase est (en quelque sorte) aux bases de données, ce que git est au code.
Liquibase a été lancé en 2006 et est maintenant compatible avec plus de 50 Systèmes de Gestion de Bases Données. Liquibase gère aussi bien des SGBD de type relationnels (comme Oracle, MySQL, PostgreSQL), “NoSQL” (MongoDB, Cassandra…) ou même cloud (Amazon RDS, Google Cloud SQL, Microsoft Azure SQL…). Cette prise en charge de nombreux SGBD lui assurent une grande flexibilité et une grande reproductibilité. Un grand avantage de Liquibase est qu’il permet de gérer facilement la migration d’une base de donnée à une autre (voir la partie Fonctionnement, un peu plus bas dans l’article).
Dans la suite de cet article, je vais montrer, un petit exemple purement pédagogique d’utilisation. Mon but n’est évidemment pas de remplacer la doc technique officielle, mais simplement de donner un aperçu de quelques possibilités qu’offre Liquibase.
Intégration de Liquibase
- Je commence par créer une base de donnée sur mon poste local
$ mysql > CREATE DATABASE liquibase_sample_database;
- Dans mon IDE, je crée un nouveau projet, je choisis Maven comme Build system
- J’édite mon fichier pom.xml de façon à ajouter la dépendance et le plugin liquibase
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>liquibase-sample</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--Dépendance liquibase-->
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>4.19.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<!--Plugin liquibase-->
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>4.19.0</version>
<configuration>
<!-- Chemin vers le fichier de propriété à créer-->
<propertyFile>src/main/resources/liquibase.properties</propertyFile>
</configuration>
<dependencies>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>9.4.1.jre8</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
- Je crée un fichier de propriétés au niveau d’arborescence décrit dans mon pom.xml et j’ajoute le contenu suivant :
driver=com.mysql.cj.jdbc.Driver # Ici mysql, mais le choix de SGBD est vaste
url=jdbc:mysql://localhost:3306/liquibase\_sample\_database
username=YOUR_USERNAME
password=YOUR_PASSWORD
# "changeLogFile" correspond au chemin de mon point d'entrée pour les
# modifications à apporter en base de donnée.
changeLogFile=1.0.0/db-changelog.xml
- Un
mvn install
et le tour est joué !
Fonctionnement
Notion de ChangeSet
Le fonctionnement de Liquibase repose sur les changeSet. Ce sont des fichiers markup (xml, yaml, json…) qui décrivent les changements que l’on souhaite apporter à une base de donnée.
Par changement on entend des actions qui peuvent être la création, la modification ou la suppression de tables, de colonnes, de contraintes référentielles, de données. En bref, un peu toutes les opérations que l’on peut-être amené à manipuler en travaillant dans une base de donnée.
L’utilisation de ces formats de fichiers balisés aide à l’automatisation, puisque c’est Liquibase qui gère la jonction entre ces derniers, et les langages propres aux SGBD. Pas besoin d’écrire des requêtes compliquées et propres à chaque SGBD.
Voici un exemple de changeSet, la syntaxe est très simple :
<?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.1.xsd">
<!-- Un changeSet est défini par un identifiant "id" (unique dans le projet)
et un "author". -->
<changeSet id="1" author="bmatthai">
<!-- Je définie ici une précondition pour éviter les conflits.
Si la table "car" existe déjà, je sette la valeur MARK\_RAN -->
<preConditions onFail="MARK\_RAN">
<not>
<tableExists tableName="car"/>
</not>
</preConditions>
<!-- Je crée une table "car" dans laquelle j'ajoute des champs id et
model -->
<createTable tableName="car">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="model" type="VARCHAR(255)" defaultValue="none">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>
Dans mon exemple minimaliste ci-dessus, j’ai défini deux champs id
et model
dans ma table Car
. Je leur ai également fourni des contraintes (nullabilité et clé primaire). Toutefois, une bonne pratique peut être de séparer les fichiers de changeSet contenant les schémas de base de donnée, ceux contenant les contraintes complexes (nullabilité, valeurs par défaut, clés étrangères, unicité…), et ceux contenant de la modification de donnée. En procédant ainsi il est plus simple de trouver rapidement un changeSet en particulier, et ainsi de mieux gérer les éventuelles erreurs de déploiement.
Les changements dans la base de données sont appliqués dans l’ordre séquentiel dans lequel ils sont définis dans le changeLogFile (comme décrit ci-dessus dans la partie Intégration).
Logging et historisation
Une fois vos différents changeSet prêts, vous pouvez “jouer” (exécuter) votre liquibase en utilisant la commande mvn install -f pom.xml
(ou en CLI avec la commande liquibase update). Attention à sélectionner le bon profil, dans le cas contraire vous pourriez appliquer prématurément vos changement sur le mauvais environnement. (En prod par exemple !)
Si tout s’est bien passé, vous devriez avoir à ce stade les tables correspondant à vos changeSet joués. Liquibase créé également deux tables annexes :
- DATABASECHANGELOG est utilisée pour suivre les modifications apportées au schéma de la base de données, permettant ainsi de garder une trace de l’historique des modifications et de garantir l’intégrité de la structure de la base de données.
- DATABASECHANGELOGLOCK permet d’éviter les accès concurrents et donc de garder une cohérence dans la base.
La table DATABASECHANGELOG contient notamment un champ MD5SUM qui correspond au “hash” d’un changeSet. Ainsi on conserve l’historique des changeSet qui ont déjà été joué. Modifier un changeSet déjà joué est une mauvaise pratique qui peut entraîner des incohérences dans la base de donnée. La bonne façon de faire est de créer un nouveau changeSet qui annule ou modifie l’existant.
Aperçu de la table DATABASECHANGELOG après avoir joué mon liquibase
Quelques notions importantes
Un avantage de Liquibase est qu’il offre une grande flexibilité au niveau de la gestion des versions. Il est possible de “jongler” entre les versions et les environnements : on peut monter de version, mais il est aussi possible de revenir à une version plus ancienne de la BDD; A condition d’utiliser certaines notions, que nous allons voir ci-dessous :
Rollback
Dans un changeSet, on peut définir une ou plusieurs balises rollback, qui contient les opérations inverses à celles faites dans le changeSet.
Dans mon exemple de changeSet ci-dessus, qui consiste à créer une table Car
je peux rajouter une balise rollback qui supprime tout simplement cette table.
<rollback>
<dropTable tableName="car"/>
</rollback>
Note : si un changeSet opère plusieurs modifications en base, chaque modification devrait avoir un rollback correspondant.
Labels
Les labels correspondent à des identifiants de version, que l’on peut définir de façon optionnelle au niveau de la balise changeSet :
<changeSet id="1" author="bmatthai" labels="1.0">
Les labels permettent d’identifier des versions de la base de donnée. En utilisant la CLI on peut filtrer sur les labels de la façon suivante :
$ liquibase update --labels=1.0
# A l'inverse on peut aussi exclure un label en particulier :
$ liquibase update --liquibase update --excludeLabels=2.0
Attention : dans Liquibase il existe aussi une notion de tags, qui servent à enregistrer un état de BDD. Les labels, eux servent à contrôler quels changeSet seront joués lors d’une migration de BDD.
Contexte
Les contextes sont un peu similaires aux labels, mais eux servent plutôt à filtrer des changements à appliquer en fonction d’autres critères comme l’environnement.
<changeSet id="1" author="bmatthai" labels="1.0" contextFilter="local">
Comme pour les labels, on peut filtrer sur le contexte:
$ liquibase update --contexts=local
# Ou bien en excluant un contexte en particulier :
# $ liquibase update --excludeContexts=production
Conclusion
Que vous partiez d’un projet déjà existant ou commenciez un nouveau projet from scratch, je recommande vivement l’utilisation d’un outil de gestion de version tel que Liquibase pour gérer vos BDD. Bien utilisé, vous y gagnerez probablement beaucoup de temps et faciliterez grandement la collaboration entre les différentes personnes travaillant sur le projet.
En résumé
- Liquibase est un outil de versionning de BDD compatible avec de nombreux SGBD (relationnel, non-relationnel, cloud)
- On peut l’utiliser avec tous types d’opérations (création, modification, suppression de schéma de base, de contraintes, de données…)
- liquibase fonctionne avec des “changeSet” (qui décrivent des changements à apporter à une BDD) dans des fichiers de balisage (markup)
- Une fois la procédure liquibase exécutée, on ne touche plus aux changeSet. Si on veut faire machine arrière, on crée un nouveau changeSet qui annule les coquilles existantes
- Les tables DATABASECHANGLOG et DATABASECHANGELOGLOCK permettent de garantir la cohérence des tables
- Les notions de labels et de contexte permettent de définir, de filtrer sur des versions spécifiques de BDD ou d’environnement