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

- Name
- Youngju Kim
- @fjvbn20031
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
核心原則(かくしんげんそく)は以下(いか)の通(とお)りです。
- すべてのスキーマ変更はマイグレーションファイルで - 直接SQL実行禁止
- マイグレーションはコードと一緒にバージョン管理 - Gitリポジトリに含める
- 自動化された実行 - CI/CDパイプラインで実行
- 冪等性(べきとうせい)(Idempotency) - 同じマイグレーションを複数回実行しても同じ結果
- 順序保証 - マイグレーションは決められた順序で実行
2. マイグレーションツール比較(ひかく)
主要(しゅよう)ツール比較表(ひかくひょう)
| ツール | アプローチ | 言語/エコシステム | 強み | 弱み |
|---|---|---|---|---|
| Flyway | バージョンベースSQL | Java/CLI | シンプル、SQLネイティブ | 有料機能が多い |
| Liquibase | チェンジセットXML/YAML/SQL | Java/CLI | ロールバック、diff | 設定が複雑 |
| Prisma Migrate | 宣言的スキーマ | TypeScript | 型安全、Shadow DB | Prisma依存 |
| TypeORM | コードベースマイグレーション | TypeScript | エンティティ自動生成 | TypeORM依存 |
| Alembic | バージョンベースPython | Python | SQLAlchemy統合 | Python専用 |
| Django Migrations | 自動検出 | Python | ORM完全統合 | Django専用 |
| Knex.js | コードベース | JavaScript | 軽量 | 機能が限定的 |
| golang-migrate | バージョンベースSQL | Go/CLI | 軽量、マルチDB | 機能が最小限 |
選択基準(せんたくきじゅん)
プロジェクトの技術スタックは?
├── Java/Spring → FlywayまたはLiquibase
│ ├── シンプルさ重視 → Flyway
│ └── ロールバック/diff必要 → Liquibase
├── TypeScript/Node.js
│ ├── Prisma使用 → Prisma Migrate
│ ├── TypeORM使用 → TypeORM Migration
│ └── 軽量 → Knex.js
├── Python
│ ├── Django → Django Migrations
│ └── Flask/FastAPI → Alembic
├── 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デフォルト):
V1 → V2 → V3 → V4
V4に問題 → V5を作成して修正
ロールバックサポート(Liquibase、Flyway Teams):
V1 → V2 → V3 → V4
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. [Blue → Green切替] トラフィック切り替え
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フェーズを説明してください。
回答(かいとう):
-
Expand(拡張):新カラムやテーブルを追加します。旧コードと新コードの両方が動作できるようにします。新カラムはnullableで追加します。
-
Migrate(移行):既存データを新構造にコピーまたは変換します。この段階で新コードは新カラムを使用するように切り替えます。
-
Contract(縮小):すべてのコードが新構造を使用していることを確認した後、旧カラムやテーブルを削除します。
キーポイントは、各フェーズが別々のデプロイメントで行われることです。1回のデプロイで全てを処理してはいけません。
Q3: Prisma Shadow Database
Prisma MigrateにおけるShadow Databaseの役割は何ですか?
回答(かいとう):
Shadow Databaseは、Prisma Migrateがマイグレーションの正確性を検証するために使用する一時データベースです。
動作過程:
- 一時データベースを作成します
- すべての既存マイグレーションファイルを順番に適用します
- 現在のPrismaスキーマと比較して差異を検出します
- 新しいマイグレーションSQLを生成します
- 生成されたSQLをShadow DBに適用して検証します
- 一時データベースを削除します
これによりマイグレーションドリフトを検出し、不正なマイグレーションファイルを防止できます。
Q4: CI/CDでのマイグレーションテスト
CI/CDパイプラインでDBマイグレーションを安全にテストする方法は?
回答(かいとう):
-
テストデータベースを使用:CI環境でDockerなどを使って一時DBを作成します。
-
Flyway validateを実行:マイグレーションファイルの整合性を先に検証します。
-
全マイグレーションを適用:空のDBで全マイグレーションを順番に実行します。
-
スキーマを検証:期待するテーブル、カラム、インデックスが存在するか確認します。
-
プロダクション相当のデータで性能テスト:大規模データでの実行時間とロック影響を測定します。
-
ステージング環境で検証:プロダクションデプロイ前にステージングで先に適用します。
-
承認ゲート:プロダクションデプロイ前に手動承認ステップを追加します。
Q5: ロールバック不可(ふか)シナリオ
DBマイグレーションでロールバックすべきでない状況を2つ説明してください。
回答(かいとう):
-
カラム削除後のデータ損失:DROP COLUMNでカラムを削除したマイグレーションは、ロールバックしてもデータが既に失われており復旧できません。削除前に必ずバックアップを確認する必要があります。
-
非可逆的データ変換:データを変換(例:メールのハッシュ化)した後、元のデータを保存していない場合、ロールバックしても元のデータを復元できません。
追加(ついか)のケース:
- 大規模データ移行後、ロールバック時間がサービス許容範囲を超過する場合
- 外部システムと連携した変更で、外部システムの状態を戻せない場合
これらの状況ではForward-Onlyアプローチ(補償マイグレーション)がより安全です。