状态管理

Quickie介绍对象状态

了解实例在会话中可以具有的状态很有帮助:

要深入了解所有可能的状态转换,请参阅描述每个转换的Object Lifecycle Events部分,以及如何以编程方式跟踪每个转换。

获取对象的当前状态

任何映射对象的实际状态都可以在任何时候使用inspect()系统查看:

>>> from sqlalchemy import inspect
>>> insp = inspect(my_object)
>>> insp.persistent
True

会话属性

Session本身的行为有点像集合式集合。所有存在的项目都可以使用迭代器接口访问:

for obj in session:
    print(obj)

可以使用常规的“包含”语义测试存在性:

if obj in session:
    print("Object is present")

会话还跟踪所有新创建的(即挂起的)对象,自上次加载或保存之后发生更改的所有对象(即“脏”)以及标记为已删除的所有对象:

# pending objects recently added to the Session
session.new

# persistent objects which currently have changes detected
# (this collection is now created on the fly each time the property is called)
session.dirty

# persistent objects that have been marked as deleted via session.delete(obj)
session.deleted

# dictionary of all persistent objects, keyed on their
# identity key
session.identity_map

(Documentation: Session.new, Session.dirty, Session.deleted, Session.identity_map).

会话参照行为

会话中的对象是弱引用的这意味着,当它们在外部应用程序中取消引用时,它们也会从Session中超出范围,并且会受到Python解释器的垃圾回收。例外情况包括挂起的对象,标记为已删除的对象或挂起更改的持久对象。完全刷新后,这些集合全部为空,并且所有对象都被弱引用。

要使Session中的对象保持强引用状态,通常只需要一个简单的方法。外部管理的强引用行为的示例包括将对象加载到与其主键相关的本地字典中,或者将对象加载到它们需要保持引用的时间范围内的列表或集合中。如果需要,可以将这些集合与Session关联,方法是将它们放入Session.info字典中。

基于事件的方法也是可行的。当所有对象保持在persistent状态时,为所有对象提供“强引用”行为的简单配方如下所示:

from sqlalchemy import event

def strong_reference_session(session):
    @event.listens_for(session, "pending_to_persistent")
    @event.listens_for(session, "deleted_to_persistent")
    @event.listens_for(session, "detached_to_persistent")
    @event.listens_for(session, "loaded_as_persistent")
    def strong_ref_object(sess, instance):
        if 'refs' not in sess.info:
            sess.info['refs'] = refs = set()
        else:
            refs = sess.info['refs']

        refs.add(instance)


    @event.listens_for(session, "persistent_to_detached")
    @event.listens_for(session, "persistent_to_deleted")
    @event.listens_for(session, "persistent_to_transient")
    def deref_object(sess, instance):
        sess.info['refs'].discard(instance)

Above, we intercept the SessionEvents.pending_to_persistent(), SessionEvents.detached_to_persistent(), SessionEvents.deleted_to_persistent() and SessionEvents.loaded_as_persistent() event hooks in order to intercept objects as they enter the persistent transition, and the SessionEvents.persistent_to_detached() and SessionEvents.persistent_to_deleted() hooks to intercept objects as they leave the persistent state.

对于任何Session可以调用上面的函数,以便在每个会话Session

from sqlalchemy.orm import Session

my_session = Session()
strong_reference_session(my_session)

它也可能被任何sessionmaker调用:

from sqlalchemy.orm import sessionmaker

maker = sessionmaker()
strong_reference_session(maker)

合并¶ T0>

merge()从外部对象将状态转换为会话中新的或已存在的实例。它还将传入的数据与数据库状态进行协调,产生将用于下一次刷新的历史流,或者可以使得产生状态的简单“转移”而不产生更改历史或访问数据库。用法如下:

merged_object = session.merge(existing_object)

给定一个实例时,它遵循以下步骤:

使用merge()时,给定的“源”实例不会被修改,也不会与目标Session关联,并且仍然可以与任何数量的其他Session对象。merge() is useful for taking the state of any kind of object structure without regard for its origins or current session associations and copying its state into a new session. 这里有一些例子:

合并提示

merge() is an extremely useful method for many purposes. 然而,它处理瞬态/分离对象与持久对象之间错综复杂的边界,以及状态的自动传输。可以在这里呈现的各种各样的场景通常需要对对象状态更谨慎的方法。合并的常见问题通常涉及有关传递给merge()的对象的一些意想不到的状态。

