学习 FastAPI 的 Day 3:企业级目录与数据库迁移

105 阅读6分钟

🚀 FastAPI 学习传送门

Day 1:看懂接口与请求流程
Day 2:用异步 ORM 完成图书增删改查
Day 3:企业级目录与数据库迁移(当前文章)
Day 4:完成用户系统与接口联调

📦 GitHub 源码: dragonxjy/FastAPI

前两天我们学习了接口、依赖注入和数据库操作。今天继续以新闻模块为例,看看代码为什么要分目录、项目如何启动,以及怎样用 Alembic 修改表结构又不丢失原有数据。内容会按照实际阅读顺序展开,新手跟着往下看即可。🧭

一、项目目录如何划分 🗂️

企业级目录并不是文件越多越好,而是把不同职责分开。先看粗体主目录,再顺着树形线条了解每个文件的位置:


├── main.py                         # 项目启动入口
├── pyproject.toml                  # Python 版本和依赖
├── alembic.ini                     # Alembic 总配置
├── alembic/                        # 数据库迁移
│   ├── env.py                      # 加载连接和 Model
│   └── versions/                   # 保存迁移版本
├── env/                            # 环境配置
│   ├── .env.dev                    # 本机真实配置
│   └── .env.example                # 配置参考模板
└── app/                            # 应用代码
    ├── __init__.py                 # 创建并初始化 FastAPI
    ├── api/v1/
    │   └── routers.py              # 汇总业务路由
    ├── core/                       # 公共基础能力
    │   ├── config.py               # 加载环境配置
    │   ├── database.py             # 数据库引擎与会话
    │   ├── database_initializer.py # 创建数据库并执行迁移
    │   ├── base_model.py           # ORM 公共字段
    │   └── exceptions.py           # 全局异常处理
    ├── common/
    │   └── response.py             # 统一响应格式
    ├── modules/                    # 业务模块
    │   ├── news/                   # 新闻业务
    │   │   ├── controller.py       # 接收请求
    │   │   ├── service.py          # 编排业务
    │   │   ├── crud.py             # 操作数据库
    │   │   ├── model.py            # 定义数据表
    │   │   └── schema.py           # 约束数据格式
    │   └── system/user/            # 用户业务
    └── utils/
        └── password_util.py        # 密码处理工具

项目入口与公共能力

文件或目录详细职责
main.py暴露 FastAPI 的 app,让 Uvicorn 能找到应用
app/__init__.py注册路由、中间件、异常处理和启动生命周期
app/api/v1/routers.py汇总新闻和用户路由,并统一添加 /api 前缀
app/core/config.py读取 .env.dev,检查配置类型并拼接数据库地址
app/core/database.py创建异步引擎、连接池和 AsyncSession
app/core/database_initializer.py数据库不存在时先创建,再执行 Alembic 迁移
app/core/base_model.py定义 ORM Model 共用的创建时间和更新时间
app/core/exceptions.py集中处理接口异常和数据库异常
app/common/response.py统一接口的 code、message、data

新闻业务模块

文件负责什么
controller.py接收参数、调用 Service、返回 HTTP 响应
service.py安排查询详情、更新浏览量等业务顺序
crud.py使用 SQLAlchemy 查询或修改数据
model.py定义分类表和新闻表
schema.py检查接口收到和返回的数据

📌 可以简单记成:Controller 管接口,Service 管流程,CRUD 管查询,Model 管表,Schema 管数据格式。用户模块也采用相同分层,以后增加评论或收藏模块时可以继续照此组织。

二、配置并启动项目 ⚙️

1. 准备配置

先安装并启动 MySQL,数据库可以暂时不创建。在项目根目录生成本地配置:

Copy-Item env\.env.example env\.env.dev

修改 .env.dev 中的真实信息:

DATABASE_HOST=localhost
DATABASE_PORT=3306
DATABASE_USER=root
DATABASE_PASSWORD=自己的数据库密码
DATABASE_NAME=news_app
DATABASE_AUTO_MIGRATE=true

.env.dev 保存真实值,不提交 Git;.env.example 只提供参考。平时修改密码或端口编辑 .env.dev,只有新增配置项时才改 config.py。💡

2. 启动项目

uv sync
uv run uvicorn main:app --reload

启动成功后访问 http://127.0.0.1:8000/docs

3. 启动顺序

导入 main:app
      ↓
create_app() 组装 FastAPI
      ↓
lifespan 进入启动阶段
      ↓
读取 .env.dev
      ↓
数据库不存在则创建
      ↓
Alembic 执行 upgrade head
      ↓
