Skip to content
Published on

Flyway & データベースマイグレーション完全ガイド2025:スキーマバージョン管理、ORM統合、ゼロダウンタイムデプロイ

Authors

1. なぜデータベースマイグレーションが必要(ひつよう)なのか

「ローカルでは動(うご)くんだけど」問題(もんだい)

コードはGitで完璧(かんぺき)にバージョン管理(かんり)しているのに、データベーススキーマは手動(しゅどう)でALTER TABLEを実行(じっこう)しているなら、深刻(しんこく)な問題(もんだい)があります。

開発者A:「usersテーブルにphoneカラムを追加しましたよ」
開発者B:「え?ローカルにはないんですけど?」
開発者A:「Slackで共有しましたよね...開発者C:「ステージングは?プロダクションは?」
3時間後に障害発生)

このシナリオは実際(じっさい)に多(おお)くのチームで繰(く)り返(かえ)されています。コードとスキーマが同期(どうき)されなければ、デプロイ時(じ)に障害(しょうがい)が発生(はっせい)し、ロールバックも困難(こんなん)になります。

手動(しゅどう)ALTER TABLEの恐怖(きょうふ)

-- 金曜日18時、プロダクションで直接実行...
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
ALTER TABLE orders MODIFY COLUMN status ENUM('pending','processing','shipped','delivered','cancelled');

-- うっかりWHEREなしで...
UPDATE users SET role = 'admin';
-- Ctrl+C... もう遅い

手動管理(かんり)の問題点(もんだいてん)は以下(いか)の通(とお)りです。

  • 誰(だれ)が、いつ、何(なに)を変更(へんこう)したか追跡(ついせき)不可能(ふかのう)
  • 環境(かんきょう)(ローカル/ステージング/プロダクション)ごとにスキーマが異(こと)なる
  • ロールバック手順(てじゅん)がない
  • コードレビューなしで直接(ちょくせつ)実行(じっこう)
  • 実行順序(じゅんじょ)の保証(ほしょう)がない

データベースバージョン管理(かんり):Gitと同(おな)じ原則(げんそく)

コードにGitを使(つか)うのと同様(どうよう)に、DBスキーマにもバージョン管理(かんり)が必要(ひつよう)です。

Gitでコード管理              DBマイグレーションでスキーマ管理
------------------          ---------------------------
commit history       →      migration history
branch/merge         →      migration branching
code review (PR)     →      migration review
rollback (revert)    →      undo migration
CI/CD pipeline       →      migration in pipeline

核心原則(かくしんげんそく)は以下(いか)の通(とお)りです。

  1. すべてのスキーマ変更はマイグレーションファイルで - 直接SQL実行禁止
  2. マイグレーションはコードと一緒にバージョン管理 - Gitリポジトリに含める
  3. 自動化された実行 - CI/CDパイプラインで実行
  4. 冪等性(べきとうせい)(Idempotency) - 同じマイグレーションを複数回実行しても同じ結果
  5. 順序保証 - マイグレーションは決められた順序で実行

2. マイグレーションツール比較(ひかく)

主要(しゅよう)ツール比較表(ひかくひょう)

ツールアプローチ言語/エコシステム強み弱み
FlywayバージョンベースSQLJava/CLIシンプル、SQLネイティブ有料機能が多い
LiquibaseチェンジセットXML/YAML/SQLJava/CLIロールバック、diff設定が複雑
Prisma Migrate宣言的スキーマTypeScript型安全、Shadow DBPrisma依存
TypeORMコードベースマイグレーションTypeScriptエンティティ自動生成TypeORM依存
AlembicバージョンベースPythonPythonSQLAlchemy統合Python専用
Django Migrations自動検出PythonORM完全統合Django専用
Knex.jsコードベースJavaScript軽量機能が限定的
golang-migrateバージョンベースSQLGo/CLI軽量、マルチDB機能が最小限

選択基準(せんたくきじゅん)

プロジェクトの技術スタックは?
├── Java/SpringFlywayまたはLiquibase
│   ├── シンプルさ重視 → Flyway
│   └── ロールバック/diff必要 → Liquibase
├── TypeScript/Node.js
│   ├── Prisma使用Prisma Migrate
│   ├── TypeORM使用TypeORM Migration
│   └── 軽量 → Knex.js
├── Python
│   ├── DjangoDjango Migrations
│   └── Flask/FastAPIAlembic
├── Go → golang-migrate
└── 言語非依存、SQL直接 → Flyway CLI

3. Flyway徹底分析(てっていぶんせき)

3.1 Flywayの紹介(しょうかい)

Flywayは最(もっと)も広(ひろ)く使(つか)われているデータベースマイグレーションツールです。「シンプルさ」を核心哲学(かくしんてつがく)とし、SQLファイルにバージョン番号(ばんごう)を付(つ)けて順番(じゅんばん)に実行(じっこう)する直感的(ちょっかんてき)な方式(ほうしき)です。

flyway_migrations/
├── V1__Create_users_table.sql
├── V2__Create_orders_table.sql
├── V3__Add_email_to_users.sql
├── V4__Create_products_table.sql
├── R__Create_views.sql          # Repeatable
└── U3__Add_email_to_users.sql   # Undo (Teams)

3.2 インストール方法(ほうほう)

CLIインストール(macOS)

# Homebrew
brew install flyway

# バージョン確認
flyway --version
# Flyway Community Edition 10.x.x

Docker

docker run --rm \
  -v $(pwd)/sql:/flyway/sql \
  -v $(pwd)/conf:/flyway/conf \
  flyway/flyway:10-alpine \
  migrate

Mavenプラグイン

<plugin>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-maven-plugin</artifactId>
    <version>10.8.1</version>
    <configuration>
        <url>jdbc:postgresql://localhost:5432/mydb</url>
        <user>postgres</user>
        <password>secret</password>
        <locations>
            <location>filesystem:src/main/resources/db/migration</location>
        </locations>
    </configuration>
</plugin>

Gradleプラグイン

