1. 从表到对象
ORM 的核心作用,是在数据库结构与 Python 对象之间建立一组明确映射。下面的示例沿用 Task、Project 等模型,并假设调用方已经取得一个 AsyncSession。阅读代码前,可以先记住这些对应关系:
| PostgreSQL | SQLAlchemy ORM |
|---|---|
tasks 表 | Task 模型类 |
| 表中的一行 | 一个 Task 对象 |
title 列 | task.title 属性 |
| 主键和外键约束 | mapped_column() 中的主键与 ForeignKey 配置 |
| 事务中的一组数据库操作 | 一个 Session 的工作范围 |
这种映射并没有取消 SQL。select(Task) 最终仍会生成 SELECT,修改对象后 flush 仍会生成 UPDATE,数据库约束也仍然由 PostgreSQL 执行。ORM 解决的是对象组织、状态跟踪和关系加载问题,而不是用 Python 规则替代数据库。
Session 会先记录本次工作中新增、修改和删除了哪些对象,再在 flush 时按依赖顺序生成 SQL。这种把一组对象变化统一同步到数据库的机制称为 Unit of Work(工作单元)。它解释了为什么修改已加载对象时不用立即写 UPDATE,也解释了为什么一次 flush 可能同时发送多条 SQL。
刚执行 Task(title="学习 ORM", ...) 时,只创建了一个普通 Python 对象,数据库中还没有对应记录。对象加入 Session 并 flush 后,SQLAlchemy 才会发送 INSERT;从数据库查询得到的 Task,则是由查询结果构造并纳入当前 Session 管理的对象。
2. 查询
ORM 查询使用 select() 构造语句,再交给 Session 执行。下面这段代码对应“查询项目中未归档的任务,并按创建时间倒序取前 20 条”:
01from sqlalchemy import select0203from app.db.models.task import Task040506statement = (07select(Task)08.where(09Task.project_id == project_id,10Task.archived_at.is_(None),11)12.order_by(Task.created_at.desc(), Task.id.desc())13.limit(20)14)1516result = await session.scalars(statement)17tasks = result.all()
select(Task) 表示选择完整的 Task 实体,多个 .where() 条件默认通过 AND 连接。Task.archived_at.is_(None) 会生成 IS NULL,不能写成 Python 的 is None,因为后者只会立即得到一个布尔值,无法构造 SQL 条件。
这段表达式生成的 SQL 大致如下。具体列清单和参数占位符会由方言与驱动决定:
1SELECT tasks.*2FROM tasks3WHERE tasks.project_id = :project_id4AND tasks.archived_at IS NULL5ORDER BY tasks.created_at DESC, tasks.id DESC6LIMIT :limit;
project_id 和 limit 会作为绑定参数发送,不是通过字符串拼接进入 SQL。session.scalars() 执行语句,并把每一行中的第一个实体或值提取出来;all() 再将结果收集为列表。因此,这里的 tasks 是 Task 对象列表,不是字典列表。
如果查询的是多个独立列,就应使用 execute() 保留完整的行结构:
01statement = select(Task.id, Task.title).where(02Task.project_id == project_id,03Task.archived_at.is_(None),04)0506result = await session.execute(statement)07rows = result.all()0809for task_id, title in rows:10print(task_id, title)
scalars() 适合只选择一个实体或一列的查询;同时选择多列时使用 execute(),可以避免其余列被丢弃。
按主键读取可以使用:
1task = await session.get(Task, task_id)
get() 只用于按主键读取。它会先查看 Session 的 identity map,当前 Session 已经加载过该对象时可能不再发送 SQL;但它不会自动附加项目范围、归档状态或权限条件。需要这些限制时,应使用完整的 select():
1statement = select(Task).where(2Task.id == task_id,3Task.project_id == project_id,4Task.archived_at.is_(None),5)6result = await session.execute(statement)7task = result.scalar_one_or_none()
这里的业务约束是“至多找到一个任务”,所以使用 scalar_one_or_none():没有结果时返回 None,出现多行时抛出异常。要求恰好一行时使用 scalar_one()。相比之下,session.scalar() 只返回第一行的第一个值,无法发现意外的重复结果。结果方法应当体现查询预期,而不只是为了方便取值。
3. 新增
新增对象仍然通过 Session:
01from sqlalchemy.ext.asyncio import AsyncSession0203from app.db.models.task import Task040506async def create_task(07session: AsyncSession,08project_id: int,09task_number: int,10title: str,11) -> Task:12task = Task(13project_id=project_id,14task_number=task_number,15title=title,16)17session.add(task)18await session.flush()19return task
session.add() 只改变 Session 中的对象状态,不执行网络 I/O,所以不需要 await。这个函数只 flush,不 commit。执行 flush 后,PostgreSQL 已经生成 task.id,服务端默认值也可以通过 RETURNING 回填,约束错误还可能在这里暴露;不过,这些变化仍然属于当前事务,可以回滚。调用它的服务可以继续写入状态历史或其他数据,最后统一提交。
不要把 Pydantic 请求对象直接展开为所有 ORM 字段。owner_id、created_at、权限字段等应由服务端上下文决定:
1task = Task(2project_id=authorized_project.id,3task_number=reserved_task_number,4title=request.title,5description=request.description,6priority=request.priority,7)
明确映射虽然多写几行,却能清楚看出客户端允许控制哪些数据库字段。