会话 / 查询


我正在使用我的 Session 重新加载数据,但它没有看到我在其他地方提交的更改


关于此行为的主要问题是 session 的行为就好像事务处于 serializable isolation 状态一样,即使它不是(通常不是)。实际上,这意味着 session 不会更改它在 transaction 范围内已经读取的任何数据。


如果 “isolation level” 这个术语不熟悉,那么您首先需要阅读此链接:


隔离级别


简而言之,可序列化隔离级别通常意味着 一旦您在事务中 SELECT 一系列行,您将获得 每次重新发出该 SELECT 时,都会返回相同的数据。如果您处于下一个较低的隔离级别,即 “repeatable read”,您将看到新添加的行(并且不再看到已删除的行),但对于加载的行,您不会看到任何变化。只有当您处于较低的隔离级别(例如 “read committed”)时,才有可能看到一行数据更改其值。


有关在使用 SQLAlchemy ORM 时控制隔离级别的信息,请参阅设置事务隔离级别/DBAPI AUTOCOMMIT


为了大大简化事情,Session 本身就是一个完全隔离的事务来工作,除非你告诉它,否则它不会覆盖已经读取的任何 Map 属性。尝试重新读取已在正在进行的事务中加载的数据的用例是一种不常见的用例,在许多情况下没有任何效果,因此这被认为是例外,而不是常态;为了解决此异常,提供了多种方法,以允许在正在进行的事务的上下文中重新加载特定数据。


要理解我们在谈论 Session,则您的 Session 只能在事务中工作。有关此内容的概述,请参阅 管理事务.


一旦我们弄清楚了我们的隔离级别是什么,并且我们认为我们的隔离级别被设置在一个足够低的级别,这样如果我们重新 SELECT 一行,我们应该在 Session 中看到新数据,我们如何看到它?


三种方式,从最常见到最不常见:


  1. 我们只需通过调用 Session.commit() 结束我们的事务,并在下次访问我们的 Session 时启动一个新的事务(请注意,如果 Session 处于较少使用的 “autocommit” 模式,也会调用 Session.begin())。这 绝大多数应用程序和用例都没有任何问题 无法“看到”其他事务中的数据,因为 他们坚持这种模式,这是最佳实践的核心 短期交易。有关此内容的一些想法,请参阅何时构造 Session、何时提交以及何时关闭它?


  2. 我们告诉 Session 重新读取已经读取的行,或者当我们下次使用 Session.expire_all() 查询它们时 或 Session.expire()的 Session.expire() 方法,或者立即在对象上使用 刷新。有关此内容的详细信息,请参阅 Refreshing / Expiring


  3. 我们可以运行整个查询,同时将它们设置为绝对覆盖 已加载的对象,因为它们使用 “populate existing” 读取行。 这是 填充现有


但请记住,如果我们的隔离 level 是 repeatable read 或更高级别,除非我们启动新的事务


“由于刷新期间的上一个异常,此 Session 的事务已回滚。”(或类似名称)


Session.flush() 引发异常,回滚事务,但在没有显式调用 Session.rollback()Session.close() 的情况下调用 Session 上的进一步命令时,会发生此错误。


它通常对应于在 Session.flush() Session.commit() 上捕获异常并且没有正确处理异常的应用程序。例如:

from sqlalchemy import create_engine, Column, Integer
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base(create_engine("sqlite://"))


class Foo(Base):
    __tablename__ = "foo"
    id = Column(Integer, primary_key=True)


Base.metadata.create_all()

session = sessionmaker()()

# constraint violation
session.add_all([Foo(id=1), Foo(id=1)])

try:
    session.commit()
except:
    # ignore error
    pass

# continue using session without rolling back
session.commit()


Session 的使用应该适合类似于这样的结构:

try:
    # <use session>
    session.commit()
except:
    session.rollback()
    raise
finally:
    session.close()  # optional, depends on use case


除了 flush 之外,许多事情都可能导致 try/except 失败。应用程序应确保将一些“框架”系统应用于面向 ORM 的进程,以便连接和事务资源具有明确的边界,并且如果发生任何故障情况,事务可以显式回滚。


这并不意味着整个应用程序应该有 try/except 块,这将不是一个可扩展的架构。相反,一种典型的方法是,当首次调用面向 ORM 的方法和函数时,从最顶部调用函数的进程将位于一个块内,该块在成功完成一系列作时提交事务,如果作因任何原因失败,包括刷新失败,则回滚事务。还有一些方法使用函数装饰器或上下文管理器来实现类似的结果。所采用的方法类型在很大程度上取决于所编写的应用程序类型。