plugins {
    id 'org.flywaydb.flyway' version '10.8.1'
}

flyway {
    url = 'jdbc:postgresql://localhost:5432/mydb'
    user = 'postgres'
    password = 'secret'
    locations = ['filesystem:src/main/resources/db/migration']
}

3.3 ネーミングコンベンション

Flywayのマイグレーションファイル命名規則(めいめいきそく)は以下(いか)の通(とお)りです。

V{version}__{description}.sql       # Versioned Migration
R__{description}.sql                # Repeatable Migration
U{version}__{description}.sql       # Undo Migration (Teams)

キーポイントは以下(いか)の通(とお)りです。

  • プレフィックス:V(Versioned)、R(Repeatable)、U(Undo)
  • バージョン:数字(1, 2, 3)または日付(20250325)またはタイムスタンプ
  • セパレータ:アンダースコア2つ(__
  • 説明:英語、アンダースコアで区切り
-- V1__Create_users_table.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(255) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
-- V2__Create_orders_table.sql
CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL REFERENCES users(id),
    total_amount DECIMAL(10,2) NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'pending',
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
-- V3__Add_phone_to_users.sql
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
ALTER TABLE users ADD COLUMN profile_image_url TEXT;
-- R__Create_reporting_views.sql(ファイル変更のたびに再実行)
CREATE OR REPLACE VIEW v_user_order_summary AS
SELECT
    u.id AS user_id,
    u.username,
    u.email,
    COUNT(o.id) AS order_count,
    COALESCE(SUM(o.total_amount), 0) AS total_spent
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.username, u.email;

3.4 主要(しゅよう)コマンド

# マイグレーション実行
flyway migrate

# 現在の状態確認
flyway info

# マイグレーション検証
flyway validate

# データベース初期化(注意:全オブジェクト削除)
flyway clean

# 既存DBにFlyway適用(ベースライン)
flyway baseline

# 失敗したマイグレーションの修復
flyway repair

info出力例(しゅつりょくれい)

+-----------+---------+---------------------+------+---------------------+---------+----------+
| Category  | Version | Description         | Type | Installed On        | State   | Undoable |
+-----------+---------+---------------------+------+---------------------+---------+----------+
| Versioned | 1       | Create users table  | SQL  | 2025-03-20 10:15:30 | Success | No       |
| Versioned | 2       | Create orders table | SQL  | 2025-03-20 10:15:31 | Success | No       |
| Versioned | 3       | Add phone to users  | SQL  |                     | Pending | No       |
+-----------+---------+---------------------+------+---------------------+---------+----------+

3.5 設定(せってい)ファイル

flyway.conf(伝統的(でんとうてき)な形式(けいしき))

# データベース接続
flyway.url=jdbc:postgresql://localhost:5432/mydb
flyway.user=postgres
flyway.password=secret

# マイグレーションの場所
flyway.locations=filesystem:./sql,classpath:db/migration

# スキーマ
flyway.schemas=public,app
flyway.defaultSchema=app

# 履歴テーブル
flyway.table=flyway_schema_history

# エンコーディング
flyway.encoding=UTF-8

# プレースホルダー
flyway.placeholders.env=production
flyway.placeholders.schema_name=app

# バリデーション
flyway.validateOnMigrate=true
flyway.validateMigrationNaming=true

# その他
flyway.outOfOrder=false
flyway.baselineOnMigrate=false
flyway.cleanDisabled=true

flyway.toml(Flyway v10の新形式(しんけいしき))

[environments.default]
url = "jdbc:postgresql://localhost:5432/mydb"
user = "postgres"
password = "secret"

[flyway]
locations = ["filesystem:./sql"]
defaultSchema = "app"
table = "flyway_schema_history"
validateOnMigrate = true
cleanDisabled = true

[flyway.placeholders]
env = "production"
schema_name = "app"

3.6 プレースホルダーと環境別(かんきょうべつ)設定(せってい)

-- V5__Create_audit_table.sql
CREATE TABLE ${schema_name}.audit_log (
    id BIGSERIAL PRIMARY KEY,
    table_name VARCHAR(100) NOT NULL,
    operation VARCHAR(10) NOT NULL,
    old_data JSONB,
    new_data JSONB,
    changed_by VARCHAR(100),
    changed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

COMMENT ON TABLE ${schema_name}.audit_log IS 'Audit log for ${env} environment';

環境別(かんきょうべつ)設定(せってい)ファイル:

# flyway-dev.conf
flyway.url=jdbc:postgresql://localhost:5432/mydb_dev
flyway.placeholders.env=development

# flyway-staging.conf
flyway.url=jdbc:postgresql://staging-db:5432/mydb
flyway.placeholders.env=staging

# flyway-prod.conf
flyway.url=jdbc:postgresql://prod-db:5432/mydb
flyway.placeholders.env=production
flyway.cleanDisabled=true
# 環境ごとに実行
flyway -configFiles=flyway-dev.conf migrate
flyway -configFiles=flyway-staging.conf migrate
flyway -configFiles=flyway-prod.conf migrate

3.7 コールバック

Flywayはマイグレーション操作(そうさ)の前後(ぜんご)に実行(じっこう)されるコールバックをサポートしています。

コールバックファイル名:
beforeMigrate.sql      - マイグレーション前
beforeEachMigrate.sql  - 各マイグレーション前
afterEachMigrate.sql   - 各マイグレーション後
afterMigrate.sql       - マイグレーション完了後
beforeClean.sql        - クリーン前
afterClean.sql         - クリーン完了後
beforeValidate.sql     - バリデーション前
afterValidate.sql      - バリデーション完了後
-- afterMigrate.sql
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_daily_stats;
ANALYZE users;
ANALYZE orders;

INSERT INTO migration_audit_log (action, executed_at)
VALUES ('migration_completed', NOW());

3.8 Javaベースマイグレーション

複雑(ふくざつ)なロジックが必要(ひつよう)な場合(ばあい)、Javaでマイグレーションを記述(きじゅつ)できます。

// V6__Encrypt_user_emails.java
package db.migration;

import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import java.sql.*;

public class V6__Encrypt_user_emails extends BaseJavaMigration {

    @Override
    public void migrate(Context context) throws Exception {
        Connection conn = context.getConnection();

        String selectSql = "SELECT id, email FROM users WHERE encrypted_email IS NULL";
        String updateSql = "UPDATE users SET encrypted_email = ? WHERE id = ?";

        try (Statement select = conn.createStatement();
             PreparedStatement update = conn.prepareStatement(updateSql)) {

            ResultSet rs = select.executeQuery(selectSql);
            int batchSize = 0;

            while (rs.next()) {
                long id = rs.getLong("id");
                String email = rs.getString("email");
                String encrypted = encrypt(email);

                update.setString(1, encrypted);
                update.setLong(2, id);
                update.addBatch();

                if (++batchSize % 1000 == 0) {
                    update.executeBatch();
                }
            }

            if (batchSize % 1000 != 0) {
                update.executeBatch();
            }
        }
    }

    private String encrypt(String value) {
        // 実際の暗号化ロジック
        return "ENC:" + value; // プレースホルダー例
    }
}

3.9 Spring Boot統合(とうごう)

# application.yml
spring:
  flyway:
    enabled: true
    locations: classpath:db/migration
    baseline-on-migrate: true
    baseline-version: '0'
    validate-on-migrate: true
    out-of-order: false
    table: flyway_schema_history
    schemas:
      - public
    placeholders:
      env: ${SPRING_PROFILES_ACTIVE:dev}
    clean-disabled: true
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: postgres
    password: secret
// FlywayConfig.java
@Configuration
public class FlywayConfig {

    @Bean
    public FlywayMigrationStrategy flywayMigrationStrategy() {
        return flyway -> {
            MigrationInfo[] pending = flyway.info().pending();
            if (pending.length > 0) {
                System.out.println("Pending migrations: " + pending.length);
                for (MigrationInfo info : pending) {
                    System.out.println("  - " + info.getVersion()
                        + ": " + info.getDescription());
                }
            }
            flyway.migrate();
        };
    }
}

3.10 完全(かんぜん)なプロジェクト例(れい)(PostgreSQL)

my-app/
├── src/main/resources/
│   └── db/
│       └── migration/
│           ├── V1__Create_schema.sql
│           ├── V2__Create_users.sql
│           ├── V3__Create_products.sql
│           ├── V4__Create_orders.sql
│           ├── V5__Add_indexes.sql
│           ├── V6__Create_audit_triggers.sql
│           └── R__Update_views.sql
├── flyway.toml
├── pom.xml
└── docker-compose.yml
# docker-compose.yml
version: '3.8'
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

  flyway:
    image: flyway/flyway:10-alpine
    depends_on:
      - db
    volumes:
      - ./src/main/resources/db/migration:/flyway/sql
      - ./flyway.toml:/flyway/flyway.toml
    command: migrate
    environment:
      FLYWAY_URL: jdbc:postgresql://db:5432/myapp
      FLYWAY_USER: postgres
      FLYWAY_PASSWORD: secret

volumes:
  pgdata:

4. Liquibase比較(ひかく)

4.1 チェンジログ形式(けいしき)

Liquibaseは4つの形式(けいしき)をサポートしています:XML、YAML、SQL、JSON。

YAML形式(けいしき)(最(もっと)も読(よ)みやすい)

# db/changelog/db.changelog-master.yaml
databaseChangeLog:
  - changeSet:
      id: 1
      author: dev-team
      changes:
        - createTable:
            tableName: users
            columns:
              - column:
                  name: id
                  type: bigint
                  autoIncrement: true
                  constraints:
                    primaryKey: true
                    nullable: false
              - column:
                  name: username
                  type: varchar(50)
                  constraints:
                    nullable: false
                    unique: true
              - column:
                  name: email
                  type: varchar(255)
                  constraints:
                    nullable: false
                    unique: true
              - column:
                  name: created_at
                  type: timestamp with time zone
                  defaultValueComputed: NOW()
      rollback:
        - dropTable:
            tableName: users

  - changeSet:
      id: 2
      author: dev-team
      changes:
        - addColumn:
            tableName: users
            columns:
              - column:
                  name: phone
                  type: varchar(20)
      rollback:
        - dropColumn:
            tableName: users
            columnName: phone

SQL形式(けいしき)

-- changeset dev-team:1
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(255) NOT NULL UNIQUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- rollback DROP TABLE users;

-- changeset dev-team:2
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- rollback ALTER TABLE users DROP COLUMN phone;

4.2 Liquibaseの主要機能(しゅようきのう)

# マイグレーション実行
liquibase update

# 直近N個のチェンジセットをロールバック
liquibase rollbackCount 1

# 特定タグまでロールバック
liquibase rollback v1.0

# 現在のDBとチェンジログの差分
liquibase diff

# 既存DBからチェンジログを生成
liquibase generateChangeLog

# ステータス確認
liquibase status

4.3 Flyway vs Liquibase選択(せんたく)ガイド

Flywayを選ぶ場合:
  -SQLを直接書きたい
  - シンプルなワークフローを好む
  - Spring Bootエコシステム
  - チームがSQLに慣れている

Liquibaseを選ぶ場合:
  - ロールバックサポートが必須
  - マルチデータベースベンダー対応が必要
  - 既存DBからチェンジログをリバースエンジニアリングしたい
  - XML/YAMLの宣言的管理を好む
  - きめ細かいコンテキスト/条件付き実行が必要

5. ORMマイグレーションシステム

5.1 Prisma Migrate(TypeScript/Node.js)

スキーマ定義(ていぎ)

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  phone     String?
  posts     Post[]
  orders    Order[]
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  @@map("users")
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int      @map("author_id")
  createdAt DateTime @default(now()) @map("created_at")

  @@map("posts")
}

model Order {
  id         Int         @id @default(autoincrement())
  user       User        @relation(fields: [userId], references: [id])
  userId     Int         @map("user_id")
  totalAmount Decimal    @map("total_amount") @db.Decimal(10, 2)
  status     OrderStatus @default(PENDING)
  items      OrderItem[]
  createdAt  DateTime    @default(now()) @map("created_at")

  @@map("orders")
}

model OrderItem {
  id       Int   @id @default(autoincrement())
  order    Order @relation(fields: [orderId], references: [id])
  orderId  Int   @map("order_id")
  product  String
  quantity Int
  price    Decimal @db.Decimal(10, 2)

  @@map("order_items")
}

enum OrderStatus {
  PENDING
  PROCESSING
  SHIPPED
  DELIVERED
  CANCELLED
}

マイグレーションコマンド

# 開発用マイグレーション(作成+適用)
npx prisma migrate dev --name create_initial_schema

# プロダクション用マイグレーション(適用のみ)
npx prisma migrate deploy

# データベースリセット
npx prisma migrate reset

# マイグレーション状態確認
npx prisma migrate status

# スキーマとデータベースの差分
npx prisma migrate diff \
  --from-schema-datamodel prisma/schema.prisma \
  --to-schema-datasource prisma/schema.prisma

生成(せいせい)されたマイグレーションファイル

-- prisma/migrations/20250325_create_initial_schema/migration.sql
CREATE TYPE "OrderStatus" AS ENUM ('PENDING', 'PROCESSING', 'SHIPPED', 'DELIVERED', 'CANCELLED');

CREATE TABLE "users" (
    "id" SERIAL NOT NULL,
    "email" TEXT NOT NULL,
    "name" TEXT,
    "phone" TEXT,
    "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updated_at" TIMESTAMP(3) NOT NULL,

    CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);

CREATE TABLE "posts" (
    "id" SERIAL NOT NULL,
    "title" TEXT NOT NULL,
    "content" TEXT,
    "published" BOOLEAN NOT NULL DEFAULT false,
    "author_id" INTEGER NOT NULL,
    "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT "posts_pkey" PRIMARY KEY ("id")
);

CREATE UNIQUE INDEX "users_email_key" ON "users"("email");

ALTER TABLE "posts"
    ADD CONSTRAINT "posts_author_id_fkey"
    FOREIGN KEY ("author_id") REFERENCES "users"("id")
    ON DELETE RESTRICT ON UPDATE CASCADE;

Shadow Databaseの概念(がいねん)

Prisma MigrateはShadow Databaseを使用(しよう)してマイグレーションの正確性(せいかくせい)を検証(けんしょう)します。

1. 一時的なShadow DBを作成
2. 既存の全マイグレーションをShadow DBに適用
3. 新しいマイグレーションSQLを生成
4. 新マイグレーションをShadow DBで検証
5. Shadow DBを削除

5.2 TypeORMマイグレーション

エンティティ定義(ていぎ)

// src/entities/User.ts
import {
  Entity, PrimaryGeneratedColumn, Column,
  CreateDateColumn, UpdateDateColumn, OneToMany
} from 'typeorm';
import { Order } from './Order';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'varchar', length: 50, unique: true })
  username: string;

  @Column({ type: 'varchar', length: 255, unique: true })
  email: string;

  @Column({ type: 'varchar', length: 20, nullable: true })
  phone: string;

  @OneToMany(() => Order, order => order.user)
  orders: Order[];

  @CreateDateColumn({ name: 'created_at' })
  createdAt: Date;

  @UpdateDateColumn({ name: 'updated_at' })
  updatedAt: Date;
}

