Dans une application de génération de plannings d'équipes, j'ai touché au schéma trois fois de façon incompatible en deux semaines. J'ai créé une table worker_verifications pour stocker le résultat des vérifications que le modèle faisait sur chaque employé. Cinq jours plus tard je l'ai supprimée entièrement et j'ai déplacé sa seule colonne utile, checked_rules, dans worker_week_rules. Deux jours après, j'ai supprimé extra_closed_days de la table weeks, que j'avais ajoutée lors de la refonte précédente.
Aucun de ces changements ne venait d'une nouvelle décision produit. Ils venaient d'une meilleure compréhension du domaine.
Le schéma du jour 1 et celui du jour 30
La table que j'ai supprimée avait huit colonnes : ai_result, worker_hash, context_hash, verified_at, checked_rules et les clés. L'idée était de mettre la vérification en cache : si le hash de l'employé et celui du contexte ne changeaient pas, vous ne rappeliez pas le modèle. Ça paraît raisonnable.
Ce qui s'est passé, c'est que l'unité réelle du domaine n'était pas « une vérification », mais « les règles d'un employé sur une semaine ». Et cette ligne existait déjà. Les vérifications étaient un attribut de cette ligne, pas une entité. La migration qui a corrigé ça est courte et se lit toute seule :
ALTER TABLE "worker_week_rules" ADD COLUMN "checked_rules" text DEFAULT '{}' NOT NULL;
UPDATE "worker_week_rules" wwr
SET "checked_rules" = wv."checked_rules"
FROM "worker_verifications" wv
WHERE wwr."week_id" = wv."week_id"
AND wwr."worker_id" = wv."worker_id"
AND wv."checked_rules" != '{}';
DROP TABLE "worker_verifications";
ALTER TABLE "worker_week_rules" DROP COLUMN "created_at";
Quatre instructions. Le coût n'était pas le SQL.
Quels changements signalent que vous aviez vu juste, et lesquels non
Il y a deux types de changements de schéma, et ils n'ont rien à voir.
- Ajouter une colonne.
checked_week_rulesdansweeks,calendar_rulesdanscalendars. C'est unALTER TABLE ADD COLUMN ... DEFAULTavec une valeur par défaut, ça se déploie sans rien arrêter, l'ancien code continue de fonctionner. C'est le système qui grandit. Bon signe. - Déplacer l'entité. Supprimer une table, renommer une colonne, changer de quoi dépend une donnée. Cela signifie que vous aviez mal modélisé le domaine la première fois, pas que le domaine a changé.
Dans ce même projet, j'ai fait passer preferred_day_off integer à preferred_days_off text, parce qu'il s'est avéré qu'un employé pouvait préférer deux jours. Ça aussi, c'est du bon : c'est une information nouvelle sur le monde, pas une erreur de modélisation. La migration tient en trois lignes et conserve les données avec un CAST.
Celui du mauvais type se repère parce que le diff n'est pas dans le dossier des migrations.
Le vrai coût est dans l'UI, pas dans le SQL
Dans un outil de documentation assisté par IA, j'avais commencé par appeler documents ce que l'utilisateur téléversait. Un mois plus tard, le bon concept était contexts : des morceaux de texte que l'agent pouvait lire, citer et réécrire, et qui pouvaient venir d'une URL, pas seulement d'un fichier. Le nom était faux et entraînait tout le modèle avec lui.
Le commit qui l'a changé ne touche pas au schéma. Il touche à la page de chat, à l'input, et supprime context-url-dialog, context-view et context-viewer en entier : 1 127 lignes supprimées d'un coup. Un RENAME COLUMN de deux secondes et une journée entière à toucher des composants.
Et un cas encore plus rapide à exécuter et plus cher à propager : dans ce même projet, j'ai renommé starCount en favoriteCount. Le SQL, ce sont cinq lignes dans deux migrations. Le diff touche au schéma, à deux fichiers d'actions de base de données, et à l'UI qui l'affichait.
Voilà la règle que j'en ai tirée : le coût de changer un nom de colonne est proportionnel au nombre de composants qui l'écrivent littéralement. Pas au coût de la migration.
Comment j'écris aujourd'hui le premier schéma
Quatre choses que je fais différemment depuis, et aucune n'est « mieux concevoir ».
- Je nomme par ce que c'est, pas par d'où ça vient.
documentsdésignait l'origine (un fichier).contextsdésigne le rôle que joue la donnée dans le système. Le second nom tient quand l'origine change. - Je ne crée pas une table pour un attribut. Si la nouvelle entité a toujours exactement une ligne pour chaque ligne d'une autre table, ce n'est pas une entité. C'est un groupe de colonnes.
worker_verificationsétait exactement ça et il m'a fallu cinq jours pour le voir. - Un type par colonne dès le départ. Lors de la grande refonte, j'ai converti en même temps
created_atdetextentimestamptz,week_startdetextendateet le JSON stocké en texte enjsonb. J'aurais pu tout mettre correctement le premier jour au même prix. Le corriger après, c'est unALTER COLUMN ... USINGsur chaque table et la relecture de tout le code qui comparait des dates comme des chaînes. - L'UI ne répète pas le nom de la colonne. Un type dérivé du schéma et deux ou trois fonctions d'accès. Alors un
RENAME COLUMNest attrapé par le compilateur au lieu d'ungrep.
Quand il vaut vraiment la peine de s'arrêter
Dans un éditeur de plans de bureaux, je suis passé de composants DualDesk et QuadDesk séparés à un unique MultiDesk avec un nombre de places, puis je l'ai refactorisé encore une fois pour ajouter des places désactivées. Deux bureaux de deux et de quatre places ressemblent à deux cas ; en réalité c'était le même avec un paramètre. Ça, on le voit dès le premier jour si on s'arrête dix minutes pour se demander si le 2 et le 4 sont des constantes ou des données.
La règle que j'utilise : si la différence entre deux modèles est un nombre, c'est une donnée. Là, il vaut la peine de réfléchir avant.
Pour le reste, j'écris le schéma en sachant qu'il est faux. Ce que j'optimise, ce n'est pas d'avoir raison, c'est que le corriger la semaine prochaine coûte une migration courte et pas une journée à toucher des composants.
