FastAPI 学习笔记03|core 核心层:config 是总控制台,database 是整个工程的地基

大家好,这里是 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 两大核心能力:
    1. 收集元数据 :自动登记所有子类的表名、字段、索引,create_all()依靠元数据完成建表;
    2. 对象映射能力:赋予 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()      ← 服务关闭阶段:释放连接池全部连接

两个关键点:

  1. 建表放在启动回调,而非每次请求 数据表仅需要初始化一次。服务启动时扫描所有继承 Base 的模型,根据元数据建表;定义 models 的过程,就是声明数据表结构,无需手写 CREATE TABLE 语句,是 ORM 核心优势。

  2. 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(CategoryProduct
  • 字段、变量、文件名:snake_case(sort_ordercreated_at
  • 常量:全大写(DEEPSEEK_API_KEY

5.3 开发经常遇到的两类环境坑

坑 1:端口占用 uvicorn 抛出 [WinError 10048],代表旧进程未正常关闭,端口持续占用。解决方案:更换端口启动,或者找到占用进程并杀死。

坑 2:修改.env 不生效

  1. 确认修改的是.env,不是 config.py
  2. env_file 配置是相对启动目录,确认.env 放在项目根目录;
  3. uvicorn 热重载不会自动重新加载.env,修改后必须重启服务。

5.4 工程规范补充

  1. engine 只在模块顶层创建,禁止在函数内部实例化。引擎开销很大,全局复用;函数内重复创建会不断新建连接池,性能严重受损。
  2. DATABASE_URL 格式标准:mysql+aiomysql://用户:密码@主机:端口/库名?charset=utf8mb4 mysql+aiomysql = MySQL 方言 + aiomysql 异步驱动;charset=utf8mb4 防止中文、emoji 乱码。
  3. db.add() 不会持久化数据。会话关闭前未执行commit(),事务自动回滚,数据丢失。依托延迟写入机制,支持多条操作统一提交,保障事务原子性。
  4. core 内部推荐相对导入 from .config import settings;跨层代码使用绝对导入 from core.database import get_db,行业通用工程习惯。
  5. 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 封装、路由前缀拼接

这套架构是我从混乱的零散代码,重构为分层工程后逐行拆解总结得来。读完三篇,你可以回答这些面试核心问题:

  1. FastAPI 和 Django 开发理念最大区别?------类型驱动开发(第一篇)
  2. FastAPI 全异步的意义,await 到底在等待什么?------I/O 阻塞时让出 CPU 资源(第二篇)
  3. 如何实现「一个请求对应一个 session」?------get_db 的 yield + finally 机制(第二篇、第三篇)
  4. 切换数据库为什么优先修改.env,不改动代码?------pydantic-settings 配置优先级(第三篇)
  5. 连接池、pool_pre_ping、expire_on_commit 各自作用?(第三篇)

本篇是整个 FastAPI 工程化系列收官笔记。如有理解偏差,欢迎评论区交流指正,觉得有帮助可以点赞收藏。 三篇系列到此完结,有缘再见。

相关推荐
math_hongfan4 小时前
鸿蒙离线数据缓存高级架构:弱网预加载/离线数据优先级/同步冲突解决/上线后数据合并策略
学习·缓存·华为·架构·harmonyos·鸿蒙
划水的code搬运工小李4 小时前
Simulink学习-自定义Storage Class配置
学习
花酒锄作田4 小时前
Repository 模式在 FastAPI 中的应用
python·fastapi
疯狂打码的少年4 小时前
【数据结构】链表变体:双向链表与循环链表
数据结构·笔记·链表
一尘之中4 小时前
量子计算机能“参禅”吗?从叠加态到不二法门的技术哲学追问
学习·ai写作·量子计算
iCxhust5 小时前
二进制文件编辑器ImHex安装步骤
笔记·微机原理·8088单板机
什么都干的派森5 小时前
高速PCB设计学习记录(更新中)
学习·高速pcb设计
lifallen6 小时前
Emdash 拆解:多 Agent 并行开发桌面端的实现思路,兼谈 ACP 与 A2A
人工智能·学习·ai·ai编程
牛艺翔6 小时前
对一些基础知识的了解 笔记1
笔记