前两天我们学习了接口、依赖注入和数据库操作。今天继续以新闻模块为例,看看代码为什么要分目录、项目如何启动,以及怎样用 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 |
Category、News |
news_category、news |
modules/system/user/model.py |
User、UserToken |
user、user_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 能让表结构继续变化,而不需要删除整张表。后续内容会随着功能继续完善,有问题欢迎指出,也欢迎一起讨论。💬