状态管理

对象状态快速入门

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

  • 瞬态 (Transient) - 不在会话中且未保存到数据库的实例;即,它没有数据库标识。此对象与 ORM 的唯一关系是其类具有与之关联的 Mapper

  • 待定 (Pending) - 当您 Session.add() 一个瞬态实例时,它将变为待定状态。它仍然没有实际刷新到数据库,但将在下一次刷新发生时刷新。

  • 持久化 (Persistent) - 存在于会话中并在数据库中具有记录的实例。您可以通过刷新使待定实例变为持久化实例,或者通过查询数据库以获取现有实例(或将持久化实例从其他会话移动到本地会话)来获得持久化实例。

  • 已删除 (Deleted) - 在刷新中已被删除但事务尚未完成的实例。此状态中的对象本质上与“待定”状态相反;当会话的事务提交时,对象将移动到分离状态。或者,当会话的事务回滚时,已删除的对象将返回到持久化状态。

  • 分离 (Detached) - 对应或先前对应于数据库中记录的实例,但当前不在任何会话中。分离对象将包含数据库标识标记,但是由于它未与会话关联,因此无法知道此数据库标识是否实际存在于目标数据库中。分离对象可以安全地正常使用,但它们无法加载未加载的属性或先前标记为“过期”的属性。

要深入了解所有可能的状态转换,请参阅 对象生命周期事件 部分,该部分描述了每个转换以及如何以编程方式跟踪每个转换。

获取对象的当前状态

可以使用映射实例上的 inspect() 函数随时查看任何映射对象的实际状态;此函数将返回相应的 InstanceState 对象,该对象管理对象的内部 ORM 状态。InstanceState 除了其他访问器外,还提供指示对象持久化状态的布尔属性,包括

例如:

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

另请参阅

映射实例的检查 - InstanceState 的更多示例

Session 属性

Session 本身的作用有点像类似集合的集合。可以使用迭代器接口访问所有存在的项目

for obj in session:
    print(obj)

可以使用常规“contains”语义测试是否存在

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

(文档:Session.new, Session.dirty, Session.deleted, Session.identity_map)。

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() 将其保存到数据库中,确保文件中的数据用于构成结构中每个元素的主键。稍后,当文件发生更改时,可以重新运行相同的过程,生成略有不同的对象结构,然后可以再次 merged 进入,并且 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="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 的反向引用,并使我们的 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 分配给已在目标会话中持久化的对象。

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

有关级联操作的更多详细信息,请参见 级联

另一个意外状态的示例

>>> 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() 问题可以通过首先检查来检查 - 对象是否过早地进入会话?

>>> 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 中删除对象,将持久化实例发送到分离状态,并将待定实例发送到瞬态状态

session.expunge(obj1)

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

刷新 / 过期

过期意味着擦除一系列对象属性中保存的数据库持久化数据,以便在下次访问这些属性时,发出 SQL 查询,该查询将从数据库刷新该数据。

当我们谈论数据过期时,我们通常谈论的是 持久化 状态的对象。例如,如果我们按如下方式加载对象

user = session.scalars(select(User).filter_by(name="user1").limit(1)).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。之后,__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.flush() 的情况下调用 Session.expire(),我们待定的 '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() 方法的 Query1.x 样式 查询中。使用此执行选项,语句结果集中返回的所有 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.lazy 参数配置为“预加载”的 relationship() 属性,在 Session.refresh() 的情况下将会加载,如果未指定任何属性名称,或者它们的名称包含在要刷新的属性列表中。

  • 配置为 deferred() 的属性通常不会加载,无论是在过期属性加载期间还是在刷新期间。未加载的 deferred() 属性会在直接访问时自行加载,或者当它是“deferred”属性组的一部分,且该组中未加载的属性被访问时也会加载。

  • 对于在访问时加载的过期属性,连接继承表映射将发出一个 SELECT 语句,该语句通常只包含存在未加载属性的表。此处的操作足够复杂,例如,如果最初过期的列子集仅包含父表或子表之一,则仅加载父表或子表。

  • Session.refresh() 用于连接继承表映射时,发出的 SELECT 语句将类似于在目标对象的类上使用 Session.query() 时的情况。这通常是所有设置为映射一部分的表。

何时过期或刷新

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

Session.expire()Session.refresh() 方法用于在某些情况下,当人们想要强制对象从数据库重新加载其数据时,在已知数据当前状态可能过时的情况下。原因可能包括:

  • 在事务中,ORM 对象处理范围之外发出了一些 SQL,例如,如果使用 Table.update() 构造并通过 Session.execute() 方法发出;

  • 如果应用程序尝试获取已知在并发事务中已被修改的数据,并且也知道生效的隔离规则允许此数据可见。

第二个要点有一个重要的注意事项,即“也知道生效的隔离规则允许此数据可见”。这意味着不能假定在另一个数据库连接上发生的 UPDATE 操作将在此处本地可见;在许多情况下,它将不可见。这就是为什么如果希望使用 Session.expire()Session.refresh() 以查看正在进行的事务之间的数据,则对生效的隔离行为的理解至关重要。

另请参阅

Session.expire()

Session.expire_all()

Session.refresh()

Populate Existing - 允许任何 ORM 查询刷新对象,就像正常加载一样,根据 SELECT 语句的结果刷新身份映射中的所有匹配对象。

隔离性 - 隔离性的术语表解释,其中包含指向维基百科的链接。

SQLAlchemy Session 深度解析 - 包含视频和幻灯片,深入讨论对象生命周期,包括数据过期的作用。