让我们使用User和Address对象的规范例子:

class User(Base):
    __tablename__ = 'user'

    id = Column(Integer, primary_key=True)
    name = Column(String(50), nullable=False)
    addresses = relationship("Address", backref="user")

class Address(Base):
    __tablename__ = 'address'

    id = Column(Integer, primary_key=True)
    email_address = Column(String(50), nullable=False)
    user_id = Column(Integer, ForeignKey('user.id'), nullable=False)

假设User对象具有一个Address,它已经是持久的:

>>> u1 = User(name='ed', addresses=[Address(email_address='ed@ed.com')])
>>> session.add(u1)
>>> session.commit()

我们现在创建a1,一个会话之外的对象,我们要在现有的Address之上合并:

>>> existing_a1 = u1.addresses[0]
>>> a1 = Address(id=existing_a1.id)

如果我们这样说,会发生一个惊喜:

>>> a1.user = u1
>>> a1 = session.merge(a1)
>>> session.commit()
sqlalchemy.orm.exc.FlushError: New instance <Address at 0x1298f50>
with identity key (<class '__main__.Address'>, (1,)) conflicts with
persistent instance <Address at 0x12a25d0>

这是为什么 ?我们没有注意到我们的瀑布。a1.user赋值给级联到User.addresses的backref的持久对象,并使我们的a1对象处于挂起状态,就好像我们已经添加它。现在我们在会话中有两个 Address对象:

>>> a1 = Address()
>>> a1.user = u1
>>> a1 in session
True
>>> existing_a1 in session
True
>>> a1 is existing_a1
False

上面,我们的a1在会话中已经挂起。随后的merge()操作本质上什么都不做。Cascade can be configured via the cascade option on relationship(), although in this case it would mean removing the save-update cascade from the User.addresses relationship - and usually, that behavior is extremely convenient. The solution here would usually be to not assign a1.user to an object already persistent in the target session.

relationship()cascade_backrefs=False选项也将阻止Address通过a1添加到会话中.user = u1分配。

关于级联操作的更多细节在Cascades

意外状态的另一个例子:

>>> a1 = Address(id=existing_a1.id, user_id=u1.id)
>>> assert a1.user is None
>>> True
>>> a1 = session.merge(a1)
>>> session.commit()
sqlalchemy.exc.IntegrityError: (IntegrityError) address.user_id
may not be NULL

在这里,我们访问了a1.user,它返回了默认值None,它作为这个访问的结果放在我们对象的__dict__a1通常,此操作不会创建更改事件,因此在刷新过程中user_id属性优先。但是当我们将Address对象合并到会话中时,操作等同于:

>>> existing_a1.id = existing_a1.id
>>> existing_a1.user_id = u1.id
>>> existing_a1.user = None

Where above, both user_id and user are assigned to, and change events are emitted for both. user关联优先,而None应用于user_id,导致失败。

大多数merge()问题可以通过首先检查来检查 - 会话中的对象是否过早?

>>> a1 = Address(id=existing_a1, user_id=user.id)
>>> assert a1 not in session
>>> a1 = session.merge(a1)

或者在对象上有我们不想要的状态?检查__dict__是检查以下内容的快速方法:

>>> a1 = Address(id=existing_a1, user_id=user.id)
>>> a1.user
>>> a1.__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x1298d10>,
    'user_id': 1,
    'id': 1,
    'user': None}
>>> # we don't want user=None merged, remove it
>>> del a1.user
>>> a1 = session.merge(a1)
>>> # success
>>> session.commit()

清除日期¶ T0>

清除从会话中删除对象,将持久实例发送到分离状态,并将待处理实例发送到瞬态:

session.expunge(obj1)

要删除所有项目,请调用expunge_all()(此方法以前称为clear())。

刷新/过期

Expiring意味着保存在一系列对象属性中的数据库持久数据被删除,这样当下次访问这些属性时,就会发出一个SQL查询,它将刷新数据库中的数据。

当我们谈论数据到期时,我们通常会谈论处于persistent状态的对象。例如,如果我们加载一个对象如下:

user = session.query(User).filter_by(name='user1').first()

