SQLAlchemy,在对象刷新时急切加载

Posted

技术标签:

【中文标题】SQLAlchemy,在对象刷新时急切加载【英文标题】:SQLAlchemy, eager loading on object refresh 【发布时间】:2013-06-03 23:14:40 【问题描述】:

如果在 SQLAlchemy 中有以下 ORM 设置:

class Foo(Base):
    id = Column(Integer, primary_key=True)
    status = Column(String)
    barId = Column(Integer, ForeignKey("bar.id"))
    bar = relationship("Bar", lazy="joined")

class Bar(Base):
   id = Column(Integer, primary_key=True)

所以我希望始终为每个 Foo 对象提供关联的 Bar 对象。我经常从会话中分离 Foo 对象并继续使用它的值和 Bar 的值。有时我需要更新 Foo 的状态字段。在这种情况下,我创建一个新会话,将 foo 对象添加到会话并提交它。提交后,与 Foo 对象关联的 Bar 对象无效,但不会通过提交对 Foo 对象的隐式刷新重新加载。再次从会话中分离 Foo 对象后,Bar 对象不再可用。我发现解决这个问题的唯一方法是在提交 foo 后显式地急切加载 bar 对象。

示例工作流程:

session = Session()
foo = session.query(Foo).get(id) <-- foo.bar is automatically eager loaded
session.close()
....
session = Session()
session.add(foo)
foo.status = 'done'
session.commit()       <-- foo is commited and refreshed, foo.bar is not
session.refresh(foo)   <-- same here, foo.bar is not loaded
#foo.bar               <-- only explicit eager loading foo.bar here works
session.close()
....
foo.bar                <-- error if not explicitly eager loaded

我想将此设置用于一些类似 Bar 的小对象。要求我记住始终显式地重新加载 foo.bar 对象很容易出错。所以我的问题是:我是否可以在所有情况下都急切加载 foo.bar,无论是 query()、commit()(隐式刷新)还是(显式)refresh()?

【问题讨论】:

【参考方案1】:

首先,“commit()”不是“刷新”——它实际上会使所有数据过期,因此您会看到所有映射的属性都不再存在于foo.__dict__ 中。当您再次触摸这些属性时,会发生隐式刷新。对于那些在提交后不需要跨事务同步的许多应用程序来说,简单地将expire_on_commit=False 设置在Session 中是一种非常常见的做法,因此这可能是最隐含的工作流程。

接下来,session.refresh(foo) 将使用配置的 Eager loader 加载 bar。不知道为什么你看到foo.bar 没有加载,我检查了一下,这个功能至少可以追溯到 0.5 版。一个简单的测试证实了这一点:

from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Foo(Base):
    __tablename__ = 'foo'
    id = Column(Integer, primary_key=True)
    status = Column(String)
    barId = Column(Integer, ForeignKey("bar.id"))
    bar = relationship("Bar", lazy="joined")

class Bar(Base):
    __tablename__ = 'bar'
    id = Column(Integer, primary_key=True)

e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)

s = Session(e)

s.add(Foo(id=1, bar=Bar()))
s.commit()

f1 = s.query(Foo).get(1)
f1.status = 'done'
s.commit()

assert 'bar' not in f1.__dict__
s.refresh(f1)
assert 'bar' in f1.__dict__
s.close()

assert f1.bar.id == 1

下一个,SQLAlchemy 不鼓励使用处于“分离”状态的对象,因为您的映射对象代表正在进行的数据库事务的代理。这就是为什么当事务结束时,所有数据都过期了。就个人而言,我认为通常没有正当理由需要在分离状态下使用对象。分离主要是为了将对象传输到其他会话,将它们存储在缓存中,诸如此类。但是我们确实有很多用户在任何情况下都依赖于分离的使用模式,而且我确保我可以在合理的程度上支持他们,所以我不会太担心。

【讨论】:

在我的特定设置中,s.commit() 确实在 foo 对象上进行了选择,而无需将其与 bar 连接(在我自己触摸任何 attr 之前)。我不确定为什么。正如您所指出的,我错误地将其视为隐式刷新。我不能用你的小演示预先制作这个。急切加载确实适用于明确的s.refresh(foo)。我以某种方式错过了这一点,我仍在使用 SQLAlchemy 进行很多学习/实验。 SQLAlchemy 不鼓励使用处于“分离”状态的对象,你说。我的 Foo 对象存在于多个 HTTP 请求中,并且我总是在请求结束时关闭我的 orm 会话。我只需要更新 foo 对象 1/100 请求其他 99 我只是使用数据。那你会推荐什么设计? 1) 将 Foo 和 Bar 的数据复制到非 orm 对象,如果需要更新,请重新查询。 2)即使我不需要更新/刷新 Foo 也打开一个新的 orm 会话。 3) ... 通常,Web 应用程序会在每次请求时加载它需要的所有数据。如果您在第一个请求上加载您的 Foo 对象,然后请求第二个在六个小时后出现,您是否在整个六个小时内将这些“Foo”对象存储在内存中?你怎么知道它们仍然是最新的?这确实是一种缓存模式。 SQLAlchemy 对缓存的建议是在每个请求上将对象重新附加或合并到一个新的 Session - 请参阅dogpile caching 以获取推荐的设计指南。 仅供参考:我正在设计和实现一个使用 XMLRPC over HTTP 的分布式系统。我正在实现双方,所以我知道我会定期收到请求,记录 Foo 对象将如何生存,并且当我将它们保存在内存中时它们不会过时。我的问题当然是偏向于这个设计的。

以上是关于SQLAlchemy,在对象刷新时急切加载的主要内容,如果未能解决你的问题,请参考以下文章

sqlalchemy.exc.NoSuchModuleError:无法加载插件:sqlalchemy.dialects:postgres

何时在休眠中使用延迟加载/急切加载?

实体框架急切加载不返回数据,延迟加载有

使用 sqlalchemy 在刷新/提交时自动散列主键并使其持久化

急切加载“二级”关联对象时出现问题

我从 sqlalchemy 得到一个“幽灵”回滚,但在使用 psql 和 postgres 时没有