SQLAlchemy 2.0 文档
常见问题解答
- 安装
- 连接 / 引擎
- MetaData / Schema
- SQL 表达式
- ORM 配置
- 性能
- 会话 / 查询¶
- 我正在使用我的会话重新加载数据,但它没有看到我在其他地方提交的更改
- “由于之前在 flush 期间发生的异常,此会话的事务已回滚。” (或类似情况)
- 如何创建一个始终向每个查询添加特定过滤器的查询?
- 我的查询返回的对象数量与 query.count() 告诉我的数量不同 - 为什么?
- 我针对外连接创建了一个映射,虽然查询返回了行,但没有返回任何对象。为什么不呢?
- 我正在使用
joinedload()
或lazy=False
来创建 JOIN/OUTER JOIN,并且当我尝试添加 WHERE、ORDER BY、LIMIT 等 (依赖于 (OUTER) JOIN) 时,SQLAlchemy 没有构造正确的查询 - 查询没有
__len__()
,为什么没有? - 如何在 ORM 查询中使用文本 SQL?
- 我正在调用
Session.delete(myobject)
,但它没有从父集合中删除! - 为什么当我加载对象时,我的
__init__()
没有被调用? - 如何在 SA 的 ORM 中使用 ON DELETE CASCADE?
- 我在我的实例上将 “foo_id” 属性设置为 “7”,但 “foo” 属性仍然是
None
- 它不应该加载 id 为 #7 的 Foo 吗? - 如何遍历与给定对象相关的所有对象?
- 有没有一种方法可以自动拥有唯一的关键字 (或其他类型的对象),而无需查询关键字并获取对包含该关键字的行的引用?
- 为什么 post_update 除了第一次 UPDATE 之外还发出 UPDATE?
- 第三方集成问题
项目版本
- 上一篇: 性能
- 下一篇: 第三方集成问题
- 上级: 首页
- 在此页上
- 会话 / 查询
- 我正在使用我的会话重新加载数据,但它没有看到我在其他地方提交的更改
- “由于之前在 flush 期间发生的异常,此会话的事务已回滚。” (或类似情况)
- 如何创建一个始终向每个查询添加特定过滤器的查询?
- 我的查询返回的对象数量与 query.count() 告诉我的数量不同 - 为什么?
- 我针对外连接创建了一个映射,虽然查询返回了行,但没有返回任何对象。为什么不呢?
- 我正在使用
joinedload()
或lazy=False
来创建 JOIN/OUTER JOIN,并且当我尝试添加 WHERE、ORDER BY、LIMIT 等 (依赖于 (OUTER) JOIN) 时,SQLAlchemy 没有构造正确的查询 - 查询没有
__len__()
,为什么没有? - 如何在 ORM 查询中使用文本 SQL?
- 我正在调用
Session.delete(myobject)
,但它没有从父集合中删除! - 为什么当我加载对象时,我的
__init__()
没有被调用? - 如何在 SA 的 ORM 中使用 ON DELETE CASCADE?
- 我在我的实例上将 “foo_id” 属性设置为 “7”,但 “foo” 属性仍然是
None
- 它不应该加载 id 为 #7 的 Foo 吗? - 如何遍历与给定对象相关的所有对象?
- 有没有一种方法可以自动拥有唯一的关键字 (或其他类型的对象),而无需查询关键字并获取对包含该关键字的行的引用?
- 为什么 post_update 除了第一次 UPDATE 之外还发出 UPDATE?
会话 / 查询¶
我正在使用我的会话重新加载数据,但它没有看到我在其他地方提交的更改¶
关于此行为的主要问题是,会话的行为就好像事务处于可序列化隔离状态一样,即使它不是 (而且通常不是)。实际上,这意味着会话不会更改它在事务范围内已读取的任何数据。
如果 “隔离级别” 术语不熟悉,那么您首先需要阅读此链接
简而言之,可序列化隔离级别通常意味着一旦您在事务中 SELECT 一系列行,您每次重新发出该 SELECT 时都会获得相同的数据。如果您处于下一个较低的隔离级别 “可重复读取”,您将看到新添加的行 (并且不再看到已删除的行),但对于您已经加载的行,您将看不到任何更改。只有当您处于较低的隔离级别 (例如 “读取已提交”) 时,才有可能看到数据行更改其值。
有关在使用 SQLAlchemy ORM 时控制隔离级别的信息,请参阅 设置事务隔离级别 / DBAPI 自动提交。
为了极大地简化事情,Session
本身在完全隔离的事务方面工作,并且除非您告诉它,否则不会覆盖它已经读取的任何映射属性。尝试重新读取您已经在进行中事务中加载的数据的用例是一个不常见的用例,在许多情况下没有效果,因此这被认为是例外,而不是常态;为了在这种例外情况下工作,提供了几种方法来允许在正在进行的事务的上下文中重新加载特定数据。
为了理解当我们谈论 Session
时 “事务” 的含义,您的 Session
旨在仅在事务中工作。有关此概述,请参见 管理事务。
一旦我们弄清楚了我们的隔离级别是什么,并且我们认为我们的隔离级别设置得足够低,以便如果我们重新 SELECT 一行,我们应该在我们的 Session
中看到新数据,我们该如何看到它呢?
三种方法,从最常见到最不常见
我们只需结束我们的事务,并在下次使用我们的
Session
访问时通过调用Session.commit()
启动一个新事务 (请注意,如果Session
处于较少使用的 “自动提交” 模式,则也会调用Session.begin()
)。绝大多数应用程序和用例都没有因无法 “看到” 其他事务中的数据而产生任何问题,因为它们坚持这种模式,这是短生命周期事务最佳实践的核心。有关这方面的一些想法,请参见 我应该何时构造会话、何时提交会话以及何时关闭会话?。我们告诉我们的
Session
重新读取它已经读取的行,无论是在我们下次使用Session.expire_all()
或Session.expire()
查询它们时,还是使用refresh
立即在对象上进行操作。有关此详细信息,请参见 刷新 / 过期。我们可以运行整个查询,同时将它们设置为通过使用 “populate existing” 肯定会覆盖已加载的对象,因为它们会读取行。这是一个执行选项,在 填充现有 中进行了描述。
但请记住,如果我们的隔离级别为可重复读取或更高,则 ORM 无法看到行中的更改,除非我们启动新事务。
“由于之前在 flush 期间发生的异常,此会话的事务已回滚。” (或类似情况)¶
当 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 的方法和函数时,从最顶层调用函数的进程将位于一个块中,该块在成功完成一系列操作时提交事务,并在操作因任何原因 (包括 flush 失败) 而失败时回滚事务。还有使用函数装饰器或上下文管理器来实现类似结果的方法。采取哪种方法在很大程度上取决于正在编写的应用程序的类型。
有关如何组织 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()
支持 SAVEPOINT。使用 Session.begin_nested()
,您可以将可能在事务中失败的操作框架化,然后在保持封闭事务的同时 “回滚” 到其失败之前的点。
但是为什么一次自动调用 ROLLBACK 不够?为什么我必须再次 ROLLBACK?¶
由 flush() 引起的 rollback 不是完整事务块的结束;虽然它结束了正在进行的数据库事务,但从 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()
将错误地作用于该事务。与其允许 SQL 操作在正常用法指示即将发生回滚的地方的新事务中继续进行,不如 Session
拒绝继续,直到实际发生显式回滚。
换句话说,期望调用代码始终调用 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 没有构造正确的查询¶
由连接的迫切加载生成的连接仅用于完全加载相关的集合,并且旨在对查询的主要结果没有影响。由于它们是匿名别名的,因此无法直接引用它们。
有关此行为的详细信息,请参阅 连接的迫切加载的禅宗。
查询没有 __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!
如何在 ORM 查询中使用文本 SQL?¶
请参阅
从文本语句获取 ORM 结果 - 带有
Query
的临时文本块在会话中使用 SQL 表达式 - 直接将会话
Session
与文本 SQL 一起使用。
我正在调用 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()
的 “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
中,则它被称为 待定。这意味着对象的行尚未 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 事件的示例,以便协调外键属性与多对一关系的设置。
如何遍历与给定对象相关的所有对象?¶
具有其他相关对象的对象将对应于映射器之间设置的 relationship()
构造。此代码片段将迭代所有对象,并纠正循环
from sqlalchemy import inspect
def walk(obj):
deque = [obj]
seen = set()
while deque:
obj = deque.pop(0)
if obj in seen:
continue
else:
seen.add(obj)
yield obj
insp = inspect(obj)
for relationship in insp.mapper.relationships:
related = getattr(obj, relationship.key)
if relationship.uselist:
deque.extend(related)
elif related is not None:
deque.append(related)
该函数可以演示如下
Base = declarative_base()
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
bs = relationship("B", backref="a")
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
c_id = Column(ForeignKey("c.id"))
c = relationship("C", backref="bs")
class C(Base):
__tablename__ = "c"
id = Column(Integer, primary_key=True)
a1 = A(bs=[B(), B(c=C())])
for obj in walk(a1):
print(obj)
输出
<__main__.A object at 0x10303b190>
<__main__.B object at 0x103025210>
<__main__.B object at 0x10303b0d0>
<__main__.C object at 0x103025490>
有没有一种方法可以自动地只拥有唯一的关键字(或其他类型的对象),而无需查询关键字并获取对包含该关键字的行的引用?¶
当人们阅读文档中的多对多示例时,他们会遇到这样一个事实:如果您两次创建相同的 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 语句的排序需要放宽,从而导致其他边缘情况下的破坏。
flambé! 龙和 炼金术士 图像设计由 Rotem Yaari 创建并慷慨捐赠。
使用 Sphinx 7.2.6 创建。文档上次生成时间:2025 年 3 月 11 日星期二下午 02:40:17 EDT