别再手改线上表了,用 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.iniAlembic 的配置入口,常见配置是数据库连接地址
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 表示迁移链的最新版本。执行成功后,数据库里会出现两样东西:

  1. users
  2. 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 里根本没有那张表。

排查顺序:

  1. env.py​ 是否设置了 target_metadata = Base.metadata
  2. 模型模块是否被导入
  3. 命令运行时的 Python 环境是否和项目一致
  4. 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。

它真正解决的是团队工程里的三个问题:

  1. 数据库结构变化有记录
  2. 不同环境可以按同一套步骤升级
  3. 出问题时至少知道从哪里回看和回退

新人刚开始用 Alembic,最容易追求「一条命令自动搞定」。但真正靠谱的用法是:

让 Alembic 帮你生成初稿,让工程师负责判断和审查。

数据库是系统里最不能随便试错的地方。把迁移脚本写清楚、审清楚、跑清楚,本质上是在给未来的自己和队友留路。

下一次你改 SQLAlchemy 模型时,别急着打开数据库客户端手写 ALTER TABLE

先问自己一句:

这次变化,数据库的 Git 记录写了吗?

参考资料