1. Flyway en tant qu'interprète de l'histoire
Après la leçon précédente, il est facile de tomber dans une illusion romantique : « Ok, je vais stocker le SQL dans le projet – donc tout est résolu. » Mais stocker des fichiers ne suffit pas – il faut encore les appliquer à la base de données dans le bon ordre et une seule fois. Flyway agit ici comme un bibliothécaire strict : il ne discute pas, n'imagine pas et ne devine pas. Il prend simplement les migrations, vérifie l'historique et exécute ce qui n'a pas encore été fait.
Point important : Flyway n'essaie pas de « comprendre votre modèle d'entité ». Il se fiche que vous ayez @Entity, @OneToMany et quel genre de Hibernate vous utilisez. Son monde est plus simple et plus honnête : il y a une base de données, des fichiers SQL, des numéros de version, et une table où sont enregistrés les fichiers déjà exécutés.
Pour simplifier avec une analogie domestique, ddl-auto=update – c'est comme un robot aspirateur qui, la nuit, déplace les meubles « comme il le juge bon », et le matin vous vous promenez dans l'appartement en vous étonnant. Flyway – c'est comme une liste de contrôle de travaux de rénovation : « d'abord on a posé le carrelage, puis on a installé la porte, puis on a peint les murs », et chaque étape est documentée. Oui, c'est un peu moins magique. Mais au moins, on n'a plus peur.
Pour saisir le caractère de Flyway, il est utile de garder à l'esprit trois règles : une migration est un fichier, l'ordre est une version, le fait d'exécution est une entrée dans l'historique.
Et il est utile de distinguer immédiatement le mécanisme de Flyway de l'ensemble de bootstrap spécifique à shop-data-jpa. Prenons d'abord une migration conditionnelle sur une seule table, juste pour voir la forme du fichier. Et pour le projet lui-même, il faut non pas un seul fichier, mais tout un ensemble de démarrage.
Migration dans Flyway : une étape de schéma
Le terme « migration » sonne presque comme « déplacement de peuples », mais en pratique c'est plus calme : c'est un fichier avec des commandes SQL qui effectue une étape significative dans l'évolution du schéma. Dans ce cours, nous utiliserons principalement des migrations versionnées – celles dont la version est intégrée dans le nom du fichier et détermine l'ordre d'application.
Une migration typique dans un projet se trouve dans les ressources (le chemin et l'intégration seront abordés plus tard, aujourd'hui il est important de comprendre l'idée) et a l'air très prosaïque. C'est du SQL ordinaire que vous pourriez exécuter manuellement – seulement maintenant il est exécuté de manière disciplinée et reproductible.
Par exemple, « une migration créant une table de catégories » pourrait ressembler à ceci :
-- Exemple de migration individuelle : V1__create_category_table.sql
-- Ceci est un fragment schématique : l'ensemble du bootstrap du projet sera plus large.
create table category (
id bigint generated by default as identity primary key, -- identifiant technique (PK)
code varchar(64) not null unique, -- code métier de la catégorie, doit être unique
name varchar(255) not null -- nom lisible de la catégorie
);
Remarquez le caractère paisible d'un tel fichier : il n'essaie pas de « corriger tout ce qui existe », ne fait pas dix ALTER TABLE sans rapport en vrac et ne suppose pas que « quelque chose existe déjà dans la base ». Une bonne migration doit être conçue pour que vous puissiez prendre une base vide et obtenir un résultat prévisible.
Et encore une nuance utile pour les débutants : une migration n'est pas nécessairement « une seule commande ». Cela peut être plusieurs opérations SQL si elles constituent logiquement une seule étape. Mais plus l'étape est grande, plus il est difficile de comprendre « ce qui a changé exactement » et pourquoi.
2. Nommer les migrations : version et description
Quand les migrations deviennent une longue histoire du projet, on découvre soudain que le nom du fichier n'est pas une partie décorative. C'est comme le nom d'un commit dans Git : on peut écrire « fix » ou on peut écrire « add unique constraint for product.sku ». Formellement, les deux fonctionnent, mais le second sauve les nerfs un mois plus tard.
Dans le format de base, Flyway attend un nom à peu près comme celui-ci :
V<version>__<description>.sql
Et il y a immédiatement deux éléments importants : la version et la description. La version est nécessaire pour déterminer sans ambiguïté l'ordre. La description – pour qu'une personne comprenne de quelle étape il s'agit sans ouvrir le fichier.
Pour shop-data-jpa lui-même, cet ensemble de démarrage se lit comme une mini-histoire du projet :
| Nom du fichier | Ce que signifie la version | Ce que signifie la description |
|---|---|---|
| V1__init_schema.sql | « première étape » | créer la structure initiale |
| V2__init_indexes.sql | « deuxième étape » | ajouter les index pour les premières lectures |
| V3__seed_reference_data.sql | « troisième étape » | ajouter les données de référence minimales |
Le double soulignement __ n'est pas un hasard ni un « easter egg ». Il sépare la partie « technique » (version) de la partie « humaine » (description). Chez certains étudiants, la main est tentée d'écrire un seul soulignement ou un trait d'union, mais il est préférable d'adopter immédiatement le format comme standard : ainsi il y a moins de chances de faire « presque correct, mais pas considéré ».
Psychologiquement, cela ressemble à la règle de nommage des tests : personne n'interdit de nommer un test test1, mais ensuite vous pleurerez dans votre oreiller. Flyway est un outil qui aime la discipline, et cette discipline n'est pas pour la bureaucratie, mais pour la prévisibilité.
3. Ordre des migrations : comment les versions sont comparées
Quand vous voyez V1, V2, V3, tout semble évident. Mais dès que le projet a une « dixième » migration, une erreur typique de débutant apparaît soudain : si on compare les versions comme des chaînes, alors "10" sera « inférieur » à "2" (parce que '1' < '2'). Heureusement, Flyway n'est pas aussi naïf : il sait comparer les versions comme une séquence numérique.
En pratique, cela signifie que V2 sera appliquée avant V10, même si vous « visuellement » triez les fichiers dans l'explorateur et voyez un ordre étrange. Flyway trie selon les règles de version, et non selon l'ordre lexicographique des chaînes.
Dans un projet d'apprentissage, l'option la plus conviviale est d'utiliser simplement des entiers : V1, V2, V3, … Et ce n'est pas parce que « c'est toujours comme ça dans la vie », mais parce que c'est plus simple de garder l'historique linéaire et compréhensible. Les schémas de versionnage complexes ont un sens quand il y a des branches, des releases et plusieurs équipes, mais nous construisons actuellement le muscle « migrations comme historique », pas « migrations comme sport de haut niveau ».
Pour ressentir comment Flyway conçoit l'ordre, imaginez ce scénario : dans le projet il y a V1, V2, V3. Vous ajoutez V4. Que fera Flyway ? Il ne rejouera pas V1–V3 « au cas où ». Il appliquera seulement V4. Et voici pourquoi : il ne regarde pas votre humeur ni la phase de la lune, mais l'historique dans la base, dont nous allons maintenant parler.
4. Table d'historique : flyway_schema_history
Si jusqu'à présent Flyway vous semblait être « juste un exécuteur de fichiers SQL », voici le tournant clé : Flyway se souvient de ce qu'il a fait et stocke la mémoire dans la base de données elle-même. Pour cela, il crée une table de service qui s'appelle par défaut flyway_schema_history.
Cette table est littéralement un « journal » : elle enregistre quelles migrations ont été appliquées, quand, par qui, combien de temps elles ont pris et si elles se sont terminées avec succès. Dans le monde des bases de données, c'est presque comme une « table de journal des modifications », mais très concrète et utile : elle permet à n'importe quel démarrage de comprendre l'état actuel du schéma.
Pour voir l'historique de vos propres yeux (c'est utile de le faire au moins une fois manuellement pour cesser de percevoir Flyway comme de la magie), vous pouvez exécuter la requête :
-- Voir quelles migrations ont déjà été appliquées (et dans quel ordre).
select installed_rank, version, description, script, checksum, success
from flyway_schema_history
order by installed_rank; -- ordre d'application des migrations
Si des migrations ont déjà été appliquées, vous verrez un résultat ressemblant à ceci (pseudo-sortie, juste pour comprendre le sens) :
-- Exemple de résultat (pseudo-sortie) : chaque ligne est un fait d'application de migration.
-- installed_rank | version | description | script | success
-- 1 | 1 | init schema | V1__init_schema.sql | true
-- 2 | 2 | init indexes | V2__init_indexes.sql | true
Ici chaque ligne est un fait : « la migration avec cette version a été appliquée ». Ainsi, quand vous ajoutez V3 et redémarrez l'application, Flyway verra que V1 et V2 sont déjà dans l'historique et n'essaiera pas de recréer les tables.
Remarquez particulièrement le champ checksum. C'est une assurance intégrée contre le désir très humain de « légèrement modifier un ancien fichier ». Nous en parlerons un peu plus tard, mais déjà maintenant on peut comprendre la logique générale : si un fichier a déjà été appliqué, il fait partie de l'historique, et Flyway protégera la base d'une réécriture silencieuse du passé.
5. Algorithme de fonctionnement de Flyway
Lorsque nous comprenons le rôle de l'historique, tout le démarrage de Flyway commence à sembler très prévisible. Ce n'est pas un « mystérieux ORM à l'intérieur d'un ORM », mais un algorithme assez direct. Bonus agréable : plus l'outil est prévisible, moins il effraie le développeur débutant.
Le modèle mental du démarrage est approximativement le suivant :
flowchart TD
A[Démarrage de l'application] --> B[Connexion à la BD via DataSource]
B --> C{"Y-a-t-il 'flyway_schema_history' ?"}
C -- non --> D[Créer la table d'historique]
C -- oui --> E[Lire les versions appliquées]
D --> E
E --> F[Scanner db/migration et trouver les fichiers V*]
F --> G[Trier par version]
G --> H[Comparer : projet vs historique]
H --> I{"Y-a-t-il des versions non appliquées ?"}
I -- non --> J[Ne rien faire, le schéma est à jour]
I -- oui --> K[Appliquer la migration suivante]
K --> L[Enregistrer le résultat dans l'historique]
L --> I
Dans cet algorithme, il y a un détail idéologique important : Flyway raisonne en « historique », pas en « état actuel ». Il n'analyse pas quelles tables vous avez déjà et n'essaie pas de calculer intelligemment la différence. Il suit simplement les faits : appliqué ou non.
Par conséquent, si la base « a vécu sa vie » (quelqu'un a ajouté une colonne à la main, quelqu'un a activé ddl-auto, quelqu'un a fait ALTER TABLE dans le terminal), et que la table d'historique n'en sait rien, Flyway ne pourra pas deviner le bon comportement. Pour un débutant, cela semble être un défaut, mais dans le développement en équipe, c'est un énorme avantage : nous supprimons la « télépathie » et la remplaçons par un « historique explicite dans le dépôt ».
6. checksum et interdiction de modifier le passé
Il y a deux catégories d'erreurs en informatique. Les premières – « accidentellement mis une mauvaise espace ». Les secondes – « accidentellement réécrit l'historique ». La deuxième catégorie est généralement plus amusante… mais seulement les cinq premières minutes.
Lorsqu'une migration a déjà été appliquée, une entrée avec son checksum apparaît dans flyway_schema_history. C'est un hachage du contenu du fichier (dans une compréhension simplifiée), qui permet à Flyway de détecter : le fichier a été modifié après avoir été exécuté.
Pourquoi est-ce important ? Parce que si vous modifiez une ancienne migration et la commitez, vous obtenez un projet où « l'historique des migrations dans Git » et « l'historique des migrations dans la base » ne coïncident plus. Sur votre base, la migration est déjà appliquée dans l'ancienne version, sur une nouvelle base elle sera appliquée dans la nouvelle version – et vous obtenez deux mondes différents. Ensuite, vous passerez du temps à déboguer des bugs du type « ça ne se reproduit pas chez moi », mais cette fois au niveau du schéma.
Flyway réagit généralement sévèrement à cela : il dit « non, ce n'est pas autorisé », et le démarrage échoue à l'étape de vérification. Ce n'est pas de la méchanceté. C'est une protection contre l'état « une équipe – plusieurs réalités ».
Oui, parfois on a envie de dire « mais j'ai juste corrigé une faute de frappe ». Mais la règle de discipline est simple : si vous devez modifier quelque chose déjà appliqué – ajoutez une nouvelle version, ne modifiez pas le passé. Nous analyserons en détail la pratique de l'évolution du schéma dans la prochaine leçon, mais dès maintenant il est utile d'accepter l'idée : une migration, après application, cesse d'être un brouillon et devient un document.
7. Mini-scénario sur shop-data-jpa
Jouons mentalement un cycle typique dans notre projet shop-data-jpa, car sans contexte, tout ceci se transforme facilement en « lu et oublié ». Imaginez que vous ayez une base propre et trois migrations V1, V2, V3. Vous démarrez l'application – Flyway les applique et les enregistre dans l'historique.
Quelques jours plus tard, vous réalisez : vous avez besoin d'un nouvel index pour des lectures fréquentes (par exemple, sur sku ou sur orderNumber). Vous ajoutez un nouveau fichier de migration V4__add_order_number_index.sql (l'index lui-même n'est pas important maintenant – c'est le principe qui compte). Et voici le comportement clé : lors du prochain démarrage, Flyway appliquera uniquement V4, car V1–V3 sont déjà dans l'historique.
Pour l'observabilité, vous pouvez revoir l'historique :
-- Version courte pour vérification : quelles versions sont appliquées et ce que c'était.
select version, description, installed_on, success
from flyway_schema_history
order by installed_rank; -- trier dans l'ordre réel d'application
Et vous verrez que de nouvelles lignes sont apparues. Ainsi, la base de données stocke elle-même votre « carte temporelle » : quand exactement l'index a été ajouté, quand les données de référence sont apparues, quand les contraintes ont été modifiées.
C'est cette discipline qui manque tant aux projets à un stade précoce. Quand le schéma est créé via ddl-auto, il vous semble que tout se fait « simplement ». Mais dès que vous voulez reproduire le résultat sur une autre machine, vous découvrez soudain que « simplement » – c'est quand vous êtes seul sur votre ordinateur portable. En équipe, « simplement » signifie presque toujours « via des migrations ».
8. Erreurs typiques des migrations Flyway
Erreur n°1 : les migrations sont nommées n'importe comment et l'ordre doit être deviné.
Souvent, cela ressemble à init.sql, new.sql, fix2.sql. Au moment de l'écriture, on pense gagner du temps, mais une semaine plus tard, on gagne seulement sur son propre sommeil. Flyway aime le format strict V<version>__<description>.sql, car il fournit à la fois l'ordre et explique l'intention. Plus tôt vous vous habituez à des noms lisibles, moins les migrations seront perçues comme une « boîte noire ».
Erreur n°2 : on a fourré la moitié du monde dans un seul fichier, et la migration a cessé d'être une « étape ».
Un débutant écrit parfois une migration de 300 lignes où sont créés simultanément des tables, des index, des contraintes, des données de référence et, « au cas où », quelques ALTER TABLE. Formellement, cela peut s'exécuter, mais ensuite vous ne pourrez pas expliquer correctement ce qui a été modifié exactement et pourquoi. Une migration doit être petite et thématiquement cohérente : c'est un projet d'apprentissage, et la clarté de l'historique est plus importante que « faire d'un seul coup ».
Erreur n°3 : on a modifié un fichier déjà appliqué V1__..., puis on s'est étonné que le démarrage échoue.
C'est là qu'intervient le checksum : Flyway vous protège contre la réécriture du passé. Il est important d'accepter qu'après application, une migration fait partie de l'historique. Si vous devez « corriger » – ajoutez une nouvelle version. Cela semble un peu bureaucratique, mais en pratique, cela économise des heures de débogage et de discussions « de quelle base tu parles ? ».
Erreur n°4 : s'attendre à ce que Flyway « devine » les modifications manuelles de la base.
Si vous avez ajouté une colonne à la main dans la console PostgreSQL hier, et aujourd'hui vous mettez des migrations dans le projet – Flyway n'est pas télépathe. Il n'analyse pas le schéma actuel, il analyse l'historique des migrations. Si l'historique est vide, il essaiera d'appliquer V1 et, très probablement, échouera sur « la table existe déjà ». Ce n'est pas « Flyway fait le difficile », c'est que vous lui avez donné deux réalités : un schéma sans historique. Comment sortir correctement de cet état et comment basculer le projet en mode migration – nous l'étudierons dans les leçons pratiques de la journée.
Erreur n°5 : les migrations essaient d'être « intelligentes » au lieu d'être prévisibles.
On rencontre parfois le désir d'écrire du SQL dans le style « si la table n'existe pas – crée, si elle existe – passe ». Sur une courte distance, cela semble pratique, mais sur la longue distance, vous obtenez des divergences silencieuses entre les environnements. Flyway est justement dans la philosophie inverse : l'historique doit être explicite, identique et reproductible. Si une étape doit être effectuée, elle est effectuée une fois et enregistrée dans la table d'historique, pas « peut-être ».
GO TO FULL VERSION