データベースマイグレーション戦略 - 安全なスキーマ変更

15分 で読める | 2025.01.10

公式ドキュメント

マイグレーションの基本原則

マイグレーションの4原則:

  1. 後方互換性を保つ - 新旧アプリが共存できる
  2. 段階的に実行 - 大きな変更を小さく分割
  3. ロールバック可能 - 常に戻れる状態を維持
  4. テスト必須 - 本番相当のデータで検証

マイグレーションツール

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 }}

関連記事

まとめ

安全なマイグレーションは、後方互換性、段階的実行、ロールバック可能性が鍵です。大規模変更はバッチ処理で、本番適用前には必ず本番相当データでテストしましょう。

参考リソース

← 一覧に戻る
PR
PR
PR
PR