SqlAlchemy 2.0 中文文档(五十四)(5)

简介: SqlAlchemy 2.0 中文文档(五十四)

SqlAlchemy 2.0 中文文档(五十四)(4)https://developer.aliyun.com/article/1563182


“此会话的事务由于刷新期间的先前异常已被回滚。”(或类似内容)

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

许多事情除了刷新之外,都可能导致 try/except 中的失败。 应用程序应确保对 ORM 导向的进程应用某种“框架”系统,以便连接和事务资源具有明确定界,并且如果发生任何失败条件,则可以显式回滚事务。

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

有关如何组织使用Session的详细讨论,请参见何时构建会话,何时提交会话,何时关闭会话?。

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

如果 Session.flush()  能部分完成然后不回滚,那将会很好,但是由于它当前的能力限制,这是不可能的,因为它的内部记录必须被修改,以便随时停止,并且与已刷新到数据库的内容完全一致。虽然这在理论上是可能的,但增强功能的有用性大大降低了,因为许多数据库操作在任何情况下都需要回滚。特别是  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()将错误地对其进行操作。 与其允许 SQL 操作在此处继续新事务,而正常用法规定要进行回滚的地方,则Session拒绝继续,直到显式回滚实际发生。

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

但为什么flush()坚持要发出一个 ROLLBACK 呢?

如果Session.flush()可以部分完成然后不回滚,那将是很好的,但是由于其当前能力范围之外,因为其内部记账必须被修改,以便它可以随时停止,并且与已经刷新到数据库的内容完全一致。  尽管理论上可能,但增强功能的实用性大大降低了,因为许多数据库操作无论如何都要求回滚。 特别是,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() 引起的回滚不是完整事务块的结束;虽然它结束了正在进行的数据库事务,在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),处理多行。去重允许我们按照查询时的方式接收对象,例如,所有User()对象其名称为'jack',对我们来说是一个对象,并且User.addresses集合被贪婪加载,就像在relationship()上使用lazy='joined'或通过joinedload()选项指示的那样。为了保持一致性,去重仍然适用于是否已建立连接加载,因为贪婪加载的核心理念是这些选项从不影响结果。
  • 消除关于身份映射的混淆 - 这显然是较不重要的原因。由于Session使用了一个身份映射,即使我们的 SQL 结果集有两行主键为 5 的记录,Session内也只有一个User(id=5)对象,必须以其身份唯一性进行维护,即其主键/类组合。如果查询User()对象,获取相同对象多次在列表中实际上没有太多意义。有序集合可能更能代表Query在返回完整对象时所寻求的内容。

Query 去重的问题仍然存在问题,主要原因是 Query.count() 方法不一致,当前状态是,在最近的发布中,联合急加载首先被“子查询急加载”策略所取代,更近期的是“选择 IN 急加载”策略,这两者通常更适用于集合急加载。随着这种演变的继续,SQLAlchemy 可能会改变 Query 的行为,这也可能涉及到新的 API,以更直接地控制这种行为,并且还可能改变联合急加载的行为,以创建更一致的使用模式。

我已经创建了一个针对 Outer Join 的映射,虽然查询返回了行,但没有返回对象。为什么?

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

当我尝试添加 WHERE、ORDER BY、LIMIT 等条件(这依赖于(外部)JOIN)时,我使用 joinedload()lazy=False 创建了一个 JOIN/OUTER JOIN,但 SQLAlchemy 在构造查询时出现了问题。

由联合急加载生成的连接仅用于完全加载相关集合,并且设计为不会影响查询的主要结果。由于它们是匿名别名,因此不能直接引用。

关于这种行为的详细信息,请参见 Joined Eager Loading 的禅意。

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!

如何在 ORM 查询中使用 Textual SQL?

请参阅:

  • 从文本语句获取 ORM 结果 - 使用 Query 进行自定义文本块。
  • 使用 SQL 表达式与会话 - 直接使用文本 SQL 与 Session

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

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

当我加载对象时,为什么我的__init__()没有被调用?

有关此行为的描述,请参阅 跨加载保持非映射状态。

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

SQLAlchemy 总是针对当前加载在 Session 中的依赖行发出 UPDATE 或 DELETE  语句。对于未加载的行,默认情况下会发出 SELECT 语句来加载这些行,并对其进行更新/删除;换句话说,它假定未配置 ON DELETE  CASCADE。要配置 SQLAlchemy 以配合 ON DELETE CASCADE,请参阅 使用 ORM 关系的外键 ON DELETE  cascade。

我将我的实例的“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

一个更简单的操作是单独使属性过期 - 这可以针对任何 persistent 对象使用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在行被插入之前没有意义;否则还没有行:

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?

该特性,详细说明请参见指向自身的行/相互依赖行,会在特定关系绑定的外键发生更改时发出 UPDATE 语句,除了会针对目标行通常发出的  INSERT/UPDATE/DELETE 之外。虽然此 UPDATE 语句的主要目的是与该行的 INSERT 或 DELETE  配对,以便它可以在后设置或前取消外键引用,以打破与相互依赖的外键的循环,但目前它也被捆绑为第二个 UPDATE,当目标行本身被 UPDATE  时发出。在这种情况下,post_update 发出的 UPDATE 通常 是不必要的,并且通常会显得浪费。

然而,一些研究试图消除这种“UPDATE / UPDATE”行为的努力表明,不仅需要在 post_update  实现中进行重大更改,还需要在与 post_update 无关的领域进行一些变更,以使其生效,因为在某些情况下,非 post_update  方面的操作顺序需要被颠倒,这反过来可能会影响其他情况,比如正确处理引用主键值的 UPDATE(参见#1063以获取概念验证)。

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

相关文章
|
2月前
|
SQL 缓存 关系型数据库
SqlAlchemy 2.0 中文文档(五十四)(2)
SqlAlchemy 2.0 中文文档(五十四)
56 1
|
2月前
|
SQL 缓存 关系型数据库
SqlAlchemy 2.0 中文文档(五十四)(3)
SqlAlchemy 2.0 中文文档(五十四)
20 1
|
2月前
|
SQL 关系型数据库 测试技术
SqlAlchemy 2.0 中文文档(五十四)(4)
SqlAlchemy 2.0 中文文档(五十四)
24 1
|
2月前
|
SQL 存储 数据库
SqlAlchemy 2.0 中文文档(五十四)(1)
SqlAlchemy 2.0 中文文档(五十四)
14 0
|
2月前
|
SQL 关系型数据库 PostgreSQL
SqlAlchemy 2.0 中文文档(五十九)(5)
SqlAlchemy 2.0 中文文档(五十九)
19 0
|
2月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(五十九)(4)
SqlAlchemy 2.0 中文文档(五十九)
14 0
|
2月前
|
SQL 关系型数据库 PostgreSQL
SqlAlchemy 2.0 中文文档(五十九)(7)
SqlAlchemy 2.0 中文文档(五十九)
12 0
|
2月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(五十九)(3)
SqlAlchemy 2.0 中文文档(五十九)
19 0
|
2月前
|
SQL Oracle 关系型数据库
SqlAlchemy 2.0 中文文档(五十九)(6)
SqlAlchemy 2.0 中文文档(五十九)
16 0
|
2月前
|
SQL 缓存 Oracle
SqlAlchemy 2.0 中文文档(五十九)(1)
SqlAlchemy 2.0 中文文档(五十九)
125 0