Comment versionner les scripts de maj de base de données dans votre logiciel?

Bonjour les gens

Dans le contexte d’un logiciel existant en plusieurs versions, comment gérer le versionning des changements de structures de base de données entre les versions majeures ? et entre les versions développement et release d’une même itération.
C’est un souci récurent dans ma boite et qu’on a jamais vraiment réussi à régler (peut être lié à notre façon de travailler qui n’est pas optimale). Et du coup j’aimerais bien avoir des avis sur ce sujets.

Actuellement pour notre logiciel, qui existe en différentes versions majeures encore actives, lorsqu’on doit modifier la structure de la base de données, on utilise un mélange de « Script », qui impactent le numéro de version en base et que l’appli contrôle, et des « patch », qui sont la même chose que les scripts sauf qu’ils n’incrémentent pas le numéro de version en base. L’appli se met à jour lors de l’installation en ne passant que les script/patch nécessaire.
La bascule entre Script et Patch se fait quand on décide de commencer de développer la version majeure supérieure : cela permet de figer le numéro de version en base de la version précédente et facilite les migrations futures.
Ça marche bien quand on n’a qu’une seule branche par version majeure.
Mais si on commence à devoir faire des développements en même temps que la maintenance applicative sur une version, cela amène à devoir créer des scripts/patchs dans une branche pendant qu’un collègue en crée sur une autre, avec des risques de doublon sur les numéros de script au moment de tout fusionner.
Y’a t’il une manière propre de gérer ça ?

(Pour le contexte complet on vient de migrer de TFS vers GIT et ça discute pas mal sur créer des branches dans tous les sens, d’où cette problématique de gérer correctement les scripts entre deux branches)
En espérant avoir été clair (ce qui n’est pas tout le temps le cas dans ma tête) :smiley:

Si tu es sur une stack Java tu as des outils comme Flyway ou Liquibase qui gèrent ces problématiques.

La page de doc de Flyway qui explique le concept:

Tu as plusieurs manières de l’utiliser. Ma préférée consiste à embarquer les updates SQL dans le code et à faire en sorte que le service mette à jour lui même la base au démarrage.

3 « J'aime »

A noter qu’il n’est pas necessaire d’avoir une stack java. Ca fonctionne parfaitement bien en ligne de commande. Ca t’empeche juste d’embarquer les scripts dans le code.

Si c’est pas encore le cas, renseigne toi sur Docker, je pense qu’une bonne infra pensée avec Docker peut résoudre sûrement une grande partie de tes problèmes.

Je pense plutot comme dit @Twin que Liquibase (ou Flyway que je connais pas) sont parfaitement adaptés à son probleme…sans avoir à repenser toute son infra avec Docker.

(et comme le precise @Teocali effectivement pas besoin d’etre sur une stack java. Liquibase fonctionne tres bien en ligne de commande)

Rien à ajouter sur Liquibase ou Flyway, c’est définitevement les première approches à considérer (java ou pas).

Par contre j’aimerais vraiment comprendre comment

Tu as des sources pour expliquer comment faire et ce que Docker peut apporter à du versioning the DB ?
Pour moi, Docker sert avant tout à avoir une base infra « stateless » qu’on peut redéployer facilement et à l’identique - ça n’a rien à voir avec une base de donnée (versionnée ou pas), qu’on ne peut pas balancer aux orties à chaque redéployment, à moins de n’avoir rien à faire de ses données stockées dedans - or c’est souvent pour assurer la persistence des données qu’on utilise une DB, non?

Chez nous par exemple, on a des images Docker du produit avec la base de donnée inclue dans l’image docker, mais c’est utilisé uniquement pour les démos - et la base de données (modifiée ou pas pour les besoins de la démo) est bazardée avec l’environnement de démo une fois la démo terminée. Par contre quand le produit tourne en prod, la partie applicative peut tourner dans une image docker, mais la base de données et toujours deployée « à l’ancienne » et est utlisée par ce qui tourne dans docker. Si il y a une meilleure façon de faire ça éveille ma curiosité :slight_smile:

2 « J'aime »

Ça va faire genre 8 ans qu’on fait ça dans ma boîte, avec le vénérable dbdeploy. Je l’ai initialement mis en place et je chapeaute toujours ça maintenant. Quel que soit l’outil, tu va rencontrer strictement les mêmes problèmes.
D’abord, un petit topo.

  1. Pourquoi versionner une base de données ?
    Ça permet de suivre proprement les releases et de savoir à quelle version de release correspond une base de données. Ça permet ainsi (théoriquement) d’éviter le syndrome du « mon appli en version x.y.z » marche pas avec mon env de DEV/INT/PREPROD/PROD/WHATEVER. Idéalement, c’est même intégré à tes applis, comme ça elles savent si la version de la base est adaptée à leurs besoins.
    C’est un outil de communication avec les DBAs. Si un script correspond à une version, ça permet d’avoir un vrai process pour le suivi des patches, avec de vraies pull-requests.

  2. Quels softs ?
    Flyway, Liquibase, Dbdeploy (mais ça sent la naphtaline) ou juste rien et tout faire à la main. Liquibase est un peu pénible à utiliser mais ça fait le taf. Si t’as des DBAs, ils te laisseront jamais utiliser le XML de Liquibase (à raison), parce que c’est une mauvaise idée de pas être sûr à 100% de quel script va être généré.