マイグレーションの生成(せいせい)と実行(じっこう)

# エンティティの変更からマイグレーションを自動生成
npx typeorm migration:generate -d src/data-source.ts src/migrations/AddPhoneToUser

# 空のマイグレーションを作成
npx typeorm migration:create src/migrations/SeedAdminUser

# マイグレーション実行
npx typeorm migration:run -d src/data-source.ts

# 直前のマイグレーションを元に戻す
npx typeorm migration:revert -d src/data-source.ts

自動生成(じどうせいせい)されたマイグレーション例(れい)

// src/migrations/1711234567890-AddPhoneToUser.ts
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddPhoneToUser1711234567890 implements MigrationInterface {
    name = 'AddPhoneToUser1711234567890';

    public async up(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(
            `ALTER TABLE "users" ADD "phone" varchar(20)`
        );
    }

    public async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(
            `ALTER TABLE "users" DROP COLUMN "phone"`
        );
    }
}

5.3 Alembic(Python/SQLAlchemy)

初期(しょき)セットアップ

# Alembicインストール
pip install alembic sqlalchemy psycopg2-binary

# プロジェクト初期化
alembic init alembic
# alembic/env.py(キーセクション)
from myapp.models import Base

target_metadata = Base.metadata

def run_migrations_online():
    connectable = engine_from_config(
        config.get_section(config.config_ini_section),
        prefix="sqlalchemy.",
    )
    with connectable.connect() as connection:
        context.configure(
            connection=connection,
            target_metadata=target_metadata,
            compare_type=True,
            compare_server_default=True,
        )
        with context.begin_transaction():
            context.run_migrations()