有关如何组织 Session 使用的详细讨论,请参阅何时构造 Session、何时提交以及何时关闭它?


但是为什么 flush() 坚持发出 ROLLBACK?


如果 Session.flush() 可以部分完成,然后不回滚,那就太好了,但是这超出了它目前的能力,因为它的内部簿记必须修改,以便它可以随时停止,并与刷新到数据库的内容完全一致。虽然这在理论上是可能的,但由于许多数据库作在任何情况下都需要 ROLLBACK 这一事实,因此增强功能的有用性大大降低。特别是 Postgres 的一些作,一旦失败,就不允许事务继续:

test=> create table foo(id integer primary key);
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "foo_pkey" for table "foo"
CREATE TABLE
test=> begin;
BEGIN
test=> insert into foo values(1);
INSERT 0 1
test=> commit;
COMMIT
test=> begin;
BEGIN
test=> insert into foo values(1);
ERROR:  duplicate key value violates unique constraint "foo_pkey"
test=> insert into foo values(2);
ERROR:  current transaction is aborted, commands ignored until end of transaction block


SQLAlchemy 提供的解决这两个问题的是通过 Session.begin_nested() 中。使用 Session.begin_nested(),您可以在事务中构建可能失败的作,然后在维护封闭事务的同时“回滚”到其失败之前的点。


但是,为什么对 ROLLBACK 的一次自动调用还不够呢?为什么我必须再次 ROLLBACK?


由 flush() 引起的回滚并不是完整事务块的结束;当它结束正在运行的数据库事务时,从 Session 从角度来看,仍有一个事务现在处于 INACTIVE 状态。


给定一个块,例如:

sess = Session()  # begins a logical transaction
try:
    sess.flush()

    sess.commit()
except:
    sess.rollback()


在上面,当第一次创建 Session 时,假设没有使用 “autocommit mode”,则会在 Session 中建立一个逻辑事务。此事务是“逻辑”的,因为它实际上不会使用任何数据库资源,直到调用 SQL 语句,此时将启动连接级别和 DBAPI 级别的事务。但是,无论数据库级事务是否是其状态的一部分,逻辑事务都将保持原位,直到使用 Session.commit() 结束。 Session.rollback()Session.close() 的 Session.close() 进行访问。


当上面的 flush() 失败时,代码仍在由 try/commit/except/rollback 块构建的事务中。如果 flush() 完全 回滚逻辑事务,这意味着当我们到达 except: block 的 Session 将处于干净状态,准备好 在全新的事务上发出新 SQL,并调用 Session.rollback() 将不按顺序运行。 具体而言, 此时 Session 将开始一个新的事务,该事务的 Session.rollback() 将错误地作用于。在正常使用指示即将发生回滚的地方,不允许 SQL作对新事务进行,而是 Session 而是拒绝继续,直到显式回滚实际发生。


换句话说,预期调用代码将始终调用 Session.commit()、Session.rollback()Session.close() 对应当前事务块。 flush() 保留 session 中,以便上述代码的行为是可预测且一致的。


如何制作始终为每个查询添加特定过滤器的 Query?


请参阅 FilteredQuery 中的配方。


我的 Query 返回的对象数量与 query.count() 告诉我的对象数量不同 - 为什么?


当要求 Query 对象返回 ORM 映射对象列表时,将根据主键删除重复的对象。也就是说,如果我们例如使用 ORM 声明式表单定义表元数据中描述的 User 映射,并且我们有一个如下所示的 SQL 查询:

q = session.query(User).outerjoin(User.addresses).filter(User.name == "jack")


在上面,本教程中使用的示例数据在地址中有两行 表,用于名称为 'jack' 且主键值为 5 的用户行。如果我们对 Query.count() 进行上述查询,我们将得到答案 2

>>> q.count()
2


但是,如果我们运行 Query.all() 或迭代查询,我们会返回 一个元素

>>> q.all()
[User(id=5, name='jack', ...)]


这是因为当 Query 对象返回完整实体时,它们将被删除重复数据。如果我们改为请求返回单个列,则不会发生这种情况:

>>> session.query(User.id, User.name).outerjoin(User.addresses).filter(
...     User.name == "jack"
... ).all()
[(5, 'jack'), (5, 'jack')]


