SQLAlchemy 会话的 Context/Scoping 是否需要非自动 Object/Attribute 过期?

Does Context/Scoping of a SQLAlchemy Session Require Non-Automatic Object/Attribute Expiration?

情况:简单 Class 具有基本属性

在我正在处理的应用程序中,特定 class 的实例在其生命周期的 结束 持续存在,并且在 not 随后被修改,可能需要读取它们的属性。例如,实例的 end_time 或其相对于相同 class 的其他实例的序号位置(第一个初始化的实例值为 1,下一个实例值为 2,依此类推)。

class Foo(object):
    def __init__(self, position):
        self.start_time = time.time()
        self.end_time = None
        self.position = position
        # ...
    def finishFoo(self):
        self.end_time = time.time()
        self.duration = self.end_time - self.start_time
    # ...

目标:使用 SQLAlchemy 持久化实例

遵循我认为的最佳实践 - 使用范围限定的 SQLAlchemy Session,作为 suggested here, by way of contextlib.contextmanager - I save the instance in a newly-created Session which immediately commits. The very next line references the newly persistent 实例,通过在日志记录中提及它,抛出 DetachedInstanceError 因为Session 提交时,其引用属性已过期。

class Database(object):
    # ...
    def scopedSession(self):
        session = self.sessionmaker()
        try:
            yield session
            session.commit()
        except:
            session.rollback()
            logger.warn("blah blah blah...")
        finally:
            session.close()
    # ...
    def saveMyFoo(self, foo):
        with self.scopedSession() as sql_session:
            sql_session.add(foo)
        logger.info("Foo number {0} finished at {1} has been saved."
                    "".format(foo.position, foo.end_time))
        ## Here the DetachedInstanceError is raised

两个已知的可能解决方案:没有过期或没有范围

我知道我可以将 expire_on_commit 标志设置为 False 来规避这个问题,但我担心这是一个有问题的做法——自动过期的存在是有原因的,我犹豫要不要随意在没有充分理由和理解的情况下,将所有与 ORM 相关的 classes 混为一谈。或者,我可以忘记确定 Session 的范围,只让事务挂起,直到我在(很多)晚些时候明确提交。

所以我的问题归结为:

  1. scoped/context-managed Session 是否在我描述的情况下被正确使用?
  2. 是否有替代方法来引用过期属性,这是 better/more 首选方法? (例如,使用 属性 来包装捕获 expiration/detached 异常的步骤,或者创建和更新一个非 ORM 链接的属性,该属性 "mirrors" ORM 链接的过期属性)
  3. 我是否误解或误用了 SQLAlchemy Session 和 ORM?在我看来使用 contextmanager 方法似乎是矛盾的,因为它排除了随后引用任何持久属性的能力,即使对于像日志记录这样简单且广泛适用的任务也是如此。

实际异常回溯

上面的例子被简化为关注手头的问题,但如果它有用,这里是产生的实际精确回溯。当 str.format()logger.debug() 调用中的 运行 时会出现问题,它会尝试执行 Set 实例的 __repr__() 方法。

Unhandled Error
Traceback (most recent call last):
  File "/opt/zenith/env/local/lib/python2.7/site-packages/twisted/python/log.py", line 73, in callWithContext
    return context.call({ILogContext: newCtx}, func, *args, **kw)
  File "/opt/zenith/env/local/lib/python2.7/site-packages/twisted/python/context.py", line 118, in callWithContext
    return self.currentContext().callWithContext(ctx, func, *args, **kw)
  File "/opt/zenith/env/local/lib/python2.7/site-packages/twisted/python/context.py", line 81, in callWithContext
    return func(*args,**kw)
  File "/opt/zenith/env/local/lib/python2.7/site-packages/twisted/internet/posixbase.py", line 614, in _doReadOrWrite
    why = selectable.doRead()
--- <exception caught here> ---
  File "/opt/zenith/env/local/lib/python2.7/site-packages/twisted/internet/udp.py", line 248, in doRead
    self.protocol.datagramReceived(data, addr)
  File "/opt/zenith/operations/network.py", line 311, in datagramReceived
    self.reactFunction(datagram, (host, port))
  File "/opt/zenith/operations/schema_sqlite.py", line 309, in writeDatapoint
    logger.debug("Data written: {0}".format(dataz))
  File "/opt/zenith/operations/model.py", line 1770, in __repr__
    repr_info = "Set: {0}, User: {1}, Reps: {2}".format(self.setNumber, self.user, self.repCount)
  File "/opt/zenith/env/local/lib/python2.7/site-packages/sqlalchemy/orm/attributes.py", line 239, in __get__
    return self.impl.get(instance_state(instance), dict_)
  File "/opt/zenith/env/local/lib/python2.7/site-packages/sqlalchemy/orm/attributes.py", line 589, in get
    value = callable_(state, passive)
  File "/opt/zenith/env/local/lib/python2.7/site-packages/sqlalchemy/orm/state.py", line 424, in __call__
    self.manager.deferred_scalar_loader(self, toload)
  File "/opt/zenith/env/local/lib/python2.7/site-packages/sqlalchemy/orm/loading.py", line 563, in load_scalar_attributes
    (state_str(state)))
sqlalchemy.orm.exc.DetachedInstanceError: Instance <Set at 0x1c96b90> is not bound to a Session; attribute refresh operation cannot proceed

1.

很有可能,是的。只要正确地将数据保存到数据库,它就会被正确使用。但是,由于您的事务仅跨越更新,因此在更新同一行时您可能 运行 进入竞争条件。根据应用程序,这可能没问题。

2.

不让属性过期是正确的做法。默认情况下过期的原因是因为它确保即使是天真的代码也能正常工作。如果你细心点,应该问题不大。

3.

将事务的概念与会话的概念分开很重要。 contextmanager 做了两件事:维护会话和事务。每个 ORM 实例的生命周期被限制在每个 transaction 的跨度内。这样您就可以假设对象的状态与数据库中相应行的状态相同。这就是为什么当你提交时框架会过期属性,因为它不能再保证事务提交后值的状态。因此,您只能在事务处于活动状态时访问实例的属性。

提交后,您访问的任何后续属性都将导致启动新事务,以便 ORM 可以再次保证数据库中值的状态。

但是为什么会报错呢?这是因为你的会话消失了,所以 ORM 没有办法开始一个事务。如果您在上下文管理器块的中间执行 session.commit(),您会注意到,如果您访问其中一个属性,就会启动一个新事务。

好吧,如果我只想访问之前获取的值怎么办?然后,你可以要求框架不要让这些属性过期。