状态管理


对象状态 Quickie 介绍


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


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


  • Pending - 当你 Session.add() 一个临时实例时,它变为 pending。它实际上仍未刷新到数据库,但会在下一次刷新发生时刷新。


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


  • 已删除 - 已在刷新中删除的实例,但 交易尚未完成。 处于此状态的对象本质上是 与 “pending” 状态相反;提交会话的事务时, 对象将移动到 detached 状态。 或者,当 会话的事务回滚,已删除的对象移动 返回到 persistent 状态。


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


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


获取对象的当前状态


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


例如:

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


另请参阅


检查映射实例 - 更多示例 实例状态


会话属性


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

for obj in session:
    print(obj)


并且可以使用常规的 “contains” 语义来测试 presence :

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.newSession.dirtySession.deleted,Session.identity_map)。


会话引用行为


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


使 Session 中的对象保持强 引用,通常只需要一种简单的方法。 例子 的外部管理的强引用行为包括 loading 对象添加到键到其主键的本地字典中,或发送到 列出或设置它们需要保留的时间范围 引用。这些集合可以与 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 标志保留为默认值,它还会检查数据库中的此主数据库 key (如果不在本地)。


  • 如果给定的实例没有主键,或者找不到具有给定主键的实例,则会创建一个新实例。


  • 然后,给定实例的状态将复制到找到的/新创建的实例上。对于源实例上存在的属性值,该值将传输到目标实例。对于源实例上不存在的属性值,目标实例上的相应属性将从内存中过期,这将丢弃该属性的目标实例中的任何本地存在的值,但不会直接修改该属性的数据库持久化值。


    如果 load=True 标志保留为默认值,则此复制过程将发出事件,并将为源对象上存在的每个属性加载目标对象的已卸载集合,以便可以将传入状态与数据库中存在的状态进行协调。如果加载 作为 False 传递,则传入数据将直接“标记”,而不会生成任何历史记录。


  • 该作将级联到相关对象和集合,如 merge cascade 所示(请参阅 Cascades)。


  • 返回新实例。


使用 Session.merge(),给定的 “source” 实例不会被修改,也不会与目标 Session 相关联,并且仍然可以与任意数量的其他 Session 合并 对象。 Session.merge() 对于获取任何类型的对象结构的状态而不考虑其来源或当前会话关联并将其状态复制到新会话中非常有用。以下是一些示例:


  • 从文件中读取对象结构并希望 保存到数据库可能会解析文件,构建 结构,然后使用 Session.merge() 将其保存到数据库中,确保文件中的数据用于构建结构体每个元素的主键。稍后,当文件更改时,可以重新运行相同的过程,产生一个略有不同的对象结构,然后可以再次合并Session 将自动更新数据库以反映这些更改,通过主键从数据库中加载每个对象,然后使用给定的新状态更新其状态。


  • 应用程序将对象存储在内存中的缓存中,由许多 Session 对象同时共享。会话.merge() 每次从缓存中检索对象以创建 在每个请求它的 Session 中,它的本地副本。缓存的对象将保持分离状态;只有它的状态被移动到单个 Session 的本地副本中 对象。


    在缓存用例中,通常使用 load=False 标志来消除协调对象状态的开销 与数据库一起使用。 还有一个 “bulk” 版本的 调用 Query.merge_result()Session.merge() 该 Query 旨在与缓存扩展的 Query 一起使用 objects - 请参阅 Dogpile 缓存 部分。


  • 应用程序希望将一系列对象的状态传输到由 worker 线程或其他并发系统维护的 Session 中。Session.merge() 会复制每个对象,并将其放入这个新的 Session 中。在作结束时,父线程/进程维护它开始使用的对象,并且线程/工作线程可以继续处理这些对象的本地副本。


    在“线程/进程之间的传输”用例中,应用程序可能还希望使用 load=False 标志,以避免在传输数据时产生开销和冗余的 SQL 查询。


合并提示


Session.merge() 对于许多用途来说都是一个非常有用的方法。但是,它处理瞬态/分离对象与持久对象之间的复杂边界,以及状态的自动传输。此处可能出现的各种方案通常需要更仔细地处理对象的状态。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 的 backref 的持久对象 并使我们的 A1 对象处于 pending 状态,就像我们添加了它一样。 现在我们有 会话中的两个Address 对象:

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


