【FastAPI筑基-Day15】别再手动改表结构!Alembic 迁移实战:升级、回滚、版本管理
专栏:FastAPI零基础后端实战系列
标签:FastAPI、Alembic、SQLAlchemy、数据库迁移、版本管理
前置学习:Day14 SQLAlchemy ORM 数据库实战
一、前言
Day14 我们用 SQLAlchemy 打通了 ORM 模型和 CRUD,但建表用的是 Base.metadata.create_all()。它有一个致命缺陷:只会新建表,不会更新已存在的表结构。业务迭代要加字段、改类型、删字段时,改 model 代码数据库不会自动同步,手动去 Navicat 里改表风险极高,团队多人协作更是灾难。
本篇引入 Alembic ------ SQLAlchemy 官方配套的数据库迁移工具,给数据库做"版本管理":
- 记录每一次表结构变更历史
- 自动生成迁移脚本,可升级 / 回滚数据库版本
- 团队协作保证开发、测试、生产环境表结构一致
文章会在真实 MySQL 环境完整演示:安装、初始化、生成迁移脚本、升级、加字段、回滚全流程,所有命令输出均为本地实测。学完本篇,你的项目表结构变更就彻底告别"手动改库"了。
二、为什么需要 Alembic
打个比方:create_all() 像只会盖新房、不会搞装修的施工队------房子不存在就盖一栋,房子已经存在就袖手旁观,哪怕你说"我要加个阳台"它也无动于衷。
Alembic 则是带施工档案的装修队:每次改动都生成一份施工单(迁移脚本),档案里记着当前装修到哪个版本(版本号),想加阳台按施工单执行,装错了还能按档案回滚到上一版。
完整工作流:
#mermaid-svg-5CrW4QEZi9fBgzL5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5CrW4QEZi9fBgzL5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5CrW4QEZi9fBgzL5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5CrW4QEZi9fBgzL5 .error-icon{fill:#552222;}#mermaid-svg-5CrW4QEZi9fBgzL5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5CrW4QEZi9fBgzL5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5CrW4QEZi9fBgzL5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5CrW4QEZi9fBgzL5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5CrW4QEZi9fBgzL5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5CrW4QEZi9fBgzL5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5CrW4QEZi9fBgzL5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5CrW4QEZi9fBgzL5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5CrW4QEZi9fBgzL5 .marker.cross{stroke:#333333;}#mermaid-svg-5CrW4QEZi9fBgzL5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5CrW4QEZi9fBgzL5 p{margin:0;}#mermaid-svg-5CrW4QEZi9fBgzL5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5CrW4QEZi9fBgzL5 .cluster-label text{fill:#333;}#mermaid-svg-5CrW4QEZi9fBgzL5 .cluster-label span{color:#333;}#mermaid-svg-5CrW4QEZi9fBgzL5 .cluster-label span p{background-color:transparent;}#mermaid-svg-5CrW4QEZi9fBgzL5 .label text,#mermaid-svg-5CrW4QEZi9fBgzL5 span{fill:#333;color:#333;}#mermaid-svg-5CrW4QEZi9fBgzL5 .node rect,#mermaid-svg-5CrW4QEZi9fBgzL5 .node circle,#mermaid-svg-5CrW4QEZi9fBgzL5 .node ellipse,#mermaid-svg-5CrW4QEZi9fBgzL5 .node polygon,#mermaid-svg-5CrW4QEZi9fBgzL5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5CrW4QEZi9fBgzL5 .rough-node .label text,#mermaid-svg-5CrW4QEZi9fBgzL5 .node .label text,#mermaid-svg-5CrW4QEZi9fBgzL5 .image-shape .label,#mermaid-svg-5CrW4QEZi9fBgzL5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-5CrW4QEZi9fBgzL5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5CrW4QEZi9fBgzL5 .rough-node .label,#mermaid-svg-5CrW4QEZi9fBgzL5 .node .label,#mermaid-svg-5CrW4QEZi9fBgzL5 .image-shape .label,#mermaid-svg-5CrW4QEZi9fBgzL5 .icon-shape .label{text-align:center;}#mermaid-svg-5CrW4QEZi9fBgzL5 .node.clickable{cursor:pointer;}#mermaid-svg-5CrW4QEZi9fBgzL5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5CrW4QEZi9fBgzL5 .arrowheadPath{fill:#333333;}#mermaid-svg-5CrW4QEZi9fBgzL5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5CrW4QEZi9fBgzL5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5CrW4QEZi9fBgzL5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5CrW4QEZi9fBgzL5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5CrW4QEZi9fBgzL5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5CrW4QEZi9fBgzL5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5CrW4QEZi9fBgzL5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5CrW4QEZi9fBgzL5 .cluster text{fill:#333;}#mermaid-svg-5CrW4QEZi9fBgzL5 .cluster span{color:#333;}#mermaid-svg-5CrW4QEZi9fBgzL5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5CrW4QEZi9fBgzL5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5CrW4QEZi9fBgzL5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-5CrW4QEZi9fBgzL5 .icon-shape,#mermaid-svg-5CrW4QEZi9fBgzL5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5CrW4QEZi9fBgzL5 .icon-shape p,#mermaid-svg-5CrW4QEZi9fBgzL5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5CrW4QEZi9fBgzL5 .icon-shape .label rect,#mermaid-svg-5CrW4QEZi9fBgzL5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5CrW4QEZi9fBgzL5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5CrW4QEZi9fBgzL5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5CrW4QEZi9fBgzL5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
修改 models 模型
autogenerate 生成迁移脚本
人工检查脚本 SQL
upgrade head 升级数据库
发现问题?
downgrade 回滚
脚本提交 git
三、环境安装
bash
pip install alembic sqlalchemy pymysql python-dotenv
说明:
pymysql是 MySQL 驱动,如果改用 SQLite 可以不需要;python-dotenv用来从.env文件读数据库配置,不硬编码密码。
MySQL 里先建好库:
sql
CREATE DATABASE fastapi_demo DEFAULT CHARACTER SET utf8mb4;
四、项目目录结构与基础代码
沿用 Day14 的工程化思路,本篇项目结构:
text
fastapi_demo/
├── main.py
├── .env # 数据库配置(环境变量)
├── requirements.txt
├── app/
│ ├── __init__.py
│ ├── database.py # sqlalchemy 数据库连接
│ ├── models.py # ORM 模型
│ └── schemas.py
└── alembic/ # alembic init 后生成
└── alembic.ini # alembic 配置文件
1. app/database.py 数据库配置
python
import os
from dotenv import load_dotenv
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker
# 加载 .env 环境变量
load_dotenv()
DB_URL = os.getenv("DATABASE_URL")
engine = create_engine(DB_URL)
SessionLocal = sessionmaker(autoflush=False, bind=engine)
Base = declarative_base()
# 获取数据库会话
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
2. .env 配置文件
text
DATABASE_URL=mysql+pymysql://root:123456@127.0.0.1:3306/fastapi_demo?charset=utf8mb4
3. app/models.py ORM 模型(初版)
python
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime
from app.database import Base
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, nullable=False, comment="用户名")
password_hash = Column(String(255), nullable=False, comment="密码哈希")
create_time = Column(DateTime, default=datetime.now, comment="创建时间")
五、初始化 Alembic 并修改两处配置
项目根目录执行:
bash
alembic init alembic
实测输出:
text
Creating directory alembic ... done
Creating directory alembic/versions ... done
Generating alembic/README ... done
Generating alembic/env.py ... done
Generating alembic/script.py.mako ... done
Generating alembic.ini ... done
生成 alembic/ 文件夹(迁移脚本放在它的 versions/ 下)和 alembic.ini。
1. 修改 alembic.ini:不硬编码密码
找到 sqlalchemy.url,注释掉默认值,改为在 env.py 里从环境变量读取:
ini
# 不硬编码数据库账号密码,改为在 env.py 中从环境变量读取
# sqlalchemy.url = driver://user:pass@localhost/dbname
2. 修改 alembic/env.py【重点】
这一步是关键:让 alembic 识别我们项目的 ORM 模型,并拿到数据库连接。完整修改后的 env.py:
python
import os
import sys
from logging.config import fileConfig
from alembic import context
from dotenv import load_dotenv
from sqlalchemy import engine_from_config, pool
# 项目根目录加入 sys.path,保证能导入 app 包
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 加载 .env
load_dotenv()
# 导入自己项目的模型模块(显式导入,保证模型注册到 Base.metadata)
from app import models
target_metadata = models.Base.metadata
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# 从环境变量读取数据库连接,不在 ini 里硬编码
config.set_main_option("sqlalchemy.url", os.getenv("DATABASE_URL"))
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode."""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode."""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
两个关键点:
target_metadata = models.Base.metadata:alembic 靠对比这份元数据和数据库真实表结构的差异,自动生成迁移脚本from app import models:必须显式导入模型模块 ,否则模型没注册到Base.metadata,autogenerate 什么都检测不到(这是新手最高频的坑)
六、生成第一次迁移脚本并升级
bash
alembic revision --autogenerate -m "init user table"
-m:备注,描述本次迁移做了什么--autogenerate:自动对比模型与数据库差异,生成脚本
实测输出:
text
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'user'
INFO [alembic.autogenerate.compare] Detected added index 'ix_user_id' on '('id',)'
Generating alembic/versions/269d4e00bcd3_init_user_table.py ... done
Detected added table 'user' 说明模型差异被正确识别,alembic/versions/ 下生成了迁移脚本。打开看一眼自动生成的核心内容:
python
# revision identifiers, used by Alembic.
revision: str = '269d4e00bcd3'
down_revision: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(length=50), nullable=False, comment='用户名'),
sa.Column('password_hash', sa.String(length=255), nullable=False, comment='密码哈希'),
sa.Column('create_time', sa.DateTime(), nullable=True, comment='创建时间'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('username')
)
op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False)
def downgrade() -> None:
"""Downgrade schema."""
op.drop_index(op.f('ix_user_id'), table_name='user')
op.drop_table('user')
upgrade 是向前迁移的 SQL 逻辑,downgrade 是它的逆操作,revision / down_revision 串成版本链表。每次生成后务必人工检查一遍脚本是否符合预期。
执行升级:
bash
alembic upgrade head
text
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> 269d4e00bcd3, init user table
去 MySQL 里确认:
text
mysql> SHOW TABLES;
+--------------------------+
| Tables_in_fastapi_demo |
+--------------------------+
| alembic_version |
| user |
+--------------------------+
mysql> SELECT version_num FROM alembic_version;
+--------------+
| version_num |
+--------------+
| 269d4e00bcd3 |
+--------------+
两张表:user 业务表按模型建好;alembic_version 是 alembic 的版本档案表,记录当前数据库迁移到哪个版本。
从现在开始,不要再使用
Base.metadata.create_all(),表结构全部交给 alembic 管理。
七、业务迭代:新增 email 字段
需求来了:用户表要加 email 邮箱字段。只改模型代码:
python
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, nullable=False, comment="用户名")
password_hash = Column(String(255), nullable=False, comment="密码哈希")
email = Column(String(100), nullable=True, comment="用户邮箱") # 新增字段
create_time = Column(DateTime, default=datetime.now, comment="创建时间")
再次生成迁移脚本并升级:
bash
alembic revision --autogenerate -m "add user email column"
alembic upgrade head
实测输出:
text
INFO [alembic.autogenerate.compare] Detected added column 'user.email'
Generating alembic/versions/78e3d02facf0_add_user_email_column.py ... done
INFO [alembic.runtime.migration] Running upgrade 269d4e00bcd3 -> 78e3d02facf0, add user email column
数据库 user 表自动新增字段,已有数据原封不动:
text
mysql> DESC user;
+---------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| username | varchar(50) | NO | UNI | NULL | |
| password_hash | varchar(255) | NO | | NULL | |
| create_time | datetime | YES | | NULL | |
| email | varchar(100) | YES | | NULL | |
+---------------+--------------+------+-----+---------+----------------+
这就是迁移的意义:改模型 → 生成脚本 → 升级,三步完成表结构变更,不写一行 ALTER TABLE。
八、版本回滚(重要!生产环境慎用)
上线后发现这次迁移有问题?回滚到上一个版本:
bash
alembic downgrade -1
实测输出:
text
INFO [alembic.runtime.migration] Running downgrade 78e3d02facf0 -> 269d4e00bcd3, add user email column
再看表结构,email 字段已经被撤掉:
text
mysql> DESC user;
+---------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| username | varchar(50) | NO | UNI | NULL | |
| password_hash | varchar(255) | NO | | NULL | |
| create_time | datetime | YES | | NULL | |
+---------------+--------------+------+-----+---------+----------------+
也可以指定版本号回滚:alembic downgrade 269d4e00bcd3。确认没问题后重新升级回去:
bash
alembic upgrade head
查看当前版本和历史:
bash
alembic current
alembic history
text
78e3d02facf0 (head)
269d4e00bcd3 -> 78e3d02facf0 (head), add user email column
<base> -> 269d4e00bcd3, init user table
⚠️ 提醒:回滚只能撤销表结构,删掉的数据找不回来。涉及删字段、删表的迁移,执行前务必备份数据库。
九、常用 Alembic 命令汇总
bash
# 生成迁移脚本(自动对比模型)
alembic revision --autogenerate -m "备注信息"
# 升级到最新版本
alembic upgrade head
# 升级到指定版本
alembic upgrade 版本号
# 回滚上一个版本
alembic downgrade -1
# 查看当前数据库版本
alembic current
# 查看所有迁移历史
alembic history
十、常见踩坑总结
坑 1:autogenerate 没有检测到模型变更,生成空脚本
原因一:env.py 没有正确设置 target_metadata;原因二:只导入了 Base 没有导入模型模块,模型没注册到元数据,alembic 扫描不到。必须在 env.py 里显式 from app import models。
坑 2:以为 autogenerate 检测不到删除字段
实测 alembic 1.16 默认能 检测删除字段并生成 drop_column(Detected removed column 'user.email')。真正的风险在于:upgrade 一执行数据就没了;而且 autogenerate 不是万能的------改表名/改字段名会被判定成"删旧建新"(数据同样丢失),部分类型变更也识别不出。所以脚本生成后必须人工检查。
坑 3:本地绕过 alembic 手动改库
禁止用 Navicat / DBeaver 直接改表结构!模型和数据库版本一旦不一致,后续迁移全部错乱。所有表变更必须改 models,走 alembic 迁移脚本。
坑 4:生产环境直接执行 autogenerate
生产环境不要执行
alembic revision --autogenerate!
正确流程:本地开发生成迁移脚本,提交到 git;服务器只执行 alembic upgrade head 运行已经检查过的脚本。
十一、结合 FastAPI 项目上线流程
- 开发:修改
models.py模型 - 本地:
alembic revision --autogenerate -m "xxx"生成脚本,人工校验脚本 - 提交
versions/下迁移脚本到 git - 服务器拉取最新代码,执行
alembic upgrade head,完成数据库更新
迁移脚本本身是代码,必须纳入 git 版本控制,团队所有人共用同一套迁移历史。
十二、本篇小结
Base.metadata.create_all()只适合演示项目,正式项目必须用 Alembic 做数据库版本迁移autogenerate自动生成脚本,但一定要人工检查脚本 SQL,不能完全依赖工具- 迁移脚本提交 git,服务器只执行 upgrade,不要在生产生成迁移脚本
- downgrade 能回滚表结构,但涉及数据删除的操作风险高,生产务必先备份数据库
十三、系列导航与下期预告
- Day1:Python 异步编程与 aiohttp
- Day2:日志封装与装饰器
- Day3:argparse 命令行参数
- Day4:异步爬虫项目实战
- Day5:Pydantic 数据校验模型
- Day6:FastAPI 基础接口与自动文档
- Day7:三种传参:路径、查询、请求体
- Day8:参数高级校验 Path/Query/Body/Field
- Day9:CORS 跨域 + 静态文件托管
- Day10:全局统一响应格式、全局异常捕获
- Day11:APIRouter 路由拆分,项目目录结构化
- Day12:Depends 依赖注入实战
- Day13:JWT 登录鉴权
- Day14:SQLAlchemy ORM 数据库 CRUD
- Day15:Alembic 数据库迁移版本管理
下一篇 Day16:FastAPI 后台任务 BackgroundTasks 异步后台任务实战。
如果本文对你有帮助,欢迎点赞收藏,专栏持续更新 FastAPI 实战内容。