Mais où est le piège, alors ? Ça a l’air trop trop merveilleux.

Logiquement ça devrait résoudre tous les problèmes de versioning. Mais là, tu vas rencontrer des problèmes humains ou de dette technique.

  1. L’automatisation
    Pour que ça marche et que les gens adhèrent à ça, il va falloir automatiser au maximum. Faut au minimum que tes scripts soient automatiquement validés en les passant sur une base dédiées à chaque commit/push. Ça évite les surprises et de devoir écrire des tas de rollbacks parce que tu t’es loupé.
    Faut que ce soit simple à utiliser par les devs et dbas, donc va te falloir des scripts utilitaires qui permettent de passer tous les scripts en une commande sans se poser de question.
    Plus ce sera confortable et automatisés, plus les gens accepteront de l’utiliser.

  2. Reconstruire une base from scratch
    Logiquement, avec du versioning, tu peux reconstruire ta base de données from scratch. Sauf que ça implique que t’utilises des outils de reverse engineering sur ta base pour recréer tous tes scripts manquants. Et les outils de reverse engineering de base de données, c’est souvent de la grosse merde. Faudra donc retoucher les scripts à la main, voire retoucher l’outil qui sert à générer tes scripts.
    À titre d’information, ça a pris quasiment 2 mois à 1.5 personnes dans ma boîte et c’était horriblement ingrat.

  3. Les scripts cross database et autres dépendances circulaires
    Dans un monde parfait, les vues, checks et autres procédures stockées de chaque base n’accéderaient qu’à leur base. Or le vrai monde est rempli de fausses bonnes idées. Faudra donc que tu trouves un moyen de gérer ça à peu près proprement. À noter que ça se combine aussi avec le problème de reconstruire tes bases from scratch. Il y a plein de chouette bidouilles pour contourner ça, comme créer des vues/procédures stockées vides, puis les référencer.

  4. Les données dites « de référence »
    T’as grosso modo 4 types de données dans une base :

  • des données de référence statiques (genre des mappings entre des IDs et des Status)
  • de la conf statique
  • des données archivées (qui n’ont rien à faire dans ta base de données live)
  • des vraies données dynamiques et valides
    Les données statiques, tu vas les vouloir dans tes scripts versionnés, mais pas le reste
  1. Le séquencement des scripts dans un environnement multibases

Tu vas te retrouver avec plein de scripts et pleins de bases de données. Mais va falloir réfléchir à dans quel ordre tu vas les passer, parce qu’il peut y avoir des interdépendances.
La solution ultime est sans doute d’avoir une numérotation unique pour tous tes scripts, quelle que soit la base. Cependant, ton dossier avec tes scripts va devenir une poubelle, avec le temps, rempli à ras-bord

  1. Versionner vraiment toutes les bases

Dans une boîte d’une taille respectable avec un SI assez gros, tu auras rarement un seul gestionnaire de base de données. Avec un peu de bol, tes archis de prod auront réussi à calmer les ardeurs des devs, mais c’est assez rare. Va donc falloir répéter le process de versioning avec tous ces serveurs de données, quel que soit le soft. Va aussi falloir avec des outils compatibles avec tout.

  1. Les DBAs/DEVs click & play

