SQLAlchemy 2.1.6 两组 loader criteria 互相引用,编译时报递归错误
SQLAlchemy 2.1.6 中,两个实体的 loader criteria 经关系互相引用,仍可能在编译时无限递归。保留数据隔离条件,核对正式修复与临时兼容边界。
SQLAlchemy 2.1.6 中,两组 with_loader_criteria() 分别通过关系条件引用对方,语句还没发给数据库,print(stmt) 或 stmt.compile() 就报 RecursionError: maximum recursion depth exceeded。如果同一映射和条件在 2.0.55 能编译,先检查 loader criteria 的递归应用,不要先提高 Python 的递归上限。
一种明确触发形状是:Foo.bars 与 Bar.foo 互相对应,查询 Foo 时同时给两类添加下面的选项。这里 Foo、Bar 为已有映射,stmt 是已有的 select(Foo):
from sqlalchemy import true
from sqlalchemy.orm import with_loader_criteria
stmt = stmt.options(
with_loader_criteria(Foo, Foo.bars.any(true())),
with_loader_criteria(Bar, Bar.foo.has(true())),
)
这段展示条件结构,不创建表,也不执行查询。真实应用的租户、软删除或权限条件必须保留;不能把它们全部换成 true() 当作修复。
2.1.6 的另一项递归修复没有覆盖这个组合
2.1.6 已修正一类相关子查询与多个 loader criteria 组合导致的编译递归,但两种实体的条件相互引用仍有独立触发。仅凭版本号已包含“RecursionError 修复”,无法证明当前组合也已恢复。
截至 2026 年 10 月 26 日,当前正式版本仍是 2.1.6;针对互相引用条件的后续改动不能当成已发布补丁。使用后续正式版前,应确认发行内容和相同条件组合。
先记录失败发生在编译阶段还是结果遍历阶段。若在 compile() 中反复进入条件生成,数据库驱动、连接池和数据量还没有参与这次执行。提高递归深度通常只让错误稍晚出现,也可能放大内存消耗。
临时恢复兼容环境,不能撤掉数据隔离条件
项目此前确实使用 2.0 系列、且完整依赖允许时,可以在隔离环境恢复保留的 2.0 锁定组合进行对照。明确案例在 2.0.55 可以编译;这不代表已经依赖 2.1 新接口的应用能直接降级。先检查上层库约束和应用接口,再决定是否采用这条临时路径。
如果必须保留 2.1.6,需要单独设计等价的查询或隔离方式,并审查关系加载、别名与子查询中的条件覆盖。把全局条件简单挪到最外层 WHERE,可能漏掉关系加载或改变外连接结果,不宜作为统一替换。尤其不能以取消 with_loader_criteria() 换取“查询能跑”,让原本不可见的记录进入结果。
验收保留双向关系与两组真实条件,既检查编译,也检查获准和不获准记录的可见范围,以及应用使用的关系加载入口。只把某个条件注释掉后编译成功,证明的是触发结构变了。恢复旧依赖或采用新正式版本时,应连同锁文件和应用版本一起管理;这个故障本身不要求修改数据库结构。


