マイグレーション戦略
概要
本プロジェクトでは、Python の Alembic をマイグレーションツールとして採用し、 SQLAlchemy ORM と組み合わせてスキーマ管理を行う。
技術スタック
| 項目 | 技術 |
|---|---|
| データベース | PostgreSQL 16+ |
| ORM | SQLAlchemy 2.x |
| マイグレーション | Alembic 1.x |
| コンテナ | Docker Compose(ローカル開発) |
ディレクトリ構成
backend/
├── alembic/
│ ├── alembic.ini
│ ├── env.py
│ ├── script.py.mako
│ └── versions/
│ ├── 001_initial_schema.py
│ ├── 002_add_xxx.py
│ └── ...
├── app/
│ ├── models/
│ │ ├── __init__.py
│ │ ├── project.py
│ │ ├── user.py
│ │ ├── issue.py
│ │ ├── assignment.py
│ │ ├── review.py
│ │ ├── reward.py
│ │ └── budget_snapshot.py
│ └── ...
└── ...初期セットアップ手順
1. Alembic の初期化
bash
cd backend
alembic init alembic2. alembic.ini の設定
ini
[alembic]
script_location = alembic
# データベースURLは環境変数から取得(env.pyで上書き)
sqlalchemy.url = postgresql://user:pass@localhost:5432/issue_outsource3. env.py の設定
python
from logging.config import fileConfig
import os
from sqlalchemy import engine_from_config, pool
from alembic import context
# モデルの Base をインポート
from app.models import Base
config = context.config
# 環境変数からDB URLを取得
database_url = os.environ.get("DATABASE_URL")
if database_url:
config.set_main_option("sqlalchemy.url", database_url)
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = Base.metadata
def run_migrations_offline() -> None:
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()4. 初期マイグレーションの作成
bash
# 自動生成
alembic revision --autogenerate -m "initial schema"
# 適用
alembic upgrade head初期スキーマ作成手順
ステップ1: ローカルDB起動
bash
docker compose up -d dbdocker-compose.yml の DB 定義例:
yaml
services:
db:
image: postgres:16
environment:
POSTGRES_DB: issue_outsource
POSTGRES_USER: app
POSTGRES_PASSWORD: localdev
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:ステップ2: SQLAlchemy モデル定義
テーブル定義書(schema.md)に基づき、各モデルクラスを実装する。
python
# app/models/base.py
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import func
from datetime import datetime
import uuid
class Base(DeclarativeBase):
pass
class TimestampMixin:
created_at: Mapped[datetime] = mapped_column(
default=func.now(), nullable=False
)
updated_at: Mapped[datetime] = mapped_column(
default=func.now(), onupdate=func.now(), nullable=False
)ステップ3: マイグレーション生成と適用
bash
# マイグレーション生成
alembic revision --autogenerate -m "initial schema"
# マイグレーション内容を確認(必ず目視レビュー)
cat alembic/versions/xxxx_initial_schema.py
# 適用
alembic upgrade head
# 確認
alembic currentステップ4: 初期データ投入(シードデータ)
開発用のシードデータを alembic/seed.py として準備し、開発環境でのみ実行する。
bash
python -m alembic.seedスキーマ変更ポリシー
基本ルール
全てのスキーマ変更はAlembicマイグレーションで管理する
- 手動でのDDL実行は禁止
alembic revision --autogenerateを起点とし、必ず手動レビューを行う
マイグレーションファイルの命名規則
- 自動生成のリビジョンIDに加え、説明的なメッセージを付与
- 例:
alembic revision --autogenerate -m "add_payment_method_to_rewards"
後方互換性の維持
- カラム追加:
nullable=Trueまたはserver_defaultを指定 - カラム削除: 2段階で行う(非推奨化 -> 次リリースで削除)
- テーブル名変更: 禁止(新テーブル作成 + データ移行で対応)
- カラム追加:
downgrade の実装
- 全マイグレーションに
downgrade()を実装する - データ損失を伴う downgrade は明示的にコメントで警告する
- 全マイグレーションに
変更プロセス
1. ブランチ作成(feature/xxx)
2. SQLAlchemy モデルを修正
3. alembic revision --autogenerate -m "説明"
4. 生成されたマイグレーションファイルを目視レビュー
5. ローカルで alembic upgrade head を実行して検証
6. テストを実行
7. MR を作成してレビュー
8. マージ後、CI/CD パイプラインでステージング環境に適用
9. 問題なければ本番環境に適用禁止事項
- マイグレーションファイルの事後編集(適用済みファイルの改変)
alembic stampによる履歴の改ざん(障害対応時を除く)- 本番DBへの直接DDL実行
データマイグレーション
スキーマ変更に伴うデータ変換が必要な場合:
- スキーマ変更とデータ変換を 別のマイグレーションファイル に分離
- データマイグレーションにはバッチ処理を使用(大量データ対応)
- 実行前にバックアップを取得
python
# データマイグレーション例
def upgrade():
# バッチ処理でデータ変換
conn = op.get_bind()
result = conn.execute(text("SELECT id FROM old_table"))
for batch in chunked(result, 1000):
# 変換処理
pass環境別マイグレーション運用
| 環境 | 適用方法 | タイミング |
|---|---|---|
| ローカル開発 | alembic upgrade head 手動実行 | 開発者が任意で実行 |
| ステージング | CI/CD パイプラインで自動適用 | マージ時 |
| 本番 | CI/CD パイプライン + 承認ゲート | リリース時 |
CI/CD パイプラインでの実行
yaml
# .gitlab-ci.yml の例
migrate:
stage: deploy
script:
- alembic upgrade head
environment:
name: staging
only:
- mainバックアップとリカバリ
マイグレーション前のバックアップ
本番環境でのマイグレーション実行前に、必ず pg_dump でバックアップを取得する。
bash
pg_dump -Fc issue_outsource > backup_$(date +%Y%m%d_%H%M%S).dumpロールバック手順
bash
# 1つ前のリビジョンに戻す
alembic downgrade -1
# 特定のリビジョンに戻す
alembic downgrade <revision_id>障害時の対応フロー
- マイグレーション失敗を検知
alembic downgrade -1でロールバック試行- ロールバック不可の場合、バックアップからリストア
- 原因調査・修正後に再適用