Les DBAs ont pleins de magnifiques outils qui permettent de faire des tas de trucs impactants, et non reproductibles en un clic. Ça, dans le monde du versioning, t’oublies. Faut aussi que les actions DBAs soient versionnées. Faut que les gens concernées appprennent à faire des vrais ALTER reproductibles plutôt que du clic bouton imprévisible.

  1. Les droits et autres permissions
    Idéalement, tu vas vouloir avoir à les mêmes utilisateurs applicatifs sur tous les environnements, avec les mêmes droits. Pour accomplir ça sans pleurer, faut que tes droits soient versionnés.

  2. Le temps de reconstruction des bases, quelques années plus tard
    Sur des environnements de dev, tu veux généralement instancier facilement de nouvelles bases. Tu veux que ça soit rapide, pour que les développeurs ne perdent pas de temps. Mais dans quelques années, je te garantis que tu vas en avoir pour une bonne trentaine de minutes minimum.
    Tu peux contourner partiellement le problème avec des sauvegardes/restoration des bases. Les moteurs de base de données peuvent faire ça de façon ultra rapide.

  3. Les jeux de données, le nerf de la guerre
    Comme dit ci avant, tes scripts de versioning n’incluront aucune donnée dans tes bases. Mais dans la vraie vie, tu vas en avoir besoin. Et ça n’a rien à faire avec le reste, puisque ça n’ira pas en production. Tu vas donc devoir les gérer dans une arborescence distincte, mais aussi les versionner.
    Dans ma boîte, on utilise des scripts SQL versionnés dans le même repo git que les autres scripts, mais dans un dossier distinct. Mais on utilise pas Flyway et leurs amis, parce que ça sert à rien pour ce besoin. On lance juste séquentiellement tous les scripts avec un job maven tout moche.

  4. Les releases
    Tu vas vouloir créer une branche par release ou par environnement, suivant ton process. Pour que ça marche, faut que les DBAs valident les scripts à un moment où à un autre et s’ils ont des modifs, qu’elles soient aussi versionnées. Ton outil pourra alors lancer les scripts manquants dans le bon ordre. Puis paf, ça marche

  5. Les trucs qui peuvent pas vraiment être versionnés
    Malheureusement dans la vraie vie, tu vas avoir des trucs qui pourront jamais suivre ce process. Les données client et les configurations distinctes en prod, par exemple. Va te falloir un autre process pour gérer ça. Nous on a pas trop trouvé de solution, c’est encore un peu à l’arrache.
    Des exemples de qui rentrent dans cette case, à mon sens : les scripts de rattrapage, les reconstructions d’index et autres scripts de maintenance.

  6. Les rollbacks et les tests de rollback
    Il y a un cercle de l’enfer pour ça. Tu vas forcément arriver un jour sur un truc qui marche pas en prod et faudra remettre la version précédente en prod, y compris pour la base de données. Donc ça veut dire qu’écrire tes scripts pour faire monter la version ne suffit pas, faut aussi des scripts pour retourner en arrière, remettre les versions précédents. Il n’y a pas d’outil magique qui gère 100% des cas sans retouche. Ça va être douloureux.
    Ces scripts de rollback, il faut les tester ! Parce que s’ils mettent encore plus ta prod en vrac, ça vaut même pas le coup de faire ledit rollback. En toute franchise, on a toujours pas trouvé la solution à ce problème là dans ma boîte. Heureusement, on fait quasiment jamais de rollback.

Je n’ai pas parlé de toutes les considérations de maintien de service pendant que tes scripts sont joués sur un environnement. C’est parce que ça ne change rien avec un process 100% manuel et sans versioning, faut y penser en concertation avec les DBAs dans tous les cas.

9 « J'aime »

J’ai lu la question en diagonale et ma réponse ne correspond pas du tout à la demande. Vu que j’ai la tête dans Docker depuis 2-3 semaines ça me paraissait logique mais non. Oubliez ce que j’ai dit :wink:
:running_man:

4 « J'aime »

Plein de choses tout à fait juste. :+1:

À propos des outils des DBA, j’espère bien qu’ils savent encore faire du ALTER TABLE. Je pense plutôt qu’ils sont envahis de scripts diverses et variées et qu’ils doivent se battre en permanence pour avoir des scripts dignes de ce nom, sans parler des générations automatiques de SQL que les développeurs leur filent.

Je dis cela car j’avais été DBA pendant 6 mois (mais ça m’avait lassé car pas assez de contact avec le métier) et que pendant des années j’ai été développeur de base de données et BI : souvent les DBA sont considérés comme la dernière roue du carrosse et sont mis devant le fait accompli. Moi avec mon profil j’essayais d’arrondir les angles entre le DBA d’une part, et les CP et les autres dévs, d’autre part.

Et je suis heureux d’apprendre qu’il existe des outils « magiques » de mise à jour et de mise en production de bases de données, mais dont tu expliques très bien les limites dans tes 13 points.

Docker se combine extrêmement bien avec les outils de versioning de base de données :wink:. Ça permet d’instancier un nouveau serveur vite fait bien fait pour valider les scripts ou juste pour un nouvel environnement. Instancier un nouveau serveur pour des besoins de tests, c’est pas forcément un travail laborieux qui mobilise un sysadmin et un DBA. :grinning:
Par exemple j’ai récemment bricolé un truc sur Kubernetes qui en une vingtaine de minutes reconstruit une bonne grosse base de dev puis lance tous les scripts, rajoute les droits, permissions et utilisateurs.

Faut évidemment faire la distinction entre les pratiques et les gens. Ce ne sont pas les gens qu’il faut mépriser mais les mauvaises pratiques, qu’elles viennent des développeurs des DBAs ou autre.

2 « J'aime »

