Versionnez vos bases avec Flyway

On ne présente plus aujourd’hui l’intérêt des outils de gestion de versions tels Git ou SVN, utilisés par tous les développeurs à travers le monde. Grâce à eux, l’évolution des applications est entièrement consolidée et historisée.

Entièrement ? Pas tout à fait.

Le plus souvent, une partie cruciale de ces évolutions reste gérée manuellement : les changements de schéma de base de données. Qui n’a jamais vu sa livraison échouer faute d’avoir pensé à modifier la colonne d’une table?

Pour remédier à cela, un certain nombre d’outils existent dont notamment Liquibase ou Flyway. Nous allons nous intéresser à ce dernier car il est orienté Java, permet de faire du SQL natif et s’utilise très simplement.

Qu’est-ce que c’est ?

Flyway désigne en anglais les corridors ou couloirs de migrations utilisés par les oiseaux… migrateurs. Aucun lien, fils unique ? Pas si l’on considère que cet outil permet de gérer les migrations de vos bases de données… Nous ne voyons pas d’autre explication.

Comme dit précédemment, sa grande force est sa simplicité de mise en œuvre. Pour l’utiliser, quatre solutions s’offrent à vous :

  • l’intégration dans votre application via Spring,
  • l’intégration à votre build Maven,
  • l’intégration à votre build Ant,
  • l’utilisation en ligne de commande.

Cette dernière solution peut s’avérer très pratique si vous voulez tester rapidement le framework ou réaliser des migrations sur un serveur sans rien installer de plus.

Nous allons ici particulièrement nous intéresser à l’utilisation via Spring mais sachez que l’emploi du plugin Maven diffère peu des autres solutions et peut tout à fait trouver sa place dans votre cycle d’intégration continue.

Comment ça marche ?

Pour simplifier, mettons-nous dans une situation où l’on désire créer une base à partir de zéro (nous expliquerons plus tard comment installer Flyway sur une base existante).

Pour commencer, il vous faut préparer votre schéma de base de données. En effet, Flyway a besoin d’installer une table dans votre schéma pour connaître la version courante et répertorier l’ensemble des modifications apportées. Un peu comme un .git ou un .svn en somme.
Pour ce faire, le plus simple est peut-être d’utiliser Flyway en ligne de commande. Il suffit alors de :

  • Télécharger l’outil sur votre serveur de base de données ou sur une machine qui y accède,
  • Ajouter le jar correspondant à votre connecteur dans le répertoire /jars (notre base étant sous MySQL 5.1, nous avons utilisé mysql-connector-java-5.1.20-bin.jar),
  • Configurer l’accès à votre base dans conf/flyway.properties en précisant l’URL, le login, le mot de passe de la base et le nom du driver utilisé,
  • Lancer la tâche d’initalisation :
    ./flyway.sh init

Vous verrez alors apparaître une table nommée schema_version qui contient en particulier le numéro de version courant, initialisé à 0.

Voilà, votre base est prête… mais vide. Regardons comment la remplir via Flyway. Avant tout, ajoutons la dépendance qui va bien. Si vous utilisez Maven :

<dependency>
	<groupId>com.googlecode.flyway</groupId>
	<artifactId>flyway-core</artifactId>
	<version>1.6.1</version><!-- A mettre à jour... -->
<dependency/>

Ensuite, ajoutons dans notre contexte Spring la déclaration du bean Flyway avec le datasource associé :

<bean id="flyway" class="com.googlecode.flyway.core.Flyway" init-method="migrate">
	<property name="dataSource" ref="myDataSource"/>
</bean>

Et précisons dans notre Session Factory l’ordre d’instantiation des beans :

<bean id="mySessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean" depends-on="flyway">
	...
</bean>

En clair, Spring va exécuter la méthode migrate de la classe com.googlecode.flyway.core.Flyway avant de créer la Session Factory.

Il ne nous reste plus qu’à ajouter les fichiers SQL de création dans le package db.migration situé dans src/main/resources/ en respectant la norme de nommage qui est par défaut :
« V[n° de version]_[n° de sous-version]__description_du_script.sql ».
Par exemple :
V1_0__creation_schema.sql
V1_0_2__ajout_table_utilisateurs.sql

Le principe est le suivant : à chaque chargement de la configuration Spring, Flyway va lancer la méthode migrate qui va scanner les scripts dans le package db.migration. En se basant sur leur nom, Flyway va exécuter les scripts dont le numéro de version est supérieur au numéro de version de votre schéma (indiqué dans la table schema_version). Simple et efficace.

Ainsi, lors du premier lancement de l’application, tous les scripts sont exécutés dans l’ordre des numéros de version. La prochaine fois que nous voudrons modifier notre schéma de base, nous n’aurons qu’à créer le fichier SQL correspondant dans db.migration en incrémentant le numéro de version (par exemple V1_1__modif_table_utilisateurs.sql) et relancer l’application.

À partir d’une base existante

