使用SQLAlchemy创建Pyramid会话时出现DetachedInstanceError

4 投票
2 回答
1257 浏览
提问于 2025-04-17 18:45

我自己实现了Pyramid的ISession接口,目的是把会话存储在数据库里。整体运行得很顺利,但不知怎么的,pyramid_tm在这方面出现了问题。一旦它被激活,就会出现这样的提示:

DetachedInstanceError: Instance <Session at 0x38036d0> is not bound to a Session;
attribute refresh operation cannot proceed

(别搞混了:<Session ...>是模型的类名,而"... to a Session"很可能是指SQLAlchemy的Session,我为了避免混淆把它叫做DBSession。)

我查阅了邮件列表和StackOverflow,发现每当有人遇到这个问题时,他们通常会:

  • 创建一个新线程,或者
  • 手动调用transaction.commit()

但我都没有做这些。不过,特别的是,我的会话在Pyramid中被频繁传递。首先我执行DBSession.add(session),然后return session。之后我可以继续使用这个会话,发送新消息等等。

然而,似乎一旦请求结束,我就会遇到这个异常。以下是完整的错误追踪信息:

Traceback (most recent call last):
  File "/home/javex/data/Arbeit/libraries/python/web_projects/pyramid/lib/python2.7/site-packages/waitress-0.8.1-py2.7.egg/waitress/channel.py", line 329, in service
    task.service()
  File "/home/javex/data/Arbeit/libraries/python/web_projects/pyramid/lib/python2.7/site-packages/waitress-0.8.1-py2.7.egg/waitress/task.py", line 173, in service
    self.execute()
  File "/home/javex/data/Arbeit/libraries/python/web_projects/pyramid/lib/python2.7/site-packages/waitress-0.8.1-py2.7.egg/waitress/task.py", line 380, in execute
    app_iter = self.channel.server.application(env, start_response)
  File "/home/javex/data/Arbeit/libraries/python/web_projects/pyramid/lib/python2.7/site-packages/pyramid/router.py", line 251, in __call__
    response = self.invoke_subrequest(request, use_tweens=True)
  File "/home/javex/data/Arbeit/libraries/python/web_projects/pyramid/lib/python2.7/site-packages/pyramid/router.py", line 231, in invoke_subrequest
    request._process_response_callbacks(response)
  File "/home/javex/data/Arbeit/libraries/python/web_projects/pyramid/lib/python2.7/site-packages/pyramid/request.py", line 243, in _process_response_callbacks
    callback(self, response)
  File "/home/javex/data/Arbeit/libraries/python/web_projects/pyramid/miniblog/miniblog/models.py", line 218, in _set_cookie
    print("Setting cookie %s with value %s for session with id %s" % (self._cookie_name, self._cookie, self.id))
  File "build/bdist.linux-x86_64/egg/sqlalchemy/orm/attributes.py", line 168, in __get__
    return self.impl.get(instance_state(instance),dict_)
  File "build/bdist.linux-x86_64/egg/sqlalchemy/orm/attributes.py", line 451, in get
    value = callable_(passive)
  File "build/bdist.linux-x86_64/egg/sqlalchemy/orm/state.py", line 285, in __call__
    self.manager.deferred_scalar_loader(self, toload)
  File "build/bdist.linux-x86_64/egg/sqlalchemy/orm/mapper.py", line 1668, in _load_scalar_attributes
    (state_str(state)))
DetachedInstanceError: Instance <Session at 0x7f4a1c04e710> is not bound to a Session; attribute refresh operation cannot proceed

在这个情况下,我关闭了调试工具栏。一旦我激活它,就会抛出这个错误。看起来问题出在任何时候访问这个对象。

我意识到我可以尝试以某种方式将其分离,但这似乎不是正确的方法,因为这个元素在没有明确再次添加到会话中时是无法被修改的。

所以,当我不创建新线程,也不明确调用提交时,我猜测事务在请求完全结束之前就已经提交了,之后又可以访问它。我该如何解决这个问题呢?

2 个回答

2

我最开始尝试注册一个动画效果,它虽然能工作,但数据并没有保存。后来我发现了SQLAlchemy事件系统。我找到了after_commit这个事件。通过这个事件,我可以在提交完成后,利用pyramid_tm来分离会话对象。我觉得这样做很灵活,不会对顺序有任何要求。

我的最终解决方案:

from sqlalchemy.event import listen
from sqlalchemy.orm import Session as SASession
def detach(db_session):
    from pyramid.threadlocal import get_current_request
    request = get_current_request()
    log.debug("Expunging (detaching) session for DBSession")
    db_session.expunge(request.session)
listen(SASession, 'after_commit', detach)

唯一的缺点是:需要调用get_current_request(),但这个做法是不推荐的。不过,我没有找到其他方法来传递会话,因为这个事件是由SQLAlchemy触发的。我想过一些复杂的解决方案,但我觉得那样太冒险,也不稳定。

5

我觉得你看到的这个问题是因为响应回调和完成回调其实是在动画效果(tweens)之后执行的。它们的位置正好在你应用程序的输出和中间件之间。pyramid_tm作为一个动画效果,会在你的响应回调执行之前就提交事务,这就导致了后面访问时出现错误。

搞清楚这些事情的执行顺序确实很难。我想到一个可能的解决办法,就是在pyramid_tm下面注册你自己的动画效果,先刷新会话,获取ID,然后在响应中设置cookie。

我很理解这个问题,因为在事务提交之后发生的任何事情在Pyramid中都是一个模糊地带,通常不太清楚会话是否应该被修改。我会记下这个问题,继续思考如何在未来改善Pyramid的工作流程。

撰写回答