在上面,我们的 a1 已经在会话中待处理。随后的 Session.merge()作基本上什么都不做。Cascade 可以通过 relationship.cascade 进行配置 选项,但在这种情况下,这意味着从 User.addresses 关系 - 通常,该行为 非常方便。 这里的解决方案通常是不分配 a1.user 添加到目标会话中已持久化的对象。


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


有关级联作的更多详细信息,请参阅 Cascades


意外状态的另一个示例:

>>> 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()


删除


Expunge 从 Session 中删除对象,将持久实例发送到 detached 状态,并将 pending 实例发送到 transient 状态:

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': <...>,
}


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


此时,我们的 User 对象中的状态与加载的 database 行。 但是,在使用 Session.expire()中,我们看到 state 被删除了:

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


我们看到,虽然内部 “state” 仍然存在,但对应于 idname 列的值已经消失了。如果我们要访问这些列之一并正在监视 SQL,我们会看到以下内容:

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


在上面,在访问 expired 属性 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.expire() 而没有先调用 Session.flush() 时,我们的待处理值 'user2' 被丢弃了:

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


Session.expire() 方法可用于将实例的所有 ORM 映射属性标记为 “expired”:

# 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.expire()Session 中包含的所有对象 立即:

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 的 Populate Existing 功能,该功能可用于使用 select()2.0 样式查询以及 QueryQuery.populate_existing() 方法 在 1.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


请参阅 Populate Existing 以了解更多详细信息。


实际加载的内容


当对象标记为 Session.expire() 时发出的 SELECT 语句 或加载 Session.refresh() 的 Interface()取决于多种因素,包括:


  • 过期属性的加载仅从列映射属性触发。 虽然任何类型的属性都可以标记为过期,包括 relationship() - 映射的属性,访问过期的 relationship() 属性将仅使用标准的 面向关系的延迟加载。 面向列的属性,即使 expired 时,不会作为此作的一部分加载,而是在 访问任何面向列的属性。


  • relationship()- 映射的属性不会在响应访问过期的基于列的属性时加载。


  • 关于关系,Session.refresh()Session.expire() 对于非列映射的属性。调用 Session.refresh() 并传递仅包含关系映射属性的名称列表实际上会引发错误。在任何情况下,非预先加载的 relationship() 属性都不会包含在任何刷新作中。


  • relationship() 属性配置为 “预先加载” relationship.lazy 参数将在 Session.refresh()中,如果未指定属性名称,或者它们的名称包含在要刷新的属性列表中。


  • 配置为 deferred() 的属性通常不会在 expired-attribute 加载期间或刷新期间加载。相反,当直接访问时,或者如果属于 deferred(组)的 deferred()属性的一部分,则 deferred() 的 unloaded 属性会自行加载,其中访问了该组中的 unloaded 属性。


  • 对于在访问时加载的过期属性,联接继承表映射将发出 SELECT,该 SELECT 通常仅包含存在已卸载属性的那些表。此处的作足够复杂,可以仅加载父表或子表,例如,如果最初过期的列的子集仅包含其中一个表。


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


何时过期或刷新


每当 Session 引用的事务结束时,Session 都会自动使用 expiration 功能。意思是,每当 Session.commit()Session.rollback() 被调用时,Session 中的所有对象 过期,使用与 Session.expire_all() 等效的功能 方法。 基本原理是事务的结束是一个 没有更多上下文可用的分界点 数据库的当前状态是什么,就像任何其他事务一样 可能会影响它。 只有当新的交易开始时,我们才能再次获得访问权限 设置为数据库的当前状态,此时会进行任意数量的更改 可能已经发生。


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


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


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


第二项有一个重要的警告,即“众所周知,隔离规则有效 允许此数据可见。 这意味着不能假设 在另一个数据库连接上发生的 UPDATE 仍将在此处可见 本地;在许多情况下,它不会。 这就是为什么如果希望使用 Session.expire()Session.refresh() 为了查看正在进行的事务之间的数据,了解有效的隔离行为是必不可少的。


另请参阅


会话.expire()


Session.expire_all()


Session.refresh()


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


isolation - 隔离的词汇表解释,包括指向 Wikipedia 的链接。


SQLAlchemy 会议深度 - 视频 + 幻灯片,深入讨论了对象生命周期,包括数据过期的作用。