マイグレーションの生成(せいせい)と実行(じっこう)

# 自動生成(モデル変更を検出)
alembic revision --autogenerate -m "add phone to users"

# 手動作成
alembic revision -m "seed initial data"

# 最新に更新
alembic upgrade head

# 特定バージョンに更新
alembic upgrade abc123

# 1ステップロールバック
alembic downgrade -1

# 現在の状態確認
alembic current

# マイグレーション履歴確認
alembic history

自動生成(じどうせいせい)されたマイグレーション

# alembic/versions/abc123_add_phone_to_users.py
"""add phone to users"""

from alembic import op
import sqlalchemy as sa

revision = 'abc123'
down_revision = 'prev456'
branch_labels = None
depends_on = None

def upgrade():
    op.add_column('users',
        sa.Column('phone', sa.String(20), nullable=True)
    )
    op.create_index('ix_users_phone', 'users', ['phone'])

def downgrade():
    op.drop_index('ix_users_phone', 'users')
    op.drop_column('users', 'phone')

5.4 Djangoマイグレーション

# models.py
from django.db import models

class User(models.Model):
    username = models.CharField(max_length=50, unique=True)
    email = models.EmailField(unique=True)
    phone = models.CharField(max_length=20, blank=True, null=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'users'
# マイグレーション作成
python manage.py makemigrations

# マイグレーション適用
python manage.py migrate

# マイグレーション状態確認
python manage.py showmigrations

# SQLプレビュー
python manage.py sqlmigrate myapp 0002

# マイグレーション圧縮
python manage.py squashmigrations myapp 0001 0005

データマイグレーション

# migrations/0003_populate_default_roles.py
from django.db import migrations

def create_roles(apps, schema_editor):
    Role = apps.get_model('myapp', 'Role')
    roles = ['admin', 'editor', 'viewer']
    for role_name in roles:
        Role.objects.get_or_create(name=role_name)

def remove_roles(apps, schema_editor):
    Role = apps.get_model('myapp', 'Role')
    Role.objects.filter(name__in=['admin', 'editor', 'viewer']).delete()

class Migration(migrations.Migration):
    dependencies = [
        ('myapp', '0002_create_role_table'),
    ]

    operations = [
        migrations.RunPython(create_roles, remove_roles),
    ]

6. ゼロダウンタイムマイグレーション戦略(せんりゃく)

6.1 Expand-Contractパターン(最(もっと)も重要(じゅうよう))

ゼロダウンタイムスキーマ変更(へんこう)の必須(ひっす)パターンです。変更を3つの安全(あんぜん)なフェーズに分割(ぶんかつ)します。

例(れい):usernameをfirst_name + last_nameに分割(ぶんかつ)

フェーズ1(Expand):新カラム追加、両方に書き込み
フェーズ2(Migrate):データバックフィル、読み取り切替
フェーズ3(Contract):旧カラム削除

フェーズ1:Expand

-- Migration V10: 新カラム追加
ALTER TABLE users ADD COLUMN first_name VARCHAR(50);
ALTER TABLE users ADD COLUMN last_name VARCHAR(50);

-- 双方向同期のトリガー
CREATE OR REPLACE FUNCTION sync_user_names()
RETURNS TRIGGER AS $func$
BEGIN
    IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
        IF NEW.first_name IS NULL AND NEW.username IS NOT NULL THEN
            NEW.first_name := split_part(NEW.username, ' ', 1);
            NEW.last_name := split_part(NEW.username, ' ', 2);
        ELSIF NEW.username IS NULL AND NEW.first_name IS NOT NULL THEN
            NEW.username := NEW.first_name || ' ' || COALESCE(NEW.last_name, '');
        END IF;
    END IF;
    RETURN NEW;
END;
$func$ LANGUAGE plpgsql;

CREATE TRIGGER trg_sync_user_names
    BEFORE INSERT OR UPDATE ON users
    FOR EACH ROW EXECUTE FUNCTION sync_user_names();
この時点でデプロイ:新コードはfirst_name/last_nameに書き込む
旧コードはusernameに書き込む(トリガーが同期)

フェーズ2:Migrate

-- Migration V11: 既存データのバックフィル
UPDATE users
SET first_name = split_part(username, ' ', 1),
    last_name = split_part(username, ' ', 2)
WHERE first_name IS NULL;

-- NOT NULL制約を追加
ALTER TABLE users ALTER COLUMN first_name SET NOT NULL;

フェーズ3:Contract

-- Migration V12: 旧カラム削除(全コードが切り替わった後)
DROP TRIGGER IF EXISTS trg_sync_user_names ON users;
DROP FUNCTION IF EXISTS sync_user_names();
ALTER TABLE users DROP COLUMN username;

6.2 ダウンタイムなしのカラム名変更(めいへんこう)

カラム名を直接(ちょくせつ)変更(へんこう)するとサービス中断(ちゅうだん)が発生(はっせい)します。安全(あんぜん)なアプローチは以下(いか)の通りです。

ステップ1:新カラム + トリガーを追加
ステップ2:データをコピー
ステップ3:コードを新カラムに切り替え
ステップ4:旧カラムを削除

6.3 NOT NULL制約(せいやく)の安全(あんぜん)な追加(ついか)

-- 危険:テーブル全体ロック
ALTER TABLE users ALTER COLUMN email SET NOT NULL;

-- 安全:PostgreSQL 12+のCHECK制約
ALTER TABLE users ADD CONSTRAINT users_email_not_null
    CHECK (email IS NOT NULL) NOT VALID;

-- 別トランザクションで検証(AccessExclusiveLockなし)
ALTER TABLE users VALIDATE CONSTRAINT users_email_not_null;

-- NOT NULL制約に変換
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
ALTER TABLE users DROP CONSTRAINT users_email_not_null;

6.4 大規模(だいきぼ)テーブルのALTER TABLE戦略(せんりゃく)

MySQL:gh-ost(GitHub Online Schema Change)

gh-ost \
  --host=db-primary \
  --database=myapp \
  --table=users \
  --alter="ADD COLUMN phone VARCHAR(20)" \
  --execute \
  --allow-on-master \
  --chunk-size=1000 \
  --max-load=Threads_running=25 \
  --critical-load=Threads_running=100

MySQL:pt-online-schema-change(Percona Toolkit)

pt-online-schema-change \
  --alter "ADD COLUMN phone VARCHAR(20)" \
  --host=db-primary \
  --user=admin \
  --ask-pass \
  --chunk-size=500 \
  --max-lag=1s \
  --execute \
  D=myapp,t=users

PostgreSQL:CREATE INDEX CONCURRENTLY

-- 危険:テーブルロック
CREATE INDEX idx_users_email ON users(email);

-- 安全:並行インデックス作成(ロックなし)
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);

