SQLAlchemy会话的上下文/范围是否需要非自动对象/属性到期?

问题描述 投票:2回答:1

The Situation: Simple Class with Basic Attributes

在我正在处理的应用程序中,特定类的实例在其生命周期结束时保持不变,虽然它们随后未被修改,但可能需要读取它们的属性。例如,实例的end_time或其相对于同一类的其他实例的序数位置(初始化的第一个实例获得值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
    # ...

The Goal: Persist an Instance using SQLAlchemy

遵循我认为最佳实践 - 使用范围SQLAlchemy Session,作为suggested here,通过contextlib.contextmanager - 我将实例保存在新创建的Session中,立即提交。下一行通过在日志记录中提及它来引用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

Two Known Possible Solutions: No Expiring or No Scope

我知道我可以将expire_on_commit标志设置为False来规避这个问题,但我担心这是一个值得怀疑的做法 - 自动过期存在是有原因的,而且我犹豫是否任意将所有ORM绑定的类转换为非 - 没有充分理由和理解背后的ex状态。或者,我可以忘记确定Session的范围,并让事务处于待处理状态,直到我在稍后的时间显式提交。

所以我的问题归结为:

  1. 在我描述的情况下,是否正确使用了作用域/上下文管理的Session
  2. 是否有另一种方法来引用过期属性,这是一种更好/更优选的方法? (例如,使用属性来包装捕获过期/分离异常的步骤,或者创建和更新“镜像”ORM链接的过期属性的非ORM链接属性)
  3. 我误解或误用了SQLAlchemy Session和ORM吗?当我排除随后引用任何持久属性的能力时,使用contextmanager方法似乎是矛盾的,即使对于像记录这样简单且广泛适用的任务也是如此。

The Actual Exception Traceback

上面的例子被简化为专注于手头的问题,但如果它有用,这里是实际的精确追溯产生的。当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
python orm sqlalchemy contextmanager
1个回答
3
投票

1.

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

2.

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

3.

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

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

但是你为什么会收到错误?这是因为您的会话已经消失,因此ORM无法启动事务。如果在上下文管理器块的中间执行session.commit(),则在访问其中一个属性时,您会注意到正在启动新事务。

好吧,如果我想访问以前获取的值,该怎么办?然后,您可以要求框架不要使这些属性到期。

© www.soinside.com 2019 - 2024. All rights reserved.