
eXo Platform utilise Liquibase pour gérer les évolutions de sa base de données. Cela permet de mettre à jour facilement le schéma de la base de données (ajouter/supprimer une table, ajouter/supprimer une colonne, ajouter un index, …) ou son contenu (mise à jour des entrées, …).
Ces changements sont définis comme une liste de changesets que Liquibase va appliquer quand il sera exécuté (au démarrage d’eXo Platform dans notre cas). Quand un changement est appliqué, Liquibase sauvegarde cet état dans ses données et le changement ne sera plus appliqué aux prochaines exécutions.
Liquibase vérifie également que le changeset n’a pas été modifié dans le fichier par rapport à la version qui a été appliquée. Et c’est de ce sujet dont nous allons parler ici.
Bien que ce soit une bonne pratique de ne pas modifier les changesets déjà appliqués, c’est parfois nécessaire. Nous allons voir dans quelles situations et comment le faire. Mais commençons par nous plonger un peu dans les mécanismes internes de Liquibase.
Comment ça marche ?
Comme expliqué précédemment, les changements appliqués sur la base d données sont définies dans un fichier XML comme une liste de changesets. Voici un exemple provenant de l’application eXo Wiki :
<?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.3.xsd"> <!-- Managing both DB that use sequences and db that use auto increment --> <property name="autoIncrement" value="true" dbms="mysql,mssql,h2,sybase,db2,hsqldb"/> <property name="autoIncrement" value="false" dbms="oracle,postgresql"/> <!-- Managing auto generation of timestamp by Database --> <property name="now" value="now()" dbms="mysql,hsqldb,postgresql,h2"/> <property name="now" value="sysdate" dbms="oracle"/> <property name="now" value="CURRENT_TIMESTAMP" dbms="mssql"/> <changeSet author="wiki" id="1.0.0-1"> <createTable tableName="WIKI_WIKIS"> <column name="WIKI_ID" type="BIGINT" autoIncrement="${autoIncrement}" startWith="1"> <constraints nullable="false" primaryKey="true" primaryKeyName="PK_WIKI_WIKIS_ID"/> </column> <column name="NAME" type="NVARCHAR(550)"/> <column name="OWNER" type="NVARCHAR(200)"> <constraints nullable="false"/> </column> <column name="TYPE" type="NVARCHAR(50)"> <constraints nullable="false"/> </column> <column name="WIKI_HOME" type="BIGINT"/> <column name="SYNTAX" type="NVARCHAR(30)"/> <column name="ALLOW_MULTI_SYNTAX" type="BOOLEAN"/> </createTable> <modifySql dbms="mysql"> <append value=" ENGINE=INNODB CHARSET=UTF8 COLLATE utf8_general_ci"/> </modifySql> </changeSet> ... <changeSet author="wiki" id="1.0.0-31" dbms="oracle,postgresql"> <createSequence sequenceName="SEQ_WIKI_WIKIS_WIKI_ID" startValue="1"/> </changeSet> … </databaseChangeLog>
Quand Liquibase exécute ces changesets, il sauvegarde l’id de tous les changesets appliqués avec succès dans ses propres données, avec leur checksum. Par exemple, pour le premier changeset de l’exemple ci-dessus (id=”1.0.0-1”), l’entrée suivante est insérée dans la table de Liquibase (DATABASECHANGELOG):
ID: 1.0.0-1 AUTHOR: wiki FILENAME: db/changelog/wiki.db.changelog-1.0.0.xml DATEEXECUTED: 2016-04-13 07:14:29 ORDEREXECUTED: 22 EXECTYPE: EXECUTED MD5SUM: 7:4714e3fc16bfa2cac4dde34703a4f58f DESCRIPTION: createTable COMMENTS: TAG: NULL LIQUIBASE: 3.4.1 CONTEXTS: NULL LABELS: NULL
Le champ MD5SUM contient le checksum du changeset. Chaque changement dans le changeset dans le fichier XML engendrera un checksum différent.
A chaque exécution, pour chaque changeset défini dans le fichier XML, Liquibase vérifie si son id/author (dans notre exemple : 1.0.0-1/wiki) est présent dans la base:
- Si ce n’est pas le cas, le changeset est appliqué et l’entrée est insérée dans la table de Liquibase
- Si c’est le cas, Liquibase calcule le checksum du changeset dans le fichier XML, et le compare avec celui présent dans sa base de données
- Si les checksums ont égaux, le changeset est ignoré (car cela signifie qu’il a déjà été appliqué et qu’il n’a pas changé dans le fichier XML)
- Si les checksums sont différents, une erreur est levée
Cela permet d’être sûr qu’un changement n’est appliqué qu’une seule fois, et qu’un changeset déjà appliqué n’a pas été modifié.
Mais certains cas nécessitent la modification de ces changesets.
Pourquoi modifier un changeset ?
La raison principale pour laquelle il est nécessaire de modifier un changeset est lorsqu’il échoue à s’appliquer, quelque soit la raison, par exemple à cause d’un bug, ou parce qu’on veut supporter une nouvelle base de données ou une nouvelle version d’une base de données non compatible avec le changeset.
Prenons un exemple réel avec un bug eXo Platform : Data initialization issues at startup with MySQL 5.7.14+. A partir de MySQL 5.7.14, les changesets de eXo Social échouent à s’appliquer sur une base de données vide, à cause de l’erreur “Invalid default value for ‘CREATED_DATE’”, alors qu’ils s’appliquent correctement sur les précédentes versions de MySQL. Dans ce cas de figure, la seule solution est de modifier le changeset dans le fichier XML.
Une autre raison pour modifier un changeset est lorsque l’on veut optimiser les changesets. Après plusieurs versions de votre application, certaines opérations effectuées vont probablement être inutiles. Par exemple, un premier changeset crée une table, puis un autre change la valeur par défaut d’une des colonnes de cette table, et finalement un dernier changeset supprime cette colonne puisqu’elle n’est plus nécessaire pour l’application. Si vous deviez concevoir votre application maintenant, vous ne créeriez pas du tout cette colonne, mais à cause de Liquibase, quand votre application va démarrer sur une base de données vide, la colonne sera créée, modifiée et supprimée. Dans ce cas de figure, modifier le changeset initial pour supprimer la colonne est intéressant.
Un article intéressant est disponible sur ce sujet dans la documentation de Liquibase.
Comment modifier un changeset ?
Supposons que nous sommes dans le cas où nous voulons modifier un changeset parce qu’il échoue à s’appliquer. Nous voulons :
- Modifier le changeset pour qu’il s’applique correctement sur une base de données où il n’a pas encore été appliqué
- S’assurer que Liquibase démarre correctement avec une base de données où le changeset a déjà été appliqué
Comme expliqué précédemment, si nous essayons de simplement modifier le changeset, le premier objectif sera atteint, mais pas le second. Liquibase va détecter ce changement (le checksum du changeset du fichier XML est différent de celui dans la base Liquibase) pour les instances déjà initialisées et va lever une erreur.
La solution est d’ajouter dans le fichier XML le nouveau checksum (celui du changeset après la modification) à l’aide de la balise validCheckSum.
Par conséquent, le processus pour modifier un changeset est :
- Démarrer eXoPlatform (pour peuplr labase de données)
- Arrêter eXoPlatform
- Modifier le changeset dans le fichier XML
- Démarrer de nouveau eXoPlatform – une erreur Liquibase va apparaître dans les logs:
2017-05-06 10:52:15,082 | ERROR | Error while applying liquibase changelogs db/changelog/wiki.db.changelog-1.0.0.xml - Cause : Validation Failed: 1 change sets check sum db/changelog/wiki.db.changelog-1.0.0.xml::1.0.0-42::wiki is now: 7:21d239448c33f39a9300ea433349d470
- Copier le checksum présent dans le message d’erreur (ici : 7:21d239448c33f39a9300ea433349d470). Il s’agit du checksum du changeset mis à jour (qui n’est donc plus égal à celui stocké dans la base Liquibase).
- Ajouter la balise <validChecksum> contenant le checksum dans le changeset modifié :
<changeSet id="1.0.0-42" author="wiki"> <validCheckSum>7:21d239448c33f39a9300ea433349d470</validCheckSum> … // content of the changeset (with the update) </changeSet>
Cela permet en fait de dire à Liquibase que le checksum 7:21d239448c33f39a9300ea433349d470 est également un checksum valide (en plus de celui présent dans la base Liquibase).
Votre serveur eXo Platform démarre maintenant correctement dans tous les cas (avec une base vide ou non) !