-- 注意:トランザクション内では使用不可
-- Flywayではこのマイグレーションのトランザクションを無効にする
-- FlywayでCONCURRENTLYを使用
-- V15__Add_index_concurrently.sql

-- Flywayトランザクション無効(ファイル先頭のコメント)
-- flyway:executeInTransaction=false

CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email
ON users(email);

6.5 外部(がいぶ)キーの安全(あんぜん)な追加(ついか)

-- 危険:フルテーブルスキャン + ロック
ALTER TABLE orders ADD CONSTRAINT fk_orders_user
    FOREIGN KEY (user_id) REFERENCES users(id);

-- 安全:NOT VALIDで追加してから別途検証
ALTER TABLE orders ADD CONSTRAINT fk_orders_user
    FOREIGN KEY (user_id) REFERENCES users(id)
    NOT VALID;

ALTER TABLE orders VALIDATE CONSTRAINT fk_orders_user;

7. ロールバック戦略(せんりゃく)

7.1 Forward-Only vs ロールバックサポート

Forward-Only(Flywayデフォルト):
  V1V2V3V4
  V4に問題 → V5を作成して修正

ロールバックサポート(Liquibase、Flyway Teams):
  V1V2V3V4
  V4に問題 → V3にロールバック

7.2 Flyway Undoマイグレーション(Teamsエディション)

