5.4 审查并执行Alembic迁移
审查并执行Alembic迁移
模型文件描述“现在希望有什么结构”,迁移文件描述“数据库怎样从旧结构走到新结构”。两者不能互相替代:应用模型会继续变化,而已经执行过的迁移必须保留当时的字段、约束和数据处理语义。Alembic 的 --autogenerate 只能根据元数据和数据库状态生成候选差异,生成后必须人工审查,不能把候选文件直接当成正确迁移。
本节演示给 posts 增加可为空的 excerpt 字段。示例代码是静态迁移文件,可以在没有数据库的环境中做结构检查;真正的 upgrade、已有数据回填、downgrade 和表结构验证必须在独立的 PostgreSQL 练习数据库中执行。本节没有连接真实数据库,因此不把数据库迁移描述成已验证。
先分清四个名字
一份 Alembic revision 至少包含四类容易混淆的信息:
| 名称 | 作用 | 是否决定执行顺序 |
|---|---|---|
revision |
当前节点在迁移图中的唯一标识 | 是,配合 down_revision |
down_revision |
当前节点的父节点 | 是 |
| 文件名中的 slug | 对人可读的变更描述 | 否 |
| 文件名前的日期或时间 | 便于目录浏览 | 否 |
迁移执行顺序由 revision 图决定,不由文件名的日期排序决定。团队可以在 alembic.ini 中设置可读的文件名模板,例如:
[alembic]
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(rev)s_%%(slug)s
在 ConfigParser 配置中需要把 % 写成 %%;Alembic 实际使用时会把它还原成模板标记。文件名中的 slug 应说明变更,例如 2026_09_05_8b17f0c4e2a1_add_posts_excerpt.py,但不要根据文件名猜执行顺序。
一份静态 revision
下面的迁移只做一件事:增加可为空的文本字段。它没有导入应用的 Post 模型,也没有在运行时读取当前模型定义,因此未来模型变化不会改变历史迁移的含义。
<!-- file: ch05_migration/versions/20260905_8b17f0c4e2a1_add_posts_excerpt.py -->
"""add posts excerpt
Revision ID: 8b17f0c4e2a1
Revises: 4f2d9a11c0e7
Create Date: 2026-09-05 10:00:00
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
revision = "8b17f0c4e2a1"
down_revision = "4f2d9a11c0e7"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"posts",
sa.Column("excerpt", sa.Text(), nullable=True),
)
def downgrade() -> None:
# 删除列会丢失已经写入的 excerpt 内容,只能在确认可接受时执行。
op.drop_column("posts", "excerpt")
生成候选 revision 的命令通常是:
alembic revision --autogenerate -m "add posts excerpt"
审查时至少确认:生成的表名和列名是否正确、nullable 是否符合已有数据、唯一和外键约束是否有明确名字、是否误删或误改了字段、upgrade 与 downgrade 是否覆盖同一变更,以及迁移是否误用了当前应用模型。审查完成后,才在练习数据库中执行:
alembic upgrade head
alembic current
alembic downgrade -1
这些命令需要正确配置 alembic.ini、env.py 和 PostgreSQL URL。本地静态检查不会执行它们。
现有数据决定迁移步骤
给已有表增加非空字段不能只写 nullable=False。旧行没有这个值,数据库在执行 ADD COLUMN 时就可能无法满足约束。常见的分阶段方案是:
- 先增加允许
NULL的字段,或者提供一个明确的server_default。 - 使用静态、可审查的 SQL 为已有行回填,例如从
content截取摘要。 - 查询并确认没有剩余
NULL,再在另一份迁移中设置NOT NULL。 - 如果默认值只为历史回填服务,确认是否应该移除它,避免未来写入得到意外默认值。
例如,回填逻辑可以是迁移文件中的明确语句:
op.execute(
sa.text(
"UPDATE posts "
"SET excerpt = left(content, 160) "
"WHERE excerpt IS NULL"
)
)
这段 SQL 依赖 PostgreSQL 的 left() 函数;如果项目要求跨数据库,应改用目标数据库都支持的表达式,或在迁移中明确数据库方言。数据回填不是“自动生成”的安全结果,必须结合数据量、锁、执行时间和失败恢复方案审查。
字段重命名也不能简单地删除旧列再创建新列,否则数据会丢失。应使用 op.alter_column() 或目标数据库支持的重命名语句,并检查索引、外键、触发器、应用代码和回滚是否同步。旧列名、旧类型和转换规则应固定写在迁移文件里,不要从当前 ORM 模型动态推导。
用无数据库检查验证 revision 结构
下面的检查脚本读取上面的 revision 文件,用 AST 验证 revision 图和 upgrade/downgrade 入口,再用 Alembic 的 PostgreSQL 离线操作生成 SQL。它没有连接数据库,也不会修改任何表;因此只能验证文件结构和操作语句,不等价于 PostgreSQL 集成测试。
<!-- file: ch05_migration/check_revision.py -->
from __future__ import annotations
import ast
import io
import re
from pathlib import Path
import sqlalchemy as sa
from alembic.migration import MigrationContext
from alembic.operations import Operations
REVISION_FILE = (
Path(__file__).parent
/ "versions"
/ "20260905_8b17f0c4e2a1_add_posts_excerpt.py"
)
def assignment_value(tree: ast.Module, name: str) -> str | None:
for node in tree.body:
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == name:
value = ast.literal_eval(node.value)
return value if isinstance(value, str) else None
return None
def self_check() -> None:
source = REVISION_FILE.read_text(encoding="utf-8")
tree = ast.parse(source, filename=str(REVISION_FILE))
revision = assignment_value(tree, "revision")
down_revision = assignment_value(tree, "down_revision")
assert revision is not None and re.fullmatch(r"[0-9a-f]{12}", revision)
assert down_revision is not None
assert "from app.models" not in source
assert any(
isinstance(node, ast.FunctionDef) and node.name == "upgrade"
for node in tree.body
)
assert any(
isinstance(node, ast.FunctionDef) and node.name == "downgrade"
for node in tree.body
)
assert "op.add_column(" in source
assert 'op.drop_column("posts", "excerpt")' in source
output = io.StringIO()
context = MigrationContext.configure(
dialect_name="postgresql",
opts={"as_sql": True, "output_buffer": output},
)
operations = Operations(context)
operations.add_column("posts", sa.Column("excerpt", sa.Text(), nullable=True))
operations.drop_column("posts", "excerpt")
generated = output.getvalue().lower()
assert "alter table posts add column excerpt text" in generated
assert "alter table posts drop column excerpt" in generated
print("check_revision self-check passed; no database connection used")
if __name__ == "__main__":
self_check()
在包含 ch05_migration/ 的工作目录运行:
python ch05_migration/check_revision.py
--autogenerate 不是最终审查
Alembic 需要在 env.py 中得到目标 MetaData,然后将数据库当前结构和应用元数据做比较。它擅长生成明显的新增列、删除列或类型差异,但不能可靠推断所有重命名、数据回填、业务语义和数据库特有对象。生成文件后必须逐行检查,尤其是自动生成的删除操作和非空字段变更。
在本节锁定的 Alembic 1.19.2 中,具名 CHECK 约束的自动检测默认关闭;需要显式启用 alembic.ext.checkconstraint_byname 扩展后才能参与比较。即使启用,生成的约束变更仍然只是候选迁移,必须人工核对名称、已有数据和目标数据库行为。
迁移文件应保持静态:结构由代码固定,数据值可以在明确的回填步骤中读取数据库当前内容。不要在迁移中导入会持续变化的业务模型、请求配置或外部 API;否则同一份历史迁移在未来运行时可能得到不同结果。多个分支产生 head 时,还要先检查迁移图和合并策略,不要用文件名排序掩盖分支问题。
升级、降级和数据损失
upgrade 应把数据库从父 revision 推进到当前 revision,downgrade 应在可接受的范围内撤销结构变化。删除 excerpt 会丢失已经填写的摘要,因此这个 downgrade 不是无损操作;执行前必须确认备份、数据保留和回退计划。只在独立练习数据库中演示 downgrade -1,不要对生产数据未经审查地执行降级。
真实 PostgreSQL 验收至少包括:迁移前后的 alembic current、alembic upgrade head、关键表结构检查、已有数据回填数量、重复值和无效引用的失败结果,以及在可接受时执行的 downgrade。当前本节只运行了无数据库静态和离线 SQL 检查,以上 PostgreSQL 验收仍待在专用环境完成。
资料来源
- 主要参考:fastapi-best-practices 中文 README 的“迁移工具 Alembic”主题。本节保留静态迁移、描述性文件名和回滚审查方向,补充已有数据、非空字段和 PostgreSQL 验收边界。
- 官方文档:Alembic 教程、Alembic 自动生成迁移、Alembic 命令。

免费 AI IDE


更多建议: