SqlAlchemy 2.0 中文文档(七十六)(1)https://developer.aliyun.com/article/1561122
关键行为变化 - 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 版本中的大多数情况下都被打破,包括所有的测试版;会生成类似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 中,这种不一致性已经解决了,不再实际设置任何东西当使用默认的“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=())
上述行为之所以没有产生太大影响,是因为在关系数据库中的插入语句在大多数情况下将缺失值视为 NULL。无论 SQLAlchemy 是否收到了针对特定属性设置为 None 的历史事件,通常都不会有影响;因为发送 None/NULL 或不发送的区别不会产生影响。但是,正如 #3060(在关系绑定属性与 FK 绑定属性上的属性更改的优先级可能会发生变化中描述的那样)所示,有一些罕见的边缘情况,我们实际上确实希望明确设置为 None
。此外,在此处允许属性事件意味着现在可以为 ORM 映射属性创建“默认值”函数。
作为这一变化的一部分,现在已禁用了在其他情况下生成隐式None
的功能;这包括在接收到对一对多的属性设置操作时;以前,如果“旧”值未设置,则“旧”值将为None
;现在将发送值 NEVER_SET
,这是一个现在可以发送到属性监听器的值。当调用诸如 Mapper.primary_key_from_instance()
这样的映射器实用程序函数时,如果主键属性根本没有设置,而以前的值为 None
,那么现在将是 NEVER_SET
符号,并且不会更改对象的状态。
#3061 ### 关系绑定属性与 FK 绑定属性上的属性更改的优先级可能会发生变化
作为 #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()
的行为存在一个 bug,导致关于已删除对象的行为存在不一致性。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
注意,当事务正在进行中且尚未调用 Session.expunge()
时,u1 not in sess
为 True 是正常的,而 inspect(u1).session
仍然引用会话;完全分离通常在事务提交后完成。此问题也会影响依赖于 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) ) ```### 处理重复连接目标的更改和修复 此处的更改包括了一些 bug,当连接两次到一个实体时,或者连接到多个单表实体对同一张表时会出现意外和不一致的行为,而不使用基于关系的 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
这将从数据库中引发错误。虽然如果我们从冗余关系和冗余非关系目标中都加入时,“重复加入目标”表现相同可能更好,但目前我们只在以前会发生隐式别名的更严重情况下更改行为,并且在关系情况下只发出警告。最终,在所有情况下,两次加入相同的内容而没有任何别名以消除歧义应该引发错误。
这个变化也对单表继承目标产生影响。使用以下映射:
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
另请参见
使用捆绑组合选择属性 ### 现在默认为使用内连接加载时的右嵌套连接
joinedload.innerjoin
以及relationship.innerjoin
的行为现在是使用“nested”内连接,也就是右嵌套,作为内连接急加载链接到外连接急加载时的默认行为。为了获得当存在外连接时将所有连接急加载链接为外连接的旧行为,请使用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>
也就是说,使用右嵌套连接进行内连接,以便返回users
的完整结果。使用内连接比使用外连接更有效,并且允许joinedload.innerjoin
优化参数在所有情况下生效。
要获得旧的行为,请使用innerjoin="unnested"
:
query(User).options( joinedload("orders", innerjoin=False).joinedload("items", innerjoin="unnested") )
这将避免右嵌套连接,并使用所有外连接将连接链接在一起,尽管有内连接指令:
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 时通常需要的 B 对象集合一样:
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
如果 LEFT OUTER JOIN 返回多行,ORM 一直会在这里发出警告并忽略uselist=False
的额外结果,因此在这种错误情况下结果不应更改。
query.update() / query.delete()如果与 join(),select_from(),from_self()一起使用会引发异常
在 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 子句不会包括单一继承条件。已经为了解决此问题而添加此条件的应用程序将希望删除其显式使用,尽管在此期间如果条件恰好被重复渲染,它应该继续正常工作。
另请参见
处理重复连接目标的更改和修复
关键行为变化 - 核心
在将完整 SQL 片段强制转换为 text()时发出警告
自 SQLAlchemy 创建以来,一直强调不妨碍使用纯文本。Core 和 ORM 表达式系统旨在允许用户在任何可以使用纯文本 SQL 表达式的地方使用纯文本,不仅仅是您可以将完整的 SQL 字符串发送到Connection.execute()
,而且您可以将带有 SQL 表达式的字符串发送到许多函数中,例如Select.where()
,Query.filter()
和Select.order_by()
。
请注意,这里所说的“SQL 表达式”是指完整的 SQL 字符串片段,例如:
# the argument sent to where() is a full SQL expression stmt = select([sometable]).where("somecolumn = 'value'")
我们不是在讨论字符串参数,即传递成为参数化的字符串值的正常行为:
# This is a normal Core expression with a string argument - # we aren't talking about this!! stmt = select([sometable]).where(sometable.c.somecolumn == "value")
Core 教程长期以来一直以使用这种技术的示例为特色,使用select()
构造,其中几乎所有组件都被指定为纯字符串。然而,尽管存在这种长期行为和示例,用户显然对这种行为感到惊讶,当在社区中询问时,我无法找到任何用户实际上不感到惊讶,即您可以将完整字符串发送到像Query.filter()
这样的方法中。
因此,这里的更改是鼓励用户在部分或完全由文本片段组成的 SQL 中对文本字符串进行限定。在如下组合 select 时:
stmt = select(["a", "b"]).where("a = b").select_from("sometable")
语句正常构建,与以前的所有强制转换相同。然而,将会看到以下警告被发出:
SAWarning: Textual column expression 'a' should be explicitly declared with text('a'), or use column('a') for more specificity (this warning may be suppressed after 10 occurrences) SAWarning: Textual column expression 'b' should be explicitly declared with text('b'), or use column('b') for more specificity (this warning may be suppressed after 10 occurrences) SAWarning: Textual SQL expression 'a = b' should be explicitly declared as text('a = b') (this warning may be suppressed after 10 occurrences) SAWarning: Textual SQL FROM expression 'sometable' should be explicitly declared as text('sometable'), or use table('sometable') for more specificity (this warning may be suppressed after 10 occurrences)
这些警告试图通过显示参数以及字符串接收位置来准确指出问题所在。这些警告利用 Session.get_bind()处理更广泛的继承场景,以便可以安全地发出参数化警告而不会耗尽内存,如果希望将警告作为异常处理,应使用Python 警告过滤器:
import warnings warnings.simplefilter("error") # all warnings raise an exception
鉴于上述警告,我们的语句运行正常,但为了摆脱警告,我们将重写我们的语句如下:
from sqlalchemy import select, text stmt = ( select([text("a"), text("b")]).where(text("a = b")).select_from(text("sometable")) )
正如警告所建议的那样,如果我们使用column()
和table()
,我们可以使我们的语句对文本更具体:
from sqlalchemy import select, text, column, table stmt = ( select([column("a"), column("b")]) .where(text("a = b")) .select_from(table("sometable")) )
还请注意,现在可以从“sqlalchemy”导入 table()
和 column()
而不需要“sql”部分。
此处的行为适用于 select()
以及 Query
上的关键方法,包括 Query.filter()
、Query.from_statement()
和 Query.having()
。
ORDER BY 和 GROUP BY 是特殊情况。
有一种情况下,使用字符串具有特殊含义,并且作为此更改的一部分,我们增强了其功能。当我们有一个引用某列名或命名标签的 select()
或 Query
时,我们可能想要对已知列或标签进行 GROUP BY 和/或 ORDER BY:
stmt = ( select([user.c.name, func.count(user.c.id).label("id_count")]) .group_by("name") .order_by("id_count") )
在上述语句中,我们期望看到“ORDER BY id_count”,而不是函数的重新陈述。在编译期间,字符串参数会被主动与列子句中的条目匹配,因此上述语句将产生我们期望的结果,不会有警告(尽管请注意 "name"
表达式已解析为 users.name
!):
SELECT users.name, count(users.id) AS id_count FROM users GROUP BY users.name ORDER BY id_count
但是,如果我们引用无法定位的名称,则会再次收到警告,如下所示:
stmt = select([user.c.name, func.count(user.c.id).label("id_count")]).order_by( "some_label" )
输出确实按我们说的做了,但再次警告我们:
SAWarning: Can't resolve label reference 'some_label'; converting to text() (this warning may be suppressed after 10 occurrences)
SELECT users.name, count(users.id) AS id_count FROM users ORDER BY some_label
上述行为适用于我们可能想要引用所谓的“标签引用”的所有地方;ORDER BY 和 GROUP BY,以及 OVER 子句和 DISTINCT ON 子句内引用列的地方(例如 PostgreSQL 语法)。
我们仍然可以使用 text()
指定任意表达式用于 ORDER BY 或其他地方:
stmt = select([users]).order_by(text("some special expression"))
整个更改的结果是,SQLAlchemy 现在希望我们告诉它当发送一个字符串时,该字符串明确是一个 text()
构造,或者是列、表等,如果我们将其用作 order by、group by 或其他表达式中的标签名称,则 SQLAlchemy 期望该字符串解析为已知内容,否则它应该再次使用 text()
或类似内容进行限定。
#2992 ### 使用多值插入时,为每一行单独调用 Python 端默认值
当使用Insert.values()
的多值版本时,对于 Python 端列默认值的支持基本上没有实现,并且只会在特定情况下“意外”工作,当使用的方言使用非位置(例如,命名)风格的绑定参数,并且不需要为每一行调用 Python 端可调用时。
该功能已进行了改进,使其更类似于“executemany”样式的调用:
import itertools counter = itertools.count(1) t = Table( "my_table", metadata, Column("id", Integer, default=lambda: next(counter)), Column("data", String), ) conn.execute( t.insert().values( [ {"data": "d1"}, {"data": "d2"}, {"data": "d3"}, ] ) )
上述示例将为每一行单独调用next(counter)
,这是预期的行为:
INSERT INTO my_table (id, data) VALUES (?, ?), (?, ?), (?, ?) (1, 'd1', 2, 'd2', 3, 'd3')
以前,位置方言会失败,因为不会为额外的位置生成绑定:
Incorrect number of bindings supplied. The current statement uses 6, and there are 4 supplied. [SQL: u'INSERT INTO my_table (id, data) VALUES (?, ?), (?, ?), (?, ?)'] [parameters: (1, 'd1', 'd2', 'd3')]
并且使用“命名”方言时,“id”的相同值将在每一行中重新使用(因此,此更改与依赖于此的系统不兼容):
INSERT INTO my_table (id, data) VALUES (:id, :data_0), (:id, :data_1), (:id, :data_2) -- {u'data_2': 'd3', u'data_1': 'd2', u'data_0': 'd1', 'id': 1}
系统还将拒绝将“服务器端”默认值作为内联渲染的 SQL 调用,因为无法保证服务器端默认值与此兼容。如果 VALUES 子句为特定列渲染,则需要 Python 端值;如果省略的值仅引用服务器端默认值,则会引发异常:
t = Table( "my_table", metadata, Column("id", Integer, primary_key=True), Column("data", String, server_default="some default"), ) conn.execute( t.insert().values( [ {"data": "d1"}, {"data": "d2"}, {}, ] ) )
将会引发:
sqlalchemy.exc.CompileError: INSERT value for column my_table.data is explicitly rendered as a boundparameter in the VALUES clause; a Python-side value or SQL expression is required
以前,“d1”值将被复制到第三行的值中(但仅适用于命名格式!):
INSERT INTO my_table (data) VALUES (:data_0), (:data_1), (:data_0) -- {u'data_1': 'd2', u'data_0': 'd1'}
#3288 ### 无法在事件的运行器内添加或删除事件侦听器
在事件内部从中删除事件侦听器将在迭代过程中修改列表的元素,这将导致仍附加的事件侦听器无声地失败。为了防止这种情况,同时保持性能,这些列表已被替换为collections.deque()
,在迭代过程中不允许添加或删除,并且会引发RuntimeError
。
#3163 ### INSERT…FROM SELECT 构造现在意味着 inline=True
现在使用Insert.from_select()
会隐含在insert()
上设置inline=True
。这有助于修复一个错误,即 INSERT…FROM SELECT 结构会在支持的后端上不经意地编译为“implicit returning”,这会导致在插入零行的情况下(因为 implicit returning 需要一行)出现故障,以及在插入多行的情况下出现任意返回数据(例如,许多行中的第一行)。类似的更改也应用于具有多个参数集的 INSERT…VALUES;这种语句也不会再发出 implicit RETURNING。由于这两个结构都涉及可变数量的行,因此ResultProxy.inserted_primary_key
访问器不适用。以前,有一个文档说明,即在一些数据库不支持返回的情况下,可能更喜欢inline=True
与 INSERT…FROM SELECT,因此无法执行“implicit”返回,但无论如何,INSERT…FROM SELECT 都不需要 implicit returning。如果需要插入的数据,应该使用常规的显式Insert.returning()
来返回可变数量的结果行。
#3169 ### autoload_with
现在隐含了 autoload=True
通过仅传递Table.autoload_with
,可以设置Table
以进行反射。
my_table = Table("my_table", metadata, autoload_with=some_engine)
#3027 ### DBAPI 异常包装和handle_error()
事件改进
在Connection
对象失效后,尝试重新连接并遇到错误时,SQLAlchemy 对 DBAPI 异常的包装没有发生;这个问题已经解决。
另外,最近添加的ConnectionEvents.handle_error()
事件现在在初始连接、重新连接时以及当通过create_engine()
给定自定义连接函数时被调用。
ExceptionContext
对象有一个新的数据成员ExceptionContext.engine
,它将始终指向正在使用的Engine
,在那些Connection
对象不可用的情况下(例如在初始连接时)。
#3266 ### ForeignKeyConstraint.columns 现在是一个 ColumnCollection
ForeignKeyConstraint.columns
以前是一个普通列表,其中包含字符串或Column
对象,具体取决于如何构建ForeignKeyConstraint
以及它是否与表相关联。该集合现在是一个ColumnCollection
,并且仅在ForeignKeyConstraint
与Table
相关联后才初始化。添加了一个新的访问器ForeignKeyConstraint.column_keys
,无论对象如何构建或其当前状态如何,都会无条件地返回本地列集的字符串键。 ### MetaData.sorted_tables 访问器是“确定性的”
由MetaData.sorted_tables
访问器返回的表的排序是“确定性的”;无论 Python 哈希如何,排序在所有情况下都应该是相同的。这是通过首先按名称对表进行排序,然后将它们传递给拓扑算法来实现的,该算法在迭代时保持该顺序。
请注意,此更改尚未应用于发出MetaData.create_all()
或MetaData.drop_all()
时应用的排序。
#3084 ### null()、false()和 true()常量不再是单例
这三个常量在 0.9 中被更改为返回“单例”值;不幸的是,这将导致类似以下的查询无法按预期渲染:
select([null(), null()])
只渲染 SELECT NULL AS anon_1
,因为两个 null()
构造将输出相同的 NULL
对象,而 SQLAlchemy 的核心模型是基于对象标识来确定词法重要性的。0.9 中的更改除了希望节省对象开销外并无重要性;通常,未命名的构造需要保持词法唯一,以便被唯一标记。
#3170 ### SQLite/Oracle 有不同的临时表/视图名称报告方法
在 SQLite/Oracle 的情况下,Inspector.get_table_names()
和 Inspector.get_view_names()
方法也会返回临时表和视图的名称,这是任何其他方言都不提供的(至少在 MySQL 的情况下甚至不可能)。这一逻辑已经移出到两个新方法 Inspector.get_temp_table_names()
和 Inspector.get_temp_view_names()
。
请注意,对于大多数(如果不是全部)方言,通过 Table('name', autoload=True)
或通过 Inspector.get_columns()
等方法反射特定命名的临时表或临时视图仍然有效。特别是对于 SQLite,还修复了从临时表中反射 UNIQUE 约束的 bug,这是 #3203。
方言改进和变更 - PostgreSQL
ENUM 类型创建/删除规则的全面改进
对于 PostgreSQL 的 ENUM
规则在创建和删除类型时变得更加严格。
创建的 ENUM
如果没有明确与 MetaData
对象关联,将会在 Table.create()
和 Table.drop()
时创建 和 删除:
table = Table( "sometable", metadata, Column("some_enum", ENUM("a", "b", "c", name="myenum")) ) table.create(engine) # will emit CREATE TYPE and CREATE TABLE table.drop(engine) # will emit DROP TABLE and DROP TYPE - new for 1.0
这意味着如果第二个表也有一个名为 ‘myenum’ 的枚举,上述 DROP 操作现在将失败。为了适应共享枚举类型的常见用例,元数据关联的枚举行为已经得到增强。
创建的 ENUM
如果明确与 MetaData
对象关联,将不会在 Table.create()
和 Table.drop()
时创建 或 删除,除了在调用带有 checkfirst=True
标志的 Table.create()
时:
my_enum = ENUM("a", "b", "c", name="myenum", metadata=metadata) table = Table("sometable", metadata, Column("some_enum", my_enum)) # will fail: ENUM 'my_enum' does not exist table.create(engine) # will check for enum and emit CREATE TYPE table.create(engine, checkfirst=True) table.drop(engine) # will emit DROP TABLE, *not* DROP TYPE metadata.drop_all(engine) # will emit DROP TYPE metadata.create_all(engine) # will emit CREATE TYPE
新的 PostgreSQL 表选项
在通过 Table
构造渲染 DDL 时,增加了对 PG 表选项 TABLESPACE、ON COMMIT、WITH(OUT) OIDS 和 INHERITS 的支持。
另请参见
PostgreSQL 表选项
具有 PostgreSQL 方言的新 get_enums() 方法
inspect()
方法在 PostgreSQL 的情况下返回一个 PGInspector
对象,其中包括一个新的 PGInspector.get_enums()
方法,返回所有可用 ENUM
类型的信息:
from sqlalchemy import inspect, create_engine engine = create_engine("postgresql+psycopg2://host/dbname") insp = inspect(engine) print(insp.get_enums())
另请参见
PGInspector.get_enums()
### PostgreSQL 方言反映了物化视图、外部表
更改如下:
- 具有
autoload=True
的Table
构造现在将匹配数据库中存在的物化视图或外部表的名称。 Inspector.get_view_names()
将返回普通和物化视图名称。Inspector.get_table_names()
对于 PostgreSQL 不会发生变化,它继续只返回普通表的名称。- 添加了一个新方法
PGInspector.get_foreign_table_names()
,它将返回在 PostgreSQL 模式表中明确标记为“外部”的表的名称。
反射的变化涉及在查询 pg_class.relkind
时添加 'm'
和 'f'
到我们使用的修饰符列表,但这个变化是在 1.0.0 中新增的,以避免对那些在生产中运行 0.9 版本的用户造成任何不兼容的惊喜。
#2891 ### PostgreSQL has_table()
现在适用于临时表
这是一个简单的修复,使得临时表的“有表”现在可以正常工作,因此像下面的代码可以继续执行:
from sqlalchemy import * metadata = MetaData() user_tmp = Table( "user_tmp", metadata, Column("id", INT, primary_key=True), Column("name", VARCHAR(50)), prefixes=["TEMPORARY"], ) e = create_engine("postgresql://scott:tiger@localhost/test", echo="debug") with e.begin() as conn: user_tmp.create(conn, checkfirst=True) # checkfirst will succeed user_tmp.create(conn, checkfirst=True)
如果这种行为导致一个非失败的应用程序表现出不同的行为,那是因为 PostgreSQL 允许一个非临时表悄悄地覆盖一个临时表。因此,像下面的代码现在将完全不同,不再创建真实表来跟随临时表:
from sqlalchemy import * metadata = MetaData() user_tmp = Table( "user_tmp", metadata, Column("id", INT, primary_key=True), Column("name", VARCHAR(50)), prefixes=["TEMPORARY"], ) e = create_engine("postgresql://scott:tiger@localhost/test", echo="debug") with e.begin() as conn: user_tmp.create(conn, checkfirst=True) m2 = MetaData() user = Table( "user_tmp", m2, Column("id", INT, primary_key=True), Column("name", VARCHAR(50)), ) # in 0.9, *will create* the new table, overwriting the old one. # in 1.0, *will not create* the new table user.create(conn, checkfirst=True)
#3264 ### PostgreSQL FILTER 关键字
PostgreSQL 现在支持聚合函数的 SQL 标准 FILTER 关键字,从 9.4 版开始。SQLAlchemy 允许使用 FunctionElement.filter()
来实现这一点:
func.count(1).filter(True)
另请参阅
FunctionElement.filter()
FunctionFilter
PG8000 方言支持客户端端编码
现在,create_engine.encoding
参数已被 pg8000 方言所遵守,使用连接处理程序发出 SET CLIENT_ENCODING
匹配所选编码。
PG8000 原生 JSONB 支持
添加了对大于 1.10.1 版本的 PG8000 的支持,其中原生支持 JSONB。
对 PyPy 上的 psycopg2cffi 方言的支持
添加了对 pypy psycopg2cffi 方言的支持。
另请参阅
sqlalchemy.dialects.postgresql.psycopg2cffi
方言改进和更改 - MySQL
MySQL TIMESTAMP 类型现在在所有情况下呈现 NULL / NOT NULL
MySQL 方言一直通过为具有nullable=True
设置的 TIMESTAMP 列发出 NULL 来解决 MySQL 与 TIMESTAMP 列相关的隐式 NOT NULL 默认值的问题。然而,MySQL 5.6.6 及以上版本具有一个新标志explicit_defaults_for_timestamp,它修复了 MySQL 的非标准行为,使其表现得像任何其他类型;为了适应这一点,SQLAlchemy 现在无条件地为所有 TIMESTAMP 列发出 NULL/NOT NULL。
另请参见
TIMESTAMP 列和 NULL
#3155 ### MySQL SET 类型进行了全面改进,以支持空集、unicode、空值处理
历史上,SET
类型并未包含处理空集和空值的系统;由于不同的驱动程序对空字符串和空字符串集表示的处理方式不同,SET 类型仅尝试在这些行为之间进行权衡,选择将空集视为set([''])
,这仍然是 MySQL-Connector-Python DBAPI 的当前行为。这里的部分理由是,否则在 MySQL SET 中实际上无法存储空字符串,因为驱动程序返回没有办法区分set([''])
和set()
的字符串。用户需要确定set([''])
是否实际上表示“空集”。
新行为将空字符串的用例移动到一个特殊情况中,这是一个在 MySQL 文档中甚至没有记录的不寻常情况,并且SET
的默认行为现在是:
- 将由 MySQL-python 返回的空字符串
''
视为空集set()
; - 将 MySQL-Connector-Python 返回的单空值集
set([''])
转换为空集set()
; - 为了处理实际希望在其可能值列表中包含空值
''
的集合类型的情况,实现了一个新功能(在这种用例中是必需的),其中集合值被持久化和加载为位整数值;添加了标志SET.retrieve_as_bitwise
以启用此功能。
使用SET.retrieve_as_bitwise
标志允许集合在没有值歧义的情况下持久化和检索。理论上,只要给定的值列表与数据库中声明的顺序完全匹配,就可以在所有情况下打开此标志;它只会使 SQL 回显输出有点不寻常。
否则,SET
的默认行为保持不变,使用字符串来往复值。基于字符串的行为现在完全支持 unicode,包括使用 use_unicode=0 的 MySQL-python。
MySQL 内部的“无此表”异常不会传递给事件处理程序
MySQL 方言现在将禁用ConnectionEvents.handle_error()
事件,以防止这些语句触发内部用于检测表是否存在的事件处理程序。这是通过使用一个执行选项skip_user_error_events
来实现的,该选项在该执行范围内禁用处理错误事件。通过这种方式,重写异常的用户代码不需要担心 MySQL 方言或其他偶尔需要捕获 SQLAlchemy 特定异常的方言。
更改了 MySQL-Connector 的raise_on_warnings
默认值
将“raise_on_warnings”的默认值更改为 False,以用于 MySQL-Connector。由于某种原因,此值设置为 True。不幸的是,“buffered”标志必须保持为 True,因为 MySQL 连接器不允许关闭游标,除非所有结果都完全获取。
MySQL 布尔符号“true”、“false”再次有效
0.9 版本对 IS/IS NOT 运算符以及#2682中的布尔类型进行了彻底改造,禁止 MySQL 方言在“IS”/“IS NOT”上下文中使用“true”和“false”符号。显然,即使 MySQL 没有“布尔”类型,但当使用特殊的“true”和“false”符号时,它支持 IS/IS NOT,尽管这些符号在其他情况下与“1”和“0”是同义的(并且 IS/IS NOT 不能与数字一起使用)。
因此,这里的变化是 MySQL 方言仍然保持“非本地布尔”,但true()
和false()
符号再次产生关键字“true”和“false”,因此像column.is_(true())
这样的表达式在 MySQL 上再次有效。
#3186 ### match()运算符现在返回与 MySQL 浮点返回值兼容的 MatchType
ColumnOperators.match()
表达式的返回类型现在是一个称为MatchType
的新类型。这是Boolean
的子类,可以被方言拦截以在 SQL 执行时产生不同的结果类型。
像下面这样的代码现在将正常运行并在 MySQL 上返回浮点数:
>>> connection.execute( ... select( ... [ ... matchtable.c.title.match("Agile Ruby Programming").label("ruby"), ... matchtable.c.title.match("Dive Python").label("python"), ... matchtable.c.title, ... ] ... ).order_by(matchtable.c.id) ... ) [ (2.0, 0.0, 'Agile Web Development with Ruby On Rails'), (0.0, 2.0, 'Dive Into Python'), (2.0, 0.0, "Programming Matz's Ruby"), (0.0, 0.0, 'The Definitive Guide to Django'), (0.0, 1.0, 'Python in a Nutshell') ]
#3263 ### Drizzle 方言现在是一个外部方言
Drizzle的方言现在是一个外部方言,可在bitbucket.org/zzzeek/sqlalchemy-drizzle
上找到。这个方言是在 SQLAlchemy 能够很好地适应第三方方言之前添加到 SQLAlchemy 的;未来,所有不属于“普遍使用”类别的数据库都是第三方方言。方言的实现没有改变,仍然基于 SQLAlchemy 中的 MySQL + MySQLdb 方言。该方言尚未发布,处于“attic”状态;但是它通过了大部分测试,通常工作正常,如果有人想要继续完善它。
方言改进和变化 - SQLite
SQLite 命名和未命名的唯一和外键约束将进行检查和反映
SQLite 现在完全反映了有名称和无名称的唯一和外键约束。以前,外键名称被忽略,未命名的唯一约束被跳过。特别是这将有助于 Alembic 的新 SQLite 迁移功能。
为了实现这一点,对于外键和唯一约束,将 PRAGMA foreign_keys、index_list 和 index_info 的结果与对 CREATE TABLE 语句的正则表达式解析相结合,以形成对约束名称的完整描述,以及区分作为唯一约束创建的唯一约束与未命名 INDEX 的不同。
方言改进和变化 - SQL Server
使用基于主机名的 SQL Server 连接需要 PyODBC 驱动程序名称
使用无 DSN 连接的 PyODBC 连接到 SQL Server,例如使用显式主机名,现在需要一个驱动程序名称 - SQLAlchemy 将不再尝试猜测默认值:
engine = create_engine( "mssql+pyodbc://scott:tiger@myhost:port/databasename?driver=SQL+Server+Native+Client+10.0" )
SQLAlchemy 在 Windows 上以前硬编码的默认值“SQL Server”已经过时,SQLAlchemy 不能根据操作系统/驱动程序检测来猜测最佳驱动程序。在使用 ODBC 时,始终首选使用 DSN 以避免这个问题。
SQL Server 2012 大文本/二进制类型呈现为 VARCHAR、NVARCHAR、VARBINARY
对于 SQL Server 2012 及更高版本,TextClause
、UnicodeText
和 LargeBinary
类型的呈现已经更改,可以完全控制行为,根据 Microsoft 的弃用指南。有关详细信息,请参阅大文本/二进制类型弃用。
方言改进和更改 - Oracle
改进的 Oracle CTE 支持
CTE 在 Oracle 中已经修复,还有一个新功能 CTE.with_suffixes()
可以帮助处理 Oracle 的特殊指令:
included_parts = ( select([part.c.sub_part, part.c.part, part.c.quantity]) .where(part.c.part == "p1") .cte(name="included_parts", recursive=True) .suffix_with( "search depth first by part set ord1", "cycle part set y_cycle to 1 default 0", dialect="oracle", ) )
DDL 的新 Oracle 关键字
COMPRESS、ON COMMIT、BITMAP 等关键字:
Oracle 表选项
Oracle 特定索引选项
介绍
本指南介绍了 SQLAlchemy 版本 1.0 中的新功能,并记录了影响用户将其应用程序从 SQLAlchemy 0.9 系列迁移到 1.0 的更改。
请仔细查看行为变化部分,可能会有不兼容的行为变化。
新功能和改进 - ORM
新会话批量插入/更新 API
创建了一系列新的Session
方法,直接提供钩子到工作单元的功能,用于生成批量插入和更新语句分组,使语句可以以与直接使用 Core 相媲美的速度进行批量处理。
另请参阅
批量操作 - 介绍和完整文档
新性能示例套件
受到为批量操作功能以及如何对 SQLAlchemy 驱动的应用程序进行性能分析?FAQ 部分进行的基准测试的启发,添加了一个新的示例部分,其中包含几个旨在说明各种核心和 ORM 技术的相对性能特征的脚本。这些脚本按用例组织,并打包在一个单一的控制台界面下,以便可以运行任何组合的演示,输出时间、Python 分析结果和/或 RunSnake 分析显示。
另请参阅
性能
“烘焙”查询
“烘焙”查询功能是一种不同寻常的新方法,允许直接构造和调用Query
对象,并使用缓存,在连续调用时大大减少了 Python 函数调用的开销(超过 75%)。通过将Query
对象指定为一系列仅调用一次的 lambda 表达式,可以开始将查询作为预编译单元来实现:
from sqlalchemy.ext import baked from sqlalchemy import bindparam bakery = baked.bakery() def search_for_user(session, username, email=None): baked_query = bakery(lambda session: session.query(User)) baked_query += lambda q: q.filter(User.name == bindparam("username")) baked_query += lambda q: q.order_by(User.id) if email: baked_query += lambda q: q.filter(User.email == bindparam("email")) result = baked_query(session).params(username=username, email=email).all() return result
另请参见
烘焙查询
改进声明混合,@declared_attr
和相关功能
使用declared_attr
的声明系统已经进行了全面改进,以支持新的功能。
现在使用declared_attr
装饰的函数仅在生成任何基于混合的列副本后才调用。这意味着函数可以调用混合建立的列,并将接收到正确的Column
对象的引用:
class HasFooBar(object): foobar = Column(Integer) @declared_attr def foobar_prop(cls): return column_property("foobar: " + cls.foobar) class SomeClass(HasFooBar, Base): __tablename__ = "some_table" id = Column(Integer, primary_key=True)
在上面的例子中,SomeClass.foobar_prop
将针对 SomeClass
调用,并且 SomeClass.foobar
将是要映射到 SomeClass
的最终 Column
对象,而不是直接存在于 HasFooBar
上的非复制对象,即使列还没有映射。
现在,declared_attr
函数在每个类基础上备忘返回的值,因此对相同属性的重复调用将返回相同的值。我们可以修改示例来说明这一点:
class HasFooBar(object): @declared_attr def foobar(cls): return Column(Integer) @declared_attr def foobar_prop(cls): return column_property("foobar: " + cls.foobar) class SomeClass(HasFooBar, Base): __tablename__ = "some_table" id = Column(Integer, primary_key=True)
以前,SomeClass
将使用一个特定的 foobar
列进行映射,但通过第二次调用 foobar
来调用 foobar_prop
将产生一个不同的列。现在,在声明设置时间内对 SomeClass.foobar
的值进行了备忘,因此即使在映射器将属性映射之前,declared_attr
被调用多少次,临时列值也将保持一致。
上述两个行为应大大有助于声明定义许多类型的映射器属性,这些属性源自其他属性,其中declared_attr
函数在实际映射类之前从其他declared_attr
函数本地调用。
对于一个相当特殊的边缘情况,其中希望构建一个声明性混合类,为每个子类建立不同的列,添加了一个新的修饰符 declared_attr.cascading
。使用这个修饰符,装饰的函数将为映射继承层次结构中的每个类单独调用。虽然对于特殊属性如 __table_args__
和 __mapper_args__
,这已经是默认行为,但对于列和其他属性,默认行为假定该属性仅附加到基类,并从子类继承。使用 declared_attr.cascading
,可以应用个别行为:
class HasIdMixin(object): @declared_attr.cascading def id(cls): if has_inherited_table(cls): return Column(ForeignKey("myclass.id"), primary_key=True) else: return Column(Integer, primary_key=True) class MyClass(HasIdMixin, Base): __tablename__ = "myclass" # ... class MySubClass(MyClass): """ """ # ...
另请参见
使用 _orm.declared_attr() 生成特定表继承列的链接
最后,AbstractConcreteBase
类已经重新设计,以便在抽象基类上内联设置关系或其他映射属性:
from sqlalchemy import Column, Integer, ForeignKey from sqlalchemy.orm import relationship from sqlalchemy.ext.declarative import ( declarative_base, declared_attr, AbstractConcreteBase, ) Base = declarative_base() class Something(Base): __tablename__ = "something" id = Column(Integer, primary_key=True) class Abstract(AbstractConcreteBase, Base): id = Column(Integer, primary_key=True) @declared_attr def something_id(cls): return Column(ForeignKey(Something.id)) @declared_attr def something(cls): return relationship(Something) class Concrete(Abstract): __tablename__ = "cca" __mapper_args__ = {"polymorphic_identity": "cca", "concrete": True}
上述映射将建立一个名为 cca
的表,其中包含 id
和 something_id
列,而 Concrete
还将具有一个名为 something
的关系。新功能是 Abstract
也将具有一个独立配置的关系 something
,该关系针对基类的多态联合构建。
ORM 完整对象提取速度提高了 25%
loading.py
模块的机制以及标识映射已经经历了几次内联、重构和修剪,因此现在原始行的加载速度比以前快了大约 25%。假设有一个包含 100 万行的表,下面的脚本说明了哪种加载方式得到了最大的改进:
import time from sqlalchemy import Integer, Column, create_engine, Table from sqlalchemy.orm import Session from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class Foo(Base): __table__ = Table( "foo", Base.metadata, Column("id", Integer, primary_key=True), Column("a", Integer(), nullable=False), Column("b", Integer(), nullable=False), Column("c", Integer(), nullable=False), ) engine = create_engine("mysql+mysqldb://scott:tiger@localhost/test", echo=True) sess = Session(engine) now = time.time() # avoid using all() so that we don't have the overhead of building # a large list of full objects in memory for obj in sess.query(Foo).yield_per(100).limit(1000000): pass print("Total time: %d" % (time.time() - now))
本地 MacBookPro 结果从 0.9 的 19 秒降至 1.0 的 14 秒。当批处理大量行时,Query.yield_per()
的调用总是一个好主意,因为它可以防止 Python 解释器一次性为所有对象及其仪器分配大量内存。没有 Query.yield_per()
,在 MacBookPro 上的上述脚本在 0.9 上为 31 秒,在 1.0 上为 26 秒,额外的时间花费在设置非常大的内存缓冲区上。
新的 KeyedTuple 实现速度大幅提升
我们研究了 KeyedTuple
的实现,希望改进这样的查询:
rows = sess.query(Foo.a, Foo.b, Foo.c).all()
使用 KeyedTuple
类而不是 Python 的 collections.namedtuple()
,因为后者具有一个非常复杂的类型创建程序,比 KeyedTuple
的速度慢得多。然而,当获取数十万行时,collections.namedtuple()
很快就会超过 KeyedTuple
,随着实例调用次数的增加,KeyedTuple
的速度会急剧变慢。怎么办?一种新类型,介于两者之间的方法。对于“大小”(返回的行数)和“num”(不同查询的数量)对所有三种类型进行测试,新的“轻量级键值元组”要么优于两者,要么略逊于更快的对象,取决于情况。在“甜蜜点”,即我们既创建了大量新类型又获取了大量行时,轻量级对象完全超过了 namedtuple 和 KeyedTuple:
----------------- size=10 num=10000 # few rows, lots of queries namedtuple: 3.60302400589 # namedtuple falls over keyedtuple: 0.255059957504 # KeyedTuple very fast lw keyed tuple: 0.582715034485 # lw keyed trails right on KeyedTuple ----------------- size=100 num=1000 # <--- sweet spot namedtuple: 0.365247011185 keyedtuple: 0.24896979332 lw keyed tuple: 0.0889317989349 # lw keyed blows both away! ----------------- size=10000 num=100 namedtuple: 0.572599887848 keyedtuple: 2.54251694679 lw keyed tuple: 0.613876104355 ----------------- size=1000000 num=10 # few queries, lots of rows namedtuple: 5.79669594765 # namedtuple very fast keyedtuple: 28.856498003 # KeyedTuple falls over lw keyed tuple: 6.74346804619 # lw keyed trails right on namedtuple
#3176 ### 结构内存使用显著改进
通过更多内部对象更显著地使用 __slots__
,改进了结构内存使用。这种优化特别针对具有大量表和列的大型应用程序的基本内存大小,减少了各种高容量对象的内存大小,包括事件监听内部、比较器对象以及 ORM 属性和加载器策略系统的部分。
一张长椅利用堆积测量 Nova 的启动大小,显示出 SQLAlchemy 的对象、相关字典以及弱引用所占空间减少了约 3.7 兆字节,或者说减少了 46%,在基本导入“nova.db.sqlalchemy.models”时:
# reported by heapy, summation of SQLAlchemy objects + # associated dicts + weakref-related objects with core of Nova imported: Before: total count 26477 total bytes 7975712 After: total count 18181 total bytes 4236456 # reported for the Python module space overall with the # core of Nova imported: Before: Partition of a set of 355558 objects. Total size = 61661760 bytes. After: Partition of a set of 346034 objects. Total size = 57808016 bytes. ```### UPDATE 语句现在使用 executemany() 进行批处理 现在可以在 ORM 刷新中批处理 UPDATE 语句,以更高效的 executemany() 调用,类似于 INSERT 语句的批处理;这将根据以下标准在刷新中调用: + 连续两个或更多个 UPDATE 语句涉及相同的要修改的列集。 + 语句在 SET 子句中没有嵌入的 SQL 表达式。 + 映射不使用 `mapper.version_id_col`,或者后端方言支持 executemany() 操作的“合理”行数;大多数 DBAPI 现在都正确支持这一点。 ### Session.get_bind() 处理更广泛的继承场景 每当查询或工作单元刷新过程寻找与特定类对应的数据库引擎时,都会调用 `Session.get_bind()` 方法。该方法已经改进,以处理各种继承导向的场景,包括: + 绑定到 Mixin 或抽象类: ```py class MyClass(SomeMixin, Base): __tablename__ = "my_table" # ... session = Session(binds={SomeMixin: some_engine}) ``` + 根据表单独绑定到继承的具体子类: ```py class BaseClass(Base): __tablename__ = "base" # ... class ConcreteSubClass(BaseClass): __tablename__ = "concrete" # ... __mapper_args__ = {"concrete": True} session = Session(binds={base_table: some_engine, concrete_table: some_other_engine}) ``` [#3035](https://www.sqlalchemy.org/trac/ticket/3035) ### 在所有相关的查询情况下,`Session.get_bind()` 将接收到 Mapper 修复了一系列问题,其中 `Session.get_bind()` 不会接收到 `Query` 的主要 `Mapper`,尽管该映射器是 readily available 的(主要映射器是与 `Query` 对象关联的单个映射器,或者是第一个映射器)。 当传递给 `Session.get_bind()` 的 `Mapper` 对象通常由使用 `Session.binds` 参数将映射与一系列引擎关联的会话使用(尽管在这种情况下,大多数情况下事情通常“工作”,因为绑定通常通过映射表对象找到),或更具体地实现一个用户定义的 `Session.get_bind()` 方法,根据映射器选择引擎的某种模式,比如水平分片或所谓的“路由”会话,将查询路由到不同的后端。 这些场景包括: + `Query.count()`: ```py session.query(User).count() ``` + `Query.update()` 和 `Query.delete()`,用于 UPDATE/DELETE 语句以及“fetch”策略所使用的 SELECT: ```py session.query(User).filter(User.id == 15).update( {"name": "foob"}, synchronize_session="fetch" ) session.query(User).filter(User.id == 15).delete(synchronize_session="fetch") ``` + 针对单独列的查询: ```py session.query(User.id, User.name).all() ``` + 针对间接映射的 SQL 函数和其他表达式,比如 `column_property`: ```py class User(Base): ... score = column_property(func.coalesce(self.tables.users.c.name, None)) session.query(func.max(User.score)).scalar() ``` [#3227](https://www.sqlalchemy.org/trac/ticket/3227) [#3242](https://www.sqlalchemy.org/trac/ticket/3242) [#1326](https://www.sqlalchemy.org/trac/ticket/1326) ### .info 字典改进 `InspectionAttr.info` 集合现在可以在从`Mapper.all_orm_descriptors` 集合中检索到的每种对象上使用。这包括`hybrid_property` 和 `association_proxy()`。然而,由于这些对象是类绑定描述符,必须**单独**从它们所附加的类中访问以获取属性。以下是使用`Mapper.all_orm_descriptors` 命名空间进行说明: ```py class SomeObject(Base): # ... @hybrid_property def some_prop(self): return self.value + 5 inspect(SomeObject).all_orm_descriptors.some_prop.info["foo"] = "bar"
它也作为所有SchemaItem
对象(例如ForeignKey
、UniqueConstraint
等)的构造函数参数,以及剩余的 ORM 构造,如synonym()
。
#2963 ### ColumnProperty 结构在使用别名、order_by 时效果更好
已修复了关于column_property()
的各种问题,特别是关于aliased()
构造以及 0.9 版本中引入的“order by label”逻辑(参见 Label constructs can now render as their name alone in an ORDER BY)。
给定如下映射:
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.b = column_property(select([func.max(B.id)]).where(B.a_id == A.id).correlate(A))
一个简单的包含两次“A.b”的场景将无法正确呈现:
print(sess.query(A, a1).order_by(a1.b))
这将按照错误的列排序:
SELECT a.id AS a_id, (SELECT max(b.id) AS max_1 FROM b WHERE b.a_id = a.id) AS anon_1, a_1.id AS a_1_id, (SELECT max(b.id) AS max_2 FROM b WHERE b.a_id = a_1.id) AS anon_2 FROM a, a AS a_1 ORDER BY anon_1
新输出:
SELECT a.id AS a_id, (SELECT max(b.id) AS max_1 FROM b WHERE b.a_id = a.id) AS anon_1, a_1.id AS a_1_id, (SELECT max(b.id) AS max_2 FROM b WHERE b.a_id = a_1.id) AS anon_2 FROM a, a AS a_1 ORDER BY anon_2
还有许多情况下,“order by”逻辑将无法按照标签排序,例如如果映射是“多态”的情况:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) type = Column(String) __mapper_args__ = {"polymorphic_on": type, "with_polymorphic": "*"}
order_by 将无法使用标签,因为由于多态加载而被匿名化:
SELECT a.id AS a_id, a.type AS a_type, (SELECT max(b.id) AS max_1 FROM b WHERE b.a_id = a.id) AS anon_1 FROM a ORDER BY (SELECT max(b.id) AS max_2 FROM b WHERE b.a_id = a.id)
现在 order by 标签跟踪匿名化标签,这现在可以工作:
SELECT a.id AS a_id, a.type AS a_type, (SELECT max(b.id) AS max_1 FROM b WHERE b.a_id = a.id) AS anon_1 FROM a ORDER BY anon_1
这些修复中包括了各种可能破坏aliased()
构造状态的 heisenbugs;这些问题也已经修复。
新的会话批量插入/更新 API
创建了一系列新的Session
方法,直接提供钩子进入工作单元的功能,用于发出 INSERT 和 UPDATE 语句。当正确使用时,这个面向专家的系统可以允许 ORM 映射用于生成批量插入和更新语句,分批执行到 executemany 组,使语句以与直接使用 Core 相媲美的速度进行。
另请参阅
批量操作 - 介绍和完整文档
新的性能示例套件
受到对批量操作功能以及 FAQ 中的如何对 SQLAlchemy 应用程序进行性能分析?部分进行的基准测试的启发,添加了一个新的示例部分,其中包含几个旨在说明各种 Core 和 ORM 技术的相对性能概况的脚本。这些脚本按用例组织,并打包在一个单一的控制台界面下,以便可以运行任意组合的演示,输出时间、Python 性能分析结果和/或 RunSnake 性能显示。
另请参阅
性能
“烘焙”查询
“烘焙”查询功能是一种不同寻常的新方法,允许使用缓存直接构建和调用Query
对象,随着连续调用,Python 函数调用开销大大降低(超过 75%)。通过将Query
对象指定为一系列仅调用一次的 lambda 表达式,作为预编译单元的查询开始变得可行:
from sqlalchemy.ext import baked from sqlalchemy import bindparam bakery = baked.bakery() def search_for_user(session, username, email=None): baked_query = bakery(lambda session: session.query(User)) baked_query += lambda q: q.filter(User.name == bindparam("username")) baked_query += lambda q: q.order_by(User.id) if email: baked_query += lambda q: q.filter(User.email == bindparam("email")) result = baked_query(session).params(username=username, email=email).all() return result
另请参阅
烘焙查询
SqlAlchemy 2.0 中文文档(七十六)(3)https://developer.aliyun.com/article/1561143