带有常规属性的SQLAlchemy DetachedInstanceError(不是关系)

时间:2010-06-14 17:52:14

标签: python sqlalchemy

我刚刚开始使用SQLAlchemy并获得DetachedInstanceError,并且无法在任何地方找到相关信息。我在会话之外使用实例,所以SQLAlchemy很自然无法加载任何关系,如果它们尚未加载,但是,我访问的属性不是关系,实际上这个对象根本没有任何关系。我找到了诸如渴望加载的解决方案,但我无法应用于此,因为这不是一种关系。我甚至在关闭会话之前尝试“触摸”此属性,但它仍然无法阻止异常。即使在非关系属性之前成功访问过一次之后,可能导致此异常的原因是什么?任何有关调试此问题的帮助表示赞赏。我将同时尝试获得可重现的独立场景并在此更新。

更新:这是包含几个堆栈的实际异常消息:

  File "/home/hari/bin/lib/python2.6/site-packages/SQLAlchemy-0.6.1-py2.6.egg/sqlalchemy/orm/attributes.py", line 159, in __get__
    return self.impl.get(instance_state(instance), instance_dict(instance))
  File "/home/hari/bin/lib/python2.6/site-packages/SQLAlchemy-0.6.1-py2.6.egg/sqlalchemy/orm/attributes.py", line 377, in get
    value = callable_(passive=passive)
  File "/home/hari/bin/lib/python2.6/site-packages/SQLAlchemy-0.6.1-py2.6.egg/sqlalchemy/orm/state.py", line 280, in __call__
    self.manager.deferred_scalar_loader(self, toload)
  File "/home/hari/bin/lib/python2.6/site-packages/SQLAlchemy-0.6.1-py2.6.egg/sqlalchemy/orm/mapper.py", line 2323, in _load_scalar_attributes
    (state_str(state)))
DetachedInstanceError: Instance <ReportingJob at 0xa41cd8c> is not bound to a Session; attribute refresh operation cannot proceed

部分模型如下所示:

metadata = MetaData()
ModelBase = declarative_base(metadata=metadata)

class ReportingJob(ModelBase):
    __tablename__ = 'reporting_job'

    job_id         = Column(BigInteger, Sequence('job_id_sequence'), primary_key=True)
    client_id      = Column(BigInteger, nullable=True)

字段client_id是导致此异常的原因,其用法如下所示:

查询:

    jobs = session \
            .query(ReportingJob) \
            .filter(ReportingJob.job_id == job_id) \
            .all()
    if jobs:
        # FIXME(Hari): Workaround for the attribute getting lazy-loaded.
        jobs[0].client_id
        return jobs[0]

这是稍后在会话范围之外触发​​异常的原因:

        msg = msg + ", client_id: %s" % job.client_id

7 个答案:

答案 0 :(得分:58)

我在尝试缩小导致异常的代码时找到了根本原因。我在会话结束后在不同的地方放置了相同的属性访问代码,发现在查询会话结束后它肯定不会立即引起任何问题。事实证明,在关闭为更新对象而打开的新会话后,问题开始出现。一旦我理解了会话结束后对象的状态无法使用,我就能找到讨论同一问题的thread。这个问题的两个解决方案是:

  • 保持会话开放(很明显)
  • expire_on_commit=False指定为sessionmaker()

第三个选项是在会话创建后手动将expire_on_commit设置为False,例如:session.expire_on_commit = False。我确认这解决了我的问题。

答案 1 :(得分:9)

即使expire_on_commit设置为False,我们也会遇到类似的错误。最后,它实际上是由于两个sessionmaker被用来在不同的请求中进行会话而引起的。我真的不明白发生了什么,但是如果你在expire_on_commit=False看到这个例外,请确保没有初始化两个sessionmaker

答案 2 :(得分:1)

DetachedInstanceError: Instance <> is not bound to a Session;

我遇到了类似的问题

情况非常简单,我将会话和要更新的记录传递给我的函数,它将合并记录并将其提交到数据库。在第一个示例中,我会得到错误,因为我很懒,并且认为我可以返回合并对象,因此我的操作记录将被更新(即它的is_modified值将为false)。它确实返回了更新的记录,is_modified现在是假的,但随后的使用引发了错误。我认为这是因为相关的儿童记录而复杂,但并不完全确定。

        def EditStaff(self, session, record):
            try:
                    r = session.merge(record)
                    session.commit()
                    return r
            except:
                    return False