Query 将删除重复数据有两个主要原因:


  • 要使联接预先加载正常工作,请执行以下作 - 联接预先加载 的工作原理是使用针对相关表的联接查询行,然后在其中路由 这些行将联接到 lead 对象上的集合中。 为此, 它必须获取每个 Lead Object Primary Key 重复的行 子条目。 然后,此模式可以继续到其他子集合中,例如 可以为单个 lead 对象处理多个行,例如 用户 (id=5) 。删除允许我们按照查询对象的方式接收对象,例如所有名称为 'jack'User() 对象,对我们来说是一个对象,User.addresses 集合被预先加载,如 relationship() 上的 lazy='joined' 或通过 joinedload() 指示的那样 选择。 为了保持一致性,无论是否 JoinedLoad 是 Eager Load 背后的关键理念 是这些选项永远不会影响结果。


  • 为了消除对身份映射的混淆 - 这无疑是不太重要的原因。作为会话 使用身份映射,即使我们的 SQL 结果集有两行主键为 5,Session 中也只有一个 User(id=5) 对象 它必须在其身份上唯一维护,即其主键 / 类组合。 如果查询 User() 对象,以在列表中多次获取相同的对象。有序集可能会更好地表示 Query 寻求在返回完整对象时返回。


查询重复数据删除的问题仍然存在问题,主要是因为 Query.count() 方法不一致,当前状态是联接的预先加载在最近的版本中首先被“子查询预先加载”策略和最近的“select IN 预先加载”策略所取代,这两种策略通常更适合集合预先加载。随着这种演变的继续,SQLAlchemy 可能会改变 Query 上的这种行为,这也可能涉及新的 API,以便更直接地控制这种行为,也可能改变联接的预先加载的行为,以创建更一致的使用模式。


我创建了一个针对 Outer Join 的映射,当查询返回行时,没有返回任何对象。为什么不呢?


外部联接返回的行可能包含部分主键的 NULL,因为主键是两个表的组合。Query 对象会忽略没有可接受主键的传入行。根据 allow_partial_pks 的设置 标志,如果该值至少有一个非 NULL 值,或者如果该值没有 NULL 值,则接受主键。查看allow_partial_pksMapper


我正在使用 joinedload()lazy=False 来创建 JOIN/OUTER JOIN,但当我尝试添加 WHERE、ORDER BY、LIMIT 等时,SQLAlchemy 没有构建正确的查询(它依赖于(外部)JOIN)


由 join 预先加载生成的 join 仅用于完全加载相关集合,并且旨在对查询的主要结果没有影响。由于它们是匿名别名,因此无法直接引用。


有关此行为的详细信息,请参阅 The Zen of Joined Eager Loading


Query 没有 __len__(),为什么不呢?


应用于对象的 Python __len__() 魔术方法允许 len() builtin 用于确定集合的长度。它很直观 SQL 查询对象会将 __len__() 链接到 Query.count() 方法,该方法会发出 SELECT COUNT。这是不可能的,因为将查询作为列表进行评估将产生两个 SQL 调用,而不是一个:

class Iterates:
    def __len__(self):
        print("LEN!")
        return 5

    def __iter__(self):
        print("ITER!")
        return iter([1, 2, 3, 4, 5])


list(Iterates())


输出:

ITER!
LEN!


如何将文本 SQL 与 ORM 查询一起使用?


看:


我正在调用 Session.delete(myobject),但它没有从父集合中删除!


有关此行为的说明,请参阅有关删除 - 删除从集合和标量关系引用的对象的说明。


为什么我在加载对象时没有调用 __init__()?


有关此行为的描述,请参阅 Maintain Non-Mapped State Across Loads (跨负载保持非映射状态)。


如何将 ON DELETE CASCADE 与 SA 的 ORM 一起使用?


SQLAlchemy 将始终为当前在 Session 中加载的依赖行发出 UPDATE 或 DELETE 语句。 对于其中的行 未加载,则默认情况下会发出 SELECT 语句来加载 这些行并更新/删除它们;换句话说,它假设 未配置 ON DELETE CASCADE。 要配置 SQLAlchemy 以配合 ON DELETE CASCADE,请参阅 将外键 ON DELETE 级联与 ORM 关系一起使用


