自動マイグレーション対決:Prisma vs SQLAlchemy+Alembic 完全ガイド
データベースマイグレーションは、現代のWebアプリケーション開発において避けて通れない重要な作業です。本記事では、完全自動化を謳うPrismaと、柔軟な半自動化を提供するSQLAlchemy+Alembicの自動マイグレーション機能を徹底比較します。
データベースマイグレーションとは?
データベースマイグレーションとは、データベーススキーマの変更を管理・実行するプロセスです。これを理解するために、身近な例で説明しましょう。
現実世界の例:図書館のリニューアル
imagine a library that needs renovation:
- 新しい書棚を追加(新しいテーブル)
- 既存の書棚に新しい区画を作成(カラム追加)
- 児童書コーナーを移動(データ移行)
- カード目録からデジタルシステムへ移行(構造変更)
データベースマイグレーションは、これらの変更を安全かつ確実に行うためのプロセスです。
なぜ自動マイグレーションが重要なのか?
手動でマイグレーションを作成すると:
- 人的エラーが発生しやすい
- 時間がかかる
- チーム間での一貫性が保てない
- 複雑な変更を見逃しやすい
自動マイグレーション機能により、これらの問題を大幅に軽減できます。
Prismaの完全自動化アプローチ
Prismaは「Schema-First」の哲学に基づいており、スキーマファイルの変更を検知して自動的にマイグレーションを生成します。
Prismaの仕組み
実践例:ユーザーテーブルの作成
1. 初期スキーマ定義
// 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?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
2. マイグレーション実行
$ npx prisma migrate dev --name init
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "myapp"
✔ Generated Prisma Client (4.16.2 | library) to ./node_modules/@prisma/client in 2.11s
The following migration(s) have been created and applied:
migrations/
└─ 20231201120000_init/
└─ migration.sql
3. 生成されたマイグレーションファイル
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
Prismaでカラム追加
スキーマに新しいフィールドを追加してみましょう:
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
age Int? // 新しく追加
profile String? // 新しく追加
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
$ npx prisma migrate dev --name add_user_fields
The following migration(s) have been created and applied:
migrations/
└─ 20231201130000_add_user_fields/
└─ migration.sql
生成されたマイグレーション:
-- AlterTable
ALTER TABLE "User" ADD COLUMN "age" INTEGER,
ADD COLUMN "profile" TEXT;
Prismaの利点
- 完全自動化: スキーマからマイグレーションまで一気通貫
- タイプセーフ: 生成されるクライアントも自動更新
- シンプル: 学習コストが低い
- 一貫性: チーム全体で同じマイグレーションが生成される
Prismaの制限
- 柔軟性の欠如: 複雑なマイグレーションには対応できない場合がある
- カスタマイズ困難: 生成されたSQLの細かい調整が難しい
- データ変換: 既存データの複雑な変換処理が苦手
SQLAlchemy+Alembicの半自動化アプローチ
AlembicはSQLAlchemyのマイグレーションツールで、--autogenerateオプションにより半自動でマイグレーションを生成します。
Alembicの仕組み
実践例:同じユーザーテーブルの作成
1. SQLAlchemyモデル定義
# models.py
from sqlalchemy import Column, Integer, String, DateTime, func
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True, autoincrement=True)
email = Column(String, unique=True, nullable=False)
name = Column(String, nullable=True)
created_at = Column(DateTime, default=func.now(), nullable=False)
updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False)
2. Alembic初期化
$ alembic init alembic
$ alembic revision --autogenerate -m "Create user table"
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'users'
INFO [alembic.autogenerate.compare] Detected added index 'ix_users_email' on '['email']'
Generating /path/to/alembic/versions/001_create_user_table.py
3. 生成されたマイグレーションファイル
"""Create user table
Revision ID: 001
Revises:
Create Date: 2023-12-01 12:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers
revision = '001'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('users',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_users_email'), table_name='users')
op.drop_table('users')
# ### end Alembic commands ###
Alembicでカラム追加
# models.py(更新版)
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True, autoincrement=True)
email = Column(String, unique=True, nullable=False)
name = Column(String, nullable=True)
age = Column(Integer, nullable=True) # 新しく追加
profile = Column(String, nullable=True) # 新しく追加
created_at = Column(DateTime, default=func.now(), nullable=False)
updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False)
$ alembic revision --autogenerate -m "Add age and profile columns"
INFO [alembic.autogenerate.compare] Detected added column 'users.age'
INFO [alembic.autogenerate.compare] Detected added column 'users.profile'
Generating /path/to/alembic/versions/002_add_age_and_profile_columns.py
生成されたマイグレーション:
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('age', sa.Integer(), nullable=True))
op.add_column('users', sa.Column('profile', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'profile')
op.drop_column('users', 'age')
# ### end Alembic commands ###
Alembicの自動検出できないケース
Alembicが自動検出できない典型的なケース:
1. カラム名の変更
# Before
name = Column(String, nullable=True)
# After
full_name = Column(String, nullable=True)
自動生成されるマイグレーション(間違い):
def upgrade() -> None:
op.add_column('users', sa.Column('full_name', sa.String(), nullable=True))
op.drop_column('users', 'name') # データが失われる!
正しい手動修正:
def upgrade() -> None:
op.alter_column('users', 'name', new_column_name='full_name')
2. テーブル名の変更
# 手動で修正が必要
def upgrade() -> None:
op.rename_table('users', 'app_users')
3. データ変換を伴う変更
def upgrade() -> None:
# カラム追加
op.add_column('users', sa.Column('status', sa.String(), nullable=True))
# データ変換(手動で追加)
op.execute("UPDATE users SET status = 'active' WHERE deleted_at IS NULL")
op.execute("UPDATE users SET status = 'deleted' WHERE deleted_at IS NOT NULL")
# NOT NULL制約追加
op.alter_column('users', 'status', nullable=False)
Alembicの高度な設定
alembic.ini設定例
[alembic]
script_location = alembic
sqlalchemy.url = postgresql://user:pass@localhost/dbname
# カスタム比較オプション
compare_type = true
compare_server_default = true
# 無視する設定
[post_write_hooks]
hooks = black
black.type = console_scripts
black.entrypoint = black
black.options = REVISION_SCRIPT_FILENAME
env.py設定例
# alembic/env.py
from alembic import context
from sqlalchemy import engine_from_config, pool
from models import Base
# カスタム比較関数
def include_object(object, name, type_, reflected, compare_to):
"""特定のオブジェクトを無視する"""
if type_ == "table" and name == "temp_table":
return False
return True
def run_migrations_online():
configuration = context.config.get_section(context.config.config_ini_section)
configuration['sqlalchemy.url'] = os.getenv('DATABASE_URL')
connectable = engine_from_config(
configuration,
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=Base.metadata,
include_object=include_object,
compare_type=True,
compare_server_default=True
)
with context.begin_transaction():
context.run_migrations()
詳細比較:同じ変更を両ツールで実行
実際の開発でよくある変更を両ツールで実行して比較してみましょう。
シナリオ1:新しいテーブル追加
Prismaの場合
model Post {
id Int @id @default(autoincrement())
title String
content String
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
$ npx prisma migrate dev --name add_post_table
生成されるSQL:
-- CreateTable
CREATE TABLE "Post" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"published" BOOLEAN NOT NULL DEFAULT false,
"authorId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Post_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Alembicの場合
class Post(Base):
__tablename__ = 'posts'
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String, nullable=False)
content = Column(String, nullable=False)
published = Column(Boolean, default=False, nullable=False)
author_id = Column(Integer, ForeignKey('users.id'), nullable=False)
created_at = Column(DateTime, default=func.now(), nullable=False)
updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False)
author = relationship("User", back_populates="posts")
$ alembic revision --autogenerate -m "Add post table"
生成されるPython:
def upgrade() -> None:
op.create_table('posts',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('title', sa.String(), nullable=False),
sa.Column('content', sa.String(), nullable=False),
sa.Column('published', sa.Boolean(), nullable=False),
sa.Column('author_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['author_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
シナリオ2:インデックス追加
Prismaの場合
model Post {
// ... other fields
title String
published Boolean @default(false)
@@index([published])
@@index([title])
}
$ npx prisma migrate dev --name add_post_indexes
生成されるSQL:
-- CreateIndex
CREATE INDEX "Post_published_idx" ON "Post"("published");
-- CreateIndex
CREATE INDEX "Post_title_idx" ON "Post"("title");
Alembicの場合
class Post(Base):
# ... other fields
title = Column(String, nullable=False, index=True)
published = Column(Boolean, default=False, nullable=False, index=True)
または手動でインデックス定義:
def upgrade() -> None:
op.create_index('ix_posts_published', 'posts', ['published'])
op.create_index('ix_posts_title', 'posts', ['title'])
シナリオ3:カラム名変更(最大の違い)
Prismaの場合
Prismaはカラム名変更を直接検出できません:
model User {
id Int @id @default(autoincrement())
email String @unique
fullName String? // nameから変更
// ...
}
結果:データが失われる可能性のあるマイグレーションが生成される
-- DropColumn
ALTER TABLE "User" DROP COLUMN "name";
-- AddColumn
ALTER TABLE "User" ADD COLUMN "fullName" TEXT;
回避策:手動でマイグレーションを編集する必要がある
Alembicの場合
初回自動生成:
def upgrade() -> None:
op.add_column('users', sa.Column('full_name', sa.String(), nullable=True))
op.drop_column('users', 'name') # 危険!
手動修正(推奨):
def upgrade() -> None:
op.alter_column('users', 'name', new_column_name='full_name')
パフォーマンス比較
マイグレーション生成速度
Prisma
- 小規模プロジェクト: 1-3秒
- 中規模プロジェクト: 3-10秒
- 大規模プロジェクト: 10-30秒
# 時間計測例
$ time npx prisma migrate dev --name test_migration
real 0m4.123s
user 0m2.456s
sys 0m0.234s
Alembic
- 小規模プロジェクト: 0.5-2秒
- 中規模プロジェクト: 2-5秒
- 大規模プロジェクト: 5-15秒
# 時間計測例
$ time alembic revision --autogenerate -m "test migration"
real 0m2.345s
user 0m1.234s
sys 0m0.123s
マイグレーション実行速度
両ツールとも実際のSQL実行時間に大差はありませんが、Prismaは追加でクライアント生成が必要です。
チーム協業での比較
Prismaでの協業
利点
- 一貫性: 全員が同じマイグレーションを生成
- シンプル: 学習コストが低い
- 自動化: 手動介入が少ない
課題
- マージコンフリクト: スキーマファイルでのコンフリクト
- カスタマイズ困難: 特殊要件に対応しにくい
ワークフロー例
# 開発者A
git pull origin main
npx prisma migrate dev # 最新マイグレーションを適用
# スキーマ変更
npx prisma migrate dev --name add_feature_a
git add . && git commit -m "feat: add feature A schema"
git push origin feature-a
# 開発者B
git pull origin main
git checkout feature-b
npx prisma migrate dev # 基本マイグレーションを適用
# スキーマ変更
npx prisma migrate dev --name add_feature_b
マージ時の対処:
git merge main
# schema.prismaでコンフリクトが発生
# 手動でスキーマを統合
npx prisma migrate dev --name merge_features
Alembicでの協業
利点
- 柔軟性: カスタマイズが容易
- レビュー可能: 生成されたマイグレーションを詳細レビューできる
- 細かい制御: 複雑な要件に対応可能
課題
- 複雑性: 学習コストが高い
- 人的エラー: 手動修正時のミス
ワークフロー例
# 開発者A
git pull origin main
alembic upgrade head # 最新マイグレーションを適用
# モデル変更
alembic revision --autogenerate -m "add feature A"
# 生成されたマイグレーションをレビュー・修正
git add . && git commit -m "feat: add feature A migration"
# プルリクエストでマイグレーションをレビュー
# マージ後
alembic upgrade head
実世界のシナリオ
シナリオA:スタートアップの快速開発
要件:
- 素早いプロトタイピング
- 小さなチーム(2-5人)
- シンプルなスキーマ変更が中心
Prismaが適している理由:
// プロトタイプ段階でのシンプルな変更
model User {
id Int @id @default(autoincrement())
email String @unique
name String
}
// 機能追加時
model User {
id Int @id @default(autoincrement())
email String @unique
name String
avatar String? // すぐに追加できる
premium Boolean @default(false) // すぐに追加できる
}
実行:
npx prisma migrate dev --name add_user_features
# 数秒で完了、即座に開発継続可能
シナリオB:大企業のレガシーシステム移行
要件:
- 複雑なデータ変換
- 段階的な移行
- ダウンタイム最小化
- 詳細な制御が必要
Alembicが適している理由:
def upgrade() -> None:
# ステップ1: 新しいカラムを追加(NULLable)
op.add_column('users', sa.Column('new_status', sa.String(), nullable=True))
# ステップ2: 既存データを段階的に変換
connection = op.get_bind()
# バッチ処理で大量データを安全に変換
batch_size = 1000
offset = 0
while True:
result = connection.execute(
f"""
UPDATE users
SET new_status = CASE
WHEN deleted_at IS NOT NULL THEN 'deleted'
WHEN last_login > NOW() - INTERVAL '30 days' THEN 'active'
ELSE 'inactive'
END
WHERE id > {offset} AND id <= {offset + batch_size}
AND new_status IS NULL
"""
)
if result.rowcount == 0:
break
offset += batch_size
# 進行状況をログ出力
print(f"Processed {offset} users...")
# ステップ3: NOT NULL制約を追加
op.alter_column('users', 'new_status', nullable=False)
# ステップ4: 古いカラムを削除(次のマイグレーションで)
# op.drop_column('users', 'deleted_at') # 次回のマイグレーションで実行
シナリオC:複数環境での運用
Prismaの環境管理
# 開発環境
DATABASE_URL="postgresql://dev:password@localhost:5432/myapp_dev"
npx prisma migrate dev
# ステージング環境
DATABASE_URL="postgresql://staging:password@staging-db:5432/myapp_staging"
npx prisma migrate deploy
# 本番環境
DATABASE_URL="postgresql://prod:password@prod-db:5432/myapp_prod"
npx prisma migrate deploy
Alembicの環境管理
# alembic.ini
[alembic]
script_location = alembic
[development]
sqlalchemy.url = postgresql://dev:password@localhost:5432/myapp_dev
[staging]
sqlalchemy.url = postgresql://staging:password@staging-db:5432/myapp_staging
[production]
sqlalchemy.url = postgresql://prod:password@prod-db:5432/myapp_prod
# 環境別実行
alembic -x environment=development upgrade head
alembic -x environment=staging upgrade head
alembic -x environment=production upgrade head
マイグレーションロールバック戦略
Prismaのロールバック
Prismaは直接的なロールバック機能を提供していませんが、以下の方法があります:
方法1: マイグレーション履歴の確認
# マイグレーション履歴確認
npx prisma migrate status
# Database: myapp_dev
# Migration status: All migrations have been applied.
#
# 20231201120000_init
# 20231201130000_add_user_fields
# 20231201140000_add_post_table
方法2: データベースリセット(開発環境のみ)
npx prisma migrate reset
# ⚠️ 全データが削除されます!
方法3: 手動ロールバック
-- 手動でロールバック用SQLを作成
DROP TABLE "Post";
ALTER TABLE "User" DROP COLUMN "age";
ALTER TABLE "User" DROP COLUMN "profile";
Alembicのロールバック
Alembicは強力なロールバック機能を提供:
# 現在のマイグレーション状態確認
alembic current
# 002 (head)
# 履歴確認
alembic history --verbose
# 002 -> 001 (head), Add age and profile columns
# 001 -> None, Create user table
# 1つ前のマイグレーションにロールバック
alembic downgrade -1
# 特定のマイグレーションにロールバック
alembic downgrade 001
# 全てロールバック
alembic downgrade base
ロールバック用の関数例
def upgrade() -> None:
op.add_column('users', sa.Column('age', sa.Integer(), nullable=True))
op.add_column('users', sa.Column('profile', sa.String(), nullable=True))
def downgrade() -> None:
op.drop_column('users', 'profile')
op.drop_column('users', 'age')
本番環境デプロイメントの考慮事項
Prismaの本番デプロイ
CI/CDパイプライン例
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Generate Prisma Client
run: npx prisma generate
- name: Run migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Deploy application
run: npm run deploy
ゼロダウンタイム戦略
# 1. 新しいカラムを追加(NULLable)
# schema.prisma
model User {
email String @unique
name String?
newEmail String? // 新しいカラム
}
# 2. アプリケーションデプロイ(両方のカラムを使用)
# 3. データ移行
# 4. 古いカラムを削除する次のマイグレーション
Alembicの本番デプロイ
ブルーグリーンデプロイメント対応
def upgrade() -> None:
# フェーズ1: 後方互換性を保つ変更
op.add_column('users', sa.Column('new_email', sa.String(), nullable=True))
# データ移行(バックグラウンドで実行可能)
op.execute("""
UPDATE users SET new_email = email
WHERE new_email IS NULL
""")
def downgrade() -> None:
op.drop_column('users', 'new_email')
# 次のマイグレーション(アプリケーション更新後)
def upgrade() -> None:
# NOT NULL制約追加
op.alter_column('users', 'new_email', nullable=False)
# 古いカラム削除
op.drop_column('users', 'email')
# カラム名変更
op.alter_column('users', 'new_email', new_column_name='email')
ベストプラクティス
Prismaのベストプラクティス
1. 段階的なスキーマ変更
// ❌ 一度に大きな変更
model User {
id Int @id @default(autoincrement())
email String @unique
profile Profile @relation(fields: [profileId], references: [id])
profileId Int @unique
}
model Profile {
id Int @id @default(autoincrement())
bio String
avatar String
user User?
}
// ✅ 段階的な変更
// Step 1: Profileテーブルを追加
model Profile {
id Int @id @default(autoincrement())
bio String
avatar String
}
// Step 2: リレーション追加
model User {
id Int @id @default(autoincrement())
email String @unique
profileId Int? @unique // 最初はoptional
profile Profile? @relation(fields: [profileId], references: [id])
}
2. マイグレーション名の命名規則
# ✅ 良い例
npx prisma migrate dev --name add_user_profile_relation
npx prisma migrate dev --name add_post_published_index
npx prisma migrate dev --name update_user_email_unique_constraint
# ❌ 悪い例
npx prisma migrate dev --name update
npx prisma migrate dev --name fix
npx prisma migrate dev --name changes
3. 環境変数の管理
# .env.local(開発環境)
DATABASE_URL="postgresql://dev:password@localhost:5432/myapp_dev"
# .env.staging
DATABASE_URL="postgresql://staging:password@staging-db:5432/myapp_staging"
# .env.production
DATABASE_URL="postgresql://prod:password@prod-db:5432/myapp_prod"
Alembicのベストプラクティス
1. 自動生成後の必須チェック
# 生成されたマイグレーションの確認ポイント
def upgrade() -> None:
# ✅ チェック1: データ損失がないか
op.add_column('users', sa.Column('full_name', sa.String(), nullable=True))
# データ移行を追加
op.execute("UPDATE users SET full_name = name WHERE full_name IS NULL")
# NOT NULL制約(必要に応じて)
op.alter_column('users', 'full_name', nullable=False)
# ✅ チェック2: インデックスが適切か
op.create_index('ix_users_full_name', 'users', ['full_name'])
# 古いカラム削除は慎重に
op.drop_column('users', 'name') # 本当に必要?
def downgrade() -> None:
# ✅ チェック3: ロールバックが正しく動作するか
op.add_column('users', sa.Column('name', sa.String(), nullable=True))
op.execute("UPDATE users SET name = full_name WHERE name IS NULL")
op.drop_index('ix_users_full_name', table_name='users')
op.drop_column('users', 'full_name')
2. カスタム比較関数の活用
# alembic/env.py
def include_object(object, name, type_, reflected, compare_to):
"""
カスタムオブジェクト比較関数
"""
# テンポラリテーブルを無視
if type_ == "table" and name.startswith("temp_"):
return False
# 特定のインデックスを無視
if type_ == "index" and name.startswith("pg_"):
return False
# ビューは無視
if type_ == "table" and hasattr(object, 'info') and object.info.get('is_view'):
return False
return True
def compare_type(context, inspected_column, metadata_column, inspected_type, metadata_type):
"""
カスタム型比較関数
"""
# PostgreSQLのVARCHARとTEXTを同じとして扱う
if isinstance(inspected_type, VARCHAR) and isinstance(metadata_type, Text):
return False
# デフォルトの比較
return None
3. 複雑なマイグレーションのテンプレート
"""Complex migration template
Revision ID: xxx
Revises: yyy
Create Date: 2023-12-01 12:00:00
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.orm import Session
# revision identifiers
revision = 'xxx'
down_revision = 'yyy'
branch_labels = None
depends_on = None
def upgrade() -> None:
# フェーズ1: 構造変更
print("Phase 1: Schema changes...")
op.add_column('users', sa.Column('temp_status', sa.String(), nullable=True))
# フェーズ2: データ移行
print("Phase 2: Data migration...")
bind = op.get_bind()
session = Session(bind=bind)
try:
# バッチ処理でデータ移行
batch_size = 1000
offset = 0
while True:
users = session.execute(
sa.text("""
SELECT id, old_status
FROM users
WHERE temp_status IS NULL
AND id > :offset
ORDER BY id
LIMIT :limit
"""),
{"offset": offset, "limit": batch_size}
).fetchall()
if not users:
break
for user in users:
new_status = map_old_to_new_status(user.old_status)
session.execute(
sa.text("UPDATE users SET temp_status = :status WHERE id = :id"),
{"status": new_status, "id": user.id}
)
session.commit()
offset = users[-1].id
print(f"Processed batch ending at ID {offset}")
# フェーズ3: 制約追加
print("Phase 3: Adding constraints...")
op.alter_column('users', 'temp_status', nullable=False, new_column_name='status')
op.drop_column('users', 'old_status')
except Exception as e:
session.rollback()
print(f"Migration failed: {e}")
raise
finally:
session.close()
def map_old_to_new_status(old_status):
"""ステータス変換ロジック"""
mapping = {
0: 'inactive',
1: 'active',
2: 'suspended',
None: 'unknown'
}
return mapping.get(old_status, 'unknown')
def downgrade() -> None:
print("Rolling back complex migration...")
# 逆の処理を実装
op.add_column('users', sa.Column('old_status', sa.Integer(), nullable=True))
# データを戻す処理
bind = op.get_bind()
session = Session(bind=bind)
reverse_mapping = {
'inactive': 0,
'active': 1,
'suspended': 2,
'unknown': None
}
users = session.execute(sa.text("SELECT id, status FROM users")).fetchall()
for user in users:
old_value = reverse_mapping.get(user.status)
session.execute(
sa.text("UPDATE users SET old_status = :old_status WHERE id = :id"),
{"old_status": old_value, "id": user.id}
)
session.commit()
session.close()
op.drop_column('users', 'status')
よくある落とし穴と対策
Prismaの落とし穴
1. マイグレーション順序の問題
// ❌ 問題のある変更
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[] // リレーション追加
}
model Post {
id Int @id @default(autoincrement())
title String
authorId Int
author User @relation(fields: [authorId], references: [id])
}
実行結果:
Error: The relation field `posts` on the `User` model is missing an opposite relation field on the `Post` model.
✅ 正しい修正:
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[] // back_populatesを指定
}
model Post {
id Int @id @default(autoincrement())
title String
authorId Int
author User @relation(fields: [authorId], references: [id])
}
2. データ型変更の危険性
// Before
model Product {
id Int @id @default(autoincrement())
price Int // 整数
}
// After
model Product {
id Int @id @default(autoincrement())
price Decimal // 小数点対応
}
生成されるマイグレーション(データ損失の可能性):
-- AlterTable
ALTER TABLE "Product" ALTER COLUMN "price" SET DATA TYPE DECIMAL;
✅ 安全な移行戦略:
// Step 1: 新しいカラム追加
model Product {
id Int @id @default(autoincrement())
price Int // 既存
priceNew Decimal? // 新しいカラム
}
// Step 2: データ移行後、古いカラム削除
model Product {
id Int @id @default(autoincrement())
price Decimal // priceNewから名前変更
}
Alembicの落とし穴
1. 自動生成の過信
# ❌ 自動生成されたマイグレーション(問題あり)
def upgrade() -> None:
op.drop_table('old_users') # データ損失!
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
✅ 手動レビューで修正:
def upgrade() -> None:
# 1. 新しいテーブル作成
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
# 2. データ移行
op.execute("""
INSERT INTO users (id, email)
SELECT id, email_address FROM old_users
""")
# 3. 古いテーブル削除(次のマイグレーションで)
# op.drop_table('old_users') # 後で実行
2. 循環依存の問題
# ❌ 問題のあるマイグレーション順序
def upgrade() -> None:
# 外部キー制約を先に作成してしまう
op.create_foreign_key(
'fk_posts_user_id', 'posts', 'users', ['user_id'], ['id']
)
# テーブルの作成が後になってエラー
op.create_table('users', ...)
✅ 正しい順序:
def upgrade() -> None:
# 1. 参照される側のテーブルを先に作成
op.create_table('users', ...)
# 2. 参照する側のテーブルを作成
op.create_table('posts', ...)
# 3. 外部キー制約を追加
op.create_foreign_key(
'fk_posts_user_id', 'posts', 'users', ['user_id'], ['id']
)
選択の決定ガイド
プロジェクト特性による選択
機能比較表
| 項目 | Prisma | Alembic | 勝者 |
|---|---|---|---|
| 学習コスト | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | Prisma |
| 自動化レベル | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Prisma |
| カスタマイズ性 | ⭐⭐ | ⭐⭐⭐⭐⭐ | Alembic |
| 複雑な変更対応 | ⭐⭐ | ⭐⭐⭐⭐⭐ | Alembic |
| ロールバック | ⭐⭐ | ⭐⭐⭐⭐⭐ | Alembic |
| 実行速度 | ⭐⭐⭐ | ⭐⭐⭐⭐ | Alembic |
| チーム協業 | ⭐⭐⭐⭐ | ⭐⭐⭐ | Prisma |
| エラー回復 | ⭐⭐ | ⭐⭐⭐⭐ | Alembic |
決定フローチャート
まとめ
データベースマイグレーションの自動化は、現代のWebアプリケーション開発において必須の要素です。PrismaとSQLAlchemy+Alembicは、それぞれ異なるアプローチで問題解決を図っています。
Prismaを選ぶべき場合
- スタートアップや小規模チーム: 素早いプロトタイピングが必要
- シンプルなアプリケーション: 複雑なスキーマ変更が少ない
- 学習コスト重視: チーム全体のスキルレベルを考慮
- 一貫性重視: 全員が同じマイグレーションを生成したい
Alembicを選ぶべき場合
- エンタープライズアプリケーション: 複雑な要件やレガシーシステム
- カスタマイズ重要: 細かい制御や特殊な処理が必要
- チーム経験豊富: SQLやデータベース設計に詳しいメンバー
- パフォーマンス重視: 大規模データベースでの運用
最終的な推奨事項
- プロジェクト初期段階: 要件の複雑さを正確に評価する
- チーム能力: メンバーのスキルレベルを考慮する
- 将来性: アプリケーションの成長に応じた拡張性を検討する
- 運用体制: デプロイメント戦略とマイグレーション管理体制を整備する
どちらのツールも優秀ですが、プロジェクトの特性とチームの状況に応じて適切に選択することが成功の鍵となります。また、選択したツール経特性を理解し、ベストプラクティスに従って運用することで、安全で効率的なデータベースマイグレーションを実現できます。
この記事では、両ツールの実践的な使用例を通して、それぞれの特徴と適用場面を詳しく解説しました。プロジェクトの成功のために、適切なツール選択の参考にしていただければ幸いです。