Migration de Base de Données
Maîtrisez le schéma de base de données et les migrations de données entre ORMs (Sequelize, TypeORM, Prisma), y compris les stratégies de rollback et les déploiements sans temps d'arrêt.
Quand utiliser cette compétence
- Migrer entre différents ORMs
- Effectuer des transformations de schéma
- Déplacer des données entre bases de données
- Implémenter des procédures de rollback
- Déploiements sans temps d'arrêt
- Mises à niveau de version de base de données
- Refactorisation du modèle de données
Migrations ORM
Migrations Sequelize
// migrations/20231201-create-users.js
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable("users", {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
email: {
type: Sequelize.STRING,
unique: true,
allowNull: false,
},
createdAt: Sequelize.DATE,
updatedAt: Sequelize.DATE,
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable("users");
},
};
// Exécuter : npx sequelize-cli db:migrate
// Rollback : npx sequelize-cli db:migrate:undo
Migrations TypeORM
// migrations/1701234567-CreateUsers.ts
import { MigrationInterface, QueryRunner, Table } from "typeorm";
export class CreateUsers1701234567 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: "users",
columns: [
{
name: "id",
type: "int",
isPrimary: true,
isGenerated: true,
generationStrategy: "increment",
},
{
name: "email",
type: "varchar",
isUnique: true,
},
{
name: "created_at",
type: "timestamp",
default: "CURRENT_TIMESTAMP",
},
],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable("users");
}
}
// Exécuter : npm run typeorm migration:run
// Rollback : npm run typeorm migration:revert
Migrations Prisma
// schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
createdAt DateTime @default(now())
}
// Générer migration : npx prisma migrate dev --name create_users
// Appliquer : npx prisma migrate deploy
Transformations de schéma
Ajouter des colonnes avec valeurs par défaut
// Migration sûre : ajouter une colonne avec valeur par défaut
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn("users", "status", {
type: Sequelize.STRING,
defaultValue: "active",
allowNull: false,
});
},
down: async (queryInterface) => {
await queryInterface.removeColumn("users", "status");
},
};
Renommer des colonnes (sans temps d'arrêt)
// Étape 1 : Ajouter une nouvelle colonne
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn("users", "full_name", {
type: Sequelize.STRING,
});
// Copier les données de l'ancienne colonne
await queryInterface.sequelize.query("UPDATE users SET full_name = name");
},
down: async (queryInterface) => {
await queryInterface.removeColumn("users", "full_name");
},
};
// Étape 2 : Mettre à jour l'application pour utiliser la nouvelle colonne
// Étape 3 : Supprimer l'ancienne colonne
module.exports = {
up: async (queryInterface) => {
await queryInterface.removeColumn("users", "name");
},
down: async (queryInterface, Sequelize) => {
await queryInterface.addColumn("users", "name", {
type: Sequelize.STRING,
});
},
};
Modifier les types de colonne
module.exports = {
up: async (queryInterface, Sequelize) => {
// Pour les grandes tables, utiliser une approche multi-étapes
// 1. Ajouter une nouvelle colonne
await queryInterface.addColumn("users", "age_new", {
type: Sequelize.INTEGER,
});
// 2. Copier et transformer les données
await queryInterface.sequelize.query(`
UPDATE users
SET age_new = CAST(age AS INTEGER)
WHERE age IS NOT NULL
`);
// 3. Supprimer l'ancienne colonne
await queryInterface.removeColumn("users", "age");
// 4. Renommer la nouvelle colonne
await queryInterface.renameColumn("users", "age_new", "age");
},
down: async (queryInterface, Sequelize) => {
await queryInterface.changeColumn("users", "age", {
type: Sequelize.STRING,
});
},
};
Transformations de données
Migration de données complexe
module.exports = {
up: async (queryInterface, Sequelize) => {
// Récupérer tous les enregistrements
const [users] = await queryInterface.sequelize.query(
"SELECT id, address_string FROM users",
);
// Transformer chaque enregistrement
for (const user of users) {
const addressParts = user.address_string.split(",");
await queryInterface.sequelize.query(
`UPDATE users
SET street = :street,
city = :city,
state = :state
WHERE id = :id`,
{
replacements: {
id: user.id,
street: addressParts[0]?.trim(),
city: addressParts[1]?.trim(),
state: addressParts[2]?.trim(),
},
},
);
}
// Supprimer l'ancienne colonne
await queryInterface.removeColumn("users", "address_string");
},
down: async (queryInterface, Sequelize) => {
// Reconstruire la colonne d'origine
await queryInterface.addColumn("users", "address_string", {
type: Sequelize.STRING,
});
await queryInterface.sequelize.query(`
UPDATE users
SET address_string = CONCAT(street, ', ', city, ', ', state)
`);
await queryInterface.removeColumn("users", "street");
await queryInterface.removeColumn("users", "city");
await queryInterface.removeColumn("users", "state");
},
};
Stratégies de rollback
Migrations basées sur les transactions
module.exports = {
up: async (queryInterface, Sequelize) => {
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.addColumn(
"users",
"verified",
{ type: Sequelize.BOOLEAN, defaultValue: false },
{ transaction },
);
await queryInterface.sequelize.query(
"UPDATE users SET verified = true WHERE email_verified_at IS NOT NULL",
{ transaction },
);
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
},
down: async (queryInterface) => {
await queryInterface.removeColumn("users", "verified");
},
};
Rollback basé sur des points de contrôle
module.exports = {
up: async (queryInterface, Sequelize) => {
// Créer une table de sauvegarde
await queryInterface.sequelize.query(
"CREATE TABLE users_backup AS SELECT * FROM users",
);
try {
// Effectuer la migration
await queryInterface.addColumn("users", "new_field", {
type: Sequelize.STRING,
});
// Vérifier la migration
const [result] = await queryInterface.sequelize.query(
"SELECT COUNT(*) as count FROM users WHERE new_field IS NULL",
);
if (result[0].count > 0) {
throw new Error("Migration verification failed");
}
// Supprimer la sauvegarde
await queryInterface.dropTable("users_backup");
} catch (error) {
// Restaurer depuis la sauvegarde
await queryInterface.sequelize.query("DROP TABLE users");
await queryInterface.sequelize.query(
"CREATE TABLE users AS SELECT * FROM users_backup",
);
await queryInterface.dropTable("users_backup");
throw error;
}
},
};
Migrations sans temps d'arrêt
Stratégie de déploiement Blue-Green
// Phase 1 : Rendre les modifications rétro-compatibles
module.exports = {
up: async (queryInterface, Sequelize) => {
// Ajouter une nouvelle colonne (l'ancien et le nouveau code peuvent fonctionner)
await queryInterface.addColumn("users", "email_new", {
type: Sequelize.STRING,
});
},
};
// Phase 2 : Déployer le code qui écrit dans les deux colonnes
// Phase 3 : Remplir les données
module.exports = {
up: async (queryInterface) => {
await queryInterface.sequelize.query(`
UPDATE users
SET email_new = email
WHERE email_new IS NULL
`);
},
};
// Phase 4 : Déployer le code qui lit depuis la nouvelle colonne
// Phase 5 : Supprimer l'ancienne colonne
module.exports = {
up: async (queryInterface) => {
await queryInterface.removeColumn("users", "email");
},
};
Migrations entre bases de données
PostgreSQL vers MySQL
// Gérer les différences
module.exports = {
up: async (queryInterface, Sequelize) => {
const dialectName = queryInterface.sequelize.getDialect();
if (dialectName === "mysql") {
await queryInterface.createTable("users", {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
data: {
type: Sequelize.JSON, // Type JSON MySQL
},
});
} else if (dialectName === "postgres") {
await queryInterface.createTable("users", {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
data: {
type: Sequelize.JSONB, // Type JSONB PostgreSQL
},
});
}
},
};