-- U4__Remove_status_column.sql(V4のUndo)
ALTER TABLE orders DROP COLUMN IF EXISTS status;
# Undo実行
flyway undo

# 特定バージョンまでUndo
flyway undo -target=2

7.3 補償(ほしょう)マイグレーション(Compensating Migration)

無料版(むりょうばん)ユーザーがロールバックを必要(ひつよう)とする場合(ばあい)、補償(ほしょう)マイグレーションを記述(きじゅつ)します。

-- V4__Add_status_to_orders.sql(元のマイグレーション)
ALTER TABLE orders ADD COLUMN status VARCHAR(20) DEFAULT 'pending';

-- V5__Revert_status_from_orders.sql(補償マイグレーション)
ALTER TABLE orders DROP COLUMN IF EXISTS status;

7.4 ロールバックすべきでない場合(ばあい)

ロールバックすべきでない場合:
  1. カラム削除によるデータ損失
     - DROP COLUMNは復旧不可
     - 削除前に必ずバックアップを確認

  2. 非可逆データ変換
     - UPDATEでデータ形式を変更
     - 元データが保存されていない

  3. 大規模データ移行
     - 数百万行を移動
     - ロールバック時間が許容範囲を超過

  4. 外部システム連携
     - 外部コンシューマーがAPI変更を適用済み
     - 外部システムはロールバック不可

8. CI/CD統合(とうごう)

8.1 GitHub Actionsパイプライン

# .github/workflows/db-migration.yml
name: Database Migration

on:
  push:
    branches: [main]
    paths:
      - 'db/migration/**'
  pull_request:
    paths:
      - 'db/migration/**'

jobs:
  validate:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: testdb
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: testpass
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4

      - name: Run Flyway Validate
        run: |
          docker run --rm --network host \
            -v $(pwd)/db/migration:/flyway/sql \
            flyway/flyway:10-alpine \
            -url=jdbc:postgresql://localhost:5432/testdb \
            -user=postgres \
            -password=testpass \
            validate

      - name: Run Flyway Info
        run: |
          docker run --rm --network host \
            -v $(pwd)/db/migration:/flyway/sql \
            flyway/flyway:10-alpine \
            -url=jdbc:postgresql://localhost:5432/testdb \
            -user=postgres \
            -password=testpass \
            info

      - name: Test Migration
        run: |
          docker run --rm --network host \
            -v $(pwd)/db/migration:/flyway/sql \
            flyway/flyway:10-alpine \
            -url=jdbc:postgresql://localhost:5432/testdb \
            -user=postgres \
            -password=testpass \
            migrate

      - name: Verify Schema
        run: |
          PGPASSWORD=testpass psql -h localhost -U postgres -d testdb -c "
            SELECT table_name FROM information_schema.tables
            WHERE table_schema = 'public'
            ORDER BY table_name;
          "

  deploy-staging:
    needs: validate
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to Staging
        run: |
          docker run --rm \
            -v $(pwd)/db/migration:/flyway/sql \
            flyway/flyway:10-alpine \
            -url=$STAGING_DB_URL \
            -user=$STAGING_DB_USER \
            -password=$STAGING_DB_PASSWORD \
            migrate
        env:
          STAGING_DB_URL: ${{ secrets.STAGING_DB_URL }}
          STAGING_DB_USER: ${{ secrets.STAGING_DB_USER }}
          STAGING_DB_PASSWORD: ${{ secrets.STAGING_DB_PASSWORD }}

  deploy-production:
    needs: deploy-staging
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to Production
        run: |
          docker run --rm \
            -v $(pwd)/db/migration:/flyway/sql \
            flyway/flyway:10-alpine \
            -url=$PROD_DB_URL \
            -user=$PROD_DB_USER \
            -password=$PROD_DB_PASSWORD \
            -validateOnMigrate=true \
            migrate
        env:
          PROD_DB_URL: ${{ secrets.PROD_DB_URL }}
          PROD_DB_USER: ${{ secrets.PROD_DB_USER }}
          PROD_DB_PASSWORD: ${{ secrets.PROD_DB_PASSWORD }}

8.2 Blue-GreenデプロイメントとDBマイグレーション

タイムライン →

1. [Blue稼働中] DB v1, App v1
2. [Blue稼働中] DB v1 → v2マイグレーション(後方互換性あり)
3. [Blue稼働中] App v2をGreenにデプロイ
4. [BlueGreen切替] トラフィック切り替え
5. [Green稼働中] Blueを廃止

キー:DBマイグレーションは必ず後方互換性を維持
     旧バージョンのコードが新スキーマで動作すること

9. Kubernetes環境(かんきょう)

