大家好,这里是 FastAPI 学习日记第三篇,也是这个系列的收官篇。
第 1 篇我说过一句话:core 才是整个工程的地基 ,还说它 "具体怎么来的,第 3 篇再拆"。第 2 篇我们其实已经提前用了一次地基 ------get_db 的依赖注入,就是 core/database.py 里写的。这一篇正式把它拆开,看看地基到底是怎么打的。
建议你回看第 1 篇 "提前立地基" 那一段(engine /sessionmaker/ Base /get_db 各一句话),这一篇就是把那四句话,一行一行拆成真的。
一、core 层长什么样
core/ 里就四个文件,职责清清楚楚:
plaintext
core/
├── config.py # 总控制台:读 .env,管理所有配置
├── database.py # 地基:engine / sessionmaker / Base / get_db
├── app.py # 应用工厂:组装 app、注册路由和中间件、管生命周期
└── middleware.py # 全局中间件(比如记录响应时间)
core 是整个工程里最 "底" 的一层:models、schemas、crud、api 全都依赖 core,但 core 不依赖它们任何一层。 这是分层架构的铁律 ------ 地基不能骑在墙上面。
二、config.py:总控制台
先看真实代码:
python
运行
python
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""应用全局配置,自动映射 .env 文件中的变量"""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False, # 环境变量名大小写不敏感
)
# 应用
APP_NAME: str = "FastAPI Demo"
DEBUG: bool = True
HOST: str = "127.0.0.1"
PORT: int = 8000
# 数据库
DATABASE_URL: str = "mysql+aiomysql://root:123456@localhost:3306/fastapiprojectdemo?charset=utf8mb4"
# DeepSeek LLM API(LangChain 用)
DEEPSEEK_API_KEY: str = ""
DEEPSEEK_MODEL: str = "deepseek-v4-flash"
DEEPSEEK_BASE_URL: str = "https://api.deepseek.com"
# 全局配置单例 ------ 整个项目只 import 这一个 settings 对象
settings = Settings()
2.1 逐块解剖
plaintext
class Settings(BaseSettings): ← 继承 pydantic-settings 的基类,自动获得"读 .env"能力
...
settings = Settings() ← 全局单例,整个项目只创建一次
表格
| 片段 | 是什么 |
|---|---|
class Settings(BaseSettings) |
让这个类具备从环境变量 /.env 读取配置的能力 |
model_config = SettingsConfigDict(...) |
配置 "怎么读":读取文件、编码、大小写规则 |
APP_NAME: str = "FastAPI Demo" |
配置项:字段名对应配置键,默认值作为兜底 |
settings = Settings() |
实例化一次,全局共享 |
model_config 内参数详解:
表格
| 参数 | 作用 |
|---|---|
env_file=".env" |
指定读取项目根目录下的 .env 文件 |
env_file_encoding="utf-8" |
.env 文件编码,避免中文注释乱码 |
case_sensitive=False |
环境变量名称大小写不敏感 |
2.2 为什么叫 "总控制台"
数据库地址、debug 开关、服务端口、LLM 的密钥与模型名称 ------所有和环境绑定、需要频繁变更的配置,全部收敛在此处 。其余文件禁止硬编码环境参数,使用时统一导入:from core.config import settings,通过 settings.DATABASE_URL 获取参数。
优势:切换本地 / 测试 / 生产环境、更换数据库、更换大模型,只修改一处配置。
2.3 .env 优先级(高频踩坑点)
pydantic-settings 取值优先级总结:
.env 文件内定义的值 > 代码内写的默认值
举个例子:config.py 默认数据库是 MySQL,如果在.env 写入:
env
DATABASE_URL=sqlite+aiosqlite:///./dev.db
项目最终生效的是 SQLite 地址,代码中的默认 MySQL 配置会被覆盖。
踩坑复盘:之前切换数据库只修改 config.py,忽略.env 优先级更高,无论怎么修改代码都不生效。 结论:config.py 默认值仅作为兜底,正式环境配置以.env 为准;切换环境参数,优先修改.env 文件。
2.4 单例模式:全局唯一 settings
settings = Settings() 写在模块顶层,模块首次导入时完成实例化,项目所有文件拿到的是同一个对象。 禁止在各个业务文件重复 Settings()。配置只需要加载一次,全局复用;多处实例化会造成资源浪费,还容易出现本地参数修改导致的数据不一致。 工程规范:全项目统一使用 from core.config import settings。
三、database.py:地基(本篇真正的重点)
整个 database.py 四段核心代码,掌控项目数据底层命脉。完整代码:
python
运行
python
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import (
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from sqlalchemy.orm import DeclarativeBase
from .config import settings
# ============================= 1. 创建异步引擎 =============================
engine = create_async_engine(
settings.DATABASE_URL,
echo=settings.DEBUG, # 开发打印 SQL,生产关掉
pool_pre_ping=True, # 连接前检测是否存活(避免断连报错)
)
# ============================= 2. 创建异步会话工厂 =============================
async_session = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
# ============================= 3. ORM 模型基类 =============================
class Base(DeclarativeBase):
"""所有 ORM 模型继承此类,Base 自动收集元数据用于建表"""
pass
# ============================= 4. 数据库会话依赖注入 =============================
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session() as session:
try:
yield session
finally:
await session.close()
一段一段拆解底层逻辑。
3.1 engine:管理连接池的引擎
python
运行
python
engine = create_async_engine(
settings.DATABASE_URL,
echo=settings.DEBUG,
pool_pre_ping=True,
)
plaintext
engine = ← 全局唯一引擎,项目启动仅创建一次
create_async_engine( ← 异步引擎工厂:构建连接池、管理TCP连接
settings.DATABASE_URL, ← 数据库连接地址
echo=settings.DEBUG, ← True 终端打印执行SQL,开发调试使用
pool_pre_ping=True, ← 获取连接前探活
)
两个核心参数(面试高频): pool_pre_ping=True --- 解决 MySQL 空闲 8 小时自动断连 MySQL 空闲连接超时会主动断开连接池内闲置链接,再次使用时抛出 Lost connection。开启该参数后,取出连接前自动执行 SELECT 1 探测连接有效性,失效连接自动丢弃并新建,从根源规避断连异常。
连接池本质 建立 MySQL TCP 连接存在握手、认证开销,频繁新建连接性能极差。engine 维护连接池,请求到来复用已有连接,使用完毕归还池内。连接复用,而不是每次请求新建。
3.2 async_session:生产会话的工厂
python
运行
python
async_session = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
plaintext
async_session = ← 会话工厂,调用async_session()生成全新会话
async_sessionmaker( ← 工厂函数
engine, ← 绑定引擎,会话从engine获取数据库连接
class_=AsyncSession, ← 指定会话类型为异步会话
expire_on_commit=False, ← commit后不使ORM对象属性过期
)
重点解析 expire_on_commit=False: 默认开启(True)时,执行commit()之后,所有 ORM 对象属性标记为过期;再次访问对象字段,SQLAlchemy 自动发起新查询(懒加载)。 在异步环境中,同步代码触发懒加载,直接抛出 MissingGreenlet,问题极难排查。 设置为 False,提交事务后对象属性保持原有数据,不会触发额外查询。异步项目强制开启。
3.3 Base:所有 ORM 模型的父类
python
运行
python
class Base(DeclarativeBase):
pass
仅仅几行代码,却是所有数据表模型的根基:
- Base 继承
DeclarativeBase,SQLAlchemy2.0 新标准,替代旧版declarative_base(); - 第一篇
class Category(Base),继承的就是这个基类; - Base 两大核心能力:
- 收集元数据 :自动登记所有子类的表名、字段、索引,
create_all()依靠元数据完成建表; - 对象映射能力:赋予 Python 类「实例 ↔ 数据库行」双向转换能力。
- 收集元数据 :自动登记所有子类的表名、字段、索引,
呼应第一篇知识点:脱离 Base,class Category只是普通 Python 类,和数据库不存在任何关联。正是 Base 完成映射绑定。
3.4 get_db:实现一请求一会话生命周期
python
运行
python
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session() as session:
try:
yield session
finally:
await session.close()
plaintext
async def get_db() -> AsyncGenerator[AsyncSession, None]: ← 异步生成器,yield函数固定标注
async with async_session() as session: ← 上下文管理器创建会话
try:
yield session ← 向外提供session,函数暂停等待路由调用结束
finally:
await session.close() ← 无论正常返回/异常崩溃,强制关闭会话
返回值标注 AsyncGenerator[AsyncSession, None] 的原因: 函数使用yield,属于生成器。普通return执行完毕直接退出;生成器可以交出资源、暂停执行,外部逻辑执行完成后,继续执行后续代码。FastAPI 依赖注入原生支持这种模式。
try-finally 的意义:强制保证会话一定会关闭,连接归还连接池。如果缺少 finally,接口抛出异常时会话无法回收,连接持续泄漏,最终连接池耗尽、服务卡死。
3.5 地基四件套完整依赖链路
plaintext
engine(连接池) ← 最底层:管理TCP连接,全局唯一
│ 绑定
async_session(会话工厂) ← 生产会话,每次从engine获取连接
│ 每次请求调用
get_db(依赖注入) ← 每个请求分配独立session,用完归还连接
│ 传给
crud 的 db(AsyncSession) ← 第二篇路由、crud接收的db,整条链路终点
Base 是横向支撑:所有模型继承 Base,启动时依靠 Base 元数据批量建表。
一句话总结四件套:engine 管连接,sessionmaker 管会话生产规则,get_db 负责按请求分配会话,Base 负责数据表映射。 第二篇路由参数 db: AsyncSession,溯源就是这条链路。
四、FastAPI 应用生命周期:启动建表,关闭释放连接
core/app.py 内的 lifespan,打通地基与应用的生命周期钩子:
python
运行
python
@asynccontextmanager
async def lifespan(app: FastAPI):
"""启动时自动建表,关闭时释放引擎连接池。"""
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
await engine.dispose()
plaintext
@asynccontextmanager ← 将函数包装为异步上下文管理器
async def lifespan(...): ← FastAPI应用生命周期回调
create_all ← 服务启动阶段:自动创建不存在的数据表
yield ← 应用正常运行,停在此处接收请求
engine.dispose() ← 服务关闭阶段:释放连接池全部连接
两个关键点:
-
建表放在启动回调,而非每次请求 数据表仅需要初始化一次。服务启动时扫描所有继承 Base 的模型,根据元数据建表;定义 models 的过程,就是声明数据表结构,无需手写 CREATE TABLE 语句,是 ORM 核心优势。
-
engine.dispose()必不可少 服务正常关闭时,如果不主动释放连接池,TCP 连接会长期占用操作系统资源。优雅停机要求主动断开所有数据库连接。
补充应用工厂设计:main.py 只负责导入并创建 app 实例;创建 app、注册路由、挂载中间件、绑定 lifespan 统一收敛在 core/app.py,严格遵循分层各司其职。
五、隐藏细节 + 高频踩坑清单(前两篇未展开)
5.1 两套类型体系别混淆
models 面向数据库,使用 SQLAlchemy 类型;schemas 面向接口协议,使用 Python 原生类型。禁止混用!
python
运行
python
# ✅ models(数据库层)
from sqlalchemy import String, Float, DateTime
price: Mapped[float] = mapped_column(Float)
# ✅ schemas(Pydantic校验层)
from pydantic import BaseModel
class ProductCreate(BaseModel):
price: float # 使用原生float,不是SQLAlchemy的Float
错误示范:
python
运行
python
# ❌ schemas导入SQLAlchemy类型,Pydantic无法识别,直接报错
from sqlalchemy.sql.sqltypes import Float
class ProductCreate(BaseModel):
price: Float
原理:Pydantic 仅识别 Python 原生类型(str/int/float/datetime),SQLAlchemy 类型专为数据库列设计,两套体系互不兼容。掌握这条可以规避绝大多数类型异常。
5.2 统一命名规范
__tablename__ = "categories":数据表名使用复数形式- 类名:PascalCase(
Category、Product) - 字段、变量、文件名:snake_case(
sort_order、created_at) - 常量:全大写(
DEEPSEEK_API_KEY)
5.3 开发经常遇到的两类环境坑
坑 1:端口占用 uvicorn 抛出 [WinError 10048],代表旧进程未正常关闭,端口持续占用。解决方案:更换端口启动,或者找到占用进程并杀死。
坑 2:修改.env 不生效
- 确认修改的是.env,不是 config.py;
- env_file 配置是相对启动目录,确认.env 放在项目根目录;
- uvicorn 热重载不会自动重新加载.env,修改后必须重启服务。
5.4 工程规范补充
- engine 只在模块顶层创建,禁止在函数内部实例化。引擎开销很大,全局复用;函数内重复创建会不断新建连接池,性能严重受损。
- DATABASE_URL 格式标准:
mysql+aiomysql://用户:密码@主机:端口/库名?charset=utf8mb4mysql+aiomysql= MySQL 方言 + aiomysql 异步驱动;charset=utf8mb4防止中文、emoji 乱码。 db.add()不会持久化数据。会话关闭前未执行commit(),事务自动回滚,数据丢失。依托延迟写入机制,支持多条操作统一提交,保障事务原子性。- core 内部推荐相对导入
from .config import settings;跨层代码使用绝对导入from core.database import get_db,行业通用工程习惯。 - DEBUG 参数同步控制 echo 开关:开发环境打印 SQL 方便调试;生产环境关闭 DEBUG,避免大量日志拖慢服务、泄露 SQL 信息。
六、三篇系列收尾:完整链路与知识地图
串联三篇内容,整条请求链路一览:
plaintext
main.py(程序入口:app = create_app())
↓
core/app.py(应用工厂 + lifespan:启动建表 / 停机释放连接)
↓
api/(路由门卫:依赖注入、路由前缀拼接、参数接收)
│ SessionDep = Annotated[AsyncSession, Depends(get_db)]
↓
crud/(数据搬运工:execute→scalars→all / add→commit→refresh)
↓
models + schemas(数据表定义 + 接口协议:from_attributes打通ORM与Pydantic)
↓
core/database.py(底层地基:engine → sessionmaker → get_db,Base负责建表)
↓
MySQL(连接池内复用TCP连接)
三层知识点汇总表
表格
| 层级 | 核心总结 | 关键知识点 |
|---|---|---|
| core/config | 总控制台 | .env 优先级、全局单例配置 |
| core/database | 项目地基 | engine 连接池、sessionmaker、Base 基类、get_db 会话生命周期 |
| models | 数据表映射 | Mapped 类型注解、导入规范、server_default 默认值 |
| schemas | 接口协议 | 三套模型分工、前置 422 校验、from_attributes 序列化 |
| crud | 数据搬运层 | scalars 解析结果、commit+refresh、async/await 异步原理 |
| api | 请求入口 | 依赖注入、Annotated 封装、路由前缀拼接 |
这套架构是我从混乱的零散代码,重构为分层工程后逐行拆解总结得来。读完三篇,你可以回答这些面试核心问题:
- FastAPI 和 Django 开发理念最大区别?------类型驱动开发(第一篇)
- FastAPI 全异步的意义,await 到底在等待什么?------I/O 阻塞时让出 CPU 资源(第二篇)
- 如何实现「一个请求对应一个 session」?------get_db 的 yield + finally 机制(第二篇、第三篇)
- 切换数据库为什么优先修改.env,不改动代码?------pydantic-settings 配置优先级(第三篇)
- 连接池、pool_pre_ping、expire_on_commit 各自作用?(第三篇)
本篇是整个 FastAPI 工程化系列收官笔记。如有理解偏差,欢迎评论区交流指正,觉得有帮助可以点赞收藏。 三篇系列到此完结,有缘再见。