Les frameworks de versionnement de base de données pour .NET

Qu’est-ce qu’une migration ?

Cet article fait suite aux deux articles précédents d’Arnaud Villenave sur le cycle de vie et le déploiement de bases de données SQL Server. Nous allons aborder le problème sur un angle légèrement différent. En effet, la mise en place de « la grosse artillerie » entraîne quelques contraintes :

  • la gestion d’une base de données SQL Server et uniquement SQL Server
  • le déploiement se base sur la comparaison de schémas entre la base référence et la base cible. Or ce n’est pas systématiquement possible.
  • confier à un outil l’ensemble des opérations de migration peut avoir pour effet pervers de déconnecter le développeur des contraintes de gestion et de maintenance de la base de données. Or il est nécessaire de maintenir une attention sur ce point.
Or il existe (au moins) une autre façon d’aborder le problème. Quel est notre cahier des charges :
  • Connaitre à un instant T la version courante de la base de données, ainsi que son historique.
  • Pouvoir upgrader son modèle de données et le downgrader.
  • Effectuer ces opérations dans un environnement maîtrisé par le développeur.
Il existe dans le monde Ruby une notion qui répond à ces contraintes : la migration. La migration est une opération visant à passer votre modèle de données d’une version stable à une autre. Les opérations d’une migration sont de plusieurs types :
  • le modèle de données en lui-même : gestion des tables et colonnes
  • les performances : les index
  • la programmation : les fonctions et procédures stockées
  • les données : remplissage d’une table de référence par exemple
La méthode traditionnelle vise à écrire ces opérations à l’aide le plus souvent de scripts SQL exécutés à la main ou par un automate. Ruby On Rails fournit un framework permettant d’écrire en Ruby ces opérations. Deux avantages immédiats : le développeur se voit offrir la possibilité de n’utiliser à 95% qu’un seul langage (les procédures stockées seront à écrire quoiqu’il arrive en T-SQL, PL/SQL ou autre), et bénéficie de la vérification syntaxique de son compilateur.

Ecrire ses migrations en C#

Le framework Migrations de RoR a été porté en C# lors de plusieurs projets. Les deux qui me paraissent les plus intéressants sont le portage strict de RoR Migrations : FluentMigrator et le petit dernier, MigSharp (ou Mig#).
Exemple de migration FluentMigrator :

[Migration(1)] 
public class TestCreateAndDropTableMigration: Migration 
{ 
 public override void Up() 
 { 
 Create.Table("TestTable") 
 .WithColumn("Id").AsInt32().NotNullable().PrimaryKey().Identity()
 .WithColumn("Name").AsString(255).NotNullable().WithDefaultValue("Anonymous"); 
 Create.Table("TestTable2") 
 .WithColumn("Id").AsInt32().NotNullable().PrimaryKey().Identity() 
 .WithColumn("Name").AsString(255).Nullable() 
 .WithColumn("TestTableId").AsInt32().NotNullable(); 
 Create.Index("ix_Name").OnTable("TestTable2").OnColumn("Name").Ascending() 
 .WithOptions().NonClustered(); 
 Create.Column("Name2").OnTable("TestTable2").AsBoolean().Nullable(); 
 Create.ForeignKey("fk_TestTable2_TestTableId_TestTable_Id") 
 .FromTable("TestTable2").ForeignColumn("TestTableId") 
 .ToTable("TestTable").PrimaryColumn("Id"); 
 Insert.IntoTable("TestTable").Row(new { Name = "Test" }); } 

 public override void Down() 
 { 
 Delete.Table("TestTable2"); 
 Delete.Table("TestTable"); 
 } 
}
Et voilà. Voici une migration assez complète avec la création de deux tables liées par une clé étrangère, d’un index et l’insertion d’une ligne. Et son rollback, consistant à supprimer ces deux tables. Notez la syntaxe chaînée très aisément compréhensible et facile à écrire.
Un exemple Mig# :
 
[MigrationExport]
  internal class Migration2 : IReversibleMigration
  {
      public void Up(IDatabase db)
      {
           ...
      }
      public void Down(IDatabase db)
      {
           ...
      }
  }
