マイグレーションの基本原則
- 後方互換性を保つ - 新旧アプリが共存できる
- 段階的に実行 - 大きな変更を小さく分割
- ロールバック可能 - 常に戻れる状態を維持
- テスト必須 - 本番相当のデータで検証
マイグレーションツール
Prisma
// schema.prisma
model User {
id String @id @default(cuid())
email String @unique
name String?
createdAt DateTime @default(now())
}
// マイグレーション作成
// npx prisma migrate dev --name add_user_table
// 生成されるSQL
// prisma/migrations/20250110_add_user_table/migration.sql
Drizzle
// drizzle.config.ts
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
});
// スキーマ定義
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: varchar('email', { length: 255 }).unique().notNull(),
name: varchar('name', { length: 100 }),
});
// npx drizzle-kit generate
// npx drizzle-kit migrate
危険な操作と安全な代替手法
カラム名の変更
-- 危険: 直接リネーム(ダウンタイム発生)
ALTER TABLE users RENAME COLUMN name TO full_name;
-- 安全: 3段階で実行
-- Step 1: 新カラム追加
ALTER TABLE users ADD COLUMN full_name VARCHAR(100);
-- Step 2: データコピー + トリガー設定
UPDATE users SET full_name = name WHERE full_name IS NULL;
CREATE OR REPLACE FUNCTION sync_name_columns()
RETURNS TRIGGER AS $$
BEGIN
NEW.full_name := NEW.name;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER sync_names
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION sync_name_columns();
-- Step 3: アプリを新カラムに移行後、旧カラム削除
DROP TRIGGER sync_names ON users;
ALTER TABLE users DROP COLUMN name;
NOT NULL制約の追加
-- 危険: 直接追加(大量データでロック)
ALTER TABLE users ALTER COLUMN name SET NOT NULL;
-- 安全: 段階的に実行
-- Step 1: 既存NULLをデフォルト値で埋める
UPDATE users SET name = 'Unknown' WHERE name IS NULL;
-- Step 2: CHECK制約を追加(NOT VALIDで高速)
ALTER TABLE users ADD CONSTRAINT name_not_null
CHECK (name IS NOT NULL) NOT VALID;
-- Step 3: 制約を検証(バックグラウンド)
ALTER TABLE users VALIDATE CONSTRAINT name_not_null;
-- Step 4: NOT NULLに変換
ALTER TABLE users ALTER COLUMN name SET NOT NULL;
ALTER TABLE users DROP CONSTRAINT name_not_null;
大規模データ移行
// バッチ処理で移行
async function migrateInBatches(batchSize = 1000) {
let lastId = '';
let processedCount = 0;
while (true) {
const rows = await db.query(`
SELECT * FROM old_table
WHERE id > $1
ORDER BY id
LIMIT $2
`, [lastId, batchSize]);
if (rows.length === 0) break;
await db.query(`
INSERT INTO new_table (id, data, migrated_at)
SELECT id, transform_data(data), NOW()
FROM unnest($1::uuid[], $2::jsonb[])
AS t(id, data)
ON CONFLICT (id) DO NOTHING
`, [rows.map(r => r.id), rows.map(r => r.data)]);
lastId = rows[rows.length - 1].id;
processedCount += rows.length;
console.log(`Migrated ${processedCount} rows`);
// レート制限
await sleep(100);
}
}
ロールバック戦略
// マイグレーションファイル構造
interface Migration {
version: string;
up: () => Promise<void>;
down: () => Promise<void>; // ロールバック必須
}
const migration: Migration = {
version: '20250110_add_status_column',
async up() {
await db.query(`
ALTER TABLE orders ADD COLUMN status VARCHAR(20) DEFAULT 'pending'
`);
},
async down() {
await db.query(`
ALTER TABLE orders DROP COLUMN status
`);
},
};
// ロールバック実行
async function rollback(targetVersion: string) {
const currentVersion = await getCurrentVersion();
const migrationsToRollback = getMigrationsBetween(currentVersion, targetVersion);
for (const migration of migrationsToRollback.reverse()) {
console.log(`Rolling back: ${migration.version}`);
await migration.down();
await updateVersion(migration.version);
}
}
Blue/Greenマイグレーション
sequenceDiagram
participant Old as 旧DB
participant New as 新DB
participant App as アプリ
Note over Old,New: Phase 1: 準備
New->>New: スキーマ作成
Note over Old,New: Phase 2: 同期
Old->>New: 初期データコピー
Old->>New: CDC (変更データキャプチャ)
Note over Old,New: Phase 3: 切り替え
App->>App: 書き込み停止
Old->>New: 最終同期
App->>New: 接続先変更
App->>App: 書き込み再開
Note over Old,New: Phase 4: 検証後、旧DB廃止
CI/CDでのマイグレーション
# GitHub Actions
name: Database Migration
on:
push:
branches: [main]
paths:
- 'prisma/migrations/**'
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run migrations on staging
run: npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
- name: Run integration tests
run: npm run test:integration
- name: Run migrations on production
if: success()
run: npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
関連記事
- データモデリング - スキーマ設計
- ブルーグリーンデプロイメント - デプロイ戦略
- データベースレプリケーション - 高可用性
- Prisma ORMガイド - Prisma実践
まとめ
安全なマイグレーションは、後方互換性、段階的実行、ロールバック可能性が鍵です。大規模変更はバッチ処理で、本番適用前には必ず本番相当データでテストしましょう。
← Back to list