SQLAlchemy 2.0 文档
- 上一章: Session 基础
- 下一章: 级联
- 上一级: 首页
- 本页内容
状态管理¶
对象状态快速介绍¶
了解实例在 Session 中可能具有的状态非常有用
瞬态 - 一个不在 Session 中且未保存到数据库中的实例;即它没有数据库标识。此类对象与 ORM 的唯一关系是其类与
Mapper
相关联。待处理 - 当您
Session.add()
一个瞬态实例时,它就变为待处理。它还没有真正刷新到数据库,但会在下次刷新时刷新。持久 - 一个存在于 Session 中且在数据库中有记录的实例。您可以通过刷新(使待处理实例变为持久)或从数据库查询现有实例(或将持久实例从其他 Session 移动到本地 Session)来获取持久实例。
已删除 - 一个在刷新中被删除的实例,但事务尚未完成。处于此状态的对象基本上处于“待处理”状态的相反状态;当 Session 的事务提交时,对象将变为分离状态。或者,当 Session 的事务回滚时,已删除的对象将返回到持久状态。
分离 - 一个对应或曾经对应数据库中记录的实例,但当前不在任何 Session 中。分离对象将包含数据库标识标记,但是由于它与 Session 不关联,因此无法确定此数据库标识是否实际存在于目标数据库中。分离对象可以正常使用,但它们无法加载未加载的属性或以前标记为“过期”的属性。
要深入了解所有可能的状态转换,请参阅部分 对象生命周期事件,其中描述了每个转换以及如何以编程方式跟踪每个转换。
获取对象的当前状态¶
可以使用映射实例上的 inspect()
函数随时查看任何映射对象的实际状态;此函数将返回相应的 InstanceState
对象,该对象管理对象的内部 ORM 状态。 InstanceState
提供了除其他访问器之外的布尔属性,指示对象的持久状态,包括
例如:
>>> from sqlalchemy import inspect
>>> insp = inspect(my_object)
>>> insp.persistent
True
另请参阅
映射实例的检查 - InstanceState
的更多示例
Session 属性¶
Session
本身有点像一个集合。所有存在的项目都可以使用迭代器接口访问
for obj in session:
print(obj)
可以使用常规的“包含”语义测试是否存在
if obj in session:
print("Object is present")
Session 还跟踪所有新创建的(即待处理的)对象、所有自上次加载或保存后发生更改的对象(即“脏”对象)以及所有标记为已删除的对象
# 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
(文档:Session.new
,Session.dirty
,Session.deleted
,Session.identity_map
)。
Session 引用行为¶
Session 中的对象是弱引用。这意味着当它们在外部应用程序中被取消引用时,它们也会从 Session
中超出范围,并会由 Python 解释器进行垃圾回收。例外情况包括待处理的对象、标记为已删除的对象或具有待处理更改的持久对象。在完全刷新后,这些集合都为空,所有对象再次被弱引用。
要使 Session
中的对象保持强引用,通常只需要使用简单的方法。外部管理的强引用行为的示例包括将对象加载到以其主键为键的本地字典中,或加载到列表或集合中,这些列表或集合在它们需要保持引用的时间范围内保持引用。如果需要,这些集合可以与 Session
相关联,方法是将它们放入 Session.info
字典中。
基于事件的方法也是可行的。一个为所有对象提供“强引用”行为的简单方法,这些对象在 持久 状态内保持引用,如下所示
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)
在上面,我们拦截了 SessionEvents.pending_to_persistent()
、SessionEvents.detached_to_persistent()
、SessionEvents.deleted_to_persistent()
和 SessionEvents.loaded_as_persistent()
事件钩子,以便在对象进入 持久 转换时拦截它们,以及 SessionEvents.persistent_to_detached()
和 SessionEvents.persistent_to_deleted()
钩子,以便在对象离开持久状态时拦截它们。
上述函数可以针对任何 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)
合并¶
Session.merge()
将状态从外部对象传输到会话中新的或已存在的实例。它还将传入数据与数据库状态进行协调,生成一个历史流,该流将应用于下次刷新,或者可以选择生成一个简单的状态“传输”,而不会生成更改历史记录或访问数据库。用法如下
merged_object = session.merge(existing_object)
给定一个实例时,它会按照以下步骤进行
它检查实例的主键。如果存在,它尝试在本地标识映射中找到该实例。如果
load=True
标志保留为默认值,如果本地找不到,它还会检查数据库以查找此主键。如果给定实例没有主键,或者找不到具有给定主键的实例,则会创建一个新实例。
然后将给定实例的状态复制到找到的/新创建的实例上。对于源实例上存在的属性值,将该值传输到目标实例。对于源实例上不存在的属性值,目标实例上的相应属性将从内存中 过期,这将丢弃目标实例中该属性的任何本地存在的价值,但不会对该属性的数据库持久化值进行任何直接修改。
如果
load=True
标志保留为默认值,此复制过程将发出事件并加载目标对象的未加载集合,这些集合对应于源对象上的每个存在的属性,以便可以将传入状态与数据库中的状态进行协调。如果load
作为False
传递,传入数据将直接“标记”,而不会生成任何历史记录。该操作级联到相关对象和集合,如
merge
级联所指示(参见 级联)。返回新的实例。
使用 Session.merge()
,给定的“源”实例不会被修改,也不会与目标 Session
关联,并且仍然可以与任意数量的其他 Session
对象合并。 Session.merge()
对于获取任何类型的对象结构的状态而无需考虑其来源或当前会话关联并将其状态复制到新会话很有用。以下是一些示例
从文件读取对象结构并希望将其保存到数据库的应用程序可能会解析文件、构建结构,然后使用
Session.merge()
将其保存到数据库,确保文件中的数据用于制定结构中每个元素的主键。稍后,当文件发生变化时,可以重新运行相同的过程,生成一个略有不同的对象结构,然后可以再次将其merge
,并且Session
会自动更新数据库以反映这些更改,通过主键从数据库加载每个对象,然后使用给定的新状态更新其状态。应用程序将对象存储在内存缓存中,该缓存由多个
Session
对象同时共享。Session.merge()
用于每次从缓存中检索对象以在每个请求它的Session
中创建其本地副本。缓存的对象保持分离;只有其状态被移动到其自身在各个Session
对象中本地的副本。在缓存用例中,通常使用
load=False
标志来消除将对象状态与数据库进行协调的开销。还有一个Session.merge()
的“批量”版本,称为Query.merge_result()
,它旨在与缓存扩展的Query
对象一起使用 - 请参阅部分 Dogpile 缓存。应用程序希望将一系列对象的状态转移到由工作线程或其他并发系统维护的
Session
中。Session.merge()
创建每个对象的副本以放置到此新的Session
中。在操作结束时,父线程/进程会维护它开始的对象,并且线程/工作器可以继续使用这些对象的本地副本。在“线程/进程之间传输”用例中,应用程序可能希望使用
load=False
标志来避免开销和冗余的 SQL 查询,因为数据正在传输。
合并技巧¶
Session.merge()
是一种非常有用的方法,适用于多种目的。但是,它处理瞬态/分离的对象和持久对象的微妙边界,以及状态的自动传递。这里可能出现的各种场景通常需要更仔细地处理对象的状态。合并的常见问题通常涉及传递到 Session.merge()
的对象的一些意外状态。
让我们使用 User 和 Address 对象的规范示例
class User(Base):
__tablename__ = "user"
id = mapped_column(Integer, primary_key=True)
name = mapped_column(String(50), nullable=False)
addresses = relationship("Address", backref="user")
class Address(Base):
__tablename__ = "address"
id = mapped_column(Integer, primary_key=True)
email_address = mapped_column(String(50), nullable=False)
user_id = mapped_column(Integer, ForeignKey("user.id"), nullable=False)
假设一个 User
对象具有一个 Address
,该对象已持久化
>>> u1 = User(name="ed", addresses=[Address(email_address="[email protected]")])
>>> 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
的反向引用,并将我们的 a1
对象置为挂起状态,就好像我们添加了它一样。现在我们在会话中拥有 *两个* Address
对象
>>> a1 = Address()
>>> a1.user = u1
>>> a1 in session
True
>>> existing_a1 in session
True
>>> a1 is existing_a1
False
在上面,我们的 a1
已经在会话中挂起。随后的 Session.merge()
操作实际上什么也没做。可以通过 relationship.cascade
选项在 relationship()
上配置级联,尽管在这种情况下,它将意味着从 User.addresses
关系中删除 save-update
级联 - 通常,这种行为非常方便。这里的解决方案通常是不要将 a1.user
分配给目标会话中已持久化的对象。
cascade_backrefs=False
是 relationship()
的一个选项,它将阻止 Address
通过 a1.user = u1
的赋值操作添加到 session 中。
有关级联操作的更多详细信息,请参见 级联。
另一个意外状态的例子
>>> a1 = Address(id=existing_a1.id, user_id=u1.id)
>>> a1.user = None
>>> a1 = session.merge(a1)
>>> session.commit()
sqlalchemy.exc.IntegrityError: (IntegrityError) address.user_id
may not be NULL
在上面,user
的赋值优先于 user_id
的外键赋值,最终导致 None
被应用于 user_id
,从而导致错误。
大多数 Session.merge()
问题可以通过首先检查以下内容来解决:对象是否过早地处于 session 中?
>>> a1 = Address(id=existing_a1, user_id=user.id)
>>> assert a1 not in session
>>> a1 = session.merge(a1)
或者对象上是否存在我们不希望存在的 state?检查 __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 中移除,将持久化实例发送到分离状态,将待处理实例发送到瞬态状态。
session.expunge(obj1)
要移除所有项,请调用 Session.expunge_all()
(此方法以前称为 clear()
)。
刷新/失效¶
失效 意味着在对象属性集中持有的数据库持久化数据被擦除,以使当这些属性下次被访问时,会发出一个 SQL 查询,从数据库刷新这些数据。
当我们谈论数据失效时,我们通常指的是处于 持久化 状态的对象。例如,如果我们加载一个对象,如下所示
user = session.scalars(select(User).filter_by(name="user1").limit(1)).first()
上面的 User
对象是持久化的,并具有一系列属性;如果我们要查看其 __dict__
,我们会看到加载的 state
>>> user.__dict__
{
'id': 1, 'name': u'user1',
'_sa_instance_state': <...>,
}
其中 id
和 name
指的是数据库中的那些列。 _sa_instance_state
是 SQLAlchemy 内部使用的一个非数据库持久化值(它指的是该实例的 InstanceState
。虽然与本节没有直接关系,但如果我们想访问它,我们应该使用 inspect()
函数来访问它)。
此时,我们 User
对象中的 state 与加载的数据库行匹配。但是,在使用诸如 Session.expire()
之类的函数使对象失效后,我们会发现 state 被移除了
>>> session.expire(user)
>>> user.__dict__
{'_sa_instance_state': <...>}
我们看到,虽然内部“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 发起了一个 延迟加载 来检索数据库中的最新 state,通过发出一个 SELECT 来获取该用户所引用的用户行。之后, __dict__
再次被填充
>>> user.__dict__
{
'id': 1, 'name': u'user1',
'_sa_instance_state': <...>,
}
注意
虽然我们正在查看 __dict__
的内部以了解 SQLAlchemy 对对象属性的处理方式,但我们不应该直接修改 __dict__
的内容,至少对于 SQLAlchemy ORM 维护的那些属性来说是这样的(SQLA 范围之外的其他属性是可以的)。这是因为 SQLAlchemy 使用 描述符 来跟踪我们对对象的更改,当我们直接修改 __dict__
时,ORM 将无法跟踪我们对某项内容的更改。
Session.expire()
和 Session.refresh()
的另一个关键行为是,它们会丢弃对象上所有未刷新的更改。也就是说,如果我们要修改 User
上的一个属性
>>> user.name = "user2"
但随后我们调用 Session.expire()
而不先调用 Session.flush()
,我们对 'user2'
的待处理值将被丢弃
>>> session.expire(user)
>>> user.name
'user1'
Session.expire()
方法可以用于将实例的所有 ORM 映射属性标记为“失效”
# expire all ORM-mapped attributes on obj1
session.expire(obj1)
它还可以接收一个字符串属性名称列表,引用要标记为失效的特定属性。
# expire only attributes obj1.attr1, obj1.attr2
session.expire(obj1, ["attr1", "attr2"])
Session.expire_all()
方法允许我们实质上对 Session
中包含的所有对象同时调用 Session.expire()
session.expire_all()
Session.refresh()
方法具有类似的接口,但它不是失效,而是立即为对象的行发出一个 SELECT 语句
# reload all attributes on obj1
session.refresh(obj1)
Session.refresh()
也接受一个字符串属性名称列表,但与 Session.expire()
不同,它期望至少一个名称是列映射属性的名称
# reload obj1.attr1, obj1.attr2
session.refresh(obj1, ["attr1", "attr2"])
提示
另一种通常更灵活的刷新方法是使用 ORM 的 填充现有对象 功能,该功能适用于 2.0 风格 查询,使用 select()
以及 Query.populate_existing()
方法,该方法位于 1.x 风格 查询中的 Query
中。使用此执行选项,语句结果集中返回的所有 ORM 对象都将使用数据库中的数据刷新。
stmt = (
select(User)
.execution_options(populate_existing=True)
.where((User.name.in_(["a", "b", "c"])))
)
for user in session.execute(stmt).scalars():
print(user) # will be refreshed for those columns that came back from the query
有关详细信息,请参见 填充现有对象。
实际加载的内容¶
当使用 Session.expire()
标记的对象或使用 Session.refresh()
加载的对象时发出的 SELECT 语句因几个因素而异,包括
失效属性的加载仅由列映射属性触发。虽然任何类型的属性都可以标记为失效,包括
relationship()
映射属性,但访问失效的relationship()
属性将仅为该属性发出加载操作,使用标准的关系型延迟加载。列定向属性,即使是失效的,也不会在该操作期间加载,而是会在访问任何列定向属性时加载。relationship()
映射属性不会响应访问失效的基于列的属性而加载。关于关系,
Session.refresh()
在非列映射属性方面比Session.expire()
更严格。调用Session.refresh()
并传递一个仅包含关系映射属性的名称列表,实际上会引发错误。在任何情况下,非急切加载的relationship()
属性将不会包含在任何刷新操作中。relationship()
属性配置为“急切加载”,通过relationship.lazy
参数,将在Session.refresh()
的情况下加载,如果未指定属性名称,或者属性名称包含在要刷新的属性列表中。配置为
deferred()
的属性通常不会加载,无论是过期属性加载还是刷新期间。未加载的属性是deferred()
,而是直接访问时加载,或者如果它属于一个“组”中的未加载属性,其中该组中的一个未加载属性被访问。对于按访问加载的过期属性,联接继承表映射将发出一个 SELECT,通常只包括存在未加载属性的那些表。此处的操作足够复杂,可以只加载父表或子表,例如,如果最初过期的列子集只包含这些表中的一个。
当
Session.refresh()
用于联接继承表映射时,发出的 SELECT 将类似于在目标对象类上使用Session.query()
时发出的 SELECT。这通常是作为映射的一部分设置的所有这些表。
何时过期或刷新¶
Session
在会话引用的事务结束时自动使用过期功能。这意味着,每当 Session.commit()
或 Session.rollback()
被调用时,Session
中的所有对象都会过期,使用与 Session.expire_all()
方法等效的功能。其基本原理是,事务的结束是一个界限点,在该点,没有更多上下文可用,因此无法知道数据库的当前状态,因为可能存在任意数量的其他事务正在影响它。只有在新事务开始时,我们才能再次访问数据库的当前状态,此时可能已经发生了一些更改。
Session.expire()
和 Session.refresh()
方法用于那些希望强制对象从数据库重新加载其数据的用例,在那些已知数据的当前状态可能已过时的情况下。这样做的原因可能包括
在 ORM 对象处理范围之外,已在事务中发出了一些 SQL,例如,如果使用
Session.execute()
方法发出了Table.update()
结构;如果应用程序试图获取已知在并发事务中被修改的数据,并且还已知生效的隔离规则允许这些数据可见。
第二点有一个重要的警告,即“还已知生效的隔离规则允许这些数据可见”。这意味着不能假设在另一个数据库连接上发生的 UPDATE 将在本地可见;在许多情况下,它不会。这就是为什么如果希望使用 Session.expire()
或 Session.refresh()
来查看正在进行的事务之间的数据,那么了解生效的隔离行为至关重要。
另请参阅
填充现有 - 允许任何 ORM 查询刷新对象,就像它们通常加载一样,根据 SELECT 语句的结果,在标识映射中刷新所有匹配的对象。
隔离 - 隔离的词汇解释,包括指向维基百科的链接。
深入了解 SQLAlchemy 会话 - 视频和幻灯片,深入探讨对象生命周期,包括数据过期的作用。
flambé! 龙和炼金术士图像设计由 Rotem Yaari 创建并慷慨捐赠。
使用 Sphinx 7.2.6 创建。最后生成文档:Fri 08 Nov 2024 08:41:19 AM EST