之前的痛还记得吗:改了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步:初始化与接线
bash
pip install alembic
alembic init migrations
生成alembic.ini + migrations/目录。接线两步,让Alembic认识你的模型和数据库:
📖 接线①:让它知道"表结构长什么样"
migrations/env.py里把target_metadata = None改为:
python
import os
from core.models import Base
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设置环境变量
bash
export DATABASE_URL="postgresql+psycopg://postgres:你的密码@localhost:5432/daily"
# 永久生效写进~/.zshrc
三、第1步:第一个迁移------把存量表纳入管理
之前的表是create_all手工建的,Alembic还不认识它们。先autogenerate一次"对齐":
bash
alembic revision --autogenerate -m "initial models"
alembic upgrade head
🔍 autogenerate的原理
📌 对比
models.py(你声明的结构)与数据库(实际的结构),把差异写成迁移脚本。
打开生成的脚本看一眼------users / rss_sources / favorites三张缺的表被补上了,articles因为已存在而被跳过。
四、第2步:变更迭代------加字段的全流程
现在体验真正的高潮:给模型加两个字段。
① 改models.py
python
class User(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) # 新增
...
class Article(Base):
...
view_count: Mapped[int] = mapped_column(server_default=0) # 新增
Tips: 注意view_count字段设置的是server_default=0,如果只设置default=0,这是Python层面插入新行进行的设置,不会进入DDL,如果数据库有字段,使用alembic进行字段的新增会导致字段非空而出现的错误。而设置server_default会写进DDL,不会出现错误。
② 生成第二个迁移
bash
alembic revision --autogenerate -m "add user email and article view_count"
打开生成的脚本核对------op.add_column两条,正是差异所在:
python
def upgrade() -> 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))
def downgrade() -> None:
op.drop_column("articles", "view_count")
op.drop_column("users", "email")
③ 执行
bash
alembic upgrade head
docker exec -it pg-daily psql -U postgres -d daily -c "\d articles"
# view_count列已经在那了
④ 回滚演示(诚实版)
bash
alembic downgrade -1
字段消失。再upgrade head回来------但注意:
🚨 回滚再升级,字段里的数据不会凭空回来。Alembic管结构,不自动管数据。
这就是为什么重要变更前要备份数据库。
五、第3步:手写迁移------autogenerate管不了的事
autogenerate只会对比表结构。两类场景需要手写迁移:
🎬 场景一:改列名
autogenerate会把它理解成"删旧列 + 加新列 "------数据全丢。安全做法是手写迁移,三步安全舞:
python
def upgrade() -> 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")
🎬 场景二:批量数据修正
比如把所有空摘要填默认值:
python
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 |
七、验收清单
bash
1. alembic upgrade head → 迁移成功执行
2. psql \d articles → view_count列已存在
3. alembic history → 看到两条迁移记录
4. alembic downgrade -1 → 字段消失;upgrade head → 字段回归(但数据不回来)
5. alembic current → 显示当前版本号
6. 全程无报错后提交Git
bash
git add .
git commit -m "Alembic接管表结构:两个迁移 + 回滚演示"
八、常见报错:这6个,Alembic的标配(重点!)
① FAILED: Can't locate revision identified by 'a1b2c3'
🔍 原因 :数据库的alembic_version里记录的版本号,在migrations/目录里找不到------删过旧迁移、或换了分支。
✅ 解法:
bash
alembic history # 核对历史
确需重置就清空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
🔍 原因:你和同事各生成了一个迁移,历史分叉了。
✅ 解法:
bash
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仓库地址】