9.1 Init Containerアプローチ

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      initContainers:
        - name: flyway-migrate
          image: flyway/flyway:10-alpine
          args: ["migrate"]
          env:
            - name: FLYWAY_URL
              valueFrom:
                secretKeyRef:
                  name: db-secret
                  key: url
            - name: FLYWAY_USER
              valueFrom:
                secretKeyRef:
                  name: db-secret
                  key: username
            - name: FLYWAY_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-secret
                  key: password
          volumeMounts:
            - name: migrations
              mountPath: /flyway/sql
      containers:
        - name: myapp
          image: myapp:latest
          ports:
            - containerPort: 8080
      volumes:
        - name: migrations
          configMap:
            name: flyway-migrations

9.2 Kubernetes Jobアプローチ

# k8s/migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: flyway-migrate-v10
  labels:
    app: flyway-migration
spec:
  backoffLimit: 3
  activeDeadlineSeconds: 300
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: flyway
          image: flyway/flyway:10-alpine
          args: ["migrate"]
          env:
            - name: FLYWAY_URL
              valueFrom:
                secretKeyRef:
                  name: db-secret
                  key: url
            - name: FLYWAY_USER
              valueFrom:
                secretKeyRef:
                  name: db-secret
                  key: username
            - name: FLYWAY_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-secret
                  key: password
            - name: FLYWAY_VALIDATE_ON_MIGRATE
              value: "true"
            - name: FLYWAY_CONNECT_RETRIES
              value: "5"
          volumeMounts:
            - name: migrations
              mountPath: /flyway/sql
      volumes:
        - name: migrations
          configMap:
            name: flyway-migrations

9.3 Helm Hooks

# helm/templates/migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: "{{ .Release.Name }}-flyway-migrate"
  annotations:
    "helm.sh/hook": pre-install,pre-upgrade
    "helm.sh/hook-weight": "-1"
    "helm.sh/hook-delete-policy": hook-succeeded
spec:
  backoffLimit: 3
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: flyway
          image: "flyway/flyway:{{ .Values.flyway.version }}"
          args: ["migrate"]
          env:
            - name: FLYWAY_URL
              value: "jdbc:postgresql://{{ .Values.db.host }}:{{ .Values.db.port }}/{{ .Values.db.name }}"
            - name: FLYWAY_USER
              valueFrom:
                secretKeyRef:
                  name: "{{ .Values.db.secretName }}"
                  key: username
            - name: FLYWAY_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: "{{ .Values.db.secretName }}"
                  key: password
          volumeMounts:
            - name: migrations
              mountPath: /flyway/sql
      volumes:
        - name: migrations
          configMap:
            name: "{{ .Release.Name }}-migrations"

9.4 同時(どうじ)マイグレーション処理(しょり)(ロック)

複数(ふくすう)のPodが同時(どうじ)にマイグレーションを実行(じっこう)しないようにする必要(ひつよう)があります。

Flyway組み込みロック:
  - flyway_schema_historyテーブルに組み込みロック機構あり
  - 最初のインスタンスがマイグレーションを実行
  - 他は待機してからスキップ

追加の安全対策:
  - K8s Job(単一Pod)を推奨
  - Init Containerアプローチ:各Podが試行(Flywayがロック処理)
  - DB Advisory Lockで追加の安全性
-- カスタムロック機構(Advisory Lock)
-- beforeMigrate.sql
SELECT pg_advisory_lock(12345);

-- afterMigrate.sql
SELECT pg_advisory_unlock(12345);

9.5 ArgoCD + Flyway統合(とうごう)

# argocd/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/myapp.git
    path: helm
    targetRevision: main
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
    retry:
      limit: 3
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m
ArgoCD同期フロー:
1. Gitの変更を検出
2. Helm hook (pre-upgrade)Flyway Jobを実行
3. Flywayマイグレーション成功
4. アプリケーションDeploymentが更新
5. ArgoCD同期完了

10. ベストプラクティスとアンチパターン

10.1 ベストプラクティス

1. 1マイグレーション = 1変更

良い例:
  V1__Create_users_table.sql
  V2__Add_email_index.sql
  V3__Create_orders_table.sql

悪い例:
  V1__Create_all_tables_and_indexes_and_data.sql

2. 適用済(てきようずみ)マイグレーションを変更(へんこう)しない

してはいけないこと:
  - 既に実行されたV1__Create_users.sqlを編集
  - Flywayがチェックサムで変更を検出 → エラー

正しいアプローチ:
  - 新しいマイグレーションファイルを作成(V5__Fix_users_table.sql)

3. プロダクション相当(そうとう)のデータでテスト

# プロダクション規模のデータでテスト
# 1000万行のテーブルでALTER TABLEをテスト

# テストデータ生成
INSERT INTO users_test (username, email)
SELECT
    'user_' || generate_series,
    'user_' || generate_series || '@example.com'
FROM generate_series(1, 10000000);

# マイグレーション実行時間を測定
\timing on
ALTER TABLE users_test ADD COLUMN phone VARCHAR(20);
# Time: 45123.456 ms(約45秒) - プロダクションで45秒のロック!

4. スキーマ変更(へんこう)とデータ変更(へんこう)を分離(ぶんり)

V10__Add_status_column.sql       # スキーマ変更
V11__Backfill_status_data.sql    # データ変更
V12__Add_status_not_null.sql     # 制約追加

5. 大規模(だいきぼ)データマイグレーションをバッチ処理(しょり)

-- 悪い例:一度に全て更新
UPDATE users SET normalized_email = LOWER(email);

-- 良い例:バッチ処理
DO $block$
DECLARE
    batch_size INT := 10000;
    total_updated INT := 0;
    rows_affected INT;
BEGIN
    LOOP
        UPDATE users
        SET normalized_email = LOWER(email)
        WHERE id IN (
            SELECT id FROM users
            WHERE normalized_email IS NULL
            LIMIT batch_size
        );
        GET DIAGNOSTICS rows_affected = ROW_COUNT;
        total_updated := total_updated + rows_affected;
        RAISE NOTICE 'Updated % rows (total: %)', rows_affected, total_updated;
        EXIT WHEN rows_affected = 0;
        PERFORM pg_sleep(0.1);
    END LOOP;
