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

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

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

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

text 复制代码
├── 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,数据库可以暂时不创建。在项目根目录生成本地配置:

powershell 复制代码
Copy-Item env\.env.example env\.env.dev

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

env 复制代码
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. 启动项目

powershell 复制代码
uv sync
uv run uvicorn main:app --reload

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

3. 启动顺序

text 复制代码
导入 main:app
      ↓
create_app() 组装 FastAPI
      ↓
lifespan 进入启动阶段
      ↓
读取 .env.dev
      ↓
数据库不存在则创建
      ↓
Alembic 执行 upgrade head
      ↓
FastAPI 开始接收请求
python 复制代码
@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.py CategoryNews news_categorynews
modules/system/user/model.py UserUserToken useruser_token

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

python 复制代码
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="更新时间",
    )

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

python 复制代码
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 数据不能重复
default Python 新增对象时使用默认值
ForeignKey(...) 当前字段引用另一张表
Index(...) 加快常用查询或排序

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

python 复制代码
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 确实没有被调用,因为导入本身就是目的

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

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

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

新增字段后如何更新 🚀

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

powershell 复制代码
uv run alembic revision --autogenerate -m "add news status"

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

powershell 复制代码
uv run uvicorn main:app --reload

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

结语

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

相关推荐
Ray133344 分钟前
交付级定时数据管道:改 JSON 声明调度,84 个单测全绿
python
mldong1 小时前
AI Agent 不能自己签字:用 Python 工作流引擎给 AI 加一道人类审批闸门
后端·python·agent
她的男孩1 小时前
多租户隔离怎么落地?拆完这1600行Starter源码,我把5个坑全踩明白了
java·后端·架构
宫水三叶的刷题日记1 小时前
铁打的影视飓风,流水的新 iPhone
后端
青少儿编程课堂1 小时前
贪心算法进阶:区间调度与最少资源整合解析
c++·python·算法·贪心·信息学竞赛·区间调度
Yanjun2i1 小时前
Agent学习记录六:Tool 类 + Tool Registry
开发语言·python·学习
Bs_MoneyMagnet1 小时前
基于springboot+vue的医院陪诊服务预约平台的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring
SimonKing1 小时前
一个Docker命令,40万首古诗词API开箱即用
java·后端·程序员
kcuwu.1 小时前
第 1 课 · Hello, World 与一个 Go 程序的诞生
开发语言·后端·golang