On peut tout à fait gérer une base existante avec Flyway sans la recréer from scratch en se contentant d’ajouter les fichiers de migrations SQL par-dessus l’existant. Il faut juste veiller à ce que les migrations soient compatibles avec la base courante.

Cependant, le répertoire db.migration ne contiendra alors pas l’historique complet des versions. Cela signifie donc que vous ne pourrez pas rejouer automatiquement l’ensemble des migrations pour recréer votre base dans sa version courante. C’est fâcheux, en particulier si vous êtes amené à redéployer la base sur un nouveau serveur.

Par conséquent, il est préférable de simuler le fonctionnement de Flyway lorsqu’on récupère une base de données déjà remplie. Il suffit de :

  • Dumper la structure de la base et toutes les données nécessaires à son fonctionnement initial,
  • Placer le script SQL obtenu dans le répertoire de migration Flyway en le nommant par exemple V0_1__dump_existant.sql,
  • Initialiser la base en créant la table schema_version comme expliqué précédemment
  • Changer manuellement le numéro de version de la base afin de ne pas réexécuter le dump de l’existant :
    UPDATE `schema_version` SET `current_version`=0; #désactive la version courante.
    INSERT INTO `schema_version` (`version`, `description`, `type`, `script`, `checksum`, `installed_by`, `installed_on`, `execution_time`, `state`, `current_version`) VALUES ('1.0', 'Initialisation', 'INIT', 'Initialisation', NULL, 'admin', NULL, 0, 'SUCCESS', 1); #force la version courante à 1.0

    Les migrations futures devront alors être créées avec un numéro de version supérieur à 1.0.

Astuces

Plutôt que de devoir effectuer manuellement l’initialisation de Flyway sur chaque schéma, on peut créer une classe héritant de Flyway exécutant une méthode personnalisée qui va vérifier l’existence de la table de version de Flyway, lancer la tâche d’initialisation si besoin puis effectuer la migration.

public class CustomFlyway extends Flyway
{
	public int initMigrate() throws FlywayException, SQLException
	{
		Connection connection = this.getDataSource().getConnection();
		try
		{
			connection.prepareStatement( "SELECT * FROM " + this.getTable() ).execute();
		}
		catch( SQLException e )
		{
			CustomFlyway.LOG.warn( "Could not perform a request on table \"" + this.getTable() + "\". Assuming it does not exist, flyway.init() is called." );
			this.init();
		}
		return this.migrate();
	}
}

N’oubliez pas de mettre à jour votre configuration Spring :

<bean id="flyway" class="com.alinto.flyway.CustomFlyway" init-method="initMigrate">
	<property name="dataSource" ref="myDataSource"/>
</bean>

Et voilà ! Vous n’avez qu’à lancer votre application et tout est transparent !

D’autre part, notons qu’un bon nombre de paramètres sont configurables comme :

  • le nom de la table utilisée par Flyway
  • le nom du répertoire contenant les scripts de migration
  • le format de nommage des scripts (partiellement configurable)

On peut alors adapter l’outil à ses besoins. Chez Alinto, nous souhaitions gérer les versions par projet plutôt que par schéma en faisant cohabiter des versions de tables différentes dans un même schéma… En résumé, notre schéma de base de données devait ressembler à ça :

  • projet1_table1 (v1.2)
  • projet1_table2 (v1.2)
  • projet1_table3 (v1.2)
  • projet2_table1 (v2.6)
  • projet2_table2 (v2.6)

Par défaut, Flyway ne peut pas gérer des versions différentes sur un même schéma de base de données. Pour émuler ce fonctionnement, il a suffit de :

  • Répliquer la configuration Spring dans chacun des projets (avec des beans Flyway différents pour chaque projet),
  • Configurer ces différents beans Flyway afin qu’ils scannent les scripts de migration dans des packages différents,
  • Placer les scripts SQL de chaque projet dans leurs packages respectifs,
  • Configurer les beans Flyway afin qu’ils utilisent chacun leur propre table de versionnage en base.

Pour finir, signalons que :

  • Flyway peut exécuter n’importe quel ordre SQL dont l’insertion de données ce qui peut par exemple servir à remplir une base avant d’exécuter ses tests unitaires,
  • Flyway peut réaliser les migrations via du code Java plutôt que du SQL : cela ouvre considérablement le champ des possibles,
  • Flyway sait gérer des placeholders dans les scripts SQL,
  • Flyway exécute chaque migration dans une transaction donnée. Si la migration échoue, un rollback est lancé et garantit en théorie la cohérence de votre schéma. Dans les faits, la plupart des SGBD (dont MySQL) effectuent un commit après chaque commande SQL, ce qui relativise quelque peu l’intérêt de cette transaction…

En résumé, Flyway est un outil indispensable pour qui veut gérer correctement ses migrations de bases de données et dont la mise en place est tellement simple qu’on ne voit aucune excuse pour s’en priver !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *