Django 用 psycopg2 命名游标报 outside of transactions:查询与读取都要在事务内
Django 使用 psycopg2 命名游标报事务外错误时,将声明和分批读取放在同一连接的事务中;区分自动提交、事务池、流式响应与 HOLD 游标生命周期。
Django 代码从底层连接创建 psycopg2 命名游标,准备分批读取大量记录,却报 can't use a named cursor outside of transactions。默认命名游标需要开放的事务,而 Django 默认采用自动提交。给同一个命名游标 execute("BEGIN"),也不能把它变成普通事务控制游标。
这里针对 PostgreSQL、psycopg2 和 Django 的连接管理。当前 Django 5.2 的事务 API 可以提供明确范围;较旧项目仍要核对自己的驱动、连接池和框架版本,不直接照搬底层 connection.connection 操作。
命名游标有服务器上的生命周期
psycopg2 的普通游标通常把查询结果传到客户端;指定 name 后使用服务器游标,分次读取结果。默认采用 WITHOUT HOLD,它只在创建它的事务内有效。自动提交模式下创建这类游标,或提交后继续读取,都会遇到生命周期错误。
检查失败连接实际的自动提交状态,以及创建、execute 和读取是否处于同一事务范围。不能只检查代码某处写过 atomic:游标在另一个连接、另一个数据库别名,或者事务退出后才开始迭代,都没有满足条件。
命名游标的 execute 用于声明对应查询,不适合先用它执行 BEGIN,再拿同一对象执行 SELECT。Django 中应优先用框架管理事务,不绕过它随意改变底层自动提交状态。
把声明和所有分批读取放在同一个 atomic 中
如果需要显式控制服务器游标,可以用 Django 的普通数据库 cursor 执行 DECLARE 与 FETCH。以下是只读结构示例:report_rows、id、tenant_id 是已经确认的实际表列,tenant_id 已完成权限与类型校验,process_row 是项目已有的处理函数:
from django.db import connections, transaction
alias = "default"
with transaction.atomic(using=alias):
with connections[alias].cursor() as cur:
cur.execute(
"DECLARE report_cursor NO SCROLL CURSOR FOR "
"SELECT id FROM report_rows "
"WHERE tenant_id = %s ORDER BY id",
[tenant_id],
)
while True:
cur.execute("FETCH FORWARD 1000 FROM report_cursor")
rows = cur.fetchall()
if not rows:
break
for row in rows:
process_row(row)
游标名在示例中固定,值仍通过参数绑定传入;不要把来自请求的表名或 SQL 片段直接拼进去。同一连接已有同名游标时会发生冲突,封装方法应管理名称和嵌套调用。
默认非 HOLD 游标随事务结束释放。异常应离开 atomic,让框架沿失败路径处理,不捕获数据库错误后继续在失效事务里读取。若此范围还包含数据库写入,正常退出会提交它们;应按实际业务审查处理函数,而不是因外层 SELECT 就认定整个任务没有写入。
大量读取会延长事务并占用连接与相关服务器资源。将网络调用或很慢的外部处理都塞进这一个范围之前,应评估任务时长和业务限制。分批 FETCH 控制每批传输,不保证查询本身不需要排序或其他资源。
ORM、连接池和流式响应分别核对
使用 ORM 时,可以先检查项目是否适合 QuerySet.iterator,而不必访问私有底层连接。Django PostgreSQL 的服务器游标使用受相关数据库设置影响;迭代时应保持所需事务范围,不在 atomic 里创建迭代器后就退出,再到外面消费。
事务池会在不同事务间分配不同服务器连接。服务器游标属于创建它的那条连接;跨事务继续读取可能找不到游标。Django 5.2 文档给出的路径包括在一个 atomic 中完整消费,或为这类读取使用数据库直连、会话池;关闭对应连接的服务器游标也是另一种选择,但会改变读取方式及资源表现。
ATOMIC_REQUESTS 也不会自动包住 StreamingHttpResponse 后续生成内容的过程。视图返回后才迭代的生成器,可能已经在原事务之外;需要按实际响应生命周期设计,而不是只在视图外加一个事务设置。
WITH HOLD 可让游标跨成功提交存在,但仍绑定同一会话,需要显式关闭,也可能将结果保存在临时文件或内存中。它不是绕过连接池和事务管理的通用开关,更不能把 rollback 后失效的游标恢复出来。
验收时读完结果,再检查失败路径
保留原连接配置与读取实现,在隔离环境检查超过一批的数据量,确认声明、每次 FETCH 和处理都使用预期连接与范围。再检查中途异常、提前结束读取及连接池配置,确认游标和连接得到释放。
成功标准包括结果数量和顺序符合查询、没有事务外游标错误,任务结束后没有遗留的开放事务。仅第一批能够读取,不能证明后面不会在事务退出后失败。
发生错误需要重跑时,还应判断 process_row 是否已产生外部副作用。数据库回滚不能撤销已经发送的消息或写出的文件;用既有批次与幂等规则恢复。回退代码或连接设置时保留这些记录,避免再次处理同一批内容。


