之前的痛还记得吗:改了Model加字段,运行报
table posts has no column named xxx——因为create_all()只建新表、不改旧表。
今天给数据库装上git:表结构变更进入版本管理,可升级、可回滚、可追溯。🎯本篇产出:Alembic接管早报站的表结构,两个真实迁移的生成与执行,一次回滚演示。含代码约80行。
📌 太长不看版(给想快速上手的你)
| 项目信息 | 一句话说明 |
|---|---|
| 本篇目标 | 用Alembic管理数据库表结构变更 |
| 代码行数 | ~80行(迁移脚本+配置) |
| 依赖 | alembic(已装SQLAlchemy) |
| 核心功能 | autogenerate迁移 + 升级 + 回滚 |
| 跑起来的命令 | alembic revision --autogenerate -m "描述"→alembic upgrade head |
| 核心知识点 | 迁移脚本=数据库的commit、autogenerate原理、手写迁移 |
| 做完你能得到 | 数据库有了git:改表结构不再删库重建 |
🚨核心认知:迁移脚本只增不改。已经执行过的迁移脚本等于历史——改历史等于篡改,Alembic会用版本号对不上来抗议(第八节①)。
一、心智模型:迁移脚本 = 数据库的commit
Git是代码的安全网。Alembic把同样的安全网装到数据库上:
| Git(代码) | Alembic(表结构) |
|---|---|
| 每次改动一个commit | 每次变更一个迁移脚本 |
git log看历史 | alembic history看历史 |
| HEAD指针指向最新提交 | alembic_version表里的版本号就是HEAD |
git revert回退 | alembic downgrade回退 |
| 分支合并 | alembic merge(多人协作冲突时用) |
📌迁移脚本只增不改——改历史等于篡改。
二、第0步:初始化与接线
pipinstallalembic alembic init migrations生成alembic.ini+migrations/目录。接线两步,让Alembic认识你的模型和数据库:
📖 接线①:让它知道"表结构长什么样"
migrations/env.py里把target_metadata = None改为:
importosfromcore.modelsimportBase config=context.config# 密钥军规:连接串走环境变量,不写死在配置里config.set_main_option("sqlalchemy.url",os.environ.get("DATABASE_URL","postgresql+psycopg://postgres:你的密码@localhost:5432/daily"))...target_metadata=Base.metadata📖 接线②:让它知道"连哪个库"
上面的sqlalchemy.url已完成。
💡 注意
postgresql+psycopg://:SQLAlchemy需要指明驱动。
🔧 macOS设置环境变量
exportDATABASE_URL="postgresql+psycopg://postgres:你的密码@localhost:5432/daily"# 永久生效写进~/.zshrc三、第1步:第一个迁移——把存量表纳入管理
之前的表是create_all手工建的,Alembic还不认识它们。先autogenerate一次"对齐":
alembic revision--autogenerate-m"initial models"alembic upgradehead🔍 autogenerate的原理
📌对比
models.py(你声明的结构)与数据库(实际的结构),把差异写成迁移脚本。
打开生成的脚本看一眼——users/rss_sources/favorites三张缺的表被补上了,articles因为已存在而被跳过。
四、第2步:变更迭代——加字段的全流程
现在体验真正的高潮:给模型加两个字段。
① 改models.py
classUser(Base):__tablename__="users"id:Mapped[int]=mapped_column(primary_key=True)username:Mapped[str]=mapped_column(unique=True)email:Mapped[str|None]=mapped_column(default=None)# 新增...classArticle(Base):...view_count:Mapped[int]=mapped_column(server_default=0)# 新增Tips: 注意view_count字段设置的是server_default=0,如果只设置default=0,这是Python层面插入新行进行的设置,不会进入DDL,如果数据库有字段,使用alembic进行字段的新增会导致字段非空而出现的错误。而设置server_default会写进DDL,不会出现错误。
② 生成第二个迁移
alembic revision--autogenerate-m"add user email and article view_count"打开生成的脚本核对——op.add_column两条,正是差异所在:
defupgrade()->None:op.add_column("users",sa.Column("email",sa.String(),nullable=True))op.add_column('articles',sa.Column('view_count',sa.Integer(),server_default='0',nullable=False))defdowngrade()->None:op.drop_column("articles","view_count")op.drop_column("users","email")③ 执行
alembic upgradeheaddockerexec-itpg-daily psql-Upostgres-ddaily-c"\d articles"# view_count列已经在那了④ 回滚演示(诚实版)
alembic downgrade-1字段消失。再upgrade head回来——但注意:
🚨回滚再升级,字段里的数据不会凭空回来。Alembic管结构,不自动管数据。
这就是为什么重要变更前要备份数据库。
五、第3步:手写迁移——autogenerate管不了的事
autogenerate只会对比表结构。两类场景需要手写迁移:
🎬 场景一:改列名
autogenerate会把它理解成"删旧列 + 加新列"——数据全丢。安全做法是手写迁移,三步安全舞:
defupgrade()->None:# ① 加新列op.add_column("articles",sa.Column("author",sa.String(80)))# ② 把旧列数据搬过去op.execute("UPDATE articles SET author = title WHERE author IS NULL")# ③ 确认无误后(可分两次部署),再删旧列op.drop_column("articles","title_copy")🎬 场景二:批量数据修正
比如把所有空摘要填默认值:
op.execute("UPDATE articles SET summary = '(暂无摘要)' WHERE summary = ''")📌 纪律
🚨手写迁移必须先在本地/测试库跑一遍,确认无误再上生产——迁移脚本也是代码,也要走"先测再上"。
六、命令速查(贴墙系列)
| 命令 | 作用 |
|---|---|
alembic init migrations | 初始化(一次性) |
alembic revision --autogenerate -m "描述" | 对比模型生成迁移 |
alembic upgrade head | 升级到最新 |
alembic upgrade +1/downgrade -1 | 前进一步 / 回退一步 |
alembic history/alembic current | 看历史 / 看当前版本 |
alembic heads | 多人协作出现分叉时看heads |
七、验收清单
1. alembic upgradehead→ 迁移成功执行2. psql\d articles → view_count列已存在3. alembichistory→ 看到两条迁移记录4. alembic downgrade-1→ 字段消失;upgradehead→ 字段回归(但数据不回来)5. alembic current → 显示当前版本号6. 全程无报错后提交Gitgitadd.gitcommit-m"Alembic接管表结构:两个迁移 + 回滚演示"八、常见报错:这6个,Alembic的标配(重点!)
①FAILED: Can't locate revision identified by 'a1b2c3'
🔍 原因:数据库的alembic_version里记录的版本号,在migrations/目录里找不到——删过旧迁移、或换了分支。
✅ 解法:
alembichistory# 核对历史确需重置就清空alembic_version表重新stamp(谨慎)。
②Target database is not up to date
🔍 原因:有新迁移脚本还没执行。
✅ 解法:alembic upgrade head。
📌 这也是部署流程的一部分:上线先upgrade(service前置命令)。
③ autogenerate生成了空迁移
🔍 原因:env.py没接target_metadata——Alembic不知道你的模型。
✅ 解法:回到第2节接好线。
📌生成空迁移时,先怀疑接线,再怀疑模型。
④ 改列名被autogenerate拆成"加列 + 删列"
🔍 原因:autogenerate只对比结构,不读你的心思。
✅ 解法:人工核对生成的脚本,改成第5节的"三步安全舞"。
📌autogenerate是草稿,人工核对才是定稿。
⑤ 多人协作出现两个head
🔍 原因:你和同事各生成了一个迁移,历史分叉了。
✅ 解法:
alembic heads# 看到两个头alembic merge heads-m"merge"# 生成合并迁移⑥ 生产库upgrade卡住 / 大表加列很慢
🔍 原因:大表加列会锁表(不同数据库行为不同)。
✅ 解法:
- 低峰期执行;
- 表特别大时了解数据库的在线加列方案。
🚨迁移脚本上生产前,永远先在测试库跑一遍。
九、课后练习
| # | 练习 | 难度 | 提示 |
|---|---|---|---|
| 1 | 加列实战:给Article加tags列,autogenerate + upgrade,psql验证 | ⭐⭐ | Mapped[str] = mapped_column(default="") |
| 2 | 回滚演练:downgrade -1再upgrade head,观察psql里列的消失与回归 | ⭐⭐ | 体会"结构回来、数据不回来" |
| 3 | 数据迁移:给favorites表加note列,并写一条op.execute给旧记录回填 | ⭐⭐⭐ | 手写迁移 |
| 4(选做) | CI接入:GitHub Actions里跑alembic upgrade head && pytest | ⭐⭐⭐⭐ | 迁移和测试一起进流水线 |
📦 配套代码
迁移脚本与env.py配置已上传Git(python_daily/):【gitee仓库地址】