Migrations : faire évoluer le schéma sans risque
Transformations versionnées, transactionnelles, réversibles.
Objectifs
À la fin de cette leçon, vous saurez :
- expliquer pourquoi synchronize ne suffit pas en production ;
- générer, relire et appliquer une migration ;
- gérer l'échec et le retour arrière.
Le problème que les migrations résolvent
Trois environnements (dev, staging, prod), trois bases qui doivent converger vers le MÊME schéma. synchronize compare entités ↔ base à chaque démarrage : imprévisible et destructeur possible. La migration est un fichier versionné qui décrit UNE transformation explicite :
src/migrations/
├── 1700000001-CreateTasks.ts ← déjà appliquée
├── 1700000100-AddTaskIndex.ts ← déjà appliquée
└── 1700000200-AddUserAvatar.ts ← nouvelle, pas encore appliquée
La table migrations en base retient ce qui a été joué : chaque environnement rejoue exactement ce qui lui manque, dans l'ordre — reproductible et auditable. C'est précisément le mécanisme de Devmind (schema_migrations, fichiers numérotés, baseline réexécutable).
Générer depuis les entités
npx typeorm migration:generate src/migrations/AddUserAvatar -d data-source.ts
L'ORM COMPARE les entités au schéma réel de la base de dev et écrit la transformation :
export class AddUserAvatar1700000200 implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"ALTER TABLE \"user\" ADD avatar text"
);
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"ALTER TABLE \"user\" DROP COLUMN avatar"
);
}
}
Deux méthodes obligatoires : up applique, down annule. Relire le fichier AVANT application est un acte professionnel : vous validez du SQL, pas de la magie — et vous complétez ce que la génération automatique rate (par exemple la sauvegarde préalable des données d'une colonne renommée).
Appliquer
npx typeorm migration:run -d data-source.ts # joue les migrations manquantes
npx typeorm migration:revert -d data-source.ts # annule la DERNIÈRE
Le run est idempotent par construction : rejoué, il ne fait rien de plus. Chaque migration s'exécute dans une transaction — crash au milieu = rien n'appliqué (chapitre SQL).
Les règles de survie
- Une migration, une intention : jamais « refactoring complet » en un fichier.
- Jamais modifier une migration appliquée : elle a peut-être déjà tourné ailleurs. Nouvelle intention → nouveau fichier.
- Penser compatibilité : déployer le code AVANT que la migration casse l'ancien code encore en marche (ajouter une colonne nullable, remplir, puis rendre NOT NULL — en trois déploiements).
- Tester down aussi : un rollback doit fonctionner le jour où il sert.
Exercice
- Ajoutez le champ
prioriteaux entités ; générez la migration ; relisez-la. - Appliquez, vérifiez
\d taskssous psql, puis revert, vérifiez, re-appliquez. - Simulez deux environnements : appliquez sur deux bases différentes, inspectez la table migrations.
- Pourquoi « renommer titre en libelle » demande-t-il plus qu'un ALTER COLUMN ?
Résumé
- Migration = transformation versionnée, transactionnelle, idempotente.
- generate → RELIRE → run ; revert pour la dernière uniquement.
- Jamais éditer une migration appliquée ; penser compatibilité de déploiement.
Correction disponibleCherchez d’abord par vous-même.Voir la correction
Correction
Réponses détaillées
Question 4. Un renommage casse instantanément tout code encore déployé qui lit titre. Chemin sûr : ajouter libelle (nullable) → déployer le code qui écrit les deux → backfill des données → déployer le code qui ne lit plus titre → supprimer titre dans une dernière migration. C'est la compatibilité progressive : chaque étape fonctionne avec son prédécesseur.