数据库也要写 Git:Python Alembic 新人入门指南
别再手改线上表了,用 Alembic 给数据库变更装上时间机器。

开场:代码有 Git,数据库怎么办?
新人第一次参与后端项目时,经常会遇到一个看似朴素、实则很危险的问题:
我在 SQLAlchemy 模型里加了一个字段,为什么数据库里没有?
于是有人打开数据库客户端,手写一条 ALTER TABLE。本地好了,测试环境忘了;测试环境好了,线上又不知道谁执行过;过了两周,另一个同事拉代码,数据库结构和代码完全对不上。
这就是数据库结构变更最容易失控的地方。
代码有 Git 记录每一次变化,数据库表结构也需要一套自己的版本管理。Alembic 做的就是这件事:它把「建表、加字段、改约束、删索引」这些数据库结构变化,沉淀成一份份可追踪、可回滚、可审查的迁移脚本。
一句话理解 Alembic:
Alembic 是 SQLAlchemy 生态里的数据库迁移工具,它让数据库结构像代码一样有版本、有历史、有回滚路径。
本文适合已经会一点 Python、SQLAlchemy,但还没系统用过 Alembic 的同学。读完你应该能独立完成一次从建表到上线前检查的完整迁移。
插图 1:Alembic 在项目里的位置
flowchart LR
A["SQLAlchemy Models<br/>你希望数据库长什么样"] --> B["Alembic Autogenerate<br/>对比模型和数据库"]
B --> C["Revision Script<br/>生成迁移脚本"]
C --> D["alembic upgrade head<br/>执行到最新版本"]
D --> E["Database Schema<br/>真实数据库结构变化"]
E --> F["alembic_version<br/>记录当前迁移版本"]
如果自媒体平台不支持 Mermaid,可以把这张图截图后作为正文配图上传。
1. 先建立一个正确的心智模型
很多新人把 Alembic 当成「自动建表工具」,这会埋坑。
更准确的心智模型是:
| 对象 | 作用 |
|---|---|
| SQLAlchemy Model | 描述应用希望拥有的表结构 |
| Alembic revision | 描述一次数据库结构变化 |
upgrade() | 数据库向前升级时执行什么 |
downgrade() | 数据库回退时执行什么 |
alembic_version 表 | 记录当前数据库走到了哪个迁移版本 |
你可以把它想成两条时间线:
flowchart TB
subgraph Code["代码时间线"]
C1["commit A"] --> C2["commit B"] --> C3["commit C"]
end
subgraph DB["数据库结构时间线"]
R1["revision A"] --> R2["revision B"] --> R3["revision C"]
end
C2 -. "通常伴随" .-> R2
C3 -. "通常伴随" .-> R3
代码提交解决「程序变了」的问题,迁移脚本解决「数据库也要跟着变」的问题。
2. 安装和初始化
以一个常见项目为例,先安装 Alembic:
pip install alembic sqlalchemy
如果你使用 PostgreSQL,还需要对应驱动,例如:
pip install psycopg[binary]
在项目根目录初始化:
alembic init alembic
执行后,你会看到类似结构:
.
├── alembic.ini
└── alembic
├── env.py
├── script.py.mako
└── versions
这些文件分别负责什么?
| 文件 | 你需要知道的事 |
|---|---|
alembic.ini | Alembic 的配置入口,常见配置是数据库连接地址 |
alembic/env.py | 每次执行迁移时都会运行的环境脚本 |
alembic/versions/ | 存放一份份迁移脚本 |
script.py.mako | 迁移脚本模板,一般新人阶段不用动 |
新人最需要关注两个地方:alembic.ini 和 env.py。
3. 配置数据库连接
最简单的方式是在 alembic.ini 里写:
sqlalchemy.url = postgresql+psycopg://user:password@localhost:5432/app_db
但真实项目里,不建议把账号密码直接写进仓库。更常见的做法是从环境变量读取:
# alembic/env.py
import os
database_url = os.getenv("DATABASE_URL")
if database_url:
config.set_main_option("sqlalchemy.url", database_url)
这样不同环境只要注入不同的 DATABASE_URL:
export DATABASE_URL="postgresql+psycopg://user:password@localhost:5432/app_db"
本地、测试、生产就能复用同一套迁移脚本,而不是维护三份配置。
4. 让 Alembic 认识你的 SQLAlchemy 模型
Alembic 的自动生成能力来自「对比」:
它拿数据库当前结构,去和 SQLAlchemy 的
MetaData比较,然后推断应该生成哪些迁移动作。
所以你必须在 env.py 中把项目的 Base.metadata 交给 Alembic。
假设你的模型是这样:
# app/models.py
from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50), nullable=False)
那么在 alembic/env.py 里找到:
target_metadata = None
改成:
from app.models import Base
target_metadata = Base.metadata
这一步非常关键。如果你忘了配置,--autogenerate 很可能生成空迁移,或者完全看不见你的模型变化。
如果你的模型分散在多个文件里,还要确保这些模型模块被导入过。因为 SQLAlchemy 只有在类被加载后,才会把表注册进 Base.metadata。
一个常见做法是:
# app/models/__init__.py
from app.models.user import User
from app.models.order import Order
__all__ = ["User", "Order"]
然后在 env.py 中导入:
from app.models import Base
5. 创建第一份迁移:让数据库真正建表
现在我们有了 User 模型,可以生成第一份迁移:
alembic revision --autogenerate -m "create users table"
你会在 alembic/versions/ 里看到一个新文件,名字类似:
20260604_abc123_create_users_table.py
打开后你会看到两个函数:
def upgrade() -> None:
op.create_table(
"users",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(length=50), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
def downgrade() -> None:
op.drop_table("users")
这里有一个新人必须记住的原则:
自动生成不是自动正确。每一份 migration 都要人工 review。
Alembic 官方文档也明确提醒,autogenerate 生成的是候选迁移,开发者需要检查和必要时手动修改。
确认迁移脚本没问题后,执行:
alembic upgrade head
head 表示迁移链的最新版本。执行成功后,数据库里会出现两样东西:
-
users表 -
alembic_version表
alembic_version 表通常只有一行,记录当前数据库处于哪个 revision。它是 Alembic 判断「下一步该执行哪些迁移」的依据。
你可以查看当前版本:
alembic current
也可以查看历史:
alembic history
6. 修改模型,再生成一次迁移
现在产品说:用户需要邮箱。我们改模型:
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50), nullable=False)
email: Mapped[str | None] = mapped_column(String(255), nullable=True)
再次生成迁移:
alembic revision --autogenerate -m "add user email"
生成的 upgrade() 可能类似:
def upgrade() -> None:
op.add_column("users", sa.Column("email", sa.String(length=255), nullable=True))
def downgrade() -> None:
op.drop_column("users", "email")
然后执行:
alembic upgrade head
这就是最典型的 Alembic 工作流:
sequenceDiagram
participant Dev as Developer
participant Model as SQLAlchemy Model
participant Alembic as Alembic
participant DB as Database
Dev->>Model: 修改模型
Dev->>Alembic: revision --autogenerate
Alembic->>DB: 读取当前结构
Alembic->>Model: 对比 metadata
Alembic-->>Dev: 生成迁移脚本
Dev->>Dev: review / 修改脚本
Dev->>Alembic: upgrade head
Alembic->>DB: 执行结构变更
7. 回滚:时间机器不是摆设
假设刚加的字段有问题,你想回退上一个版本:
alembic downgrade -1
回到最初状态:
alembic downgrade base
再升级到最新:
alembic upgrade head
但请注意,回滚不等于无损撤销。
如果一次迁移删除了字段:
op.drop_column("users", "email")
那字段里的数据可能就没了。downgrade() 能把字段加回来,但不能凭空恢复已经丢掉的数据。
所以涉及生产数据时,迁移脚本要像手术一样谨慎:
- 删除字段前先确认不再被代码读取
- 大表加字段要考虑锁表和执行时间
- 复杂数据迁移要先备份或灰度执行
-
downgrade()是否真的可用,要在团队里形成约定
8. 最容易踩的 6 个坑
坑 1:把 autogenerate 当神谕
Alembic 能检测很多常见变化,例如新增表、新增字段、字段 nullable 变化、部分索引和外键变化。
但它不是全知全能。尤其是重命名表、重命名字段,Alembic 通常无法知道你的真实意图。
比如你把:
name = mapped_column(String(50))
改成:
username = mapped_column(String(50))
Alembic 可能会认为:
删除 name 字段
新增 username 字段
这和「重命名字段」完全不是一回事。正确做法通常是手动改迁移脚本:
op.alter_column("users", "name", new_column_name="username")
记住:
只要涉及重命名,不要闭眼执行自动生成的迁移。
坑 2:模型没导入,生成空迁移
你明明加了表,revision --autogenerate 却生成一个空文件。常见原因是模型类没有被导入,导致 Base.metadata 里根本没有那张表。
排查顺序:
-
env.py 是否设置了target_metadata = Base.metadata - 模型模块是否被导入
- 命令运行时的 Python 环境是否和项目一致
-
DATABASE_URL是否指向了你以为的数据库
坑 3:忘记给约束命名
约束包括 unique、foreign key、check 等。如果不命名,不同数据库可能会自动生成不同名字,后续迁移就不稳定。
推荐在 SQLAlchemy 里配置命名规范:
from sqlalchemy import MetaData
from sqlalchemy.orm import DeclarativeBase
naming_convention = {
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s",
}
class Base(DeclarativeBase):
metadata = MetaData(naming_convention=naming_convention)
这会让迁移脚本更稳定,也更适合团队协作。
坑 4:多人开发产生多个 head
两位同事同时从同一个 revision 拉分支,各自生成一份迁移。合并代码后,Alembic 可能出现多个 head。
查看:
alembic heads
如果确实出现分叉,可以创建 merge revision:
alembic merge heads -m "merge migration heads"
这不一定改变数据库结构,但会把迁移历史重新合成一条可以继续前进的路径。
坑 5:生产环境直接跑未审查迁移
迁移脚本也是代码,而且是会改变数据资产的代码。上线前至少检查:
- 是否会删表、删字段
- 是否会给大表加非空字段
- 是否会创建耗时索引
- 是否包含数据迁移
- 是否需要先发布兼容代码
- 是否能在预发环境完整执行
坑 6:只会 upgrade,不会 check
Alembic 提供了 check 命令,可以检测当前模型变化是否还会生成新的迁移操作:
alembic check
它很适合放进 CI,用来提醒团队:你改了模型,但可能忘了提交 migration。
9. 新人常用命令速查表
| 命令 | 作用 |
|---|---|
alembic init alembic | 初始化 Alembic 目录 |
alembic revision -m "message" | 创建一份空迁移 |
alembic revision --autogenerate -m "message" | 根据模型和数据库差异生成迁移 |
alembic upgrade head | 升级到最新版本 |
alembic downgrade -1 | 回退一个版本 |
alembic current | 查看当前数据库版本 |
alembic history | 查看迁移历史 |
alembic heads | 查看当前迁移链的 head |
alembic merge heads -m "message" | 合并多个 head |
alembic check | 检查是否存在未生成的迁移 |
10. 推荐团队工作流
如果你是新人,只要记住下面这条流程,基本就不会离谱:
flowchart TD
A["修改 SQLAlchemy Model"] --> B["生成迁移<br/>revision --autogenerate"]
B --> C["人工 review 迁移脚本"]
C --> D{"是否涉及危险操作?"}
D -- "否" --> E["本地 upgrade head"]
D -- "是" --> F["和同事确认方案<br/>必要时拆成多步迁移"]
F --> E
E --> G["本地验证业务功能"]
G --> H["提交 model + migration"]
H --> I["CI 执行 alembic check / 测试"]
更成熟一点的团队,可以继续加上这些规范:
- 每次模型结构变化必须伴随迁移脚本
- PR review 时必须看 migration 文件
- 禁止把数据库密码写进
alembic.ini - 生产迁移前先在预发执行
- 大表结构变更单独评审
- 数据迁移和结构迁移尽量拆清楚
11. 一个真正实用的判断标准
很多新人会问:到底什么时候要写 Alembic migration?
一个简单判断:
只要你的代码变更要求数据库结构也发生变化,就应该有 migration。
包括但不限于:
- 新增表
- 删除表
- 新增字段
- 修改字段类型
- 修改 nullable
- 新增索引
- 新增唯一约束
- 新增外键
- 表或字段重命名
反过来,如果你只是改业务逻辑、改接口返回、改 Python 函数,不涉及数据库结构,就不需要 migration。
结尾:Alembic 不是高级魔法,而是工程卫生
Alembic 最重要的价值,不是少写几条 SQL。
它真正解决的是团队工程里的三个问题:
- 数据库结构变化有记录
- 不同环境可以按同一套步骤升级
- 出问题时至少知道从哪里回看和回退
新人刚开始用 Alembic,最容易追求「一条命令自动搞定」。但真正靠谱的用法是:
让 Alembic 帮你生成初稿,让工程师负责判断和审查。
数据库是系统里最不能随便试错的地方。把迁移脚本写清楚、审清楚、跑清楚,本质上是在给未来的自己和队友留路。
下一次你改 SQLAlchemy 模型时,别急着打开数据库客户端手写 ALTER TABLE。
先问自己一句:
这次变化,数据库的 Git 记录写了吗?