Commençons par les points communs. Dans les deux cas, une migration est une classe, composée d’un ensemble d’opérations. La classe hérite d’une classe mère, exposant un ensemble de méthodes permettant de manipuler le modèle physique de données : tables, colonnes, index, contraintes, clés, ainsi qu’une méthode Execute(String sqlQuery) permettant donc d’exécuter n’importe quelle requête SQL. Les deux outils sont ouverts sur tout type de base de données supporté par ADO.NET. Le versionnement de la base de données est une simple table de versions, isolée du reste du modèle de données contenant la liste des migrations déjà jouées. La version courante est donc la dernière migration insérée dans cette table.
Pour exécuter les migrations, Mig# et FluentMigrator fournissent des exécutables prenant en paramètre 2 éléments obligatoires : la base de données cible et l’assembly contenant vos migrations, et un élément facultatif : la version cible. L’exécutable effectue une comparaison ensembliste des versions déjà jouées sur la base de données et, par réflexion, des migrations contenues dans l’assembly. Si la version cible n’a pas été fournie, le framework jouera l’ensemble des migrations de l’assembly non-encore existantes en base. C’est donc une mise à jour complète de la base de données. Si vous spécifiez une version supérieure à la version courante, l’outil jouera les upgrades manquants. Si vous spécifiez une version inférieure, l’outil jouera les downgrades.
A noter que pour garantir un modèle de données stable, il n’est pas possible d’avoir des « trous » dans vos versions : être en version N nécessite obligatoirement d’avoir joué les migrations inférieures à N.
Regardons maintenant les différences entre ces deux frameworks.
Avantages de FluentMigrator :
  • Exposition très intéressante d’une méthode d’insertion de données :
Insert.IntoTable("Users").Row(new { FirstName = "John", LastName = "Smith" });
  • L’exécution des migrations peut se faire en ligne de commande, mais aussi via des runners NAnt, MSBuild ou Rake. Cela ouvre facilement la porte au déploiement continu.
Avantages de Mig# :
  • La ségrégation par modules : si la base de données est partagée par plusieurs équipes de développement, déployant indépendamment les unes des autres, Mig# apporte une solution simple et élégante. Chaque migration est étiquetée avec un nom d’équipe et jouée indépendamment des migrations des autres équipes.
  • En plus d’un exe, Mig# fournit une API sous forme de DLL à intégrer et plusieurs possibilités d’extension. Libre à vous de broder autour un système de déploiement simple et souple.

Pour aller plus loin

Dans l’équipe où je travaille actuellement, nous avons opté pour Mig# du fait de la base de données partagée par plusieurs équipes. La ségrégation était donc obligatoire. La méthode d’insertion de données me manque, et même si nous passons par la fonction db.Execute(), il n’est pas impossible que nous y portions la class native Insert de Fluent Migrator, extrêmement intéressante. J’ai développé une simple surcouche à Mig# exposant des méthodes personnalisées, avec des énumérations pour nos bases de données et serveurs de fichiers de façon à rendre l’exécution plus simple. Le but est à terme de faire exécuter nos migrations par l’outil maison de déploiement des composants logiciels.
Vous voilà parés pour écrire des migrations de base de données, maitrisant ainsi mieux le versionnement de celui-ci. Il est tout à fait possible d’aller plus loin dans l’industrialisation du processus. Nous avons parlé plus haut de déploiement continu, qui est une étape supplémentaire après l’intégration continue et l’inspection continue.
Une autre possibilité est de contrôler au démarrage de votre application que sa version et la version de la base de données sont bien compatibles. De même que du monitoring de l’ensemble des bases de l’ensemble de vos environnements.
Voyez-vous d’autres applications ?

Un commentaire. Laisser un commentaire

[…] ASP.NET MVC4 bénéficie de l’intégration par défaut de Entity Framework 5, et du support de Code First Migrations, l’implémentation par Microsoft des migrations de base de données, dont je vous parlais ici. […]

Répondre

Laisser un commentaire

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