了解实例在会话中可以具有的状态很有帮助:
Transient - 一个不在会话中的实例,不会保存到数据库中;即它没有数据库身份。这种对象与ORM唯一的关系是它的类有一个与它关联的mapper()
。
Pending - when you add()
a transient instance, it becomes pending. 它仍然没有被实际刷新到数据库,但它会在下一次刷新时发生。
持久 - 存在于会话中且在数据库中具有记录的实例。通过刷新来获得持久性实例,以便挂起的实例变为持久性,或通过查询数据库查找现有实例(或将其他会话的持久实例移动到本地会话中)。
已删除 - 在刷新中已删除的实例,但事务尚未完成。处于这种状态的对象本质上与“挂起”状态相反;当会话的事务提交时,对象将移至分离状态。或者,当会话的事务回滚时,被删除的对象将返回为持久状态。
版本1.1中已更改:“已删除”状态是与“持久”状态不同的新添加的会话对象状态。
Detached - 与数据库中的记录相对应或先前对应的实例,但当前不在任何会话中。分离的对象将包含数据库标识标记,但是因为它与会话没有关联,所以不知道该数据库标识是否实际存在于目标数据库中。分离的对象可以正常使用,除非它们无法加载先前标记为“已过期”的未加载属性或属性。
要深入了解所有可能的状态转换,请参阅描述每个转换的Object Lifecycle Events部分,以及如何以编程方式跟踪每个转换。
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)
merge()
从外部对象将状态转换为会话中新的或已存在的实例。它还将传入的数据与数据库状态进行协调,产生将用于下一次刷新的历史流,或者可以使得产生状态的简单“转移”而不产生更改历史或访问数据库。用法如下:
merged_object = session.merge(existing_object)
给定一个实例时,它遵循以下步骤:
它检查实例的主键。如果存在,它会尝试在本地标识映射中找到该实例。如果load=True
标志处于默认状态,它还会检查数据库中是否存在本地主键。
如果给定实例没有主键,或者在给定主键时没有找到实例,则创建一个新实例。
然后将给定实例的状态复制到位于/新创建的实例上。对于源实例上存在的属性,该值将传输到目标实例。对于源上不存在的映射属性,属性在目标实例上过期,放弃其现有值。
如果load=True
标志保留为其默认值,则此复制过程将发出事件并为源对象上存在的每个属性加载目标对象的卸载集合,以便可以调整传入状态数据库中存在什么。如果load
作为False
传递,则传入的数据将直接“加盖”而不会产生任何历史记录。
如merge
级联所示(请参阅Cascades),操作级联到相关的对象和集合。
新实例返回。
使用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()
将其保存到数据库中,以确保该文件中的数据用于形成结构中每个元素的主键。之后,当文件发生变化时,可以重新运行相同的进程,产生稍微不同的对象结构,然后再次merged
,Session
将会自动更新数据库以反映这些更改,使用主键从数据库加载每个对象,然后使用给定的新状态更新其状态。
应用程序正在将对象存储在内存缓存中,并同时被许多Session
对象共享。merge()
is used each time an object is retrieved from the cache to create a local copy of it in each Session
which requests it. 缓存的对象保持分离状态;只有它的状态被移动到本身对个人Session
对象本地的副本中。
在缓存用例中,通常使用load=False
标志来消除协调对象状态与数据库的开销。还有一个名为merge_result()
的merge()
的“批量”版本,设计用于与缓存扩展的Query
对象一起使用 - 请参阅section Dogpile Caching。
应用程序想要将一系列对象的状态转换为由工作线程或其他并发系统维护的Session
。merge()
makes a copy of each object to be placed into this new Session
. 在操作结束时,父线程/进程维护它所启动的对象,并且线程/工作者可以继续处理这些对象的本地副本。
在“线程/进程之间的传输”用例中,应用程序可能也希望使用load=False
标志以避免数据传输时出现开销和冗余SQL查询。
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()
清除从会话中删除对象,将持久实例发送到分离状态,并将待处理实例发送到瞬态:
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': <...>,
}
其中id
和name
指数据库中的那些列。_sa_instance_state
是SQLAlchemy内部使用的非数据库持久化值(它指向实例的InstanceState
)。虽然与本节不直接相关,但如果我们想要了解它,我们应该使用inspect()
函数来访问它)。
此时,我们的User
对象中的状态与加载的数据库行的状态匹配。但是在使用诸如Session.expire()
之类的方法使对象过期时,我们看到状态被删除:
>>> session.expire(user)
>>> user.__dict__
{'_sa_instance_state': <...>}
我们看到,虽然内部“状态”仍然存在,但与id
和name
列对应的值已消失。如果我们要访问这些列中的一个并且正在观察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语句根据以下几个因素而变化:
relationship()
映射属性,但访问过期的relationship()
属性将仅为该属性发出加载,使用标准的面向关系的延迟加载。面向列的属性(即使过期)不会作为此操作的一部分加载,而是在访问任何面向列的属性时加载。relationship()
映射的属性不会加载,以响应正在访问的到期的基于列的属性。refresh()
比expire()
更具限制性。调用refresh()
并传递仅包含关系映射属性的名称列表实际上会引发错误。无论如何,非急切加载relationship()
属性将不会包含在任何刷新操作中。lazy
参数配置为“急切加载”的relationship()
属性将在refresh()
的情况下加载,如果没有属性名称被指定,或者如果他们的名字被包含在要被刷新的属性列表中。deferred()
的属性通常不会加载。直接访问时,deferred()
的卸载属性自己加载,或者如果访问该组中的未加载属性的延迟属性“组”的一部分。refresh()
时,发出的SELECT类似于在目标对象的类上使用Session.query()
时的SELECT。这通常是所有那些设置为映射一部分的表。只要会话引用的事务结束,Session
就会自动使用到期功能。这意味着,无论何时Session.commit()
或Session.rollback()
被调用,Session
中的所有对象都会过期,到Session.expire_all()
方法。其基本原理是事务的结束是一个划分点,在这个点上没有更多的上下文可用来了解数据库的当前状态,因为任何数量的其他事务都可能影响它。只有当新事务开始时,我们才能再次访问数据库的当前状态,此时可能发生了任何数量的更改。
当需要强制对象从数据库中重新加载其数据时,会使用Session.expire()
和Session.refresh()
方法,那些知道当前数据状态可能已过时的情况。其原因可能包括:
Table.update()
construct were emitted using the Session.execute()
method;第二个要点有一个重要的警告,即“还知道隔离规则实际上允许这些数据可见”。这意味着不能认为发生在另一个数据库连接上的UPDATE在本地仍然可见;在很多情况下,它不会。这就是为什么如果希望使用expire()
或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.