经过大量的谷歌搜索和阅读会话等,我意识到,因为我在提交之前捕获了实例r并返回它,当同一条记录被发送回此函数进行另一次编辑/提交时,它已经丢失了它的会话。

所以为了解决这个问题,我只是在数据库中查询刚刚更新的记录并返回它以使其保持在会话中并将其is_modified值标记为false。

        def EditStaff(self, session, record):
            try:
                    session.merge(record)
                    session.commit()
                    r = self.GetStaff(session, record)
                    return r
            except:
                    return False

如上所述,设置expire_on_commit=False也避免了错误,但我认为它实际上并未解决错误,并且可能会导致许多其他问题IMO。

答案 3 :(得分:0)

为解决问题,我使用flaskflask-sqlalchemy来管理所有会话内容。当我通过站点执行操作时,这很好,但是当通过命令行和脚本执行操作时,则必须确保执行flask-y操作的所有操作都必须与flask上下文一起完成。

因此,在我的情况下,我需要从数据库中获取内容(使用flask-sqlalchemy),然后将其呈现为模板(使用flask的render_template),然后通过电子邮件发送(使用flask-mail)。 / p>

在代码中,我所做的就是这样,

def render_obj(db_obj):
  with app.app_context():
    return render_template('template_for_my_db_obj.html', db_obj=db_obj

def get_renders():
  my_db_objs = MyDbObj.query.all()

  renders = []
  for day, _db_objs in itertools.groupby(my_db_objs, MyDbObj.get_date):
    renders.extend(list(map(render_obj, _db_obj)))

  return renders

def email_report():
  renders = get_renders()
  report = '\n'.join(renders)

  with app.app_context():
    mail.send(Message('Subject', ['me@me.com'], html=report))

(这基本上是伪代码,我在分组部分中做了其他事情)

当我跑步时,我会先通过 _db_obj,但随后的任何运行中都会出错。

罪魁祸首? with app.app_context()

基本上,当您从上下文中退出时,它会执行一些操作,包括刷新数据库连接。随之而来的事情之一就是摆脱了最近的会话,即所有my_db_objs都与之相关的会话。

解决方案有几种不同的选择,但是我选择了一个变体,

def render_obj(db_obj):
  return render_template('template_for_my_db_obj.html', db_obj=db_obj

def get_renders():
  my_db_objs = MyDbObj.query.all()

  renders = []
  for day, _db_objs in itertools.groupby(my_db_objs, MyDbObj.get_date):
    renders.extend(list(map(render_obj, _db_obj)))

  return renders

def email_report():
  with app.app_context():
    renders = get_renders()
    report = '\n'.join(renders)

    mail.send(Message('Subject', ['me@me.com'], html=report))

只有1 with app.app_context()可以将它们全部包装起来。您需要做的主要事情(如果您有类似我的设置)是确保您正在使用的任何dB对象都在您正在使用的任何app_context中。如果执行我在第一次迭代中所做的操作,则所有dB对象都将失去会话,像我一样以DetachedInstanceError结尾。

答案 4 :(得分:0)

我对这个错误的解决方案也是一个简单的疏忽,我认为其他任何答案都没有涵盖。

我的函数正在获取对象 x,修改它,然后返回原始 x,因为我想要旧版本。

在提交和返回 x 之前,我调用了 expunge_all,但“太晚了”,因为对象已经被标记为脏。

解决方案只是尽早清除对象。

# pseudo code
x = session.fetch_x()
# adding the following line fixed it
session.expunge(x)
y = session.update(x)
return x

答案 5 :(得分:0)

我在当前项目中遇到了类似的问题,此修复程序对我有用。请检查您的数据库关系以获取选项 lazy=True 并将其更改为 lazy='dynamic'

答案 6 :(得分:-1)

至于我(新手),我在缩进上犯了一个错误并关闭了我的循环中的会话,其中我循环每一行,每次执行一些操作并提交。

所以对于像我这样的新手,在设置expire_on_commit=False之类的内容之前检查一下你的代码,它可能会引导你进入另一个陷阱。