提示:本网站现在为维护调试状态

Alembic ディープガイド:DBマイグレーション + 自動生成 + ブランチマージ

Choyeon
Choyeon
2026年9月30日 1 分で読了 358 閲覧
866 字 5,133 文字 更新日 2026年11月8日
Alembic ディープガイド:DBマイグレーション + 自動生成 + ブランチマージ
目次

Alembic Deep Dive: Migrations, Autogenerate, and Branch Merging

Schema change management is the most accident-prone part of team collaboration: handwritten DDL not tracked, prod/dev out of sync, deadlocks from migrations after multi-branch merge. As SQLAlchemy's official tool, Alembic auto-detects model changes, versions them rollbackably, and handles complex merges.

Autogenerate and Naming Convention

Naming convention (NAMING_CONVENTION) is the foundation of team collaboration. Without it, constraint names on two dev machines autogenerate randomly (hash suffixes), causing migration conflicts on PR merge the instant they land. env.py options compare_type + compare_server_default boost detection accuracy, catching common false-negatives like bool default or VARCHAR length tweaks.

# ====== alembic.ini 核心调优 ======
[alembic]
script_location = migrations
sqlalchemy.url = postgresql+psycopg://app:${DB_PASSWORD}@${DB_HOST}:5432/app
prepend_sys_path = .
timezone = Asia/Shanghai

[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

# ====== migrations/env.py 支持自动生成 + 命名约定 ======
from __future__ import with_statement
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool, MetaData
import os, sys

sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), '..')))
from app.models import Base
from app.db.naming import NAMING_CONVENTION

config = context.config
if config.config_file_name is not None:
    fileConfig(config.config_file_name)

target_metadata: MetaData = Base.metadata
target_metadata.naming_convention = NAMING_CONVENTION  # 关键:约束自动命名

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"},
        render_as_batch=True,  # SQLite 等支持批处理ALTER
        compare_type=True, compare_server_default=True,
        include_object=include_object_fn
    )
    with context.begin_transaction():
        context.run_migrations()

def include_object_fn(object, name, type_, reflected, compare_to):
    if type_ == "table" and name in {"spatial_ref_sys", "alembic_version"}:
        return False
    return True

def run_migrations_online() -> None:
    connectable = engine_from_config(
        config.get_section(config.config_ini_section, {}),
        prefix="sqlalchemy.", poolclass=pool.NullPool, future=True
    )
    with connectable.connect() as connection:
        context.configure(
            connection=connection, target_metadata=target_metadata,
            render_as_batch=True,
            compare_type=True,
            compare_server_default=True,
            include_object=include_object_fn,
            transaction_per_migration=True,  # 每个迁移独立事务
            lock_timeout=30                   # DDL 锁超时 30s
        )
        with context.begin_transaction():
            context.run_migrations()

if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()

# ====== app/db/naming.py — 约束命名公约防分支冲突 ======
NAMING_CONVENTION = {
    "ix": "ix_%(column_0_label)s",
    "uq": "uq_%(table_name)s_%(column_0_N_name)s",
    "ck": "ck_%(table_name)s_%(constraint_name)s",
    "fk": "fk_%(table_name)s_%(column_0_N_name)s_%(referred_table_name)s",
    "pk": "pk_%(table_name)s",
}

# ====== 典型迁移 revision 示例:data-only 迁移 ======
'''${message}

Revision ID: 0004_add_user_status
Revises: 0003_user_baseline
Create Date: 2024-06-15 11:22:33

'''
from alembic import op
import sqlalchemy as sa
from sqlalchemy import update
from sqlalchemy.orm import Session

# revision identifiers, used by Alembic.
revision = '0004_add_user_status'
down_revision = '0003_user_baseline'
branch_labels = None
depends_on = None

def upgrade() -> None:
    op.add_column('users', sa.Column('status', sa.String(16), nullable=False,
                  server_default='ACTIVE'))
    op.create_index('ix_users_status_created', 'users', ['status', 'created_at'])
    bind = op.get_bind(); sess = Session(bind)
    from app.models import User
    sess.execute(update(User).where(User.email.endswith('@test.com')).
                 values(status='INACTIVE'))
    sess.commit()

def downgrade() -> None:
    op.drop_index('ix_users_status_created', 'users')
    op.drop_column('users', 'status')

# ====== 多分支 HEAD 合并:建一个 merge migration ======
# 场景: Alice 分支 head=0005_add_posts, Bob 分支 head=0006_add_tags
# 解决:
#   alembic merge -m "merge posts and tags branches" 0005_add_posts 0006_add_tags
# 生成:
revision = '0007_merge_posts_tags'
down_revision = ('0005_add_posts', '0006_add_tags')
branch_labels = None
depends_on = None

