joinedload()
或 lazy=False
当我试图添加一个where、order by、limit等(依赖于(外部)连接)时,创建一个join/outer连接,而sqlAlchemy没有构造正确的查询。__len__()
为什么不呢?Session.delete(myobject)
它不会从父集合中删除!__init__()
加载对象时调用?None
-难道它不应该给foo加载id_7吗?关于这种行为的主要问题是,会话的作用就像事务在 可串行化的 隔离状态,即使不是(通常不是)。实际上,这意味着会话不会更改在事务范围内已经读取的任何数据。
如果不熟悉术语“隔离级别”,则首先需要阅读此链接:
简而言之,可序列化隔离级别通常意味着一旦在事务中选择一系列行,您将获得 相同的数据 每次重新发出该选择时返回。如果您处于下一个较低的隔离级别“可重复读取”,您将看到新添加的行(不再看到已删除的行),但是对于您已经 已经 加载后,您将看不到任何更改。只有当您处于较低的隔离级别(例如“已提交读取”)时,才能看到一行数据更改其值。
有关使用SQLAlchemy ORM时控制隔离级别的信息,请参阅 设置事务隔离级别/DBAPI AUTOCOMMIT .
为了大大简化事情, Session
它本身的工作方式是完全独立的事务,并且不会覆盖它已经读取的任何映射属性,除非您告诉它。尝试重新读取已在正在进行的事务中加载的数据的用例是 罕见的 在许多情况下没有效果的用例,因此这被认为是异常,而不是规范;为了在这个异常中工作,提供了几种方法,允许在正在进行的事务的上下文中重新加载特定的数据。
当我们谈论 Session
你的 Session
仅用于事务中。概述如下: 管理交易 .
一旦我们知道了我们的隔离级别是什么,并且我们认为我们的隔离级别设置在足够低的级别,这样如果我们重新选择一行,我们应该在 Session
,我们怎么看?
三种方式,从最常见到最少:
我们只需结束我们的交易,并在下一次访问时使用我们的 Session
通过呼叫 Session.commit()
(注意,如果 Session
是在使用较少的“自动提交”模式下,会有一个呼叫 Session.begin()
同样如此。绝大多数应用程序和用例不存在无法在其他事务中“看到”数据的任何问题,因为它们坚持这种模式,这是 短期交易 . 见 我什么时候做一个 Session ,什么时候提交,什么时候关闭? 想一想。
我们告诉我们 Session
当我们下一次查询已经读取的行时,可以使用 Session.expire_all()
或 Session.expire()
或立即在对象上使用 refresh
. 见 刷新/过期 有关详细信息。
我们可以运行整个查询,同时通过使用“填充现有”将它们设置为在读取行时明确覆盖已加载的对象。这是一个执行选项,请参阅 填充现有 。
但请记住, 如果隔离级别为可重复读取或更高,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
除了冲洗以外,很多事情都会导致在尝试中失败。应用程序应确保将某些“框架”系统应用于面向ORM的流程,以便连接和事务资源具有确定的边界,并且在出现任何故障条件时,可以显式回滚事务。
这并不意味着在整个应用程序中都应该有try/except块,这将不是一个可扩展的体系结构。相反,一种典型的方法是,当第一次调用面向ORM的方法和函数时,从最上面调用函数的过程将位于一个块内,该块在一系列操作成功完成时提交事务,并在操作结束时回滚事务。操作因任何原因失败,包括失败的刷新。也有一些方法使用函数修饰器或上下文管理器来实现类似的结果。所采用的方法在很大程度上取决于所编写的应用程序的类型。
有关如何组织使用 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()
,您可以为可能在事务中失败的操作设置框架,然后在维护封闭事务的同时“回滚”到失败前的点。
由flush()引起的回滚不是完整事务块的结尾;当它在play中结束数据库事务时,从 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
对象在被要求返回ORM映射对象的列表时,将 基于主键对对象进行重复数据消除 . 也就是说,如果我们使用 User
映射描述于 对象关系教程(1.x API) ,我们有一个SQL查询,如下所示:
q = session.query(User).outerjoin(User.addresses).filter(User.name == 'jack')
上面,教程中使用的示例数据在 addresses
为 users
具有名称的行 'jack'
,主键值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
将重复数据消除:
使连接的紧急加载正常工作 - 加入的热切装载 它的工作原理是使用关联对相关表查询行,然后将这些联接中的行路由到lead对象上的集合中。对于主键中的每一行,它都要按照主键的顺序来执行。例如,可以将这种模式进一步处理成多行的子对象,这样就可以继续处理这种模式 User(id=5)
. depulation允许我们以查询对象的方式接收对象,例如 User()
名称为的对象 'jack'
对我们来说是一个目标 User.addresses
收藏急切地装载,如 lazy='joined'
上 relationship()
或者通过 joinedload()
选项。为了保持一致性,无论是否建立joinedload,都将应用重复数据消除,因为急切加载背后的关键原理是这些选项永远不会影响结果。
消除关于身份图的混乱 -诚然,这是不那么关键的原因。作为 Session
利用 identity map ,即使我们的SQL结果集有两行主键为5,但只有一行 User(id=5)
对象内部 Session
必须在其标识(即,其主键/类组合)上进行唯一维护。如果有人在询问 User()
对象,以在列表中多次获取同一对象。一个有序集可能会更好地表示什么 Query
在返回完整对象时寻求返回。
问题 Query
重复数据消除仍然存在问题,主要原因是 Query.count()
方法是不一致的,当前的状态是,在最近的版本中,joined-eager-loading首先被“subquery-eager-loading”策略和最近的“select-in-eager-loading”策略所取代,这两种策略通常更适合于集合预加载。随着这一演进的继续,SQLAlchemy可能会在 Query
,这也可能涉及新的api,以便更直接地控制这种行为,也可能会改变joined eager loading的行为,以创建更一致的使用模式。
外部联接返回的行可能包含部分主键的空值,因为主键是两个表的组合。这个 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!
参见:
在会话中使用SQL表达式 -使用 Session
直接使用文本SQL。
Session.delete(myobject)
它不会从父集合中删除!¶见 删除引用的对象和标量关系的注释 有关此行为的描述。
__init__()
加载对象时调用?¶见 构造函数和对象初始化 有关此行为的描述。
对于当前加载在 Session
. 对于未加载的行,默认情况下,它将发出select语句来加载这些行并更新/删除这些行;换句话说,它假定没有配置on delete cascade。要将SQLAlchemy配置为在删除级联时与之协作,请参见 在具有ORM关系的DELETE cascade中使用外键 .
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事件的示例,以便将外键属性的设置与多对一关系协调起来。
具有与之相关的其他对象的对象将与 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 食谱就是为了解决这个问题而创建的。
后更新功能,记录在 指向自身/相互依赖的行的行 ,涉及到除了通常为目标行发出的insert/update/delete之外,还会根据对特定关系绑定外键的更改发出update语句。虽然此update语句的主要目的是与该行的insert或delete进行配对,以便它可以对外键引用进行post-set或pre-unset,以便用相互依赖的外键中断循环,但当前它还绑定为第二个更新,当目标行本身受到更新时发出。在这种情况下,post-update发出的更新是 通常 不必要,而且经常显得浪费。
然而,一些试图消除这种“更新/更新”行为的研究表明,不仅需要在更新后的整个实施过程中,而且需要在与更新后工作无关的领域中,对工作单元过程进行重大更改,因为在非更新后,操作顺序需要颠倒。_在某些情况下,更新端会反过来影响其他情况,例如正确处理引用的主键值的更新(请参见 #1063 作为概念证明)。
答案是,“post_update”用于中断两个相互依赖的外键之间的循环,并且要使此循环中断仅限于插入/删除目标表,意味着需要对其他地方的更新语句的顺序进行自由化,从而在其他边缘情况下导致中断。
flambé! the dragon and The Alchemist image designs created and generously donated by Rotem Yaari.
Created using Sphinx 4.2.0.