database-migration について
Sequelize、TypeORM、Prismaを使ったスキーマやデータの変更を扱う開発者向けのデータベース移行スキルです。ロールバック設計やダウンタイムを抑えるデプロイ手法も扱います。
主な機能
- 01Sequelize、TypeORM、Prismaの移行パターン
- 02スキーマとデータの変換
- 03トランザクション・チェックポイント方式のロールバック
- 04ダウンタイムなしの移行戦略
- 05データベース間移行のガイド
活用シーン
- 01ORM間の移行
- 02データベーススキーマのリファクタリング
- 03データベース間のデータ移行
- 04ロールバックと低ダウンタイムリリースの計画
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ ovachiever/ droid-tings --skill database-migrationインストール後、Claude Code で/database-migrationを入力して読み込まれたか確認します。
詳細
データベース移行
Sequelize、TypeORM、Prisma などの ORM をまたいだデータベースのスキーマおよびデータのマイグレーションを習得し、ロールバック戦略とゼロダウンタイムデプロイメントに対応します。
このスキルを使用する場面
- 異なる ORM 間の移行
- スキーマ変換の実施
- データベース間でのデータ移行
- ロールバック手順の実装
- ゼロダウンタイムデプロイメント
- データベースのバージョンアップ
- データモデルのリファクタリング
ORM のマイグレーション
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');
}
};
// Run: npx sequelize-cli db:migrate
// Rollback: npx sequelize-cli db:migrate:undo
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');
}
}
// Run: npm run typeorm migration:run
// Rollback: npm run typeorm migration:revert
Prisma のマイグレーション
// schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
createdAt DateTime @default(now())
}
// Generate migration: npx prisma migrate dev --name create_users
// Apply: npx prisma migrate deploy
スキーマ変換
デフォルト値を設定したカラムの追加
// Safe migration: add column with default
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');
}
};
カラム名の変更(ゼロダウンタイム)
// Step 1: Add new column
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('users', 'full_name', {
type: Sequelize.STRING
});
// Copy data from old column
await queryInterface.sequelize.query(
'UPDATE users SET full_name = name'
);
},
down: async (queryInterface) => {
await queryInterface.removeColumn('users', 'full_name');
}
};
// Step 2: Update application to use new column
// Step 3: Remove old column
module.exports = {
up: async (queryInterface) => {
await queryInterface.removeColumn('users', 'name');
},
down: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('users', 'name', {
type: Sequelize.STRING
});
}
};
カラム型の変更
module.exports = {
up: async (queryInterface, Sequelize) => {
// For large tables, use multi-step approach
// 1. Add new column
await queryInterface.addColumn('users', 'age_new', {
type: Sequelize.INTEGER
});
// 2. Copy and transform data
await queryInterface.sequelize.query(`
UPDATE users
SET age_new = CAST(age AS INTEGER)
WHERE age IS NOT NULL
`);
// 3. Drop old column
await queryInterface.removeColumn('users', 'age');
// 4. Rename new column
await queryInterface.renameColumn('users', 'age_new', 'age');
},
down: async (queryInterface, Sequelize) => {
await queryInterface.changeColumn('users', 'age', {
type: Sequelize.STRING
});
}
};
データ変換
複雑なデータ移行
module.exports = {
up: async (queryInterface, Sequelize) => {
// Get all records
const [users] = await queryInterface.sequelize.query(
'SELECT id, address_string FROM users'
);
// Transform each record
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()
}
}
);
}
// Drop old column
await queryInterface.removeColumn('users', 'address_string');
},
down: async (queryInterface, Sequelize) => {
// Reconstruct original column
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');
}
};
ロールバック戦略
トランザクションベースのマイグレーション
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');
}
};
チェックポイントベースのロールバック
module.exports = {
up: async (queryInterface, Sequelize) => {
// Create backup table
await queryInterface.sequelize.query(
'CREATE TABLE users_backup AS SELECT * FROM users'
);
try {
// Perform migration
await queryInterface.addColumn('users', 'new_field', {
type: Sequelize.STRING
});
// Verify 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');
}
// Drop backup
await queryInterface.dropTable('users_backup');
} catch (error) {
// Restore from backup
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;
}
}
};
ゼロダウンタイムマイグレーション
ブルーグリーンデプロイメント戦略
// Phase 1: Make changes backward compatible
module.exports = {
up: async (queryInterface, Sequelize) => {
// Add new column (both old and new code can work)
await queryInterface.addColumn('users', 'email_new', {
type: Sequelize.STRING
});
}
};
// Phase 2: Deploy code that writes to both columns
// Phase 3: Backfill data
module.exports = {
up: async (queryInterface) => {
await queryInterface.sequelize.query(`
UPDATE users
SET email_new = email
WHERE email_new IS NULL
`);
}
};
// Phase 4: Deploy code that reads from new column
// Phase 5: Remove old column
module.exports = {
up: async (queryInterface) => {
await queryInterface.removeColumn('users', 'email');
}
};
データベース間のマイグレーション
PostgreSQL から MySQL への移行
// Handle differences
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 // MySQL JSON type
}
});
} else if (dialectName === 'postgres') {
await queryInterface.createTable('users', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
data: {
type: Sequelize.JSONB // PostgreSQL JSONB type
}
});
}
}
};
リソース
- references/orm-switching.md:ORM の移行ガイド
- references/schema-migration.md:スキーマ変換のパターン
- references/data-transformation.md:データ移行スクリプト
- references/rollback-strategies.md:ロールバック手順
- assets/schema-migration-template.sql:SQL マイグレーションのテンプレート
- assets/data-migration-script.py:データ移行ユーティリティ
- scripts/test-migration.sh:マイグレーションのテストスクリプト
ベストプラクティス
- 必ずロールバックを用意する:すべての
up()にdown()を用意します - マイグレーションをテストする:最初にステージング環境でテストします
- トランザクションを使用する:可能な場合はアトミックなマイグレーションにします
- 事前にバックアップする:マイグレーションの前には必ずバックアップします
- 変更を小さくする:小さな段階的変更に分割します
- 監視する:デプロイ中にエラーがないか確認します
- ドキュメントに記録する:理由と方法を説明します
- 冪等性を確保する:マイグレーションは再実行できるようにします
よくある落とし穴
- ロールバック手順をテストしない
- ダウンタイム対策なしで互換性を損なう変更を行う
- NULL 値の処理を忘れる
- インデックスのパフォーマンスを考慮しない
- 外部キー制約を無視する
- 一度に大量のデータを移行する
コメント
Markdown 対応