搞数据库最怕什么?不是写SQL,不是调索引,而是上线前才发现:本地加了个字段,开发库加了个表,预发布环境改了个约束,生产环境还停在三天前的老结构。这时候手动补ALTER TABLE脚本,补着补着就乱了,少跑一个文件就是线上事故。我算是在这上面吃过不少亏,后来把Alembic用顺了才真正解脱——它是SQLAlchemy官方维护的数据库迁移工具,专门负责把数据库结构的每次变更记录成可追溯、可回滚、可多人协作的版本脚本。这篇文章不打算翻文档念概念,就按我自己实际使用和踩坑的顺序,把Alembic从初始化到上生产环境的完整链路讲透,适合正在用SQLAlchemy但还没上迁移工具的人,也适合已经在用却经常被autogenerate坑到的人。
1. 为什么数据库结构变更需要“版本管理”
1.1 没有迁移工具的时候,到底有多难受
先说个最典型的场景。项目一开始,数据库就一张users表,所有人建表都靠手写一个schema.sql,然后手动往开发库、测试库、生产库挨个执行一遍。刚开始表少还能应付,但业务跑了三个月之后呢?users表加了手机号字段,orders表拆出了order_items,中间还改过两次索引。问题就来了:每个人的本地库结构都不一样,谁也说不清线上库到底执行了哪些脚本、缺了哪个变更。最经典的就是“我本地跑得好好的,拉下来部署到服务器就报字段不存在”,一查,原来是有人直接在数据库客户端里手动加列,没把变更同步给任何人。
还有更痛苦的版本:把变更脚本按日期命名,2024_01_15_add_mobile.sql、2024_02_01_create_orders.sql,看起来挺有规划,但脚本之间互相依赖,执行顺序错了就报错。更别提跨环境了,开发库执行到第10个脚本,测试库只执行到第7个,预发布环境第8个脚本失败了一次但建了一半表,这种状态根本没法用代码去追踪。你需要的不是一堆SQL文件,而是一条明确的“状态链”:当前数据库处于哪个版本、下一个该执行哪个变更、执行完了怎么回退。
1.2 Alembic解决的核心问题
Alembic把数据库迁移这件事做成了类似Git的版本管理模型。每一次结构变更都是一个revision(版本),它知道自己从哪个版本升级而来(down_revision)、升级到哪一步(upgrade)、回滚时做什么(downgrade)。Alembic在数据库里建了一张alembic_version表,专门记录当前处于哪个版本。这样升级时执行alembic upgrade head,它会沿着版本链,把缺失的迁移全部补齐,回滚时执行alembic downgrade -1,只会退回上一步,方向上完全受控。
相比直接维护SQL脚本,Alembic最大的价值有两个。第一是幂等和顺序确定,整个迁移链是线性的,每个节点的前驱后继都明确写在脚本里,不存在“不知道跑没跑过”的问题。第二是支持代码生成,它会扫描你的SQLAlchemy模型(Base.metadata),和当前数据库结构做对比,自动生成upgrade/downgrade代码,省掉手写大部分建表改表的工作。
1.3 和Django migrate、Flyway等方案怎么选
用Django的人会问:Django自带的migrate不是挺好用吗?确实好用,但它是跟Django ORM绑死的。如果你的项目是Python生态但用SQLAlchemy,或者项目里有几套模型定义混着用,Alembic就是官方钦定的选择,因为它和SQLAlchemy是同一批作者贡献的,对MetaData的解析最准确。Flyway是Java生态常用的迁移工具,它也能管SQL脚本,但不会帮你从ORM模型生成迁移代码,而且协同开发模型时会有额外心智负担。
我的建议很直接:只要是SQLAlchemy项目,直接上Alembic,不用犹豫。它跟Flask-SQLAlchemy、FastAPI+SQLAlchemy、甚至完全不用ORM、只靠原生SQL管理的项目都能配合。哪怕你连SQLAlchemy都没用,只要你的Python服务连了PostgreSQL/MySQL,一样可以用Alembic按版本目录管理纯SQL脚本,它的灵活性其实被很多人低估了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Alembic核心机制拆解:从revision链到env.py
2.1 一套必须讲清楚的概念
第一次接触Alembic的人,往往被几个名词绕晕:revision、down_revision、base、head、branch、merge。其实可以把它想成一群版本节点排列成的链。
revision:当前这个迁移版本的唯一ID,一般是一串哈希值,比如1a2b3c4d5e6f,当然也可以手动改成可读性强的标识。down_revision:当前版本的前一个版本ID。它把节点串成链,定义“从哪个版本升级到当前版本”。base:链的起点,没有前驱节点的版本。head:某个链的末端,没有后继节点的版本。正常项目里应该只有一个head,如果出现多个head,说明迁移链分叉了。branch/merge:分支和合并。当两个人都改了数据库结构并各自生成了迁移,就会产生分支,需要用alembic merge合并节点。
一个迁移文件打开后,里面固定是upgrade()和downgrade()两个函数。upgrade里写变更前进一步该做什么,downgrade里写回退一步该做什么。Alembic默认要求这两个函数成对且互为逆操作,这点是底线:能升就应该能降,不能光写升级不写回滚,不然出问题的时候,你只能站在坏掉的数据库面前发呆。
2.2 迁移脚本目录长什么样
用alembic init alembic初始化后,会生成一个alembic目录和alembic.ini配置文件。我贴一个标准结构:
text复制project/
├── alembic/
│ ├── env.py
│ ├── script.py.mako
│ └── versions/
│ ├── 1a2b3c4d5e6f_create_users_table.py
│ ├── 7f8a9b0c1d2e_add_mobile_to_users.py
│ └── ...
├── alembic.ini
└── app/
└── models.py
versions/目录里放的就是每个迁移文件,文件名一般是<revision_id>_<描述>.py。script.py.mako是生成新迁移文件的模板,可以改模板来定制每个迁移文件的额外注释或公共逻辑。env.py是整个迁移过程的执行入口,它负责把配置加载起来、连上数据库、把Base.metadata传给Alembic以便做自动比对。
2.3 env.py到底在干什么
对新手来说,env.py是最大的神秘区域。别慌,它其实就干了以下几件事:
- 读取
alembic.ini里的sqlalchemy.url,或者从应用配置里拿数据库连接串。 - 导入你的SQLAlchemy模型定义,拿到
Base.metadata。这样--autogenerate时才有东西可以对比。 - 调用Alembic的
MigrationContext,让迁移进入“离线”或“在线”模式。在线模式是直接对数据库执行DDL,离线模式是生成SQL脚本,方便DBA人工审核后再执行。 - 提供
render_item、process_revision_directives这类钩子,可以自定义迁移脚本生成的行为。
很多项目用Flask或FastAPI,它们的数据库连接串是写在应用配置里的,不会放进alembic.ini。规避这个矛盾的方式很常见:在env.py里从应用配置导入URL,而不是用配置文件里的值。
python复制from app.config import DATABASE_URL
config.set_main_option("sqlalchemy.url", DATABASE_URL)
但这里有个小坑:env.py里写这种导入时,要保证应用依赖包能被找到,否则会抛出导入错误。我习惯在env.py顶部先把项目根目录加入sys.path,避免不同环境下模块导入路径不一致。
2.4 upgrade和downgrade为什么要成对维护
Alembic默认会在你--autogenerate时同时生成upgrade和downgrade,但手动写迁移时,很多人会偷懒只写upgrade,不写downgrade。短期看没问题,因为大部分时候你只用upgrade,从来不回滚。但一旦某个版本上线后导致线上数据异常,你需要快速回退到上一个稳定结构时,没有downgrade就真的手足无措了——你可以用op.execute把变更SQL反着写一遍,但那时候数据库里的数据状态已经变了,远没有预先设计好逆操作来得稳妥。
所以我的建议是:每个迁移都当成“可能要回滚”来写。加列时,upgrade里add_column,downgrade里配套drop_column;建表时,upgrade里create_table,downgrade里drop_table;改枚举类型时更要注意,upgrade里把旧枚举改成新枚举,downgrade里必须能改回来,否则数据库迁移脚本在团队协作中容易被同事一用就报错。
3. 实操:把Alembic接进一个真实项目
3.1 安装与初始化
假设你已经有项目了,依赖管理用的是pip或poetry。安装Alembic很简单,它会在环境里带上对SQLAlchemy的依赖,但如果你项目里已经用了SQLAlchemy 2.x版本,建议显式装一下确保版本不冲突。
bash复制pip install alembic
然后切换到项目根目录,执行初始化命令:
bash复制alembic init alembic
这条命令会创建alembic目录以及alembic.ini。此时先别急着改数据库,打开alembic.ini,找到sqlalchemy.url配置项:
ini复制sqlalchemy.url = postgresql://user:password@localhost:5432/mydb
如果你的环境变量管理比较规范,我更推荐不在配置文件里写死密码,而是在env.py里动态获取。因为密码写进alembic.ini很容易被误提交到Git仓库,泄露风险很大。我见过不只一个项目把线上库密码留在alembic.ini里传到了GitLab,这种安全隐患能避免就避免。
3.2 修改env.py,让Alembic认识你的模型
初始化完成后,必须修改env.py,否则--autogenerate会告诉你“没有找到任何模型”。核心是这两段:
第一段,导入Base和模型定义。以SQLAlchemy 2.x为例,你的模型通常是这样组织的:
python复制# app/db.py
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
然后在env.py里:
python复制from app.db import Base
from app import models # 确保所有模型类都被加载到Base.metadata
target_metadata = Base.metadata
这里有个最容易踩的坑:如果你在app/models/里定义了模型,但env.py里只导入了Base而没有导入具体模型模块,那Base.metadata里只有空壳,Alembic自然扫描不到任何表。必须确保所有模型模块都被import一次,注册到Base.metadata后,autogenerate才能生成对应的建表代码。
第二段,改数据库URL的来源。如果项目用的是FastAPI,配置通常在app/config.py里:
python复制from app.config import settings
config.set_main_option("sqlalchemy.url", settings.database_url)
如果配置里还用到SSL、连接池参数,可以塞进URL里,也可以设置connect_args,但最简单的方法是直接用一个可用的连接串。
3.3 用autogenerate生成第一个迁移
改完env.py,就可以用自动生成迁移了:
bash复制alembic revision --autogenerate -m "create users table"
此时Alembic会对比Base.metadata里定义的表结构和当前数据库实际结构。如果数据库里啥都没有,生成的迁移文件就会包含建表语句;如果数据库里已经有表了,Alembic会识别哪些表缺失、哪些列缺失、哪些索引缺失,生成对应的add_column、create_index等操作。
我强烈建议在生成后打开文件检查一遍,不要盲目执行。因为autogenerate是“尽力而为”,它不能帮你识别列重命名(它会把旧列删掉再加新列),也不能识别条件约束的意图。我后面会专门说这个问题。
生成的迁移文件大概长这样:
python复制"""create users table
Revision ID: 1a2b3c4d5e6f
Revises:
Create Date: 2025-01-15 10:30:00.123456
"""
from alembic import op
import sqlalchemy as sa
revision = "1a2b3c4d5e6f"
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"users",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("email", sa.String(length=255), nullable=False),
sa.Column("hashed_password", sa.String(length=255), nullable=False),
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now()),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("email"),
)
def downgrade() -> None:
op.drop_table("users")
3.4 执行升级、查看历史、回滚的日常命令
升级到最新版本:
bash复制alembic upgrade head
升级到指定版本:
bash复制alembic upgrade 1a2b3c4d5e6f
回退一步:
bash复制alembic downgrade -1
回退到指定版本:
bash复制alembic downgrade 7f8a9b0c1d2e
查看当前数据库所处版本:
bash复制alembic current
查看历史版本链:
bash复制alembic history
查看迁移链的末端:
bash复制alembic heads
如果是第一次使用,我建议先把upgrade head跑一遍,然后执行current确认版本号,再执行downgrade -1验证回滚,再执行upgrade head恢复。整套练一遍,对Alembic的“版本可控”会有很直接的体感。
3.5 团队协作时,迁移文件怎么进Git
先说结论:alembic/versions/目录必须进Git,alembic.ini按需进(最好去掉密码后再进),数据库本身永远不进Git。每个迁移文件只应该被“追加”式修改,一旦某个迁移已经提交并被同事应用过,就不要再回头改这个迁移文件的内容,否则会出现状态对不上。如果发现刚才生成迁移文件时写错了代码,而它还没有被任何环境执行过,直接改掉再提交是可以的;但已经执行过的迁移,正确做法是再写一个新的迁移去“修正”它,而不是改历史。
多人协作时很容易遇到另一个问题:两个人各自从同一个旧head生成新迁移,结果仓库里出现了两个head。这时候要用alembic merge来合并分支:
bash复制alembic merge -m "merge two heads" <revision1> <revision2>
merge会生成一个特殊的迁移节点,它有两个down_revision,把两条链并成一条。这个操作本身不改变表结构,只是让迁移链重新回归线性。合并后记得提示团队其他成员拉取最新代码,把本地alembic upgrade head到最新状态。
4. 手工迁移:autogenerate覆盖不了的操作
4.1 重命名列和重命名表,别指望autogenerate
autogenerate本质上是“结构比对器”,它比对的是两个结构的差异。在数据库里,把users.mobile列改名为users.phone,对Alembic来说看起来就是一个列没了、另一个列出现了,于是它的建议往往是:删除mobile,新增phone。如果真照着这个迁移执行,那列里的数据就全没了。
所以,遇到重命名操作,正确方式是用alter_column或别名实现,并且在upgrade里显式写清楚:
python复制def upgrade() -> None:
op.alter_column("users", "mobile", new_column_name="phone")
def downgrade() -> None:
op.alter_column("users", "phone", new_column_name="mobile")
重命名表同理:
python复制def upgrade() -> None:
op.rename_table("old_name", "new_name")
def downgrade() -> None:
op.rename_table("new_name", "old_name")
这些操作autogenerate是识别不出来的,它只会建议你删表和建表。所以每次动列名、表名前,先手写迁移文件,别让工具替你乱来。
4.2 批量数据变更,用op.execute还是ORM?
数据库迁移不只是改结构,经常还要先改数据。比如给老用户统一设置默认头像,或者把历史的status从字符串改成数字枚举。两者实现的路径不同:
- 纯SQL更新,数据量大的时候效率最高,适合简单的批量赋值。
- 如果逻辑复杂,比如要遍历每行调用第三方API或做计算,那就在迁移里用ORM批量处理。
最简单直接的写法是op.execute:
python复制def upgrade() -> None:
op.execute("UPDATE users SET status = 'active' WHERE status IS NULL")
如果要在迁移里跑ORM代码,Alembic官方其实不建议直接使用全局的Session,因为迁移执行时模型类和当前最新代码可能不完全匹配。更稳妥的方式是迁移文件里用sa.text()执行原生SQL,或者临时定义一个轻量的ORM模型来绑定到已有的表,而不要引用业务模块里的模型类。否则你以后改了模型字段,旧迁移再跑一遍时,ORM映射对不上就直接报错。
举一个临时模型的例子:
python复制from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from alembic import op
def upgrade() -> None:
bind = op.get_bind()
Session = sessionmaker(bind=bind)
session = Session()
# 直接使用SQL表达,避免依赖业务模型
session.execute(
sa.text("UPDATE orders SET discount = 0 WHERE discount IS NULL")
)
session.commit()
session.close()
4.3 加列时设置server_default,是老库线上变更的救命稻草
给一张已经有很多数据的表加非空列,直接执行add_column会失败,因为旧行在新列上没有值。常规做法是给列加server_default,让数据库给旧行填一个默认值:
python复制def upgrade() -> None:
op.add_column(
"users",
sa.Column("timezone", sa.String(length=50), nullable=False, server_default="Asia/Shanghai"),
)
def downgrade() -> None:
op.drop_column("users", "timezone")
注意,加上server_default后,表的DDL层面会保留默认值约束。如果你不想让这个默认值永久保留在表结构里,可以分两步:先add_column加带默认值的列,等数据稳定后再用alter_column去掉默认值。
python复制def upgrade() -> None:
op.add_column(
"users",
sa.Column("timezone", sa.String(length=50), nullable=False, server_default="Asia/Shanghai"),
)
op.alter_column("users", "timezone", server_default=None)
这样做的好处是:旧行的字段有值,新行插入时又不强制依赖这个默认值,而是由应用层传入。
4.4 用batch_alter_table处理SQLite的特殊限制
如果你项目里用的是SQLite(开发环境、小工具、嵌入式场景很多见),一定要小心。SQLite对表结构修改的支持非常弱,删列、改列、加约束时,传统ALTER TABLE根本做不到。Alembic提供了batch_alter_table,它会在内部模拟“重建表”:先把旧表复制成临时表、创建新表、拷贝数据、删除旧表、重命名新表。
python复制def upgrade() -> None:
with op.batch_alter_table("items") as batch_op:
batch_op.add_column(sa.Column("category", sa.String(length=50), nullable=True))
用batch_alter_table的迁移,upgrade和downgrade两个方向都要保持一致。而且批量操作模式下,很多操作会变得比想象的慢,因为要整表重建,表数据量大时务必谨慎。
4.5 分支合并与多环境部署时怎么安排
项目跑久了,alembic/versions/目录下会有几十个文件。分支合并不只是在多人协作时出现,有时候同一个数据库服务要支持不同版本的同步,比如微服务架构下,订单库和用户库各自使用一套迁移脚本,但它们共享同一套Alembic依赖时,也容易产生多head。
解决思路是在多个服务共用的那个Alembic项目里,让每个服务维护自己的branch_labels,然后统一做一次merge。另一种方案是每个服务独立维护一套迁移脚本目录,互不干扰,代价是不能共享同一套alembic_version表。我更推荐后一种方式,结构清晰,不容易互相污染。
多环境部署(开发、测试、预发布、生产)时,只要保证目标库的alembic_version和代码里的迁移链一致,执行alembic upgrade head就可以。但你需要特别注意:迁移脚本的执行和业务代码发布是有顺序讲究的。如果你的迁移包含删除列或删除表这种破坏性操作,务必先发布新代码(旧代码不依赖被删除的字段),再执行迁移;反过来,如果是加列操作,可以先顺滑地执行迁移再发布新代码。这种顺序问题,Alembic本身不管你,要在CI/CD流水线里自行定义好。
5. 生产环境踩坑实录与排查技巧
5.1 autogenerate检测出莫名其妙的变更怎么办
最常见的情况:你只是改了业务模型里一个字段的注释,autogenerate却生成了一堆索引或者约束变更,有时候甚至把整个表要重建一遍。原因往往是模型定义里某些属性没有显式写全,autogenerate和数据库实际结构存在“隐式差异”。比如模型里写了index=True,但某个旧环境里索引名和默认命名规则不一致,Alembic会认为这是一个新的索引。又比如枚举类型,PostgreSQL的ENUM类型和SQLAlchemy的Enum之间,名称不一致就会反复检测到变更。
排查思路是:先看alembic history确认最近的合法变更,再跑alembic revision --autogenerate时,把生成的迁移文件拿来和真实变化的比对,确认没有多余内容后再执行。如果就是不想让autogenerate管某些表,可以在env.py里配置include_object回调函数,过滤掉指定的表。
python复制def include_object(object, name, type_, reflected, compare_to):
if type_ == "table" and name == "audit_log":
return False
return True
context.configure(
connection=connection,
target_metadata=target_metadata,
include_object=include_object,
)
5.2 外键约束命名不一致导致的诡异报错
外键是数据库里最麻烦的对象的之一,Alembic在比对时经常卡在外键约束名上。数据库A里外键名是fk_users_orders_user_id,模型里SQLAlchemy自动生成的约束名却是fk_orders_user_id_users,两者对不上,autogenerate就会重复生成删除和新增外键的迁移,执行时甚至可能因为外键依赖关系而失败。
解决办法是在模型定义里显式写好外键约束名:
python复制__table_args__ = (
ForeignKeyConstraint(
["user_id"], ["users.id"], name="fk_orders_user_id"
),
)
这样Alembic每次比对时使用的名称都是确定且统一的,不会再被自动命名的差异性坑到。类似的,给主键、唯一约束也最好起明确的名字,尤其在项目规模和团队人数上去之后,数据库约束命名的一致性会省掉大量排查时间。
5.3 大表加列加索引,直接执行有可能锁表
生产环境的大表动结构一定要小心。MySQL下用ALTER TABLE加列,有些版本会锁住整表的写操作;PostgreSQL的ADD COLUMN本身是秒级完成的,但如果加上默认值,或者直接创建索引,也会造成长时间锁表。
Alembic本身不处理这类在线DDL问题,它只负责执行。所以我在生产环境的经验是:迁移文件如果涉及大表操作,要结合具体数据库特性来优化。比如在PostgreSQL里可以:
sql复制-- 先加列,不带默认值
ALTER TABLE orders ADD COLUMN region VARCHAR(20);
-- 再回填数据,分批次更新,避免长事务
-- 最后再创建索引时使用 CONCURRENTLY
CREATE INDEX CONCURRENTLY idx_orders_region ON orders(region);
Alembic迁移里也可以用op.execute执行这些原生SQL,只是权限上要有单独的处理。如果公司的DBA制度严格,那更常见的做法是用Alembic生成SQL文件,交给DBA审核后在维护窗口执行,而不是让应用直接连生产库跑迁移。
5.4 alembic_version表被误删或者版本落后
有人可能遇到过这种情况:手动恢复数据库备份后,alembic_version表没了,或者版本号和代码里对不上。此时alembic upgrade head会尝试把所有迁移重新执行一遍,大概率会报“表已存在”之类的错误。
处理思路是先把版本标记到当前数据库结构所对应的版本,再考虑是否继续升级。如果确定数据库结构已经是某个历史版本,可以用alembic stamp命令直接标记:
bash复制alembic stamp 7f8a9b0c1d2e
stamp会让Alembic认为当前库已处于该版本,但不会真实执行任何迁移。这个命令在“从备份恢复库但迁移状态丢失”的场景中非常有用,但前提是你得非常确定数据库的实际结构和标记的版本一致,否则后续upgrade会基于错误基线继续,结果更糟。
5.5 并发部署时多个实例同时跑迁移怎么办
如果有多个应用实例同时启动,并且它们都会自动执行alembic upgrade head,那数据库端会收到多个实例的并发迁移请求。Alembic自身没有分布式锁,两个实例同时执行同一个迁移时,就会出现重复建表、重复加列等竞态问题。更麻烦的是,两个实例可能各自生成新的迁移head,版本状态就会乱掉。
我的应对策略很明确:迁移一定要从CI/CD流程里的单一任务执行,不要让应用实例自己跑迁移。CI里设置一个“migrate”步骤,用alembic upgrade head跑完,再启动新版本的应用实例。如果历史遗留项目已经有多个实例自动迁移了,短期内可以给迁移命令包一层分布式锁,比如先用Redis实现互斥,再用Alembic执行,但长期还是要收敛到“单一执行者”方案。
5.6 变更迁移脚本导致历史版本不可复现
这是团队协作里最隐蔽的坑。迁移文件一旦被某个环境执行过,它的内容就像是烙印一样,不能再改动了。因为它执行后会改变数据库的实际结构,而alembic_version表只记录版本号,不记录这个版本当初的确切行为。你改了旧迁移文件,新环境执行的是改动后的版本,老环境已经被执行过,仍然保持旧结构,两边就从这个版本开始分叉了。
如果非要修正旧迁移,一个更稳妥的做法是:写一个新的迁移去“覆盖”掉之前错误的变更。虽然历史链上会多一个看起来没必要的版本,但所有环境执行的都是同一条确定性的链,不会再错位。这也是Alembic设计上最核心的思维方式:历史不可变,变更靠新节点。
6. 用顺了之后,值得养成的几个习惯
最后聊几个我自己的习惯,不一定所有人适用,但确实是几次踩坑之后沉淀下来的。
第一,每次在alembic/versions/下生成新文件后,第一件事不是执行,而是打开文件再读一遍。很多错误——比如字段名拼错、nullable写反、server_default漏掉——都是这一步发现的。尤其用了--autogenerate,它给的“答案”和真实意图之间可能差着十万八千里,不检查就执行等于把命交给一个编译器。
第二,尽量保证upgrade和downgrade都验证过再提交。本地先在开发库跑upgrade head,确认结构正确后,再跑downgrade -1,再跑回upgrade head。这一套流程虽然多花两分钟,但能把90%的迁移脚本问题扼杀在提交之前,特别推荐新手养成这个肌肉记忆。
第三,迁移脚本里涉及大量数据更新时,优先写op.execute原生SQL,少引业务模型。原因我在前面提到过:迁移是长期有效的,而业务模型的字段会变。今天你引用了User.email,三个月后User表改名或者字段改名,这个旧迁移再跑就会崩。用原生SQL虽然写起来多几个字母,但它只依赖数据库列名本身,稳定性高出太多。
第四,加列时给列一个合适的server_default非常值得。刚开始图省事,加列时nullable=True直接加,结果线上数据质量立刻变得不可控,很多该填的字段全是NULL。后来我养成了习惯:新加的列尽量定义nullable=False,并带上server_default。宁可加完之后再专门跑一个迁移去掉默认值,也比让新列悬空要好。
Alembic说到底不复杂,理解了revision链和upgrade/downgrade这两个核心,加上几个生产环境常用的命令,日常运维就完全够用了。真正的复杂度从来不在工具本身,而在那些“批量改数据”“大表加索引”“多环境并发”的真实场景里。希望这一篇能把你在使用Alembic路上可能踩的坑提前填平一大部分。