我将实例上的“foo_id”属性设置为“7”,但“foo”属性仍然是 None - 它不应该加载 id 为 #7 的 Foo 吗?


ORM 的构建方式不是为了支持由外键属性更改驱动的立即关系填充 - 相反,它被设计为以相反的方式工作 - 外键属性由 ORM 在幕后处理,最终用户自然地设置对象关系。因此,设置 o.foo 的推荐方法是这样做 - 设置 it!:

foo = session.get(Foo, 7)
o.foo = foo
Session.commit()


纵外键属性当然是完全合法的。但是,将 foreign-key 属性设置为新值当前不会触发涉及它的 relationship() 的 “expire” 事件。这意味着,对于以下序列:

o = session.scalars(select(SomeClass).limit(1)).first()

# assume the existing o.foo_id value is None;
# accessing o.foo will reconcile this as ``None``, but will effectively
# "load" the value of None
assert o.foo is None

# now set foo_id to something.  o.foo will not be immediately affected
o.foo_id = 7


o.foo 在加载其有效数据库值 None 时 首先访问。 设置 o.foo_id = 7 的值将为 “7” 作为待处理更改,但未发生刷新 - 因此 o.foo 仍为 None

# attribute is already "loaded" as None, has not been
# reconciled with o.foo_id = 7 yet
assert o.foo is None


对于基于外键的加载 o.foo,通常在提交后自然实现,这既会刷新新的外键值,又会使所有状态过期:

session.commit()  # expires all attributes

foo_7 = session.get(Foo, 7)

# o.foo will lazyload again, this time getting the new object
assert o.foo is foo_7


一个更简单的作是单独使属性过期 - 这可以使用 Session.expire() 对任何持久对象执行:

o = session.scalars(select(SomeClass).limit(1)).first()
o.foo_id = 7
Session.expire(o, ["foo"])  # object must be persistent for this

foo_7 = session.get(Foo, 7)

assert o.foo is foo_7  # o.foo lazyloads on access


请注意,如果对象不是持久对象,但存在于 Session 中,则称为 pending。这意味着对象的行尚未 INSERT 到数据库中。对于此类对象,在插入行之前,设置 foo_id 没有意义;否则还没有 ROW:

new_obj = SomeClass()
new_obj.foo_id = 7

Session.add(new_obj)

# returns None but this is not a "lazyload", as the object is not
# persistent in the DB yet, and the None value is not part of the
# object's state
assert new_obj.foo is None

Session.flush()  # emits INSERT

assert new_obj.foo is foo_7  # now it loads


配方 ExpireRelationshipOnFKChange 包含一个使用 SQLAlchemy 事件的示例,以便将外键属性的设置与多对一关系进行协调。


有没有办法自动只拥有唯一的关键字(或其他类型的对象),而无需对关键字进行查询并获取对包含该关键字的行的引用?


当人们阅读文档中的多对多示例时,他们会发现如果你两次创建相同的 Keyword,它会被放入 DB 中两次。这有点不方便。


创建此 UniqueObject 配方是为了解决此问题。


为什么 post_update 除了第一个 UPDATE 之外还会发出 UPDATE?


post_update功能记录在指向自身的行/相互依赖的行中,除了通常为目标行发出的 INSERT/UPDATE/DELETE 之外,还涉及响应对特定关系绑定外键的更改而发出 UPDATE 语句。虽然此 UPDATE 语句的主要目的是它与该行的 INSERT 或 DELETE 配对,以便它可以后设置或预先取消设置外键引用,以便打破具有相互依赖的外键的循环,但它目前也被捆绑为第二个 UPDATE,当目标行本身受到 UPDATE 约束时发出。在这种情况下,post_update 发出的 UPDATE 通常是不必要的,并且通常会显得浪费。


然而,一些关于尝试删除这种 “UPDATE / UPDATE” 行为的研究表明,不仅需要在整个 post_update 实现过程中对工作单元进行重大更改,还需要在与 post_update 无关的领域进行重大更改才能使其正常工作,因为在某些情况下,非 post_update 端的作顺序需要颠倒。 这反过来又会影响其他情况,例如正确处理引用的主键值的 UPDATE(有关概念验证,请参阅 #1063)。


答案是 “post_update” 用于打破两个相互依赖的外键之间的循环,并且将这种循环打破仅限于目标表的 INSERT/DELETE 意味着需要放宽其他地方的 UPDATE 语句的排序,从而导致其他边缘情况下的中断。