会话/查询

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

关于这种行为的主要问题是,会话的行为就好像事务处于可序列化隔离状态,即使它实际上并非如此(通常并非如此)。从实际角度来看,这意味着会话不会更改它已经在事务范围内读取的任何数据。

如果“隔离级别”这个词不熟悉,那么你需要先阅读以下链接

隔离级别

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

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

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

要理解当我们谈论 Session 时“事务”是什么意思,你的 Session 旨在只在一个事务中工作。关于这方面的内容,请参见 管理事务

一旦我们弄清楚了隔离级别,并且我们认为隔离级别设置得足够低,以便如果我们重新 SELECT 一行,我们应该在 Session 中看到新数据,那么我们如何看到它呢?

三种方法,从最常见到最不常见

  1. 我们只需通过调用 Session.commit() 结束我们与 Session 的事务并开始一个新事务(注意,如果 Session 处于较少使用的“自动提交”模式,那么也会调用 Session.begin())。绝大多数应用程序和用例在无法在其他事务中“看到”数据方面没有任何问题,因为它们坚持这种模式,这是短暂事务的最佳实践的核心。有关这方面的一些想法,请参见 何时构建 Session,何时提交它,以及何时关闭它?

  2. 我们告诉我们的 Session 重新读取它已经读取的行,无论是在下次查询时使用 Session.expire_all()Session.expire() 查询它们,还是立即对对象使用 refresh。有关这方面的详细信息,请参见 刷新/过期

  3. 我们可以在运行整个查询时,将它们设置为在读取行时绝对覆盖已加载的对象,方法是使用“填充现有”。这是一种在 填充现有 中描述的执行选项。

但请记住,除非我们开始一个新事务,否则 ORM 无法看到行中的更改,如果隔离级别是可重复读或更高。

“此会话的事务已回滚,因为先前在刷新期间发生异常。”(或类似内容)

这是一个错误,发生在 Session.flush() 抛出异常,回滚事务,但在对 Session 进行进一步操作时,没有显式调用 Session.rollback()Session.close()

它通常对应于一个应用程序,该应用程序在 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

除了刷新之外,很多事情都可能导致 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 提供了解决这两个问题的方法是支持 SAVEPOINT,通过 Session.begin_nested()。使用 Session.begin_nested(),您可以构建一个可能失败的操作,然后“回滚”到失败之前的点,同时保持包含事务。

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

由 flush() 引起的回滚不是完整事务块的结束;虽然它结束了正在进行的数据库事务,但从 Session 的角度来看,仍然存在一个现在处于非活动状态的事务。

给定一个这样的块

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

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

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

当上面的 flush() 失败时,代码仍然在由 try/commit/except/rollback 块构成的事务中。如果 flush() 要完全回滚逻辑事务,则意味着当我们到达 except: 块时,Session 将处于干净状态,准备在全新的事务中发出新的 SQL,并且对 Session.rollback() 的调用将是无序的。特别是,Session 在此时将开始一个新的事务,Session.rollback() 将错误地作用于该事务。Session 不会在正常使用情况下需要回滚的情况下允许 SQL 操作继续进行新的事务,而是拒绝继续,直到显式回滚真正发生。

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

如何创建始终为每个查询添加特定过滤器的查询?

请参阅 FilteredQuery 中的食谱。

我的查询返回的项目数量与 query.count() 的结果不一致,这是为什么?

Query 对象被要求返回一个 ORM 映射对象的列表时,它将根据主键对这些对象进行去重。也就是说,如果我们例如使用 使用 ORM 声明式表单定义表元数据 中描述的 User 映射,并且我们有一个像下面的 SQL 查询

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

