DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

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 是否已产生外部副作用。数据库回滚不能撤销已经发送的消息或写出的文件;用既有批次与幂等规则恢复。回退代码或连接设置时保留这些记录,避免再次处理同一批内容。