Résumé de l’article
La prévalence est une méthode peu connue consistant à remplacer la sérialisation dans une base de données par une sérialisation transparente des données dans des fichiers binaires, avec un système de logs et de snapshots pour pallier à tout redémarrage de la machine hôte. Le présent article cherche à savoir si cette méthode est industrialisable, et pose les principes de modification des objets métier. La conclusion sur ce point est qu’une application prévalence supporte très bien des montées en version, en deux phases ou bien par l’insertion d’un code de migration des données au démarrage. Le fait que le framework purge les données non utilisées garantit que l’application ne gonflera pas démesurément au fur et à mesure des versions. On aborde très rapidement la problématique de montée en charge (un article suivra sur ce point), et on finit en montrant le fort intérêt qu’une solution comme Linq To Objects peut avoir dans le contexte d’une application prévalente, pour remplacer le langage de requête SQL qui n’a plus de sens dans ce type de logiciels.
Introduction à la prévalence
Pour ceux qui ne connaissent pas (et personnellement, je me suis toujours étonné que cette technologie ne soit pas plus utilisée), la prévalence consiste à stocker les données d’une application en mémoire au lieu de les mettre en base de données. Logique : les serveurs sont aujourd’hui allumés en permanence, avec des redondances d’alimentation, et tout ce qu’il faut. Et puis, lorsqu’ils ne disposaient que de 64 Mo pour mettre 1 Go de données en mémoire, on était bien obligé de passer par le disque… Mais aujourd’hui où le moindre serveur digne de ce nom embarque 32 Go de RAM, pourquoi s’embêter à sérialiser des données sur un disque. Pour la gestion des index ? Mais une bonne hashtable fait aussi bien ! Pour garder ses données suite à une grosse panne électrique ? Bon, là d’accord… Mais la prévalence stocke également vos données sur disque ! La grosse différence de paradigme est que pour une base de données, la localisation principale des données est sur le disque, et elle propose des mécanismes pour monter des bouts de données rapidement en mémoire. Le principe de la prévalence est d’inverser ce paradigme : les données sont plus utiles en mémoire, donc on va principalement les stocker et les manipuler en mémoire. Par contre, on va également tracer en arrière plan sur un disque le log de modification de ces données, et de temps en temps, prendre un snapshot de l’état de la mémoire, de façon à pouvoir remonter une application en mémoire en cas de catastrophe. Bien sûr, ce genre d’opération prend du temps, mais comme on le disait plus haut, sur des vrais serveurs bien sécurisés et redondés, c’est censé arriver une fois tous les dix ans. Bref, avec un peu de chance, ça n’arrivera pas durant l’exploitation de votre application.
Ressources sur la prévalence
Bon, je ne vais pas non plus répéter ce qui a déjà très bien été écrit sur la prévalence. Pour les sceptiques, une page a été écrite spécialement pour vous sur le site de référence prevayler.org : http://www.prevayler.org/old_wiki/PrevalenceSkepticalFAQ.html. Et pour apprendre en quelques pages comment fonctionne Bamboo, qui est le framework de référence pour la prévalence en .NET, je vous engage à voir http://www.eggheadcafe.com/articles/20031019.asp ou directement les ressources du site http://bbooprevalence.sourceforge.net/.
Présentations des enjeux
Il faut quand même que je vous fasse voir quelques bouts de code de démonstration de la prévalence pour fixer les idées, surtout que le but de cet article est de poser les conditions d’industrialisation de la prévalence. Personnellement, j’aime bien tester des nouvelles technologies et faire joujou avec, mais j’ai toujours tendance à me méfier des articles où tout est facile. Souvent, au moment de mettre vraiment en place une solution technologique, on commence à réfléchir deux minutes, et on se rend compte que ça risque de tourner à quelque chose de plus difficile que prévu. J’ai déjà fait deux-trois tests sur la prévalence, et quelques mois après, je me suis dit « comment on gère le cycle de vie de l’application ? », « et est-ce que la sérialisation de tous ces objets ne va pas me bouffer mes performances ? », « et combien de temps ça prend réellement de remonter 2 Go de données sur un serveur qui vient de redémarrer ? », « Et comment ça se passe si je dépasse la mémoire autorisée, surtout que j’ai beau avoir 4 Go, ASP.NET n’en utilise que 1,5, si je me rappelle bien ? », etc. Bref, posons tout ça en présentant d’abord l’exemple (bateau) sur lequel on va travailler.
Présentation d’un prototype
Je ne vais pas atteindre le niveau le plus bas de l’originalité en proposant une gestion d’annuaire téléphonique, mais ce sera à peine mieux : une gestion des réservations. Ca peut être n’importe quoi, un hôtel, un port de plaisance, ou même un cimetière…
Je fais exprès au plus simple (moche, diront certains) en WinForms. Le but est de voir ce qu’on va mettre dans les objets et comment est censé se comporter ce prototype. Bref, voici la GUI de l’application :
Deux objets métier seront utilisés, à savoir des “Personne” et des “Reservation”. Le code source pour ces deux objets est également on ne peut plus simple :
using System; using System.Collections.Generic; using System.Text; namespace PrevalenceQuery.Work.Personnes { [Serializable] public class Personne { private string _UID; public string UID { get { return _UID; } } private string _Nom; public string Nom { get { return _Nom; } } private string _Prenom; public string Prenom { get { return _Prenom; } } public string NomComplet { get { return string.Concat(_Prenom, " ", _Nom); } } public Personne() { } public Personne(string iNom, string iPrenom) { _UID = Guid.NewGuid().ToString("N"); _Nom = iNom; _Prenom = iPrenom; } } }
using System; using System.Collections.Generic; using System.Text; namespace PrevalenceQuery.Work.Reservation { [Serializable] public class Reservation { private string _UID; public string UID { get { return _UID; } } private DateTime _Date; public DateTime Date { get { return _Date; } } private Personnes.Personne _Personne; public Personnes.Personne Personne { get { return _Personne; } } public string Description { get { return string.Concat(_Personne.NomComplet, " (", _Date.ToShortDateString(), ")"); } } public Reservation() { } public Reservation(DateTime iDate, Personnes.Personne iPersonne) { _UID = Guid.NewGuid().ToString("N"); _Date = iDate; _Personne = iPersonne; } } }
Première remarque : ces objets sont notés Serializable, ce qui est logique car la prévalence a besoin de sérialiser les objets métier pour les stocker sur le disque dur, en cas de panne. Ceci est fait automatiquement, sans que vous ayez à intervenir. Toutefois, le moteur de prévalence ne stocke pas bêtement tout ce qui traîne en mémoire dans votre application, mais seulement les objets rattachés à un ensemble commun, et qui sert de base pour la structure objet de l’application.
Détaillons du coup cette classe de stockage de données :
using System; using System.Collections.Generic; namespace PrevalenceQuery.Work { [Serializable] public class Ensemble { public List<Reservation.Reservation> ListeReservations = new List<Reservation.Reservation>(); public List<Personnes.Personne> ListePersonnes = new List<Personnes.Personne>(); public Ensemble() { } } }
Là encore, la classe est sérialisable, et elle contient tout simplement une référence aux deux listes d’objets que nous allons gérer dans l’application. On peut considérer que cette classe représente la totalité des objets qui seront stockés en mémoire et, de manière transparente, dans le disque dur, même si on verra plus loin que les commandes métier sont également sérialisées.
En plus de cette classe de base des objets métier, il faut une classe pour la gestion du moteur de sérialisation lui-même, de façon à lui signifier qu’il doit pointer sur la classe de base. Cette classe, dans le présent prototype, s’appelle Racine, et le code source est repris ci-dessous :
using System; using System.IO; using Bamboo.Prevalence; using Bamboo.Prevalence.Util; namespace PrevalenceQuery.Prevalence { public class Racine { public static readonly PrevalenceEngine Moteur; static Racine() { Moteur = PrevalenceActivator.CreateEngine(typeof(PrevalenceQuery.Work.Ensemble), Path.Combine(Environment.CurrentDirectory, "Persistence")); new SnapshotTaker(Moteur, TimeSpan.FromMinutes(1), CleanUpAllFilesPolicy.Default); } } }
On note en tout premier la référence aux librairies Bamboo, qui est le gestionnaire de prévalence que j’utilise pour ce prototype. Il y en a apparemment un autre qui existe en .NET, sous le nom de XPrevail, mais je ne l’ai pas testé. Apparemment, il n’y a plus d’activité dessus depuis 2008. En même temps, il en va de même pour Bamboo… Comme je vous le disais, ce n’est pas une technologie qui mobilise les foules, malgré les énormes avantages qu’elle peut avoir.
La classe Racine possède un constructeur statique qui va charger le moteur de prévalence dès qu’il y en aura besoin, et ce avec les paramètres suivants :
- Le type de la classe de base Ensemble, qui contiendra toutes les données de l’application référencées dans une seule instance.
- Le chemin du répertoire utilisé pour la persistence, en l’occurrence ici le répertoire courant suivi de Persistence. L’avantage de ce genre de stockage est que, pour supprimer le contenu complet des données métier de votre application, c’est très simple : vous supprimez le répertoire. Très pratique en débuggage.
Il y a une deuxième ligne de commande dans le constructeur statique, à savoir la mise en place d’un moteur de Snapshot, basé sur ce moteur. Dans notre cas, il est réglé pour prendre un cliché de l’état mémoire toutes les minutes, et fonctionner avec le mode par défaut de gestion des fichiers.
Gestion des commandes
En général, lorsqu’on explique cette histoire de snapshot, une première question vient à l’esprit : « si un cliché de l’application est pris toutes les minutes, est-ce que cela signifie que tout ce que je fais dans la dernière minute avant qu’une application plante est perdu ? ». La réponse est non, et je reviens à ce que je disais précédemment sur les commandes. Elles aussi sont sérialisées. Un peu comme dans un système de base de données où les commandes sont enregistrées dans le log, pour être rejouées en cas de problème. Les commandes sont sérialisées dès leur création par le moteur Bamboo, et pourront donc être rejouée sur un snapshot remonté en mémoire après un arrêt brusque de l’application. On peut également imaginer un système de type MSMQ pour continuer à stocker les commandes sérialisées pendant que l’application redémarre.
Voici le code source d’une commande :
using System; using System.Collections.Generic; using Bamboo.Prevalence; namespace PrevalenceQuery.Commandes { [Serializable] public class CreationPersonne : ICommand { private string _Nom; private string _Prenom; public CreationPersonne(string iNom, string iPrenom) { _Nom = iNom; _Prenom = iPrenom; } public object Execute(object system) { (system as Work.Ensemble).ListePersonnes.Add(new Work.Personnes.Personne(_Nom, _Prenom)); return (system as Work.Ensemble).ListePersonnes[(system as Work.Ensemble).ListePersonnes.Count - 1]; } public object Execute() { return Prevalence.Racine.Moteur.ExecuteCommand(this); } } }
Encore une fois, on voit bien que l’objet est sérialisable. En plus de ça, il hérite de l’interface ICommand définie par le framework de prévalence Bamboo, et qui demande l’implémentation des fonctions d’exécution de la commande. La première fonction prend en paramètre un objet nommé system, et qui n’est autre que l’instance de classe Ensemble, point de fédération des objets métier, qui est gérée par le moteur de prévalence. La classe Racine, quant à elle, expose de manière statique le moteur de prévalence, de façon à pouvoir appeler facilement ExecuteCommand en lui passant l’instance de l’objet ICommand.
Le code de la fonction d’implémentation proprement dite de la commande est assez simple, à savoir dans notre cas exemple de création d’une personne, qu’on ajoute dans la liste statique une instance de Personne construite avec les paramètres qui avaient été envoyés lors de la création de l’instance de commande, puis qu’on renvoie cette instance en sortie de la commande.
Pour déclencher les commandes, rien de plus simple :
private void btnCreerPersonne_Click(object sender, EventArgs e) { new Commandes.CreationPersonne(txtNom.Text, txtPrenom.Text).Execute(); }
Voici pour l’introduction à la prévalence et au prototype qui va nous servir de base d’étude. Maintenant, passons à la partie intéressante de l’article, à savoir comment gérer industriellement une application basée sur la prévalence. Est-ce que les avantages compensent suffisamment les inconvénients, que faut-il tester, et quels sont les problèmes auxquels on ne pense pas initialement, mais qui peuvent se révéler dangereux ?
Cycle de vie de l’application
Une application vit et se développe au fur et à mesure de ses versions. Dans notre application de réservation avec une seule date, un client aurait tôt fait de nous demander de pouvoir plutôt utiliser un système avec une date de début et une date de fin plutôt que de rentrer des séries d’enregistrements avec une seule date.
Modifions donc la classe de réservation pour prendre ceci en compte. Comment être le plus compatible avec l’existant ? J’ai déjà des données avec une date de réservation, et je ne souhaite pas avoir à les migrer. La proposition retenue est de rajouter une DateFin qui sera à null pour les instances déjà existantes, et qui sera remplie pour celles nouvellement créées. Dans le cas où on trouve une DateFin à null, on affichera une date de fin égale à celle de début.
private DateTime? _DateFin = null; public DateTime DateFin { get { return _DateFin == null ? _Date : (DateTime)_DateFin; } }
Du coup, la description change également un peu :
public string Description { get { string Duree = _Date.ToShortDateString(); if (_DateFin != null) Duree += "-" + ((DateTime)_DateFin).ToShortDateString(); return string.Concat(_Personne.NomComplet, " (", Duree, ")"); } }
Question bête : est-ce que ça marche avec la prévalence ? Réponse : oui, en tout cas avec Bamboo. La déserialisation ne se soucie pas du fait qu’un nouveau membre ait été rajouté, vu qu’il a une valeur par défaut. On peut donc mettre à jour notre interface graphique avec un deuxième champ, et vérifier la fonctionnalité plus complète :
Bref, ça marche dans ce cas de membre rajouté. C’est une bonne nouvelle, car c’est quand même 90% des évolutions du Modèle Conceptuel de Données. Maintenant, il faut tout de même faire quelques petites remarques sur le fonctionnement précis de ces modifications.
Tout d’abord, on n’a pas modifié le nom de Date, qui auparavant correspondait à la date réservée, et désormais à la date de début. Du coup, le code n’est pas parfaitement propre : un développeur qui ne connaît pas l’historique de l’applicatif va se demander pourquoi on a un DateFin et pas un DateDebut, alors que le premier champ est visiblement utilisé pour le début des durées de réservation.
Si on cherche à renommer le champ, il est évident que la désérialisation ne pourra pas se retrouver dans les données binaires enregistrées. Essayons de modifier le champ et la propriété pour avoir :
private DateTime _DateDebut; public DateTime DateDebut { get { return _DateDebut; } }
Lorsqu’on lance l’application, pas de plantage, mais les dates de début sont toutes à la valeur par défaut, et les données ont été perdues :
Au passage, attention à bien sauvegarder votre répertoire Persistence lorsque vous faites ce genre de manipulation, parce que même en revenant en arrière sur la modification de code, les données ont été perdues, et pour poursuivre, j’ai été obligé de restaurer les fichiers de persistence générés par le framework Bamboo.
D’ailleurs, par curiosité, voici le contenu du répertoire de persistence :
On voit bien les snapshots ainsi que les logs de commandes, gérés dans deux types de fichiers de sérialisation bien différents.
Imaginons maintenant une deuxième modification de l’application, dans laquelle on en viendrait à supprimer un champ, par exemple le prénom dans la classe Personne. Là encore, tout se passe bien. Bon, c’est relativement rare qu’on supprime de la donnée, mais c’est toujours un cas de plus qui se passe bien en prévalence. On obtient juste les résultats tronqués dans l’interface :
Encore une fois, attention lorsqu’on revient en arrière dans le code : la prévalence s’est réellement débarrassée de la donnée. Si on remet le code de Personne exactement comme avant et qu’on relance à nouveau l’application sans restaurer le répertoire de persistence, on s’aperçoit que les données ont été effacées :
Notez l’espace au début des noms complets : il se trouve là car les prénoms ont été rétablis et il faut les séparer du nom de famille, mais tous les prénoms sont vides. C’est très intéressant, car cela veut dire que la sérialisation a supprimé les données qui n’étaient plus utilisées. Premier enseignement intéressant sur ce moteur… En effet, cela veut dire qu’il y a nettoyage au fur et à mesure et qu’on ne risque pas, après plusieurs années de modification d’une application, de se retrouver avec des sérialisations d’une taille fortement supérieure au contenu réel de l’application. Seul ce qui sert est gardé.
Restaurons donc à nouveau les données pour tenter une dernière chose : si les données sont nettoyées, pourquoi ne pas utiliser cette fonctionnalité pour, en deux étapes, migrer notre Date en DateDebut ? On commence par ajouter DateDebut dans Reservation, et modifier la propriété Description pour profiter de ce qu’elle est appelée pour toutes les instances pour remplir le champ DateDebut :
private DateTime _DateDebut; public DateTime DateDebut { get { return _DateDebut; } } public string Description { get { // On profite que cet appel soit systématiquement réalisé au lancement de l'application // pour faire migrer la valeur de Date dans DateDebut _DateDebut = _Date; string Duree = _Date.ToShortDateString(); if (_DateFin != null) Duree += "-" + ((DateTime)_DateFin).ToShortDateString(); return string.Concat(_Personne.NomComplet, " (", Duree, ")"); } }
On lance l’application, et normalement, notre DateDebut est désormais bien remplie. Il faut ensuite modifier à nouveau le code pour supprimer le champ _Date et la propriété associée, et remplacer toutes les occurrences par DateDebut. Et ça marche !
Bref, nous avons donc fait évoluer le code de notre application et les données à la suite. Il faut bien sûr faire attention à parcourir toutes les instances, mais ce genre de manipulation test ouvre la voie à des migrations plus complexes, qu’on pourrait par exemple mettre en place lors du chargement de l’application en parcourant toutes les données à migrer. Le framework se chargeant de nettoyer la persistence des données qui ne sont plus en correspondance avec des champs existants, cela veut dire qu’on peut se permettre ce genre de migration en deux phases sans risquer de faire gonfler la taille globale de la sérialisation.
Propositions pour une migration complexe de modèle
Le but de ce blog est de se placer systématiquement d’un point de vue industriel. Dans les paragraphes suivants, on a montré qu’il y avait moyen de réaliser un changement simple de modèle, en lançant l’application en phase intermédiaire. Mais comment réaliser des migrations plus complexes, en modifiant également les interactions entre les objets ? Deux approches sont possibles :
- Mettre en place tous les champs supplémentaires dans les objets, réaliser le code de migration lors du lancement de l’application, puis supprimer tous les champs devenus inutiles. C’est l’extension de ce qui a été montré plus haut, et ceci n’est recommandable que dans les cas où on peut maîtriser assez simplement la migration. Il sera tout de même utile de mettre en place un plan papier avant de coder.
- Dans les cas les plus complexes, il peut être intéressant de réaliser la migration en un seul coup, en ajoutant une seconde classe de support des racines d’objets métier, voire en dupliquant carrément le moteur de prévalence. Ces deux approches ont l’avantage de séparer radicalement les données avant et après la migration, et ainsi de rendre plus compréhensible la migration des données.
Dans un cas comme dans l’autre, il est bien évidemment fortement recommandé de sauvegarder le répertoire de persistence et de tester la migration, car si des données sont perdues, elles le seront de manière irrémédiable, le moteur de prévalence purgeant les données devenues inutiles, comme on l’a vu précédemment.
Montée mémoire en mode prévalent
Dans l’application prototype qui a été montrée, un problème sur des gros volumes est que, lors du démarrage du moteur de prévalence, toutes les données sont désérialisées en mémoire. Tant qu’on est sur des petits volumes, pas de problème, mais si on passe sur des fortes charges, cela peut prendre du temps. C’est par exemple une bonne raison pour ne pas faire une migration en deux passes, mais plutôt charger un deuxième ensemble de prévalence, puis libérer le premier à la suite du processus de migration.
L’expérience montre qu’à partir du chargement de 100 000 objets métier (de taille raisonnable) sur un ordinateur à peu près récent, une lenteur de démarrage commence à être plus que sensible, presque gênante. La désérialisation étant binaire, le processus reste très rapide, mais il faut en tenir compte.
Une solution dans le cas de données très nombreuses peut consister à scinder les données en domaines, qu’on chargera dans des moteurs de prévalence différents, et ainsi de ne pas avoir une grosse attente lors du chargement. Typiquement, dans une application industrielle sur le même domaine métier que le prototype décrit ici, on pourrait ne charger que les personnes pour un utilisateur se chargeant des relations clients, et charger le domaine de réservation uniquement lors du premier appel d’un utilisateur se chargeant de ces données. Il faut toutefois se rappeler que les applications en prévalence sont censées rester longtemps en mémoire, donc l’effet de cache jouera à plein dans ces cas.
Serveur obligatoire
Lorsqu’on utilise une base de données, l’application est naturellement décorrélée du métier, car elle ne se charge que du stockage des données. Dans le cas de la prévalence, la persistence se fait au niveau des objets métier eux-mêmes. C’est incontestablement l’avantage majeur de cette technologie, mais il s’accompagne d’une obligation supplémentaire, à savoir que le métier doit absolument être séparé de la GUI, sauf bien sûr dans le cas particulier d’une application autonome. Bon, les applications client-serveur ne sont plus légion, mais il faut tout simplement en être conscient si vous voulez utiliser la prévalence : vous serez obligé d’utiliser un serveur applicatif pour le métier, et de grouper vos traitements derrière des services web, du remoting, ou n’importe quelle autre technologie vous permettant de séparer le métier de plusieurs instances clientes.
Arrêt de l’application pour migration
De ce point de vue, pas de réponse extraordinaire de la prévalence. Lorsque vous mettez en place une migration de modèle, vous êtes obligés d’arrêter l’application.
En soi, ce n’est pas très différent de ce qui se passe avec une base de données. Oui, on peut très bien mettre à jour une base de données tout en la gardant en ligne, mais honnêtement, c’est vraiment limité à des besoins très spécifiques où la mise hors ligne pour quelques heures est considérée comme coûtant trop cher par rapport au surplus de risque. Non, je vous parle simplement d’une application de gestion de données que des gens utilisent dans la journée, par exemple. Maintenant, si on repose le problème de la sérialisation, potentiellement, lorsque vous allez redémarrer l’application, vous pouvez attendre quelques heures sur des gros ensembles de données. Il vaut donc mieux bien planifier vos migrations, et comme on le disait auparavant, pourquoi ne pas scinder les données en plusieurs moteurs de prévalence indépendants lorsque cela est possible ?
Et SQL là-dedans ?
Un des reproches fait à la prévalence tient au fait que les bases de données disposent d’une technologie avancée de requêtage des données, et que la prévalence n’a rien à proposer d’équivalent à SQL. Il y a quelques années, ce reproche m’avait laissé perplexe : SQL et les bases de données ont été inventés pour rendre moins dramatique la perte de performance due à la migration des données de la mémoire vive à une mémoire disque beaucoup plus lente. Si on rendait les données à la RAM, quel besoin avait-on de chercher une technologie de requêtage ? Il suffisait de faire ces requêtes en code, ce que n’importe quel programmeur fait mieux qu’une requête SQL.
J’ai compris plus tard lorsque j’ai voulu mettre en place un ETL pour des données XML. Cette technologie, très utilisée dans la société pour laquelle je travaille, consiste à décrire en XML une liste d’actions de transformations qui s’applique sur des données XML accompagnées de leur XMLSchema associé, et enrichi de nos métadonnées. Après des mois et des mois de développement, nous avons quelque chose qui convient parfaitement à nos besoins, et qui est à peu près capable de s’optimiser dynamiquement, en choisissant quelle action doit être menée en base de données, quelle action doit être menée sur le DOM, etc.
Note : Au passage, que pensez-vous être le plus rapide sur 10 000 lignes : une opération Sort XSL avec le parseur .NET, ou bien le tri par index d’une base de données ? Beaucoup de gens pense que la base de données est bien plus optimisée pour ce genre de travail. Et bien ce n’est pas toujours le cas, et je vous encourage à tester.
Pour revenir à nos moutons, cette technologie de requêtage sur des données en mémoire a représenté un gros investissement, ce qui veut dire que quelque chose de simple comme de transformer de la donnée présente en mémoire, sans avoir à se soucier de l’extraction, représente tout de même beaucoup de travail. Mais au fait, lorsque je parle de requêter des données en mémoire, ça ne vous fait pas penser à quelque chose ? Linq, bien sûr !
Linq To Objects à la rescousse
Lorsque Linq est apparu, en tant que créateur de l’ETL en mémoire dont je vous parlais auparavant, j’étais… comment dire… un peu écœuré. J’étais bien content de mon petit framework, et voilà que sortait une technologie, incorporé directement à .NET, et qui allait envoyer directement mon aimable bricolage à la poubelle. J’étais capable d’optimiser les tris, les variables calculées, les restrictions sur des formules, les jointures ? Linq faisait dix fois mieux partout, en générant des requêtes paramétrées, et surtout en étant capable de transformer tout ce qu’on lui demandait en du bon SQL fonctionnel et rapide.
Note : pour garder un peu la tête haute, je précise que mon API supportait Oracle, ce que Linq ne fait pas nativement, bien qu’il existe des librairies pour ça. Je profite d’ailleurs de cet article pour lancer un appel sur ce sujet à des gens ayant testé ce genre d’API : lesquelles avez-vous testées ? La qualité et les performances sont-elles au rendez-vous ?
Bref, il est temps de moderniser mon API et de la baser sur Linq. Et c’est dans la versatilité de Linq qu’apparaît tout l’intérêt de cette technologie pour les situations de prévalence. Autant Linq To SQL est une excellent candidat au remplacement d’un ETL, autant Linq To Objects est une extension naturelle à la prévalence, en permettant à l’utilisateur de requêter ses objets métier avec une performance et une souplesse à mon avis sans pareil. Un exemple tout bête :
var resultat = from Reservation in (system as Work.Ensemble).ListeReservations where Reservation.Personne.Nom.StartsWith("G") where Reservation.DateDebut.CompareTo(DateTime.Now.AddDays(2)) < 0 select Reservation.UID;
Ce code n’a bien évidemment pas nécessité une seule modification sur les objets métier utilisés dans le prototype présenté.
Tout n’est pas absolument rose
Arrivés à ce point de mon monologue (si certains ont réussi à tenir), vous devez vous demander ce qui cloche. Ben oui, je vous présente une technologie connue depuis de nombreuses années, et pourtant étonnamment utilisée par aucun éditeur de logiciel, ou en tout cas pas ouvertement, alors qu’elle paraît révolutionnaire… Il y a bien SAP qui parle de mettre la base de données en mémoire, mais après enquête rapide, ça n’a rien à voir avec de la prévalence : on garde la bonne vieille structure tabulaire de base de données, et on la met en RAM au lieu des disques durs. La belle affaire : la moitié des administrateurs réseau ont eu l’idée de monter les fichiers de base de données sur des RAMDISK lorsque la performance était vraiment capitale. Bon, j’exagère, et SAP semble mettre un peu de travail pour améliorer effectivement le fonctionnement d’une base de données utilisant la mémoire, mais rien de similaire à la prévalence.
Alors, pourquoi est-ce que la prévalence n’est pas plus utilisée ? Honnêtement, je suis bien en peine de trouver une réponse valable. Il y a bien sûr avant tout la frilosité de personnes auxquelles vous dites que les données vitales de son entreprise vont être dans une mémoire volatile, alors qu’il ne rêve à rien tant qu’un système de sauvegarde absolument infaillible. Exactement le même genre de recul spontané que l’on observe sur les technologies Cloud : « alors mes données vont être dans les nuages, c’est ça ? ». Ou bien avec les network computers : « et mes données, physiquement, elles sont où ? ». Tout ça met du temps à s’estomper. Ceci n’est d’ailleurs pas une critique : j’ai moi-même de nombreux a priori sur ces technologies.
Il y a aussi, il ne faut pas se voiler la face, le problème du SQL. On a vu que Linq permettait de remplacer le SQL par quelque chose de plus souple et qui fonctionne en mémoire. Oui, mais lorsque l’application legacy contient 1000 requêtes SQL dont certaines bien complexes ont mis des jours et des jours à être optimisées, il est compréhensible que l’idée de tout réécrire pour des gains de performance doive être soigneusement pesée.
Il est vrai également que, si on ne fait pas attention à la qualité du code de migration, on se retrouve avec des erreurs du genre :
Eh oui, j’aurais du faire plus attention dans ce cas à mes structures d’objets, car deux modifications en parallèle m’ont amené à utiliser un nom commun, et le résultat était que le moteur a vu un changement de type sur un même champ. La sérialisation plante alors, et il vaut mieux revenir à des anciennes données par sécurité.
C’est là qu’on se retrouve confronté à un autre problème potentiel : les données sont stockées de manière sérialisée en binaire, et donc absolument illisible si on perd le programme associé. Avec une base de données vieille de dix ans, on peut retrouver, au prix d’efforts, des données intéressantes même si le programme qui allait avec n’est plus trouvable. Oubliez ça avec la prévalence : vos données sont liées intimement à votre programme, et les fichiers que Bamboo créent ne vous seront d’aucune utilité sans l’applicatif correspondant.
Alors, on y va ?
Bon, j’ai essayé d’être le plus honnête possible avec les aspects négatifs de cette technologie, mais comme moi, vous vous demandez peut-être encore pourquoi ce n’est pas plus utilisée… Peut-être tout simplement qu’il s’agit d’une des ces technologies maudites dont l’histoire de l’informatique et de l’électronique est friande ? Rappelez-vous le Betamax, Next, OS/2, et toutes ces autres technologies superbement architecturées et révolutionnaires… Elles n’ont pourtant pas connu la gloire. Je pense que la prévalence est partie sur la même voie : peut-être trop en avance ? Mais peut-être aussi qu’il manque tout simplement la killer application qui va rendre cette technologie incontournable… Donc, à vos claviers !