上面,教程中使用的示例数据在 addresses 表中包含两行,对应于名为 'jack'users 行,主键值为 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 会进行去重有两个主要原因

  • 为了使关联式急切加载正常工作 - 关联式急切加载 通过对相关表使用连接查询行来工作,然后它将来自这些连接的行路由到引导对象的集合中。为了做到这一点,它必须获取引导对象主键重复出现的行,用于每个子条目。这种模式可以继续进入更深层次的子集合,这样,可能为一个引导对象处理多行,例如 User(id=5)。去重允许我们以查询的方式接收对象,例如,所有名为 'jack'User() 对象,对我们来说是一个对象,其中 User.addresses 集合被急切加载,如 relationship() 上的 lazy='joined' 或通过 joinedload() 选项所指示。为了保持一致性,无论是否建立了 joinedload,都会应用去重,因为急切加载的关键理念是,这些选项永远不会影响结果。

  • 为了消除关于身份映射的困惑 - 这确实是一个不那么重要的原因。由于 Session 使用了 身份映射,即使我们的 SQL 结果集中有两行主键为 5,Session 中只有一个 User(id=5) 对象,它必须在自己的身份上保持唯一,即,它的主键/类组合。实际上,如果有人正在查询 User() 对象,在列表中多次获得同一个对象是没有意义的。有序集合可能是对 Query 在返回完整对象时试图返回的内容的更好表示。

关于Query 去重的問題仍然存在,主要原因是Query.count() 方法不一致,目前状态是,连接预加载在最近的版本中已经被“子查询预加载”策略和最新的“select IN 预加载”策略取代,这两个策略通常更适合集合预加载。随着这种演进的继续,SQLAlchemy 可能会改变Query 上的这种行为,这可能包括新的 API 以更直接地控制这种行为,也可能改变连接预加载的行为,以创建一个更一致的用法模式。

我创建了一个映射到外连接的映射,虽然查询返回了行,但没有返回任何对象。为什么?

由外连接返回的行可能包含主键的一部分为 NULL,因为主键是两个表的组合。 Query 对象忽略了没有可接受主键的传入行。根据 Mapper 上的 allow_partial_pks 标志的设置,如果值至少有一个非 NULL 值,或者如果值没有 NULL 值,则主键被接受。见 Mapper 中的 allow_partial_pks

我使用 joinedload()lazy=False 创建一个 JOIN/OUTER JOIN,而当我尝试添加 WHERE、ORDER BY、LIMIT 等(依赖于(OUTER)JOIN)时,SQLAlchemy 并没有构建正确的查询。

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

有关此行为的详细信息,请参见 连接预加载的禅宗.

Query 没有 __len__(),为什么?

应用于对象的 Python __len__() 魔术方法允许使用 len() 内置函数来确定集合的长度。直观地,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__() 没有被调用?

有关此行为的说明,请参见 在加载之间维护非映射状态.

如何在 SA 的 ORM 中使用 ON DELETE CASCADE?

SQLAlchemy 将始终针对当前在 Session 中加载的依赖行发出 UPDATE 或 DELETE 语句。对于未加载的行,它默认会发出 SELECT 语句以加载这些行并更新/删除这些行;换句话说,它假设没有配置 ON DELETE CASCADE。要配置 SQLAlchemy 与 ON DELETE CASCADE 配合使用,请参见 使用外键 ON DELETE cascade 与 ORM 关系.

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

ORM 的构造方式不支持由外键属性更改驱动的关系的立即填充 - 相反,它的设计方式是反过来 - 外键属性由 ORM 在后台处理,最终用户自然地设置对象关系。因此,设置 o.foo 的推荐方法就是这样做 - 设置它!

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

外键属性的操作当然完全合法。但是,将外键属性设置为新值目前不会触发其所涉及的 relationship() 的“失效”事件。这意味着对于以下序列

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 中,它被称为 待处理。这意味着对象的行的 INSERT 操作尚未进行到数据库中。对于这样的对象,设置 foo_id 只有在行插入后才有意义;否则还没有行。

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,它就会被两次放入数据库中。这多少有些不方便。

这个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 语句的顺序,从而导致其他边缘情况的破坏。