FastAPI 开始接收请求
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
    # 启动时先创建数据库,再执行尚未运行的迁移。
    if settings.DATABASE_AUTO_MIGRATE:
        await initialize_database()

    # 执行到这里以后,FastAPI 才开始接收请求。
    yield

    # 项目关闭时释放数据库连接。
    await async_engine.dispose()

Alembic 通常不负责创建 MySQL 数据库,所以初始化模块先执行建库,再把表结构交给 Alembic。✅

三、为什么要使用 Alembic 🧰

Alembic 可以理解为数据库表结构的版本管理工具。它把建表、增加字段和修改索引等变化保存成迁移文件。

假设 news 表已经有 403 条数据,现在增加 status 字段:

  • 删除表再创建会丢失 403 条数据。
  • create_all() 发现表已存在,会直接跳过,无法增加字段。
  • Alembic 会生成 ALTER TABLE 迁移,在保留数据的情况下增加字段。

项目中的关键文件:

位置作用
alembic.ini指定迁移目录、项目路径和日志格式
alembic/env.py.env.dev 读取连接,并加载全部 Model
alembic/versions/保存每次表结构变化的迁移文件
alembic_version记录数据库已经执行到哪个版本

alembic.ini 不保存密码,可以提交 Git,但不能删除。

四、Model 和 Alembic 如何配合 🧱

当前项目管理四个 Model:

Model 所在位置模型对应数据表
modules/news/model.pyCategoryNewsnews_categorynews
modules/system/user/model.pyUserUserTokenuseruser_token

base_model.py 提取公共时间字段:

class Base(DeclarativeBase):
    # 新增数据时自动写入创建时间。
    created_at: Mapped[datetime] = mapped_column(
        DateTime,
        default=datetime.now,
        comment="创建时间",
    )

    # 数据更新时重新生成更新时间。
    updated_at: Mapped[datetime] = mapped_column(
        DateTime,
        default=datetime.now,
        onupdate=datetime.now,
        comment="更新时间",
    )

新闻表中的关键字段如下:

class News(Base):
    __tablename__ = "news"

    # 常用筛选和排序字段建立索引。
    __table_args__ = (
        Index("fk_news_category_idx", "category_id"),
        Index("idx_publish_time", "publish_time"),
        {"comment": "新闻表"},
    )

    id: Mapped[int] = mapped_column(
        Integer,
        primary_key=True,
        autoincrement=True,
        comment="新闻ID",
    )
    title: Mapped[str] = mapped_column(
        String(255),
        nullable=False,
        comment="新闻标题",
    )
    category_id: Mapped[int] = mapped_column(
        Integer,
        ForeignKey("news_category.id"),
        nullable=False,
        comment="分类ID",
    )

常用参数可以这样理解:

参数通俗解释
primary_key=True每条数据的唯一编号
autoincrement=True新增时编号自动加一
nullable=False字段必须有值
unique=True数据不能重复
defaultPython 新增对象时使用默认值
ForeignKey(...)当前字段引用另一张表
Index(...)加快常用查询或排序

alembic/env.py 必须导入全部 Model,Alembic 才能从 Base.metadata 中发现这些表:

from app.core.base_model import Base
# 看起来没有直接使用,但导入会执行 Model 类的定义。
from app.modules.news.model import Category, News  # noqa: F401
from app.modules.system.user.model import User, UserToken  # noqa: F401

target_metadata = Base.metadata

这里的 Model 确实没有被调用,因为导入本身就是目的

导入 model.py
    ↓
Python 执行 class News(Base)
    ↓
SQLAlchemy 把 news 表登记到 Base.metadata
    ↓
Alembic 根据 metadata 比较数据库结构

因此不需要创建 News() 对象。# noqa: F401 表示“这是有意保留的未直接使用导入”,避免代码检查工具把它报告成无用代码。

📌 如果只是在已经导入的新闻 Model 中增加一个模型类,不需要修改这里;如果新建了其他业务的 model.py,就要确保新模块也在读取 Base.metadata 前被导入。

新增字段后如何更新 🚀

修改 Model 后,先生成迁移文件:

uv run alembic revision --autogenerate -m "add news status"

简单检查新迁移没有误删表或字段,然后重新启动项目:

uv run uvicorn main:app --reload

项目启动时会自动执行 alembic upgrade head,把新字段更新到数据表中,同时保留原有数据。

结语

以新闻模块为例,我们把目录职责、启动配置、Model 和数据库迁移串在了一起。项目中已有数据后,Alembic 能让表结构继续变化,而不需要删除整张表。后续内容会随着功能继续完善,有问题欢迎指出,也欢迎一起讨论。💬