关于这种行为的主要问题是,会话的作用就像事务在 可串行化的 隔离状态,即使不是(通常不是)。实际上,这意味着会话不会更改在事务范围内已经读取的任何数据。
如果不熟悉术语“隔离级别”,则首先需要阅读此链接:
简而言之,可序列化隔离级别通常意味着一旦在事务中选择一系列行,您将获得 相同的数据 每次重新发出该选择时返回。如果您处于下一个较低的隔离级别“可重复读取”,您将看到新添加的行(不再看到已删除的行),但是对于您已经 已经 加载后,您将看不到任何更改。只有当您处于较低的隔离级别(例如“已提交读取”)时,才能看到一行数据更改其值。
有关使用SQLAlchemy ORM时控制隔离级别的信息,请参阅 设置事务隔离级别 .
为了大大简化事情, Session
它本身的工作方式是完全独立的事务,并且不会覆盖它已经读取的任何映射属性,除非您告诉它。尝试重新读取已在正在进行的事务中加载的数据的用例是 罕见的 在许多情况下没有效果的用例,因此这被认为是异常,而不是规范;为了在这个异常中工作,提供了几种方法,允许在正在进行的事务的上下文中重新加载特定的数据。
当我们谈论 Session
你的 Session
仅用于事务中。概述如下: 管理交易 .
一旦我们知道了我们的隔离级别是什么,并且我们认为我们的隔离级别设置在足够低的级别,这样如果我们重新选择一行,我们应该在 Session
,我们怎么看?
三种方式,从最常见到最少:
我们只需结束我们的交易,并在下一次访问时使用我们的 Session
通过调用 Session.commit()
(注意,如果 Session
是在使用较少的“自动提交”模式下,会有一个调用 Session.begin()
同样如此。绝大多数应用程序和用例不存在无法在其他事务中“看到”数据的任何问题,因为它们坚持这种模式,这是 短期交易 . 见 我什么时候做一个 Session ,什么时候提交,什么时候关闭? 想一想。
我们告诉我们 Session
当我们下一次查询已经读取的行时,可以使用 Session.expire_all()
或 Session.expire()
或立即在对象上使用 Session.refresh
. 见 刷新/过期 有关详细信息。
我们可以运行整个查询,同时设置它们在读取行时绝对覆盖已加载的对象。 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 ,什么时候提交,什么时候关闭? .
如果 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_pks
在 mapper()
.
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!
Session.delete(myobject)
它不会从父集合中删除!¶见 从集合和标量关系中删除引用的对象 有关此行为的描述。
__init__()
加载对象时调用?¶见 构造函数和对象初始化 有关此行为的描述。
对于当前加载在 Session
. 对于未加载的行,默认情况下,它将发出select语句来加载这些行并更新/删除这些行;换句话说,它假定没有配置on delete cascade。要将SQLAlchemy配置为在删除级联时与之协作,请参见 使用被动删除 .
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_pending
在 relationship()
. 设置此标志时,惰性加载程序将为 new_obj.foo
在插入进行之前;这的另一个变体是使用 Session.enable_relationship_loading()
方法,它可以将对象“附加”到 Session
以这样一种方式,不管对象处于任何特定状态,多对一关系都按照外键属性加载。这两种技术都是 不建议一般使用 ;它们是为了适应用户遇到的特定编程场景而添加的,这些场景涉及到ORM通常对象状态的重新调整用途。
方法 ExpireRelationshipOnFKChange 以一个使用sqlAlchemy事件的示例为特色,以协调具有多对一关系的外键属性设置。
当人们阅读文档中的多对多示例时,他们会受到这样一个事实的打击:如果您创建了相同的示例 Keyword
两次,它被放入数据库两次。有点不方便。
这个 UniqueObject 方法是为了解决这个问题而创建的。
后更新功能,记录在 指向自身/相互依赖的行的行 ,涉及到除了通常为目标行发出的insert/update/delete之外,还会根据对特定关系绑定外键的更改发出update语句。虽然此update语句的主要目的是与该行的insert或delete进行配对,以便它可以对外键引用进行post-set或pre-unset,以便用相互依赖的外键中断循环,但当前它还绑定为第二个更新,当目标行本身受到更新时发出。在这种情况下,post-update发出的更新是 通常 不必要,而且经常显得浪费。
然而,一些试图消除这种“更新/更新”行为的研究表明,不仅需要在更新后的整个实施过程中,而且需要在与更新后工作无关的领域中,对工作单元过程进行重大更改,因为在非更新后,操作顺序需要颠倒。_在某些情况下,更新端会反过来影响其他情况,例如正确处理引用的主键值的更新(请参见 #1063 作为概念证明)。
答案是,“post_update”用于中断两个相互依赖的外键之间的循环,并且要使此循环中断仅限于插入/删除目标表,意味着需要对其他地方的更新语句的顺序进行自由化,从而在其他边缘情况下导致中断。