Je viens de sortir de 2h de réunion sur le sujet encore, pour au final en conclure qu’on changera pas notre façon de faire sur un logiciel en fin de vie :clown_face:
Mais vos retours sont intéressants en tout cas. Merci.
Une autre méthode que j’ai vu passer c’est celle de Rails, qui horodate les scripts et conserve lors des migrations le listing des scripts exécutés. Cela permet de passer les scripts dans un ordre bien défini et uniquement ceux nécessaire. Alors que chez nous, on a choisi de numéroter les scripts, et seul le numéro du dernier est conservé. Du coup quand un dev et un correcteur veulent créer un script 14 sur leurs branches resp, ça coince lors des fusions.

C’est ce que font Flyway et Liquibase, sauf que l’ordre est déterminé par le nom des fichiers (Flyway) ou un fichier de conf dédié (Liquibase).

Ok
Pour l’avoir fait tourner (dans ma tête) j’ai l’impression que ça pourrait fonctionner avec notre façon de travailler. J’ai essayé de militer pour mais bon.

Chez nous depuis récemment on s’est mis à une approche plus « objet ».
On a fait des scripts qui appellent des fonctions qui prennent en paramètres les champs et index voulus pour nos tables.
Les fonctions comparent ce qui est attendu par rapport à l’existant et génèrent sur mesure les modifications de la structure de la base de données.

Comme c’est le bordel les bases. Y a des super messages plus haut sur comment on gère quand on est obligé. Je crois que au final je préfère l’alternative ou y a pas de schéma fixe en nosql (ce qui veut pas dire que c’est la foire amorphe dans les data non plus) mais ça veut dire que c’est à l’application de tout gérer. Parce que au final … c’est à l’application de tout gérer quand même (ou à un pauvre humain qui sert pas à grand chose au milieu qui s’appelle DBA et qui se bat pour tout gérer).

1 « J'aime »

Pas de schéma fixe, ça veut dire que tu dois littéralement implémenter l’équivalent de Liquibase à la main dans ton code. Ça revient donc totalement au même, à part que tu vas faire ça à la volée lors de l’accès au données.
La dernière fois où j’ai du faire ça, j’avais mis une clé numéro de version sur mes objets json et dans le code, je faisais séquentiellement toutes les opérations pour faire la montée de version, jusqu’à la dernière. C’est ni mieux ni moins bien que les tambouilles Liquibase, c’est juste quasiment pareil.

1 « J'aime »

En fait le seul truc important, lors d’une mise en prod d’une nouvelle version nécessitant une maj de la structure de la BDD, c’est de passer par une version intermédiaire entre N et N+1 afin de pouvoir faire un rollback si quelque chose vient à déconner au moment de la mise en prod.
La migration finale arrivant une fois tout stabilisé.

1 « J'aime »

Non, pas vraiment. Ça dépend des types de changements et de la manière dont est implémenté l’applicatif. En particulier si c’est juste pour remplacer une BDR par du NoSQL sous des objets métiers à base de serialization/déserialization c’est louper (et détruire) ce qui fait l’intérêt du truc. Une app orientée document ne devrait pas être construite pareil de bas en haut et rien ne t’oblige a mettre systématiquement à jour les documents. C’est donc pas “juste quasiment pareil” sauf si tu passes à côté de l’utilisation idiomatique des documents et tu fais “juste” les mêmes couches à l’ancienne en remplaçant une table par un JSON. Par exemple quand tu charges ton historique de commande Amazon et tu vois la commande de 1997, le document rendu n’est ni homogène, ni homéomorphique avec celui de ta commande de hier.

Mais bref c’est pas le sujet du topic “comment je versionne des documents” donc on va pas s’étendre.

1 « J'aime »

Nan mais perso, c’est connexe et ca m’interesse un peu, donc hesites pas a developper :slight_smile: Surtout que la question a été un peu repondue.

1 « J'aime »

On a pas mal discuté de ce sujet avec @GloP sur le discord y’a quelques semaines, et je vais vulgariser ce que j’ai retenu de son propos :smiley:
En gros, quand tu mets en production, ton code qui va charger les données, gerera les version N et N-1. Et quand il chargera un document, si c’est une version N-1, il fera la migration vers l’objet N « a la volée » avant de l’utiliser.
Tu as également un job qui tourne en asynchrone pour mettre a jour l’ensemble des objets en version N-1 sans bloquer l’accès aux données.
De même, une MEP déprécie le code qui gère les données en version N-2.

Voila, pour faire simple, ce que j’ai principalement retenu de notre discussion et qui touche au sujet de ce thread.

Bien entendu, c’est incomplet, et vraisemblablement incorrect sur certain aspect, je laisse le principal interessé venir corriger mon propos au lance-flamme :ninja:

7 « J'aime »