1. 认识 SQLAlchemy
Psycopg 已经可以完成所有数据库操作,但项目规模扩大以后,如果每个函数都要手动处理连接、游标、结果转换和重复 SQL,维护成本会逐渐增加。SQLAlchemy 建立在数据库驱动之上,帮助应用统一组织 SQL、连接、事务和对象映射。
SQLAlchemy 在驱动之上提供两套相关能力:
| 能力 | 作用 |
|---|---|
| Core | 使用 Python 对象构造 SQL 表达式,管理 Engine、连接和事务 |
| ORM | 把表映射为 Python 类,在 Core 之上管理对象状态和关系 |
Core 和 ORM 不是两套互不相关的技术。ORM 建立在 Core 之上,最终仍然把 Python 操作转换成 SQL,再通过 Psycopg 发送给 PostgreSQL。它不会取代数据库约束,也不会让我们可以忽略 SQL 和执行计划。
同一个项目也不必在 Core 和 ORM 之间二选一。常规的增删改查可以使用 ORM 组织对象,批量更新、复杂统计或 PostgreSQL 专用语句则可以直接使用 Core 表达式。选择依据是查询是否清楚、可维护,而不是强制所有 SQL 都写成同一种形式。
1FastAPI 业务代码2-> SQLAlchemy ORM3-> SQLAlchemy Core / Engine / Pool / Dialect4-> Psycopg5-> PostgreSQL
安装 SQLAlchemy 与 Psycopg:
1uv add "sqlalchemy[asyncio]>=2.0,<3" "psycopg[binary]>=3.2"
asyncio 额外项会确保 greenlet 等 SQLAlchemy 异步功能需要的依赖可用。接下来使用 DeclarativeBase 创建声明基类,并通过 Mapped 和 mapped_column() 定义类型化模型。
对于刚接触 SQLAlchemy 的读者,可以先记住接下来几篇文章的分工:Engine 负责连接数据库,声明式模型描述表与 Python 类的对应关系,Session 则负责一次业务操作中的查询、对象状态和事务。本篇先完成前两部分,下一篇再进入 Session。
2. Engine
Engine 是应用访问数据库的入口,它包含方言和连接池。异步 FastAPI 项目使用 create_async_engine():
1from sqlalchemy.ext.asyncio import create_async_engine23from app.core.settings import settings456engine = create_async_engine(7settings.database_url,8pool_pre_ping=True,9)
配置中的 URL 使用 SQLAlchemy 方言格式:
1DATABASE_URL=postgresql+psycopg://task_app:task_app_dev_password@127.0.0.1:5432/task_app
这段 URL 可以拆成下面几部分:
| 组成部分 | 示例值 | 作用 |
|---|---|---|
| 数据库方言 | postgresql | 告诉 SQLAlchemy 使用 PostgreSQL 语法和类型 |
| 数据库驱动 | psycopg | 选择实际负责通信的 Python 驱动 |
| 用户名与密码 | task_app:task_app_dev_password | 用于数据库身份认证 |
| 主机与端口 | 127.0.0.1:5432 | 指定 PostgreSQL 服务地址 |
| 数据库名 | task_app | 指定连接的目标数据库 |
postgresql 选择 PostgreSQL 方言,psycopg 选择驱动。与 create_async_engine() 组合后,SQLAlchemy 会使用 Psycopg 的异步实现。同一个 postgresql+psycopg:// 方言也支持同步 Engine,究竟使用同步还是异步实现,由 create_engine() 或 create_async_engine() 决定。
如果用户名或密码中含有 @、:、/ 等 URL 特殊字符,需要进行百分号编码,不能直接拼进 URL。应用代码也不应把完整数据库 URL 写入日志,因为其中通常包含密码。
这里可以把 Engine 的职责拆成三个部分理解:
- Dialect 知道 PostgreSQL 的 SQL 语法、类型和驱动调用方式;
- Pool 复用已经建立的数据库连接;
- Engine 统一提供连接、事务和 SQL 执行入口。
这里创建出来的对象是 AsyncEngine。它通常是进程级长生命周期对象,不应该在每个请求中重复创建。调用 create_async_engine() 时一般也不会立刻连接数据库,第一次真正取得连接或执行 SQL 时,连接池才会建立连接。因此,仅仅导入模块或启动应用没有报错,并不能证明数据库 URL 一定可用。
可以执行一条轻量查询检查连接:
1from sqlalchemy import text23from app.db.engine import engine456async def check_database() -> bool:7async with engine.connect() as connection:8value = await connection.scalar(text("SELECT 1"))9return value == 1
engine.connect() 从连接池取得一条连接,离开上下文时再归还。SQLAlchemy 2.x 默认会在第一次执行语句时自动开始事务;这里是只读检查,连接归还前会结束事务。需要提交写入时,应使用 engine.begin()、显式事务或下一篇介绍的 Session 事务,不能把关闭连接误解为自动提交。
pool_pre_ping=True 会在连接池借出连接时检查它是否仍然可用,减少数据库重启或网络断开后拿到失效连接的问题。它会增加一次轻量检查,但不能挽救执行到一半时断开的事务;这种情况仍会抛出异常,应用必须回滚,并根据操作的幂等性决定是否重试整个事务。
应用关闭时应执行 await engine.dispose(),让连接池主动关闭空闲连接。这个操作属于 FastAPI lifespan 的关闭阶段,而不是每个请求的清理步骤:
01from collections.abc import AsyncIterator02from contextlib import asynccontextmanager0304from fastapi import FastAPI0506from app.db.engine import engine070809@asynccontextmanager10async def lifespan(_app: FastAPI) -> AsyncIterator[None]:11try:12yield13finally:14await engine.dispose()151617app = FastAPI(lifespan=lifespan)
连接池容量、worker 数量和 PostgreSQL 的连接上限要一起规划,不能把每个进程的池大小孤立看待。这里暂时使用默认池配置,生产环境中的 pool_size、max_overflow 和超时设置会在部署章节单独讨论。