K 的一隅

Python FastAPI 后端实战

改表不靠手写:Alembic 迁移流程

为什么需要 Alembic 管理 schema 版本、revision 链如何工作,以及从改 Model 到 upgrade 的概念流程(非操作手册)。

7 分钟阅读 更新于 2026-07-28
本文目录

开发库上 Base.metadata.create_all() 一时爽,上线后要给 itemsstock 列: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.ini

env.pySQLAlchemy Base.metadata 交给 Alembic,让它对比 Model 与数据库当前 版本

python
# alembic/env.py 核心片段(概念)
from shop.models import Base  # 确保所有 Model 已 import

target_metadata = Base.metadata

初始化与生成 revision 的命令此处不展开成运维手册;概念流程是:改 Model → 生成 revision → 人工审脚本 → upgrade

Revision 链与版本表

每个 migration 文件是一个 revision,通过 revision / down_revision 形成链表:

python
# 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 脚本;downgradedowngrade() 回退一步(能否无损取决于脚本质量)。

版本 语义:

  • head:链上最新 revision
  • base:空库起点
  • 分支 merge:多人并行 迁移 时需 merge revision(团队要有约定)

从 Model 变更到可部署脚本

推荐工作流(概念):

  1. 修改 ORM Model(如给 Itemstock: Mapped[int])。
  2. alembic revision --autogenerate -m "add item stock" 生成草稿。
  3. 人工审查 autogenerate 输出——它可能漏改索引、误删 、对 SQLite 生成不可执行 DDL。
  4. 本地 upgrade 验证,跑测试。
  5. 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

数据 迁移 示例(回填默认值):

python
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),避免连错库:

python
# 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 启动的关系

开发时有人图省事在 lifespancreate_all;与 Alembic 并存易 drift——迁移 创建,本地也应走 upgrade。测试库可在 fixture 里 upgrade headcreate_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 变更的思路

加列、加索引、改类型,线上流量不停时常见三步:

  1. 迁移 加可空列或带 server default——旧代码仍可跑。
  2. 发版应用,双写或回填数据。
  3. 迁移 设 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 有价值。

测试里跑迁移

python
# 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 漏列、错类型等部署问题。

小结

Alembicrevision 链管理 schema 版本;改 Model 后生成并审查 migration,各环境 upgrade 到同一 head。把 DDL 变更当作与功能代码同级的发布物,而不是 DBA 侧车。发布清单里同时打勾「代码 tag」与「DB head」。切勿手工改生产 而不补 revision。schema 与代码同仓同行。下一篇回到代码组织:业务逻辑 不应堆在 路由 里,服务分层 如何起步。