FastAPI筑基_Day15_Alembic数据库迁移实战

【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_columnDetected removed column 'user.email')。真正的风险在于:upgrade 一执行数据就没了;而且 autogenerate 不是万能的------改表名/改字段名会被判定成"删旧建新"(数据同样丢失),部分类型变更也识别不出。所以脚本生成后必须人工检查。

坑 3:本地绕过 alembic 手动改库

禁止用 Navicat / DBeaver 直接改表结构!模型和数据库版本一旦不一致,后续迁移全部错乱。所有表变更必须改 models,走 alembic 迁移脚本。

坑 4:生产环境直接执行 autogenerate

生产环境不要执行 alembic revision --autogenerate

正确流程:本地开发生成迁移脚本,提交到 git;服务器只执行 alembic upgrade head 运行已经检查过的脚本。


十一、结合 FastAPI 项目上线流程

  1. 开发:修改 models.py 模型
  2. 本地:alembic revision --autogenerate -m "xxx" 生成脚本,人工校验脚本
  3. 提交 versions/ 下迁移脚本到 git
  4. 服务器拉取最新代码,执行 alembic upgrade head,完成数据库更新

迁移脚本本身是代码,必须纳入 git 版本控制,团队所有人共用同一套迁移历史。


十二、本篇小结

  1. Base.metadata.create_all() 只适合演示项目,正式项目必须用 Alembic 做数据库版本迁移
  2. autogenerate 自动生成脚本,但一定要人工检查脚本 SQL,不能完全依赖工具
  3. 迁移脚本提交 git,服务器只执行 upgrade,不要在生产生成迁移脚本
  4. 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 实战内容。

相关推荐
Broccoli5230266516 分钟前
FastAPI 中 Pydantic 模型与 SQLAlchemy ORM 模型的分工与转换规范
fastapi
叫我:松哥22 分钟前
基于Flask的教师评教管理系统,支持学生和教师两种角色登录
数据库·后端·python·数据挖掘·flask
Tisfy29 分钟前
LeetCode 3720.大于目标字符串的最小字典序排列:状态机 —— :从左往右枚举,失败则退回(最多退回一次)
linux·数据库·leetcode·字符串·状态机·构造
csdn_aspnet39 分钟前
mysql 函数、存储过程写法及示例
数据库·mysql·存储过程·函数·function·procedure
IvorySQL39 分钟前
倒计时 6 天!PGConf.Asia 2026 演讲征集即将截止
数据库·人工智能·postgresql
qyyyyy5701 小时前
PDF 转 JSON 怎么做?从表格和元数据提取到 LLM 结构化处理
数据库·pdf·json·erlang·llama
今天AI了吗1 小时前
从“金鱼脑”到“大象记忆”:AI Agent 短期记忆与长期记忆的存储与检索全解
数据库·人工智能·python·sql·rust
Elasticsearch1 小时前
Agent 上岗做日志检测,日志赛道降本的叙事快讲不下去了
elasticsearch
Java小白笔记1 小时前
Codex CLI 使用与斜杠指令实战教程
服务器·数据库·oracle