国家管理

优质
小牛编辑
130浏览
2023-12-01

Quickie对象状态简介

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

  • 瞬变 -不在会话中且未保存到数据库中的实例;即没有数据库标识。这类对象与ORM的唯一关系是它的类有一个 Mapper 与之相关的。

  • 悬而未决的 当你 Session.add() 一个暂时的实例,它将变为挂起。它实际上还没有被刷新到数据库中,但是下一次刷新发生时会被刷新。

  • 持久的 -存在于会话中且在数据库中有记录的实例。通过刷新使挂起的实例成为持久实例,或者通过查询数据库中的现有实例(或者将持久实例从其他会话移动到本地会话),可以获得持久实例。

  • 删除 -在刷新过程中被删除但事务尚未完成的实例。处于此状态的对象本质上与“挂起”状态相反;当会话的事务被提交时,对象将移动到分离状态。或者,当会话的事务回滚时,删除的对象将移动 back 到持久状态。

    在 1.1 版更改: “已删除”状态是新添加的会话对象状态,与“持久”状态不同。

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

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

获取对象的当前状态

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

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

参见

InstanceState.transient

InstanceState.pending

InstanceState.persistent

InstanceState.deleted

InstanceState.detached

会话属性

这个 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

(文件: Session.newSession.dirtySession.deletedSession.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)

上面,我们截取了 SessionEvents.pending_to_persistent()SessionEvents.detached_to_persistent()SessionEvents.deleted_to_persistent()SessionEvents.loaded_as_persistent() 事件挂钩,以便在对象进入 persistent 过渡,以及 SessionEvents.persistent_to_detached()SessionEvents.persistent_to_deleted() 当对象离开持久状态时,钩住对象。

上述函数可用于 Session 为了在Per上提供强大的引用行为 -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 对象-请参见部分 狗堆缓存 .

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

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

合并提示

Session.merge() 是一种非常有用的方法。然而,它处理的是瞬变/分离对象与持久对象之间的复杂边界,以及状态的自动转移。在这里,各种各样的场景都需要更仔细地了解对象的状态。合并的常见问题通常涉及与要传递给的对象有关的一些意外状态 Session.merge() .

让我们使用用户和地址对象的规范示例:

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)

假设A 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 到级联到的backref的持久对象 User.addresses 使我们 a1 对象挂起,就好像我们添加了它。现在我们有 two 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() 尽管在这种情况下,这意味着移除 save-update 层叠自 User.addresses 关系——通常,这种行为非常方便。这里的解决方案通常是不分配 a1.user 目标会话中已持久的对象。

这个 cascade_backrefs=False 选择权 relationship() 也会阻止 Address 通过添加到会话 a1.user = u1 指派。

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

意外状态的另一个示例:

>>> 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.expunge(obj1)

要删除所有项目,请致电 Session.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启动了 lazy load 要从数据库中检索最新状态,请为此用户引用的用户行发出一个选择。之后, __dict__ 再次填充:

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

注解

当我们窥视 __dict__ 为了了解SQLAlchemy对对象属性的作用,我们 不应修改 内容 __dict__ 直接地说,至少在sqlachemy orm维护的那些属性中(sqla领域之外的其他属性都很好)。这是因为sqlacalchemy使用 descriptors 为了跟踪我们对对象所做的更改,以及当我们修改 __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.expire() 在包含在 Session 马上::

session.expire_all()

这个 Session.refresh() 方法有一个类似的接口,但它不会过期,而是立即为对象的行发出立即选择:

# 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 style 查询使用 select() 以及来自 Query.populate_existing() 一种方法 Query1.x style 查询。使用此执行选项,将使用数据库中的数据刷新语句结果集中返回的所有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() 取决于几个因素,包括:

  • 过期属性的加载从触发 column-mapped attributes only . 而任何类型的属性都可以标记为过期,包括 relationship() -映射属性,访问过期的 relationship() 属性将只为该属性发出一个加载,使用标准的面向关系的延迟加载。列定向属性,即使过期,也不会作为此操作的一部分加载,而是在访问任何列定向属性时加载。

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

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

  • relationship() 属性配置为通过 relationship.lazy 在以下情况下将加载参数: Session.refresh() ,如果没有指定属性名,或者它们的名称包含在要刷新的属性列表中。

  • 配置为的属性 deferred() 通常不会在过期的属性加载期间或刷新期间加载。卸载的属性 deferred() 相反,当直接访问时,或者如果延迟属性的“组”的一部分(在该组中的已卸载属性被访问时)自行加载。

  • 对于在访问时加载的过期属性,联接继承表映射将发出一个选择,该选择通常只包括那些存在已卸载属性的表。这里的操作非常复杂,可以只加载父表或子表,例如,如果最初过期的列的子集只包含这些表中的一个或另一个表。

  • 什么时候? Session.refresh() 用于联接继承表映射,发出的选择将类似于 Session.query() 用于目标对象的类。这通常是作为映射的一部分设置的所有表。

何时到期或刷新

这个 Session 每当会话引用的事务结束时,自动使用过期功能。意思是,无论何时 Session.commit()Session.rollback() 调用,其中的所有对象 Session 已过期,使用的功能与 Session.expire_all() 方法。理由是,事务的结束是一个分界点,在该分界点上没有更多的上下文可供使用,以了解数据库的当前状态,因为任何数量的其他事务可能会影响数据库。只有当新事务启动时,我们才能再次访问数据库的当前状态,此时可能发生了任何数量的更改。

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

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

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

第二个要点有一个重要的警告:“众所周知,实际上隔离规则允许此数据可见。”这意味着不能假定在另一个数据库连接上发生的更新在本地仍然可见;在许多情况下,它不会。这就是为什么如果一个人想要使用 Session.expire()Session.refresh() 为了查看正在进行的事务之间的数据,了解实际的隔离行为是至关重要的。

参见

Session.expire()

Session.expire_all()

Session.refresh()

填充现有 -允许任何ORM查询按照正常加载的方式刷新对象,根据SELECT语句的结果刷新标识映射中的所有匹配对象。

isolation -包括维基百科链接的隔离术语解释。

The SQLAlchemy Session In-Depth -深入讨论对象生命周期(包括数据过期的作用)的视频+幻灯片。