SqlAlchemy 2.0 中文文档(七十六)(3)https://developer.aliyun.com/article/1561143
关键行为变化 - ORM
query.update()现在将字符串名称解析为映射属性名称
Query.update()
的文档说明给定的values
字典是“以属性名称为键的字典”,这意味着这些是映射的属性名称。不幸的是,该函数更多地是设计为接收属性和 SQL 表达式,而不是字符串;当传递字符串时,这些字符串将直接传递到核心更新语句,而不解析这些名称在映射类上的表示方式,这意味着名称必须与表列的名称完全匹配,而不是该名称被映射到类的属性上的方式。
现在字符串名称会认真解析为属性名称:
class User(Base): __tablename__ = "user" id = Column(Integer, primary_key=True) name = Column("user_name", String(50))
上面,列user_name
被映射为name
。以前,传递字符串的Query.update()
调用必须如下调用:
session.query(User).update({"user_name": "moonbeam"})
现在给定的字符串将根据实体解析:
session.query(User).update({"name": "moonbeam"})
通常最好直接使用属性,以避免任何歧义:
session.query(User).update({User.name: "moonbeam"})
此更改还表明,同义词和混合属性也可以通过字符串名称引用:
class User(Base): __tablename__ = "user" id = Column(Integer, primary_key=True) name = Column("user_name", String(50)) @hybrid_property def fullname(self): return self.name session.query(User).update({"fullname": "moonbeam"})
#3228 ### 当将对象与 None 值比较时发出警告
这个更改是从 1.0.1 版本开始的。一些用户正在执行基本上是这种形式的查询:
session.query(Address).filter(Address.user == User(id=None))
SQLAlchemy 当前不支持这种模式。对于所有版本,它生成类似于以下的 SQL:
SELECT address.id AS address_id, address.user_id AS address_user_id, address.email_address AS address_email_address FROM address WHERE ? = address.user_id (None,)
请注意上面,有一个比较WHERE ? = address.user_id
,其中绑定值?
接收None
,或者在 SQL 中是NULL
。这在 SQL 中总是返回 False。这里的比较理论上会生成以下 SQL:
SELECT address.id AS address_id, address.user_id AS address_user_id, address.email_address AS address_email_address FROM address WHERE address.user_id IS NULL
但是现在并不是这样。依赖于“NULL = NULL”在所有情况下产生 False 的应用程序存在风险,因为有一天,SQLAlchemy 可能会修复这个问题以生成“IS NULL”,然后查询将产生不同的结果。因此,在这种操作中,你会看到一个警告:
SAWarning: Got None for value of column user.id; this is unsupported for a relationship comparison and will not currently produce an IS comparison (but may in a future release)
请注意,这种模式在 1.0.0 版本中的大多数情况下都被破坏,包括所有的 beta 版本;像SYMBOL('NEVER_SET')
这样的值将被生成。这个问题已经修复,但由于识别到这种模式,现在有了警告,以便我们可以更安全地修复这个破损的行为(现在在#3373中捕获)在未来的版本中。
#3371 ### “否定包含或等于”关系比较将使用属性的当前值,而不是数据库值
此更改是 1.0.1 的新功能;虽然我们希望这个功能在 1.0.0 中就存在,但这只是作为 #3371 的结果才变得明显。
给定一个映射:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id")) a = relationship("A")
给定 A
,其主键为 7,但我们在不刷新的情况下将其更改为 10:
s = Session(autoflush=False) a1 = A(id=7) s.add(a1) s.commit() a1.id = 10
针对此对象作为目标的多对一关系的查询将使用绑定参数中的值 10:
s.query(B).filter(B.a == a1)
生成:
SELECT b.id AS b_id, b.a_id AS b_a_id FROM b WHERE ? = b.a_id (10,)
然而,在此更改之前,这个条件的否定不会使用 10,而是使用 7,除非先刷新对象:
s.query(B).filter(B.a != a1)
生成(在 0.9 版本和所有 1.0.1 版本之前的版本中):
SELECT b.id AS b_id, b.a_id AS b_a_id FROM b WHERE b.a_id != ? OR b.a_id IS NULL (7,)
对于一个临时对象,它会产生一个错误的查询:
SELECT b.id, b.a_id FROM b WHERE b.a_id != :a_id_1 OR b.a_id IS NULL -- {u'a_id_1': symbol('NEVER_SET')}
此不一致性已经修复,在所有查询中,当前属性值,例如此示例中的 10
,现在将被使用。
#3374 ### 关于没有预先存在值的属性的属性事件和其他操作的更改
在这个更改中,当访问对象时,None
的默认返回值现在会在每次访问时动态返回,而不是在首次访问时通过特殊的“设置”操作隐式地设置属性的状态。这个更改的可见结果是,obj.__dict__
在获取时不会被隐式修改,并且对于 get_history()
和相关函数也有一些轻微的行为变化。
给定一个没有状态的对象:
>>> obj = Foo()
SQLAlchemy 的行为一直是这样的,如果我们访问一个从未设置过的标量或多对一属性,它会返回 None
:
>>> obj.someattr None
这个None
值实际上现在是obj
状态的一部分,并且与我们明确设置属性的情况类似,例如 obj.someattr = None
。然而,这里的“获取时设置”在历史和事件方面会有不同的行为。它不会触发任何属性事件,此外,如果我们查看历史记录,我们会看到这样的情况:
>>> inspect(obj).attrs.someattr.history History(added=(), unchanged=[None], deleted=()) # 0.9 and below
也就是说,就好像属性始终是None
,且从未更改过一样。这与我们首先设置属性的情况明显不同:
>>> obj = Foo() >>> obj.someattr = None >>> inspect(obj).attrs.someattr.history History(added=[None], unchanged=(), deleted=()) # all versions
上述意味着我们的“设置”操作的行为可能会被通过“获取”访问值的事实所破坏。在 1.0 中,这个不一致性已经得到了解决,不再在首次访问时实际设置任何东西。
>>> obj = Foo() >>> obj.someattr None >>> inspect(obj).attrs.someattr.history History(added=(), unchanged=(), deleted=()) # 1.0 >>> obj.someattr = None >>> inspect(obj).attrs.someattr.history History(added=[None], unchanged=(), deleted=())
以上行为之所以没有产生太大影响,是因为在关系数据库中的 INSERT 语句在大多数情况下将缺失的值视为 NULL。对于 SQLAlchemy 是否接收到了将特定属性设置为 None 的历史事件,通常并不重要;因为发送 None/NULL 或者不发送之间的区别通常不会产生影响。然而,正如#3060(在属性变更的优先级:与关系绑定的属性相比,外键绑定的属性可能会出现变化中描述的那样)所示,有一些罕见的边缘情况,我们确实希望明确将None
设置为属性。此外,允许在此处发生属性事件意味着现在可以为 ORM 映射的属性创建“默认值”函数。
作为这一变更的一部分,现在已禁用了在其他情况下生成隐式None
的功能;这包括在收到对 many-to-one 进行属性设置操作时;以前,如果“旧”值没有被设置,那么“旧”值将是None
;现在它将发送值NEVER_SET
,这是一个现在可以发送到属性监听器的值。在调用诸如Mapper.primary_key_from_instance()
之类的映射器实用程序函数时,也可能接收到此符号;如果主键属性根本没有设置,而以前值是None
,那么现在将是NEVER_SET
符号,并且对象的状态不会发生任何更改。
#3061 ### 属性变更的优先级:与关系绑定的属性相比,外键绑定的属性可能会出现变化
作为#3060的副作用,将关系绑定的属性设置为None
现在是一个被追踪的历史事件,它指的是将None
持久化到该属性的意图。由于一直以来,设置关系绑定的属性将优先于直接分配给外键属性,因此在分配 None 时可以看到行为的变化。给定一个映射:
class A(Base): __tablename__ = "table_a" id = Column(Integer, primary_key=True) class B(Base): __tablename__ = "table_b" id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("table_a.id")) a = relationship(A)
在 1.0 中,无论我们分配的值是指向A
对象的引用还是None
,关系绑定的属性都优先于 FK 绑定的属性。在 0.9 中,行为是不一致的,只有在分配了值时才生效;None 不被考虑:
a1 = A(id=1) a2 = A(id=2) session.add_all([a1, a2]) session.flush() b1 = B() b1.a = a1 # we expect a_id to be '1'; takes precedence in 0.9 and 1.0 b2 = B() b2.a = None # we expect a_id to be None; takes precedence only in 1.0 b1.a_id = 2 b2.a_id = 2 session.add_all([b1, b2]) session.commit() assert b1.a is a1 # passes in both 0.9 and 1.0 assert b2.a is None # passes in 1.0, in 0.9 it's a2
#3060 ### session.expunge() 将完全分离已被删除的对象
Session.expunge()
的行为存在一个错误,导致已删除对象的行为不一致。object_session()
函数以及 InstanceState.session
属性会在 expunge 之后仍然报告对象属于 Session
:
u1 = sess.query(User).first() sess.delete(u1) sess.flush() assert u1 not in sess assert inspect(u1).session is sess # this is normal before commit sess.expunge(u1) assert u1 not in sess assert inspect(u1).session is None # would fail
请注意,u1 not in sess
为 True 而 inspect(u1).session
仍然指向会话,当事务正在进行删除操作之后,但尚未调用 Session.expunge()
时,完全分离通常会在事务提交后完成。这个问题也会影响到依赖于 Session.expunge()
的函数,比如 make_transient()
。
#3139 ### 使用 yield_per 明确禁止连接/子查询即时加载
为了使 Query.yield_per()
方法更容易使用,在使用 yield_per 时如果任何子查询即时加载器或将使用集合的连接即时加载器生效,则会引发异常,因为这些当前与 yield_per 不兼容(理论上子查询加载可以,然而)。当引发此错误时,可以使用带有星号的 lazyload()
选项:
q = sess.query(Object).options(lazyload("*")).yield_per(100)
或者使用 Query.enable_eagerloads()
:
q = sess.query(Object).enable_eagerloads(False).yield_per(100)
lazyload()
选项的优点是仍然可以使用额外的多对一连接加载器选项:
q = ( sess.query(Object) .options(lazyload("*"), joinedload("some_manytoone")) .yield_per(100) ) ```### 对重复连接目标的处理中的更改和修复 对于在两次连接到同一实体或多次连接到同一张表的单表实体而不使用基于关系的 ON 子句时,某些情况下可能会出现意外和不一致行为的错误进行了更改,以及当多次连接到同一目标关系时。 从一个映射开始: ```py from sqlalchemy import Integer, Column, String, ForeignKey from sqlalchemy.orm import Session, relationship from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) bs = relationship("B") class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id"))
一个连接到 A.bs
两次的查询:
print(s.query(A).join(A.bs).join(A.bs))
将会渲染:
SELECT a.id AS a_id FROM a JOIN b ON a.id = b.a_id
查询对冗余的 A.bs
进行了去重,因为它试图支持以下情况:
s.query(A).join(A.bs).filter(B.foo == "bar").reset_joinpoint().join(A.bs, B.cs).filter( C.bar == "bat" )
也就是说,A.bs
是“路径”的一部分。作为 #3367 的一部分,如果到达相同的终点两次而不是作为更大路径的一部分,现在会发出警告:
SAWarning: Pathed join target A.bs has already been joined to; skipping
更大的变化涉及加入到一个实体而不使用关系绑定路径。如果我们两次加入到 B
:
print(s.query(A).join(B, B.a_id == A.id).join(B, B.a_id == A.id))
在 0.9 版本中,这将呈现如下:
SELECT a.id AS a_id FROM a JOIN b ON b.a_id = a.id JOIN b AS b_1 ON b_1.a_id = a.id
这是有问题的,因为别名是隐式的,在不同的 ON 子句的情况下可能导致不可预测的结果。
在 1.0 版本中,不会应用自动别名,我们会得到:
SELECT a.id AS a_id FROM a JOIN b ON b.a_id = a.id JOIN b ON b.a_id = a.id
这将从数据库中引发错误。虽然如果“重复加入目标”在我们从冗余关系 vs. 冗余非关系目标中都加入时表现相同可能会很好,但目前我们只在以前会发生隐式别名的更严重情况下更改行为,并且在关系情况下只发出警告。最终,在所有情况下,加入到相同的东西两次而没有任何别名以消除歧义应该引发错误。
此更改还影响单表继承目标。使用以下映射:
from sqlalchemy import Integer, Column, String, ForeignKey from sqlalchemy.orm import Session, relationship from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) type = Column(String) __mapper_args__ = {"polymorphic_on": type, "polymorphic_identity": "a"} class ASub1(A): __mapper_args__ = {"polymorphic_identity": "asub1"} class ASub2(A): __mapper_args__ = {"polymorphic_identity": "asub2"} class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(Integer, ForeignKey("a.id")) a = relationship("A", primaryjoin="B.a_id == A.id", backref="b") s = Session() print(s.query(ASub1).join(B, ASub1.b).join(ASub2, B.a)) print(s.query(ASub1).join(B, ASub1.b).join(ASub2, ASub2.id == B.a_id))
底部的两个查询是等效的,应该都呈现相同的 SQL:
SELECT a.id AS a_id, a.type AS a_type FROM a JOIN b ON b.a_id = a.id JOIN a ON b.a_id = a.id AND a.type IN (:type_1) WHERE a.type IN (:type_2)
上述 SQL 是无效的,因为在 FROM 列表中两次呈现了“a”。然而,隐式别名 bug 只会在第二个查询中发生,并呈现如下:
SELECT a.id AS a_id, a.type AS a_type FROM a JOIN b ON b.a_id = a.id JOIN a AS a_1 ON a_1.id = b.a_id AND a_1.type IN (:type_1) WHERE a_1.type IN (:type_2)
在上面,对“a”的第二次加入被别名。虽然这看起来很方便,但这不是单继承查询的一般工作方式,而且是误导性和不一致的。
其净效果是依赖于此 bug 的应用程序现在将由数据库引发错误。解决方案是使用预期的形式。在查询中引用单继承实体的多个子类时,必须手动使用别名来消除表的歧义,因为所有子类通常指向同一张表:
asub2_alias = aliased(ASub2) print(s.query(ASub1).join(B, ASub1.b).join(asub2_alias, B.a.of_type(asub2_alias)))
延迟列不再隐式取消延迟
标记为延迟的映射属性如果没有明确取消延迟,现在即使它们的列以某种方式存在于结果集中,也将保持“延迟”。这是一个性能增强,因为 ORM 加载在获得结果集时不再花时间搜索每个延迟列。然而,对于一直依赖于此的应用程序,现在应该使用显式的 undefer()
或类似选项,以防止在访问属性时发出 SELECT。
废弃的 ORM 事件钩子已移除
自 0.5 以来已被弃用的以下 ORM 事件钩子已被删除:translate_row
、populate_instance
、append_result
、create_instance
。这些钩子的用例起源于早期的 0.1 / 0.2 系列 SQLAlchemy,并且早已不再需要。特别是,这些钩子在很大程度上无法使用,因为这些事件中的行为契约与周围内部的强烈联系,例如需要如何创建和初始化实例以及如何在 ORM 生成的行中定位列。删除这些钩子极大地简化了 ORM 对象加载的机制。 ### 当使用自定义行加载器时,新 Bundle 功能的 API 变更
0.9 版本的新 Bundle
对象在 API 上有一个小变化,当在自定义类上覆盖 create_row_processor()
方法时。以前的示例代码如下:
from sqlalchemy.orm import Bundle class DictBundle(Bundle): def create_row_processor(self, query, procs, labels): """Override create_row_processor to return values as dictionaries""" def proc(row, result): return dict(zip(labels, (proc(row, result) for proc in procs))) return proc
未使用的 result
成员现已被删除:
from sqlalchemy.orm import Bundle class DictBundle(Bundle): def create_row_processor(self, query, procs, labels): """Override create_row_processor to return values as dictionaries""" def proc(row): return dict(zip(labels, (proc(row) for proc in procs))) return proc
另见
使用 Bundles 对选定属性进行分组 ### 内连接右嵌套现在是 joinedload
的默认设置,innerjoin=True
当内连接贪婪加载链接到外连接贪婪加载时,joinedload.innerjoin
的行为以及 relationship.innerjoin
现在默认使用“嵌套”内连接,即右嵌套。为了获得当外连接存在时将所有贪婪加载链接为外连接的旧行为,请使用 innerjoin="unnested"
。
正如从 0.9 版本开始介绍的 内连接右嵌套可用于连接贪婪加载,innerjoin="nested"
的行为是将内连接贪婪加载链到外连接贪婪加载时使用右嵌套连接。当使用 innerjoin=True
时,现在隐含了 "nested"
:
query(User).options( joinedload("orders", innerjoin=False).joinedload("items", innerjoin=True) )
使用新的默认设置,FROM 子句将以以下形式呈现:
FROM users LEFT OUTER JOIN (orders JOIN items ON <onclause>) ON <onclause>
也就是说,使用右嵌套连接进行 INNER 连接,以便能够返回 users
的完整结果。使用 INNER 连接比使用 OUTER 连接更高效,并且允许在所有情况下使用 joinedload.innerjoin
优化参数。
要获得旧的行为,请使用 innerjoin="unnested"
:
query(User).options( joinedload("orders", innerjoin=False).joinedload("items", innerjoin="unnested") )
这将避免右嵌套连接,并使用所有 OUTER 连接将连接链在一起,尽管有内连接指令:
FROM users LEFT OUTER JOIN orders ON <onclause> LEFT OUTER JOIN items ON <onclause>
如 0.9 备注中所述,唯一在右嵌套连接方面有困难的数据库后端是 SQLite;从 0.9 开始,SQLAlchemy 会将右嵌套连接转换为子查询作为 SQLite 上的连接目标。
另见
连接式加载中可用的右嵌套内连接 - 介绍了 0.9.4 版本中引入的功能。
#3008 ### 不再将子查询应用于 uselist=False 的连接式加载
给定如下的连接式加载:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) b = relationship("B", uselist=False) class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id")) s = Session() print(s.query(A).options(joinedload(A.b)).limit(5))
SQLAlchemy 认为关系A.b
是“一对多,加载为单个值”,本质上是“一对一”关系。然而,连接式加载一直将上述情况视为主查询需要在子查询中的情况,就像对主查询应用 LIMIT 时通常需要的那样:
SELECT anon_1.a_id AS anon_1_a_id, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id FROM (SELECT a.id AS a_id FROM a LIMIT :param_1) AS anon_1 LEFT OUTER JOIN b AS b_1 ON anon_1.a_id = b_1.a_id
然而,由于内部查询与外部查询的关系在uselist=False
的情况下最多只共享一行(与一对多的方式相同),因此在使用 LIMIT +连接式加载时,“子查询”在这种情况下现在被取消:
SELECT a.id AS a_id, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id FROM a LEFT OUTER JOIN b AS b_1 ON a.id = b_1.a_id LIMIT :param_1
在左外连接返回多于一行的情况下,ORM 一直在此处发出警告,并对于uselist=False
忽略额外的结果,因此在该错误情况下结果不应更改。
使用 join(),select_from(),from_self()时,query.update() / query.delete()会引发异常。
在 SQLAlchemy 0.9.10 版本(截至 2015 年 6 月 9 日尚未发布)中,当调用Query.update()
或Query.delete()
方法时,如果查询还调用了Query.join()
,Query.outerjoin()
,Query.select_from()
或Query.from_self()
,则会发出警告。这些是不受支持的用例,在 0.9 系列中静默失败,直到 0.9.10 版本发出警告。在 1.0 版本中,这些情况会引发异常。
使用synchronize_session='evaluate'
时,query.update()在多表更新时会引发异常。
Query.update()
的“评估器”在多表更新时不起作用,当存在多个表时,需要将其设置为synchronize_session=False
或synchronize_session='fetch'
。新的行为是现在会显式引发异常,并提醒更改同步设置。这是从 0.9.7 版本开始升级的,之前只是发出警告。
复活事件已被移除
“复活”ORM 事件已完全删除。自版本 0.8 删除了工作单元中的旧“可变”系统以来,此事件已不再起作用。
使用 from_self(),count() 时对单表继承条件的更改
给定单表继承映射,例如:
class Widget(Base): __table__ = "widget_table" class FooWidget(Widget): pass
使用 Query.from_self()
或 Query.count()
对子类进行操作将生成一个子查询,然后将子类型的“WHERE”条件添加到外部:
sess.query(FooWidget).from_self().all()
渲染:
SELECT anon_1.widgets_id AS anon_1_widgets_id, anon_1.widgets_type AS anon_1_widgets_type FROM (SELECT widgets.id AS widgets_id, widgets.type AS widgets_type, FROM widgets) AS anon_1 WHERE anon_1.widgets_type IN (?)
这个问题在于,如果内部查询没有指定所有列,那么我们就无法在外部添加 WHERE 子句(实际上会尝试,并生成一个糟糕的查询)。这个决定显然可以追溯到 0.6.5 版本,带有注释“可能需要对此进行更多调整”。好吧,这些调整已经到位!因此,上述查询现在将渲染为:
SELECT anon_1.widgets_id AS anon_1_widgets_id, anon_1.widgets_type AS anon_1_widgets_type FROM (SELECT widgets.id AS widgets_id, widgets.type AS widgets_type, FROM widgets WHERE widgets.type IN (?)) AS anon_1
这样,不包括“type”的查询仍将有效!:
sess.query(FooWidget.id).count()
渲染:
SELECT count(*) AS count_1 FROM (SELECT widgets.id AS widgets_id FROM widgets WHERE widgets.type IN (?)) AS anon_1
#3177 ### 单表继承条件无条件地添加到所有 ON 子句中
当连接到单表继承子类目标时,ORM 在连接关系时始终添加“单表条件”。给定一个映射如下:
class Widget(Base): __tablename__ = "widget" id = Column(Integer, primary_key=True) type = Column(String) related_id = Column(ForeignKey("related.id")) related = relationship("Related", backref="widget") __mapper_args__ = {"polymorphic_on": type} class FooWidget(Widget): __mapper_args__ = {"polymorphic_identity": "foo"} class Related(Base): __tablename__ = "related" id = Column(Integer, primary_key=True)
长时间以来,JOIN 关系将为类型渲染“单继承”子句:
s.query(Related).join(FooWidget, Related.widget).all()
SQL 输出:
SELECT related.id AS related_id FROM related JOIN widget ON related.id = widget.related_id AND widget.type IN (:type_1)
上面,因为我们连接到了子类 FooWidget
,Query.join()
知道要将 AND widget.type IN ('foo')
条件添加到 ON 子句中。
这里的变化是 AND widget.type IN()
条件现在附加到任何 ON 子句,不仅仅是从关系生成的那些,包括明确声明的一个:
# ON clause will now render as # related.id = widget.related_id AND widget.type IN (:type_1) s.query(Related).join(FooWidget, FooWidget.related_id == Related.id).all()
以及当没有任何 ON 子句时的“隐式”连接:
# ON clause will now render as # related.id = widget.related_id AND widget.type IN (:type_1) s.query(Related).join(FooWidget).all()
以前,这些的 ON 子句不会包括单继承条件。已经在应用程序中添加此条件以解决此问题的应用程序将希望删除其显式使用,尽管如果在此期间该条件恰好被渲染两次,则应该继续正常工作。
另请参阅
处理重复连接目标的更改和修复
#3222 ### query.update() 现在将字符串名称解析为映射属性名称
Query.update()
的文档说明给定的values
字典是“以属性名称为键的字典”,暗示这些是映射的属性名称。不幸的是,该函数更多地设计为接收属性和 SQL 表达式,而不是字符串;当传递字符串时,这些字符串将直接传递到核心更新语句,而不解析这些名称在映射类上如何表示,这意味着名称必须与表列的名称完全匹配,而不是映射到类的属性的名称。
字符名称现在认真解析为属性名称:
class User(Base): __tablename__ = "user" id = Column(Integer, primary_key=True) name = Column("user_name", String(50))
上面,列user_name
被映射为name
。以前,传递字符串的Query.update()
调用必须按照以下方式调用:
session.query(User).update({"user_name": "moonbeam"})
现在给定的字符串已经解析为实体:
session.query(User).update({"name": "moonbeam"})
通常最好直接使用属性,以避免任何歧义:
session.query(User).update({User.name: "moonbeam"})
此更改还表明,同义词和混合属性也可以通过字符串名称进行引用:
class User(Base): __tablename__ = "user" id = Column(Integer, primary_key=True) name = Column("user_name", String(50)) @hybrid_property def fullname(self): return self.name session.query(User).update({"fullname": "moonbeam"})
当将具有 None 值的对象与关系进行比较时发出警告
这个更改是从 1.0.1 版本开始的。一些用户正在执行基本上是这种形式的查询:
session.query(Address).filter(Address.user == User(id=None))
SQLAlchemy 目前不支持这种模式。对于所有版本,它生成类似于以下的 SQL:
SELECT address.id AS address_id, address.user_id AS address_user_id, address.email_address AS address_email_address FROM address WHERE ? = address.user_id (None,)
请注意上面,有一个比较WHERE ? = address.user_id
,其中绑定值?
接收到None
,或者在 SQL 中是NULL
。这在 SQL 中总是返回 False。理论上,这里的比较会生成如下的 SQL:
SELECT address.id AS address_id, address.user_id AS address_user_id, address.email_address AS address_email_address FROM address WHERE address.user_id IS NULL
但是现在,它不会。依赖于“NULL = NULL”在所有情况下产生 False 的应用程序可能会面临这样的风险,即有一天,SQLAlchemy 可能会修复此问题以生成“IS NULL”,然后查询将产生不同的结果。因此,在进行这种操作时,您将看到一个警告:
SAWarning: Got None for value of column user.id; this is unsupported for a relationship comparison and will not currently produce an IS comparison (but may in a future release)
请注意,这种模式在 1.0.0 版本中的大多数情况下都被破坏,包括所有的 beta 版本;像SYMBOL('NEVER_SET')
这样的值将被生成。这个问题已经修复,但由于识别到这种模式,现在有了警告,以便我们可以更安全地修复这个破损的行为(现在在#3373中捕获)在未来的版本中。
“否定包含或等于”关系比较将使用属性的当前值,而不是数据库值
这个更改是从 1.0.1 版本开始的;虽然我们更希望这在 1.0.0 中实现,但只有通过#3371才变得明显。
给定一个映射:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id")) a = relationship("A")
给定A
,主键为 7,但我们将其更改为 10 而没有刷新:
s = Session(autoflush=False) a1 = A(id=7) s.add(a1) s.commit() a1.id = 10
对于以这个对象为目标的一对多关系的查询将使用绑定参数中的值 10:
s.query(B).filter(B.a == a1)
产生的结果:
SELECT b.id AS b_id, b.a_id AS b_a_id FROM b WHERE ? = b.a_id (10,)
然而,在这个改变之前,对这个标准的否定将不会使用 10,而是使用 7,除非对象首先被刷新:
s.query(B).filter(B.a != a1)
产生的结果(在 0.9 版本和所有 1.0.1 版本之前的版本中):
SELECT b.id AS b_id, b.a_id AS b_a_id FROM b WHERE b.a_id != ? OR b.a_id IS NULL (7,)
对于一个瞬态对象,它将产生一个错误的查询:
SELECT b.id, b.a_id FROM b WHERE b.a_id != :a_id_1 OR b.a_id IS NULL -- {u'a_id_1': symbol('NEVER_SET')}
这种不一致性已经得到修复,在所有查询中,当前属性值,比如这个例子中的10
,现在将被使用。
关于没有预先存在值的属性事件和其他操作的更改
在这个改变中,当访问一个对象时,默认的返回值None
现在会在每次访问时动态返回,而不是在第一次访问时隐式地使用特殊的“set”操作设置属性的状态。这个改变的可见结果是,obj.__dict__
在获取时不会隐式修改,并且对于get_history()
和相关函数也有一些轻微的行为变化。
鉴于一个没有状态的对象:
>>> obj = Foo()
一直以来,SQLAlchemy 的行为是,如果我们访问一个从未设置过的标量或一对多属性,它会返回None
:
>>> obj.someattr None
这个None
值实际上现在是obj
的状态的一部分,与我们明确设置属性的情况类似,比如obj.someattr = None
。然而,在这里“获取时设置”会在历史和事件方面有所不同。它不会触发任何属性事件,而且如果我们查看历史记录,我们会看到这样:
>>> inspect(obj).attrs.someattr.history History(added=(), unchanged=[None], deleted=()) # 0.9 and below
也就是说,就好像属性始终是None
,并且从未更改过。这与我们首先设置属性的情况明显不同:
>>> obj = Foo() >>> obj.someattr = None >>> inspect(obj).attrs.someattr.history History(added=[None], unchanged=(), deleted=()) # all versions
上述意味着我们的“set”操作的行为可能会受到早期通过“get”访问值的影响而被破坏。在 1.0 版本中,这种不一致性已经得到解决,不再在使用默认“getter”时实际设置任何内容。
>>> obj = Foo() >>> obj.someattr None >>> inspect(obj).attrs.someattr.history History(added=(), unchanged=(), deleted=()) # 1.0 >>> obj.someattr = None >>> inspect(obj).attrs.someattr.history History(added=[None], unchanged=(), deleted=())
上述行为之所以没有太大影响,是因为在关系数据库中的 INSERT 语句在大多数情况下将缺失值视为 NULL。对于特定属性设置为 None 的情况,SQLAlchemy 是否收到历史事件通常不重要;因为发送 None/NULL 或不发送的区别通常不会产生影响。然而,正如#3060(在关于关系绑定属性与 FK 绑定属性的属性更改优先级可能会发生变化中描述)所示,有一些很少见的边缘情况,我们实际上确实希望明确设置None
。此外,在这里允许属性事件意味着现在可以为 ORM 映射属性创建“默认值”函数。
作为这一变化的一部分,现在已禁用了在其他情况下生成隐式None
的操作;这包括当接收到对多对一属性的属性设置操作时;以前,如果“旧”值未设置,那么“旧”值将为None
;现在将发送值NEVER_SET
,这是一个现在可能发送给属性监听器的值。当调用映射器实用函数时,也可能收到此符号,例如Mapper.primary_key_from_instance()
;如果主键属性根本没有设置,而以前的值为None
,那么现在将是NEVER_SET
符号,并且对象状态不会发生任何变化。
属性变化对关系绑定属性和外键绑定属性的优先级可能会发生变化
作为#3060的一个副作用,将关系绑定属性设置为None
现在是一个被跟踪的历史事件,指的是将None
持久化到该属性的意图。由于一直以来设置关系绑定属性将优先于直接赋值给外键属性,因此在分配None
时可以看到行为上的变化。给定一个映射:
class A(Base): __tablename__ = "table_a" id = Column(Integer, primary_key=True) class B(Base): __tablename__ = "table_b" id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("table_a.id")) a = relationship(A)
在 1.0 版本中,无论我们分配的值是对A
对象的引用还是None
,关系绑定属性在所有情况下都优先于外键绑定属性。在 0.9 版本中,行为是不一致的,只有在分配值时才会生效;None
不被考虑:
a1 = A(id=1) a2 = A(id=2) session.add_all([a1, a2]) session.flush() b1 = B() b1.a = a1 # we expect a_id to be '1'; takes precedence in 0.9 and 1.0 b2 = B() b2.a = None # we expect a_id to be None; takes precedence only in 1.0 b1.a_id = 2 b2.a_id = 2 session.add_all([b1, b2]) session.commit() assert b1.a is a1 # passes in both 0.9 and 1.0 assert b2.a is None # passes in 1.0, in 0.9 it's a2
session.expunge()将完全分离已删除的对象
Session.expunge()
的行为存在一个错误,导致关于已删除对象的行为不一致。object_session()
函数以及InstanceState.session
属性仍会报告对象属于Session
,即使在执行 expunge 之后:
u1 = sess.query(User).first() sess.delete(u1) sess.flush() assert u1 not in sess assert inspect(u1).session is sess # this is normal before commit sess.expunge(u1) assert u1 not in sess assert inspect(u1).session is None # would fail
请注意,u1 not in sess
为 True 而inspect(u1).session
仍然指向会话是正常的,而事务正在进行中,删除操作之后尚未调用Session.expunge()
;完全分离通常在事务提交后完成。这个问题也会影响依赖于Session.expunge()
的函数,比如make_transient()
。
使用 yield_per 明确禁止连接/子查询的急加载
为了使Query.yield_per()
方法更容易使用,如果在使用 yield_per 时要生效任何子查询急加载器或使用集合的连接急加载器,将引发异常,因为这些当前与 yield-per 不兼容(子查询加载理论上可能是兼容的)。当出现此错误时,可以使用带有星号的lazyload()
选项:
q = sess.query(Object).options(lazyload("*")).yield_per(100)
或使用Query.enable_eagerloads()
:
q = sess.query(Object).enable_eagerloads(False).yield_per(100)
lazyload()
选项的优势在于仍然可以使用额外的多对一连接加载器选项:
q = ( sess.query(Object) .options(lazyload("*"), joinedload("some_manytoone")) .yield_per(100) )
处理重复连接目标的更改和修复
这里的更改涵盖了在某些情况下连接到实体两次或对同一表的多个单表实体进行多次连接时会发生意外和不一致行为的错误,而不使用基于关系的 ON 子句时,以及在多次连接到相同目标关系时。
从一个映射开始:
from sqlalchemy import Integer, Column, String, ForeignKey from sqlalchemy.orm import Session, relationship from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) bs = relationship("B") class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id"))
一个查询两次连接到A.bs
的问题:
print(s.query(A).join(A.bs).join(A.bs))
将呈现为:
SELECT a.id AS a_id FROM a JOIN b ON a.id = b.a_id
查询去重了冗余的A.bs
,因为它试图支持以下情况:
s.query(A).join(A.bs).filter(B.foo == "bar").reset_joinpoint().join(A.bs, B.cs).filter( C.bar == "bat" )
也就是说,A.bs
是“路径”的一部分。作为#3367的一部分,到达相同的终点两次而不是作为更大路径的一部分现在会发出警告:
SAWarning: Pathed join target A.bs has already been joined to; skipping
更大的变化涉及在不使用基于关系的路径连接到实体时。如果我们两次连接到B
:
print(s.query(A).join(B, B.a_id == A.id).join(B, B.a_id == A.id))
在 0.9 版本中,这将呈现如下:
SELECT a.id AS a_id FROM a JOIN b ON b.a_id = a.id JOIN b AS b_1 ON b_1.a_id = a.id
这是有问题的,因为别名是隐式的,在不同的 ON 子句的情况下可能导致结果不可预测。
在 1.0 中,不会自动应用别名,我们得到:
SELECT a.id AS a_id FROM a JOIN b ON b.a_id = a.id JOIN b ON b.a_id = a.id
这将从数据库中引发错误。虽然如果“重复连接目标”在我们从冗余关系 vs. 冗余非关系目标中都加入时表现相同的话会很好,但是目前我们只在以前会发生隐式别名时更改行为,在关系情况下只发出警告。最终,在所有情况下,未加别名以消除歧义地两次连接到相同内容都应该引发错误。
此更改还对单表继承目标产生影响。使用以下映射:
from sqlalchemy import Integer, Column, String, ForeignKey from sqlalchemy.orm import Session, relationship from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) type = Column(String) __mapper_args__ = {"polymorphic_on": type, "polymorphic_identity": "a"} class ASub1(A): __mapper_args__ = {"polymorphic_identity": "asub1"} class ASub2(A): __mapper_args__ = {"polymorphic_identity": "asub2"} class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(Integer, ForeignKey("a.id")) a = relationship("A", primaryjoin="B.a_id == A.id", backref="b") s = Session() print(s.query(ASub1).join(B, ASub1.b).join(ASub2, B.a)) print(s.query(ASub1).join(B, ASub1.b).join(ASub2, ASub2.id == B.a_id))
底部的两个查询是等价的,应该都会渲染相同的 SQL:
SELECT a.id AS a_id, a.type AS a_type FROM a JOIN b ON b.a_id = a.id JOIN a ON b.a_id = a.id AND a.type IN (:type_1) WHERE a.type IN (:type_2)
上面的 SQL 是无效的,因为它在 FROM 列表中两次呈现 “a”。然而,隐式别名错误只会在第二个查询中发生,而不是呈现以下结果:
SELECT a.id AS a_id, a.type AS a_type FROM a JOIN b ON b.a_id = a.id JOIN a AS a_1 ON a_1.id = b.a_id AND a_1.type IN (:type_1) WHERE a_1.type IN (:type_2)
在上面的情况下,第二次加入到 “a” 的连接已经被别名化。虽然这看起来很方便,但这并不是单继承查询通常的工作方式,这是误导性和不一致的。
净影响是依赖于此错误的应用程序现在将由数据库引发错误。解决方案是使用期望的形式。在查询中引用单继承实体的多个子类时,必须手动使用别名来消除表的歧义,因为所有子类通常都指向相同的表:
asub2_alias = aliased(ASub2) print(s.query(ASub1).join(B, ASub1.b).join(asub2_alias, B.a.of_type(asub2_alias)))
不再隐式取消延迟列
如果未明确取消设置延迟的延迟列,现在标记为延迟的映射属性将始终保持“延迟”,即使它们的列以某种方式出现在结果集中。这是一种性能增强,因为 ORM 加载在获取结果集时不再花时间搜索每个延迟列。然而,对于依赖于此的应用程序,现在应该使用显式 undefer()
或类似选项,以防止在访问属性时发出 SELECT。
废弃的 ORM 事件钩子已移除
以下 ORM 事件钩子,其中一些从 0.5 版本开始已被弃用,已被移除:translate_row
、populate_instance
、append_result
、create_instance
。这些钩子的用例始于早期的 0.1 / 0.2 版本的 SQLAlchemy,并且早已不再需要。特别是,这些钩子在很大程度上无法使用,因为这些事件内部的行为约定与周围内部的密切联系,比如实例需要如何创建和初始化以及如何在 ORM 生成的行中定位列。移除这些钩子大大简化了 ORM 对象加载的机制。
在使用自定义行加载器时对新 Bundle 功能的 API 更改
新的 Bundle
对象在自定义类上覆盖 create_row_processor()
方法时 API 有所变化。以前,示例代码如下所示:
from sqlalchemy.orm import Bundle class DictBundle(Bundle): def create_row_processor(self, query, procs, labels): """Override create_row_processor to return values as dictionaries""" def proc(row, result): return dict(zip(labels, (proc(row, result) for proc in procs))) return proc
未使用的 result
成员现已删除:
from sqlalchemy.orm import Bundle class DictBundle(Bundle): def create_row_processor(self, query, procs, labels): """Override create_row_processor to return values as dictionaries""" def proc(row): return dict(zip(labels, (proc(row) for proc in procs))) return proc
另请参阅
使用 Bundles 分组选定属性
右嵌套内连接现在是 joinedload
的默认值,innerjoin=True
当 INNER JOIN 连接式预加载链接到 OUTER JOIN 连接式预加载时,默认行为为使用“嵌套” INNER JOIN,也就是右嵌套。如果要获得当存在 OUTER JOIN 时链接所有 INNER JOIN 连接式预加载的旧行为,请使用 innerjoin="unnested"
。
正如在 右嵌套内连接可用于连接式预加载 中介绍的,innerjoin="nested"
的行为是,当 INNER JOIN 连接式预加载链接到 OUTER JOIN 连接式预加载时,将使用右嵌套连接。当使用 innerjoin=True
时,现在会隐含 "nested"
:
query(User).options( joinedload("orders", innerjoin=False).joinedload("items", innerjoin=True) )
使用新的默认值,FROM 子句将呈现为以下形式:
FROM users LEFT OUTER JOIN (orders JOIN items ON <onclause>) ON <onclause>
也就是说,对于 INNER JOIN,使用右嵌套连接,以便返回users
的完整结果集。使用 INNER JOIN 比使用 OUTER JOIN 更有效,并且允许在所有情况下生效 joinedload.innerjoin
优化参数。
要获得旧行为,请使用 innerjoin="unnested"
:
query(User).options( joinedload("orders", innerjoin=False).joinedload("items", innerjoin="unnested") )
这将避免右嵌套连接,并使用所有 OUTER JOIN 将连接链接在一起,尽管存在 innerjoin 指令:
FROM users LEFT OUTER JOIN orders ON <onclause> LEFT OUTER JOIN items ON <onclause>
如在 0.9 版本的说明中指出的,唯一有困难的数据库后端是 SQLite;从 0.9 版本开始,SQLAlchemy 会将右嵌套连接转换为 SQLite 上的子查询作为连接目标。
另请参阅
右嵌套内连接可用于连接式预加载 - 在 0.9.4 版本中引入的功能的描述。
不再将子查询应用于 uselist=False 的连接式预加载
给定如下连接式预加载:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) b = relationship("B", uselist=False) class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id")) s = Session() print(s.query(A).options(joinedload(A.b)).limit(5))
SQLAlchemy 认为关系 A.b
是“加载为单个值的一对多”,实质上是“一对一”关系。然而,连接式预加载始终将以上情况视为需要主查询位于子查询内的情况,正如在应用 LIMIT 于主查询时通常需要的情况下一样:
SELECT anon_1.a_id AS anon_1_a_id, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id FROM (SELECT a.id AS a_id FROM a LIMIT :param_1) AS anon_1 LEFT OUTER JOIN b AS b_1 ON anon_1.a_id = b_1.a_id
然而,由于内部查询与外部查询的关系,在 uselist=False
的情况下最多只有一行是共享的(与多对一相同),因此在此情况下现已放弃使用带有 LIMIT + joined eager loading 的“子查询”:
SELECT a.id AS a_id, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id FROM a LEFT OUTER JOIN b AS b_1 ON a.id = b_1.a_id LIMIT :param_1
如果 LEFT OUTER JOIN 返回多于一行的情况下,ORM 一直会在此处发出警告,并且对于 uselist=False
,会忽略额外的结果,因此在这种错误情况下,结果不应更改。
当与 join()、select_from()、from_self() 一起使用时,query.update() / query.delete() 会引发异常。
当调用 Query.update()
或 Query.delete()
方法的查询还调用了 Query.join()
、Query.outerjoin()
、Query.select_from()
或 Query.from_self()
时,SQLAlchemy 0.9.10 发出警告(截至 2015 年 6 月 9 日尚未发布)。这些是不受支持的用例,直到 0.9.10 发出警告,但在 1.0 中,这些情况会引发异常。
当使用 synchronize_session='evaluate'
进行多表更新时,query.update() 会引发异常。
当进行多表更新时,Query.update()
的“评估器”不适用,并且在存在多个表时需要将其设置为 synchronize_session=False
或 synchronize_session='fetch'
。新的行为是现在会显式引发异常,并提供消息以更改同步设置。这是从 0.9.7 开始发出的警告升级而来。
已删除 Resurrect 事件
完全删除了“复活”ORM 事件。自从版本 0.8 移除了工作单元中的旧“可变”系统以来,该事件已不再起作用。
使用 from_self()、count() 时的单表继承条件更改
给定单表继承映射,例如:
class Widget(Base): __table__ = "widget_table" class FooWidget(Widget): pass
对子类使用 Query.from_self()
或 Query.count()
会生成子查询,但然后将子类型的“WHERE”条件添加到外部:
sess.query(FooWidget).from_self().all()
渲染:
SELECT anon_1.widgets_id AS anon_1_widgets_id, anon_1.widgets_type AS anon_1_widgets_type FROM (SELECT widgets.id AS widgets_id, widgets.type AS widgets_type, FROM widgets) AS anon_1 WHERE anon_1.widgets_type IN (?)
这样做的问题是,如果内部查询没有指定所有列,那么我们无法在外部添加 WHERE 子句(实际上尝试了,并生成了一个错误的查询)。这个决定显然可以追溯到 0.6.5,注明“可能需要对此进行更多调整”。好吧,这些调整已经到来了!因此,现在上述查询将呈现为:
SELECT anon_1.widgets_id AS anon_1_widgets_id, anon_1.widgets_type AS anon_1_widgets_type FROM (SELECT widgets.id AS widgets_id, widgets.type AS widgets_type, FROM widgets WHERE widgets.type IN (?)) AS anon_1
因此,即使查询不包括“type”,也会工作!:
sess.query(FooWidget.id).count()
渲染:
SELECT count(*) AS count_1 FROM (SELECT widgets.id AS widgets_id FROM widgets WHERE widgets.type IN (?)) AS anon_1
单表继承条件无条件添加到所有 ON 子句
当加入到单表继承子类目标时,ORM 总是在关系上加入“单表条件”。假设有一个映射如下:
class Widget(Base): __tablename__ = "widget" id = Column(Integer, primary_key=True) type = Column(String) related_id = Column(ForeignKey("related.id")) related = relationship("Related", backref="widget") __mapper_args__ = {"polymorphic_on": type} class FooWidget(Widget): __mapper_args__ = {"polymorphic_identity": "foo"} class Related(Base): __tablename__ = "related" id = Column(Integer, primary_key=True)
很长一段时间以来,JOIN 关系都会为类型生成“单继承”子句:
s.query(Related).join(FooWidget, Related.widget).all()
SQL 输出:
SELECT related.id AS related_id FROM related JOIN widget ON related.id = widget.related_id AND widget.type IN (:type_1)
在上面的例子中,由于我们连接到了子类 FooWidget
,Query.join()
知道要将 AND widget.type IN ('foo')
的条件添加到 ON 子句中。
此处的更改是现在 AND widget.type IN()
的条件现在附加到 任何 ON 子句上,不仅仅是从关系生成的那些,包括明确声明的一个:
# ON clause will now render as # related.id = widget.related_id AND widget.type IN (:type_1) s.query(Related).join(FooWidget, FooWidget.related_id == Related.id).all()
以及当没有任何类型的 ON 子句时的“隐式”连接:
# ON clause will now render as # related.id = widget.related_id AND widget.type IN (:type_1) s.query(Related).join(FooWidget).all()
以前,这些 ON 子句不会包含单继承的条件。已经添加了此条件以解决此问题的应用程序将希望删除其显式使用,尽管在此期间如果条件恰好被重复呈现,则应该继续正常工作。
另请参阅
处理重复的联接目标中的更改和修复
SqlAlchemy 2.0 中文文档(七十六)(5)https://developer.aliyun.com/article/1561163