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

APIs de recherche et de consolidation d’adresses postales

La saisie d’adresses postales est problématique quand la détection des doublons est nécessaire. Dans le cadre d’une gestion de parc immobilier par exemple, il est indispensable de pouvoir détecter qu’un immeuble donné a déjà été renseigné dans l’application. Mais détecter les variations et surtout les erreurs de saisie (ex : “St Rémy” au lieu de “Saint Rémy de Provence”, “Av” au lieu de “Avenue”) est un problème beaucoup plus complexe que ce que l’intuition pourrait le laisser penser. Il est plus prudent de s’appuyer sur des outils dédiés à ces problématiques.

Dans le cadre de la solution NeoLegal, nous avons testé deux APIs :

  1. L’API Adresse du gouvernement français
  2. L’API Google Places

Les deux API sont simples d’utilisation et donnent des résultats très satisfaisants. Voici les principaux critères orientant le choix de l’une ou l’autre de ces solutions :

  1. L’API du gouvernement est limitée à des adresses françaises alors que celle de Google couvre la majorité des pays.
  2. L’API Google demande une authentification et est payante au delà d’un certain volume de données alors que celle du gouvernement est gratuite et ne demande pas d’authentification.
  3. L’API du gouvernement est une API seule, alors que celle de Google a déjà tout un écosystème de composants HTML prêts à être utilisés

Gérer les versions concurrentes de Node.JS avec NVM

Je ne suis pas un expert frontend, j’ai longtemps rechigné à me plonger dans cet aspect du développement parce que c’était le farwest, que je ne retrouvais pas le cadre, les structures, les patterns que j’apprécie côté backend.

Et puis est arrivé Node.JS, et Angular, et TypeScript, et j’ai réalisé que même avec du JavaScript sous le capot, on pouvait faire des applications riches et propres d’un point de vue code. (ceci-dit, je reste convaincu que le navigateur ne deviendra le nouvel OS que quand il y aura autre chose que du JavaScript sous le capot. A quand la généralisation du WebAssembly ?).

Mais après cette réconciliation sont venus les problèmes, et particulièrement celui de la gestion des versions de Node, de @Angular/cli, des librairies, quand on travaille sur plusieurs projets en simultanées, sur différentes branches. Les conflits de versions arrivent rapidement, on peut perdre des heures à résoudre des messages cryptiques et des problèmes de compatibilités.

Pour maîtriser la version de Node déployée pour un projet donné, sur Windows, la solution la plus simple est Node Version Manager (NVM) for Windows. L’outil permet de basculer d’une version de Node vers une autre en deux lignes de commandes :

nvm install 16.10.0 // pour installer la version de node
nvm use 16.10.0 // pour utiliser une version spécifique

How to list Vertica Tables by number of rows ?

When working with Vertica, you may need to find out which tables have the largest number of rows. To do so, you must tap into the storage_containers system table, that will give metrics about the number of rows, AND the number of deleted rows. But since containers are distributed, you will need to aggregate this data, and ensure to limit your query to the super-projection, which guarantee to contain all table info. In brief :

with num_rows as (
    select schema_name,
           anchor_table_name as table_name,
           sum(total_row_count - deleted_row_count) as rows
    from v_monitor.storage_containers sc
    join v_catalog.projections p
         on sc.projection_id = p.projection_id
         and p.is_super_projection = true
    group by schema_name,
             table_name,
             sc.projection_id
)
select schema_name,
       table_name,
       max(rows) as rows
from num_rows
group by schema_name,
         table_name
order by rows desc;