SQLAlchemy 空 do_orm_execute 监听,也让 yield_per 关系加载失败
SQLAlchemy 的空 do_orm_execute 监听也可能让内部加载选项泄漏,导致 yield_per 与关系加载失败,2.0.50包含修补。
查询原本使用 yield_per 和 select-in 关系加载正常,增加一个什么也不做的 do_orm_execute 监听后,却报:
Can't use the ORM yield_per feature in conjunction with unique()
监听函数没有改语句,也没有主动调用 unique(),并不意味着事件路径与无事件路径完全一致。受影响实现会在事件执行前后进行两轮 ORM 预处理,第一轮产生的内部执行选项泄漏到第二轮及关系加载路径,触发了不该出现的组合。
对照空监听与原查询
在独立测试 Session 中,可以保留同一份关系映射和数据,只增加一个空监听:
from sqlalchemy import event, select
from sqlalchemy.orm import selectinload
stmt = (
select(Message)
.options(selectinload(Message.corrections))
.execution_options(yield_per=5)
)
def noop(state):
pass
event.listen(session, "do_orm_execute", noop)
try:
for row in session.scalars(stmt):
print(row.id, len(row.corrections))
finally:
event.remove(session, "do_orm_execute", noop)
Message.corrections 对应应用已有的 select-in 关系,测试数据应包含已知关联行数。这里移除的只是临时空监听,不能顺带关闭 Session 上原有的租户、权限或其他业务事件。
SQLAlchemy 2.0.50 包含这项修补。修补后的第二轮预处理以原始选项为基础,只保留事件中显式更新的内容,避免带入第一轮的内部加载状态。更新到包含修补且兼容项目的版本后,再比较有、无空监听的结果。
不必一律关闭流式读取或关系加载
如果那个监听本来就是无用的调试入口,可以移除它;确有业务用途时,应保留事件行为并修补运行库。直接取消所有关系加载、关闭所有事件或切换语句缓存,会改变应用行为,也没有解释这个选项传播问题。
yield_per 与手工 Result.unique() 的限制仍然存在。集合 joined eager loading、subquery eager loading 也有各自的流式读取限制;select-in 加载还取决于驱动能否支持所需的游标行为。不能把这项修补理解成所有加载策略都能任意组合。
验收要覆盖主查询每行的值、关联集合内容及完整迭代结束,而不是只执行 session.scalars() 就认为成功。错误可能在迭代或关系加载时才显现。对于空监听,修补前后的对照应保持查询和数据不变;对于真实监听,还应确认其显式设置的执行选项仍然生效。


