会话/查询

优质
小牛编辑
145浏览
2023-12-01

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

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

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

Isolation Level

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

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

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

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

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

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

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

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

  3. 我们可以运行整个查询,同时通过使用“填充现有”将它们设置为在读取行时明确覆盖已加载的对象。这是一个执行选项,请参阅 填充现有

但请记住, 如果隔离级别为可重复读取或更高,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 ,什么时候提交,什么时候关闭? .

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

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

由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

我的查询返回的对象数与查询.count()告诉我-为什么?

这个 Query 对象在被要求返回ORM映射对象的列表时,将 基于主键对对象进行重复数据消除 . 也就是说,如果我们使用 User 映射描述于 对象关系教程(1.x API) ,我们有一个SQL查询,如下所示:

q = session.query(User).outerjoin(User.addresses).filter(User.name == 'jack')

上面,教程中使用的示例数据在 addressesusers 具有名称的行 '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_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配置为在删除级联时与之协作,请参见 在具有ORM关系的DELETE cascade中使用外键 .

我将实例上的“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事件的示例,以便将外键属性的设置与多对一关系协调起来。

如何浏览与给定对象相关的所有对象?

具有与之相关的其他对象的对象将与 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除了第一个更新外还发出更新?

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

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

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