def upgrade(): pass   # merge 迁移通常空实现,只解决 DAG 头分叉
def downgrade(): pass

Branch Merging and Production Release Flow

Alice and Bob open two feature branches with separate migrations; merging back to main yields "two divergent heads". Standard fix: alembic merge <hash1> <hash2> -m "merge A and B" produces an empty merge revision tying both chains together, with down_revision as a tuple. Production playbook: (1) take DB backup first; (2) per-migration transactions with DDL lock timeout; (3) large tables (>1M rows) use CREATE INDEX CONCURRENTLY — never wrap long migrations in single txn.

Scenario Command Caveats
Auto-detect model change → migration alembic revision --autogenerate -m "msg" Always human-review generated up/down
Upgrade to latest alembic upgrade head CI must verify upgrade→downgrade→upgrade idempotent
Roll back one revision alembic downgrade -1 Destructive downgrades: backup-first data-only
Merge two divergent heads alembic merge h1 h2 -m "merge" Merge migration typically no-op
Offline SQL for DBA review alembic upgrade head --sql Use for DBA approval workflows
Online non-blocking index op.create_index(..., postgresql_concurrently=True) Cannot run inside txn; use transaction_per_migration=True

Best Practices

Split each migration into Schema-only (structural change, rollback-safe) vs Data-only (backfill/cleanup - use sparingly). Never mix DDL and bulk writes in same migration. Every PR CI runs three checks: upgrade once, downgrade one revision then upgrade again (idempotency), count of migration heads matches main branch count.

X微博Telegram

筆者について

投票

你最希望 Rosetta 优先加入哪项功能?

社区投票(演示数据,可后台删除)

コメント (3)

0 / 3000
咖啡与代码
咖啡与代码2026年9月21日

受益匪浅,感谢大佬分享!

东京留学僧
东京留学僧2026年9月21日

哈哈,楼主的踩坑经历和我一模一样!我当时就是没注意到这个配置,排查了整整两天,看到这篇文章泪目了。

产品汪本汪
产品汪本汪pmdiary.io2026年9月20日

建议加一下关于幂等性的讨论,现在的分布式系统里这个真的太重要了,很多坑都是没处理好幂等导致的。

関連記事

RAG 検索拡張生成実践:Embedding + Vector DB + ReRank

RAG 検索拡張生成実践:Embedding + Vector DB + ReRank

プロダクション級RAGをゼロから構築:文書チャンク戦略、Embeddingモデル選定、Vector DBフィルタ検索、RerankerによるTop-K精緻化、引用根拠とプロンプト圧縮。完全動作コード付き。

1 分で読了
FastAPI クイックスタート - 高性能 REST API の構築

FastAPI クイックスタート - 高性能 REST API の構築

FastAPI は現代の Python Web フレームワークの中でも傑出しており、型ヒントによる自動ドキュメント生成、非同期サポート、Node.js 並みのパフォーマンスを提供します。この実践ガイドでは、ルート定義、リクエスト検証、依存性注入、データベース統合を含む完全な CRUD API をゼロから構築します。

この記事は閲覧パスワードが必要です 4 分で読了
Python 並行処理:asyncio vs ThreadPool vs ProcessPool

Python 並行処理:asyncio vs ThreadPool vs ProcessPool

Pythonの3大並行モデル徹底比較:GIL下のThreadPool、GIL回避のProcessPool、超高IO向けasyncio。性能と複雑性の4次元デシジョンツリー。

1 分で読了
アルゴリズム:動的計画法 5大テンプレート

アルゴリズム:動的計画法 5大テンプレート

DP完全制覇:5ステップ共通フレームワーク+5大高頻度テンプレート:ナップサック、部分列、区間DP、ビットDP、木DPに典型コードと変形ポイント。

1 分で読了
Nuxt 3 応用 - SSR と SSG の深堀り解説

Nuxt 3 応用 - SSR と SSG の深堀り解説

Nuxt 3 は Vue 3 と Vite 上に構築され、新しいレンダリングエンジン Nitro を導入しました。この記事では、サーバーサイドレンダリング(SSR)、静的サイト生成(SSG)、インクリメンタル静的再生成(ISR)の3つのコアモードの動作原理、ユースケース、パフォーマンスの違いを深堀りします。

1 分で読了
CI/CD パイプライン設計:GitHub Actions + Cache + Matrix Build

CI/CD パイプライン設計:GitHub Actions + Cache + Matrix Build

本番級GitHub Actionsパイプライン設計:npm/pipキャッシュ、マルチバージョンNode/Python Matrix並列ビルド、Dockerレイヤーキャッシュ、Lint/Test/Build/Publish段階化、条件実行、環境マトリクス、失敗リトライとリリースゲートベストプラクティス。完全ワークフロー付き。

1 分で読了