上面的User对象是持久的,并且有一系列属性存在;如果我们要查看它的__dict__,我们会看到加载状态:

>>> user.__dict__
{
  'id': 1, 'name': u'user1',
  '_sa_instance_state': <...>,
}

其中idname指数据库中的那些列。_sa_instance_state是SQLAlchemy内部使用的非数据库持久化值(它指向实例的InstanceState)。虽然与本节不直接相关,但如果我们想要了解它,我们应该使用inspect()函数来访问它)。

此时,我们的User对象中的状态与加载的数据库行的状态匹配。但是在使用诸如Session.expire()之类的方法使对象过期时,我们看到状态被删除:

>>> session.expire(user)
>>> user.__dict__
{'_sa_instance_state': <...>}

我们看到,虽然内部“状态”仍然存在,但与idname列对应的值已消失。如果我们要访问这些列中的一个并且正在观察SQL,我们会看到:

>>> print(user.name)
SELECT user.id AS user_id, user.name AS user_name FROM user WHERE user.id = ? (1,)
user1

以上,在访问过期属性user.name时,ORM通过发出用户行的SELECT来启动lazy load以从数据库中检索最新状态这个用户提到的。之后,再次填充__dict__

>>> user.__dict__
{
  'id': 1, 'name': u'user1',
  '_sa_instance_state': <...>,
}

注意

当我们在__dict__里面查看时,为了看到SQLAlchemy用对象属性做些什么,我们不应该修改__dict__的内容。直接,至少就SQLAlchemy ORM所维护的属性而言(SQLA领域之外的其他属性都可以)。这是因为SQLAlchemy使用descriptors来跟踪我们对对象所做的更改,并且当我们直接修改__dict__时,ORM将无法跟踪我们改变了一些。

expire()refresh()的另一个关键行为是丢弃对象上所有未刷新的更改。也就是说,如果我们要修改User上的属性:

>>> user.name = 'user2'

但是我们在没有先调用flush()的情况下调用expire(),我们的'user2'的未决值将被丢弃:

>>> session.expire(user)
>>> user.name
'user1'

可以使用expire()方法将实例的所有ORM映射属性标记为“过期”:

# expire all ORM-mapped attributes on obj1
session.expire(obj1)

它也可以传递一个字符串属性名称列表,引用特定的属性来标记为过期:

# expire only attributes obj1.attr1, obj1.attr2
session.expire(obj1, ['attr1', 'attr2'])

refresh()方法有一个类似的接口,但不是过期,而是立即为对象的行发出立即的SELECT:

# reload all attributes on obj1
session.refresh(obj1)

refresh()也接受字符串属性名称列表,但与expire()不同,期望至少有一个名称是列映射属性的名称:

# reload obj1.attr1, obj1.attr2
session.refresh(obj1, ['attr1', 'attr2'])

Session.expire_all()方法允许我们实时调用Session中包含的所有对象的Session.expire()

session.expire_all()

什么实际上加载

标有expire()或加载refresh()的对象发出的SELECT语句根据以下几个因素而变化:

何时过期或刷新

只要会话引用的事务结束,Session就会自动使用到期功能。这意味着,无论何时Session.commit()Session.rollback()被调用,Session中的所有对象都会过期,到Session.expire_all()方法。其基本原理是事务的结束是一个划分点,在这个点上没有更多的上下文可用来了解数据库的当前状态,因为任何数量的其他事务都可能影响它。只有当新事务开始时,我们才能再次访问数据库的当前状态,此时可能发生了任何数量的更改。

当需要强制对象从数据库中重新加载其数据时,会使用Session.expire()Session.refresh()方法,那些知道当前数据状态可能已过时的情况。其原因可能包括:

第二个要点有一个重要的警告,即“还知道隔离规则实际上允许这些数据可见”。这意味着不能认为发生在另一个数据库连接上的UPDATE在本地仍然可见;在很多情况下,它不会。这就是为什么如果希望使用expire()refresh()来查看正在进行的事务之间的数据,理解隔离行为是非常重要的。

也可以看看

Session.expire()

Session.expire_all()

Session.refresh()

isolation - glossary explanation of isolation which includes links to Wikipedia.

The SQLAlchemy Session In-Depth - a video + slides with an in-depth discussion of the object lifecycle including the role of data expiration.