database-migration

Par wshobson · agents

Exécute des migrations de bases de données sur différents ORM et plateformes avec des stratégies zéro interruption, la transformation de données et des procédures de rollback. À utiliser lors de migrations de bases de données, de modifications de schémas, de transformations de données ou de l'implémentation de stratégies de déploiement zéro interruption.

npx skills add https://github.com/wshobson/agents --skill database-migration

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
        },
      });
    }
  },
};

Skills similaires