Alembic 数据库迁移
SQLAlchemy 官方数据库迁移工具,用于 Python 项目的数据库 schema 版本管理。
一、核心概念
- 迁移文件:
alembic/versions/下每个.py文件是一个版本,包含upgrade()和downgrade() - 版本链:每个迁移记录
down_revision(依赖的上一个版本),形成线性链 alembic_version表:数据库中的单行表,记录当前已应用的版本号- 每次
upgrade head会把未应用的迁移按链顺序执行,并更新alembic_version
二、常用命令
cdserver uv run alembic current# 查看当前版本uv run alembichistory# 查看迁移链uv run alembic check# 检查模型与数据库是否一致uv run alembic revision--autogenerate-m"描述"# 自动生成迁移uv run alembic upgradehead# 应用所有未执行迁移uv run alembic upgrade<rev># 升级到指定版本uv run alembic downgrade-1# 回滚一个版本uv run alembic upgradehead--sql# 输出 SQL 而不执行(> 文件导出)三、标准工作流
- 修改 SQLModel 模型定义
uv run alembic revision --autogenerate -m "描述变更"- 审查生成的迁移文件(autogenerate 非 100% 可靠)
uv run alembic upgrade head应用- 产出增量 SQL:
uv run alembic upgrade <上条rev>:head --sql > sql/migration_<日期>.sql
四、server_default 规则
数据库默认值必须在模型中声明(Column(server_default=...)或sa_column_kwargs),不能只在迁移文件中手写:
| 场景 | 写法 |
|---|---|
| 时间戳 | server_default=text("now()") |
| 布尔 false | server_default="false" |
| 字符串 | server_default=text("'value'")— 必须用text()防止引号被错误转义 |
五、迁移压缩(Squash)
5.1 Alembic 无内置 squash 命令
官方没有提供类似 Djangosquashmigrations的功能。
alembic merge:用于合并分叉分支(多个 head),创建指向多父节点的交汇迁移,形成菱形结构。这是在加文件而非减文件,与压缩无关。--splice(1.18.5 新增):仅允许merge操作非 head 节点,仍然是分支合并,不是压缩。
Alembic 维护者 zzzeek 在 Discussion #1259 中给出的 squash 方法是:手动 autogenerate 空库法。全量压缩和部分合并原理相同,仅参数不同。
5.2 全量压缩(base → head)
整个链替换为一个初始迁移:
| 步骤 | 操作 |
|---|---|
| 1 | 确保模型与数据库无差异(alembic check) |
| 2 | 创建空库,设DATABASE_URL指向空库 |
| 3 | alembic revision --autogenerate -m "initial schema"→down_revision = None |
| 4 | 空库上upgrade head验证 |
| 5 | 导出init.sql,删旧迁移文件 |
| 6 | 开发库UPDATE alembic_version SET version_num = '<新ID>' |
5.3 部分合并(链中间段)
只合并中间 B→C→D 为 B’,保留首尾 A 和 E:
A → B → C → D → E (head) └── B' ──┘ (B' 替代 B/C/D,E 接在 B' 后)关键技巧:空库 schema 跑到 D 状态,但 alembic_version 回退到 A,autogenerate 就会把 A→D 的全部差异生成到一个迁移里。
# 1. 空库先跑到合并起点 A$env:DATABASE_URL="postgresql+asyncpg://.../arbai_squash"uv run alembic upgrade A# 2. 再跑到合并终点 D(schema = D 状态)uv run alembic upgrade D# 3. 回退版本标记到 Apsql-darbai_squash-c"UPDATE alembic_version SET version_num = 'A';"# 4. autogenerate 生成 B'(对比"A 标记" vs "D 实际 schema")uv run alembic revision--autogenerate-m"squash B C D"# 5. 编辑 B':down_revision = 'A'# 6. 编辑 E:down_revision = '<B'的新ID>'# 7. 删除 B、C、D 旧文件# 8. 开发库升级版本标记(如果当前 > D)六、生产环境部署
Alembic 适合开发环境使用,不适合直接在生产环境执行:
- Alembic 依赖项目代码和 SQLModel 模型导入,与项目耦合太重,生产部署不应附带完整开发依赖
- 直接连生产库执行 autogenerate 有风险(误判、连接中断等),且难以审计"实际会执行什么 SQL"
- 运维团队通常只需要执行确定的 DDL,不需要了解 Python 迁移框架
开发时通过 Alembic 生成增量 SQL 文件,生产环境直接执行 SQL:
首次部署(完整建库):
init.sql后续升级(增量迁移,按日期顺序执行):
migration_20260616.sql migration_20260620.sql七、常见坑
7.1 手写迁移与模型不一致
旧手动迁移的 DDL 和 SQLModel 模型定义不一致是常见问题。应始终以模型为唯一数据源,autogenerate 生成迁移。
7.2 字符串 server_default 引号转义
server_default="'active'"→ autogenerate 可能丢失外层引号 → 生成server_default='active'→ SQL 变成DEFAULT active(被当成标识符而非字符串)。
正确做法:模型中使用server_default=text("'active'"),autogenerate 能正确保留。
7.3--sql输出中文乱码
设置环境变量:
PYTHONUTF8=1PYTHONIOENCODING=utf-8 uv run alembic upgradehead--sql>output.sql7.4 autogenerate 使用 sqlmodel.AutoString
autogenerate 会对Field(max_length=N)的字段生成sqlmodel.sql.sqltypes.AutoString,迁移文件中需import sqlmodel。
7.5 布尔 server_default 不需要 text()
server_default="false"对 Boolean 列是正确的 —false在 PostgreSQL 中是布尔字面量,不会被当成标识符。