由于懒加载导致的 Pony ORM 中 DatabaseSessionIsOver?

6 投票
2 回答
2312 浏览
提问于 2025-04-20 09:49

我在使用Pony ORM来做一个Flask项目时,遇到了一些问题。

考虑一下这个情况:

@db_session
def get_orders_of_the_week(self, user, date):
    q = select(o for o in Order for s in o.supplier if o.user == user)
    q2 = q.filter(lambda o: o.date >= date and o.date <= date+timedelta(days=7))
    res = q2[:]

    #for r in res:
    #    print r.supplier.name

    return res

当我需要在Jinja2中得到结果时,它看起来像这样:

{% for order in res %}
    Supplier: {{ order.supplier.name }}
{% endfor %}

但是我得到了一个

DatabaseSessionIsOver: Cannot load attribute Supplier[3].name: the database session is over

如果我把for r in res这一部分取消注释,它就能正常工作了。我怀疑这里有某种懒加载的机制,使用res = q2[:]时没有加载到。难道我完全理解错了什么,还是说这里发生了什么?

2 个回答

7

我刚刚添加了预取功能,这个功能应该能解决你的问题。你可以从GitHub 仓库获取可以运行的代码。这个功能将会包含在即将发布的 Pony ORM 0.5.4 版本中。

现在你可以这样写:

q = q.prefetch(Supplier)

或者

q = q.prefetch(Order.supplier)

这样 Pony 就会自动加载相关的 supplier 对象。

下面我会展示几个使用预取的查询,使用的是标准的 Pony 示例,包括学生、班级和系。

from pony.orm.examples.presentation import *

仅加载学生对象,不进行任何预取:

students = select(s for s in Student)[:]

同时加载学生、班级和系:

students = select(s for s in Student).prefetch(Group, Department)[:]

for s in students: # no additional query to the DB is required
    print s.name, s.group.major, s.group.dept.name

和上面一样,但指定属性而不是实体:

students = select(s for s in Student).prefetch(Student.group, Group.dept)[:]

for s in students: # no additional query to the DB is required
    print s.name, s.group.major, s.group.dept.name

加载学生及其课程(多对多关系):

students = select(s for s in Student).prefetch(Student.courses)

for s in students:
    print s.name
    for c in s.courses: # no additional query to the DB is required
        print c.name

prefetch() 方法的参数中,你可以指定实体和/或属性。如果你指定了一个实体,那么所有与这个类型相关的 一对多 属性都会被预取。如果你指定了一个属性,那么这个特定的属性会被预取。多对多属性只有在明确指定时才会被预取(就像 Student.courses 的例子)。预取是递归进行的,所以你可以加载很长的属性链,比如 student.group.dept

当对象被预取时,默认情况下它的所有属性都会被加载,除了懒加载属性和多对多属性。如果需要,你可以明确预取懒加载和多对多属性。

我希望这个新方法能完全满足你的需求。如果有什么地方没有按预期工作,请在 GitHub 上提个新问题。你也可以在Pony ORM 邮件列表上讨论功能和提出功能请求。

附言:我不太确定你使用的仓库模式是否真的给你带来了好处。我觉得这实际上增加了模板渲染和仓库实现之间的耦合,因为当模板代码开始使用新属性时,你可能需要更改仓库实现(例如,添加新的实体到预取列表)。使用顶层的 @db_session 装饰器,你可以直接将查询结果发送到模板,所有事情都会自动发生,而不需要明确的预取。但也许我错过了什么,所以我很想看到关于在你这种情况下使用仓库模式的好处的更多评论。

5

这个问题出现的原因是你试图访问一个没有加载的相关对象,而你又是在数据库会话之外进行访问的(也就是在用db_session装饰的函数外)。所以,Pony就抛出了这个异常。

推荐的做法是在最上面使用db_session装饰器,和Flask的app.route装饰器放在一起:

@app.route('/index')
@db_session
def index():
    ....
    return render_template(...)

这样,所有对数据库的调用都会被包裹在数据库会话中,直到网页生成后会话才会结束。

如果你有理由想把数据库会话限制在某个单独的函数中,那么你需要在用db_session装饰的函数内部遍历返回的对象,并访问所有必要的相关对象。Pony会用最有效的方式从数据库加载相关对象,避免N+1查询问题。这样,Pony会在db_session的范围内提取所有必要的对象,同时数据库连接仍然是活跃的。

--- 更新:

现在,为了加载相关对象,你应该遍历查询结果并调用相关对象的属性:

for r in res:
    r.supplier.name 

这和你示例中的代码类似,我只是去掉了print语句。当你“触碰”r.supplier.name属性时,Pony会加载相关supplier对象的所有非懒加载属性。如果你需要加载懒加载属性,你需要单独触碰每一个。

看起来我们需要引入一种方法,在查询执行期间指定应该加载哪些相关对象。我们会在未来的某个版本中添加这个功能。

撰写回答