会话/查询¶

我正在用会话加载数据,但没有看到我在其他地方所做的更改。

关于这种行为的主要问题是,会话的作用就像事务在 可串行化的 隔离状态,即使不是(通常不是)。实际上,这意味着会话不会更改在事务范围内已经读取的任何数据。

如果不熟悉术语“隔离级别”,则首先需要阅读此链接:

Isolation Level

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

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

为了大大简化事情, Session 它本身的工作方式是完全独立的事务,并且不会覆盖它已经读取的任何映射属性,除非您告诉它。尝试重新读取已在正在进行的事务中加载的数据的用例是 罕见的 在许多情况下没有效果的用例,因此这被认为是异常,而不是规范;为了在这个异常中工作,提供了几种方法,允许在正在进行的事务的上下文中重新加载特定的数据。

当我们谈论 Session 你的 Session 仅用于事务中。概述如下: 管理交易 .

一旦我们知道了我们的隔离级别是什么,并且我们认为我们的隔离级别设置在足够低的级别,这样如果我们重新选择一行,我们应该在 Session ,我们怎么看?

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

  1. 我们只需结束我们的交易,并在下一次访问时使用我们的 Session 通过调用 Session.commit() (注意,如果 Session 是在使用较少的“自动提交”模式下,会有一个调用 Session.begin() 同样如此。绝大多数应用程序和用例不存在无法在其他事务中“看到”数据的任何问题,因为它们坚持这种模式,这是 短期交易 . 见 我什么时候做一个 Session ,什么时候提交,什么时候关闭? 想一想。

  2. 我们告诉我们 Session 当我们下一次查询已经读取的行时,可以使用 Session.expire_all()Session.expire() 或立即在对象上使用 Session.refresh . 见 刷新/过期 有关详细信息。

  3. 我们可以运行整个查询,同时设置它们在读取行时绝对覆盖已加载的对象。 Query.populate_existing() .

但请记住, 如果隔离级别为可重复读取或更高,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块——相反,这将是一个糟糕的想法。您应该设计您的应用程序,以便在会话操作周围有一个(或几个)框架点。

有关如何组织使用 Session 请看 我什么时候做一个 Session ,什么时候提交,什么时候关闭? .

但是为什么flush()坚持发布回滚?

如果 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() . 使用 Session.begin_nested() ,您可以为可能在事务中失败的操作设置框架,然后在维护封闭事务的同时“回滚”到失败前的点。

但是为什么一个自动调用回滚不够?为什么我必须再次回滚?

这又是一个问题 Session 提供一个一致的接口,并拒绝猜测它使用的上下文。例如, Session 在多个层面上支持“框架”。比如,假设你有一个装饰符 @with_session() 是这样做的:

def with_session(fn):
   def go(*args, **kw):
       session.begin(subtransactions=True)
       try:
           ret = fn(*args, **kw)
           session.commit()
           return ret
       except:
           session.rollback()
           raise
   return go

如果一个事务还不存在,那么上面的装饰器将启动该事务,如果它是创建者,则提交该事务。“SubTransactions”标志表示如果 Session.begin() 已由封闭函数调用,除了递增计数器外,什么都不会发生-当 Session.commit() 只有当它返回到零时,才会调用实际提交。它允许这种使用模式:

@with_session
def one():
   # do stuff
   two()


@with_session
def two():
   # etc.

one()

two()

one() 可以调用 two()two() 可以自己调用,并且 @with_session decorator确保适当的“框架”——事务边界保持在最外层的调用级别上。如你所见,如果 two() 调用 flush() 它抛出一个异常,然后发出一个 rollback() 那里 总是 是第二 rollback() 由装饰符执行,可能是第三个相当于两级装饰符。如果 flush()rollback() 一直走到塔顶,然后我们说剩下的 rollback() 调用是没有意义的,有一些沉默的行为正在发生。写得不好的封闭方法可能会抑制异常,然后调用 commit() 假设没有任何错误,那么您就有了一个无声的失败条件。事实上,人们之所以会出现这种错误,主要是因为他们没有编写干净的“框架”代码,而且他们在将来还会遇到其他问题。

如果您认为上面的用例有点奇怪,那么如果您想保存点,同样的事情也会发生——您可以调用 begin_nested() several times, and the commit()/rollback() 调用每个解决最近的 begin_nested(). The meaning of rollback() or commit() is dependent upon which enclosing block it is called, and you might have any sequence of rollback()/commit() 以任何顺序,以及决定其行为的嵌套级别。

在上述两种情况下,如果 flush() 打破了事务块的嵌套,根据场景的不同,行为是从“魔力”到无声故障到明目张胆地中断代码流的任何地方。

flush() 生成自己的“子事务”,以便无论外部事务状态如何,都启动事务,并在完成时调用 commit()rollback() 一旦失败——但是 rollback() 对应于它自己的子事务-它不想猜测您希望如何处理事务的外部“框架”,该框架可以嵌套多个级别,其中包含子事务和实际保存点的任意组合。开始/结束“帧”的工作与外部代码保持一致。 flush() 我们决定这是最一致的方法。

如何创建一个始终为每个查询添加特定筛选器的查询?

见方法 PreFilteredQuery .

我已经创建了一个针对外部联接的映射,当查询返回行时,不会返回任何对象。为什么不呢?

外部联接返回的行可能包含部分主键的空值,因为主键是两个表的组合。这个 Query 对象忽略没有可接受主键的传入行。基于 allow_partial_pks 旗上 mapper() ,如果值至少有一个非空值,或者如果值没有空值,则接受主键。见 allow_partial_pksmapper() .

我在用 joinedload()lazy=False 当我试图添加一个where、order by、limit等(依赖于(外部)连接)时,创建一个join/outer连接,而sqlAlchemy没有构造正确的查询。

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

有关此行为的详细信息,请参见 加入渴望装载的禅宗 .

查询没有 __len__() 为什么不呢?

Python __len__() 应用于对象的magic方法允许 len() 用于确定集合长度的内置项。很直观,SQL查询对象会链接 __len__()Query.count() 方法,它发出 SELECT COUNT . 这不可能的原因是,将查询作为列表进行计算将导致两个SQL调用,而不是一个:

class Iterates(object):
    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?

参见:

我调用来 Session.delete(myobject) 它不会从父集合中删除!

从集合和标量关系中删除引用的对象 有关此行为的描述。

为什么不是我的 __init__() 加载对象时调用?

构造函数和对象初始化 有关此行为的描述。

如何使用SA的ORM删除层叠?

对于当前加载在 Session . 对于未加载的行,默认情况下,它将发出select语句来加载这些行并更新/删除这些行;换句话说,它假定没有配置on delete cascade。要将SQLAlchemy配置为在删除级联时与之协作,请参见 使用被动删除 .

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

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

foo = Session.query(Foo).get(7)
o.foo = foo
Session.commit()

当然,操纵外键属性是完全合法的。但是,当前将外键属性设置为新值不会触发 relationship() 其中涉及到。这意味着对于以下序列:

o = Session.query(SomeClass).first()
assert o.foo is None  # accessing an un-set attribute sets it to None
o.foo_id = 7

o.foo 初始化为 None 当我们第一次访问它时。设置 o.foo_id = 7 将值“7”设为挂起,但未发生刷新-因此 o.foo 仍然是 None ::

# attribute is already set to 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.query(Foo).get(7)

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

最简单的操作是单独终止属性-这可以对任何 persistent 对象使用 Session.expire() ::

o = Session.query(SomeClass).first()
o.foo_id = 7
Session.expire(o, ['foo'])  # object must be persistent for this

foo_7 = Session.query(Foo).get(7)

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

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

new_obj = SomeClass()
new_obj.foo_id = 7

Session.add(new_obj)

# accessing an un-set attribute sets it to None
assert new_obj.foo is None

Session.flush()  # emits INSERT

# expire this because we already set .foo to None
Session.expire(o, ['foo'])

assert new_obj.foo is foo_7  # now it loads

非持久对象的属性加载

上面“挂起”行为的一个变体是如果我们使用标志 load_on_pendingrelationship() . 设置此标志时,惰性加载程序将为 new_obj.foo 在插入进行之前;这的另一个变体是使用 Session.enable_relationship_loading() 方法,它可以将对象“附加”到 Session 以这样一种方式,不管对象处于任何特定状态,多对一关系都按照外键属性加载。这两种技术都是 不建议一般使用 ;它们是为了适应用户遇到的特定编程场景而添加的,这些场景涉及到ORM通常对象状态的重新调整用途。

方法 ExpireRelationshipOnFKChange 以一个使用sqlAlchemy事件的示例为特色,以协调具有多对一关系的外键属性设置。

是否有一种方法可以自动只具有唯一的关键字(或其他类型的对象),而不进行关键字查询并获取对包含该关键字的行的引用?

当人们阅读文档中的多对多示例时,他们会受到这样一个事实的打击:如果您创建了相同的示例 Keyword 两次,它被放入数据库两次。有点不方便。

这个 UniqueObject 方法是为了解决这个问题而创建的。

为什么post-update除了第一个更新外还发出更新?

后更新功能,记录在 指向自身/相互依赖的行的行 ,涉及到除了通常为目标行发出的insert/update/delete之外,还会根据对特定关系绑定外键的更改发出update语句。虽然此update语句的主要目的是与该行的insert或delete进行配对,以便它可以对外键引用进行post-set或pre-unset,以便用相互依赖的外键中断循环,但当前它还绑定为第二个更新,当目标行本身受到更新时发出。在这种情况下,post-update发出的更新是 通常 不必要,而且经常显得浪费。

然而,一些试图消除这种“更新/更新”行为的研究表明,不仅需要在更新后的整个实施过程中,而且需要在与更新后工作无关的领域中,对工作单元过程进行重大更改,因为在非更新后,操作顺序需要颠倒。_在某些情况下,更新端会反过来影响其他情况,例如正确处理引用的主键值的更新(请参见 #1063 作为概念证明)。

答案是,“post_update”用于中断两个相互依赖的外键之间的循环,并且要使此循环中断仅限于插入/删除目标表,意味着需要对其他地方的更新语句的顺序进行自由化,从而在其他边缘情况下导致中断。