在我正在处理的应用程序中,特定类的实例在其生命周期结束时保持不变,虽然它们随后未被修改,但可能需要读取它们的属性。例如,实例的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
# ...
遵循我认为最佳实践 - 使用范围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
我知道我可以将expire_on_commit
标志设置为False来规避这个问题,但我担心这是一个值得怀疑的做法 - 自动过期存在是有原因的,而且我犹豫是否任意将所有ORM绑定的类转换为非 - 没有充分理由和理解背后的ex状态。或者,我可以忘记确定Session
的范围,并让事务处于待处理状态,直到我在稍后的时间显式提交。
所以我的问题归结为:
Session
?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
最有可能的是,是的。只要正确地将数据保存到数据库,它就会正确使用。但是,由于您的事务仅跨越更新,因此在更新同一行时可能会遇到竞争条件。根据应用程序,这可以。
不到期属性是正确的方法。默认情况下到期的原因是因为它确保即使是天真的代码也能正常工作。如果你小心,那应该不是问题。
将事务的概念与会话的概念分开是很重要的。 contextmanager
做了两件事:它维护会话以及交易。每个ORM实例的生命周期仅限于每个事务的跨度。这样您就可以假设对象的状态与数据库中相应行的状态相同。这就是框架在提交时使属性到期的原因,因为在事务提交后它不再能保证值的状态。因此,您只能在事务处于活动状态时访问实例的属性。
提交后,您访问的任何后续属性都将导致启动新事务,以便ORM可以再次保证数据库中值的状态。
但是你为什么会收到错误?这是因为您的会话已经消失,因此ORM无法启动事务。如果在上下文管理器块的中间执行session.commit()
,则在访问其中一个属性时,您会注意到正在启动新事务。
好吧,如果我想访问以前获取的值,该怎么办?然后,您可以要求框架不要使这些属性到期。