本文目录
开发库上 Base.metadata.create_all() 一时爽,上线后要给 items 加 stock 列:Staging 手工 ALTER 过了,生产忘了跑,新代码一部署全线 500。Alembic 解决的是 schema 版本 与代码同行——每次 表 结构变更是一条可审查、可回滚、可重放的 migration 记录,而不是 DBA 聊天记录里的 SQL。
为什么不用 create_all 打天下
| 方式 | 适用 | 问题 |
|---|---|---|
create_all | 本地从零搭库 | 不 ALTER 已有 表;不记录 版本 |
| 手工 SQL | 紧急 hotfix | 环境 drift,难复现 |
| Alembic 迁移 | 团队与多环境 | 需维护 revision 链 |
迁移 管的是结构(DDL),不是业务数据行——尽管 revision 里也可以 op.execute("UPDATE ...") 做数据回填,但要格外谨慎。
Alembic 在项目里的位置
常见目录:
shop/
alembic/
env.py # 连接 URL、target_metadata
versions/
20260728_001_add_item_stock.py
alembic.inienv.py 把 SQLAlchemy Base.metadata 交给 Alembic,让它对比 Model 与数据库当前 版本:
# alembic/env.py 核心片段(概念)
from shop.models import Base # 确保所有 Model 已 import
target_metadata = Base.metadata初始化与生成 revision 的命令此处不展开成运维手册;概念流程是:改 Model → 生成 revision → 人工审脚本 → upgrade。
Revision 链与版本表
每个 migration 文件是一个 revision,通过 revision / down_revision 形成链表:
# alembic/versions/20260728_add_stock.py(简化示例)
from alembic import op
import sqlalchemy as sa
revision = "20260728_add_stock"
down_revision = "20260727_initial"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column("items", sa.Column("stock", sa.Integer(), nullable=False, server_default="0"))
def downgrade() -> None:
op.drop_column("items", "stock")Alembic 在库里维护 alembic_version 表,存当前 head revision id。upgrade 应用 pending 脚本;downgrade 按 downgrade() 回退一步(能否无损取决于脚本质量)。
版本 语义:
- head:链上最新 revision
- base:空库起点
- 分支 merge:多人并行 迁移 时需 merge revision(团队要有约定)
从 Model 变更到可部署脚本
推荐工作流(概念):
- 修改 ORM Model(如给
Item加stock: Mapped[int])。 alembic revision --autogenerate -m "add item stock"生成草稿。- 人工审查 autogenerate 输出——它可能漏改索引、误删 表、对 SQLite 生成不可执行 DDL。
- 本地
upgrade验证,跑测试。 - CI/CD 或发布 Job 对目标环境执行
alembic upgrade head。
autogenerate 是 diff 助手,不是无脑真理。Renamed column 常被识别成 drop+add,会丢数据——应手写 op.alter_column 或分步 迁移。
upgrade 与 downgrade 的直觉
- upgrade:向前演进 schema,与发版代码期望的结构一致。
- downgrade:撤销最近一次(或指定)revision,用于失败回滚演练。
生产 downgrade 并不总安全:删列后 downgrade 加回空列,数据已丢。团队更应依赖 forward-fix revision 而非频繁 downgrade。
数据 迁移 示例(回填默认值):
def upgrade() -> None:
op.add_column("items", sa.Column("stock", sa.Integer(), nullable=True))
op.execute("UPDATE items SET stock = 0 WHERE stock IS NULL")
op.alter_column("items", "stock", nullable=False)
def downgrade() -> None:
op.drop_column("items", "stock")多环境与配置
Alembic 应读与应用相同的 DATABASE_URL(来自 env),避免连错库:
# alembic/env.py 片段
from shop.core.config import settings
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)Docker 镜像里打包 alembic/ 与 versions/;容器启动命令可以是「先 upgrade head 再启 Uvicorn」,或 K8s init container 专跑 迁移——选型属部署篇,本篇只强调:版本 脚本应随代码一起发布。
与 FastAPI 启动的关系
开发时有人图省事在 lifespan 里 create_all;与 Alembic 并存易 drift——表 由 迁移 创建,本地也应走 upgrade。测试库可在 fixture 里 upgrade head 或 create_all 于隔离 SQLite,但 CI 应接近生产路径。
团队协作:谁改 revision、何时 merge
迁移 文件像代码一样走 PR:DBA 与后端共同 review DDL。两人同时 autogenerate 可能产生两个 down_revision 指向同一父 版本 的分叉,需要 alembic merge 生成合并 revision。约定「动 Model 的人负责当天合并 迁移 链」比事后解冲突省时间。
命名规范建议:revision id 可短 hash,但 -m 消息写人话:add item stock column,方便 git log 与 版本 目录搜索。不要把环境相关数据(生产-only 账号)写进 migration——revision 应在 staging 可重放。
零 downtime 变更的思路
加列、加索引、改类型,线上流量不停时常见三步:
- 迁移 加可空列或带 server default——旧代码仍可跑。
- 发版应用,双写或回填数据。
- 再 迁移 设 NOT NULL / 删旧列。
Alembic 只提供执行顺序容器;策略在团队 playbook 里。生产环境很少用 downgrade 回滚 schema,更多是本地试验「这条 revision 脚本是否写对」。
与 ORM Model 的双向同步
autogenerate 对比 Base.metadata 与 live database。若有人手工改库没写 revision,下次 autogenerate 会生成「修复 drift」的脚本——可能是 drop 你不知哪来的索引。纪律是:所有 DDL 都应进入 版本 链。开发环境允许 drop 库重来;生产不允许。
空 表 初始化:第一个 revision 往往是 create_table 全套;之后只做增量。保留初始 revision 不要 squash 成黑盒,除非全员重建环境——历史 版本 对 audit 有价值。
测试里跑迁移
# tests/conftest.py 概念片段
from alembic import command
from alembic.config import Config
def run_migrations(database_url: str) -> None:
cfg = Config("alembic.ini")
cfg.set_main_option("sqlalchemy.url", database_url)
command.upgrade(cfg, "head")集成测试前 upgrade head,保证 表 结构与 Model 一致;比 create_all 更接近部署。速度要求高时,session 级 SQLite 内存库 + 一次 迁移 仍比每条用例连远程 Postgres 快。
revision 文件里写什么、不写什么
upgrade() 里应是可重复执行的 DDL/DML 步骤,避免依赖「当前机器上碰巧存在的文件路径」。种子数据(初始管理员账号)可以单独 data migration revision,或与首个 表 revision 分开,方便测试库跳过灌数。
版本 链是审计日志:何时加了 GDPR 相关列、何时删了废弃 表,git blame revision 文件即可追溯。不要把临时 hotfix SQL 只贴在工单里——工单会丢,migration 在仓库里。
发版顺序建议:先跑 迁移 再发应用(加列可空),或应用与 迁移 同时发但 迁移 先行几分钟——取决于变更是否 backward compatible。团队应写一页 runbook,而不是每次上线临时猜。
staging 数据库应定期从生产脱敏灌数,但 Alembic head 必须一致——否则 staging 通过、生产 upgrade 失败。把 alembic current 纳入发布 checklist,与 git tag 并列。
空库从零启动:先 alembic upgrade head 再启动 FastAPI,比 create_all 更接近线上。本地删库重来时,删文件后同样跑 upgrade,不要手工建 表 再让 autogenerate 困惑「表已存在」。
版本 号与发版标签解耦:git tag v1.2.0 对应某次 migration head,但 revision id 是 Alembic 自己的 hash——文档里写清「1.2.0 需要 DB revision ≥ xyz」,运维不用猜。迁移 失败回滚应用版本时,也要确认 DB schema 是否仍兼容旧代码,必要时 forward-fix 而非盲目 downgrade。
squash 历史 migration 仅适合全员可重建的空项目;已有生产的链不要压成单文件——失去逐步 升级 能力。新环境仍可用 alembic upgrade head 一次跑完所有 revision,不必 squash 来「变快」。
手工改 revision 文件后,在空 Docker 库跑一遍 upgrade + downgrade + 再 upgrade,验证脚本可逆性(在允许 downgrade 的前提下)。迁移 代码 review 与 路由 代码 review 同等重要——它改的是共享状态,全 应用 实例共用同一 schema 版本。
列 rename 在生产上几乎总要「加新列、双写、迁数据、删旧列」多 revision 完成——autogenerate 单步 rename 风险高。把 迁移 当产品发布的一部分排期,而不是发版前夜十五分钟补 DDL。
alembic stamp head 只改 版本 表不跑 SQL——用于已有手工 schema 与代码对齐的极端情况,误用会制造「以为已 迁移、实际缺列」的幽灵状态。除非 DBA 书面确认 schema 一致,否则别 stamp 跳过 revision。
常见误解
误解一:「autogenerate 完直接上线。」 必须 code review revision 文件。
误解二:「改 DB 手工更快,Alembic 以后再补。」 后补往往对不上已 drift 的环境,重建链很痛苦。
误解三:「downgrade 等于时间倒流。」 只保证结构步骤可逆,不保证数据。
误解四:「migration 里可以 import 业务 service。」 revision 应自洽、可重复;复杂逻辑用纯 SQL 或独立脚本,减少 import 副作用。
误解五:「本地 upgrade 过了,staging 会自动跟上。」 每个环境各自执行 upgrade;发版流水线里要有显式 迁移 步骤,不能假设。
误解六:「revision 越多越好说明项目成熟。」 链过长增加 review 成本;合理粒度是「一次可描述的 schema 变更一条 revision」。
误解七:「测试库可以不跑 迁移。」 集成测试越接近 upgrade head 路径,越能 caught 漏列、错类型等部署问题。
小结
Alembic 用 revision 链管理 schema 版本;改 Model 后生成并审查 migration,各环境 upgrade 到同一 head。把 DDL 变更当作与功能代码同级的发布物,而不是 DBA 侧车。发布清单里同时打勾「代码 tag」与「DB head」。切勿手工改生产 表 而不补 revision。schema 与代码同仓同行。下一篇回到代码组织:业务逻辑 不应堆在 路由 里,服务 与 分层 如何起步。