END;
$block$;

10.2 アンチパターン

アンチパターン1:プロダクションで直接DDLを実行
  → 必ずマイグレーションファイルを経由

アンチパターン2:環境ごとに異なるマイグレーションファイル
  → 全環境で同じマイグレーション(プレースホルダーを使用)

アンチパターン3:マイグレーションにビジネスロジック
  → マイグレーションはスキーマ/データ変更のみ。ロジックはアプリケーションに

アンチパターン4:後方互換性を無視
  → ゼロダウンタイムデプロイでは旧コードが新スキーマで動作すること

アンチパターン5:テストなしでプロダクションにデプロイ
CIでテストDBを使って必ず検証

アンチパターン6:ロールバック計画なしでデプロイ
  → 全マイグレーションにロールバック/補償戦略を準備

アンチパターン7:マイグレーションツールのバージョンを固定しない
  → ロックファイルまたは明示的なバージョン指定を使用

11. プロダクションデプロイチェックリスト

毎回(まいかい)のデプロイ前(まえ)に確認(かくにん)する項目(こうもく)は以下(いか)の通(とお)りです。

デプロイ前レビュー:
  [ ] マイグレーションSQLコードをレビュー済み
  [ ] 後方互換性を確認(旧コードで動作するか)
  [ ] プロダクション相当のデータでテスト済み
  [ ] 実行時間を測定(大規模テーブルに注意)
  [ ] ロックの影響を分析
  [ ] ロールバック/補償マイグレーションを準備

デプロイ中:
  [ ] データベースバックアップを確認
  [ ] モニタリングダッシュボードを確認(CPU、接続数、スロークエリ)
  [ ] メンテナンスウィンドウ内で実行(必要に応じて)
  [ ] flyway validateを先に実行
  [ ] マイグレーション後にflyway infoを確認
  [ ] アプリケーションヘルスチェックを確認

デプロイ後:
  [ ] スキーマ変更を確認(テーブル、インデックス、制約)
  [ ] アプリケーションの正常動作を確認
  [ ] パフォーマンスモニタリング(クエリ性能の変化)
  [ ] エラーログを確認
  [ ] チームに共有(変更内容、影響範囲)

12. クイズ

Q1: Flywayネーミングコンベンション

Flywayで繰り返し実行(Repeatable)マイグレーションファイルのプレフィックスは何ですか?

回答(かいとう):R__

Flywayマイグレーションファイルのプレフィックスは以下(いか)の通りです。

  • V - Versioned Migration(1回のみ実行)
  • R - Repeatable Migration(ファイルが変更されるたびに再実行)
  • U - Undo Migration(Teamsエディション、ロールバック用)

Repeatableマイグレーションは、ビュー、ストアドプロシージャ、関数など、毎回再作成が必要なオブジェクトに使用されます。

Q2: Expand-Contractパターン

ゼロダウンタイムスキーマ変更におけるExpand-Contractパターンの3フェーズを説明してください。

回答(かいとう):

  1. Expand(拡張):新カラムやテーブルを追加します。旧コードと新コードの両方が動作できるようにします。新カラムはnullableで追加します。

  2. Migrate(移行):既存データを新構造にコピーまたは変換します。この段階で新コードは新カラムを使用するように切り替えます。

  3. Contract(縮小):すべてのコードが新構造を使用していることを確認した後、旧カラムやテーブルを削除します。

キーポイントは、各フェーズが別々のデプロイメントで行われることです。1回のデプロイで全てを処理してはいけません。

Q3: Prisma Shadow Database

Prisma MigrateにおけるShadow Databaseの役割は何ですか?

回答(かいとう):

Shadow Databaseは、Prisma Migrateがマイグレーションの正確性を検証するために使用する一時データベースです。

動作過程:

  1. 一時データベースを作成します
  2. すべての既存マイグレーションファイルを順番に適用します
  3. 現在のPrismaスキーマと比較して差異を検出します
  4. 新しいマイグレーションSQLを生成します
  5. 生成されたSQLをShadow DBに適用して検証します
  6. 一時データベースを削除します

これによりマイグレーションドリフトを検出し、不正なマイグレーションファイルを防止できます。

Q4: CI/CDでのマイグレーションテスト

CI/CDパイプラインでDBマイグレーションを安全にテストする方法は?

回答(かいとう):

  1. テストデータベースを使用:CI環境でDockerなどを使って一時DBを作成します。

  2. Flyway validateを実行:マイグレーションファイルの整合性を先に検証します。

  3. 全マイグレーションを適用:空のDBで全マイグレーションを順番に実行します。

  4. スキーマを検証:期待するテーブル、カラム、インデックスが存在するか確認します。

  5. プロダクション相当のデータで性能テスト:大規模データでの実行時間とロック影響を測定します。

  6. ステージング環境で検証:プロダクションデプロイ前にステージングで先に適用します。

  7. 承認ゲート:プロダクションデプロイ前に手動承認ステップを追加します。

Q5: ロールバック不可(ふか)シナリオ

DBマイグレーションでロールバックすべきでない状況を2つ説明してください。

回答(かいとう):

  1. カラム削除後のデータ損失:DROP COLUMNでカラムを削除したマイグレーションは、ロールバックしてもデータが既に失われており復旧できません。削除前に必ずバックアップを確認する必要があります。

  2. 非可逆的データ変換:データを変換(例:メールのハッシュ化)した後、元のデータを保存していない場合、ロールバックしても元のデータを復元できません。

追加(ついか)のケース:

  • 大規模データ移行後、ロールバック時間がサービス許容範囲を超過する場合
  • 外部システムと連携した変更で、外部システムの状態を戻せない場合

これらの状況ではForward-Onlyアプローチ(補償マイグレーション)がより安全です。


13. 参考資料(さんこうしりょう)