Monthly Archives: October 2022

Gérer les mises à jour de sa base données SQL avec Liquibase

Dans les années 2000, pour résoudre des problèmes de mise à jour de base de données avec des scripts SQL à appliquer manuellement, dans un ordre précis, pour un type de base de données spécifique, en fonction d’une version de départ variable, j’ai développé un outil en C++ qui automatisait tout ce processus. Et ce qui devait être un outil développé rapidement a été beaucoup plus complexe que prévu.

Quelques années plus tard, société et application différentes mais même problématique. Cette fois-ci, c’est en Delphi que j’ai traité le problème, plus rapidement que la première fois, mais moins rapidement que ce que j’aurais imaginé.

Un problème rencontré deux fois en quelques années signifie qu’il est probablement commun. Et les problèmes communs ont toujours une solution standardisée, qui évite de réinventer la roue. En l’occurrence, cette solution s’appelle Liquibase.

Liquibase résout tous les problèmes auxquels on ne pense pas forcément quand on commence à écrire un petit script de mise à jour de la BD. Quelle sera la version du moteur SQL cible ? Est ce que je dois supporter plusieurs moteurs SQL ? Quelle sera la version initiale de la base de données à mettre à jour ? Comment gérer la migration des données ? Que faire en cas d’erreur pendant la mise à jour ? Comment identifier et fusionner les différences entre des évolutions concurrentes de la BD ? Etc.

Toutes ces réponses apportées à des questions auxquelle on n’a pas encore pensé quand on envisage de traiter le problème manuellement justifient la learning curve pour s’approprier Liquibase, en particulier l’organisation et le format des fichiers qui rendent les fichiers de mise à jour indépendants de la base de données cible.

Heureusement, il y a une forte communauté autour de l’outil, et des documentations claires et précises. En l’occurrence, il s’agissait d’adopter Liquibase pour un projet existant. Liquibase a une procédure pour cette situtation spécifique, ce qui ne dispense pas de lire au moins la présentation générale du fonctionnement de Liquibase

Mon environnement de dev s’appuie sur Maven, JAVA, SpringBoot. L’adoption de Liquibase se faisait dans un contexte plus général de migration de l’application vers le modèle JHipster. J’avais donc des scripts d’initialisation de la base de données générés pour le projet de demo JHipster.

Première étape, configurer le plugin Liquibase dans le POM.xml pour pointer vers la base de données à utiliser, et pour indiquer le package contenant toutes les entités :

<plugin>
	<groupId>org.liquibase</groupId>
	<artifactId>liquibase-maven-plugin</artifactId>
	<version>${liquibase.version}</version>
	<configuration>
		<changeLogFile>${project.basedir}/src/main/resources/config/liquibase/master.xml</changeLogFile>
		<outputChangeLogFile>${project.basedir}/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml</outputChangeLogFile>
		<diffChangeLogFile>${project.basedir}/src/main/resources/config/liquibase/changelog/${maven.build.timestamp}_changelog.xml</diffChangeLogFile>
		<driver>org.mariadb.jdbc.Driver</driver>
		<url>jdbc:mariadb://localhost:3310/bartleby</url>
		<defaultSchemaName></defaultSchemaName>
		<username>myUser</username>
		<password>myPassword</password>
		<referenceUrl>hibernate:spring:com.riousset.bartleby.persistence?dialect=org.hibernate.dialect.MariaDB103Dialect&amp;hibernate.physical_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy&amp;hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy</referenceUrl>
		<verbose>true</verbose>
		<logging>debug</logging>
		<contexts>!test</contexts>
	</configuration>
        ...

Les points clés : les propriétés driver, url, username, password concernent la base de données à mettre à jour. La propriété referenceUrl concerne le modèle de référence, en fonction duquel la BD devra être mise à jour. Dans l’exemple ci-dessous, je pointe vers le package contenant les entités JPA / Hibernate de mon application.

Seconde étape, générer le script d’initialisation de la base de données en fonction du schéma existant. Le changeLog sera généré dans le fichier indiqué par la propriété outputChangeLogFile :

./mvnw liquibase:generateChangeLog -Pprod

Troisième étape, maintenant que le changelog d’initialisation a été créé, il est possible d’initialiser un base de données vierge. Mais la mise à jour d’une base de données préexistante causerait des conflits. Liquibase offre la commande changeLogSync pour indiquer qu’un changeLog a déjà été exécuté sur un base. C’est un outil “bête”: changeLogSync ne vérifie pas que les scripts ont réellement été exécutés, il se contente d’indiquer dans la table DATABASECHANGELOG que tous les changeSet ont bien été exécutés :

./mvnw liquibase:changelogSync -Pprod -D"liquibase.changeLogFile=config/liquibase/changelog/00000000000000_initial_schema.xml"

Quatrième étape, on peut créer ou modifier des entités JPA, et laisser liquibase générer les scripts de mise à jour :

./mvnw liquibase:diff

Le fichier généré doit alors être ajouté au fichier master.xml. par exemple :

<?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-latest.xsd">
    <property name="now" value="now()" dbms="h2"/>
    <property name="floatType" value="float4" dbms="h2"/>
    <property name="uuidType" value="varchar(36)" dbms="h2"/>
    <property name="datetimeType" value="datetime" dbms="h2"/>
    <property name="clobType" value="clob" dbms="h2"/>
    <property name="blobType" value="blob" dbms="h2"/>
    <property name="now" value="now()" dbms="mariadb"/>
    <property name="floatType" value="float" dbms="mariadb"/>
    <property name="clobType" value="clob" dbms="mariadb"/>
    <property name="blobType" value="longblob" dbms="mariadb"/>
    <property name="uuidType" value="varchar(36)" dbms="mariadb"/>
    <property name="datetimeType" value="datetime(6)" dbms="mariadb"/>

    <include file="config/liquibase/changelog/00000000000000_initial_schema.xml" relativeToChangelogFile="false"/>
	<include file="config/liquibase/changelog/20221004092650_changelog.xml" relativeToChangelogFile="false"/>
    <!-- jhipster-needle-liquibase-add-changelog - JHipster will add liquibase changelogs here -->
    <!-- jhipster-needle-liquibase-add-constraints-changelog - JHipster will add liquibase constraints changelogs here -->
    <!-- jhipster-needle-liquibase-add-incremental-changelog - JHipster will add incremental liquibase changelogs here -->
</databaseChangeLog>

Enfin, l’application des changements à une base de données se fait avec la commande :

./mvnw liquibase:update -Pprod