SqlAlchemy-2-0-中文文档-四十八-
SqlAlchemy 2.0 中文文档(四十八)
SQLAlchemy 1.3 有什么新功能?
关于本文档
本文描述了 SQLAlchemy 版本 1.2 和 SQLAlchemy 版本 1.3 之间的更改。
介绍
本指南介绍了 SQLAlchemy 版本 1.3 中的新功能,还记录了影响用户将其应用程序从 SQLAlchemy 1.2 系列迁移到 1.3 的更改。
请仔细查看行为变化部分,可能会有不兼容的行为变化。
通用
对所有弃用元素发出弃用警告;新增弃用项
发行版 1.3 确保所有被弃用的行为和 API,包括那些长期被列为“遗留”的,都会发出DeprecationWarning
警告。这包括在使用参数时,比如Session.weak_identity_map
和类似MapperExtension
的情况。虽然所有弃用情况都已在文档中记录,但通常它们没有使用正确的重构文本指令,或者包含它们被弃用的版本。特定 API 功能是否实际发出弃用警告并不一致。一般的态度是,大多数或所有这些弃用功能都被视为长期遗留功能,没有计划删除它们。
这一变化包括,所有记录的弃用现在在文档中使用了正确的重构文本指令,并附有版本号,明确说明该功能或用例将在将来的版本中被移除(例如,不再有永久遗留用例),并且使用任何此类功能或用例将明确发出DeprecationWarning
,在 Python 3 中以及使用现代测试工具如 Pytest 时,现在在标准错误流中更加明确。目标是,这些长期被弃用的功能,甚至可以追溯到版本 0.7 或 0.6,应该开始被完全移除,而不是将它们保留为“遗留”功能。此外,从版本 1.3 开始,还添加了一些重大的新弃用项。由于 SQLAlchemy 已经被成千上万的开发人员实际使用了 14 年,可以指出一个混合得很好的用例流,以及修剪掉与这种单一工作方式相悖的功能和模式。
更大的背景是,SQLAlchemy 致力于适应即将到来的仅支持 Python 3 的世界,以及类型注释的世界,为此,SQLAlchemy 有暂定计划进行一项重大重构,希望大大减少 API 的认知负担,并对 Core 和 ORM 之间的许多实现和使用差异进行重大调整。由于这两个系统在 SQLAlchemy 首次发布后发生了巨大变化,特别是 ORM 仍然保留着许多“外挂”行为,使得 Core 和 ORM 之间的隔离墙过高。通过提前将 API 集中在每个支持的用例的单一模式上,将来迁移到显著改变的 API 的工作变得更简单。
有关 1.3 版本中添加的最重要的弃用功能,请参见下面的链接部分。
另请参阅
“threadlocal”引擎策略已弃用
convert_unicode 参数已弃用
AliasedClass 与非主映射器的关系取代了
新功能和改进 - ORM
AliasedClass 与非主映射器的关系取代了
“非主映射器”是以 Imperative Mapping 风格创建的Mapper
,它充当已经映射的类的附加映射器,针对不同类型的可选择项。非主映射器起源于 SQLAlchemy 的 0.1、0.2 系列,当时预期Mapper
对象将是主要的查询构造接口,而Query
对象尚不存在。
随着Query
的出现,以及后来的AliasedClass
构造,大多数非主映射器的用例都消失了。这是一件好事,因为 SQLAlchemy 在 0.5 系列左右也完全摆脱了“经典”映射,转而采用了声明式系统。
当意识到一些非常难以定义的relationship()
配置可能成为可能时,保留了一个非主映射器的用例,当一个具有替代可选择项的非主映射器被作为映射目标时,而不是尝试构建一个涵盖特定对象间关系所有复杂性的relationship.primaryjoin
。
随着这种用例变得更加流行,它的局限性变得明显,包括非主映射器难以配置到可选择添加新列的可选项上,映射器不继承原始映射的关系,显式配置在非主映射器上的关系与加载器选项不兼容,非主映射器也没有提供可用于查询的基于列的属性的完全功能命名空间(在旧的 0.1 - 0.4 版本中,人们会直接使用Table
对象与 ORM 一起使用)。
缺失的部分是允许relationship()
直接引用AliasedClass
。AliasedClass
已经做了我们希望非主映射器做的一切;它允许从替代可选择项加载现有映射类,继承现有映射器的所有属性和关系,与加载器选项非常配合,提供一个类似类的对象,可以像类本身一样混入查询中。通过这种改变,以前针对非主映射器的配方在配置关系连接方式中被更改为别名类。
在关系到别名类时,原始的非主映射器看起来像:
j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)
B_viacd = mapper(
B,
j,
non_primary=True,
primary_key=[j.c.b_id],
properties={
"id": j.c.b_id, # so that 'id' looks the same as before
"c_id": j.c.c_id, # needed for disambiguation
"d_c_id": j.c.d_c_id, # needed for disambiguation
"b_id": [j.c.b_id, j.c.d_b_id],
"d_id": j.c.d_id,
},
)
A.b = relationship(B_viacd, primaryjoin=A.b_id == B_viacd.c.b_id)
这些属性是必要的,以便重新映射额外的列,使其不与映射到B
的现有列发生冲突,同时也需要定义一个新的主键。
使用新方法,所有这些冗长的内容都消失了,并且在建立关系时直接引用了额外的列:
j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)
B_viacd = aliased(B, j, flat=True)
A.b = relationship(B_viacd, primaryjoin=A.b_id == j.c.b_id)
非主映射器现在已被弃用,最终目标是使经典映射作为一项功能完全消失。声明式 API 将成为映射的唯一手段,这希望能够实现内部改进和简化,以及更清晰的文档故事。
#4423 ### selectin 加载不再对简单的一对多使用 JOIN。
在 1.2 版本中添加的“selectin”加载功能引入了一种极其高效的新方法来急切加载集合,在许多情况下比“subquery”急切加载要快得多,因为它不依赖于重新声明原始 SELECT 查询,而是使用一个简单的 IN 子句。然而,“selectin”加载仍然依赖于在父表和相关表之间渲染 JOIN,因为它需要父表主键值在行中以匹配行。在 1.3 中,添加了一种新的优化,将在简单的一对多加载的最常见情况下省略此 JOIN,其中相关行已经包含了父行的主键值,表达在其外键列中。这再次提供了显著的性能改进,因为 ORM 现在可以在一个查询中加载大量集合,而根本不使用 JOIN 或子查询。
给定一个映射:
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
bs = relationship("B", lazy="selectin")
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
在“selectin”加载的 1.2 版本中,A 到 B 的加载如下:
SELECT a.id AS a_id FROM a
SELECT a_1.id AS a_1_id, b.id AS b_id, b.a_id AS b_a_id
FROM a AS a_1 JOIN b ON a_1.id = b.a_id
WHERE a_1.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY a_1.id
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
使用新行为,加载如下:
SELECT a.id AS a_id FROM a
SELECT b.a_id AS b_a_id, b.id AS b_id FROM b
WHERE b.a_id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY b.a_id
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
该行为被释放为自动的,使用类似于延迟加载使用的启发式方法,以确定是否可以直接从标识映射中获取相关实体。然而,与大多数查询功能一样,由于涉及多态加载的高级场景,该功能的实现变得更加复杂。如果遇到问题,用户应该报告错误,但更改还包括一个标志relationship.omit_join
,可以在relationship()
上设置为False
以禁用优化。
#4340 ### 改进多对一查询表达式的行为
当构建一个将多对一关系与对象值进行比较的查询时,例如:
u1 = session.query(User).get(5)
query = session.query(Address).filter(Address.user == u1)
上述表达式Address.user == u1
,最终编译为基于User
对象的主键列的 SQL 表达式,如"address.user_id = 5"
,使用延迟可调用以在绑定表达式中尽可能晚地检索值5
。这是为了适应Address.user == u1
表达式可能针对尚未刷新的User
对象的用例,该对象依赖于服务器生成的主键值,以及该表达式始终返回正确结果的情况,即使自创建表达式以来u1
的主键值已更改。
然而,这种行为的一个副作用是,如果在评估表达式时u1
最终过期,它将导致额外的 SELECT 语句,并且如果u1
也从Session
中分离,它将引发错误:
u1 = session.query(User).get(5)
query = session.query(Address).filter(Address.user == u1)
session.expire(u1)
session.expunge(u1)
query.all() # <-- would raise DetachedInstanceError
对象的过期/清除可以在Session
提交时隐式发生,并且u1
实例超出范围时,因为Address.user == u1
表达式并不强烈引用对象本身,只引用其InstanceState
。
修复的方法是允许Address.user == u1
表达式根据尝试在表达式编译时正常检索或加载值来评估值5
,就像现在一样,但如果对象已分离并已过期,则从InstanceState
上的新机制中检索,该机制将在属性过期时在该状态上记忆该属性的最后已知值。当表达式功能需要时,此机制仅为特定属性/ InstanceState
启用,以节省性能/内存开销。
最初,尝试了诸如立即评估表达式并尝试稍后加载值的各种简化方法,但困难的边缘情况是正在更改的列属性值(通常是自然主键)。为了确保像Address.user == u1
这样的表达式始终返回u1
当前状态的正确答案,它将返回持久对象的当前数据库持久化值,如果需要,通过 SELECT 查询取消过期,并且对于分离对象,它将返回最近已知的值,而不管对象何时使用InstanceState
中的新功能过期跟踪列属性的最后已知值。
当值无法评估时,现代属性 API 功能用于指示特定的错误消息,这两种情况是当列属性从未设置时,以及当对象在进行第一次评估时已过期并且现在已分离。在所有情况下,不再引发DetachedInstanceError
。
#4359 ### 多对一替换不会对“raiseload”或“old”对象引发异常
在许多对一关系上进行延迟加载以加载“旧”值的情况下,如果关系未指定relationship.active_history
标志,则不会为分离对象引发断言:
a1 = session.query(Address).filter_by(id=5).one()
session.expunge(a1)
a1.user = some_user
在上面的情况下,当在分离的a1
对象上替换.user
属性时,将引发DetachedInstanceError
,因为属性试图从标识映射中检索.user
的先前值。变化在于,操作现在继续进行而不加载旧值。
相同的更改也适用于lazy="raise"
加载策略:
class Address(Base):
# ...
user = relationship("User", ..., lazy="raise")
以前,a1.user
的关联会触发“raiseload”异常,因为属性试图检索先前的值。在加载“旧”值的情况下,现在跳过此断言。
#4353 ### 为 ORM 属性实现了“del”
Python 的del
操作实际上对于映射属性(标量列或对象引用)并不可用。已添加支持,使其能够正确工作,其中del
操作大致相当于将属性设置为None
值:
some_object = session.query(SomeObject).get(5)
del some_object.some_attribute # from a SQL perspective, works like "= None"
#4354 ### 在 InstanceState 中添加了 info 字典
将.info
字典添加到InstanceState
类中,该对象是通过调用映射对象上的inspect()
而来。这允许自定义方案添加有关对象的额外信息,这些信息将随着对象在内存中的整个生命周期而传递:
from sqlalchemy import inspect
u1 = User(id=7, name="ed")
inspect(u1).info["user_info"] = "7|ed"
#4257 ### 水平分片扩展支持批量更新和删除方法
ShardedQuery
扩展对象支持Query.update()
和Query.delete()
批量更新/删除方法。在调用它们时,将咨询query_chooser
可调用对象,以便根据给定的条件在多个分片上运行更新/删除操作。
Association Proxy 改进
尽管没有特定原因,但是在本周期内,Association Proxy 扩展进行了许多改进。
Association proxy 新增了 cascade_scalar_deletes 标志
给定一个映射如下:
class A(Base):
__tablename__ = "test_a"
id = Column(Integer, primary_key=True)
ab = relationship("AB", backref="a", uselist=False)
b = association_proxy(
"ab", "b", creator=lambda b: AB(b=b), cascade_scalar_deletes=True
)
class B(Base):
__tablename__ = "test_b"
id = Column(Integer, primary_key=True)
ab = relationship("AB", backref="b", cascade="all, delete-orphan")
class AB(Base):
__tablename__ = "test_ab"
a_id = Column(Integer, ForeignKey(A.id), primary_key=True)
b_id = Column(Integer, ForeignKey(B.id), primary_key=True)
对A.b
的赋值将生成一个AB
对象:
a.b = B()
A.b
关联是标量的,并包括一个新标志AssociationProxy.cascade_scalar_deletes
。设置时,��A.b
设置为None
也将删除A.ab
。默认行为仍然是保留a.ab
不变:
a.b = None
assert a.ab is None
起初,这种逻辑看起来应该只查看现有关系的“级联”属性,这似乎很直观,但仅凭这一点就不清楚代理对象是否应该被移除,因此行为被作为一个明确的选项提供。
此外,del
现在对标量的操作方式与设置为 None
相似:
del a.b
assert a.ab is None
#4308 #### AssociationProxy 在每个类上存储特定于类的状态
AssociationProxy
对象基于其关联的父映射类做出许多决策。虽然 AssociationProxy
最初只是一个相对简单的‘getter’,但很快就明显地需要做出关于其引用的属性类型的决策——比如标量或集合、映射对象或简单值等。为了实现这一点,它需要检查映射属性或其他引用描述符或属性,如从其父类引用的那样。然而,在 Python 描述符机制中,描述符只有在在其“父”类的上下文中被访问时才会了解其“父”类,比如调用 MyClass.some_descriptor
,这会调用 __get__()
方法,该方法传递类。因此,AssociationProxy
对象将存储特定于该类的状态,但只有在调用此方法后才会调用;在未首先将 AssociationProxy
作为描述符访问的情况下尝试检查此状态将引发错误。此外,它会假设 __get__()
首次看到的类将是唯一需要了解的父类。尽管如果特定类具有继承子类,关联代理实际上是代表不止一个父类工作,即使没有明确重用。即使在存在这种缺陷的情况下,关联代理仍然可以通过其当前行为取得相当大的进展,但在某些情况下仍存在缺陷,以及确定最佳“所有者”类的复杂问题。
现在这些问题已经得到解决,当调用 __get__()
时,AssociationProxy
不再修改自己的内部状态;相反,每个类都生成了一个名为 AssociationProxyInstance
的新对象,它处理了特定于特定映射父类的所有状态(当父类未映射时,不会生成 AssociationProxyInstance
)。关于关联代理的单一“拥有类”的概念,尽管在 1.1 中有所改进,但基本上已被一种方法所取代,即 AP 现在可以平等地处理任意数量的“拥有”类。
为了适应那些想要检查这种状态的应用程序,而不一定要调用 __get__()
的 AssociationProxy
,添加了一个新方法 AssociationProxy.for_class()
,提供了直接访问特定于类的 AssociationProxyInstance
,如下所示:
class User(Base):
# ...
keywords = association_proxy("kws", "keyword")
proxy_state = inspect(User).all_orm_descriptors["keywords"].for_class(User)
一旦我们有了 AssociationProxyInstance
对象,如上例中存储在 proxy_state
变量中,我们可以查看特定于 User.keywords
代理的属性,例如 target_class
:
>>> proxy_state.target_class
Keyword
#3423 #### AssociationProxy
现在为基于列的目标提供了标准的列操作符
给定一个 AssociationProxy
,其中目标是数据库列,并且不是对象引用或另一个关联代理:
class User(Base):
# ...
elements = relationship("Element")
# column-based association proxy
values = association_proxy("elements", "value")
class Element(Base):
# ...
value = Column(String)
User.values
关联代理指的是 Element.value
列。现在已经提供了标准的列操作,比如 like
:
>>> print(s.query(User).filter(User.values.like("%foo%")))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND element.value LIKE :value_1)
equals
:
>>> print(s.query(User).filter(User.values == "foo"))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND element.value = :value_1)
当与 None
比较时,IS NULL
表达式被增强,以测试相关行根本不存在;这与以前的行为相同:
>>> print(s.query(User).filter(User.values == None))
SELECT "user".id AS user_id
FROM "user"
WHERE (EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND element.value IS NULL)) OR NOT (EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id))
注意 ColumnOperators.contains()
操作符实际上是一个字符串比较操作符;这是行为上的变化,以前,关联代理仅将 .contains
用作列表包含操作符。使用列导向的比较,它现在的行为类似于“like”:
>>> print(s.query(User).filter(User.values.contains("foo")))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND (element.value LIKE '%' || :value_1 || '%'))
为了测试 User.values
集合是否包含值 "foo"
,应使用等号操作符(例如 User.values == 'foo'
);这在以前的版本中也适用。
当使用基于对象的关联代理与集合时,行为与以前相同,即测试集合成员资格,例如,给定一个映射:
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
user_elements = relationship("UserElement")
# object-based association proxy
elements = association_proxy("user_elements", "element")
class UserElement(Base):
__tablename__ = "user_element"
id = Column(Integer, primary_key=True)
user_id = Column(ForeignKey("user.id"))
element_id = Column(ForeignKey("element.id"))
element = relationship("Element")
class Element(Base):
__tablename__ = "element"
id = Column(Integer, primary_key=True)
value = Column(String)
.contains()
方法生成与以前相同的表达式,测试 User.elements
列表中是否存在 Element
对象:
>>> print(s.query(User).filter(User.elements.contains(Element(id=1))))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM user_element
WHERE "user".id = user_element.user_id AND :param_1 = user_element.element_id)
总的来说,这个改变是基于 AssociationProxy stores class-specific state on a per-class basis 的结构性改变而启用的;因为代理现在在生成表达式时衍生了额外的状态,所以有一个对象目标版本和一个列目标版本的AssociationProxyInstance
类。
关联代理现在强引用父对象
关联代理集合仅维护对父对象的弱引用的长期行为被还原;代理现在将在代理集合本身也在内存中的情况下维护对父对象的强引用,从而消除“stale association proxy”错误。此更改正在实验性地进行,以查看是否会引起任何副作用。
例如,给定一个具有关联代理的映射:
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
bs = relationship("B")
b_data = association_proxy("bs", "data")
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
data = Column(String)
a1 = A(bs=[B(data="b1"), B(data="b2")])
b_data = a1.b_data
以前,如果 a1
超出范围被删除:
del a1
在 a1
在范围内被删除后尝试迭代 b_data
集合会引发错误 "stale association proxy, parent object has gone out of scope"
。这是因为关联代理需要访问实际的 a1.bs
集合以生成视图,在这次更改之前,它仅维护对 a1
的弱引用。特别是,用户在执行内联操作时经常会遇到这个错误,例如:
collection = session.query(A).filter_by(id=1).first().b_data
以上,因为在 b_data
集合实际使用之前,A
对象将被垃圾回收。
更改是 b_data
集合现在维护对 a1
对象的强引用,以使其保持存在:
assert b_data == ["b1", "b2"]
此更改引入了一个副作用,即如果应用程序像上面那样传递集合,父对象在集合被丢弃之前不会被垃圾回收。一如既往,如果a1
在特定的Session
中是持久的,它将保持在该会话的状态中,直到被垃圾回收。
请注意,如果此更改导致问题,可能会对其进行修订。
为集合和关联代理实现了批量替换
将集合或字典分配给关联代理集合现在应该能正常工作了,而以前会为现有键重新创建关联代理成员,导致由于相同对象的删除+插入而导致潜在刷新失败的问题,现在应该只在适当的情况下创建新的关联对象:
class A(Base):
__tablename__ = "test_a"
id = Column(Integer, primary_key=True)
b_rel = relationship(
"B",
collection_class=set,
cascade="all, delete-orphan",
)
b = association_proxy("b_rel", "value", creator=lambda x: B(value=x))
class B(Base):
__tablename__ = "test_b"
__table_args__ = (UniqueConstraint("a_id", "value"),)
id = Column(Integer, primary_key=True)
a_id = Column(Integer, ForeignKey("test_a.id"), nullable=False)
value = Column(String)
# ...
s = Session(e)
a = A(b={"x", "y", "z"})
s.add(a)
s.commit()
# re-assign where one B should be deleted, one B added, two
# B's maintained
a.b = {"x", "z", "q"}
# only 'q' was added, so only one new B object. previously
# all three would have been re-created leading to flush conflicts
# against the deleted ones.
assert len(s.new) == 1
#2642 ### 多对一反向引用在删除操作期间检查集合重复项
当作为 Python 序列存在的 ORM 映射集合,通常是 Python list
(作为relationship()
的默认值),包含重复项,并且对象从其中一个位置移除但未从其他位置移除时,多对一反向引用会将其属性设置为None
,即使一对多侧仍然表示对象存在。即使一对多集合在关系模型中不能有重复项,但使用序列集合的 ORM 映射的relationship()
在内存中可以有重复项,限制是此重复状态既不能持久化也不能从数据库中检索。特别是,在列表中临时存在重复项是 Python“交换”操作的固有特性。给定标准的一对多/多对一设置:
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"))
如果我们有一个具有两个B
成员的A
对象,并执行交换:
a1 = A(bs=[B(), B()])
a1.bs[0], a1.bs[1] = a1.bs[1], a1.bs[0]
在上述操作期间,拦截标准 Python __setitem__
__delitem__
方法提供了第二个B()
对象在集合中出现两次的临时状态。当B()
对象从一个位置移除时,B.a
反向引用将将引用设置为None
,导致在刷新期间删除A
和B
对象之间的链接。相同的问题也可以使用普通重复项来演示:
>>> a1 = A()
>>> b1 = B()
>>> a1.bs.append(b1)
>>> a1.bs.append(b1) # append the same b1 object twice
>>> del a1.bs[1]
>>> a1.bs # collection is unaffected so far...
[<__main__.B object at 0x7f047af5fb70>]
>>> b1.a # however b1.a is None
>>>
>>> session.add(a1)
>>> session.commit() # so upon flush + expire....
>>> a1.bs # the value is gone
[]
此修复确保在触发反向引用之前(在集合被改变之前),会检查集合中是否恰好有一个或零个目标项的实例,然后在取消多对一侧时使用线性搜索,目前使用list.search
和list.__contains__
。
最初认为需要在集合内部使用基于事件的引用计数方案,以便在整个集合的生命周期中跟踪所有重复的实例,这将对所有集合操作产生性能/内存/复杂性影响,包括非常频繁的加载和追加操作。相反采取的方法将额外的开销限制在集合移除和批量替换这些不太常见的操作上,并且线性扫描的观察开销是可以忽略的;在工作单元内以及在集合进行批量替换时,已经在关系绑定集合中使用了线性扫描。
关键行为变化 - ORM
Query.join()更明确地处理决定“左”侧的模棱两可情况
从历史上看,给定如下查询:
u_alias = aliased(User)
session.query(User, u_alias).join(Address)
鉴于标准教程映射,查询将产生一个 FROM 子句如下:
SELECT ...
FROM users AS users_1, users JOIN addresses ON users.id = addresses.user_id
也就是说,JOIN 将隐式地与第一个匹配的实体进行连接。新的行为是,异常请求解决这种模棱两可的情况:
sqlalchemy.exc.InvalidRequestError: Can't determine which FROM clause to
join from, there are multiple FROMS which can join to this entity.
Try adding an explicit ON clause to help resolve the ambiguity.
解决方案是提供一个 ON 子句,可以是一个表达式:
# join to User
session.query(User, u_alias).join(Address, Address.user_id == User.id)
# join to u_alias
session.query(User, u_alias).join(Address, Address.user_id == u_alias.id)
或者使用关系属性,如果可用的话:
# join to User
session.query(User, u_alias).join(Address, User.addresses)
# join to u_alias
session.query(User, u_alias).join(Address, u_alias.addresses)
更改包括现在 JOIN 可以正确地链接到不是列表中第一个元素的 FROM 子句,如果 JOIN 本身不是模棱两可的话:
session.query(func.current_timestamp(), User).join(Address)
在此增强之前,上述查询将引发:
sqlalchemy.exc.InvalidRequestError: Don't know how to join from
CURRENT_TIMESTAMP; please use select_from() to establish the
left entity/selectable of this join
现在查询正常工作:
SELECT CURRENT_TIMESTAMP AS current_timestamp_1, users.id AS users_id,
users.name AS users_name, users.fullname AS users_fullname,
users.password AS users_password
FROM users JOIN addresses ON users.id = addresses.user_id
总体而言,这种变化直接符合 Python 的“显式优于隐式”的哲学。
#4365 ### FOR UPDATE 子句在联合加载子查询中以及外部呈现
此更改特别适用于使用joinedload()
加载策略与行限制查询结合使用时,例如使用Query.first()
或Query.limit()
,以及使用Query.with_for_update()
方法。
给定一个查询如下:
session.query(A).options(joinedload(A.b)).limit(5)
当联合加载与 LIMIT 结合时,Query
对象呈现以下形式的 SELECT:
SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id
这样做是为了使主实体的行限制不影响相关项目的联合加载。当上述查询与“SELECT..FOR UPDATE”结合时,行为是这样的:
SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id FOR UPDATE
然而,由于 MySQL bugs.mysql.com/bug.php?id=90693
不锁定子查询中的行,不像 PostgreSQL 和其他数据库。因此,上述查询现在呈现为:
SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5 FOR UPDATE
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id FOR UPDATE
在 Oracle 方言中,内部的“FOR UPDATE”不会呈现,因为 Oracle 不支持此语法,方言会跳过针对子查询的任何“FOR UPDATE”;在任何情况下都不是必要的,因为 Oracle 像 PostgreSQL 一样正确锁定返回行的所有元素。
当使用 Query.with_for_update.of
修饰符时,通常在 PostgreSQL 上,外部的“FOR UPDATE”被省略,OF 现在在内部呈现;以前,OF 目标不会被正确转换以适应子查询。所以给定:
session.query(A).options(joinedload(A.b)).with_for_update(of=A).limit(5)
查询现在会呈现为:
SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5 FOR UPDATE OF a
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id
以上形式在 PostgreSQL 上应该有所帮助,此外,由于 PostgreSQL 不允许在 LEFT OUTER JOIN 目标之后呈现 FOR UPDATE 子句。
总的来说,FOR UPDATE 仍然高度特定于正在使用的目标数据库,并且不能轻易地推广到更复杂的查询。
#4246 ### passive_deletes=’all’ 将使 FK 在从集合中移除的对象中保持不变
relationship.passive_deletes
选项接受值 "all"
,表示当对象被刷新时,不应修改任何外键属性,即使关系的集合/引用已被移除。以前,在以下情况下不会发生这种情况:
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
addresses = relationship("Address", passive_deletes="all")
class Address(Base):
__tablename__ = "addresses"
id = Column(Integer, primary_key=True)
email = Column(String)
user_id = Column(Integer, ForeignKey("users.id"))
user = relationship("User")
u1 = session.query(User).first()
address = u1.addresses[0]
u1.addresses.remove(address)
session.commit()
# would fail and be set to None
assert address.user_id == u1.id
修复现在包括 address.user_id
保持不变,根据 passive_deletes="all"
。这种情况对于构建自定义“版本表”方案等非常有用,其中行被归档而不是删除。
#3844 ## 新功能和改进 - 核心
新的多列命名约定标记,长名称截断
为了适应一个MetaData
命名约定需要在多列约束之间消除歧义,并希望在生成的约束名中使用所有列的情况,添加了一系列新的命名约定标记,包括column_0N_name
、column_0_N_name
、column_0N_key
、column_0_N_key
、referred_column_0N_name
、referred_column_0_N_name
等,它们将约束中所有列的列名(或键或标签)连接在一起,要么没有分隔符,要么用下划线分隔符连接。下面我们定义一个约定,将UniqueConstraint
约束命名为所有列名称的组合:
metadata_obj = MetaData(
naming_convention={"uq": "uq_%(table_name)s_%(column_0_N_name)s"}
)
table = Table(
"info",
metadata_obj,
Column("a", Integer),
Column("b", Integer),
Column("c", Integer),
UniqueConstraint("a", "b", "c"),
)
上述表的 CREATE TABLE 将呈现为:
CREATE TABLE info (
a INTEGER,
b INTEGER,
c INTEGER,
CONSTRAINT uq_info_a_b_c UNIQUE (a, b, c)
)
此外,现在将长名称截断逻辑应用于由命名约定生成的名称,特别是为了适应可能产生非常长名称的多列标签。这种逻辑与在 SELECT 语句中截断长标签名称所使用的逻辑相同,它会用一个确定性生成的 4 字符哈希替换超过目标数据库标识符长度限制的多余字符。例如,在 PostgreSQL 中,标识符不能超过 63 个字符,一个长约束名通常会从下面的表定义中生成:
long_names = Table(
"long_names",
metadata_obj,
Column("information_channel_code", Integer, key="a"),
Column("billing_convention_name", Integer, key="b"),
Column("product_identifier", Integer, key="c"),
UniqueConstraint("a", "b", "c"),
)
截断逻辑将确保不会为唯一约束生成过长的名称:
CREATE TABLE long_names (
information_channel_code INTEGER,
billing_convention_name INTEGER,
product_identifier INTEGER,
CONSTRAINT uq_long_names_information_channel_code_billing_conventi_a79e
UNIQUE (information_channel_code, billing_convention_name, product_identifier)
)
上述后缀a79e
基于长名称的 md5 哈希,并且每次都会生成相同的值,以产生给定模式的一致名称。
请注意,当约束名称对于给定方言明确过大时,截断逻辑还会引发IdentifierError
。这已经是很长时间以来Index
对象的行为,但现在也适用于其他类型的约束:
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import Table
from sqlalchemy import UniqueConstraint
from sqlalchemy.dialects import postgresql
from sqlalchemy.schema import AddConstraint
m = MetaData()
t = Table("t", m, Column("x", Integer))
uq = UniqueConstraint(
t.c.x,
name="this_is_too_long_of_a_name_for_any_database_backend_even_postgresql",
)
print(AddConstraint(uq).compile(dialect=postgresql.dialect()))
将输出:
sqlalchemy.exc.IdentifierError: Identifier
'this_is_too_long_of_a_name_for_any_database_backend_even_postgresql'
exceeds maximum length of 63 characters
异常抛出阻止了由数据库后端截断的非确定性约束名称的生成,这些名称后来与数据库迁移不兼容。
要将 SQLAlchemy 端的截断规则应用于上述标识符,请使用conv()
构造:
uq = UniqueConstraint(
t.c.x,
name=conv("this_is_too_long_of_a_name_for_any_database_backend_even_postgresql"),
)
这将再次输出确定性截断的 SQL,如下所示:
ALTER TABLE t ADD CONSTRAINT this_is_too_long_of_a_name_for_any_database_backend_eve_ac05 UNIQUE (x)
目前还没有选项使名称通过以允许数据库端截断。这在Index
名称上已经有一段时间了,也没有引起问题。
此更改还修复了另外两个问题。 其中一个是column_0_key
令牌尽管已记录文档,但却不可用,另一个是referred_column_0_name
令牌如果这两个值不同,则会无意中渲染.key
而不是列的.name
。
另请参阅
配置约束命名约定
MetaData.naming_convention
#3989 ### SQL 函数的二进制比较解释
此增强功能是在核心级别实现的,但主要适用于 ORM。
现在,可以将比较两个元素的 SQL 函数用作“比较”对象,适用于 ORM relationship()
的使用,首先像往常一样使用 func
工厂创建函数,然后当函数完成时调用 FunctionElement.as_comparison()
修改器以生成具有“左”和“右”侧的 BinaryExpression
:
class Venue(Base):
__tablename__ = "venue"
id = Column(Integer, primary_key=True)
name = Column(String)
descendants = relationship(
"Venue",
primaryjoin=func.instr(remote(foreign(name)), name + "/").as_comparison(1, 2)
== 1,
viewonly=True,
order_by=name,
)
上述relationship.primaryjoin
中的“descendants”关系将基于传递给instr()
的第一个和第二个参数生成“左”和“右”表达式。 这允许 ORM 懒加载等功能生成类似以下的 SQL:
SELECT venue.id AS venue_id, venue.name AS venue_name
FROM venue
WHERE instr(venue.name, (? || ?)) = ? ORDER BY venue.name
('parent1', '/', 1)
和 joinedload,例如:
v1 = (
s.query(Venue)
.filter_by(name="parent1")
.options(joinedload(Venue.descendants))
.one()
)
使其工作如下:
SELECT venue.id AS venue_id, venue.name AS venue_name,
venue_1.id AS venue_1_id, venue_1.name AS venue_1_name
FROM venue LEFT OUTER JOIN venue AS venue_1
ON instr(venue_1.name, (venue.name || ?)) = ?
WHERE venue.name = ? ORDER BY venue_1.name
('/', 1, 'parent1')
该功能预计将有助于处理诸如在关系连接条件中使用几何函数,或者任何在 SQL 连接的 ON 子句中以 SQL 函数的形式表达的情况等情况。
#3831 ### 扩展 IN 功能现在支持空列表
在版本 1.2 中引入的“expanding IN”功能现在支持传递给ColumnOperators.in_()
运算符的空列表。 对于空列表的实现将生成一个针对目标后端具体的“空集合”表达式,例如对于 PostgreSQL,“SELECT CAST(NULL AS INTEGER) WHERE 1!=1”,对于 MySQL,“SELECT 1 FROM (SELECT 1) as _empty_set WHERE 1!=1”:
>>> from sqlalchemy import create_engine
>>> from sqlalchemy import select, literal_column, bindparam
>>> e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
>>> with e.connect() as conn:
... conn.execute(
... select([literal_column("1")]).where(
... literal_column("1").in_(bindparam("q", expanding=True))
... ),
... q=[],
... )
{exexsql}SELECT 1 WHERE 1 IN (SELECT CAST(NULL AS INTEGER) WHERE 1!=1)
该功能还适用于基于元组的 IN 语句,其中“空 IN”表达式将被扩展以支持元组中给定的元素,例如在 PostgreSQL 上:
>>> from sqlalchemy import create_engine
>>> from sqlalchemy import select, literal_column, tuple_, bindparam
>>> e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
>>> with e.connect() as conn:
... conn.execute(
... select([literal_column("1")]).where(
... tuple_(50, "somestring").in_(bindparam("q", expanding=True))
... ),
... q=[],
... )
{exexsql}SELECT 1 WHERE (%(param_1)s, %(param_2)s)
IN (SELECT CAST(NULL AS INTEGER), CAST(NULL AS VARCHAR) WHERE 1!=1)
#4271 ### TypeEngine 方法 bind_expression, column_expression 适用于 Variant、特定类型
当这些方法存在于特定数据类型的“impl”上时,TypeEngine.bind_expression()
和 TypeEngine.column_expression()
方法现在可以工作,允许方言以及 TypeDecorator
和 Variant
使用这些方法。
以下示例说明了一个将 SQL 时间转换函数应用于 LargeBinary
的 TypeDecorator
。为了使此类型在 Variant
的上下文中工作,编译器需要深入到变体表达式的“impl”中以定位这些方法:
from sqlalchemy import TypeDecorator, LargeBinary, func
class CompressedLargeBinary(TypeDecorator):
impl = LargeBinary
def bind_expression(self, bindvalue):
return func.compress(bindvalue, type_=self)
def column_expression(self, col):
return func.uncompress(col, type_=self)
MyLargeBinary = LargeBinary().with_variant(CompressedLargeBinary(), "sqlite")
上述表达式仅在 SQLite 上使用时会呈现为 SQL 中的函数:
from sqlalchemy import select, column
from sqlalchemy.dialects import sqlite
print(select([column("x", CompressedLargeBinary)]).compile(dialect=sqlite.dialect()))
将呈现为:
SELECT uncompress(x) AS x
此更改还包括方言可以在方言级别的实现类型上实现TypeEngine.bind_expression()
和TypeEngine.column_expression()
,在那里它们现在将被使用;特别是这将用于 MySQL 的新“二进制前缀”要求以及用于将 MySQL 的十进制绑定值转换为强制转换的情况。
#3981 ### 新的后进先出策略适用于 QueuePool
通常由create_engine()
使用的连接池被称为QueuePool
。 这个池使用一个类似于 Python 内置的Queue
类的对象来存储等待使用的数据库连接。 Queue
具有先进先出的行为,旨在提供对池中持久存在的数据库连接的循环使用。 然而,这种方法的一个潜在缺点是,当池的利用率较低时,池中每个连接的串行重复使用意味着试图减少未使用连接的服务器端超时策略被阻止关闭这些连接。 为了适应这种用例,添加了一个新标志create_engine.pool_use_lifo
,它将Queue
的.get()
方法反转,从队列的开头而不是末尾获取连接,从本质上将“队列”变成“栈”(考虑到这太啰嗦,因此没有添加一个名为StackPool
的全新池)。
另请参阅
使用 FIFO vs. LIFO
核心关键变化
完全移除将字符串 SQL 片段强制转换为 text()
首次添加于版本 1.0 的警告,描述在将完整 SQL 片段强制转换为 text() 时发出的警告,现已转换为异常。 对于像Query.filter()
和Select.order_by()
这样的方法传递的字符串片段自动转换为text()
构造的自动转换引发了持续的担忧,尽管这已发出警告。 在Select.order_by()
、Query.order_by()
、Select.group_by()
和Query.group_by()
的情况下,字符串标签或列名仍然会解析为相应的表达式构造,但如果解析失败,则会引发CompileError
,从而防止直接呈现原始 SQL 文本。
#4481 ### “threadlocal”引擎策略已弃用
“线程本地引擎策略”是在 SQLAlchemy 0.2 左右添加的,作为解决在 SQLAlchemy 0.1 中操作的标准方式存在问题的解决方案,可以总结为“一切都是线程本地”。回顾来看,似乎相当荒谬,SQLAlchemy 的首次发布在各方面都是“alpha”版本,已经担心太多用户已经定居在现有 API 上,无法简单地更改它。
SQLAlchemy 的原始使用模型如下:
engine.begin()
table.insert().execute(parameters)
result = table.select().execute()
table.update().execute(parameters)
engine.commit()
经过几个月的实际使用,很明显,假装“连接”或“事务”是一个隐藏的实现细节是一个坏主意,特别是当有人需要同时处理多个数据库连接时。因此,我们今天看到的使用范式被引入,减去了上下文管理器,因为它们在 Python 中尚不存在:
conn = engine.connect()
try:
trans = conn.begin()
conn.execute(table.insert(), parameters)
result = conn.execute(table.select())
conn.execute(table.update(), parameters)
trans.commit()
except:
trans.rollback()
raise
finally:
conn.close()
上述范式是人们所需要的,但由于仍然有点啰嗦(因为没有上下文管理器),旧的工作方式也被保留了下来,并成为了线程本地引擎策略。
今天,与 Core 一起工作要简洁得多,甚至比原始模式更简洁,这要归功于上下文管理器:
with engine.begin() as conn:
conn.execute(table.insert(), parameters)
result = conn.execute(table.select())
conn.execute(table.update(), parameters)
此时,任何仍然依赖“threadlocal”风格的代码都将通过此弃用被鼓励进行现代化改造 - 该功能应在下一个主要系列的 SQLAlchemy 中完全移除,例如 1.4 版。连接池参数Pool.use_threadlocal
也已弃用,因为在大多数情况下实际上没有任何效果,Engine.contextual_connect()
方法也已弃用,该方法通常与Engine.connect()
方法是同义的,除非使用了线程本地引擎。
#4393 ### convert_unicode 参数已弃用
参数String.convert_unicode
和create_engine.convert_unicode
已被弃用。这些参数的目的是指示 SQLAlchemy 确保在 Python 2 中传递给数据库之前将传入的 Python Unicode 对象编码为字节字符串,并期望从数据库接收的字节字符串转换回 Python Unicode 对象。在 Python 3 之前的时代,这是一个巨大的挑战,因为几乎所有的 Python DBAPI 默认情况下都没有启用 Unicode 支持,并且大多数都存在与它们提供的 Unicode 扩展相关的主要问题。最终,SQLAlchemy 添加了 C 扩展,其中一个主要目的是加快结果集中的 Unicode 解码过程。
一旦引入了 Python 3,DBAPI 开始更全面地支持 Unicode,并且更重要的是,默认情况下支持 Unicode。然而,特定 DBAPI 在何种条件下返回 Unicode 数据以及接受 Python Unicode 值作为参数的条件仍然非常复杂。这标志着“convert_unicode”标志开始过时,因为它们不再足以确保编码/解码仅在需要时发生,而不是在不需要时发生。相反,“convert_unicode”开始被方言自动检测。这可以在引擎第一次连接时发出的“SELECT ‘test plain returns’”和“SELECT ‘test_unicode_returns’”SQL 中看到;方言正在测试当前 DBAPI 及其当前设置和后端数据库连接是否默认返回 Unicode。
最终结果是,终端用户在任何情况下都不再需要使用“convert_unicode”标志,如果需要,SQLAlchemy 项目需要知道这些情况及原因。目前,在所有主要数据库上,数百个 Unicode 往返测试通过,而不使用此标志,因此可以相当有信心地说它们不再需要,除非在争议的非使用情况下,例如访问来自传统数据库的错误编码数据,这种情况最好使用自定义类型。
方言改进和更改 - PostgreSQL
为 PostgreSQL 分区表添加基本反射支持
SQLAlchemy 可以使用版本 1.2.6 中添加的postgresql_partition_by
标志,在 PostgreSQL 的 CREATE TABLE 语句中呈现“PARTITION BY”序列。然而,'p'
类型直到现在都不是反射查询的一部分。
给定一个类似于以下的模式:
dv = Table(
"data_values",
metadata_obj,
Column("modulus", Integer, nullable=False),
Column("data", String(30)),
postgresql_partition_by="range(modulus)",
)
sa.event.listen(
dv,
"after_create",
sa.DDL(
"CREATE TABLE data_values_4_10 PARTITION OF data_values "
"FOR VALUES FROM (4) TO (10)"
),
)
两个表名 'data_values'
和 'data_values_4_10'
将从 Inspector.get_table_names()
中返回,此外,列也将从 Inspector.get_columns('data_values')
以及 Inspector.get_columns('data_values_4_10')
中返回。这也适用于对这些表使用 Table(..., autoload=True)
。
方言改进和变更 - MySQL
协议级别的 ping 现在用于预 ping
包括 mysqlclient、python-mysql、PyMySQL 和 mysql-connector-python 在内的 MySQL 方言现在使用 connection.ping()
方法进行池预 ping 功能,详情请参阅 Disconnect Handling - Pessimistic。这比以前在连接上发出 “SELECT 1” 的方法更轻量级。### 控制 ON DUPLICATE KEY UPDATE 中参数的排序
可以通过传递一个 2 元组列表来显式地为 ON DUPLICATE KEY UPDATE
子句中的 UPDATE 参数进行排序:
from sqlalchemy.dialects.mysql import insert
insert_stmt = insert(my_table).values(id="some_existing_id", data="inserted value")
on_duplicate_key_stmt = insert_stmt.on_duplicate_key_update(
[
("data", "some data"),
("updated_at", func.current_timestamp()),
],
)
另请参阅
INSERT…ON DUPLICATE KEY UPDATE(Upsert)
方言改进和变更 - SQLite
添加对 SQLite JSON 的支持
添加了新的数据类型 JSON
,它代表了 JSON
基本数据类型的 SQLite 的 json 成员访问函数。实现使用 SQLite 的 JSON_EXTRACT
和 JSON_QUOTE
函数提供基本的 JSON 支持。
请注意,数据库中呈现的数据类型本身的名称为“JSON”。这将创建一个具有“numeric”亲和性的 SQLite 数据类型,通常情况下不应该成为问题,除非是由单个整数值组成的 JSON 值的情况。尽管如此,根据 SQLite 自己文档中的示例,JSON 的名称仍然被用于其熟悉性。参见 www.sqlite.org/json1.html
#3850 ### 添加对约束中 SQLite ON CONFLICT 的支持
SQLite 支持非标准的 ON CONFLICT 子句,可为独立约束以及一些列内约束(如 NOT NULL)指定。通过向 UniqueConstraint
等对象添加 sqlite_on_conflict
关键字,已对这些子句进行了支持,以及几种 Column
-特定变体:
some_table = Table(
"some_table",
metadata_obj,
Column("id", Integer, primary_key=True, sqlite_on_conflict_primary_key="FAIL"),
Column("data", Integer),
UniqueConstraint("id", "data", sqlite_on_conflict="IGNORE"),
)
上表将在 CREATE TABLE 语句中呈现为:
CREATE TABLE some_table (
id INTEGER NOT NULL,
data INTEGER,
PRIMARY KEY (id) ON CONFLICT FAIL,
UNIQUE (id, data) ON CONFLICT IGNORE
)
另请参阅
对约束的 ON CONFLICT 支持
方言改进和变化 - Oracle
国家字符数据类型弱化以支持通用 Unicode,可通过选项重新启用
现在,默认情况下,Unicode
和UnicodeText
数据类型现在对应于 Oracle 上的VARCHAR2
和CLOB
数据类型,而不是NVARCHAR2
和NCLOB
(也称为“国家”字符集类型)。这将在CREATE TABLE
语句中的呈现行为中看到,以及当使用Unicode
或UnicodeText
的绑定参数时,不会传递类型对象给setinputsizes()
;cx_Oracle 会原生处理字符串值。这种变化基于 cx_Oracle 维护者的建议,即 Oracle 中的“国家”数据类型在很大程度上已经过时且性能不佳。它们还会在某些情况下干扰,比如应用于trunc()
等函数的格式说明符时。
当数据库不使用符合 Unicode 标准的字符集时,可能需要使用NVARCHAR2
和相关类型的情况。在这种情况下,可以通过向create_engine()
传递标志use_nchar_for_unicode
来重新启用旧行为。
始终明确使用NVARCHAR2
和NCLOB
数据类型将继续使用NVARCHAR2
和NCLOB
,包括在 DDL 中以及在处理绑定参数时使用 cx_Oracle 的setinputsizes()
。
在读取方面,在 Python 2 下已添加了对 CHAR/VARCHAR/CLOB 结果行的自动 Unicode 转换,以匹配 Python 3 下 cx_Oracle 的行为。为了减轻 cx_Oracle 方言在 Python 2 下先前具有的性能问题,SQLAlchemy 在 Python 2 下使用非常高效(当构建 C 扩展时)的本机 Unicode 处理程序。可以通过将coerce_to_unicode
标志设置为 False 来禁用自动 Unicode 强制转换。此标志现在默认为 True,并适用于所有在结果集中返回的不明确为Unicode
或 Oracle 的 NVARCHAR2/NCHAR/NCLOB 数据类型的字符串数据。
#4242 ### cx_Oracle 连接参数现代化,已弃用的参数已移除
�� cx_oracle 方言接受的参数以及 URL 字符串进行了一系列现代化处理:
-
弃用的参数
auto_setinputsizes
、allow_twophase
、exclude_setinputsizes
已被移除。 -
threaded
参数的值,在 SQLAlchemy 方言中一直默认为 True,现在不再默认生成。SQLAlchemyConnection
对象本身不被视为线程安全,因此不需要传递此标志。 -
将
threaded
传递给create_engine()
本身已被弃用。要将threaded
的值设置为True
,请将其传递给create_engine.connect_args
字典或使用查询字符串,例如oracle+cx_oracle://...?threaded=true
。 -
所有传递到 URL 查询字符串的参数,除非另有特殊处理,现在都传递给 cx_Oracle.connect()函数。其中一些也被强制转换为 cx_Oracle 常量或布尔值,包括
mode
、purity
、events
和threaded
。 -
与以往一样,所有 cx_Oracle
.connect()
参数都通过create_engine.connect_args
字典接受,文档对此描述不准确。
方言改进和变化 - SQL Server
支持 pyodbc fast_executemany
Pyodbc 最近添加的“fast_executemany”模式,在使用 Microsoft ODBC 驱动程序时可用,现在是 pyodbc / mssql 方言的选项。通过create_engine()
传递:
engine = create_engine(
"mssql+pyodbc://scott:tiger@mssql2017:1433/test?driver=ODBC+Driver+13+for+SQL+Server",
fast_executemany=True,
)
另请参阅
快速 Executemany 模式
#4158 ### 新参数影响 IDENTITY 的起始和增量,使用 Sequence 已被弃用
SQL Server 自 SQL Server 2012 起现在支持具有真实 CREATE SEQUENCE
语法的序列。在 #4235 中,SQLAlchemy 将添加对这些序列的支持,使用 Sequence
,方式与其他任何方言一样。然而,目前的情况是 Sequence
已经在 SQL Server 上重新用途,以影响主键列的 IDENTITY
规范的 “start” 和 “increment” 参数。为了向普通序列也可用过渡,使用 Sequence
将在 1.3 系列中的整个过渡期间发出弃用警告。为了影响 “start” 和 “increment”,请在 Column
上使用新的 mssql_identity_start
和 mssql_identity_increment
参数:
test = Table(
"test",
metadata_obj,
Column(
"id",
Integer,
primary_key=True,
mssql_identity_start=100,
mssql_identity_increment=10,
),
Column("name", String(20)),
)
要在非主键列上发出 IDENTITY
,这是一个很少使用但有效的 SQL Server 情况,请使用 Column.autoincrement
标志,在目标列上将其设置为 True
,在任何整数主键列上设置为 False
:
test = Table(
"test",
metadata_obj,
Column("id", Integer, primary_key=True, autoincrement=False),
Column("number", Integer, autoincrement=True),
)
另请参阅
自动增量行为 / IDENTITY 列
#4235 ## 更改的 StatementError 格式(换行符和 %s)
对于 StatementError
的字符串表示引入了两个更改。字符串表示的“详细信息”和“SQL”部分现在由换行符分隔,并且保留了原始 SQL 语句中存在的换行符。目标是在保持原始错误消息单行记录的同时提高可读性。
这意味着以前看起来像这样的错误消息:
sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError) A value is
required for bind parameter 'id' [SQL: 'select * from reviews\nwhere id = ?']
(Background on this error at: https://sqlalche.me/e/cd3x)
现在将如下所示:
sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError) A value is required for bind parameter 'id'
[SQL: select * from reviews
where id = ?]
(Background on this error at: https://sqlalche.me/e/cd3x)
此更改的主要影响是消费者不再能假定完整的异常消息在单行上,但是来自 DBAPI 驱动程序或 SQLAlchemy 内部生成的原始 “错误” 部分仍将位于第一行。
介绍
本指南介绍了 SQLAlchemy 版本 1.3 中的新功能,并记录了对将应用程序从 SQLAlchemy 1.2 系列迁移到 1.3 系列的用户产生影响的更改。
请仔细查看行为更改部分,了解可能的向后不兼容的行为更改。
一般
为所有弃用元素发出弃用警告;添加新的弃用
发行版 1.3 确保所有被弃用的行为和 API,包括所有长期被列为“遗留”的行为和 API,都会发出 DeprecationWarning
警告。这包括在使用诸如 Session.weak_identity_map
这样的参数和 MapperExtension
这样的类时。虽然所有弃用已在文档中注明,但通常它们没有使用正确的重新构造文本指令,或者包含它们被弃用的版本。一个特定的 API 功能是否实际发出弃用警告并不一致。一般的态度是,大多数或所有这些弃用功能都被视为长期遗留功能,没有计划删除它们。
此次变更包括所有文档化的弃用现在都在文档中使用了正确的重新构造文本指令,并附有版本号,明确指出该功能或用例将在将来的版本中删除(例如,不再有永久使用的旧用例),以及使用任何此类功能或用例都将明确发出 DeprecationWarning
警告,在 Python 3 中以及使用像 Pytest 这样的现代测试工具时,现在在标准错误流中更加明确。目标是,这些长时间弃用的功能,回溯到版本 0.7 或 0.6,应该开始被完全删除,而不是保留它们作为“遗留”功能。此外,从版本 1.3 开始,一些重大的新弃用将被添加。由于 SQLAlchemy 在数千开发人员的实际使用中已有 14 年,因此可以指出一种混合得很好的用例流,并且修剪掉与这种单一工作方式相违背的功能和模式。
更大的背景是 SQLAlchemy 试图适应即将到来的仅支持 Python 3 的世界,以及类型注解的世界,为此,初步计划对 SQLAlchemy 进行重大改版,希望大大减少 API 的认知负担,以及对 Core 和 ORM 之间众多实现和使用差异的重大调整。由于这两个系统在 SQLAlchemy 首次发布后发生了巨大变化,特别是 ORM 仍保留了许多“后期添加的”行为,这使得 Core 和 ORM 之间的隔离墙过高。通过提前将 API 集中在每个支持的用例的单一模式上,将来对显着改变的 API 进行迁移的工作变得更加简单。
对于在 1.3 中添加的最重大的弃用,请参见下面的链接部分。
另请参阅
“threadlocal” 引擎策略已弃用
convert_unicode 参数已弃用
关于 AliasedClass 的关系取代了非主映射器的需要
#4393 ### 所有弃用元素都会发出弃用警告;新增弃用
发布 1.3 确保所有被弃用的行为和 API,包括那些多年来一直被列为“遗留”的行为,都会发出DeprecationWarning
警告。这包括当使用参数如Session.weak_identity_map
和类似MapperExtension
时。虽然所有弃用都已在文档中记录,但通常它们没有使用适当的重构文本指令,或者包含它们被弃用的版本。特定 API 功能是否实际发出弃用警告并不一致。一般的态度是,大多数或所有这些弃用功能都被视为长期遗留功能,没有计划删除它们。
此更改包括,所有记录的弃用现在在文档中使用适当的重构文本指令,并附有版本号,明确说明该功能或用例将在将来的版本中被移除(例如,不再有永久的遗留用例),并且使用任何此类功能或用例肯定会发出DeprecationWarning
,在 Python 3 中以及使用现代测试工具如 Pytest 时,现在在标准错误流中更加明确。目标是,这些长期弃用的功能,可以追溯到版本 0.7 或 0.6,应该开始被完全移除,而不是将它们保留为“遗留”功能。此外,一些重大的新弃用功能正在版本 1.3 中添加。由于 SQLAlchemy 在数千开发人员的实际使用中已经有了 14 年的历史,可以指出一个混合在一起的使用案例流,以及修剪掉与这种单一工作方式相悖的功能和模式。
SQLAlchemy 的更大背景是,它试图适应即将到来的 Python 3-only 世界,以及一个类型注释的世界,为了实现这个目标,有暂定计划对 SQLAlchemy 进行重大改造,希望能大大减少 API 的认知负荷,并对 Core 和 ORM 之间的实现和使用之间的许多差异进行重大调整。由于这两个系统在 SQLAlchemy 首次发布后发生了巨大变化,特别是 ORM 仍然保留了许多“外挂”行为,使得 Core 和 ORM 之间的隔离墙过高。通过提前将 API 集中在每个支持的用例的单一模式上,将来迁移到显著改变的 API 的工作变得更简单。
对于 1.3 中添加的最重要的弃用功能,请参见下面的链接部分。
另请参阅
“threadlocal” engine strategy deprecated
convert_unicode parameters deprecated
与别名类的关系替代了非主要映射器的需求
新功能和改进 - ORM
与别名类的关系替代了非主要映射器的需求
“非主要映射器”是以命令式映射风格创建的Mapper
,它充当已经映射的类的额外映射器,针对不同类型的可选择对象。非主要映射器起源于 SQLAlchemy 的 0.1、0.2 系列,那时预计Mapper
对象将是主要的查询构建接口,之后才有Query
对象的存在。
随着Query
的出现,以及后来的AliasedClass
构造,大多数非主要映射器的用例都消失了。这是一件好事,因为 SQLAlchemy 在 0.5 系列左右也完全放弃了“经典”映射,转而采用了声明式系统。
当意识到一些非常难以定义的relationship()
配置可能会成为可能时,仍然保留了非主要映射器的一个用例。当使用非主要映射器作为映射目标时,可以使用替代可选择项,而不是尝试构建一个relationship.primaryjoin
,该关系涵盖了特定对象间关系的所有复杂性。
随着这个用例变得越来越流行,它的局限性也变得明显,包括非主要映射器难以配置以适应添加新列的可选择项,映射器不继承原始映射的关系,明确配置在非主要映射器上的关系与加载器选项不兼容,非主要映射器还不能提供可在查询中使用的基于列的属性的完全功能命名空间(在旧的 0.1 - 0.4 时代,人们会直接使用Table
对象与 ORM)。
缺失的部分是允许 relationship()
直接引用 AliasedClass
。AliasedClass
已经做了非主映射器所要做的一切;它允许从备用可选择中加载现有映射的类,它继承现有映射器的所有属性和关系,它与加载器选项非常配合,还提供了一个可以像类本身一样混入查询的类似对象。通过这个改变,原来针对非主映射器的配置关系连接的配方被更改为别名类。
在关系到别名类处,原始的非主映射器如下所示:
j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)
B_viacd = mapper(
B,
j,
non_primary=True,
primary_key=[j.c.b_id],
properties={
"id": j.c.b_id, # so that 'id' looks the same as before
"c_id": j.c.c_id, # needed for disambiguation
"d_c_id": j.c.d_c_id, # needed for disambiguation
"b_id": [j.c.b_id, j.c.d_b_id],
"d_id": j.c.d_id,
},
)
A.b = relationship(B_viacd, primaryjoin=A.b_id == B_viacd.c.b_id)
这些属性是必需的,以便重新映射额外的列,使其不与已映射到B
的现有列发生冲突,同时还需要定义一个新的主键。
使用新方法,所有这些冗长都会消失,并且在建立关系时可以直接引用额外的列:
j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)
B_viacd = aliased(B, j, flat=True)
A.b = relationship(B_viacd, primaryjoin=A.b_id == j.c.b_id)
非主映射器现已不推荐使用,最终目标是经典映射作为一种特性完全消失。声明性 API 将成为唯一的映射方式,这有望带来内部改进和简化,以及更清晰的文档说明。
#4423 ### selectin 加载不再使用 JOIN 来进行简单的一对多加载
1.2 版中添加的“selectin”加载功能引入了一种极其高效的新方法来急切加载集合,在许多情况下比“subquery”急切加载要快得多,因为它不依赖于重复原始 SELECT 查询,而是使用一个简单的 IN 子句。然而,“selectin”加载仍然依赖于在父表和相关表之间渲染 JOIN,因为它需要在行中使用父主键值来匹配行。在 1.3 中,添加了一个新的优化,将在简单的一对多加载的最常见情况下省略这个 JOIN,其中相关的行已经包含了其外键列中表达的父行的主键。这再次提供了显着的性能改进,因为 ORM 现在可以一次性加载大量集合,而完全不使用 JOIN 或子查询。
给定一个映射:
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
bs = relationship("B", lazy="selectin")
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
在 1.2 版本的“selectin”加载中,从 A 到 B 的加载如下所示:
SELECT a.id AS a_id FROM a
SELECT a_1.id AS a_1_id, b.id AS b_id, b.a_id AS b_a_id
FROM a AS a_1 JOIN b ON a_1.id = b.a_id
WHERE a_1.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY a_1.id
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
使用新行为,加载如下:
SELECT a.id AS a_id FROM a
SELECT b.a_id AS b_a_id, b.id AS b_id FROM b
WHERE b.a_id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY b.a_id
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
该行为被自动释放,使用类似于延迟加载的启发式方法,以确定相关实体是否可以直接从标识映射中获取。然而,与大多数查询功能一样,由于关于多态加载的高级场景,该功能的实现变得更加复杂。如果遇到问题,用户应该报告错误,但是该更改还包括一个标志relationship.omit_join
,可以在relationship()
上设置为False
以禁用优化。
#4340 ### 改进多对一查询表达式的行为
当构建一个查询,将一个多对一的关系与一个对象值进行比较时,比如:
u1 = session.query(User).get(5)
query = session.query(Address).filter(Address.user == u1)
上述表达式Address.user == u1
最终编译成一个 SQL 表达式,通常基于User
对象的主键列,如"address.user_id = 5"
,使用延迟可调用来在绑定表达式中尽可能晚地检索值5
。这是为了适应两种情况:Address.user == u1
表达式可能针对尚未刷新的User
对象,依赖于服务器生成的主键值,以及即使自表达式创建以来u1
的主键值已更改,表达式始终返回正确结果的情况。
然而,这种行为的一个副作用是,如果u1
在表达式被评估时已经过期,就会导致额外的 SELECT 语句,而且如果u1
也已经从Session
中分离,就会引发错误:
u1 = session.query(User).get(5)
query = session.query(Address).filter(Address.user == u1)
session.expire(u1)
session.expunge(u1)
query.all() # <-- would raise DetachedInstanceError
当Session
提交并且u1
实例超出范围时,对象的过期/清除可能会隐式发生,因为Address.user == u1
表达式并不强烈引用对象本身,只引用其InstanceState
。
修复方法是允许 Address.user == u1
表达式根据尝试在表达式编译时正常检索或加载值的结果来评估值 5
,就像现在一样,但如果对象已分离并已过期,则从一个新的机制中检索它 InstanceState
,该机制将在属性过期时对该状态上的特定属性的最后已知值进行备忘录。此机制仅在表达式功能需要时为特定属性/ InstanceState
启用,以节省性能/内存开销。
最初,尝试了诸如立即评估表达式并尝试稍后加载值的各种安排等更简单的方法,但困难的边缘情况是正在更改的列属性(通常是自然主键)的值。为了确保诸如 Address.user == u1
的表达式始终返回 u1
当前状态的正确答案,它将返回持久对象的当前数据库持久值,如果需要通过 SELECT 查询取消过期,并且对于已分离的对象,它将返回最近已知的值,而不管对象何时使用 InstanceState
来跟踪列属性的最后已知值,无论何时属性将被过期。
当无法评估值时,现代属性 API 功能用于指示特定的错误消息,这两种情况是当列属性从未设置时,以及当第一次进行评估时对象已过期时。在所有情况下,不再引发 DetachedInstanceError
。
#4359 ### 多对一替换不会对“raiseload”或“old”对象引发异常
考虑到延迟加载将继续在多对一关系上进行,以加载“old”值,如果关系未指定 relationship.active_history
标志,则不会对分离的对象引发断言:
a1 = session.query(Address).filter_by(id=5).one()
session.expunge(a1)
a1.user = some_user
上面,在分离的 a1
对象上替换 .user
属性时,如果属性试图从标识映射中检索 .user
的先前值,则会引发 DetachedInstanceError
。变化在于该操作现在在不加载旧值的情况下继续进行。
同样的改变也适用于lazy="raise"
加载器策略:
class Address(Base):
# ...
user = relationship("User", ..., lazy="raise")
以前,对a1.user
的关联将引发“raiseload”异常,因为属性试图检索先前的值。在加载“旧”值的情况下,现在会跳过此断言。
#4353 ### 为 ORM 属性实现了“del”
Python del
操作实际上对于映射属性(标量列或对象引用)并不可用。已添加支持,使其可以正常工作,其中del
操作大致等同于将属性设置为None
值:
some_object = session.query(SomeObject).get(5)
del some_object.some_attribute # from a SQL perspective, works like "= None"
#4354 ### info 字典添加到 InstanceState
将.info
字典添加到InstanceState
类,这个对象是调用一个映射对象上的inspect()
而来。这允许自定义配方添加有关对象的其他信息,这些信息将随着对象在内存中的完整生命周期一起传递:
from sqlalchemy import inspect
u1 = User(id=7, name="ed")
inspect(u1).info["user_info"] = "7|ed"
#4257 ### 水平分片扩展支持批量更新和删除方法
ShardedQuery
扩展对象支持Query.update()
和Query.delete()
批量更新/删除方法。在调用它们时,将咨询query_chooser
可调用对象,以便根据给定的条件跨多个分片运行更新/删除。
关联代理改进
尽管没有任何特定的原因,但本周期的 Association Proxy 扩展有许多改进。
关联代理有新的 cascade_scalar_deletes 标志
给定一个映射为:
class A(Base):
__tablename__ = "test_a"
id = Column(Integer, primary_key=True)
ab = relationship("AB", backref="a", uselist=False)
b = association_proxy(
"ab", "b", creator=lambda b: AB(b=b), cascade_scalar_deletes=True
)
class B(Base):
__tablename__ = "test_b"
id = Column(Integer, primary_key=True)
ab = relationship("AB", backref="b", cascade="all, delete-orphan")
class AB(Base):
__tablename__ = "test_ab"
a_id = Column(Integer, ForeignKey(A.id), primary_key=True)
b_id = Column(Integer, ForeignKey(B.id), primary_key=True)
对A.b
的赋值将生成一个AB
对象:
a.b = B()
A.b
关联是标量的,并包含一个新标志AssociationProxy.cascade_scalar_deletes
。设置时,将A.b
设置为None
将同时删除A.ab
。默认行为仍然是保留a.ab
不变:
a.b = None
assert a.ab is None
虽然乍一看这个逻辑应该只看现有关系的“级联”属性,但仅从那个属性本身来看不清楚应该删除代理对象,因此行为被作为一个显式选项提供。
此外,del
现在对标量的工作方式类似于设置为None
:
del a.b
assert a.ab is None
#4308 #### 关联代理在每个类上都存储特定于类的状态
AssociationProxy
对象根据其关联的父映射类做出许多决策。虽然 AssociationProxy
在历史上起初是一个相对简单的‘getter’,但很快就明显需要它也需要对其引用的属性类型做出决策——比如标量或集合、映射对象或简单值等等。为了实现这一点,它需要检查映射属性或其他引用的描述符或属性,如从其父类引用的那样。然而,在 Python 的描述符机制中,描述符仅在其在该类的上下文中被访问时才了解其“父”类,例如调用 MyClass.some_descriptor
,这会调用 __get__()
方法并传入类。AssociationProxy
对象因此会存储特定于该类的状态,但只有在调用此方法后才会这样做;尝试在首先将 AssociationProxy
作为描述符之前预先检查此状态将会引发错误。此外,它会假设由 __get__()
首先看到的第一个类是它需要了解的唯一父类。尽管事实上,如果特定类具有继承的子类,那么关联代理实际上是代表不止一个父类工作的,尽管没有明确地重新使用它。尽管即使存在这样的缺点,关联代理仍然会通过其当前行为取得相当大的进展,但在某些情况下仍存在缺陷以及确定最佳“所有者”类的复杂问题。
这些问题现在已经在AssociationProxy
中得到解决,当调用__get__()
时不再修改其自身的内部状态;相反,每个已知类别都会生成一个名为AssociationProxyInstance
的新对象,该对象处理特定映射父类的所有状态(当父类未映射时,不会生成AssociationProxyInstance
)。 单个“拥有类”的概念用于关联代理,尽管在 1.1 中有所改进,但基本上已被替换为现在的方法,其中 AP 现在可以平等地处理任意数量的“拥有”类。
为了适应希望检查此状态的应用程序而不必调用__get__()
的情况,添加了一个新方法AssociationProxy.for_class()
,它提供了直接访问特定类别AssociationProxyInstance
的功能,如下所示:
class User(Base):
# ...
keywords = association_proxy("kws", "keyword")
proxy_state = inspect(User).all_orm_descriptors["keywords"].for_class(User)
一旦我们有了AssociationProxyInstance
对象,在上面的示例中存储在proxy_state
变量中,我们可以查看特定于User.keywords
代理的属性,例如target_class
:
>>> proxy_state.target_class
Keyword
#3423 #### AssociationProxy 现在为基于列的目标提供了标准列操作符
鉴于AssociationProxy
的目标是数据库列,并且不是对象引用或另一个关联代理的情况:
class User(Base):
# ...
elements = relationship("Element")
# column-based association proxy
values = association_proxy("elements", "value")
class Element(Base):
# ...
value = Column(String)
User.values
关联代理指的是Element.value
列。现在可用标准列操作,例如like
:
>>> print(s.query(User).filter(User.values.like("%foo%")))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND element.value LIKE :value_1)
equals
:
>>> print(s.query(User).filter(User.values == "foo"))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND element.value = :value_1)
在与None
比较时,IS NULL
表达式增加了一个测试,即相关行根本不存在;这与以前的行为相同:
>>> print(s.query(User).filter(User.values == None))
SELECT "user".id AS user_id
FROM "user"
WHERE (EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND element.value IS NULL)) OR NOT (EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id))
注意ColumnOperators.contains()
操作符实际上是一个字符串比较操作符;这是一个行为上的改变,之前,关联代理只将.contains
作为列表包含操作符使用。现在,通过列比较,它现在的行为类似于“like”:
>>> print(s.query(User).filter(User.values.contains("foo")))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND (element.value LIKE '%' || :value_1 || '%'))
为了测试User.values
集合是否包含值"foo"
,应该使用等于操作符(例如User.values == 'foo'
);这在之前的版本中也是有效的。
当使用基于对象的关联代理与集合时,行为与以前相同,即测试集合成员资格,例如给定一个映射:
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
user_elements = relationship("UserElement")
# object-based association proxy
elements = association_proxy("user_elements", "element")
class UserElement(Base):
__tablename__ = "user_element"
id = Column(Integer, primary_key=True)
user_id = Column(ForeignKey("user.id"))
element_id = Column(ForeignKey("element.id"))
element = relationship("Element")
class Element(Base):
__tablename__ = "element"
id = Column(Integer, primary_key=True)
value = Column(String)
.contains()
方法产生与以前相同的表达式,测试User.elements
列表中是否存在Element
对象:
>>> print(s.query(User).filter(User.elements.contains(Element(id=1))))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM user_element
WHERE "user".id = user_element.user_id AND :param_1 = user_element.element_id)
总的来说,这个改变是基于 AssociationProxy stores class-specific state on a per-class basis 的架构变化而启用的;因为代理现在在生成表达式时会衍生出额外的状态,所以AssociationProxyInstance
类现在有对象目标和列目标版本。
关联代理现在强引用父对象
长期以来,关联代理集合仅保持对父对象的弱引用的行为被恢复;代理现在将保持对父对象的强引用,只要代理集合本身也在内存中,消除了“过时的关联代理”错误。这个改变是基于实验性基础进行的,以查看是否会出现任何导致副作用的用例。
举例来说,给定一个带有关联代理的映射:
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
bs = relationship("B")
b_data = association_proxy("bs", "data")
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
data = Column(String)
a1 = A(bs=[B(data="b1"), B(data="b2")])
b_data = a1.b_data
之前,如果a1
超出范围被删除:
del a1
在a1
超出范围被删除后尝试迭代b_data
集合会引发错误“过时的关联代理,父对象已经超出范围”。这是因为关联代理需要访问实际的a1.bs
集合以产生视图,在这个改变之前,它只保持对a1
的弱引用。特别是,用户在执行内联操作时经常会遇到这个错误,比如:
collection = session.query(A).filter_by(id=1).first().b_data
上面的例子中,因为A
对象在实际使用b_data
集合之前已被垃圾回收。
改变是b_data
集合现在保持对a1
对象的强引用,以便它保持存在:
assert b_data == ["b1", "b2"]
此更改引入的副作用是,如果一个应用程序像上面一样传递集合,父对象在集合被丢弃之前不会被垃圾回收。正如以往一样,如果 a1
在特定的 Session
内是持久化的,它将保持为该会话的状态直到被垃圾回收。
请注意,如果这种变化导致问题,可能会对此进行修订。
为集合,具有 AssociationProxy 的字典实现批量替换
现在,将集合代理分配给集合代理集合的集合应该正常工作,而以前会为现有键重新创建集合代理成员,导致潜在的刷新失败问题,因为删除+插入相同对象,现在应该只在适当的情况下创建新的关联对象:
class A(Base):
__tablename__ = "test_a"
id = Column(Integer, primary_key=True)
b_rel = relationship(
"B",
collection_class=set,
cascade="all, delete-orphan",
)
b = association_proxy("b_rel", "value", creator=lambda x: B(value=x))
class B(Base):
__tablename__ = "test_b"
__table_args__ = (UniqueConstraint("a_id", "value"),)
id = Column(Integer, primary_key=True)
a_id = Column(Integer, ForeignKey("test_a.id"), nullable=False)
value = Column(String)
# ...
s = Session(e)
a = A(b={"x", "y", "z"})
s.add(a)
s.commit()
# re-assign where one B should be deleted, one B added, two
# B's maintained
a.b = {"x", "z", "q"}
# only 'q' was added, so only one new B object. previously
# all three would have been re-created leading to flush conflicts
# against the deleted ones.
assert len(s.new) == 1
#2642 ### 多对一反向引用在删除操作期间检查集合重复项
当一个 ORM 映射的集合存在作为 Python 序列时,通常是 Python list
,作为 relationship()
的默认值,包含重复项,并且对象从其中一个位置被移除但其他位置没有移除时,一个多对一的反向引用会将其属性设置为 None
,即使一对多的一侧仍然表示对象存在。即使一对多的集合在关系模型中不能有重复项,在内存中使用序列集合的 ORM 映射的 relationship()
可以包含其中的重复项,但限制是这种重复状态既不能持久化也不能从数据库中检索。特别是,在列表中临时存在重复项是 Python “swap” 操作的固有特性。考虑到标准的一对多/多对一设置:
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"))
如果我们有一个带有两个 B
成员的 A
对象,并执行交换:
a1 = A(bs=[B(), B()])
a1.bs[0], a1.bs[1] = a1.bs[1], a1.bs[0]
在上述操作期间,拦截标准的 Python __setitem__
__delitem__
方法提供了一个临时状态,其中第二个 B()
对象在集合中出现两次。当从一个位置移除 B()
对象时,B.a
反向引用将引用设置为 None
,导致刷新期间删除 A
和 B
对象之间的链接。同样的问题也可以使用普通重复项演示:
>>> a1 = A()
>>> b1 = B()
>>> a1.bs.append(b1)
>>> a1.bs.append(b1) # append the same b1 object twice
>>> del a1.bs[1]
>>> a1.bs # collection is unaffected so far...
[<__main__.B object at 0x7f047af5fb70>]
>>> b1.a # however b1.a is None
>>>
>>> session.add(a1)
>>> session.commit() # so upon flush + expire....
>>> a1.bs # the value is gone
[]
修复确保在触发反向引用之前,即在修改集合之前,对集合进行检查,确保目标项的实例数量为零或一,然后取消多对一的一侧,使用线性搜索,目前使用 list.search
和 list.__contains__
。
最初认为在集合内部需要使用基于事件的引用计数方案,以便在整个集合的生命周期中跟踪所有重复实例,这将对所有集合操作产生性能/内存/复杂性影响,包括加载和追加这些非常频繁的操作。相反采取的方法将额外的开销限制在较少常见的集合移除和批量替换操作上,线性扫描的观察开销是可以忽略的;在工作单元中已经使用了与关系绑定集合的线性扫描,以及在集合进行批量替换时。
#1103 ### 与 AliasedClass 的关系取代了非主映射器的需求
“非主映射器”是以 Imperative Mapping 风格创建的Mapper
,它充当已经映射类的另一种可选择的附加映射器。非主映射器起源于 SQLAlchemy 的 0.1、0.2 系列,当时预期Mapper
对象将是主要的查询构造接口,而Query
对象还不存在。
随着Query
的出现,以及后来的AliasedClass
构造,大多数非主映射器的用例都消失了。这是一件好事,因为 SQLAlchemy 在 0.5 系列左右也完全摆脱了“经典”映射,转而采用了声明式系统。
当意识到一些非常难以定义的relationship()
配置可能成为可能时,仍然存在一个非主映射器的用例,当一个具有替代可选择项的非主映射器被作为映射目标时,而不是尝试构建一个包含特定对象间关系所有复杂性的relationship.primaryjoin
。
随着这种使用情况越来越普遍,它的局限性变得明显,包括非主映射器难以配置到可选的添加新列的地方,映射器不继承原始映射的关系,非主映射器上明确配置的关系在加载器选项中表现不佳,非主映射器也不提供可以在查询中使用的基于列的属性的完整功能命名空间(在旧的 0.1 至 0.4 版本中,人们可以直接使用Table
对象与 ORM)。
缺失的部分是允许relationship()
直接引用AliasedClass
。 AliasedClass
已经实现了我们希望非主映射器实现的所有功能;它允许从替代选择加载现有映射类,它继承了现有映射器的所有属性和关系,它与加载器选项非常配合,它提供了一个可以像类本身一样混入查询的类似类对象。通过这种改变,以前针对非主映射器的配方在配置关联加入处被改为别名类。
在关联到别名类,原始的非主映射器如下所示:
j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)
B_viacd = mapper(
B,
j,
non_primary=True,
primary_key=[j.c.b_id],
properties={
"id": j.c.b_id, # so that 'id' looks the same as before
"c_id": j.c.c_id, # needed for disambiguation
"d_c_id": j.c.d_c_id, # needed for disambiguation
"b_id": [j.c.b_id, j.c.d_b_id],
"d_id": j.c.d_id,
},
)
A.b = relationship(B_viacd, primaryjoin=A.b_id == B_viacd.c.b_id)
这些属性是必需的,以便重新映射附加列,以便它们不与映射到B
的现有列发生冲突,同时还需要定义一个新的主键。
采用新方法,所有这些冗长都消失了,并且在建立关系时直接引用附加列:
j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)
B_viacd = aliased(B, j, flat=True)
A.b = relationship(B_viacd, primaryjoin=A.b_id == j.c.b_id)
非主映射器现在已经被弃用,最终目标是将经典映射作为一种功能完全取消。声明性 API 将成为映射的唯一手段,这希望能够实现内部改进和简化,以及更清晰的文档编写。
selectin 加载不再对简单的一对多使用 JOIN
在 1.2 中添加的“selectin”加载功能引入了一种极其高效的新方法来急切加载集合,在许多情况下比“subquery”急切加载要快得多,因为它不依赖于重新声明原始 SELECT 查询,而是使用一个简单的 IN 子句。然而,“selectin”加载仍然依赖于在父表和相关表之间渲染 JOIN,因为它需要行中的父主键值以匹配行。在 1.3 中,添加了一种新的优化,将在简单的一对多加载的最常见情况下省略此 JOIN,其中相关行已经包含了父行的主键值,表达为其外键列。这再次提供了显著的性能改进,因为 ORM 现在可以一次性加载大量集合,而无需使用 JOIN 或子查询。
给定一个映射:
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
bs = relationship("B", lazy="selectin")
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
在“selectin”加载的 1.2 版本中,从 A 到 B 的加载看起来像:
SELECT a.id AS a_id FROM a
SELECT a_1.id AS a_1_id, b.id AS b_id, b.a_id AS b_a_id
FROM a AS a_1 JOIN b ON a_1.id = b.a_id
WHERE a_1.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY a_1.id
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
使用新行为后,负载看起来像:
SELECT a.id AS a_id FROM a
SELECT b.a_id AS b_a_id, b.id AS b_id FROM b
WHERE b.a_id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY b.a_id
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
该行为被释放为自动的,使用了类似于延迟加载的启发式方法,以确定是否可以直接从标识映射中获取相关实体。然而,与大多数查询功能一样,由于涉及多态加载的高级场景,该功能的实现变得更加复杂。如果遇到问题,用户应该报告错误,但是该更改还包括一个标志relationship.omit_join
,可以在relationship()
上设置为False
,以禁用优化。
改进多对一查询表达式的行为
当构建一个将多对一关系与对象值进行比较的查询时,例如:
u1 = session.query(User).get(5)
query = session.query(Address).filter(Address.user == u1)
上述表达式Address.user == u1
,最终编译成一个基于User
对象的主键列的 SQL 表达式,如"address.user_id = 5"
,使用延迟可调用以在绑定表达式中尽可能晚地检索值5
。这是为了适应这样一个用例,即Address.user == u1
表达式可能针对尚未刷新的User
对象,该对象依赖于服务器生成的主键值,以及该表达式始终返回正确的结果,即使自创建表达式以来u1
的主键值已更改。
然而,这种行为的一个副作用是,如果在评估表达式时u1
最终过期,将导致额外的 SELECT 语句,并且在u1
也从Session
中分离的情况下,将引发错误:
u1 = session.query(User).get(5)
query = session.query(Address).filter(Address.user == u1)
session.expire(u1)
session.expunge(u1)
query.all() # <-- would raise DetachedInstanceError
当 Session
提交并且 u1
实例超出范围时,对象的过期 / 删除可能会隐式发生,因为 Address.user == u1
表达式不会强烈引用对象本身,而只会引用其InstanceState
。
修复方法是允许 Address.user == u1
表达式根据在表达式编译时尝试正常检索或加载值的基础上评估值 5
,就像现在一样,但如果对象是分离的并且已过期,则从 InstanceState
上的新机制中检索,该机制将在该状态上的当属性过期时为该特定属性的最后已知值进行存储。仅当表达式功能需要时,此机制才会为特定属性 / InstanceState
启用以节省性能 / 内存开销。
最初,尝试了诸如立即评估表达式并在以后尝试加载值时采取各种安排的简单方法,但困难的边缘案例是正在更改的列属性的值(通常是自然主键)的值。为了确保像 Address.user == u1
这样的表达式始终返回 u1
的当前状态的正确答案,如果需要,它将返回持久对象的当前数据库持久值,通过 SELECT 查询取消到期,并且对于分离的对象,它将返回最近已知的值,无论对象何时被使用新特性将其过期在InstanceState
中跟踪列属性的最后已知值时。
当值无法评估时,现代属性 API 功能用于指示特定的错误消息,两种情况是当列属性从未设置过时,以及当对象在首次评估时已经过期且现在分离时。在所有情况下,DetachedInstanceError
不再被引发。
多对一替换不会对“raiseload”或“old”对象进行提升
考虑到延迟加载将在多对一关系上进行以加载“old”值的情况,如果关系未指定relationship.active_history
标志,则不会为分离的对象引发断言:
a1 = session.query(Address).filter_by(id=5).one()
session.expunge(a1)
a1.user = some_user
上面,当在分离的 a1
对象上替换 .user
属性时,会引发 DetachedInstanceError
,因为属性试图从标识映射中检索 .user
的先前值。更改是现在操作会在不加载旧值的情况下继续进行。
对 lazy="raise"
加载器策略也进行了相同的更改:
class Address(Base):
# ...
user = relationship("User", ..., lazy="raise")
以前,a1.user
的关联会引发 “raiseload” 异常,因为属性试图检索先前的值。现在在加载 “旧” 值的情况下跳过了此断言。
为 ORM 属性实现了 “del”
Python del
操作实际上不能用于映射属性,无论是标量列还是对象引用。已经添加了对此的支持,使其能够正常工作,其中 del
操作大致相当于将属性设置为 None
值:
some_object = session.query(SomeObject).get(5)
del some_object.some_attribute # from a SQL perspective, works like "= None"
添加到 InstanceState 的 info 字典
将 .info
字典添加到 InstanceState
类,该类是通过在映射对象上调用 inspect()
获得的。这允许自定义方案为对象添加关于对象的其他信息,该信息将随对象在内存中的完整生命周期一起传递:
from sqlalchemy import inspect
u1 = User(id=7, name="ed")
inspect(u1).info["user_info"] = "7|ed"
水平分片扩展支持批量更新和删除方法
ShardedQuery
扩展对象支持 Query.update()
和 Query.delete()
批量更新/删除方法。在调用它们时会咨询 query_chooser
可调用对象,以便根据给定的条件在多个分片上运行更新/删除操作。
协会代理改进
虽然没有特定的原因,但本次周期内协会代理扩展进行了许多改进。
协会代理有新的 cascade_scalar_deletes 标志
给定一个映射如下:
class A(Base):
__tablename__ = "test_a"
id = Column(Integer, primary_key=True)
ab = relationship("AB", backref="a", uselist=False)
b = association_proxy(
"ab", "b", creator=lambda b: AB(b=b), cascade_scalar_deletes=True
)
class B(Base):
__tablename__ = "test_b"
id = Column(Integer, primary_key=True)
ab = relationship("AB", backref="b", cascade="all, delete-orphan")
class AB(Base):
__tablename__ = "test_ab"
a_id = Column(Integer, ForeignKey(A.id), primary_key=True)
b_id = Column(Integer, ForeignKey(B.id), primary_key=True)
对 A.b
的赋值将生成一个 AB
对象:
a.b = B()
A.b
关联是标量的,并包括一个新标志 AssociationProxy.cascade_scalar_deletes
。设置了该标志后,将 A.b
设置为 None
将同时移除 A.ab
。默认行为仍然是保留 a.ab
不变:
a.b = None
assert a.ab is None
虽然最初看起来这个逻辑应该只需查看现有关系的“cascade”属性,但仅凭这一点并不清楚代理对象是否应该被移除,因此行为被作为显式选项提供。
另外,del
现在对标量的工作方式类似于设置为 None
:
del a.b
assert a.ab is None
#4308 #### AssociationProxy 在每个类的基础上存储类特定的状态
AssociationProxy
对象根据它关联的父映射类做出许多决策。虽然 AssociationProxy
在历史上始作为相对简单的‘getter’,但很早就显而易见它还需要做出关于它所引用的属性类型的决策——例如标量或集合、映射对象或简单值等。为了实现这一点,它需要检查映射属性或其他引用描述符或属性,从其父类中引用。然而,在 Python 描述符机制中,描述符仅在上下文中被访问时才了解其“父”类,例如调用 MyClass.some_descriptor
,这将调用 __get__()
方法,该方法传入类。因此,AssociationProxy
对象将存储特定于该类的状态,但只有在首次调用此方法时才会; 在首次访问 AssociationProxy
作为描述符之前尝试检查此状态会引发错误。此外,它将假定__get__()
看到的第一个类是它需要知道的唯一父类。尽管如果一个特定类有继承的子类,协会代理实际上是代表超过一个父类工作的,即使它没有明确地被重新使用。即使有这个缺陷,协会代理仍然可以通过其当前行为取得很大进展,但在某些情况下仍存在缺陷,以及确定最佳“所有者”类的复杂问题。
这些问题现在得到解决,因为当调用 __get__()
时,AssociationProxy
不再修改自己的内部状态;相反,每个类都生成一个名为 AssociationProxyInstance
的新对象,处理特定于特定映射父类的所有状态(当父类未映射时,不会生成 AssociationProxyInstance
)。关联代理的单一“拥有类”概念,尽管在 1.1 中得到改进,但实质上已被一种方法取代,即 AP 现在可以平等对待任意数量的“拥有”类。
为了适应希望检查此状态的应用程序,而不一定调用 __get__()
的 AssociationProxy
,添加了一个新方法 AssociationProxy.for_class()
,提供对特定类的 AssociationProxyInstance
的直接访问,示例如下:
class User(Base):
# ...
keywords = association_proxy("kws", "keyword")
proxy_state = inspect(User).all_orm_descriptors["keywords"].for_class(User)
一旦我们有了 AssociationProxyInstance
对象,在上面的示例中存储在 proxy_state
变量中,我们可以查看特定于 User.keywords
代理的��性,比如 target_class
:
>>> proxy_state.target_class
Keyword
#3423 #### AssociationProxy 现在为基于列的目标提供标准列运算符
给定一个 AssociationProxy
,其中目标是数据库列,而不是对象引用或另一个关联代理:
class User(Base):
# ...
elements = relationship("Element")
# column-based association proxy
values = association_proxy("elements", "value")
class Element(Base):
# ...
value = Column(String)
User.values
关联代理指向 Element.value
列。现在可以进行标准列操作,比如 like
:
>>> print(s.query(User).filter(User.values.like("%foo%")))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND element.value LIKE :value_1)
equals
:
>>> print(s.query(User).filter(User.values == "foo"))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND element.value = :value_1)
当与 None
进行比较时,IS NULL
表达式会增加一个测试,即相关行根本不存在;这与以前的行为相同:
>>> print(s.query(User).filter(User.values == None))
SELECT "user".id AS user_id
FROM "user"
WHERE (EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND element.value IS NULL)) OR NOT (EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id))
请注意,ColumnOperators.contains()
操作实际上是一个字符串比较操作符;这是行为上的变化,以前,关联代理仅使用.contains
作为列表包含操作符。通过基于列的比较,它现在的行为类似于“like”:
>>> print(s.query(User).filter(User.values.contains("foo")))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND (element.value LIKE '%' || :value_1 || '%'))
为了测试User.values
集合是否包含值"foo"
,应该使用等于操作符(例如User.values == 'foo'
);这在以前的版本中也适用。
当使用基于对象的关联代理与集合时,行为与以前相同,即测试集合成员资格,例如给定一个映射:
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
user_elements = relationship("UserElement")
# object-based association proxy
elements = association_proxy("user_elements", "element")
class UserElement(Base):
__tablename__ = "user_element"
id = Column(Integer, primary_key=True)
user_id = Column(ForeignKey("user.id"))
element_id = Column(ForeignKey("element.id"))
element = relationship("Element")
class Element(Base):
__tablename__ = "element"
id = Column(Integer, primary_key=True)
value = Column(String)
.contains()
方法产生与以前相同的表达式,测试User.elements
列表中是否存在Element
对象:
>>> print(s.query(User).filter(User.elements.contains(Element(id=1))))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM user_element
WHERE "user".id = user_element.user_id AND :param_1 = user_element.element_id)
总的来说,这种变化是基于 AssociationProxy stores class-specific state on a per-class basis 的架构变化实现的;因为代理现在在生成表达式时会产生额外的状态,所以AssociationProxyInstance
类现在有对象目标和列目标版本。
关联代理现在强引用父对象
关联代理集合长期以来只维护对父对象的弱引用的行为被还原;代理现在将在代理集合本身也在内存中的情况下维护对父对象的强引用,消除了“过时的关联代理”错误。这种变化是基于实验性的基础进行的,以查看是否会出现任何导致副作用的用例。
举例来说,给定一个带有关联代理的映射:
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
bs = relationship("B")
b_data = association_proxy("bs", "data")
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
data = Column(String)
a1 = A(bs=[B(data="b1"), B(data="b2")])
b_data = a1.b_data
以前,如果a1
在作用域外被删除:
del a1
在a1
从作用域中删除后尝试迭代b_data
集合会引发错误“过时的关联代理,父对象已经超出作用域”。这是因为关联代理需要访问实际的a1.bs
集合以产生视图,在此更改之前,它只维护对a1
的弱引用。特别是,用户在执行内联操作时经常会遇到此错误,例如:
collection = session.query(A).filter_by(id=1).first().b_data
由于A
对象在b_data
集合实际使用之前可能被垃圾回收。
变化在于b_data
集合现在维护对a1
对象的强引用,使其保持存在:
assert b_data == ["b1", "b2"]
这个改变带来了一个副作用,即如果一个应用程序像上面那样传递集合,父对象在集合被丢弃之前不会被垃圾回收。一如既往,如果a1
在特定的Session
中是持久的,它将一直保留在该会话的状态中,直到被垃圾回收。
请注意,如果这个改变导致问题,可能会进行修订。
为集合、字典实现了批量替换与 AssociationProxy
将集合或字典分配给关联代理集合现在应该能够正确工作,而以前会为现有键重新创建关联代理成员,导致由于相同对象的删除+插入而导致潜在的刷新失败问题,现在应该只在适当的情况下创建新的关联对象:
class A(Base):
__tablename__ = "test_a"
id = Column(Integer, primary_key=True)
b_rel = relationship(
"B",
collection_class=set,
cascade="all, delete-orphan",
)
b = association_proxy("b_rel", "value", creator=lambda x: B(value=x))
class B(Base):
__tablename__ = "test_b"
__table_args__ = (UniqueConstraint("a_id", "value"),)
id = Column(Integer, primary_key=True)
a_id = Column(Integer, ForeignKey("test_a.id"), nullable=False)
value = Column(String)
# ...
s = Session(e)
a = A(b={"x", "y", "z"})
s.add(a)
s.commit()
# re-assign where one B should be deleted, one B added, two
# B's maintained
a.b = {"x", "z", "q"}
# only 'q' was added, so only one new B object. previously
# all three would have been re-created leading to flush conflicts
# against the deleted ones.
assert len(s.new) == 1
#2642 #### 关联代理新增了新的 cascade_scalar_deletes 标志
给定一个映射如下:
class A(Base):
__tablename__ = "test_a"
id = Column(Integer, primary_key=True)
ab = relationship("AB", backref="a", uselist=False)
b = association_proxy(
"ab", "b", creator=lambda b: AB(b=b), cascade_scalar_deletes=True
)
class B(Base):
__tablename__ = "test_b"
id = Column(Integer, primary_key=True)
ab = relationship("AB", backref="b", cascade="all, delete-orphan")
class AB(Base):
__tablename__ = "test_ab"
a_id = Column(Integer, ForeignKey(A.id), primary_key=True)
b_id = Column(Integer, ForeignKey(B.id), primary_key=True)
对A.b
的赋值将生成一个AB
对象:
a.b = B()
A.b
关联是标量的,并包括一个新标志AssociationProxy.cascade_scalar_deletes
。当设置时,将A.b
设置为None
将同时移除A.ab
。默认行为仍然是保留a.ab
在原地:
a.b = None
assert a.ab is None
起初似乎很直观的是,这个逻辑应该只查看现有关系的“级联”属性,但仅仅从那个属性本身并不清楚代理对象是否应该被移除,因此行为被作为一个明确的选项提供。
另外,del
现在对标量的操作方式与设置为None
类似:
del a.b
assert a.ab is None
关联代理在每个类上存储特定于类的状态
AssociationProxy
对象基于其关联的父映射类做出许多决策。虽然 AssociationProxy
在历史上最初是一个相对简单的“getter”,但很快就明显地需要做出关于其引用的属性类型的决策——比如标量或集合、映射对象或简单值等。为了实现这一点,它需要检查映射属性或其他引用描述符或属性,这些都是从其父类引用的。然而,在 Python 描述符机制中,描述符只有在在其“父”类的上下文中被访问时才会了解其“父”类,比如调用 MyClass.some_descriptor
,这会调用 __get__()
方法并传递类。因此,AssociationProxy
对象将存储特定于该类的状态,但只有在调用此方法后才会这样;在未首先将 AssociationProxy
作为描述符访问的情况下尝试检查此状态将引发错误。此外,它会假定 __get__()
看到的第一个类就是它需要了解的唯一父类。尽管如果一个特定类有继承的子类,关联代理实际上是代表不止一个父类工作,即使没有明确重用。尽管即使有这个缺点,关联代理仍然可以通过其当前行为取得相当大的进展,但在某些情况下仍存在缺陷,以及确定最佳“所有者”类的复杂问题。
现在这些问题已经解决了,因为在调用 __get__()
时,AssociationProxy
不再修改自己的内部状态;相反,针对每个类生成一个新对象,称为 AssociationProxyInstance
,该对象处理与特定映射的父类相关的所有状态(当父类未映射时,不会生成 AssociationProxyInstance
)。一个关联代理的单一“拥有类”的概念,尽管在 1.1 中得到了改进,但基本上已被一种方法取代,即 AP 现在可以平等对待任意数量的“拥有”类。
为了适应那些想要检查此状态的应用,而不一定调用 __get__()
的应用程序,添加了一个新方法 AssociationProxy.for_class()
,它提供了对特定类的 AssociationProxyInstance
的直接访问,如下所示:
class User(Base):
# ...
keywords = association_proxy("kws", "keyword")
proxy_state = inspect(User).all_orm_descriptors["keywords"].for_class(User)
一旦我们拥有 AssociationProxyInstance
对象,在上面的示例中存储在 proxy_state
变量中,我们可以查看特定于 User.keywords
代理的属性,例如 target_class
:
>>> proxy_state.target_class
Keyword
关联代理现在为基于列的目标提供标准列操作符
给定一个 AssociationProxy
,其中目标是数据库列,并且不是对象引用或另一个关联代理:
class User(Base):
# ...
elements = relationship("Element")
# column-based association proxy
values = association_proxy("elements", "value")
class Element(Base):
# ...
value = Column(String)
User.values
关联代理指的是 Element.value
列。现在可使用标准列操作,例如 like
:
>>> print(s.query(User).filter(User.values.like("%foo%")))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND element.value LIKE :value_1)
equals
:
>>> print(s.query(User).filter(User.values == "foo"))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND element.value = :value_1)
当与 None
比较时,IS NULL
表达式会增加一个测试,即相关行根本不存在;这与以前的行为相同:
>>> print(s.query(User).filter(User.values == None))
SELECT "user".id AS user_id
FROM "user"
WHERE (EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND element.value IS NULL)) OR NOT (EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id))
请注意,ColumnOperators.contains()
操作符实际上是一个字符串比较操作符;这是一个行为上的改变,之前,关联代理仅将 .contains
用作列表包含操作符。通过基于列的比较,它现在的行为类似于“like”:
>>> print(s.query(User).filter(User.values.contains("foo")))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND (element.value LIKE '%' || :value_1 || '%'))
为了测试 User.values
集合是否包含值 "foo"
,应该使用等于操作符(例如 User.values == 'foo'
);这在之前的版本中也适用。
当使用基于对象的关联代理与集合时,行为与以前相同,即测试集合成员资格,例如给定一个映射:
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
user_elements = relationship("UserElement")
# object-based association proxy
elements = association_proxy("user_elements", "element")
class UserElement(Base):
__tablename__ = "user_element"
id = Column(Integer, primary_key=True)
user_id = Column(ForeignKey("user.id"))
element_id = Column(ForeignKey("element.id"))
element = relationship("Element")
class Element(Base):
__tablename__ = "element"
id = Column(Integer, primary_key=True)
value = Column(String)
.contains()
方法产生与之前相同的表达式,测试 User.elements
列表中是否存在 Element
对象:
>>> print(s.query(User).filter(User.elements.contains(Element(id=1))))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM user_element
WHERE "user".id = user_element.user_id AND :param_1 = user_element.element_id)
总的来说,这个改变是基于关联代理在每个类的基础上存储特定于类的状态的架构变化而启用的;由于代理现在在生成表达式时会产生额外的状态,AssociationProxyInstance
类现在有对象目标和列目标版本。
关联代理现在强引用父对象
关联代理集合长期维持对父对象的弱引用的行为被撤销;代理现在将在代理集合本身也在内存中的情况下维持对父对象的强引用,消除了“过时的关联代理”错误。这个改变是基于试验性基础进行的,以查看是否会出现任何导致副作用的用例。
举例来说,给定一个带有关联代理的映射:
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
bs = relationship("B")
b_data = association_proxy("bs", "data")
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
data = Column(String)
a1 = A(bs=[B(data="b1"), B(data="b2")])
b_data = a1.b_data
以前,如果 a1
超出范围被删除:
del a1
在 a1
从范围中删除后尝试迭代 b_data
集合会引发错误 "过时的关联代理,父对象已超出范围"
。这是因为关联代理需要访问实际的 a1.bs
集合以生成视图,在此改变之前,它只维持对 a1
的弱引用。特别是,用户在执行内联操作时经常会遇到这个错误:
collection = session.query(A).filter_by(id=1).first().b_data
上面的情况是因为 A
对象在 b_data
集合实际使用之前会被垃圾回收。
变化在于 b_data
集合现在维持对 a1
对象的强引用,使其保持存在:
assert b_data == ["b1", "b2"]
这种改变引入了一个副作用,即如果应用程序像上面那样传递集合,父对象在集合被丢弃之前不会被垃圾回收。一如既往,如果a1
在特定的Session
内是持久的,它将一直保留在该会话的状态中,直到被垃圾回收。
注意,如果这种改变导致问题,可能会进行修订。
使用 AssociationProxy 为集合实现批量替换的功能
现在,将集合分配给关联代理集合应该可以正常工作,而以前会为现有键重新创建关联代理成员,导致由于删除+插入相同对象而导致潜在刷新失败的问题,现在应该只在适当的情况下创建新的关联对象:
class A(Base):
__tablename__ = "test_a"
id = Column(Integer, primary_key=True)
b_rel = relationship(
"B",
collection_class=set,
cascade="all, delete-orphan",
)
b = association_proxy("b_rel", "value", creator=lambda x: B(value=x))
class B(Base):
__tablename__ = "test_b"
__table_args__ = (UniqueConstraint("a_id", "value"),)
id = Column(Integer, primary_key=True)
a_id = Column(Integer, ForeignKey("test_a.id"), nullable=False)
value = Column(String)
# ...
s = Session(e)
a = A(b={"x", "y", "z"})
s.add(a)
s.commit()
# re-assign where one B should be deleted, one B added, two
# B's maintained
a.b = {"x", "z", "q"}
# only 'q' was added, so only one new B object. previously
# all three would have been re-created leading to flush conflicts
# against the deleted ones.
assert len(s.new) == 1
多对一反向引用在移除操作期间检查集合中的重复项
当 ORM 映射的集合作为 Python 序列存在时,通常是 Python list
,这是relationship()
的默认值,包含重复项,并且对象从一个位置被移除但未从其他位置移除时,一个多对一的反向引用会将其属性设置为None
,即使一对多方仍然表示对象存在。尽管一对多集合在关系模型中不能有重复项,但使用序列集合的 ORM 映射的relationship()
在内存中可以有重复项,但这些重复状态既不能持久化也不能从数据库中检索。特别是,在列表中临时存在重复项是 Python“交换”操作的固有特性。考虑一个标准的一对多/多对一设置:
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"))
如果我们有一个具有两个B
成员的A
对象,并执行交换:
a1 = A(bs=[B(), B()])
a1.bs[0], a1.bs[1] = a1.bs[1], a1.bs[0]
在上述操作期间,拦截标准 Python __setitem__
__delitem__
方法会提供一个中间状态,其中第二个B()
对象在集合中出现两次。当B()
对象从一个位置移除时,B.a
反向引用会将引用设置为None
,导致在刷新期间删除A
和B
对象之间的链接。相同的问题也可以使用普通重复项来演示:
>>> a1 = A()
>>> b1 = B()
>>> a1.bs.append(b1)
>>> a1.bs.append(b1) # append the same b1 object twice
>>> del a1.bs[1]
>>> a1.bs # collection is unaffected so far...
[<__main__.B object at 0x7f047af5fb70>]
>>> b1.a # however b1.a is None
>>>
>>> session.add(a1)
>>> session.commit() # so upon flush + expire....
>>> a1.bs # the value is gone
[]
修复确保在反向引用触发之前,在集合被改变之前,检查集合中是否恰好有一个或零个目标项的实例,然后在取消多对一方面时使用线性搜索,目前使用list.search
和list.__contains__
。
最初人们认为在集合内部需要使用基于事件的引用计数方案,以便在整个集合的生命周期内跟踪所有重复的实例,这将对所有集合操作产生性能/内存/复杂性影响,包括非常频繁的加载和追加操作。取而代之的方法是将额外的开销限制在较不常见的集合移除和批量替换操作上,并且观察到的线性扫描开销可以忽略不计;关系绑定集合的线性扫描已经在工作单元中使用,以及在集合被批量替换时已经被使用。
关键行为变化 - ORM
Query.join() 更明确地处理决定“左”侧的歧义
从历史上看,给定以下查询:
u_alias = aliased(User)
session.query(User, u_alias).join(Address)
鉴于标准教程映射,查询将生成一个 FROM 子句,如下所示:
SELECT ...
FROM users AS users_1, users JOIN addresses ON users.id = addresses.user_id
也就是说,JOIN 会隐式地针对第一个匹配的实体。新的行为是要求解决这种模糊性的异常:
sqlalchemy.exc.InvalidRequestError: Can't determine which FROM clause to
join from, there are multiple FROMS which can join to this entity.
Try adding an explicit ON clause to help resolve the ambiguity.
解决方案是提供一个 ON 子句,可以是一个表达式:
# join to User
session.query(User, u_alias).join(Address, Address.user_id == User.id)
# join to u_alias
session.query(User, u_alias).join(Address, Address.user_id == u_alias.id)
或者使用关系属性,如果可用的话:
# join to User
session.query(User, u_alias).join(Address, User.addresses)
# join to u_alias
session.query(User, u_alias).join(Address, u_alias.addresses)
这个变化包括,如果连接是非模糊的,那么现在连接可以正确地链接到不是列表中第一个元素的 FROM 子句:
session.query(func.current_timestamp(), User).join(Address)
在这项增强之前,上述查询将引发:
sqlalchemy.exc.InvalidRequestError: Don't know how to join from
CURRENT_TIMESTAMP; please use select_from() to establish the
left entity/selectable of this join
现在查询可以正常工作了:
SELECT CURRENT_TIMESTAMP AS current_timestamp_1, users.id AS users_id,
users.name AS users_name, users.fullname AS users_fullname,
users.password AS users_password
FROM users JOIN addresses ON users.id = addresses.user_id
总的来说,这种变化直接符合 Python 的“显式优于隐式”的哲学。
#4365 ### FOR UPDATE 子句在联合贪婪加载子查询中以及外部呈现
此更改特别适用于使用joinedload()
加载策略与行限制查询相结合,例如使用Query.first()
或Query.limit()
,以及使用Query.with_for_update()
方法。
给定一个查询:
session.query(A).options(joinedload(A.b)).limit(5)
当联合贪婪加载与 LIMIT 结合时,Query
对象呈现如下形式的 SELECT:
SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id
这样,对主要实体的行限制就会发生,而不会影响到关联项的贪婪加载。当上述查询与“SELECT..FOR UPDATE”结合时,行为是这样的:
SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id FOR UPDATE
然而,由于bugs.mysql.com/bug.php?id=90693
,MySQL 不会锁定子查询中的行,不像 PostgreSQL 和其他数据库。因此,上述查询现在呈现为:
SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5 FOR UPDATE
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id FOR UPDATE
在 Oracle 方言中,内部的“FOR UPDATE”不会呈现,因为 Oracle 不支持这种语法,方言会跳过针对子查询的任何“FOR UPDATE”;在任何情况下都不是必要的,因为 Oracle 像 PostgreSQL 一样正确锁定返回行的所有元素。
当在Query.with_for_update.of
修饰符上使用时,通常在 PostgreSQL 上,外部的“FOR UPDATE”被省略,OF 现在在内部呈现;以前,OF 目标不会被转换以正确适应子查询。因此,考虑到:
session.query(A).options(joinedload(A.b)).with_for_update(of=A).limit(5)
现在查询将呈现为:
SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5 FOR UPDATE OF a
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id
上述形式应该对 PostgreSQL 有所帮助,因为 PostgreSQL 不允许在 LEFT OUTER JOIN 目标之后呈现 FOR UPDATE 子句。
总的来说,对于正在使用的目标数据库,FOR UPDATE 仍然非常具体,不容易推广到更复杂的查询。
#4246 ### passive_deletes=’all’将使 FK 对于从集合中移除的对象保持不变
relationship.passive_deletes
选项接受值"all"
,表示在刷新对象时不应修改任何外键属性,即使关系的集合/引用已被移除。以前,在以下情况下,这种情况并不适用于一对多或一对一关系:
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
addresses = relationship("Address", passive_deletes="all")
class Address(Base):
__tablename__ = "addresses"
id = Column(Integer, primary_key=True)
email = Column(String)
user_id = Column(Integer, ForeignKey("users.id"))
user = relationship("User")
u1 = session.query(User).first()
address = u1.addresses[0]
u1.addresses.remove(address)
session.commit()
# would fail and be set to None
assert address.user_id == u1.id
修复现在包括address.user_id
根据passive_deletes="all"
保持不变。这种情况对于构建自定义“版本表”方案等非常有用,其中行被归档而不是被删除。
#3844 ### Query.join()更明确地处理决定“左”侧的模棱两可性
历史上,给定如下查询:
u_alias = aliased(User)
session.query(User, u_alias).join(Address)
鉴于标准教程映射,查询将生成一个 FROM 子句:
SELECT ...
FROM users AS users_1, users JOIN addresses ON users.id = addresses.user_id
这意味着 JOIN 将隐式地针对第一个匹配的实体。新行为是,异常请求解决这种模棱两可的情况:
sqlalchemy.exc.InvalidRequestError: Can't determine which FROM clause to
join from, there are multiple FROMS which can join to this entity.
Try adding an explicit ON clause to help resolve the ambiguity.
解决方案是提供一个 ON 子句,可以是一个表达式:
# join to User
session.query(User, u_alias).join(Address, Address.user_id == User.id)
# join to u_alias
session.query(User, u_alias).join(Address, Address.user_id == u_alias.id)
或者使用关系属性,如果可用的话:
# join to User
session.query(User, u_alias).join(Address, User.addresses)
# join to u_alias
session.query(User, u_alias).join(Address, u_alias.addresses)
更改包括现在 JOIN 可以正确地链接到不是列表中第一个元素的 FROM 子句,如果 JOIN 是非模棱两可的话:
session.query(func.current_timestamp(), User).join(Address)
在此增强之前,上述查询将引发:
sqlalchemy.exc.InvalidRequestError: Don't know how to join from
CURRENT_TIMESTAMP; please use select_from() to establish the
left entity/selectable of this join
现在查询正常工作:
SELECT CURRENT_TIMESTAMP AS current_timestamp_1, users.id AS users_id,
users.name AS users_name, users.fullname AS users_fullname,
users.password AS users_password
FROM users JOIN addresses ON users.id = addresses.user_id
总的来说,这种变化直接符合 Python 的“显式优于隐式”的哲学。
FOR UPDATE 子句在连接贪婪加载子查询内部以及外部都被渲染
此更改特别适用于使用joinedload()
加载策略与行限制查询结合使用时,例如使用Query.first()
或Query.limit()
,以及使用Query.with_for_update()
方法。
给定一个查询如下:
session.query(A).options(joinedload(A.b)).limit(5)
当连接贪婪加载与 LIMIT 结合时,Query
对象会渲染以下形式的 SELECT:
SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id
这样做是为了使主实体的行限制生效,而不影响相关项目的连接贪婪加载。当上述查询与“SELECT..FOR UPDATE”结合时,行为如下:
SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id FOR UPDATE
然而,由于bugs.mysql.com/bug.php?id=90693
,MySQL 不会锁定子查询内的行,不像 PostgreSQL 和其他数据库。因此,上述查询现在会渲染为:
SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5 FOR UPDATE
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id FOR UPDATE
在 Oracle 方言中,内部的“FOR UPDATE”不会被渲染,因为 Oracle 不支持这种语法,方言会跳过任何针对子查询的“FOR UPDATE”;在任何情况下都不是必要的,因为 Oracle 像 PostgreSQL 一样正确地锁定了返回行的所有元素。
当在Query.with_for_update.of
修饰符上使用时,通常在 PostgreSQL 上,外部的“FOR UPDATE”会被省略,OF 现在会在内部被渲染;以前,OF 目标不会被转换以适应子查询。所以给定:
session.query(A).options(joinedload(A.b)).with_for_update(of=A).limit(5)
查询现在会渲染为:
SELECT subq.a_id, subq.a_data, b_alias.id, b_alias.data FROM (
SELECT a.id AS a_id, a.data AS a_data FROM a LIMIT 5 FOR UPDATE OF a
) AS subq LEFT OUTER JOIN b ON subq.a_id=b.a_id
以上形式在 PostgreSQL 上也应该有所帮助,因为 PostgreSQL 不允许在 LEFT OUTER JOIN 目标之后渲染 FOR UPDATE 子句。
总的来说,FOR UPDATE 对于正在使用的目标数据库非常具体,不能轻易地推广到更复杂的查询。
passive_deletes='all'将使从集合中移除的对象的 FK 保持不变
relationship.passive_deletes
选项接受值"all"
,表示在刷新对象时不应修改任何外键属性,即使关系的集合/引用已被移除。以前,在以下情况下,这不会发生在一对多或一对一关系中:
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
addresses = relationship("Address", passive_deletes="all")
class Address(Base):
__tablename__ = "addresses"
id = Column(Integer, primary_key=True)
email = Column(String)
user_id = Column(Integer, ForeignKey("users.id"))
user = relationship("User")
u1 = session.query(User).first()
address = u1.addresses[0]
u1.addresses.remove(address)
session.commit()
# would fail and be set to None
assert address.user_id == u1.id
修复现在包括address.user_id
按照passive_deletes="all"
不变。这种情况对于构建自定义“版本表”方案等非常有用,其中行被归档而不是删除。
新功能和改进 - 核心
新的多列命名约定标记,长名称截断
为了适应MetaData
命名约定需要区分多列约束并希望在生成的约束名称中使用所有列的情况,添加了一系列新的命名约定标记,包括column_0N_name
、column_0_N_name
、column_0N_key
、column_0_N_key
、referred_column_0N_name
、referred_column_0_N_name
等,它们将所有列的列名(或键或标签)连接在一起,没有分隔符或使用下划线分隔符。下面我们定义一个约定,将UniqueConstraint
约束命名为连接所有列名称的名称:
metadata_obj = MetaData(
naming_convention={"uq": "uq_%(table_name)s_%(column_0_N_name)s"}
)
table = Table(
"info",
metadata_obj,
Column("a", Integer),
Column("b", Integer),
Column("c", Integer),
UniqueConstraint("a", "b", "c"),
)
上述表的 CREATE TABLE 将呈现为:
CREATE TABLE info (
a INTEGER,
b INTEGER,
c INTEGER,
CONSTRAINT uq_info_a_b_c UNIQUE (a, b, c)
)
此外,现在将长名称截断逻辑应用于命名约定生成的名称,特别是为了适应可能产生非常长名称的多列标签。这种逻辑与用于截断 SELECT 语句中的长标签名称的逻辑相同,用一个确定性生成的 4 字符哈希替换超过目标数据库标识符长度限制的多余字符。例如,在 PostgreSQL 上,标识符不能超过 63 个字符,长约束名称通常从下面的表定义中生成:
long_names = Table(
"long_names",
metadata_obj,
Column("information_channel_code", Integer, key="a"),
Column("billing_convention_name", Integer, key="b"),
Column("product_identifier", Integer, key="c"),
UniqueConstraint("a", "b", "c"),
)
截断逻辑将确保不会为 UNIQUE 约束生成过长的名称:
CREATE TABLE long_names (
information_channel_code INTEGER,
billing_convention_name INTEGER,
product_identifier INTEGER,
CONSTRAINT uq_long_names_information_channel_code_billing_conventi_a79e
UNIQUE (information_channel_code, billing_convention_name, product_identifier)
)
上述后缀a79e
基于长名称的 md5 哈希,并且每次生成相同的值,以产生给定模式的一致名称。
注意,当约束名在给定方言中显式过大时,截断逻辑也会引发IdentifierError
。这已经是Index
对象的行为很长时间了,但现在也适用于其他类型的约束:
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import Table
from sqlalchemy import UniqueConstraint
from sqlalchemy.dialects import postgresql
from sqlalchemy.schema import AddConstraint
m = MetaData()
t = Table("t", m, Column("x", Integer))
uq = UniqueConstraint(
t.c.x,
name="this_is_too_long_of_a_name_for_any_database_backend_even_postgresql",
)
print(AddConstraint(uq).compile(dialect=postgresql.dialect()))
将输出:
sqlalchemy.exc.IdentifierError: Identifier
'this_is_too_long_of_a_name_for_any_database_backend_even_postgresql'
exceeds maximum length of 63 characters
异常引发阻止了由数据库后端截断的非确定性约束名称的生成,这些名称后来与数据库迁移不兼容。
要将 SQLAlchemy 端的截断规则应用于上述标识符,请使用conv()
构造:
uq = UniqueConstraint(
t.c.x,
name=conv("this_is_too_long_of_a_name_for_any_database_backend_even_postgresql"),
)
这将再次输出确定性截断的 SQL,如下所示:
ALTER TABLE t ADD CONSTRAINT this_is_too_long_of_a_name_for_any_database_backend_eve_ac05 UNIQUE (x)
目前还没有选项可以使名称传递以允许数据库端截断。这对于Index
名称已经有一段时间了,而且并没有提出问题。
此更改还修复了另外两个问题。一个是column_0_key
标记虽然被记录,但却无法使用,另一个是如果这两个值不同,referred_column_0_name
标记会错误地呈现.key
而不是.name
列。
另请参阅
配置约束命名约定
MetaData.naming_convention
#3989 ### 用于 SQL 函数的二进制比较解释
这个增强功能是在核心层实现的,但主要适用于 ORM。
现在可以将比较两个元素的 SQL 函数用作“比较”对象,适用于 ORM relationship()
的用法,首先像往常一样使用func
工厂创建函数,然后在函数完成时调用FunctionElement.as_comparison()
修饰符来生成一个具有“左”和“右”两侧的BinaryExpression
:
class Venue(Base):
__tablename__ = "venue"
id = Column(Integer, primary_key=True)
name = Column(String)
descendants = relationship(
"Venue",
primaryjoin=func.instr(remote(foreign(name)), name + "/").as_comparison(1, 2)
== 1,
viewonly=True,
order_by=name,
)
上面,“后代”关系的relationship.primaryjoin
将基于传递给instr()
的第一个和第二个参数产生一个“左”和一个“右”表达式。这使得 ORM 懒加载等功能能够产生 SQL,例如:
SELECT venue.id AS venue_id, venue.name AS venue_name
FROM venue
WHERE instr(venue.name, (? || ?)) = ? ORDER BY venue.name
('parent1', '/', 1)
以及一个 joinedload,例如:
v1 = (
s.query(Venue)
.filter_by(name="parent1")
.options(joinedload(Venue.descendants))
.one()
)
工作原理如下:
SELECT venue.id AS venue_id, venue.name AS venue_name,
venue_1.id AS venue_1_id, venue_1.name AS venue_1_name
FROM venue LEFT OUTER JOIN venue AS venue_1
ON instr(venue_1.name, (venue.name || ?)) = ?
WHERE venue.name = ? ORDER BY venue_1.name
('/', 1, 'parent1')
该功能预期将有助于处理诸如在关系连接条件中使用几何函数或任何通过 SQL 函数来表达 SQL 连接的 ON 子句等情况。
#3831 ### 扩展 IN 特性现在支持空列表
版本 1.2 中引入的“扩展 IN”功能现在支持传递给ColumnOperators.in_()
运算符的空列表。对于空列表的实现将产生一个特定于目标后端的“空集”表达式,例如对于 PostgreSQL,“SELECT CAST(NULL AS INTEGER) WHERE 1!=1”,对于 MySQL,“SELECT 1 FROM (SELECT 1) as _empty_set WHERE 1!=1”:
>>> from sqlalchemy import create_engine
>>> from sqlalchemy import select, literal_column, bindparam
>>> e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
>>> with e.connect() as conn:
... conn.execute(
... select([literal_column("1")]).where(
... literal_column("1").in_(bindparam("q", expanding=True))
... ),
... q=[],
... )
{exexsql}SELECT 1 WHERE 1 IN (SELECT CAST(NULL AS INTEGER) WHERE 1!=1)
该功能还适用于基于元组的 IN 语句,其中“空 IN”表达式将被扩展以支持元组中给定的元素,例如在 PostgreSQL 上:
>>> from sqlalchemy import create_engine
>>> from sqlalchemy import select, literal_column, tuple_, bindparam
>>> e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
>>> with e.connect() as conn:
... conn.execute(
... select([literal_column("1")]).where(
... tuple_(50, "somestring").in_(bindparam("q", expanding=True))
... ),
... q=[],
... )
{exexsql}SELECT 1 WHERE (%(param_1)s, %(param_2)s)
IN (SELECT CAST(NULL AS INTEGER), CAST(NULL AS VARCHAR) WHERE 1!=1)
#4271 ### TypeEngine 方法 bind_expression、column_expression 与 Variant、特定类型一起工作
当TypeEngine.bind_expression()
和TypeEngine.column_expression()
方法存在于特定数据类型的“impl”上时,这些方法现在可以被方言使用,也可以用于TypeDecorator
和Variant
的用例。
以下示例说明了一个TypeDecorator
,它将 SQL 时间转换函数应用于LargeBinary
。为了使此类型在Variant
的上下文中工作,编译器需要深入“impl”变体表达式以定位这些方法:
from sqlalchemy import TypeDecorator, LargeBinary, func
class CompressedLargeBinary(TypeDecorator):
impl = LargeBinary
def bind_expression(self, bindvalue):
return func.compress(bindvalue, type_=self)
def column_expression(self, col):
return func.uncompress(col, type_=self)
MyLargeBinary = LargeBinary().with_variant(CompressedLargeBinary(), "sqlite")
上述表达式仅在 SQLite 上使用时会呈现为 SQL 中的函数:
from sqlalchemy import select, column
from sqlalchemy.dialects import sqlite
print(select([column("x", CompressedLargeBinary)]).compile(dialect=sqlite.dialect()))
将呈现:
SELECT uncompress(x) AS x
此更改还包括方言可以在方言级别的实现类型上实现TypeEngine.bind_expression()
和TypeEngine.column_expression()
,它们现在将被使用;特别是这将用于 MySQL 的新“二进制前缀”要求以及用于将 MySQL 的十进制绑定值转换为浮点数。
#3981 ### QueuePool 的新后进先出策略
通常由 create_engine()
使用的连接池称为 QueuePool
。此连接池使用类似于 Python 内置的 Queue
类的对象来存储等待使用的数据库连接。Queue
具有先进先出的行为,旨在提供对持续在池中的数据库连接的循环使用。然而,这样做的一个潜在缺点是,当池的利用率低时,池中的每个连接的串行重复使用意味着试图减少未使用连接的服务器端超时策略被阻止关闭这些连接。为了适应这种情况,添加了一个新的标志 create_engine.pool_use_lifo
,它将 Queue
的 .get()
方法反转,以从队列的开头而不是末尾获取连接,从而实质上将“队列”变为“栈”(考虑到这样做会增加一个称为 StackPool
的全新连接池,但这太啰嗦了)。
另见
使用 FIFO vs. LIFO ### 新的多列命名约定标记,长名称截断
为了适应需要通过 MetaData
命名约定消除多列约束的歧义,并希望在生成的约束名称中使用所有列的情况,添加了一系列新的命名约定标记,包括 column_0N_name
、column_0_N_name
、column_0N_key
、column_0_N_key
、referred_column_0N_name
、referred_column_0_N_name
等,这些标记将约束中的所有列的列名(或键或标签)连接在一起,可以是没有分隔符或带有下划线分隔符。下面我们定义一个约定,将会以将所有列的名称连接在一起的方式命名 UniqueConstraint
约束:
metadata_obj = MetaData(
naming_convention={"uq": "uq_%(table_name)s_%(column_0_N_name)s"}
)
table = Table(
"info",
metadata_obj,
Column("a", Integer),
Column("b", Integer),
Column("c", Integer),
UniqueConstraint("a", "b", "c"),
)
上述表的 CREATE TABLE 将呈现为:
CREATE TABLE info (
a INTEGER,
b INTEGER,
c INTEGER,
CONSTRAINT uq_info_a_b_c UNIQUE (a, b, c)
)
此外,现在对通过命名约定生成的名称应用长名称截断逻辑,特别是为了适应可能产生非常长名称的多列标签。这个逻辑与在 SELECT 语句中截断长标签名称所使用的逻辑相同,它用一个确定性生成的 4 字符哈希替换了超过目标数据库标识符长度限制的多余字符。例如,在 PostgreSQL 中,标识符不能超过 63 个字符,长约束名通常是从下面的表定义生成的:
long_names = Table(
"long_names",
metadata_obj,
Column("information_channel_code", Integer, key="a"),
Column("billing_convention_name", Integer, key="b"),
Column("product_identifier", Integer, key="c"),
UniqueConstraint("a", "b", "c"),
)
截断逻辑将确保不会为 UNIQUE 约束生成过长的名称:
CREATE TABLE long_names (
information_channel_code INTEGER,
billing_convention_name INTEGER,
product_identifier INTEGER,
CONSTRAINT uq_long_names_information_channel_code_billing_conventi_a79e
UNIQUE (information_channel_code, billing_convention_name, product_identifier)
)
上述后缀a79e
基于长名称的 md5 哈希,并且每次生成相同的值,以为给定的模式生成一致的名称。
请注意,当约束名称显式过大时,截断逻辑还会引发IdentifierError
。这已经是Index
对象的行为很长时间了,但现在也适用于其他类型的约束:
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import Table
from sqlalchemy import UniqueConstraint
from sqlalchemy.dialects import postgresql
from sqlalchemy.schema import AddConstraint
m = MetaData()
t = Table("t", m, Column("x", Integer))
uq = UniqueConstraint(
t.c.x,
name="this_is_too_long_of_a_name_for_any_database_backend_even_postgresql",
)
print(AddConstraint(uq).compile(dialect=postgresql.dialect()))
输出将是:
sqlalchemy.exc.IdentifierError: Identifier
'this_is_too_long_of_a_name_for_any_database_backend_even_postgresql'
exceeds maximum length of 63 characters
异常抛出可防止由数据库后端截断的不确定性约束名称的生成,这些名称随后与数据库迁移不兼容。
要将 SQLAlchemy 端的截断规则应用于上述标识符,请使用conv()
构造:
uq = UniqueConstraint(
t.c.x,
name=conv("this_is_too_long_of_a_name_for_any_database_backend_even_postgresql"),
)
这将再次输出确定性截断的 SQL,如下所示:
ALTER TABLE t ADD CONSTRAINT this_is_too_long_of_a_name_for_any_database_backend_eve_ac05 UNIQUE (x)
目前尚无选项可使名称通过以允许数据库端截断。这在一段时间内已经适用于Index
名称,并且并未引起问题。
这一变更还修复了其他两个问题。其中一个是column_0_key
令牌虽然已记录,但却不可用,另一个是如果这两个值不同,referred_column_0_name
令牌会意外地呈现.key
而不是.name
。
另请参阅
配置约束命名约定
MetaData.naming_convention
SQL 函数的二进制比较解释
此增强功能是在核心级别实现的,但主要适用于 ORM。
现在可以将比较两个元素的 SQL 函数用作适用于 ORM relationship()
中的“比较”对象,首先使用func
工厂通常创建该函数,然后当函数完成时调用FunctionElement.as_comparison()
修饰符,以生成具有“左”和“右”两侧的BinaryExpression
:
class Venue(Base):
__tablename__ = "venue"
id = Column(Integer, primary_key=True)
name = Column(String)
descendants = relationship(
"Venue",
primaryjoin=func.instr(remote(foreign(name)), name + "/").as_comparison(1, 2)
== 1,
viewonly=True,
order_by=name,
)
上面,“descendants”关系的relationship.primaryjoin
将基于传递给instr()
的第一个和第二个参数产生一个“left”和一个“right”表达式。这允许 ORM lazyload 等功能生成类似以下的 SQL:
SELECT venue.id AS venue_id, venue.name AS venue_name
FROM venue
WHERE instr(venue.name, (? || ?)) = ? ORDER BY venue.name
('parent1', '/', 1)
以及一个 joinedload,例如:
v1 = (
s.query(Venue)
.filter_by(name="parent1")
.options(joinedload(Venue.descendants))
.one()
)
作为:
SELECT venue.id AS venue_id, venue.name AS venue_name,
venue_1.id AS venue_1_id, venue_1.name AS venue_1_name
FROM venue LEFT OUTER JOIN venue AS venue_1
ON instr(venue_1.name, (venue.name || ?)) = ?
WHERE venue.name = ? ORDER BY venue_1.name
('/', 1, 'parent1')
这个功能预计将有助于处理诸如在关系连接条件中使用几何函数或任何 ON 子句以 SQL 函数形式表达的情况。
扩展 IN 功能现在支持空列表
在版本 1.2 中引入的“扩展 IN”功能现在支持传递给ColumnOperators.in_()
运算符的空列表。对于空列表的实现将产生一个特定于目标后端的“空集合”表达式,例如对于 PostgreSQL,“SELECT CAST(NULL AS INTEGER) WHERE 1!=1”,对于 MySQL,“SELECT 1 FROM (SELECT 1) as _empty_set WHERE 1!=1”:
>>> from sqlalchemy import create_engine
>>> from sqlalchemy import select, literal_column, bindparam
>>> e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
>>> with e.connect() as conn:
... conn.execute(
... select([literal_column("1")]).where(
... literal_column("1").in_(bindparam("q", expanding=True))
... ),
... q=[],
... )
{exexsql}SELECT 1 WHERE 1 IN (SELECT CAST(NULL AS INTEGER) WHERE 1!=1)
该功能还适用于基于元组的 IN 语句,其中“空 IN”表达式将被扩展以支持元组中给定的元素,例如在 PostgreSQL 上:
>>> from sqlalchemy import create_engine
>>> from sqlalchemy import select, literal_column, tuple_, bindparam
>>> e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
>>> with e.connect() as conn:
... conn.execute(
... select([literal_column("1")]).where(
... tuple_(50, "somestring").in_(bindparam("q", expanding=True))
... ),
... q=[],
... )
{exexsql}SELECT 1 WHERE (%(param_1)s, %(param_2)s)
IN (SELECT CAST(NULL AS INTEGER), CAST(NULL AS VARCHAR) WHERE 1!=1)
TypeEngine 方法 bind_expression、column_expression 与 Variant、特定类型一起工作
TypeEngine.bind_expression()
和TypeEngine.column_expression()
方法现在在特定数据类型的“impl”上存在时也能工作,允许这些方法被方言以及TypeDecorator
和Variant
用例使用。
以下示例说明了一个TypeDecorator
,它将 SQL 时间转换函数应用于LargeBinary
。为了使这种类型在Variant
的上下文中工作,编译器需要深入“impl”变体表达式以定位这些方法:
from sqlalchemy import TypeDecorator, LargeBinary, func
class CompressedLargeBinary(TypeDecorator):
impl = LargeBinary
def bind_expression(self, bindvalue):
return func.compress(bindvalue, type_=self)
def column_expression(self, col):
return func.uncompress(col, type_=self)
MyLargeBinary = LargeBinary().with_variant(CompressedLargeBinary(), "sqlite")
上述表达式仅在 SQLite 上使用时会在 SQL 中呈现一个函数:
from sqlalchemy import select, column
from sqlalchemy.dialects import sqlite
print(select([column("x", CompressedLargeBinary)]).compile(dialect=sqlite.dialect()))
将呈现:
SELECT uncompress(x) AS x
此更改还包括方言可以在方言级别的实现类型上实现TypeEngine.bind_expression()
和TypeEngine.column_expression()
,在那里它们现在将被使用;特别是这将用于 MySQL 的新“二进制前缀”要求以及用于将 MySQL 的十进制绑定值转换的情况。
QueuePool 的新后进先出策略
通常由create_engine()
使用的连接池被称为QueuePool
。此池使用一个类似于 Python 内置的Queue
类的对象来存储等待使用的数据库连接。Queue
具有先进先出的行为,旨在提供对持久在池中的数据库连接的循环使用。然而,这种方法的一个潜在缺点是,当池的利用率较低时,池中每个连接的串行重复使用意味着试图减少未使用连接的服务器端超时策略被阻止关闭这些连接。为了适应这种用例,添加了一个新标志create_engine.pool_use_lifo
,它将Queue
的.get()
方法反转,从队列的开头而不是末尾获取连接,从本质上将“队列”变成“栈”(考虑到添加一个名为StackPool
的全新池,但这太啰嗦了)。
另请参阅
使用 FIFO vs. LIFO
核心关键变化
完全删除将字符串 SQL 片段强制转换为 text()
首次在版本 1.0 中添加的警告,描述在将完整 SQL 片段强制转换为 text()时发出的警告,现在已转换为异常。对于像Query.filter()
和Select.order_by()
等方法传递的字符串片段自动转换为text()
构造的持续关注,尽管这已发出警告。在Select.order_by()
、Query.order_by()
、Select.group_by()
和Query.group_by()
的情况下,字符串标签或列名仍然解析为相应的表达式构造,但如果解析失败,则会引发CompileError
,从而防止直接呈现原始 SQL 文本。
#4481 ### “线程本地”引擎策略已弃用
“线程本地引擎策略”是在 SQLAlchemy 0.2 左右添加的,作为解决 SQLAlchemy 0.1 中操作的标准方式的问题的解决方案,可以总结为“线程本地一切”,发现存在不足。回顾起来,似乎相当荒谬,SQLAlchemy 的首次发布在各个方面都是“alpha”,却担心太多用户已经定居在现有 API 上,无法简单地更改它。
SQLAlchemy 的原始用法模型如下:
engine.begin()
table.insert().execute(parameters)
result = table.select().execute()
table.update().execute(parameters)
engine.commit()
在几个月的实际使用后,很明显,假装“连接”或“事务”是一个隐藏的实现细节是一个坏主意,特别是当有人需要同时处理多个数据库连接时。因此,我们今天看到的使用范式被引入,减去了上下文管理器,因为它们在 Python 中尚不存在:
conn = engine.connect()
try:
trans = conn.begin()
conn.execute(table.insert(), parameters)
result = conn.execute(table.select())
conn.execute(table.update(), parameters)
trans.commit()
except:
trans.rollback()
raise
finally:
conn.close()
上述范式是人们所需要的,但由于它仍然有点冗长(因为没有上下文管理器),因此保留了旧的工作方式,并成为线程本地引擎策略。
今天,使用 Core 更加简洁,甚至比原始模式更加简洁,这要归功于上下文管理器:
with engine.begin() as conn:
conn.execute(table.insert(), parameters)
result = conn.execute(table.select())
conn.execute(table.update(), parameters)
此时,仍依赖“threadlocal”风格的任何剩余代码将通过此弃用来鼓励现代化 - 该功能应在下一个主要系列的 SQLAlchemy 中完全移除,例如 1.4 版。连接池参数Pool.use_threadlocal
也已弃用,因为在大多数情况下实际上没有任何效果,Engine.contextual_connect()
方法也是如此,该方法通常与Engine.connect()
方法是同义词,除非使用了 threadlocal 引擎。
#4393 ### convert_unicode 参数已弃用
参数String.convert_unicode
和create_engine.convert_unicode
已弃用。这些参数的目的是指示 SQLAlchemy 确保在 Python 2 下传递给数据库之前对传入的 Python Unicode 对象进行编码为字节字符串,并期望从数据库返回的字节字符串转换回 Python Unicode 对象。在 Python 3 之前的时代,要做到这一点是一件非常艰巨的事情,因为几乎所有 Python DBAPI 默认情况下都没有启用 Unicode 支持,并且大多数都存在与它们提供的 Unicode 扩展相关的主要问题。最终,SQLAlchemy 添加了 C 扩展,这些扩展的主要目的之一是加快结果集中的 Unicode 解码过程。
一旦引入了 Python 3,DBAPI 开始更全面地支持 Unicode,并且更重要的是,默认情况下支持 Unicode。然而,特定 DBAPI 在何种条件下会或不会从结果返回 Unicode 数据,以及接受 Python Unicode 值作为参数的条件仍然非常复杂。这标志着“convert_unicode”标志开始过时,因为它们不再足以确保仅在需要时进行编码/解码,而不是在不需要时进行。相反,“convert_unicode”开始被 dialects 自动检测。这一部分可以在引擎第一次连接时发出的“SELECT ‘test plain returns’”和“SELECT ‘test_unicode_returns’”SQL 中看到;方言正在测试当前 DBAPI 及其当前设置和后端数据库连接是否默认返回 Unicode。
结果是,不再需要在任何情况下使用“convert_unicode”标志,如果需要,SQLAlchemy 项目需要知道这些情况及其原因。目前,在所有主要数据库上,使用该标志的 Unicode 往返测试通过了数百次,因此相当有把握地认为它们不再需要,除非是在争议性的非使用情况,例如访问来自传统数据库的错误编码数据,最好使用自定义类型。
#4393 ### 完全移除将字符串 SQL 片段强制转换为 text()
在 1.0 版本中首次添加的警告,描述在将完整 SQL 片段强制转换为 text() 时发出的警告,现已转换为异常。对于将字符串片段传递给诸如Query.filter()
和 Select.order_by()
等方法的自动转换成text()
构造的情况仍然存在持续的担忧,尽管已发出警告。对于Select.order_by()
、Query.order_by()
、Select.group_by()
和 Query.group_by()
,字符串标签或列名仍然解析为相应的表达式构造,但如果解析失败,则引发CompileError
,从而防止原始 SQL 文本直接呈现。
“threadlocal” 引擎策略已弃用
“threadlocal 引擎策略” 是在 SQLAlchemy 0.2 左右添加的,作为 SQLAlchemy 0.1 中标准操作方式的解决方案,这种方式可以总结为“threadlocal everything”,但后来发现存在不足之处。回顾起来,很荒谬的是,即使 SQLAlchemy 的第一个版本在各个方面都是“alpha”版本,仍然担心已有太多用户已经习惯了现有的 API 以至于不能轻易更改。
SQLAlchemy 的原始使用模型如下:
engine.begin()
table.insert().execute(parameters)
result = table.select().execute()
table.update().execute(parameters)
engine.commit()
经过几个月的实际使用,很明显,假装“连接”或“事务”是一个隐藏的实现细节是一个坏主意,特别是当有人需要同时处理多个数据库连接时。因此,我们今天看到的使用范式被引入,减去了上下文管理器,因为它们在 Python 中尚不存在:
conn = engine.connect()
try:
trans = conn.begin()
conn.execute(table.insert(), parameters)
result = conn.execute(table.select())
conn.execute(table.update(), parameters)
trans.commit()
except:
trans.rollback()
raise
finally:
conn.close()
上述范式是人们所需要的,但由于仍然有点啰嗦(因为没有上下文管理器),旧的工作方式也被保留下来,并成为线程本地引擎策略。
今天,使用 Core 要简洁得多,甚至比原始模式更简洁,这要归功于上下文管理器:
with engine.begin() as conn:
conn.execute(table.insert(), parameters)
result = conn.execute(table.select())
conn.execute(table.update(), parameters)
此时,任何仍然依赖“threadlocal”风格的代码都将通过此弃用被鼓励进行现代化 - 该功能应在下一个主要的 SQLAlchemy 系列(例如 1.4)中完全移除。连接池参数Pool.use_threadlocal
也被弃用,因为在大多数情况下实际上没有任何效果,Engine.contextual_connect()
方法也是如此,该方法通常与Engine.connect()
方法是同义的,除非使用线程本地引擎。
convert_unicode 参数已弃用
参数String.convert_unicode
和create_engine.convert_unicode
已被弃用。这些参数的目的是指示 SQLAlchemy 在将 Python 2 中的传入 Unicode 对象传递到数据库之前确保对其进行字节串编码,并期望从数据库接收字节串并将其转换回 Python Unicode 对象。在 Python 3 之前的时代,要做到这一点是一个巨大的挑战,因为几乎所有的 Python DBAPI 默认情况下都没有启用 Unicode 支持,并且大多数都存在与其提供的 Unicode 扩展相关的主要问题。最终,SQLAlchemy 添加了 C 扩展,其中这些扩展的主要目的之一是加速结果集中的 Unicode 解码过程。
一旦引入了 Python 3,DBAPI 开始更全面地支持 Unicode,更重要的是,默认情况下支持 Unicode。然而,特定 DBAPI 是否返回 Unicode 数据以及接受 Python Unicode 值作为参数的条件仍然非常复杂。这标志着“convert_unicode”标志开始过时,因为它们不再足以确保仅在需要时进行编码/解码,而不是在不需要时进行。相反,“convert_unicode”开始由方言自动检测。可以从引擎第一次连接时发出的 SQL “SELECT ‘test plain returns’” 和 “SELECT ‘test_unicode_returns’” 中看到其中一部分;方言正在测试当前 DBAPI 与其当前设置和后端数据库连接是否默认返回 Unicode。
最终结果是,在任何情况下,用户对“convert_unicode”标志的使用都不再需要,并且如果需要,SQLAlchemy 项目需要知道这些情况以及原因。当前,在所有主要数据库上,通过了数百个 Unicode 往返测试,而不使用此标志,因此相当有信心不再需要它们,除非在可争议的非使用情况下,例如访问来自遗留数据库的错误编码数据,此时最好使用自定义类型。
方言改进和更改 - PostgreSQL
为 PostgreSQL 分区表添加了基本的反射支持
SQLAlchemy 可以在 PostgreSQL CREATE TABLE 语句中使用 postgresql_partition_by
标志渲染“PARTITION BY”序列,该标志在版本 1.2.6 中添加。然而,'p'
类型直到现在都不是反射查询的一部分。
给定这样一个模式:
dv = Table(
"data_values",
metadata_obj,
Column("modulus", Integer, nullable=False),
Column("data", String(30)),
postgresql_partition_by="range(modulus)",
)
sa.event.listen(
dv,
"after_create",
sa.DDL(
"CREATE TABLE data_values_4_10 PARTITION OF data_values "
"FOR VALUES FROM (4) TO (10)"
),
)
两个表名 'data_values'
和 'data_values_4_10'
将通过 Inspector.get_table_names()
返回,并且列也将通过 Inspector.get_columns('data_values')
和 Inspector.get_columns('data_values_4_10')
返回。这也适用于对这些表使用 Table(..., autoload=True)
。
#4237 ### 为 PostgreSQL 分区表添加了基本的反射支持
SQLAlchemy 可以在 PostgreSQL CREATE TABLE 语句中使用 postgresql_partition_by
标志渲染“PARTITION BY”序列,该标志在版本 1.2.6 中添加。然而,'p'
类型直到现在都不是反射查询的一部分。
给定这样一个模式:
dv = Table(
"data_values",
metadata_obj,
Column("modulus", Integer, nullable=False),
Column("data", String(30)),
postgresql_partition_by="range(modulus)",
)
sa.event.listen(
dv,
"after_create",
sa.DDL(
"CREATE TABLE data_values_4_10 PARTITION OF data_values "
"FOR VALUES FROM (4) TO (10)"
),
)
两个表名'data_values'
和'data_values_4_10'
将从Inspector.get_table_names()
返回,并且还将从Inspector.get_columns('data_values')
以及Inspector.get_columns('data_values_4_10')
返回列。这也适用于使用这些表的Table(..., autoload=True)
。
方言改进和变化 - MySQL
协议级别的 ping 现在用于预先 ping
包括 mysqlclient、python-mysql、PyMySQL 和 mysql-connector-python 在内的 MySQL 方言现在使用connection.ping()
方法进行池预 ping 功能,详细信息请参见断开处理 - 悲观。这比以前在连接上发出“SELECT 1”的方法要轻量得多。 ### 控制 ON DUPLICATE KEY UPDATE 中参数顺序
ON DUPLICATE KEY UPDATE
子句中 UPDATE 参数的顺序现在可以通过传递一个 2 元组列表来明确排序:
from sqlalchemy.dialects.mysql import insert
insert_stmt = insert(my_table).values(id="some_existing_id", data="inserted value")
on_duplicate_key_stmt = insert_stmt.on_duplicate_key_update(
[
("data", "some data"),
("updated_at", func.current_timestamp()),
],
)
参见
INSERT…ON DUPLICATE KEY UPDATE (Upsert) ### 协议级别的 ping 现在用于预先 ping
包括 mysqlclient、python-mysql、PyMySQL 和 mysql-connector-python 在内的 MySQL 方言现在使用connection.ping()
方法进行池预 ping 功能,详细信息请参见断开处理 - 悲观。这比以前在连接上发出“SELECT 1”的方法要轻量得多。
控制 ON DUPLICATE KEY UPDATE 中参数顺序
ON DUPLICATE KEY UPDATE
子句中 UPDATE 参数的顺序现在可以通过传递一个 2 元组列表来明确排序:
from sqlalchemy.dialects.mysql import insert
insert_stmt = insert(my_table).values(id="some_existing_id", data="inserted value")
on_duplicate_key_stmt = insert_stmt.on_duplicate_key_update(
[
("data", "some data"),
("updated_at", func.current_timestamp()),
],
)
参见
INSERT…ON DUPLICATE KEY UPDATE (Upsert)
方言改进和变化 - SQLite
对 SQLite JSON 的支持已添加
添加了一个新的数据类型JSON
,它代表了JSON
基础数据类型的 SQLite 的 json 成员访问函数。实现使用 SQLite 的JSON_EXTRACT
和JSON_QUOTE
函数来提供基本的 JSON 支持。
请注意,数据库中呈现的数据类型本身的名称是“JSON”。这将创建一个带有“numeric”亲和力的 SQLite 数据类型,通常情况下不应该成为问题,除非是由单个整数值组成的 JSON 值的情况。尽管如此,根据 SQLite 自己文档中的示例www.sqlite.org/json1.html
,名称 JSON 正在被用于其熟悉性。
#3850 ### 增加对 SQLite 约束中 ON CONFLICT 的支持
SQLite 支持一个非标准的 ON CONFLICT 子句,可以为独立约束以及一些列内约束(如 NOT NULL)指定。通过向诸如UniqueConstraint
之类的对象添加sqlite_on_conflict
关键字以及几个Column
-特定的变体:
some_table = Table(
"some_table",
metadata_obj,
Column("id", Integer, primary_key=True, sqlite_on_conflict_primary_key="FAIL"),
Column("data", Integer),
UniqueConstraint("id", "data", sqlite_on_conflict="IGNORE"),
)
上述表在 CREATE TABLE 语句中呈现为:
CREATE TABLE some_table (
id INTEGER NOT NULL,
data INTEGER,
PRIMARY KEY (id) ON CONFLICT FAIL,
UNIQUE (id, data) ON CONFLICT IGNORE
)
另请参阅
ON CONFLICT 支持约束
#4360 ### 增加对 SQLite JSON 的支持
添加了一个新的数据类型JSON
,该数据类型代表JSON
基本数据类型的 SQLite 的 json 成员访问函数。该实现使用 SQLite 的JSON_EXTRACT
和JSON_QUOTE
函数来提供基本的 JSON 支持。
注意,数据库中呈现的数据类型本身的名称是“JSON”。这将创建一个带有“数字”亲和力的 SQLite 数据类型,这通常不应该是问题,除非 JSON 值仅包含单个整数值。尽管如此,根据 SQLite 自己文档中的示例,www.sqlite.org/json1.html
中使用了 JSON 这个名字以保持熟悉性。
增加对 SQLite 约束中 ON CONFLICT 的支持
SQLite 支持一个非标准的 ON CONFLICT 子句,可以为独立约束以及一些列内约束(如 NOT NULL)指定。通过向诸如UniqueConstraint
之类的对象添加sqlite_on_conflict
关键字以及几个Column
-特定的变体,已为这些子句添加了支持:
some_table = Table(
"some_table",
metadata_obj,
Column("id", Integer, primary_key=True, sqlite_on_conflict_primary_key="FAIL"),
Column("data", Integer),
UniqueConstraint("id", "data", sqlite_on_conflict="IGNORE"),
)
上述表在 CREATE TABLE 语句中呈现为:
CREATE TABLE some_table (
id INTEGER NOT NULL,
data INTEGER,
PRIMARY KEY (id) ON CONFLICT FAIL,
UNIQUE (id, data) ON CONFLICT IGNORE
)
另请参阅
ON CONFLICT 支持约束
方言改进和更改 - Oracle
对于通用 unicode,国家字符数据类型被减弱,可通过选项重新启用
现在,默认情况下,Unicode
和 UnicodeText
数据类型现在对应于 Oracle 上的 VARCHAR2
和 CLOB
数据类型,而不是 NVARCHAR2
和 NCLOB
(也称为“国家”字符集类型)。这将在诸如它们在CREATE TABLE
语句中的呈现方式等行为中看到,以及当使用Unicode
或UnicodeText
绑定参数时,不会传递任何类型对象给setinputsizes()
;cx_Oracle 会原生处理字符串值。这种变化基于 cx_Oracle 的维护者的建议,即 Oracle 中的“国家”数据类型在很大程度上已经过时且性能不佳。它们还会在某些情况下干扰,比如应用于像trunc()
这样的函数的格式说明符时。
当数据库不使用符合 Unicode 标准的字符集时,可能需要使用NVARCHAR2
和相关类型的情况。在这种情况下,可以将标志use_nchar_for_unicode
传递给create_engine()
以重新启用旧行为。
如往常一样,明确使用NVARCHAR2
和NCLOB
数据类型将继续使用NVARCHAR2
和NCLOB
,包��在 DDL 中以及处理带有 cx_Oracle 的setinputsizes()
的绑定参数时。
在读取方面,在 Python 2 下已经添加了对 CHAR/VARCHAR/CLOB 结果行的自动 Unicode 转换,以匹配 Python 3 下 cx_Oracle 的行为。为了减轻 cx_Oracle 方言在 Python 2 下先前具有的性能损失,SQLAlchemy 在 Python 2 下使用非常高效(当构建 C 扩展时)的本地 Unicode 处理程序。可以通过将coerce_to_unicode
标志设置为 False 来禁用自动 Unicode 强制转换。此标志现在默认为 True,并适用于所有在结果集中返回的字符串数据,这些数据不明确位于Unicode
或 Oracle 的 NVARCHAR2/NCHAR/NCLOB 数据类型下。
#4242 ### cx_Oracle 连接参数现代化,废弃的参数已移除
对于 cx_oracle 方言接受的参数以及 URL 字符串进行了一系列现代化处理:
-
废弃的参数
auto_setinputsizes
、allow_twophase
、exclude_setinputsizes
已被移除。 -
threaded
参数的值,对于 SQLAlchemy 方言一直默认为 True,现在不再默认生成。SQLAlchemy 的Connection
对象本身不被视为线程安全,因此不需要传递此标志。 -
将
threaded
传递给create_engine()
本身已被弃用。要将threaded
的值设置为True
,请将其传递给create_engine.connect_args
字典或使用查询字符串,例如oracle+cx_oracle://...?threaded=true
。 -
现在,传递到 URL 查询字符串的所有参数,如果不被特别消耗,都会传递给 cx_Oracle.connect() 函数。其中一些也会被强制转换为 cx_Oracle 常量或布尔值,包括
mode
、purity
、events
和threaded
。 -
与之前一样,所有 cx_Oracle
.connect()
参数都通过create_engine.connect_args
字典接受,文档在这方面是不准确的。
#4369 ### 国家字符数据类型被弱化以支持通用 Unicode,可通过选项重新启用。
默认情况下,Unicode
和 UnicodeText
数据类型现在对应于 Oracle 上的 VARCHAR2
和 CLOB
数据类型,而不是 NVARCHAR2
和 NCLOB
(也称为“国家”字符集类型)。这将在诸如它们在 CREATE TABLE
语句中的呈现方式等行为中看到,以及当使用 Unicode
或 UnicodeText
绑定参数时,不会传递任何类型对象给 setinputsizes()
;cx_Oracle 会原生处理字符串值。这一变化基于 cx_Oracle 的维护者的建议,即 Oracle 中的“国家”数据类型在很大程度上已经过时且性能不佳。它们还会在某些情况下干扰,比如应用于 trunc()
等函数的格式说明符时。
可能需要使用 NVARCHAR2
和相关类型的情况是数据库未使用符合 Unicode 标准的字符集。在这种情况下,可以通过将标志 use_nchar_for_unicode
传递给 create_engine()
来重新启用旧行为。
始终如此,在 DDL 中明确使用NVARCHAR2
和NCLOB
数据类型将继续使用NVARCHAR2
和NCLOB
,包括在处理绑定参数时使用 cx_Oracle 的setinputsizes()
。
在读取方面,在 Python 2 下已添加了 CHAR/VARCHAR/CLOB 结果行的自动 Unicode 转换,以匹配 Python 3 下 cx_Oracle 的行为。为了减轻以前在 Python 2 下 cx_Oracle 方言在这种行为下的性能损失,SQLAlchemy 在 Python 2 下使用了非常高效(当构建了 C 扩展时)的本地 Unicode 处理程序。自动 Unicode 强制转换可以通过将coerce_to_unicode
标志设置为 False 来禁用。该标志现在默认为 True,并适用于结果集中返回的所有不明确为Unicode
或 Oracle 的 NVARCHAR2/NCHAR/NCLOB 数据类型的字符串数据。
cx_Oracle 连接参数现代化,弃用的参数已移除
对 cx_oracle 方言接受的参数以及 URL 字符串进行了一系列现代化改进:
-
弃用的参数
auto_setinputsizes
、allow_twophase
、exclude_setinputsizes
已被移除。 -
threaded
参数的值,对于 SQLAlchemy 方言始终默认为 True,现在不再默认生成。SQLAlchemy 的Connection
对象本身不被认为是线程安全的,因此不需要传递此标志。 -
将
threaded
传递给create_engine()
本身已被弃用。要将threaded
的值设置为True
,请将其传递给create_engine.connect_args
字典或使用查询字符串,例如oracle+cx_oracle://...?threaded=true
。 -
现在,URL 查询字符串中传递的所有参数,如果不被特殊消耗,都会传递给 cx_Oracle.connect()函数。其中一些参数也会被强制转换为 cx_Oracle 常量或布尔值,包括
mode
、purity
、events
和threaded
。 -
与之前一样,所有 cx_Oracle 的
.connect()
参数都可以通过create_engine.connect_args
字典接受,文档在这方面描述不准确。
方言改进和变化 - SQL Server
支持 pyodbc fast_executemany
Pyodbc 最近添加的“fast_executemany”模式,在使用 Microsoft ODBC 驱动程序时可用,现在是 pyodbc / mssql 方言的选项。通过create_engine()
传递:
engine = create_engine(
"mssql+pyodbc://scott:tiger@mssql2017:1433/test?driver=ODBC+Driver+13+for+SQL+Server",
fast_executemany=True,
)
另请参阅
快速执行多模式
#4158 ### 新参数影响 IDENTITY 的起始和增量,使用 Sequence 已被弃用
从 SQL Server 2012 开始,SQL Server 现在支持具有真实CREATE SEQUENCE
语法的序列。在#4235中,SQLAlchemy 将添加对这些的支持,使用Sequence
方式与任何其他方言相同。然而,当前情况是,Sequence
已经在 SQL Server 上重新用途,以影响主键列上IDENTITY
规范的“start”和“increment”参数。为了使过渡向正常序列也可用,使用Sequence
将在整个 1.3 系列中发出弃用警告。为了影响“start”和“increment”,在Column
上使用新的mssql_identity_start
和mssql_identity_increment
参数:
test = Table(
"test",
metadata_obj,
Column(
"id",
Integer,
primary_key=True,
mssql_identity_start=100,
mssql_identity_increment=10,
),
Column("name", String(20)),
)
为了在非主键列上发出IDENTITY
,这是一个很少使用但有效的 SQL Server 用例,使用Column.autoincrement
标志,将其设置为True
在目标列上,False
在任何整数主键列上:
test = Table(
"test",
metadata_obj,
Column("id", Integer, primary_key=True, autoincrement=False),
Column("number", Integer, autoincrement=True),
)
另请参阅
自增行为 / IDENTITY 列
#4235 ### 支持 pyodbc fast_executemany
Pyodbc 最近添加的“fast_executemany”模式,在使用 Microsoft ODBC 驱动程序时可用,现在是 pyodbc / mssql 方言的选项。通过create_engine()
传递:
engine = create_engine(
"mssql+pyodbc://scott:tiger@mssql2017:1433/test?driver=ODBC+Driver+13+for+SQL+Server",
fast_executemany=True,
)
另请参阅
快速执行多模式
新参数影响 IDENTITY 的起始和增量,使用 Sequence 已被弃用
从 SQL Server 2012 开始,SQL Server 现在支持具有真实CREATE SEQUENCE
语法的序列。在#4235中,SQLAlchemy 将使用Sequence
来支持这些,方式与任何其他方言相同。然而,当前情况是,Sequence
已经在 SQL Server 上重新用途,以影响主键列上IDENTITY
规范的“start”和“increment”参数。为了使过渡向正常序列也可用,使用Sequence
将在整个 1.3 系列中发出弃用警告。为了影响“start”和“increment”,请在Column
上使用新的mssql_identity_start
和mssql_identity_increment
参数:
test = Table(
"test",
metadata_obj,
Column(
"id",
Integer,
primary_key=True,
mssql_identity_start=100,
mssql_identity_increment=10,
),
Column("name", String(20)),
)
为了在非主键列上发出IDENTITY
,这是一个很少使用但有效的 SQL Server 用例,可以使用Column.autoincrement
标志,在目标列上将其设置为True
,在任何整数主键列上将其设置为False
:
test = Table(
"test",
metadata_obj,
Column("id", Integer, primary_key=True, autoincrement=False),
Column("number", Integer, autoincrement=True),
)
另请参阅
自动增量行为 / IDENTITY 列
更改了 StatementError 的格式(换行和%s)
对StatementError
的字符串表示引入了两个更改。字符串表示的“detail”和“SQL”部分现在由换行符分隔,并保留了原始 SQL 语句中存在的换行符。目标是提高可读性,同时仍然保持原始错误消息在一行上以便于日志记录。
这意味着以前看起来像这样的错误消息:
sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError) A value is
required for bind parameter 'id' [SQL: 'select * from reviews\nwhere id = ?']
(Background on this error at: https://sqlalche.me/e/cd3x)
现在看起来像这样:
sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError) A value is required for bind parameter 'id'
[SQL: select * from reviews
where id = ?]
(Background on this error at: https://sqlalche.me/e/cd3x)
此更改的主要影响是消费者不能再假设完整的异常消息在单行上,但是从 DBAPI 驱动程序或 SQLAlchemy 内部生成的原始“error”部分仍将在第一行上。
SQLAlchemy 1.2 中的新内容是什么?
关于本文档
本文描述了 SQLAlchemy 1.1 版本与 SQLAlchemy 1.2 版本之间的更改。
简介
本指南介绍了 SQLAlchemy 版本 1.2 中的新功能,并记录了影响用户将其应用程序从 SQLAlchemy 1.1 系列迁移到 1.2 系列的更改。
请仔细查看行为更改部分,可能会出现不兼容的行为更改。
平台支持
针对 Python 2.7 及更高版本
SQLAlchemy 1.2 现在将最低 Python 版本提高到 2.7,不再支持 2.6。预计会将不支持 Python 2.6 的新语言特性合并到 1.2 系列中。对于 Python 3 的支持,SQLAlchemy 目前在版本 3.5 和 3.6 上进行了测试。
ORM 中的新功能和改进
“Baked” 加载现在是懒加载的默认设置
sqlalchemy.ext.baked
扩展是在 1.0 系列中首次引入的,允许构建所谓的 BakedQuery
对象,它是一个生成 Query
对象的对象,与表示查询结构的缓存键相结合;然后将此缓存键链接到生成的字符串 SQL 语句,以便后续使用具有相同结构的另一个 BakedQuery
将绕过构建 Query
对象的所有开销,构建内部的核心 select()
对象,以及 select()
编译为字符串,大大减少了通常与构建和发出 ORM Query
对象相关的函数调用开销。
当 ORM 为惰性加载一个relationship()
构造生成“懒惰”查询时,默认情况下现在使用BakedQuery
,例如默认的lazy="select"
关系加载器策略。这将允许在应用程序使用惰性加载查询加载集合和相关对象的范围内显著减少函数调用。以前,此功能在 1.0 和 1.1 中通过使用全局 API 方法或使用baked_select
策略可用,现在是此行为的唯一实现。该功能还得到了改进,以便对于在延迟加载后生效的具有附加加载器选项的对象仍然可以进行缓存。
可以通过relationship.bake_queries
标志在每个关系基础上禁用缓存行为,这对于非常罕见的情况非常有用,比如使用不兼容缓存的自定义Query
实现的关系。
#3954 ### 新的“selectin”急切加载,使用 IN 一次加载所有集合
添加了一个名为“selectin”加载的新急切加载器,这在许多方面类似于“子查询”加载,但是生成了一个更简单的 SQL 语句,该语句也可以缓存并且更有效。
给定如下查询:
q = (
session.query(User)
.filter(User.name.like("%ed%"))
.options(subqueryload(User.addresses))
)
生成的 SQL 将是针对User
的查询,然后是User.addresses
的子查询加载(注意还列出了参数):
SELECT users.id AS users_id, users.name AS users_name
FROM users
WHERE users.name LIKE ?
('%ed%',)
SELECT addresses.id AS addresses_id,
addresses.user_id AS addresses_user_id,
addresses.email_address AS addresses_email_address,
anon_1.users_id AS anon_1_users_id
FROM (SELECT users.id AS users_id
FROM users
WHERE users.name LIKE ?) AS anon_1
JOIN addresses ON anon_1.users_id = addresses.user_id
ORDER BY anon_1.users_id
('%ed%',)
使用“selectin”加载,我们得到一个 SELECT 语句,该语句引用在父查询中加载的实际主键值:
q = (
session.query(User)
.filter(User.name.like("%ed%"))
.options(selectinload(User.addresses))
)
产生:
SELECT users.id AS users_id, users.name AS users_name
FROM users
WHERE users.name LIKE ?
('%ed%',)
SELECT users_1.id AS users_1_id,
addresses.id AS addresses_id,
addresses.user_id AS addresses_user_id,
addresses.email_address AS addresses_email_address
FROM users AS users_1
JOIN addresses ON users_1.id = addresses.user_id
WHERE users_1.id IN (?, ?)
ORDER BY users_1.id
(1, 3)
上述 SELECT 语句包括以下优点:
-
它不使用子查询,只使用 INNER JOIN,这意味着在像 MySQL 这样不喜欢子查询的数据库上性能会更好。
-
其结构与原始查询无关;与新的扩展的 IN 参数系统结合,我们在大多数情况下可以使用“烘焙”查询来缓存字符串 SQL,从而显著减少每个查询的开销。
-
由于查询仅针对给定的主键标识符列表进行,"selectin" 加载可能与
Query.yield_per()
兼容,以便一次操作 SELECT 结果的一部分,前提是数据库驱动程序允许多个同时游标(SQLite、PostgreSQL;不是 MySQL 驱动程序或 SQL Server ODBC 驱动程序)。联接式急切加载和子查询急切加载都不兼容Query.yield_per()
。
selectin 急切加载的缺点是可能产生大量的 SQL 查询,具有大量的 IN 参数列表。IN 参数列表本身被分组为每组 500 个,因此超过 500 个主对象的结果集将有更多的额外“SELECT IN”查询。此外,对复合主键的支持取决于数据库能否使用包含 IN 的元组,例如 (table.column_one, table_column_two) IN ((?, ?), (?, ?) (?, ?))
。目前,已知 PostgreSQL 和 MySQL 兼容此语法,SQLite 不兼容。
另请参阅
选择 IN 加载
#3944 ### “selectin” 多态加载,使用单独的 IN 查询加载子类
与刚刚描述的“selectin”关系加载功能类似的是“selectin”多态加载。这是一个专门针对联接式急切加载的多态加载功能,允许基本实体的加载通过简单的 SELECT 语句进行,然后额外子类的属性通过额外的 SELECT 语句进行加载:
>>> from sqlalchemy.orm import selectin_polymorphic
>>> query = session.query(Employee).options(
... selectin_polymorphic(Employee, [Manager, Engineer])
... )
>>> query.all()
SELECT
employee.id AS employee_id,
employee.name AS employee_name,
employee.type AS employee_type
FROM employee
()
SELECT
engineer.id AS engineer_id,
employee.id AS employee_id,
employee.type AS employee_type,
engineer.engineer_name AS engineer_engineer_name
FROM employee JOIN engineer ON employee.id = engineer.id
WHERE employee.id IN (?, ?) ORDER BY employee.id
(1, 2)
SELECT
manager.id AS manager_id,
employee.id AS employee_id,
employee.type AS employee_type,
manager.manager_name AS manager_manager_name
FROM employee JOIN manager ON employee.id = manager.id
WHERE employee.id IN (?) ORDER BY employee.id
(3,)
另请参阅
使用 selectin_polymorphic()
#3948 ### ORM 属性可以接收临时 SQL 表达式
新的 ORM 属性类型 query_expression()
被添加,类似于 deferred()
,不同之处在于它的 SQL 表达式是在查询时确定的,使用了一个新选项 with_expression()
;如果未指定,则属性默认为 None
:
from sqlalchemy.orm import query_expression
from sqlalchemy.orm import with_expression
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
x = Column(Integer)
y = Column(Integer)
# will be None normally...
expr = query_expression()
# but let's give it x + y
a1 = session.query(A).options(with_expression(A.expr, A.x + A.y)).first()
print(a1.expr)
另请参阅
查询时 SQL 表达式作为映射属性
#3058 ### ORM 支持多表删除
ORM Query.delete()
方法支持多表条件的 DELETE,就像在支持多表条件的 DELETE 中介绍的那样。该功能的工作方式与在 0.8 中首次引入的 UPDATE 的多表条件相同,并在 Query.update()支持 UPDATE..FROM 中描述。
下面,我们对SomeEntity
执行 DELETE 操作,并添加一个 FROM 子句(或等效的,取决于后端)对SomeOtherEntity
进行操作:
query(SomeEntity).filter(SomeEntity.id == SomeOtherEntity.id).filter(
SomeOtherEntity.foo == "bar"
).delete()
另请参阅
支持多表条件的 DELETE
#959 ### 支持混合属性、复合属性的批量更新
现在混合属性(例如sqlalchemy.ext.hybrid
)以及复合属性(复合列类型)在使用Query.update()
时支持在 UPDATE 语句的 SET 子句中使用。
对于混合属性,可以直接使用简单表达式,或者可以使用新的装饰器hybrid_property.update_expression()
将一个值拆分为多个列/表达式:
class Person(Base):
# ...
first_name = Column(String(10))
last_name = Column(String(10))
@hybrid.hybrid_property
def name(self):
return self.first_name + " " + self.last_name
@name.expression
def name(cls):
return func.concat(cls.first_name, " ", cls.last_name)
@name.update_expression
def name(cls, value):
f, l = value.split(" ", 1)
return [(cls.first_name, f), (cls.last_name, l)]
上面,可以使用以下方式呈现 UPDATE:
session.query(Person).filter(Person.id == 5).update({Person.name: "Dr. No"})
类似的功能也适用于复合属性,其中复合值将被拆分为其各个列以进行批量 UPDATE:
session.query(Vertex).update({Edge.start: Point(3, 4)})
另请参阅
允许批量 ORM 更新 ### 混合属性支持在子类之间重用,重新定义@getter
sqlalchemy.ext.hybrid.hybrid_property
类现在支持在子类中多次调用诸如@setter
、@expression
等的变异器,并且现在提供了@getter
变异器,以便特定的混合属性可以在子类或其他类中重新使用。这与标准 Python 中@property
的行为类似:
class FirstNameOnly(Base):
# ...
first_name = Column(String)
@hybrid_property
def name(self):
return self.first_name
@name.setter
def name(self, value):
self.first_name = value
class FirstNameLastName(FirstNameOnly):
# ...
last_name = Column(String)
@FirstNameOnly.name.getter
def name(self):
return self.first_name + " " + self.last_name
@name.setter
def name(self, value):
self.first_name, self.last_name = value.split(" ", maxsplit=1)
@name.expression
def name(cls):
return func.concat(cls.first_name, " ", cls.last_name)
上面,FirstNameOnly.name
混合属性被FirstNameLastName
子类引用,以便将其专门用于新子类。这是通过在每次调用@getter
、@setter
以及所有其他变异器方法(如@expression
)中将混合对象复制到新对象中来实现的,从而保持先前混合属性的定义不变。以前,诸如@setter
的方法会直接修改现有的混合属性,干扰了超类的定义。
注意
请务必阅读在子类之间重用混合属性处的文档,了解如何覆盖hybrid_property.expression()
和hybrid_property.comparator()
的重要注意事项,因为在某些情况下可能需要一个特殊的限定符hybrid_property.overrides
来避免与QueryableAttribute
发生名称冲突。
注意
这种对@hybrid_property
的更改意味着,当向@hybrid_property
添加 setter 和其他状态时,方法必须保留原始混合的名称,否则具有附加状态的新混合将作为不匹配的名称存在于类中。这与标准 Python 的@property
构造的行为相同:
class FirstNameOnly(Base):
@hybrid_property
def name(self):
return self.first_name
# WRONG - will raise AttributeError: can't set attribute when
# assigning to .name
@name.setter
def _set_name(self, value):
self.first_name = value
class FirstNameOnly(Base):
@hybrid_property
def name(self):
return self.first_name
# CORRECT - note regular Python @property works the same way
@name.setter
def name(self, value):
self.first_name = value
#3912 ### 新的 bulk_replace 事件
为了适应 A @validates method receives all values on bulk-collection set before comparison 中描述的验证用例,添加了一个新的AttributeEvents.bulk_replace()
方法,该方法与AttributeEvents.append()
和AttributeEvents.remove()
事件一起调用。“bulk_replace”在“append”和“remove”之前调用,以便在与现有集合进行比较之前修改集合。之后,单个项目将附加到新的目标集合,触发对于集合中新项目的“append”事件,这与以前的行为相同。下面同时说明了“bulk_replace”和“append”,包括“append”将接收已由“bulk_replace”处理的对象(如果使用集合赋值)。一个新的符号attributes.OP_BULK_REPLACE
可用于确定此“append”事件是否是批量替换的第二部分:
from sqlalchemy.orm.attributes import OP_BULK_REPLACE
@event.listens_for(SomeObject.collection, "bulk_replace")
def process_collection(target, values, initiator):
values[:] = [_make_value(value) for value in values]
@event.listens_for(SomeObject.collection, "append", retval=True)
def process_collection(target, value, initiator):
# make sure bulk_replace didn't already do it
if initiator is None or initiator.op is not OP_BULK_REPLACE:
return _make_value(value)
else:
return value
#3896 ### 新的“modified”事件处理程序用于 sqlalchemy.ext.mutable
添加了新的事件处理程序AttributeEvents.modified()
,它与对flag_modified()
方法的调用对应,通常从sqlalchemy.ext.mutable
扩展调用:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy import event
Base = declarative_base()
class MyDataClass(Base):
__tablename__ = "my_data"
id = Column(Integer, primary_key=True)
data = Column(MutableDict.as_mutable(JSONEncodedDict))
@event.listens_for(MyDataClass.data, "modified")
def modified_json(instance):
print("json value modified:", instance.data)
上述情况下,当对.data
字典进行原地更改时,事件处理程序将被触发。
#3303 ### 在 Session.refresh 中添加了“for update”参数
为Session.refresh()
方法添加了新参数Session.refresh.with_for_update
。当Query.with_lockmode()
方法被弃用,而是采用Query.with_for_update()
时,Session.refresh()
方法从未更新以反映新选项:
session.refresh(some_object, with_for_update=True)
Session.refresh.with_for_update
参数接受一个选项字典,该字典将作为与Query.with_for_update()
发送的相同参数一样发送的参数:
session.refresh(some_objects, with_for_update={"read": True})
新参数取代了Session.refresh.lockmode
参数。
#3991 ### 原地突变操作符适用于 MutableSet、MutableList
为MutableSet
实现了原地突变操作符__ior__
、__iand__
、__ixor__
和__isub__
,以及MutableList
的__iadd__
。虽然这些方法以前可以成功更新集合,但它们不会正确地触发更改事件。这些操作符像以前一样突变集合,但额外地发出正确的更改事件,以便更改成为下一个刷新过程的一部分:
model = session.query(MyModel).first()
model.json_set &= {1, 3}
#3853 ### AssociationProxy 的 any()、has()、contains() 方法与链式关联代理一起工作
AssociationProxy.any()
、AssociationProxy.has()
和AssociationProxy.contains()
比较方法现在支持链接到一个属性,该属性本身也是AssociationProxy
,递归地。下面,A.b_values
是一个关联代理,链接到AtoB.bvalue
,而AtoB.bvalue
本身是一个关联代理,链接到B
:
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
b_values = association_proxy("atob", "b_value")
c_values = association_proxy("atob", "c_value")
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
value = Column(String)
c = relationship("C")
class C(Base):
__tablename__ = "c"
id = Column(Integer, primary_key=True)
b_id = Column(ForeignKey("b.id"))
value = Column(String)
class AtoB(Base):
__tablename__ = "atob"
a_id = Column(ForeignKey("a.id"), primary_key=True)
b_id = Column(ForeignKey("b.id"), primary_key=True)
a = relationship("A", backref="atob")
b = relationship("B", backref="atob")
b_value = association_proxy("b", "value")
c_value = association_proxy("b", "c")
我们可以使用AssociationProxy.contains()
在A.b_values
上进行查询,以跨两个代理A.b_values
、AtoB.b_value
进行查询:
>>> s.query(A).filter(A.b_values.contains("hi")).all()
SELECT a.id AS a_id
FROM a
WHERE EXISTS (SELECT 1
FROM atob
WHERE a.id = atob.a_id AND (EXISTS (SELECT 1
FROM b
WHERE b.id = atob.b_id AND b.value = :value_1)))
类似地,我们可以使用AssociationProxy.any()
在A.c_values
上进行查询,以跨两个代理A.c_values
、AtoB.c_value
进行查询:
>>> s.query(A).filter(A.c_values.any(value="x")).all()
SELECT a.id AS a_id
FROM a
WHERE EXISTS (SELECT 1
FROM atob
WHERE a.id = atob.a_id AND (EXISTS (SELECT 1
FROM b
WHERE b.id = atob.b_id AND (EXISTS (SELECT 1
FROM c
WHERE b.id = c.b_id AND c.value = :value_1)))))
#3769 ### 身份键增强以支持分片
现在 ORM 使用的身份键结构包含一个额外的成员,以便来自不同上下文的两个相同的主键可以共存于同一身份映射中。
水平分片中的示例已更新以说明这种行为。示例展示了一个分片类WeatherLocation
,引用一个依赖的WeatherReport
对象,其中WeatherReport
类映射到一个存储简单整数主键的表。来自不同数据库的两个WeatherReport
对象可能具有相同的主键值。该示例现在说明了一个新的identity_token
字段跟踪这种差异,以便这两个对象可以共存于同一身份映射中:
tokyo = WeatherLocation("Asia", "Tokyo")
newyork = WeatherLocation("North America", "New York")
tokyo.reports.append(Report(80.0))
newyork.reports.append(Report(75))
sess = create_session()
sess.add_all([tokyo, newyork, quito])
sess.commit()
# the Report class uses a simple integer primary key. So across two
# databases, a primary key will be repeated. The "identity_token" tracks
# in memory that these two identical primary keys are local to different
# databases.
newyork_report = newyork.reports[0]
tokyo_report = tokyo.reports[0]
assert inspect(newyork_report).identity_key == (Report, (1,), "north_america")
assert inspect(tokyo_report).identity_key == (Report, (1,), "asia")
# the token representing the originating shard is also available directly
assert inspect(newyork_report).identity_token == "north_america"
assert inspect(tokyo_report).identity_token == "asia"
新功能和改进 - 核心
布尔数据类型现在强制使用严格的 True/False/None 值
在 1.1 版本中,描述的更改将非本地布尔整数值强制转换为零/一/None 产生了一个意外的副作用,改变了当Boolean
遇到非整数值(如字符串)时的行为。特别是,先前会生成值False
的字符串值"0"
,现在会产生True
。更糟糕的是,行为的改变只针对某些后端而不是其他后端,这意味着将字符串"0"
值发送给Boolean
的代码在各个后端上会不一致地中断。
这个问题的最终解决方案是不支持字符串值与布尔值,因此在 1.2 版本中,如果传递了非整数/True/False/None 值,将引发严格的TypeError
。此外,只接受整数值 0 和 1。
为了适应希望对布尔值有更自由解释的应用程序,应使用TypeDecorator
。下面演示了一个配方,允许对 1.1 版本之前的Boolean
数据类型进行“自由”行为:
from sqlalchemy import Boolean
from sqlalchemy import TypeDecorator
class LiberalBoolean(TypeDecorator):
impl = Boolean
def process_bind_param(self, value, dialect):
if value is not None:
value = bool(int(value))
return value
#4102 ### 连接池中添加了悲观断开检测
连接池文档长期以来一直提供了一个使用ConnectionEvents.engine_connect()
引擎事件在检出的连接上发出简单语句以测试其活动性的方法。现在,当与适当的方言一起使用时,此配方的功能已添加到连接池本身中。使用新参数create_engine.pool_pre_ping
,每个检出的连接在返回之前都将被测试是否新鲜:
engine = create_engine("mysql+pymysql://", pool_pre_ping=True)
虽然“预先 ping”方法会在连接池检出时增加一点延迟,但对于典型的面向事务的应用程序(包括大多数 ORM 应用程序),这种开销是很小的,并且消除了获取到一个过时连接会引发错误的问题,需要应用程序放弃或重试操作。
该功能不适用于在进行中的事务或 SQL 操作中断开的连接。如果应用程序必须从这些错误中恢复,它需要使用自己的操作重试逻辑来预期这些错误。
另请参阅
断开处理 - 悲观
#3919 ### IN / NOT IN 运算符的空集合行为现在是可配置的;默认表达式简化了
诸如column.in_([])
这样的表达式,假定为 false,现在默认产生表达式1 != 1
,而不是column != column
。这将改变查询结果,比较 SQL 表达式或列与空集合时,产生一个布尔值 false 或 true(对于 NOT IN),而不是 NULL。在这种情况下发出的警告也被移除了。可以使用create_engine.empty_in_strategy
参数来create_engine()
获取旧的行为。
在 SQL 中,IN 和 NOT IN 运算符不支持与明确为空的值集合进行比较;也就是说,这种语法是非法的:
mycolumn IN ()
为了解决这个问题,SQLAlchemy 和其他数据库库检测到这种情况,并渲染一个替代表达式,该表达式评估为 false,或者在 NOT IN 的情况下评估为 true,基于“col IN ()”始终为 false 的理论,因为“空集合”中没有任何内容。通常,为了生成一个跨数据库可移植且在 WHERE 子句上下文中起作用的 false/true 常量,通常使用简单的重言式,如1 != 1
评估为 false,1 = 1
评估为 true(简单的常量“0”或“1”通常不能作为 WHERE 子句的目标)。
SQLAlchemy 在早期也采用了这种方法,但很快有人推测 SQL 表达式column IN ()
如果“column”为 NULL,则不会评估为 false;相反,该表达式会产生 NULL,因为“NULL”表示“未知”,在 SQL 中与 NULL 的比较通常产生 NULL。
为了模拟这个结果,SQLAlchemy 从使用1 != 1
改为使用表达式expr != expr
来处理空的“IN”,并使用expr = expr
来处理空的“NOT IN”;也就是说,我们使用表达式的实际左侧而不是固定值。如果传递的表达式左侧求值为 NULL,则整体比较结果也会得到 NULL 结果,而不是 false 或 true。
不幸的是,用户最终抱怨说这种表达式对一些查询规划器的性能影响非常严重。在那时,当遇到空的 IN 表达式时,会添加警告,建议 SQLAlchemy 继续保持“正确”,并敦促用户避免通常可以安全省略的生成空 IN 谓词的代码。然而,在动态构建查询的情况下,这当然会增加负担,因为输入变量的一组值可能为空。
最近几个月,这个决定的最初假设受到了质疑。表达式“NULL IN ()”应该返回 NULL 的想法只是理论上的,无法测试,因为数据库不支持该语法。然而,事实证明,实际上可以通过模拟空集合来询问关系数据库对于“NULL IN ()”会返回什么值:
SELECT NULL IN (SELECT 1 WHERE 1 != 1)
通过上述测试,我们看到数据库本身无法就答案达成一致。大多数人认为最“正确”的数据库 PostgreSQL 返回 False;因为即使“NULL”代表“未知”,“空集合”意味着没有任何内容,包括所有未知值。另一方面,MySQL 和 MariaDB 对上述表达式返回 NULL,采用更常见的“所有与 NULL 的比较都返回 NULL”的行为。
SQLAlchemy 的 SQL 架构比在做出此设计决定时更复杂,因此现在可以在 SQL 字符串编译时调用任一行为。以前,转换为比较表达式是在构造时完成的,也就是说,在调用ColumnOperators.in_()
或ColumnOperators.notin_()
运算符时。使用编译时行为,可以指示方言本身调用任一方法,即“static”1 != 1
比较或“dynamic”expr != expr
比较。默认已被更改为“static”比较,因为这与 PostgreSQL 在任何情况下的行为一致,这也是绝大多数用户喜欢的。这将改变查询结果,特别是将空表达式与空集进行比较的查询,特别是查询否定where(~null_expr.in_([]))
,因为现在这将评估为 true 而不是 NULL。
现在可以使用标志create_engine.empty_in_strategy
来控制行为,其默认设置为"static"
,但也可以设置为"dynamic"
或"dynamic_warn"
,其中"dynamic_warn"
设置等同于以前发出expr != expr
以及性能警告的行为。然而,预计大多数用户会喜欢"static"
默认设置。
#3907 ### 允许使用缓存语句的延迟扩展 IN 参数集合
添加了一种名为“expanding”的新类型bindparam()
。这用于在语句执行时将元素列表渲染为单独的绑定参数,而不是在语句编译时。这允许将单个绑定参数名称链接到多个元素的 IN 表达式,同时还允许使用查询缓存与 IN 表达式。这一新功能允许相关功能“select in”加载和“polymorphic in”加载利用烘焙查询扩展来减少调用开销:
stmt = select([table]).where(table.c.col.in_(bindparam("foo", expanding=True)))
conn.execute(stmt, {"foo": [1, 2, 3]})
该功能在 1.2 系列中应被视为实验性。
#3953 ### 压平比较运算符的运算符优先级
像 IN、LIKE、equals、IS、MATCH 和其他比较运算符的运算符优先级已经被压平到一个级别。当比较运算符组合在一起时,将生成更多的括号,例如:
(column("q") == null()) != (column("y") == null())
现在将生成(q IS NULL) != (y IS NULL)
而不是q IS NULL != y IS NULL
。
#3999 ### 支持在 Table、Column 上的 SQL 注释,包括 DDL、反射
Core 接收了与表和列关联的字符串注释的支持。这些通过Table.comment
和Column.comment
参数指定:
Table(
"my_table",
metadata,
Column("q", Integer, comment="the Q value"),
comment="my Q table",
)
上面的 DDL 将在表创建时适当地呈现,以将上述注释与模式中的表/列关联起来。当上述表被 autoload 或使用Inspector.get_columns()
检查时,注释将被包含在内。表注释也可以独立使用Inspector.get_table_comment()
方法获得。
当前后端支持包括 MySQL,PostgreSQL 和 Oracle。
#1546 ### 支持多表条件的 DELETE
Delete
构造现在支持多表条件,已在支持的后端实现,目前支持的后端有 PostgreSQL,MySQL 和 Microsoft SQL Server(对目前不工作的 Sybase 方言也添加了支持)。该功能的工作方式与 0.7 和 0.8 系列中首次引入的 UPDATE 的多表条件相同。
给定一个语句如下:
stmt = (
users.delete()
.where(users.c.id == addresses.c.id)
.where(addresses.c.email_address.startswith("ed%"))
)
conn.execute(stmt)
在 PostgreSQL 后端上,上述语句的生成 SQL 将呈现为:
DELETE FROM users USING addresses
WHERE users.id = addresses.id
AND (addresses.email_address LIKE %(email_address_1)s || '%%')
另请参阅
多表删除
#959 ### 新的“autoescape”选项用于 startswith(),endswith()
“autoescape”参数被添加到ColumnOperators.startswith()
,ColumnOperators.endswith()
,ColumnOperators.contains()
。当设置为True
时,此参数将自动转义所有出现的%
、_
,并使用默认的转义字符,默认为斜杠/
;转义字符本身的出现也会被转义。斜杠用于避免与诸如 PostgreSQL 的standard_confirming_strings
(从 PostgreSQL 9.1 开始默认值已更改)和 MySQL 的NO_BACKSLASH_ESCAPES
设置等设置发生冲突。现在可以使用现有的“escape”参数来更改自动转义字符,如果需要的话。
注意
从 1.2.0b2 的初始实现到 1.2.0,此功能已更改,现在 autoescape 被传递为布尔值,而不是用作转义字符的特定字符。
例如一个表达式:
>>> column("x").startswith("total%score", autoescape=True)
渲染为:
x LIKE :x_1 || '%' ESCAPE '/'
参数“x_1”的值为'total/%score'
。
同样,一个带有反斜杠的表达式:
>>> column("x").startswith("total/score", autoescape=True)
将以相同方式渲染,参数“x_1”的值为'total//score'
。
#2694 ### “float”数据类型的强类型化
一系列更改允许使用Float
数据类型更强烈地将自己与 Python 浮点值联系起来,而不是更通用的Numeric
。这些更改主要与确保 Python 浮点值不会错误地被强制转换为Decimal()
有关,并且在需要时被强制转换为float
,如果应用程序正在处理普通浮点数。
-
传递给 SQL 表达式的普通 Python“float”值现在将被拉入具有类型
Float
的文字参数;以前,类型为Numeric
,默认情况下“asdecimal=True”标志,这意味着结果类型将强制转换为Decimal()
。特别是,这将在 SQLite 上发出令人困惑的警告:float_value = connection.scalar( select([literal(4.56)]) # the "BindParameter" will now be # Float, not Numeric(asdecimal=True) )
-
在
Numeric
、Float
和Integer
之间的数学运算现在会保留结果表达式的类型,包括asdecimal
标志以及类型是否应该是Float
:# asdecimal flag is maintained expr = column("a", Integer) * column("b", Numeric(asdecimal=False)) assert expr.type.asdecimal == False # Float subclass of Numeric is maintained expr = column("a", Integer) * column("b", Float()) assert isinstance(expr.type, Float)
-
如果 DBAPI 已知支持本机
Decimal()
模式,则Float
数据类型将无条件地将float()
处理器应用于结果值。一些后端不总是保证浮点数以纯浮点数而不是精确数值(如 MySQL)的形式返回。
支持 GROUPING SETS、CUBE、ROLLUP
所有的 GROUPING SETS、CUBE、ROLLUP 都可以通过func
命名空间访问。在 CUBE 和 ROLLUP 的情况下,这些函数在之前的版本中已经可以使用,但是对于 GROUPING SETS,编译器中添加了一个占位符以便为其腾出空间。现在文档中已经命名了这三个函数:
>>> from sqlalchemy import select, table, column, func, tuple_
>>> t = table("t", column("value"), column("x"), column("y"), column("z"), column("q"))
>>> stmt = select([func.sum(t.c.value)]).group_by(
... func.grouping_sets(
... tuple_(t.c.x, t.c.y),
... tuple_(t.c.z, t.c.q),
... )
... )
>>> print(stmt)
SELECT sum(t.value) AS sum_1
FROM t GROUP BY GROUPING SETS((t.x, t.y), (t.z, t.q))
用于具有上下文默认生成器的多值插入的参数助手
默认生成函数,例如在上下文敏感默认函数中描述的函数,可以通过DefaultExecutionContext.current_parameters
属性查看与语句相关的当前参数。然而,在通过Insert.values()
方法指定多个 VALUES 子句的Insert
构造中,用户定义的函数会被多次调用,每个参数集一次,但是没有办法知道DefaultExecutionContext.current_parameters
中的哪些键子集适用于该列。添加了一个新函数DefaultExecutionContext.get_current_parameters()
,其中包括一个关键字参数DefaultExecutionContext.get_current_parameters.isolate_multiinsert_groups
默认为True
,它执行额外的工作,提供一个DefaultExecutionContext.current_parameters
的子字典,其中的名称被本地化为当前正在处理的 VALUES 子句:
def mydefault(context):
return context.get_current_parameters()["counter"] + 12
mytable = Table(
"mytable",
metadata_obj,
Column("counter", Integer),
Column("counter_plus_twelve", Integer, default=mydefault, onupdate=mydefault),
)
stmt = mytable.insert().values([{"counter": 5}, {"counter": 18}, {"counter": 20}])
conn.execute(stmt)
键行为更改 - ORM
在对象过期之前,after_rollback()
会话事件现在会发出
SessionEvents.after_rollback()
事件现在可以访问对象的属性状态,而不是在它们的状态被过期之前(例如,“快照删除”)。这使得该事件与SessionEvents.after_commit()
事件的行为保持一致,后者也会在“快照”被删除之前发出:
sess = Session()
user = sess.query(User).filter_by(name="x").first()
@event.listens_for(sess, "after_rollback")
def after_rollback(session):
# 'user.name' is now present, assuming it was already
# loaded. previously this would raise upon trying
# to emit a lazy load.
print("user name: %s" % user.name)
@event.listens_for(sess, "after_commit")
def after_commit(session):
# 'user.name' is present, assuming it was already
# loaded. this is the existing behavior.
print("user name: %s" % user.name)
if should_rollback:
sess.rollback()
else:
sess.commit()
请注意,Session
仍将禁止在此事件中发出 SQL;这意味着未加载的属性仍然无法在事件范围内加载。
#3934 ### 修复了与 select_from()
结合使用单表继承的问题
当生成 SQL 时,Query.select_from()
方法现在将遵循单表继承列鉴别器;以前,仅查询列列表中的表达式会被考虑进去。
假设 Manager
是 Employee
的子类。像以下这样的查询:
sess.query(Manager.id)
将生成的 SQL 如下:
SELECT employee.id FROM employee WHERE employee.type IN ('manager')
但是,如果仅在列列表中指定了 Manager
,而没有在 Query.select_from()
中指定,那么将不会添加鉴别器:
sess.query(func.count(1)).select_from(Manager)
将生成如下:
SELECT count(1) FROM employee
通过此修复,Query.select_from()
现在可以正确工作,我们可以得到:
SELECT count(1) FROM employee WHERE employee.type IN ('manager')
可能已经通过手动提供 WHERE 子句来解决此问题的应用程序可能需要进行调整。
#3891 ### 替换集合时,先前的集合不再发生变化
当映射的集合成员发生更改时,ORM 会发出事件。在将集合分配给将替换先前集合的属性时,这样做的副作用是被替换的集合也将被改变,这是误导性和不必要的:
>>> a1, a2, a3 = Address("a1"), Address("a2"), Address("a3")
>>> user.addresses = [a1, a2]
>>> previous_collection = user.addresses
# replace the collection with a new one
>>> user.addresses = [a2, a3]
>>> previous_collection
[Address('a1'), Address('a2')]
在上述更改之前,previous_collection
将已删除 “a1” 成员,对应于不再存在于新集合中的成员。
#3913 ### 在进行批量集合设置之前,@validates 方法接收所有值
在“批量设置”操作期间,使用 @validates
的方法现在将接收到集合的所有成员,然后再对现有集合进行比较。
给定映射如下:
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
bs = relationship("B")
@validates("bs")
def convert_dict_to_b(self, key, value):
return B(data=value["data"])
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
data = Column(String)
在上述情况中,我们可以按照以下方式使用验证器,在集合附加时将传入的字典转换为 B
的实例:
a1 = A()
a1.bs.append({"data": "b1"})
但是,集合赋值将失败,因为 ORM 将假定传入的对象已经是 B
的实例,因此在进行集合成员比较之前,它将尝试将它们与现有集合成员进行比较,然后执行实际调用验证器的集合附加操作。这将使得批量设置操作无法适应需要提前修改的非 ORM 对象,如需要提前修改的字典:
a1 = A()
a1.bs = [{"data": "b1"}]
新逻辑使用新的 AttributeEvents.bulk_replace()
事件确保所有值在开始时发送到 @validates
函数。
作为此更改的一部分,这意味着验证器现在将在批量设置时接收所有集合成员,而不仅仅是新成员。假设一个简单的验证器如下:
class A(Base):
# ...
@validates("bs")
def validate_b(self, key, value):
assert value.data is not None
return value
在上述情况下,如果我们从一个集合开始:
a1 = A()
b1, b2 = B(data="one"), B(data="two")
a1.bs = [b1, b2]
然后,用与第一个重叠的集合替换了该集合:
b3 = B(data="three")
a1.bs = [b2, b3]
以前,第二个赋值将仅触发一次 A.validate_b
方法,对于 b3
对象。b2
对象将被视为已经存在于集合中并且不受验证。采用新行为后,b2
和 b3
都会在传递到集合之前传递给 A.validate_b
。因此,验证方法必须采用幂等行为以适应这种情况。
另见
新的 bulk_replace 事件
#3896 ### 使用 flag_dirty() 将对象标记为“脏”,而不改变任何属性
如果使用 flag_modified()
函数标记一个实际未加载的属性为已修改,则现在会引发异常:
a1 = A(data="adf")
s.add(a1)
s.flush()
# expire, similarly as though we said s.commit()
s.expire(a1, "data")
# will raise InvalidRequestError
attributes.flag_modified(a1, "data")
这是因为如果属性在冲刷发生时仍然未出现,则刷新过程很可能无论如何都会失败。要将对象标记为“修改”,而不具体引用任何属性,以便在自定义事件处理程序(如 SessionEvents.before_flush()
)中考虑到刷新过程,请使用新的 flag_dirty()
函数:
from sqlalchemy.orm import attributes
attributes.flag_dirty(a1)
#3753 ### 从 scoped_session 中删除“scope”关键字
一个非常古老且未记录的关键字参数 scope
已被删除:
from sqlalchemy.orm import scoped_session
Session = scoped_session(sessionmaker())
session = Session(scope=None)
此关键字的目的是尝试允许可变“范围”,其中 None
表示“无范围”,因此将返回一个新的 Session
。此关键字从未被文档化,并且现在如果遇到将会引发 TypeError
。尽管不预期使用此关键字,但如果用户在测试期间报告与此相关的问题,则可以通过弃用来恢复。
#3796 ### 与 onupdate 结合使用的 post_update 的细化
使用 relationship.post_update
功能的关系现在将更好地与设置了 Column.onupdate
值的列进行交互。如果对象插入了列的显式值,则在更新期间重新声明它,以便“onupdate”规则不会覆盖它:
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
favorite_b_id = Column(ForeignKey("b.id", name="favorite_b_fk"))
bs = relationship("B", primaryjoin="A.id == B.a_id")
favorite_b = relationship(
"B", primaryjoin="A.favorite_b_id == B.id", post_update=True
)
updated = Column(Integer, onupdate=my_onupdate_function)
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id", name="a_fk"))
a1 = A()
b1 = B()
a1.bs.append(b1)
a1.favorite_b = b1
a1.updated = 5
s.add(a1)
s.flush()
上面,以前的行为是在 INSERT 之后发出 UPDATE,从而触发“onupdate”并覆盖值“5”。现在的 SQL 看起来像这样:
INSERT INTO a (favorite_b_id, updated) VALUES (?, ?)
(None, 5)
INSERT INTO b (a_id) VALUES (?)
(1,)
UPDATE a SET favorite_b_id=?, updated=? WHERE a.id = ?
(1, 5, 1)
此外,如果“updated”的值未设置,那么我们将会正确地在a1.updated
上获取到新生成的值;以前,刷新或使属性过期以允许生成的值出现的逻辑不会对 post-update 触发。在这种情况下,当刷新 flush 内发生时,也会触发InstanceEvents.refresh_flush()
事件。
#3472 ### post_update 与 ORM 版本控制集成
post_update 功能,文档中记录在指向自身的行 / 相互依赖的行,涉及到对特定与关系绑定的外键的更改而发出 UPDATE 语句,除了针对目标行通常会发出的 INSERT/UPDATE/DELETE。这个 UPDATE 语句现在参与版本控制功能,文档记录在配置版本计数器。
鉴于一个映射:
class Node(Base):
__tablename__ = "node"
id = Column(Integer, primary_key=True)
version_id = Column(Integer, default=0)
parent_id = Column(ForeignKey("node.id"))
favorite_node_id = Column(ForeignKey("node.id"))
nodes = relationship("Node", primaryjoin=remote(parent_id) == id)
favorite_node = relationship(
"Node", primaryjoin=favorite_node_id == remote(id), post_update=True
)
__mapper_args__ = {"version_id_col": version_id}
更新将另一个节点关联为“favorite”的节点现在也将增加版本计数器,并匹配当前版本:
node = Node()
session.add(node)
session.commit() # node is now version #1
node = session.query(Node).get(node.id)
node.favorite_node = Node()
session.commit() # node is now version #2
注意这意味着一个对象在响应其他属性变化而接收到 UPDATE,并且由于 post_update 关系变化而收到第二个 UPDATE,现在将会为一个 flush 接收到两次版本计数更新。然而,如果对象在当前 flush 内受到 INSERT,版本计数将不会额外增加一次,除非服务器端采用了版本控制方案。
现在讨论 post_update 即使对于 UPDATE 也会发出 UPDATE 的原因在为什么 post_update 除了第一个 UPDATE 之外还会发出 UPDATE?。
另请参阅
指向自身的行 / 相互依赖的行
为什么 post_update 除了第一个 UPDATE 之外还会发出 UPDATE?
关键行为更改 - 核心
自定义运算符的类型行为已经变得一致
可以使用Operators.op()
函数即时制作用户定义的运算符。以前,针对这样的运算符的表达式的类型行为是不一致的,也是不可控的。
而在 1.1 中,以下表达式将产生没有返回类型的结果(假设-%>
是数据库支持的某个特殊运算符):
>>> column("x", types.DateTime).op("-%>")(None).type
NullType()
其他类型将使用使用左侧类型作为返回类型的默认行为:
>>> column("x", types.String(50)).op("-%>")(None).type
String(length=50)
这些行为大多是偶然发生的,因此行为已经与第二种形式保持一致,即默认返回类型与左侧表达式相同:
>>> column("x", types.DateTime).op("-%>")(None).type
DateTime()
由于大多数用户定义的运算符往往是“比较”运算符,通常是由 PostgreSQL 定义的许多特殊运算符之一,Operators.op.is_comparison
标志已经修复,遵循其文档化行为,允许返回类型在所有情况下都是 Boolean
,包括对于 ARRAY
和 JSON
:
>>> column("x", types.String(50)).op("-%>", is_comparison=True)(None).type
Boolean()
>>> column("x", types.ARRAY(types.Integer)).op("-%>", is_comparison=True)(None).type
Boolean()
>>> column("x", types.JSON()).op("-%>", is_comparison=True)(None).type
Boolean()
为了辅助布尔比较运算符,新增了一个新的简写方法 Operators.bool_op()
。这个方法应该优先用于即时布尔运算符:
>>> print(column("x", types.Integer).bool_op("-%>")(5))
x -%> :x_1
``` ### literal_column() 中的百分号现在有条件地转义
`literal_column` 构造现在根据使用的 DBAPI 是否使用了百分号敏感的参数风格(例如‘format’或‘pyformat’)有条件地转义百分号字符。
以前,无法生成一个声明单个百分号的 `literal_column` 构造:
```py
>>> from sqlalchemy import literal_column
>>> print(literal_column("some%symbol"))
some%%symbol
百分号现在不受未设置为使用‘format’或‘pyformat’参数风格的方言的影响;大多数 MySQL 方言等声明了其中一个参数风格的方言将继续适当地转义:
>>> from sqlalchemy import literal_column
>>> print(literal_column("some%symbol"))
some%symbol
>>> from sqlalchemy.dialects import mysql
>>> print(literal_column("some%symbol").compile(dialect=mysql.dialect()))
some%%symbol
作为这一变化的一部分,使用像 ColumnOperators.contains()
、ColumnOperators.startswith()
和 ColumnOperators.endswith()
这样的运算符时,现在只在适当时才会发生加倍。
#3740 ### 列级别的 COLLATE 关键字现在引用排序规则名称
修复了在collate()
和ColumnOperators.collate()
函数中的一个错误,用于在语句级别提供临时列排序规则,其中区分大小写的名称不会被引用:
stmt = select([mytable.c.x, mytable.c.y]).order_by(
mytable.c.somecolumn.collate("fr_FR")
)
现在呈现为:
SELECT mytable.x, mytable.y,
FROM mytable ORDER BY mytable.somecolumn COLLATE "fr_FR"
以前,区分大小写的名称“fr_FR”不会被引用。目前,手动引用“fr_FR”名称不会被检测到,因此手动引用标识符的应用程序应进行调整。请注意,此更改不影响在类型级别使用排序规则(例如在数据类型上指定的String
在表级别),其中已经应用了引用。
方言改进和更改 - PostgreSQL
支持批处理模式 / 快速执行助手
已确定 psycopg2 的 cursor.executemany()
方法性能较差,特别是在 INSERT 语句中。为了缓解这一问题,psycopg2 添加了快速执行助手,通过将多个 DML 语句批量发送,将语句重新组织为更少的服务器往返次数。SQLAlchemy 1.2 现在包括对这些助手的支持,以便在 Engine
使用 cursor.executemany()
对多个参数集调用语句时,可以透明地使用这些助手。该功能默认关闭,可以通过在 create_engine()
上使用 use_batch_mode
参数来启用:
engine = create_engine(
"postgresql+psycopg2://scott:tiger@host/dbname", use_batch_mode=True
)
目前该功能被视为��验性质,但可能在将来的版本中默认开启。
另请参阅
Psycopg2 快速执行助手
#4109 ### 支持 INTERVAL 中字段规范的指定,包括完整反射
PostgreSQL 的 INTERVAL 数据类型中的“fields”规范允许指定要存储的间隔的字段,包括诸如“YEAR”、“MONTH”、“YEAR TO MONTH”等值。 INTERVAL
数据类型现在允许指定这些值:
from sqlalchemy.dialects.postgresql import INTERVAL
Table("my_table", metadata, Column("some_interval", INTERVAL(fields="DAY TO SECOND")))
此外,现在所有 INTERVAL 数据类型都可以独立于“fields”规范进行反射;数据类型本身中的“fields”参数也将存在:
>>> inspect(engine).get_columns("my_table")
[{'comment': None,
'name': u'some_interval', 'nullable': True,
'default': None, 'autoincrement': False,
'type': INTERVAL(fields=u'day to second')}]
方言改进和更改 - MySQL
支持 INSERT..ON DUPLICATE KEY UPDATE
MySQL 支持的 INSERT
的 ON DUPLICATE KEY UPDATE
子句现在可以使用 MySQL 特定版本的 Insert
对象来支持,通过 sqlalchemy.dialects.mysql.dml.insert()
。这个 Insert
子类添加了一个新方法 Insert.on_duplicate_key_update()
,实现了 MySQL 的语法:
from sqlalchemy.dialects.mysql import insert
insert_stmt = insert(my_table).values(id="some_id", data="some data to insert")
on_conflict_stmt = insert_stmt.on_duplicate_key_update(
data=insert_stmt.inserted.data, status="U"
)
conn.execute(on_conflict_stmt)
以上将呈现为:
INSERT INTO my_table (id, data)
VALUES (:id, :data)
ON DUPLICATE KEY UPDATE data=VALUES(data), status=:status_1
另请参阅
INSERT…ON DUPLICATE KEY UPDATE (Upsert)
方言改进和变更 - Oracle
cx_Oracle 方言、类型系统的重大重构
随着 cx_Oracle DBAPI 的 6.x 系列的引入,SQLAlchemy 的 cx_Oracle 方言已经重新设计和简化,以利用 cx_Oracle 的最新改进,并放弃了在 cx_Oracle 的 5.x 系列之前更相关的模式支持。
-
支持的最低 cx_Oracle 版本现在是 5.1.3;推荐使用 5.3 或最新的 6.x 系列。
-
数据类型的处理已经重构。根据 cx_Oracle 的开发人员建议,
cursor.setinputsizes()
方法不再用于除 LOB 类型之外的任何数据类型。因此,参数auto_setinputsizes
和exclude_setinputsizes
已被弃用,也不再起作用。 -
当将
coerce_to_decimal
标志设置为 False 以指示不应发生具有精度和标度的数值类型到Decimal
的强制转换时,仅影响未经类型化的语句(例如,没有TypeEngine
对象的普通字符串)。包含Numeric
类型或子类型的 Core 表达式现在将遵循该类型的十进制强制转换规则。 -
“两阶段”事务支持在方言中已经在 cx_Oracle 的 6.x 系列中被删除,现在已完全移除,因为这个功能从未正确工作过,也不太可能被投入生产使用。因此,
allow_twophase
方言标志已被弃用,也不再起作用。 -
修复了涉及带有 RETURNING 的列键的 bug。给定如下语句:
result = conn.execute(table.insert().values(x=5).returning(table.c.a, table.c.b))
以前,结果中每行的键将是
ret_0
和ret_1
,这是 cx_Oracle RETURNING 实现内部的标识符。现在键将是a
和b
,与其他方言的预期相符。 -
cx_Oracle 的 LOB 数据类型将返回值表示为
cx_Oracle.LOB
对象,这是一个与游标关联的代理,通过.read()
方法返回最终数据值。从历史上看,如果在消耗这些 LOB 对象之前读取了更多行(具体来说,读取了比 cursor.arraysize 值更多的行,这会导致读取新批次的行),这些 LOB 对象将引发错误“在后续获取后 LOB 变量不再有效”。SQLAlchemy 通过其类型系统自动调用这些 LOB 的.read()
,以及使用特殊的BufferedColumnResultSet
来解决这个问题,该结果集将确保在使用cursor.fetchmany()
或cursor.fetchall()
这样的调用时,这些数据被缓冲。方言现在使用 cx_Oracle outputtypehandler 来处理这些
.read()
调用,以便无论获取多少行,它们始终被提前调用,因此不再会发生此错误。因此,BufferedColumnResultSet
的使用,以及一些其他特定于此用例的 CoreResultSet
内部部分已被移除。由于类型对象不再需要处理二进制列结果,因此它们也变得更简化。此外,cx_Oracle 6.x 已删除了发生此错误的任何情况,因此不再可能发生错误。如果在使用极少(如果有的话)使用的
auto_convert_lobs=False
选项的情况下,与先前的 5.x 系列 cx_Oracle 结合使用,并且在 LOB 对象可以被消耗之前读取了更多行,则可能会在 SQLAlchemy 中发生此错误。升级到 cx_Oracle 6.x 将���决此问题。### Oracle Unique, Check 约束现在反映出来
UNIQUE 和 CHECK 约束现在通过Inspector.get_unique_constraints()
和 Inspector.get_check_constraints()
反映出来。被反映的Table
对象现在也将包括CheckConstraint
对象。有关此处行为怪癖的信息,请参阅约束反射,包括大多数Table
对象仍然不会包括任何UniqueConstraint
对象,因为这些通常通过Index
表示。
另请参见
约束反射
#4003 ### Oracle 外键约束名称现在是“名称标准化”
在表反射期间传递给 ForeignKeyConstraint
对象的外键约束名称以及在 Inspector.get_foreign_keys()
方法中,现在将被“名称标准化”,即,以小写形式表示以进行大小写不敏感的名称,而不是 Oracle 使用的原始大写格式:
>>> insp.get_indexes("addresses")
[{'unique': False, 'column_names': [u'user_id'],
'name': u'address_idx', 'dialect_options': {}}]
>>> insp.get_pk_constraint("addresses")
{'name': u'pk_cons', 'constrained_columns': [u'id']}
>>> insp.get_foreign_keys("addresses")
[{'referred_table': u'users', 'referred_columns': [u'id'],
'referred_schema': None, 'name': u'user_id_fk',
'constrained_columns': [u'user_id']}]
以前,外键结果看起来像:
[
{
"referred_table": "users",
"referred_columns": ["id"],
"referred_schema": None,
"name": "USER_ID_FK",
"constrained_columns": ["user_id"],
}
]
上述可能会特别与 Alembic autogenerate 创建问题。
方言改进和更改 - SQL Server
支持带有嵌入点的 SQL Server 模式名称
SQL Server 方言具有这样的行为,即假定具有其中一个点的模式名称是“数据库”。“所有者”标识符对,这在表和组件反射操作以及在呈现模式名称的引号时必须将这两个符号分开时会被分开。现在可以使用括号传递模式参数以手动指定此拆分发生的位置,从而允许数据库和/或所有者名称本身包含一个或多个点:
Table("some_table", metadata, Column("q", String(50)), schema="[MyDataBase.dbo]")
上表将考虑“所有者”为 MyDataBase.dbo
,在呈现时也将被引用,并且“数据库”为 None。要单独引用数据库名称和所有者,请使用两对括号:
Table(
"some_table",
metadata,
Column("q", String(50)),
schema="[MyDataBase.SomeDB].[MyDB.owner]",
)
此外,当传递给 SQL Server 方言的“模式”时,现在将尊重 quoted_name
构造;如果引号标志为 True,则给定的符号不会在点上拆分,并且将被解释为“所有者”。
另请参阅
多部分模式名称
AUTOCOMMIT 隔离级别支持
现在 PyODBC 和 pymssql 方言都支持由 Connection.execution_options()
设置的“AUTOCOMMIT”隔离级别,这将在 DBAPI 连接对象上建立正确的标志。
介绍
本指南介绍了 SQLAlchemy 版本 1.2 中的新功能,并记录了影响从 SQLAlchemy 1.1 系列迁移其应用程序的用户的更改。
请仔细查看行为变化部分,可能会对行为产生不兼容的变化。
平台支持
针对 Python 2.7 及更高版本
SQLAlchemy 1.2 现在将最低 Python 版本提升至 2.7,不再支持 2.6。预计将合并到 1.2 系列中的新语言特性在 Python 2.6 中不受支持。对于 Python 3 的支持,SQLAlchemy 目前在 3.5 和 3.6 版本上进行测试。
针对 Python 2.7 及更高版本
SQLAlchemy 1.2 现在将最低 Python 版本提升至 2.7,不再支持 2.6。预计将合并到 1.2 系列中的新语言特性在 Python 2.6 中不受支持。对于 Python 3 的支持,SQLAlchemy 目前在 3.5 和 3.6 版本上进行测试。
新功能和改进 - ORM
“Baked” 加载现在是懒加载的默认选项
sqlalchemy.ext.baked
扩展首次引入于 1.0 系列,允许构建所谓的BakedQuery
对象,该对象与表示查询结构的缓存键一起生成Query
对象;然后将此缓存键链接到生成的字符串 SQL 语句,以便后续使用具有相同结构的另一个BakedQuery
将绕过构建Query
对象、构建其中的核心select()
对象,以及将select()
编译为字符串的所有开销,从而削减通常与构建和发出 ORM Query
对象相关的大部分函数调用开销。
BakedQuery
现在在 ORM 默认情况下用于生成“延迟”查询,用于懒加载relationship()
构造,例如默认的lazy="select"
关系加载策略。这将显著减少应用程序在使用懒加载查询加载集合和相关对象时的函数调用。此功能以前在 1.0 和 1.1 中通过使用全局 API 方法或使用baked_select
策略可用,现在是此行为的唯一实现。该功能还得到改进,使得对于具有懒加载后生效的其他加载器选项的对象仍然可以进行缓存。
可以使用 relationship.bake_queries
标志在每个关系基础上禁用缓存行为,这对于非常罕见的情况非常有用,例如使用不兼容缓存的自定义 Query
实现的关系。
#3954 ### 新的“selectin”急加载,一次性使用 IN 加载所有集合
添加了一个名为“selectin”加载的新急加载器,这在许多方面类似于“子查询”加载,但生成的 SQL 语句更简单,可缓存且更高效。
给定以下查询:
q = (
session.query(User)
.filter(User.name.like("%ed%"))
.options(subqueryload(User.addresses))
)
生成的 SQL 将是针对 User
的查询,然后是 User.addresses
的 subqueryload(请注意还列出了参数):
SELECT users.id AS users_id, users.name AS users_name
FROM users
WHERE users.name LIKE ?
('%ed%',)
SELECT addresses.id AS addresses_id,
addresses.user_id AS addresses_user_id,
addresses.email_address AS addresses_email_address,
anon_1.users_id AS anon_1_users_id
FROM (SELECT users.id AS users_id
FROM users
WHERE users.name LIKE ?) AS anon_1
JOIN addresses ON anon_1.users_id = addresses.user_id
ORDER BY anon_1.users_id
('%ed%',)
使用“selectin”加载,我们实际上得到了一个引用父查询中加载的实际主键值的 SELECT:
q = (
session.query(User)
.filter(User.name.like("%ed%"))
.options(selectinload(User.addresses))
)
产生:
SELECT users.id AS users_id, users.name AS users_name
FROM users
WHERE users.name LIKE ?
('%ed%',)
SELECT users_1.id AS users_1_id,
addresses.id AS addresses_id,
addresses.user_id AS addresses_user_id,
addresses.email_address AS addresses_email_address
FROM users AS users_1
JOIN addresses ON users_1.id = addresses.user_id
WHERE users_1.id IN (?, ?)
ORDER BY users_1.id
(1, 3)
上述 SELECT 语句包括以下优点:
-
它不使用子查询,只是一个 INNER JOIN,这意味着在像 MySQL 这样不喜欢子查询的数据库上性能会更好
-
其结构独立于原始查询;与新的 扩展 IN 参数系统 结合使用,我们在大多数情况下可以使用“烘焙”查询来缓存字符串 SQL,从而显著减少每个查询的开销。
-
因为查询仅获取给定主键标识符列表,“selectin”加载可能与
Query.yield_per()
兼容,以便一次处理 SELECT 结果的块,前提是数据库驱动程序允许多个同时游标(SQLite、PostgreSQL;不是 MySQL 驱动程序或 SQL Server ODBC 驱动程序)。联接急加载和子查询急加载都不兼容Query.yield_per()
。
选择急加载的缺点可能是潜在的大型 SQL 查询,带有大量的 IN 参数列表。 IN 参数列表本身被分组为 500 个一组,因此超过 500 个结果对象的结果集将有更多额外的“SELECT IN”查询。此外,对复合主键的支持取决于数据库是否能够使用带有 IN 的元组,例如 (table.column_one, table_column_two) IN ((?, ?), (?, ?) (?, ?))
。目前,已知 PostgreSQL 和 MySQL 兼容此语法,而 SQLite 不兼容。
另请参见
Select IN 加载
#3944 ### “selectin” 多态加载,使用单独的 IN 查询加载子类
与刚刚在新“selectin” eager loading, loads all collections at once using IN 中描述的“selectin”关系加载功能类似的是“selectin”多态加载。这是一种专为连接式的急加载定制的多态加载功能,允许基本实体的加载通过简单的 SELECT 语句进行,但是额外的子类属性使用额外的 SELECT 语句加载:
>>> from sqlalchemy.orm import selectin_polymorphic
>>> query = session.query(Employee).options(
... selectin_polymorphic(Employee, [Manager, Engineer])
... )
>>> query.all()
SELECT
employee.id AS employee_id,
employee.name AS employee_name,
employee.type AS employee_type
FROM employee
()
SELECT
engineer.id AS engineer_id,
employee.id AS employee_id,
employee.type AS employee_type,
engineer.engineer_name AS engineer_engineer_name
FROM employee JOIN engineer ON employee.id = engineer.id
WHERE employee.id IN (?, ?) ORDER BY employee.id
(1, 2)
SELECT
manager.id AS manager_id,
employee.id AS employee_id,
employee.type AS employee_type,
manager.manager_name AS manager_manager_name
FROM employee JOIN manager ON employee.id = manager.id
WHERE employee.id IN (?) ORDER BY employee.id
(3,)
另请参阅
使用 selectin_polymorphic()
#3948 ### 可接收临时 SQL 表达式的 ORM 属性
新增了一个 ORM 属性类型query_expression()
,与deferred()
类似,但其 SQL 表达式在查询时确定,使用新选项with_expression()
;如果未指定,属性默认为None
:
from sqlalchemy.orm import query_expression
from sqlalchemy.orm import with_expression
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
x = Column(Integer)
y = Column(Integer)
# will be None normally...
expr = query_expression()
# but let's give it x + y
a1 = session.query(A).options(with_expression(A.expr, A.x + A.y)).first()
print(a1.expr)
另请参阅
查询时的 SQL 表达式作为映射属性
#3058 ### ORM 支持多表删除
ORM Query.delete()
方法支持多表条件的删除,如多表条件支持的删除中所介绍的。该功能的工作方式与更新的多表条件相同,最初在 0.8 版本中引入,并在 Query.update()支持 UPDATE..FROM 中描述。
下面,我们对SomeEntity
发出一个 DELETE 请求,添加一个 FROM 子句(或者等效的,根据后端不同)对SomeOtherEntity
进行操作:
query(SomeEntity).filter(SomeEntity.id == SomeOtherEntity.id).filter(
SomeOtherEntity.foo == "bar"
).delete()
另请参阅
多表条件支持的删除
#959 ### 支持混合、复合的批量更新
现在,混合属性(例如sqlalchemy.ext.hybrid
)以及复合属性(复合列类型)在使用Query.update()
更新语句的 SET 子句中均得到支持。
对于混合属性,可以直接使用简单的表达式,或者可以使用新的装饰器hybrid_property.update_expression()
将值拆分为多个列/表达式:
class Person(Base):
# ...
first_name = Column(String(10))
last_name = Column(String(10))
@hybrid.hybrid_property
def name(self):
return self.first_name + " " + self.last_name
@name.expression
def name(cls):
return func.concat(cls.first_name, " ", cls.last_name)
@name.update_expression
def name(cls, value):
f, l = value.split(" ", 1)
return [(cls.first_name, f), (cls.last_name, l)]
如上所述,可以使用以下方式渲染 UPDATE:
session.query(Person).filter(Person.id == 5).update({Person.name: "Dr. No"})
类似的功能也适用于复合属性,其中复合值将被拆分为其各个列以进行批量更新:
session.query(Vertex).update({Edge.start: Point(3, 4)})
另请参阅
允许批量 ORM 更新 ### 混合属性支持在子类之间重用,重新定义@getter
sqlalchemy.ext.hybrid.hybrid_property
类现在支持在子类之间多次调用像@setter
、@expression
等的变异器,并且现在提供了一个@getter
变异器,以便特定的混合属性可以在子类或其他类之间重新使用。这与标准 Python 中@property
的行为类似:
class FirstNameOnly(Base):
# ...
first_name = Column(String)
@hybrid_property
def name(self):
return self.first_name
@name.setter
def name(self, value):
self.first_name = value
class FirstNameLastName(FirstNameOnly):
# ...
last_name = Column(String)
@FirstNameOnly.name.getter
def name(self):
return self.first_name + " " + self.last_name
@name.setter
def name(self, value):
self.first_name, self.last_name = value.split(" ", maxsplit=1)
@name.expression
def name(cls):
return func.concat(cls.first_name, " ", cls.last_name)
在上面的示例中,FirstNameOnly.name
混合属性被FirstNameLastName
子类引用,以便将其专门用于新子类。这是通过在每次调用@getter
、@setter
以及所有其他变异器方法像@expression
中复制混合对象到新对象来实现的,从而保持先前混合属性的定义不变。以前,像@setter
这样的方法会直接修改现有的混合属性,干扰了超类上的定义。
注意
请务必阅读在子类之间重用混合属性的文档,了解如何覆盖hybrid_property.expression()
和hybrid_property.comparator()
的重要注意事项,因为在某些情况下,可能需要使用特殊限定符hybrid_property.overrides
来避免与QueryableAttribute
发生名称冲突。
注意
@hybrid_property
中的这种变化意味着,当向@hybrid_property
添加 setter 和其他状态时,方法必须保留原始混合属性的名称,否则具有附加状态的新混合属性将以不匹配的名称存在于类中。这与标准 Python 中的@property
构造的行为相同:
class FirstNameOnly(Base):
@hybrid_property
def name(self):
return self.first_name
# WRONG - will raise AttributeError: can't set attribute when
# assigning to .name
@name.setter
def _set_name(self, value):
self.first_name = value
class FirstNameOnly(Base):
@hybrid_property
def name(self):
return self.first_name
# CORRECT - note regular Python @property works the same way
@name.setter
def name(self, value):
self.first_name = value
#3912 ### 新的 bulk_replace 事件
为了适应 A @validates method receives all values on bulk-collection set before comparison 中描述的验证用例,添加了一个新的AttributeEvents.bulk_replace()
方法,该方法与AttributeEvents.append()
和AttributeEvents.remove()
事件一起调用。在比较现有集合之前调用“bulk_replace”,以便可以修改集合。之后,单个项目将附加到新的目标集合,触发为集合中的新项目触发的“append”事件,这与以前的行为相同。下面同时说明了“bulk_replace”和“append”,包括如果使用集合赋值,“append”将接收到已由“bulk_replace”处理的对象。新符号attributes.OP_BULK_REPLACE
可以用于确定此“append”事件是否是批量替换的第二部分:
from sqlalchemy.orm.attributes import OP_BULK_REPLACE
@event.listens_for(SomeObject.collection, "bulk_replace")
def process_collection(target, values, initiator):
values[:] = [_make_value(value) for value in values]
@event.listens_for(SomeObject.collection, "append", retval=True)
def process_collection(target, value, initiator):
# make sure bulk_replace didn't already do it
if initiator is None or initiator.op is not OP_BULK_REPLACE:
return _make_value(value)
else:
return value
#3896 ### 为 sqlalchemy.ext.mutable 添加了新的“modified”事件处理程序
添加了一个新的事件处理程序AttributeEvents.modified()
,该处理程序在对flag_modified()
方法的调用时触发,通常是从sqlalchemy.ext.mutable
扩展调用的。
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy import event
Base = declarative_base()
class MyDataClass(Base):
__tablename__ = "my_data"
id = Column(Integer, primary_key=True)
data = Column(MutableDict.as_mutable(JSONEncodedDict))
@event.listens_for(MyDataClass.data, "modified")
def modified_json(instance):
print("json value modified:", instance.data)
上述事件处理程序将在对.data
字典进行就地更改时触发。
#3303 ### 添加了对 Session.refresh 的“for update”参数
添加了新参数Session.refresh.with_for_update
到Session.refresh()
方法。当Query.with_lockmode()
方法被弃用,而倾向于Query.with_for_update()
时,Session.refresh()
方法从未更新以反映新选项:
session.refresh(some_object, with_for_update=True)
Session.refresh.with_for_update
参数现在接受一个选项字典,该字典将作为发送给Query.with_for_update()
的相同参数:
session.refresh(some_objects, with_for_update={"read": True})
新参数取代了Session.refresh.lockmode
参数。
#3991 ### 就地变异操作符适用于 MutableSet、MutableList
对于MutableSet
,我们实现了就地变异操作符__ior__
、__iand__
、__ixor__
和__isub__
,以及对于MutableList
的__iadd__
。虽然这些方法以前可以成功地更新集合,但它们不会正确地触发更改事件。这些操作符像以前一样改变集合,但额外地发出了正确的更改事件,以便更改成为下一个刷新进程的一部分:
model = session.query(MyModel).first()
model.json_set &= {1, 3}
#3853 ### AssociationProxy any()、has()、contains()可以与链式关联代理一起使用
AssociationProxy.any()
、AssociationProxy.has()
和AssociationProxy.contains()
比较方法现在支持链接到一个属性,该属性本身也是一个AssociationProxy
,递归地。在下面的示例中,A.b_values
是一个关联到AtoB.bvalue
的关联代理,它本身是一个关联到B
的关联代理:
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
b_values = association_proxy("atob", "b_value")
c_values = association_proxy("atob", "c_value")
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
value = Column(String)
c = relationship("C")
class C(Base):
__tablename__ = "c"
id = Column(Integer, primary_key=True)
b_id = Column(ForeignKey("b.id"))
value = Column(String)
class AtoB(Base):
__tablename__ = "atob"
a_id = Column(ForeignKey("a.id"), primary_key=True)
b_id = Column(ForeignKey("b.id"), primary_key=True)
a = relationship("A", backref="atob")
b = relationship("B", backref="atob")
b_value = association_proxy("b", "value")
c_value = association_proxy("b", "c")
我们可以使用AssociationProxy.contains()
在A.b_values
上进行查询,以跨越两个代理A.b_values
,AtoB.b_value
:
>>> s.query(A).filter(A.b_values.contains("hi")).all()
SELECT a.id AS a_id
FROM a
WHERE EXISTS (SELECT 1
FROM atob
WHERE a.id = atob.a_id AND (EXISTS (SELECT 1
FROM b
WHERE b.id = atob.b_id AND b.value = :value_1)))
类似地,我们可以使用AssociationProxy.any()
在A.c_values
上进行查询,以跨越两个代理A.c_values
,AtoB.c_value
:
>>> s.query(A).filter(A.c_values.any(value="x")).all()
SELECT a.id AS a_id
FROM a
WHERE EXISTS (SELECT 1
FROM atob
WHERE a.id = atob.a_id AND (EXISTS (SELECT 1
FROM b
WHERE b.id = atob.b_id AND (EXISTS (SELECT 1
FROM c
WHERE b.id = c.b_id AND c.value = :value_1)))))
#3769 ### 标识键增强以支持分片
现在,ORM 使用的标识键结构包含了一个额外的成员,以便来自不同上下文的两个相同的主键可以共存于同一个标识映射中。
水平分片 中的示例已更新以说明此行为。该示例显示了一个分片类 WeatherLocation
,引用一个依赖的 WeatherReport
对象,其中 WeatherReport
类映射到一个存储简单整数主键的表。来自不同数据库的两个 WeatherReport
对象可能具有相同的主键值。该示例现在说明了一个新的 identity_token
字段跟踪此差异,以便这两个对象可以共存于同一标识映射中:
tokyo = WeatherLocation("Asia", "Tokyo")
newyork = WeatherLocation("North America", "New York")
tokyo.reports.append(Report(80.0))
newyork.reports.append(Report(75))
sess = create_session()
sess.add_all([tokyo, newyork, quito])
sess.commit()
# the Report class uses a simple integer primary key. So across two
# databases, a primary key will be repeated. The "identity_token" tracks
# in memory that these two identical primary keys are local to different
# databases.
newyork_report = newyork.reports[0]
tokyo_report = tokyo.reports[0]
assert inspect(newyork_report).identity_key == (Report, (1,), "north_america")
assert inspect(tokyo_report).identity_key == (Report, (1,), "asia")
# the token representing the originating shard is also available directly
assert inspect(newyork_report).identity_token == "north_america"
assert inspect(tokyo_report).identity_token == "asia"
#4137 ### “烘焙”加载现在是延迟加载的默认设置
sqlalchemy.ext.baked
扩展首次在 1.0 系列中引入,允许构建所谓的 BakedQuery
对象,该对象生成一个与表示查询结构的缓存键相关联的 Query
对象;然后将此缓存键链接到生成的字符串 SQL 语句,以便后续使用具有相同结构的另一个 BakedQuery
将绕过构建 Query
对象的所有开销,构建其中的核心 select()
对象,以及将 select()
编译为字符串,从而削减通常与构建和发出 ORM Query
对象相关的大部分函数调用开销。
当 ORM 生成“延迟”查询以懒加载 relationship()
构造时,默认现在使用 BakedQuery
,例如默认的 lazy="select"
关系加载器策略。这将显著减少应用程序使用延迟加载查询加载集合和相关对象时的函数调用数量。以前,此功能在 1.0 和 1.1 中通过使用全局 API 方法或使用 baked_select
策略可用,现在是此行为的唯一实现。该功能还得到改进,使得对于具有延迟加载后生效的其他加载器选项的对象仍然可以进行缓存。
可以使用 relationship.bake_queries
标志在每个关系基础上禁用缓存行为,这对于非常不寻常的情况非常有用,例如使用不兼容缓存的自定义 Query
实现的关系。
新的 “selectin” 急切加载,一次加载所有集合使用 IN
添加了一个名为 “selectin” 加载的新急切加载器,从许多方面来看,它类似于 “subquery” 加载,但是生成了一个更简单的可缓存的 SQL 语句,而且更有效率。
给定如下查询:
q = (
session.query(User)
.filter(User.name.like("%ed%"))
.options(subqueryload(User.addresses))
)
生成的 SQL 将是针对 User
的查询,然后是 User.addresses
的 subqueryload(注意还列出了参数):
SELECT users.id AS users_id, users.name AS users_name
FROM users
WHERE users.name LIKE ?
('%ed%',)
SELECT addresses.id AS addresses_id,
addresses.user_id AS addresses_user_id,
addresses.email_address AS addresses_email_address,
anon_1.users_id AS anon_1_users_id
FROM (SELECT users.id AS users_id
FROM users
WHERE users.name LIKE ?) AS anon_1
JOIN addresses ON anon_1.users_id = addresses.user_id
ORDER BY anon_1.users_id
('%ed%',)
使用 “selectin” 加载,我们得到���是一个 SELECT,它引用了在父查询中加载的实际主键值:
q = (
session.query(User)
.filter(User.name.like("%ed%"))
.options(selectinload(User.addresses))
)
产生:
SELECT users.id AS users_id, users.name AS users_name
FROM users
WHERE users.name LIKE ?
('%ed%',)
SELECT users_1.id AS users_1_id,
addresses.id AS addresses_id,
addresses.user_id AS addresses_user_id,
addresses.email_address AS addresses_email_address
FROM users AS users_1
JOIN addresses ON users_1.id = addresses.user_id
WHERE users_1.id IN (?, ?)
ORDER BY users_1.id
(1, 3)
上述 SELECT 语句包括以下优点:
-
它不使用子查询,只是一个 INNER JOIN,这意味着在像 MySQL 这样不喜欢子查询的数据库上性能会更好
-
其结构独立于原始查询;与新的 扩展 IN 参数系统 结合,我们在大多数情况下可以使用 “baked” 查询来缓存字符串 SQL,显著减少每个查询的开销
-
由于查询仅为给定的主键标识符列表获取数据,“selectin” 加载可能与
Query.yield_per()
兼容,以便一次操作 SELECT 结果的一部分,前提是数据库驱动程序允许多个同时游标(SQLite,PostgreSQL;不是 MySQL 驱动程序或 SQL Server ODBC 驱动程序)。联接式急切加载和子查询急切加载都不兼容Query.yield_per()
。
selectin 急切加载的缺点是潜在的大型 SQL 查询,具有大量的 IN 参数列表。 IN 参数列表本身被分组为 500 个一组,因此超过 500 个 lead 对象的结果集将有更多的附加 “SELECT IN” 查询。此外,对复合主键的支持取决于数据库是否能够使用带有 IN 的元组,例如 (table.column_one, table_column_two) IN ((?, ?), (?, ?) (?, ?))
。目前,已知 PostgreSQL 和 MySQL 兼容此语法,SQLite 不兼容。
另请参见
选择 IN 加载
“selectin” 多态加载,使用单独的 IN 查询加载子类
与刚刚在新的“selectin”急加载,使用 IN 一次加载所有集合中描述的“selectin”关系加载功能类似的是“selectin”多态加载。这是一个主要针对连接式急加载的多态加载功能,允许基本实体的加载通过简单的 SELECT 语句进行,然后额外子类的属性通过额外的 SELECT 语句进行加载:
>>> from sqlalchemy.orm import selectin_polymorphic
>>> query = session.query(Employee).options(
... selectin_polymorphic(Employee, [Manager, Engineer])
... )
>>> query.all()
SELECT
employee.id AS employee_id,
employee.name AS employee_name,
employee.type AS employee_type
FROM employee
()
SELECT
engineer.id AS engineer_id,
employee.id AS employee_id,
employee.type AS employee_type,
engineer.engineer_name AS engineer_engineer_name
FROM employee JOIN engineer ON employee.id = engineer.id
WHERE employee.id IN (?, ?) ORDER BY employee.id
(1, 2)
SELECT
manager.id AS manager_id,
employee.id AS employee_id,
employee.type AS employee_type,
manager.manager_name AS manager_manager_name
FROM employee JOIN manager ON employee.id = manager.id
WHERE employee.id IN (?) ORDER BY employee.id
(3,)
另请参阅
使用 selectin_polymorphic()
可接收临时 SQL 表达式的 ORM 属性
新的 ORM 属性类型query_expression()
被添加,类似于deferred()
,不同之处在于其 SQL 表达式在查询时使用新选项with_expression()
确定;如果未指定,则属性默认为None
:
from sqlalchemy.orm import query_expression
from sqlalchemy.orm import with_expression
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
x = Column(Integer)
y = Column(Integer)
# will be None normally...
expr = query_expression()
# but let's give it x + y
a1 = session.query(A).options(with_expression(A.expr, A.x + A.y)).first()
print(a1.expr)
另请参阅
查询时 SQL 表达式作为映射属性
ORM 支持多表删除
ORM Query.delete()
方法支持多表条件的 DELETE,就像在支持多表条件的 DELETE 中介绍的那样。该功能与 0.8 中首次引入的 UPDATE 的多表条件相同,详细描述在 Query.update()支持 UPDATE..FROM 中。
下面,我们对SomeEntity
执行一个 DELETE 操作,添加一个 FROM 子句(或等效的,取决于后端)对SomeOtherEntity
:
query(SomeEntity).filter(SomeEntity.id == SomeOtherEntity.id).filter(
SomeOtherEntity.foo == "bar"
).delete()
另请参阅
支持多表条件的 DELETE
支持混合属性,复合属性的批量更新
混合属性(例如sqlalchemy.ext.hybrid
)以及复合属性(复合列类型)现在都支持在使用Query.update()
时用于 UPDATE 语句的 SET 子句中。
对于混合属性,可以直接使用简单表达式,或者可以使用新的装饰器hybrid_property.update_expression()
将一个值分解为多个列/表达式:
class Person(Base):
# ...
first_name = Column(String(10))
last_name = Column(String(10))
@hybrid.hybrid_property
def name(self):
return self.first_name + " " + self.last_name
@name.expression
def name(cls):
return func.concat(cls.first_name, " ", cls.last_name)
@name.update_expression
def name(cls, value):
f, l = value.split(" ", 1)
return [(cls.first_name, f), (cls.last_name, l)]
上面,一个 UPDATE 可以使用以下方式呈现:
session.query(Person).filter(Person.id == 5).update({Person.name: "Dr. No"})
类似的功能也适用于复合类型,其中复合值将被拆分为其各个列以进行批量更新:
session.query(Vertex).update({Edge.start: Point(3, 4)})
另请参阅
允许批量 ORM 更新
混合属性支持在子类之间重用,重新定义 @getter
sqlalchemy.ext.hybrid.hybrid_property
类现在支持在子类之间多次调用修改器,如 @setter
、@expression
等,并且现在提供了一个 @getter
修改器,以便可以在子类或其他类之间重新用特定的混合属性。这与标准 Python 中 @property
的行为类似:
class FirstNameOnly(Base):
# ...
first_name = Column(String)
@hybrid_property
def name(self):
return self.first_name
@name.setter
def name(self, value):
self.first_name = value
class FirstNameLastName(FirstNameOnly):
# ...
last_name = Column(String)
@FirstNameOnly.name.getter
def name(self):
return self.first_name + " " + self.last_name
@name.setter
def name(self, value):
self.first_name, self.last_name = value.split(" ", maxsplit=1)
@name.expression
def name(cls):
return func.concat(cls.first_name, " ", cls.last_name)
在上面的例子中,FirstNameOnly.name
混合属性被 FirstNameLastName
子类引用,以便将其专门重新用于新的子类。这是通过在每次调用 @getter
、@setter
以及所有其他修改器方法(如 @expression
)中将混合对象复制到一个新对象中来实现的,从而保持先前混合属性的定义不变。以前,像 @setter
这样的方法会就地修改现有的混合属性,干扰了超类上的定义。
注意
请务必阅读在子类之间重用混合属性的文档,了解如何覆盖hybrid_property.expression()
和 hybrid_property.comparator()
的重要注意事项,因为在某些情况下可能需要一个特殊的限定符 hybrid_property.overrides
来避免与 QueryableAttribute
的名称冲突。
注意
这种对 @hybrid_property
的更改意味着,当向 @hybrid_property
添加 setter 和其他状态时,方法必须保留原始混合属性的名称,否则新的带有额外状态的混合属性将以不匹配的名称存在于类中。这与标准 Python 中 @property
的行为相同:
class FirstNameOnly(Base):
@hybrid_property
def name(self):
return self.first_name
# WRONG - will raise AttributeError: can't set attribute when
# assigning to .name
@name.setter
def _set_name(self, value):
self.first_name = value
class FirstNameOnly(Base):
@hybrid_property
def name(self):
return self.first_name
# CORRECT - note regular Python @property works the same way
@name.setter
def name(self, value):
self.first_name = value
新的 bulk_replace 事件
为了适应 在批量集合设置之前比较时,@validates 方法接收所有值 中描述的验证用例,添加了一个新的 AttributeEvents.bulk_replace()
方法,它与 AttributeEvents.append()
和 AttributeEvents.remove()
事件一起调用。“bulk_replace” 在 “append” 和 “remove” 之前调用,以便在比较现有集合之前修改集合。之后,单个项目将被附加到新的目标集合,触发针对集合中新项目的 “append” 事件,就像以前的行为一样。下面同时说明了 “bulk_replace” 和 “append”,包括如果使用集合赋值,“append” 将接收到已由 “bulk_replace” 处理过的对象的情况。新的符号 attributes.OP_BULK_REPLACE
可以用于确定此 “append” 事件是否是批量替换的第二部分:
from sqlalchemy.orm.attributes import OP_BULK_REPLACE
@event.listens_for(SomeObject.collection, "bulk_replace")
def process_collection(target, values, initiator):
values[:] = [_make_value(value) for value in values]
@event.listens_for(SomeObject.collection, "append", retval=True)
def process_collection(target, value, initiator):
# make sure bulk_replace didn't already do it
if initiator is None or initiator.op is not OP_BULK_REPLACE:
return _make_value(value)
else:
return value
新的 sqlalchemy.ext.mutable 的“modified”事件处理器
新的事件处理器 AttributeEvents.modified()
被添加了,它会在调用 flag_modified()
方法时触发,通常这个方法是从 sqlalchemy.ext.mutable
扩展中调用的:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy import event
Base = declarative_base()
class MyDataClass(Base):
__tablename__ = "my_data"
id = Column(Integer, primary_key=True)
data = Column(MutableDict.as_mutable(JSONEncodedDict))
@event.listens_for(MyDataClass.data, "modified")
def modified_json(instance):
print("json value modified:", instance.data)
上面的事件处理程序将在对 .data
字典进行原位更改时触发。
添加了“for update”参数到 Session.refresh
向 Session.refresh()
方法添加了新参数 Session.refresh.with_for_update
。当 Query.with_lockmode()
方法被弃用,改用 Query.with_for_update()
后,Session.refresh()
方法从未更新以反映新选项:
session.refresh(some_object, with_for_update=True)
Session.refresh.with_for_update
参数接受一个选项字典,这些选项将作为传递给 Query.with_for_update()
的相同参数:
session.refresh(some_objects, with_for_update={"read": True})
新参数取代了 Session.refresh.lockmode
参数。
可变集合 MutableSet
和 可变列表 MutableList
支持原地变异操作符
为 MutableSet
实现了原地变异操作符 __ior__
、__iand__
、__ixor__
和 __isub__
,以及为 MutableList
实现了 __iadd__
。虽然这些方法以前可以成功更新集合,但它们不会正确触发更改事件。这些操作符像以前一样改变集合,但另外会发出正确的更改事件,以便更改成为下一个刷新过程的一部分:
model = session.query(MyModel).first()
model.json_set &= {1, 3}
AssociationProxy
的 any()
、has()
和 contains()
方法可以与链式关联代理一起使用
AssociationProxy.any()
、AssociationProxy.has()
和 AssociationProxy.contains()
比较方法现在支持链接到一个属性,该属性本身也是一个 AssociationProxy
,递归地。下面,A.b_values
是一个关联代理,链接到 AtoB.bvalue
,而 AtoB.bvalue
本身是一个关联代理,链接到 B
:
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
b_values = association_proxy("atob", "b_value")
c_values = association_proxy("atob", "c_value")
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
value = Column(String)
c = relationship("C")
class C(Base):
__tablename__ = "c"
id = Column(Integer, primary_key=True)
b_id = Column(ForeignKey("b.id"))
value = Column(String)
class AtoB(Base):
__tablename__ = "atob"
a_id = Column(ForeignKey("a.id"), primary_key=True)
b_id = Column(ForeignKey("b.id"), primary_key=True)
a = relationship("A", backref="atob")
b = relationship("B", backref="atob")
b_value = association_proxy("b", "value")
c_value = association_proxy("b", "c")
我们可以使用 AssociationProxy.contains()
在 A.b_values
上进行查询,以跨越两个代理 A.b_values
、AtoB.b_value
:
>>> s.query(A).filter(A.b_values.contains("hi")).all()
SELECT a.id AS a_id
FROM a
WHERE EXISTS (SELECT 1
FROM atob
WHERE a.id = atob.a_id AND (EXISTS (SELECT 1
FROM b
WHERE b.id = atob.b_id AND b.value = :value_1)))
类似地,我们可以使用 AssociationProxy.any()
在 A.c_values
上进行查询,以跨越两个代理 A.c_values
、AtoB.c_value
:
>>> s.query(A).filter(A.c_values.any(value="x")).all()
SELECT a.id AS a_id
FROM a
WHERE EXISTS (SELECT 1
FROM atob
WHERE a.id = atob.a_id AND (EXISTS (SELECT 1
FROM b
WHERE b.id = atob.b_id AND (EXISTS (SELECT 1
FROM c
WHERE b.id = c.b_id AND c.value = :value_1)))))
身份键增强以支持分片
ORM 现在使用的身份键结构包含一个额外成员,因此来自不同上下文的两个相同主键可以共存于同一身份映射中。
水平分片的示例已更新以说明这种行为。示例展示了一个分片类WeatherLocation
,它引用一个依赖的WeatherReport
对象,其中WeatherReport
类映射到一个存储简单整数主键的表。来自不同数据库的两个WeatherReport
对象可能具有相同的主键值。现在的示例说明了一个新的identity_token
字段跟踪这种差异,以便这两个对象可以共存于同一个标识映射中:
tokyo = WeatherLocation("Asia", "Tokyo")
newyork = WeatherLocation("North America", "New York")
tokyo.reports.append(Report(80.0))
newyork.reports.append(Report(75))
sess = create_session()
sess.add_all([tokyo, newyork, quito])
sess.commit()
# the Report class uses a simple integer primary key. So across two
# databases, a primary key will be repeated. The "identity_token" tracks
# in memory that these two identical primary keys are local to different
# databases.
newyork_report = newyork.reports[0]
tokyo_report = tokyo.reports[0]
assert inspect(newyork_report).identity_key == (Report, (1,), "north_america")
assert inspect(tokyo_report).identity_key == (Report, (1,), "asia")
# the token representing the originating shard is also available directly
assert inspect(newyork_report).identity_token == "north_america"
assert inspect(tokyo_report).identity_token == "asia"
新功能和改进 - 核心
布尔数据类型现在强制使用严格的 True/False/None 值
在版本 1.1 中,描述的更改将非本地布尔整数值强制为零/一/无的所有情况产生了一个意外的副作用,改变了当Boolean
遇到非整数值(如字符串)时的行为。特别是,先前会生成值False
的字符串值"0"
现在会生成True
。更糟糕的是,行为的更改只针对某些后端而不是其他后端,这意味着将字符串"0"
值发送给Boolean
的代码在不同后端上会不一致地中断。
这个问题的最终解决方案是不支持将字符串值与布尔值一起使用,因此在 1.2 中,如果传递了非整数/True/False/None 值,将引发严格的TypeError
。此外,只接受整数值 0 和 1。
为了适应希望对布尔值有更自由解释的应用程序,应使用TypeDecorator
。下面说明了一个配方,将允许对 1.1 之前的Boolean
数据类型进行“自由”行为:
from sqlalchemy import Boolean
from sqlalchemy import TypeDecorator
class LiberalBoolean(TypeDecorator):
impl = Boolean
def process_bind_param(self, value, dialect):
if value is not None:
value = bool(int(value))
return value
#4102 ### 悲观的断开连接检测添加到连接池
连接池文档长期以来一直提供了一个使用ConnectionEvents.engine_connect()
引擎事件在检出的连接上发出简单语句以测试其活动性的示例。现在,当与适当的方言一起使用时,此示例的功能已添加到连接池本身中。使用新参数create_engine.pool_pre_ping
,每个检出的连接在返回之前将被测试是否新鲜:
engine = create_engine("mysql+pymysql://", pool_pre_ping=True)
尽管“预检测”方法会在连接池检出时增加少量延迟,但对于典型的事务性应用(包括大多数 ORM 应用),这种开销很小,并且消除了获取到过期连接而引发错误、需要应用程序放弃或重试操作的问题。
该特性不适用于在进行中的事务或 SQL 操作中断开的连接。如果应用程序必须从中恢复,它需要使用自己的操作重试逻辑来预期这些错误。
另请参阅
断开连接处理 - 悲观
#3919 ### IN / NOT IN 运算符的空集合行为现在可配置;默认表达式简化
诸如column.in_([])
这样的表达式,默认情况下现在会产生表达式1 != 1
,而不是column != column
。这将改变查询的结果,该查询比较了一个在与空集合进行比较时求值为 NULL 的 SQL 表达式或列,产生了布尔值 false 或 true(对于 NOT IN),而不是 NULL。在这种情况下发出的警告也被移除了。可以使用create_engine()
的create_engine.empty_in_strategy
参数来获得旧行为。
在 SQL 中,IN 和 NOT IN 运算符不支持显式空值集合的比较;也就是说,这种语法是非法的:
mycolumn IN ()
为了解决这个问题,SQLAlchemy 和其他数据库库检测到这种情况,并渲染一个替代表达式,该表达式求值为 false,或者在 NOT IN 的情况下,求值为 true,根据“col IN ()”始终为 false 的理论,因为“空集合中没有任何东西”。通常为了生成一个跨数据库可移植且在 WHERE 子句的上下文中起作用的 false/true 常量,会使用一个简单的重言式,比如1 != 1
会求值为 false,1 = 1
会求值为 true(简单的常量“0”或“1”通常不能作为 WHERE 子句的目标)。
SQLAlchemy 在早期也采用了这种方法,但很快有人推测,如果“column”为 NULL,SQL 表达式column IN ()
不会求值为 false;相反,该表达式将产生 NULL,因为“NULL”表示“未知”,而在 SQL 中与 NULL 的比较通常产生 NULL。
为了模拟这个结果,SQLAlchemy 从使用1 != 1
改为使用表达式expr != expr
来处理空的“IN”,并使用expr = expr
处理空的“NOT IN”;也就是说,我们使用表达式的实际左侧而不是固定值。如果传递的表达式左侧评估为 NULL,则整体比较也会得到 NULL 结果,而不是 false 或 true。
不幸的是,用户最终抱怨这个表达式对一些查询规划器有非常严重的性能影响。在那时,当遇到空的 IN 表达式时添加了一个警告,支持 SQLAlchemy 继续保持“正确”,并敦促用户避免一般情况下生成空的 IN 谓词的代码,因为通常它们可以安全地省略。然而,在从输入变量动态构建查询的情况下,这在查询中是繁琐的,因为传入的值集可能为空。
最近几个月,对这个决定的最初假设进行了质疑。表达式“NULL IN ()”应该返回 NULL 的想法只是理论上的,无法测试,因为数据库不支持该语法。然而,事实证明,实际上可以通过模拟空集来询问关系数据库对“NULL IN ()”会返回什么值:
SELECT NULL IN (SELECT 1 WHERE 1 != 1)
通过上述测试,我们发现数据库本身无法就答案达成一致。被大多数人认为是最“正确”的数据库 PostgreSQL 返回 False;因为即使“NULL”代表“未知”,“空集”意味着没有任何内容,包括所有未知值。另一方面,MySQL 和 MariaDB 对上述表达式返回 NULL,采用更常见的“所有与 NULL 的比较都返回 NULL”的行为。
SQLAlchemy 的 SQL 架构比最初做出这个设计决定时更复杂,因此我们现在可以在 SQL 字符串编译时调用任一行为。以前,转换为比较表达式是在构造时完成的,也就是说,在调用ColumnOperators.in_()
或ColumnOperators.notin_()
操作符时。通过编译时行为,方言本身可以被指示调用任一方法,即“静态”的1 != 1
比较或“动态”的expr != expr
比较。默认已经更改为“静态”比较,因为这与 PostgreSQL 的行为一致,而且这也是绝大多数用户喜欢的。这将改变查询结果,特别是对比较空表达式和空集的查询,特别是查询否定where(~null_expr.in_([]))
,因为现在这将评估为 true 而不是 NULL。
现在可以使用标志 create_engine.empty_in_strategy
控制此行为,默认为"static"
设置,但也可以设置为"dynamic"
或"dynamic_warn"
,其中"dynamic_warn"
设置等效于以前的行为,即发出 expr != expr
以及性能警告。但预计大多数用户将欣赏"static"
默认设置。
#3907 ### 支持延迟扩展的 IN 参数集,允许具有缓存语句的 IN 表达式
添加了一种名为“expanding”的新类型的 bindparam()
。这是用于 IN
表达式的,其中元素列表在语句执行时被渲染为单独的绑定参数,而不是在语句编译时。这允许将单个绑定参数名称链接到多个元素的 IN 表达式,并且允许使用查询缓存与 IN 表达式。新功能允许相关功能的“select in”加载和“polymorphic in”加载利用烘焙查询扩展以减少调用开销:
stmt = select([table]).where(table.c.col.in_(bindparam("foo", expanding=True)))
conn.execute(stmt, {"foo": [1, 2, 3]})
在 1.2 系列中,该功能应被视为 实验性的。
#3953 ### 对比运算符的展开操作优先级
对于诸如 IN、LIKE、等于、IS、MATCH 等比较运算符的运算符优先级已被展开为一个级别。当组合比较运算符时,将生成更多的括号化效果,例如:
(column("q") == null()) != (column("y") == null())
现在将生成 (q IS NULL) != (y IS NULL)
而不是 q IS NULL != y IS NULL
。
#3999 ### 支持对表、列的 SQL 注释,包括 DDL、反射
数据核心现支持与表和列相关联的字符串注释。这些通过 Table.comment
和 Column.comment
参数指定:
Table(
"my_table",
metadata,
Column("q", Integer, comment="the Q value"),
comment="my Q table",
)
上述 DDL 将在创建表时适当渲染,以将上述注释与模式中的表/列关联起来。当上述表以自动加载或通过 Inspector.get_columns()
进行检查时,将包括这些注释。表注释也可以使用 Inspector.get_table_comment()
方法独立获取。
当前后端支持包括 MySQL、PostgreSQL 和 Oracle。
#1546 ### 支持 DELETE 的多表条件
Delete
构造现在支持多表条件,已在支持的后端实现,目前这些后端包括 PostgreSQL、MySQL 和 Microsoft SQL Server(对当前不工作的 Sybase 方言也添加了支持)。该功能的工作方式与 0.7 和 0.8 系列中首次引入的 UPDATE 的多表条件相同。
给出一个语句如下:
stmt = (
users.delete()
.where(users.c.id == addresses.c.id)
.where(addresses.c.email_address.startswith("ed%"))
)
conn.execute(stmt)
在 PostgreSQL 后端上,上述语句生成的 SQL 将呈现为:
DELETE FROM users USING addresses
WHERE users.id = addresses.id
AND (addresses.email_address LIKE %(email_address_1)s || '%%')
另请参阅
多表删除
#959 ### 新的 “autoescape” 选项用于 startswith()、endswith()
“autoescape” 参数已添加到 ColumnOperators.startswith()
、ColumnOperators.endswith()
、ColumnOperators.contains()
。当将此参数设置为 True
时,将自动使用转义字符转义所有 %
、_
的出现,默认为斜杠 /
;转义字符本身的出现也会被转义。斜杠用于避免与诸如 PostgreSQL 的 standard_confirming_strings
、MySQL 的 NO_BACKSLASH_ESCAPES
等设置发生冲突。现在可以使用现有的 “escape” 参数来更改自动转义字符,如果需要的话。
注意
从 1.2.0 的初始实现 1.2.0b2 开始,此功能已更改,现在 autoescape 被传递为布尔值,而不是用作转义字符的特定字符。
诸如以下表达式:
>>> column("x").startswith("total%score", autoescape=True)
呈现为:
x LIKE :x_1 || '%' ESCAPE '/'
其中参数 “x_1” 的值为 'total/%score'
。
同样,具有反斜杠的表达式:
>>> column("x").startswith("total/score", autoescape=True)
将以相同方式呈现,参数 “x_1” 的值为 'total//score'
。
#2694 ### “float” 数据类型增加更强的类型化
一系列更改允许使用 Float
数据类型更强烈地将其与 Python 浮点值联系起来,而不是更通用的 Numeric
。这些更改主要涉及确保 Python 浮点值不会错误地被强制转换为 Decimal()
,并且在需要时被强制转��为 float
,在结果方面,如果应用程序正在处理普通浮点数。
-
当传递给 SQL 表达式的普通 Python “float” 值现在将被拉入具有类型
Float
的字面参数中;之前,该类型为Numeric
,带有默认的“asdecimal=True”标志,这意味着结果类型将被强制转换为Decimal()
。特别是,这将在 SQLite 上发出令人困惑的警告:float_value = connection.scalar( select([literal(4.56)]) # the "BindParameter" will now be # Float, not Numeric(asdecimal=True) )
-
Numeric
、Float
和Integer
之间的数学运算现在将保留结果表达式的类型Numeric
或Float
,包括asdecimal
标志以及类型是否应为Float
:# asdecimal flag is maintained expr = column("a", Integer) * column("b", Numeric(asdecimal=False)) assert expr.type.asdecimal == False # Float subclass of Numeric is maintained expr = column("a", Integer) * column("b", Float()) assert isinstance(expr.type, Float)
-
如果 DBAPI 已知支持本地
Decimal()
模式,则Float
数据类型将无条件地将float()
处理器应用于结果值。某些后端不总是保证浮点数返回为普通浮点数,而不是诸如 MySQL 等精度数字。
对 GROUPING SETS、CUBE、ROLLUP 的支持
GROUPING SETS、CUBE、ROLLUP 三者都可以通过 func
命名空间使用。对于 CUBE 和 ROLLUP,在之前的版本中这些函数已经可以使用,但对于 GROUPING SETS,在编译器中添加了一个占位符以允许空间。这三个函数现在在文档中命名:
>>> from sqlalchemy import select, table, column, func, tuple_
>>> t = table("t", column("value"), column("x"), column("y"), column("z"), column("q"))
>>> stmt = select([func.sum(t.c.value)]).group_by(
... func.grouping_sets(
... tuple_(t.c.x, t.c.y),
... tuple_(t.c.z, t.c.q),
... )
... )
>>> print(stmt)
SELECT sum(t.value) AS sum_1
FROM t GROUP BY GROUPING SETS((t.x, t.y), (t.z, t.q))
用于具有上下文默认生成器的多值插入的参数助手
默认生成函数,例如在上下文敏感的默认函数中描述的函数,可以通过DefaultExecutionContext.current_parameters
属性查看与语句相关的当前参数。然而,在通过Insert.values()
方法指定多个 VALUES 子句的Insert
构造中,用户定义的函数会被多次调用,每个参数集一次,但是无法知道DefaultExecutionContext.current_parameters
中的哪个键子集适用于该列。添加了一个新函数DefaultExecutionContext.get_current_parameters()
,其中包括一个关键字参数DefaultExecutionContext.get_current_parameters.isolate_multiinsert_groups
默认为True
,执行额外的工作,提供一个DefaultExecutionContext.current_parameters
的子字典,其中的名称局限于当前正在处理的 VALUES 子句:
def mydefault(context):
return context.get_current_parameters()["counter"] + 12
mytable = Table(
"mytable",
metadata_obj,
Column("counter", Integer),
Column("counter_plus_twelve", Integer, default=mydefault, onupdate=mydefault),
)
stmt = mytable.insert().values([{"counter": 5}, {"counter": 18}, {"counter": 20}])
conn.execute(stmt)
#4075 ### 布尔数据类型现在强制使用严格的 True/False/None 值
在版本 1.1 中,将非本地布尔整数值强制转换为零/一/None 的所有情况中描述的更改产生了一个意外的副作用,改变了当Boolean
遇到非整数值(如字符串)时的行为。特别是,先前会生成值False
的字符串值"0"
,现在会生成True
。更糟糕的是,行为的变化只针对某些后端而不是其他后端,这意味着将字符串"0"
值发送给Boolean
的代码在不同后端上会不一致地出现故障。
这个问题的最终解决方案是不支持将字符串值与布尔值一起使用,因此在 1.2 版本中,如果传递了非整数/True/False/None 值,将会引发严格的TypeError
。此外,只有整数值 0 和 1 会被接受。
为了适应希望对布尔值有更自由解释的应用程序,应该使用TypeDecorator
。下面演示了一个配方,可以允许在 1.1 版本之前的Boolean
数据类型的“自由”行为:
from sqlalchemy import Boolean
from sqlalchemy import TypeDecorator
class LiberalBoolean(TypeDecorator):
impl = Boolean
def process_bind_param(self, value, dialect):
if value is not None:
value = bool(int(value))
return value
将悲观的断开检测添加到连接池
连接池文档长期以来一直提供了一个使用ConnectionEvents.engine_connect()
引擎事件在检出的连接上发出简单语句以测试其活动性的配方。现在,当与适当的方言一起使用时,此配方的功能已经添加到连接池本身中。使用新参数create_engine.pool_pre_ping
,每个检出的连接在返回之前都会被测试是否仍然有效:
engine = create_engine("mysql+pymysql://", pool_pre_ping=True)
虽然“预检”方法会在连接池检出时增加一小部分延迟,但对于典型的面向事务的应用程序(其中包括大多数 ORM 应用程序),这种开销是很小的,并且消除了获取到一个过时连接会引发错误的问题,需要应用程序放弃或重试操作。
该功能不适用于在进行中的事务或 SQL 操作中断开的连接。如果应用程序必须从这些错误中恢复,它需要使用自己的操作重试逻辑来预期这些错误。
另请参阅
断开处理 - 悲观
IN / NOT IN 运算符的空集合行为现在是可配置的;默认表达式简化
例如,假设column.in_([])
这样的表达式被假定为 false,默认情况下现在会产生表达式1 != 1
,而不是column != column
。这将改变查询结果,如果比较 SQL 表达式或列与空集合时评估为 NULL,则会产生布尔值 false 或 true(对于 NOT IN),而不是 NULL。在这种情况下发出的警告也被移除了。可以使用create_engine.empty_in_strategy
参数来create_engine()
,以保留旧的行为。
在 SQL 中,IN 和 NOT IN 运算符不支持与明确为空的值集合进行比较;也就是说,以下语法是不合法的:
mycolumn IN ()
为了解决这个问题,SQLAlchemy 和其他数据库库检测到这种情况,并生成一个替代表达式,该表达式评估为 false,或者在 NOT IN 的情况下,根据“col IN ()”始终为 false 的理论,评估为 true,因为“空集合”中没有任何内容。通常,为了生成一个跨数据库可移植且在 WHERE 子句上下文中起作用的 false/true 常量,会使用一个简单的重言式,比如1 != 1
评估为 false,1 = 1
评估为 true(一个简单的常量“0”或“1”通常不能作为 WHERE 子句的目标)。
SQLAlchemy 在早期也采用了这种方法,但很快就有人推测,如果 SQL 表达式column IN ()
中的“column”为 NULL,则不会评估为 false;相反,该表达式会产生 NULL,因为“NULL”表示“未知”,而在 SQL 中与 NULL 的比较通常会产生 NULL。
为了模拟这个结果,SQLAlchemy 从使用1 != 1
改为使用表达式expr != expr
来表示空的“IN”,以及使用expr = expr
来表示空的“NOT IN”;也就是说,我们不再使用固定值,而是使用表达式的实际左侧。如果传递的表达式左侧评估为 NULL,则比较整体也会得到 NULL 结果,而不是 false 或 true。
不幸的是,用户最终抱怨说这个表达式对一些查询规划器有非常严重的性能影响。在那时,当遇到空的 IN 表达式时,添加了一个警告,建议 SQLAlchemy 继续保持“正确”,并敦促用户避免生成通常可以安全省略的空 IN 谓词的代码。然而,在动态构建查询的情况下,这在输入变量为空时可能会带来负担。
近几个月来,对这个决定的原始假设受到了质疑。认为表达式“NULL IN ()”应该返回 NULL 的想法只是理论上的,无法进行测试,因为数据库不支持该语法。然而,事实证明,你确实可以询问关系数据库关于“NULL IN ()”将返回什么值,方法是模拟空集如下:
SELECT NULL IN (SELECT 1 WHERE 1 != 1)
通过上述测试,我们发现数据库本身无法就答案达成一致。大多数人认为最“正确”的数据库 PostgreSQL 返回 False;因为即使“NULL”表示“未知”,“空集”表示什么都没有,包括所有未知的值。另一方面,MySQL 和 MariaDB 返回上述表达式的 NULL,默认为“所有与 NULL 的比较都返回 NULL”的更常见行为。
SQLAlchemy 的 SQL 架构比最初做出这个设计决定时更复杂,因此我们现在可以在 SQL 字符串编译时调用任一行为。先前,将转换为比较表达式是在构建时完成的,也就是说,在调用 ColumnOperators.in_()
或 ColumnOperators.notin_()
操作符时完成。通过编译时行为,方言本身可以被指示调用任一方法,即“static” 1 != 1
比较或“dynamic” expr != expr
比较。默认值已经更改为“static”比较,因为这与 PostgreSQL 在任何情况下的行为一致,这也是绝大多数用户喜欢的。这将改变查询空表达式与空集的比较结果,特别是查询否定 where(~null_expr.in_([]))
的查询,因为现在这将计算为 true 而不是 NULL。
可以使用标志create_engine.empty_in_strategy
来控制行为,该标志默认为"static"
设置,但也可以设置为"dynamic"
或"dynamic_warn"
,其中"dynamic_warn"
设置等效于以前的行为,即发出expr != expr
以及性能警告。但预计大多数用户会喜欢"static"
默认值。
晚扩展的 IN 参数集允许使用缓存语句的 IN 表达式
添加了一种名为“expanding”的新类型的bindparam()
。这用于在IN
表达式中,元素列表在语句执行时被渲染为单独的绑定参数,而不是在语句编译时。这允许将单个绑定参数名称链接到多个元素的 IN 表达式,并允许使用查询缓存与 IN 表达式一起使用。新功能允许“select in”加载和“polymorphic in”加载相关功能利用烘焙查询扩展以减少调用开销:
stmt = select([table]).where(table.c.col.in_(bindparam("foo", expanding=True)))
conn.execute(stmt, {"foo": [1, 2, 3]})
该功能应被视为实验性,属于 1.2 系列。
比较运算符的操作符优先级已经被展开
对于 IN、LIKE、equals、IS、MATCH 和其他比较运算符等运算符的操作符优先级已被展开为一个级别。当比较运算符组合在一起时,将生成更多的括号,例如:
(column("q") == null()) != (column("y") == null())
现在将生成(q IS NULL) != (y IS NULL)
而不是q IS NULL != y IS NULL
。
支持在表、列上添加 SQL 注释,包括 DDL、反射
核心支持与表和列相关的字符串注释。这些通过Table.comment
和Column.comment
参数指定:
Table(
"my_table",
metadata,
Column("q", Integer, comment="the Q value"),
comment="my Q table",
)
上述 DDL 将在表创建时适当地呈现,以将上述注释与模式中的表/列关联起来。当上述表被自动加载或使用Inspector.get_columns()
检查时,注释将被包含在内。表注释也可以通过Inspector.get_table_comment()
方法独立获取。
当前后端支持包括 MySQL、PostgreSQL 和 Oracle。
DELETE 的多表条件支持
Delete
构造现在支持多表条件,已在支持的后端实现,目前这些后端是 PostgreSQL、MySQL 和 Microsoft SQL Server(支持也已添加到当前不工作的 Sybase 方言)。该功能的工作方式与 0.7 和 0.8 系列中首次引入的 UPDATE 的多表条件相同。
给定语句如下:
stmt = (
users.delete()
.where(users.c.id == addresses.c.id)
.where(addresses.c.email_address.startswith("ed%"))
)
conn.execute(stmt)
在 PostgreSQL 后端上,上述语句的结果 SQL 将呈现为:
DELETE FROM users USING addresses
WHERE users.id = addresses.id
AND (addresses.email_address LIKE %(email_address_1)s || '%%')
另请参见
多表删除
新的“autoescape”选项用于 startswith(),endswith()
“autoescape”参数被添加到ColumnOperators.startswith()
,ColumnOperators.endswith()
,ColumnOperators.contains()
中。当将此参数设置为True
时,将自动使用转义字符转义所有出现的%
、_
,默认为斜杠/
;转义字符本身的出现也会被转义。斜杠用于避免与诸如 PostgreSQL 的standard_confirming_strings
(从 PostgreSQL 9.1 开始更改默认值)和 MySQL 的NO_BACKSLASH_ESCAPES
设置等设置发生冲突。现在可以使用现有的“escape”参数来更改自动转义字符,如果需要的话。
注意
从 1.2.0 的初始实现 1.2.0b2 开始,此功能已更改,现在 autoescape 被传递为布尔值,而不是用作转义字符的特定字符。
例如:
>>> column("x").startswith("total%score", autoescape=True)
渲染为:
x LIKE :x_1 || '%' ESCAPE '/'
其中参数“x_1”的值为'total/%score'
。
同样,具有反斜杠的表达式:
>>> column("x").startswith("total/score", autoescape=True)
将以相同方式渲染,参数“x_1”的值为'total//score'
。
对“float”数据类型进行了更强的类型化
一系列更改允许使用Float
数据类型更强烈地将其与 Python 浮点值关联起来,而不是更通用的Numeric
。这些更改主要涉及确保 Python 浮点值不会错误地被强制转换为Decimal()
,并且在需要时,如果应用程序正在处理普通浮点数,则会被强制转换为float
。
-
传递给 SQL 表达式的普通 Python“float”值现在将被拉入具有类型
Float
的文字参数;以前,类型为Numeric
,带有默认的“asdecimal=True”标志,这意味着结果类型将强制转换为Decimal()
。特别是,这将在 SQLite 上发出令人困惑的警告:float_value = connection.scalar( select([literal(4.56)]) # the "BindParameter" will now be # Float, not Numeric(asdecimal=True) )
-
在
Numeric
、Float
和Integer
之间的数学操作现在将保留结果表达式的类型,包括asdecimal
标志以及类型是否应为Float
:# asdecimal flag is maintained expr = column("a", Integer) * column("b", Numeric(asdecimal=False)) assert expr.type.asdecimal == False # Float subclass of Numeric is maintained expr = column("a", Integer) * column("b", Float()) assert isinstance(expr.type, Float)
-
Float
数据类型将始终将float()
处理器应用于结果值,如果 DBAPI 已知支持原生Decimal()
模式。一些后端并不总是保证浮点数作为普通浮点数返回,而不是像 MySQL 这样的精度数字。
支持 GROUPING SETS、CUBE、ROLLUP
GROUPING SETS、CUBE、ROLLUP 这三个功能都可以通过 func
命名空间来调用。在 CUBE 和 ROLLUP 的情况下,这些函数在之前的版本中已经可以使用,但是对于 GROUPING SETS,编译器中添加了一个占位符以允许使用这个功能。现在文档中已经命名了这三个函数:
>>> from sqlalchemy import select, table, column, func, tuple_
>>> t = table("t", column("value"), column("x"), column("y"), column("z"), column("q"))
>>> stmt = select([func.sum(t.c.value)]).group_by(
... func.grouping_sets(
... tuple_(t.c.x, t.c.y),
... tuple_(t.c.z, t.c.q),
... )
... )
>>> print(stmt)
SELECT sum(t.value) AS sum_1
FROM t GROUP BY GROUPING SETS((t.x, t.y), (t.z, t.q))
多值插入的参数辅助器,带有上下文默认生成器
默认生成函数,例如在上下文敏感的默认函数中描述的函数,可以通过DefaultExecutionContext.current_parameters
属性查看与语句相关的当前参数。然而,在通过Insert.values()
方法指定多个 VALUES 子句的Insert
构造中,用户定义的函数会被多次调用,每个参数集一次,但是无法知道DefaultExecutionContext.current_parameters
中的哪些键子集适用于该列。添加了一个新函数DefaultExecutionContext.get_current_parameters()
,其中包括一个关键字参数DefaultExecutionContext.get_current_parameters.isolate_multiinsert_groups
默认为True
,执行额外的工作,提供一个DefaultExecutionContext.current_parameters
的子字典,其中的名称局限于当前正在处理的 VALUES 子句:
def mydefault(context):
return context.get_current_parameters()["counter"] + 12
mytable = Table(
"mytable",
metadata_obj,
Column("counter", Integer),
Column("counter_plus_twelve", Integer, default=mydefault, onupdate=mydefault),
)
stmt = mytable.insert().values([{"counter": 5}, {"counter": 18}, {"counter": 20}])
conn.execute(stmt)
关键行为变化 - ORM
after_rollback() Session 事件现在在对象过期之前触发
SessionEvents.after_rollback()
事件现在可以访问对象在其状态被过期之前的属性状态(例如“快照移除”)。这使得该事件与SessionEvents.after_commit()
事件的行为保持一致,后者在“快照”被移除之前也会触发:
sess = Session()
user = sess.query(User).filter_by(name="x").first()
@event.listens_for(sess, "after_rollback")
def after_rollback(session):
# 'user.name' is now present, assuming it was already
# loaded. previously this would raise upon trying
# to emit a lazy load.
print("user name: %s" % user.name)
@event.listens_for(sess, "after_commit")
def after_commit(session):
# 'user.name' is present, assuming it was already
# loaded. this is the existing behavior.
print("user name: %s" % user.name)
if should_rollback:
sess.rollback()
else:
sess.commit()
请注意,Session
仍将禁止在此事件中发出 SQL;这意味着未加载的属性仍无法在事件范围内加载。
#3934 ### 修复了与 select_from()
一起使用单表继承的问题
当生成 SQL 时,Query.select_from()
方法现在会尊重单表继承列鉴别器;之前,只有查询列列表中的表达式会被考虑。
假设 Manager
是 Employee
的子类。像下面这样的查询:
sess.query(Manager.id)
会生成如下 SQL:
SELECT employee.id FROM employee WHERE employee.type IN ('manager')
然而,如果 Manager
只是通过 Query.select_from()
指定而不在列列表中,鉴别器将不会被添加:
sess.query(func.count(1)).select_from(Manager)
会生成:
SELECT count(1) FROM employee
通过修复,Query.select_from()
现在可以正常工作,我们得到:
SELECT count(1) FROM employee WHERE employee.type IN ('manager')
可能一直通过手动提供 WHERE 子句来解决此问题的应用程序可能需要进行调整。
#3891 ### 在替换时不再改变先前集合
ORM 在映射集合成员更改时会发出事件。将集合分配给将替换先前集合的属性时,这样做的一个副作用是,被替换的集合也会被改变,这是误导性的和不必要的:
>>> a1, a2, a3 = Address("a1"), Address("a2"), Address("a3")
>>> user.addresses = [a1, a2]
>>> previous_collection = user.addresses
# replace the collection with a new one
>>> user.addresses = [a2, a3]
>>> previous_collection
[Address('a1'), Address('a2')]
在更改之前,previous_collection
将删除“a1”成员,对应于不再在新集合中的成员。
#3913 ### 在批量集合设置之前,@validates 方法会接收所有值
使用 @validates
的方法现在在“批量设置”操作期间会接收集合的所有成员,然后再应用比较到现有集合上。
给定一个映射如下:
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
bs = relationship("B")
@validates("bs")
def convert_dict_to_b(self, key, value):
return B(data=value["data"])
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
data = Column(String)
在上面的示例中,我们可以如下使用验证器,将传入的字典转换为 B
的实例并在集合附加时使用:
a1 = A()
a1.bs.append({"data": "b1"})
然而,集合赋值会失败,因为 ORM 会假定传入的对象已经是 B
的实例,因此在进行集合附加之前会尝试将它们与集合的现有成员进行比较,而实际上是在执行调用验证器的集合附加之前。这将使得批量设置操作无法适应需要事先修改的非 ORM 对象,比如字典:
a1 = A()
a1.bs = [{"data": "b1"}]
新逻辑使用新的 AttributeEvents.bulk_replace()
事件,以确保所有值都会被提前发送到 @validates
函数。
作为这一变化的一部分,这意味着验证器现在将在批量设置时接收所有集合成员,而不仅仅是新成员。假设一个简单的验证器如下:
class A(Base):
# ...
@validates("bs")
def validate_b(self, key, value):
assert value.data is not None
return value
如果我们开始的集合如下:
a1 = A()
b1, b2 = B(data="one"), B(data="two")
a1.bs = [b1, b2]
然后,用与第一个重叠的集合替换了原集合:
b3 = B(data="three")
a1.bs = [b2, b3]
以前,第二次赋值只会触发一次A.validate_b
方法,对于b3
对象。b2
对象将被视为已经存在于集合中且不会被验证。使用新行为,b2
和b3
都会在传递到集合之前传递给A.validate_b
。因此,验证方法必须具有幂等行为以适应这种情况。
另请参阅
新的批量替换事件
#3896 ### 使用 flag_dirty()标记对象为“脏”而不更改任何属性
如果flag_modified()
函数用于标记未加载的属性为已修改,则会引发异常:
a1 = A(data="adf")
s.add(a1)
s.flush()
# expire, similarly as though we said s.commit()
s.expire(a1, "data")
# will raise InvalidRequestError
attributes.flag_modified(a1, "data")
这是因为如果属性在刷新发生时仍未出现,那么刷新过程很可能会失败。要将对象标记为“已修改”而不指定任何特定属性,以便在自定义事件处理程序(如SessionEvents.before_flush()
)中考虑到刷新过程中,使用新的flag_dirty()
函数:
from sqlalchemy.orm import attributes
attributes.flag_dirty(a1)
#3753 ### 从 scoped_session 中移除“scope”关键字
一个非常古老且未记录的关键字参数scope
已被移除:
from sqlalchemy.orm import scoped_session
Session = scoped_session(sessionmaker())
session = Session(scope=None)
该关键字的目的是尝试允许变量“作用域”,其中None
表示“无作用域”,因此会返回一个新的Session
。该关键字从未被记录在案,如果遇到将会引发TypeError
。预计该关键字未被使用,但如果用户在测试期间报告与此相关的问题,可以通过弃用来恢复。
#3796 ### 与 onupdate 一起对 post_update 进行细化
使用relationship.post_update
功能的关系现在将与设置了Column.onupdate
值的列更好地交互。如果插入对象时为列显式指定了值,则在 UPDATE 期间会重新声明该值,以便“onupdate”规则不会覆盖它:
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
favorite_b_id = Column(ForeignKey("b.id", name="favorite_b_fk"))
bs = relationship("B", primaryjoin="A.id == B.a_id")
favorite_b = relationship(
"B", primaryjoin="A.favorite_b_id == B.id", post_update=True
)
updated = Column(Integer, onupdate=my_onupdate_function)
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id", name="a_fk"))
a1 = A()
b1 = B()
a1.bs.append(b1)
a1.favorite_b = b1
a1.updated = 5
s.add(a1)
s.flush()
在上述情况下,以前的行为是 UPDATE 会在 INSERT 之后发出,从而触发“onupdate”并覆盖值“5”。现在的 SQL 如下:
INSERT INTO a (favorite_b_id, updated) VALUES (?, ?)
(None, 5)
INSERT INTO b (a_id) VALUES (?)
(1,)
UPDATE a SET favorite_b_id=?, updated=? WHERE a.id = ?
(1, 5, 1)
此外,如果“updated”的值未设置,则我们将正确地在a1.updated
上获取新生成的值;以前,刷新或过期属性以允许生成的值存在的逻辑不会为 post-update 触发。在这种情况下,当刷新中发生 flush 时,也会发出InstanceEvents.refresh_flush()
事件。
#3472 ### post_update 与 ORM 版本控制集成
post_update 功能,文档化在指向自身的行 / 相互依赖的行,涉及对特定关系绑定外键的更改发出 UPDATE 语句,除了针对目标行通常会发出的 INSERT/UPDATE/DELETE。此 UPDATE 语句现在参与版本控制功能,文档化在配置版本计数器。
给定一个映射:
class Node(Base):
__tablename__ = "node"
id = Column(Integer, primary_key=True)
version_id = Column(Integer, default=0)
parent_id = Column(ForeignKey("node.id"))
favorite_node_id = Column(ForeignKey("node.id"))
nodes = relationship("Node", primaryjoin=remote(parent_id) == id)
favorite_node = relationship(
"Node", primaryjoin=favorite_node_id == remote(id), post_update=True
)
__mapper_args__ = {"version_id_col": version_id}
更新将另一个节点关联为“favorite”的节点的 UPDATE 现在也会增加版本计数器,并匹配当前版本:
node = Node()
session.add(node)
session.commit() # node is now version #1
node = session.query(Node).get(node.id)
node.favorite_node = Node()
session.commit() # node is now version #2
请注意,这意味着一个对象由于其他属性的更改而接收到一个 UPDATE,以及由于 post_update 关系更改而接收到第二个 UPDATE,现在将会为一个 flush 接收到两个版本计数器更新。但是,如果对象在当前 flush 中受到 INSERT 的影响,则版本计数器不会额外增加一次,除非存在服务器端版本控制方案。
现在讨论 post_update 为什么即使是 UPDATE 也会发出 UPDATE 的原因在为什么 post_update 除了第一个 UPDATE 还会发出 UPDATE?。
另请参阅
指向自身的行 / 相互依赖的行
为什么 post_update 除了第一个 UPDATE 还会发出 UPDATE?
#3496 ### 在对象过期之前,after_rollback() 会发出 Session 事件
SessionEvents.after_rollback()
事件现在可以在对象状态被过期之前访问属性状态(例如“快照移除”)。这使得事件与SessionEvents.after_commit()
事件的行为一致,后者也会在“快照”被移除之前发出:
sess = Session()
user = sess.query(User).filter_by(name="x").first()
@event.listens_for(sess, "after_rollback")
def after_rollback(session):
# 'user.name' is now present, assuming it was already
# loaded. previously this would raise upon trying
# to emit a lazy load.
print("user name: %s" % user.name)
@event.listens_for(sess, "after_commit")
def after_commit(session):
# 'user.name' is present, assuming it was already
# loaded. this is the existing behavior.
print("user name: %s" % user.name)
if should_rollback:
sess.rollback()
else:
sess.commit()
请注意,Session
仍将禁止在此事件中发出 SQL;这意味着未加载的属性仍将无法在事件范围内加载。
修复了与select_from()
一起使用单表继承的问题
Query.select_from()
方法现在在生成 SQL 时尊重单表继承列鉴别器;以前,只有查询列列表中的表达式会被考虑进去。
假设Manager
是Employee
的子类。像下面这样的查询:
sess.query(Manager.id)
将生成的 SQL 如下:
SELECT employee.id FROM employee WHERE employee.type IN ('manager')
然而,如果Manager
仅由Query.select_from()
指定,而不在列列表中,那么鉴别器将不会被添加:
sess.query(func.count(1)).select_from(Manager)
将生成:
SELECT count(1) FROM employee
通过修复,Query.select_from()
现在可以正常工作,我们得到:
SELECT count(1) FROM employee WHERE employee.type IN ('manager')
可能一直通过手动提供 WHERE 子句来解决此问题的应用程序可能需要进行调整。
在替换���不再改变先前集合
ORM 在映射集合的成员发生变化时会发出事件。将集合分配给将替换先前集合的属性时,这样做的一个副作用是,被替换的集合也会被改变,这是误导性和不必要的:
>>> a1, a2, a3 = Address("a1"), Address("a2"), Address("a3")
>>> user.addresses = [a1, a2]
>>> previous_collection = user.addresses
# replace the collection with a new one
>>> user.addresses = [a2, a3]
>>> previous_collection
[Address('a1'), Address('a2')]
在上面的示例中,在更改之前,previous_collection
将删除“a1”成员,对应于不再在新集合中的成员。
在比较之前,@validates 方法会接收批量集合设置的所有值
使用@validates
的方法现在在“批量设置”操作期间将接收集合的所有成员,然后再将比较应用于现有集合。
给定一个映射如下:
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
bs = relationship("B")
@validates("bs")
def convert_dict_to_b(self, key, value):
return B(data=value["data"])
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
data = Column(String)
在上面的示例中,我们可以使用验证器如下,将传入的字典转换为B
的实例进行集合附加:
a1 = A()
a1.bs.append({"data": "b1"})
然而,集合赋值将失败,因为 ORM 会假定传入的对象已经是B
的实例,因为它在尝试将它们与集合的现有成员进行比较之前,会执行集合附加操作,这实际上会调用验证器。这将使得批量设置操作无法容纳像需要事先修改的字典这样的非 ORM 对象:
a1 = A()
a1.bs = [{"data": "b1"}]
新逻辑使用新的AttributeEvents.bulk_replace()
事件,以确保所有值都被提前发送到@validates
函数。
作为这一变化的一部分,现在验证器将在批量设置时接收所有集合成员,而不仅仅是新成员。假设一个简单的验证器如下:
class A(Base):
# ...
@validates("bs")
def validate_b(self, key, value):
assert value.data is not None
return value
如上所述,如果我们从一个集合开始:
a1 = A()
b1, b2 = B(data="one"), B(data="two")
a1.bs = [b1, b2]
然后,用一个与第一个重叠的集合替换了该集合:
b3 = B(data="three")
a1.bs = [b2, b3]
以前,第二次赋值只会触发一次A.validate_b
方法,对于b3
对象。b2
对象将被视为已经存在于集合中并且不会被验证。通过新行为,b2
和b3
都会在传递到集合之前传递给A.validate_b
。因此,验证方法必须采用幂等行为以适应这种情况。
另请参阅
新的 bulk_replace 事件
使用 flag_dirty()将对象标记为“dirty”而不改变任何属性
如果使用flag_modified()
函数标记一个未加载的属性为已修改,现在会引发异常:
a1 = A(data="adf")
s.add(a1)
s.flush()
# expire, similarly as though we said s.commit()
s.expire(a1, "data")
# will raise InvalidRequestError
attributes.flag_modified(a1, "data")
这是因为如果属性在刷新发生时仍未出现,则刷新过程很可能会失败。要将对象标记为“修改”,而不是特指任何属性,以便考虑到自定义事件处理程序(如SessionEvents.before_flush()
)的刷新过程中,使用新的flag_dirty()
函数:
from sqlalchemy.orm import attributes
attributes.flag_dirty(a1)
从 scoped_session 中移除了“scope”关键字
一个非常古老且未记录的关键字参数scope
已被移除:
from sqlalchemy.orm import scoped_session
Session = scoped_session(sessionmaker())
session = Session(scope=None)
此关键字的目的是尝试允许变量“scopes”,其中None
表示“无范围”,因此将返回一个新的Session
。该关键字从未被记录在案,如果遇到将引发TypeError
。预计不会使用此关键字,但如果用户在测试期间报告与此相关的问题,则可以通过弃用来恢复。
与 onupdate 一起的 post_update 的改进
使用relationship.post_update
功能的关系现在将更好地与设置了Column.onupdate
值的列交互。如果插入对象时为列显式指定了值,则在 UPDATE 期间将重新声明该值,以便“onupdate”规则不会覆盖它:
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
favorite_b_id = Column(ForeignKey("b.id", name="favorite_b_fk"))
bs = relationship("B", primaryjoin="A.id == B.a_id")
favorite_b = relationship(
"B", primaryjoin="A.favorite_b_id == B.id", post_update=True
)
updated = Column(Integer, onupdate=my_onupdate_function)
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id", name="a_fk"))
a1 = A()
b1 = B()
a1.bs.append(b1)
a1.favorite_b = b1
a1.updated = 5
s.add(a1)
s.flush()
在上述情况下,以前的行为是在插入之后会发出更新,从而触发“onupdate”,并覆盖值“5”。现在的 SQL 如下:
INSERT INTO a (favorite_b_id, updated) VALUES (?, ?)
(None, 5)
INSERT INTO b (a_id) VALUES (?)
(1,)
UPDATE a SET favorite_b_id=?, updated=? WHERE a.id = ?
(1, 5, 1)
此外,如果“updated”的值 未 设置,则我们将在 a1.updated
上正确地获得新生成的值;以前,刷新或过期属性的逻辑以允许生成的值存在将不会触发 post-update。在此情况下,还会在刷新期间发出 InstanceEvents.refresh_flush()
事件。
post_update 与 ORM 版本控制集成
post_update 功能,记录在 指向自身的行 / 相互依赖的行,涉及响应特定关系绑定外键的更改而发出 UPDATE 语句,除了为目标行正常发出的 INSERT/UPDATE/DELETE 外。此 UPDATE 语句现在参与版本控制功能,记录在 配置版本计数器。
给定一个映射:
class Node(Base):
__tablename__ = "node"
id = Column(Integer, primary_key=True)
version_id = Column(Integer, default=0)
parent_id = Column(ForeignKey("node.id"))
favorite_node_id = Column(ForeignKey("node.id"))
nodes = relationship("Node", primaryjoin=remote(parent_id) == id)
favorite_node = relationship(
"Node", primaryjoin=favorite_node_id == remote(id), post_update=True
)
__mapper_args__ = {"version_id_col": version_id}
将另一个节点关联为“favorite”的节点进行更新将会增加版本计数器并匹配当前版本:
node = Node()
session.add(node)
session.commit() # node is now version #1
node = session.query(Node).get(node.id)
node.favorite_node = Node()
session.commit() # node is now version #2
请注意,这意味着对象在响应其他属性更改而接收到 UPDATE,并在 post_update 关系更改导致第二个 UPDATE 时,现在将收到一次刷新两次版本计数器更新。但是,如果对象在当前刷新中接受到 INSERT,则版本计数器 不会 再次增加,除非存在服务器端版本控制方案。
现在讨论了 post_update 为何即使是对 UPDATE 也会发出 UPDATE 的原因 为什么 post_update 除了第一个 UPDATE 还会发出 UPDATE?。
另请参阅
指向自身的行 / 相互依赖的行
为什么 post_update 除了第一个 UPDATE 还会发出 UPDATE?
关键行为变化 - 核心
自定义运算符的类型行为已经统一
可以使用 Operators.op()
函数动态创建用户定义的运算符。以前,表达式对此类运算符的类型行为不一致,也无法控制。
而在 1.1 版本中,类似以下表达式将产生无返回类型的结果(假设 -%>
是数据库支持的某个特殊运算符):
>>> column("x", types.DateTime).op("-%>")(None).type
NullType()
其他类型将使用使用左侧类型作为返回类型的默认行为:
>>> column("x", types.String(50)).op("-%>")(None).type
String(length=50)
这些行为大多是偶然发生的,因此行为已与第二种形式保持一致,即默认返回类型与左侧表达式相同:
>>> column("x", types.DateTime).op("-%>")(None).type
DateTime()
由于大多数用户定义的运算符往往是“比较”运算符,通常是由 PostgreSQL 定义的许多特殊运算符之一,Operators.op.is_comparison
标志已修复,以遵循其允许返回类型为 Boolean
的文档行为,包括对 ARRAY
和 JSON
:
>>> column("x", types.String(50)).op("-%>", is_comparison=True)(None).type
Boolean()
>>> column("x", types.ARRAY(types.Integer)).op("-%>", is_comparison=True)(None).type
Boolean()
>>> column("x", types.JSON()).op("-%>", is_comparison=True)(None).type
Boolean()
为了辅助布尔比较运算符,添加了一个新的简写方法 Operators.bool_op()
。应优先使用此方法进行即时布尔运算符:
>>> print(column("x", types.Integer).bool_op("-%>")(5))
x -%> :x_1
``` ### `literal_column()` 中的百分号现在有条件地转义
`literal_column` 结构现在根据使用的 DBAPI 是否使用了对百分号敏感的参数样式有条件地转义百分号字符(例如‘format’或‘pyformat’)。
以前,无法生成一个声明单个百分号的 `literal_column` 结构:
```py
>>> from sqlalchemy import literal_column
>>> print(literal_column("some%symbol"))
some%%symbol
对于未设置为使用‘format’或‘pyformat’参数样式的方言,百分号现在不受影响;大多数 MySQL 方言等声明了其中一个参数样式的方言将继续适当转义:
>>> from sqlalchemy import literal_column
>>> print(literal_column("some%symbol"))
some%symbol
>>> from sqlalchemy.dialects import mysql
>>> print(literal_column("some%symbol").compile(dialect=mysql.dialect()))
some%%symbol
作为这一变化的一部分,使用诸如 ColumnOperators.contains()
、ColumnOperators.startswith()
和 ColumnOperators.endswith()
等运算符时,之前出现的加倍现象也被精细化,只在适当时发生。
#3740 ### 列级别的 COLLATE 关键字现在引用排序规则名称
collate()
和 ColumnOperators.collate()
函数中的一个 bug,用于在语句级别提供临时列排序,已修复,其中一个大小写敏感的名称不会被引用:
stmt = select([mytable.c.x, mytable.c.y]).order_by(
mytable.c.somecolumn.collate("fr_FR")
)
现在显示为:
SELECT mytable.x, mytable.y,
FROM mytable ORDER BY mytable.somecolumn COLLATE "fr_FR"
之前,大小写敏感的名称“fr_FR”不会被引用。目前,手动引用“fr_FR”名称不会被检测到,因此手动引用标识符的应用程序应进行调整。请注意,此更改不会影响类型级别的排序的使用(例如,在表级别上指定在 String
类型上的排序),其中已经应用了引用。
#3785 ### 自定义运算符的输入行为已经保持一致
用户定义的运算符可以使用 Operators.op()
函数动态创建。先前,针对此类运算符的表达式的输入行为是不一致的,也无法控制。
在 1.1 中,例如以下表达式将产生一个没有返回类型的结果(假设 -%>
是数据库支持的某种特殊运算符):
>>> column("x", types.DateTime).op("-%>")(None).type
NullType()
其他类型将使用默认行为,即使用左侧类型作为返回类型:
>>> column("x", types.String(50)).op("-%>")(None).type
String(length=50)
这些行为大多是偶然发生的,因此行为已与第二种形式保持一致,即默认返回类型与左侧表达式相同:
>>> column("x", types.DateTime).op("-%>")(None).type
DateTime()
由于大多数用户定义的运算符倾向于是“比较”运算符,通常是 PostgreSQL 定义的许多特殊运算符之一,因此已修复了 Operators.op.is_comparison
标志,使其遵循其文档化的行为,即在所有情况下允许返回类型为 Boolean
,包括对于 ARRAY
和 JSON
:
>>> column("x", types.String(50)).op("-%>", is_comparison=True)(None).type
Boolean()
>>> column("x", types.ARRAY(types.Integer)).op("-%>", is_comparison=True)(None).type
Boolean()
>>> column("x", types.JSON()).op("-%>", is_comparison=True)(None).type
Boolean()
为了辅助布尔比较运算符,添加了一个新的简写方法 Operators.bool_op()
。应优先使用此方法进行即时布尔运算:
>>> print(column("x", types.Integer).bool_op("-%>")(5))
x -%> :x_1
literal_column()
中的百分号现在有条件地转义
literal_column
构造现在有条件地转义百分号字符,取决于正在使用的 DBAPI 是否使用了对百分号敏感的参数样式(例如‘format’或‘pyformat’)。
以前,无法生成一个声明了单个百分号的literal_column
构造:
>>> from sqlalchemy import literal_column
>>> print(literal_column("some%symbol"))
some%%symbol
对于未设置为使用‘format’或‘pyformat’参数样式的方言,百分号现在不受影响;大多数 MySQL 方言等声明了这些参数样式的方言将继续适当地进行转义:
>>> from sqlalchemy import literal_column
>>> print(literal_column("some%symbol"))
some%symbol
>>> from sqlalchemy.dialects import mysql
>>> print(literal_column("some%symbol").compile(dialect=mysql.dialect()))
some%%symbol
作为这一变化的一部分,使用ColumnOperators.contains()
、ColumnOperators.startswith()
和ColumnOperators.endswith()
等操作符时,之前出现的加倍现象也被优化为仅在适当时发生。
列级别的 COLLATE 关键字现在引用了排序规则名称
修复了collate()
和ColumnOperators.collate()
函数中的一个错误,用于在语句级别提供临时列排序规则,其中一个区分大小写的名称将不会被引用:
stmt = select([mytable.c.x, mytable.c.y]).order_by(
mytable.c.somecolumn.collate("fr_FR")
)
现在呈现为:
SELECT mytable.x, mytable.y,
FROM mytable ORDER BY mytable.somecolumn COLLATE "fr_FR"
以前,区分大小写的名称“fr_FR”将不会被引用。目前,不会检测手动引用“fr_FR”名称,因此手动引用标识符的应用程序应进行调整。请注意,此更改不影响在类型级别使用排序规则(例如在数据类型上指定的String
在表级别),其中已经应用了引用。
方言改进和变更 - PostgreSQL
支持批处理模式 / 快速执行助手
psycopg2 的 cursor.executemany()
方法被认为性能较差,特别是在 INSERT 语句中。为了缓解这一问题,psycopg2 添加了快速执行助手,通过批量发送多个 DML 语句来减少服务器往返次数。SQLAlchemy 1.2 现在包括对这些助手的支持,可以在Engine
使用 cursor.executemany()
对多个参数集合执行语句时透明地使用。该功能默认关闭,可以通过在 create_engine()
上使用 use_batch_mode
参数来启用:
engine = create_engine(
"postgresql+psycopg2://scott:tiger@host/dbname", use_batch_mode=True
)
目前该功能被视为实验性质,但可能在未来的版本中默认开启。
另请参阅
Psycopg2 快速执行助手
#4109 ### 支持 INTERVAL 中字段规范的支持,包括完整反射
PostgreSQL 的 INTERVAL 数据类型中的“fields”规范允许指定要存储的间隔的哪些字段,包括“YEAR”、“MONTH”、“YEAR TO MONTH”等值。INTERVAL
数据类型现在允许指定这些值:
from sqlalchemy.dialects.postgresql import INTERVAL
Table("my_table", metadata, Column("some_interval", INTERVAL(fields="DAY TO SECOND")))
此外,所有 INTERVAL 数据类型现在都可以独立于存在的“fields”规范进行反射;数据类型本身的“fields”参数也将存在:
>>> inspect(engine).get_columns("my_table")
[{'comment': None,
'name': u'some_interval', 'nullable': True,
'default': None, 'autoincrement': False,
'type': INTERVAL(fields=u'day to second')}]
#3959 ### 支持批处理模式 / 快速执行助手
psycopg2 的 cursor.executemany()
方法被认为性能较差,特别是在 INSERT 语句中。为了缓解这一问题,psycopg2 添加了快速执行助手,通过批量发送多个 DML 语句来减少服务器往返次数。SQLAlchemy 1.2 现在包括对这些助手的支持,可以在Engine
使用 cursor.executemany()
对多个参数集合执行语句时透明地使用。该功能默认关闭,可以通过在 create_engine()
上使用 use_batch_mode
参数来启用:
engine = create_engine(
"postgresql+psycopg2://scott:tiger@host/dbname", use_batch_mode=True
)
目前该功能被视为实验性质,但可能在未来的版本中默认开启。
另请参阅
Psycopg2 快速执行助手
支持 INTERVAL 中字段规范的支持,包括完整反射
PostgreSQL 的 INTERVAL 数据类型中的“fields”指定符允许指定要存储的间隔的哪些字段,包括“YEAR”、“MONTH”、“YEAR TO MONTH”等值。INTERVAL
数据类型现在允许指定这些值:
from sqlalchemy.dialects.postgresql import INTERVAL
Table("my_table", metadata, Column("some_interval", INTERVAL(fields="DAY TO SECOND")))
此外,现在可以独立于“fields”指定符反映所有 INTERVAL 数据类型;数据类型本身的“fields”参数也将存在:
>>> inspect(engine).get_columns("my_table")
[{'comment': None,
'name': u'some_interval', 'nullable': True,
'default': None, 'autoincrement': False,
'type': INTERVAL(fields=u'day to second')}]
方言改进和变更 - MySQL
支持 INSERT..ON DUPLICATE KEY UPDATE
MySQL 支持的 INSERT
的 ON DUPLICATE KEY UPDATE
子句现在可以使用 MySQL 特定版本的 Insert
对象来支持,通过 sqlalchemy.dialects.mysql.dml.insert()
。这个 Insert
子类添加了一个新方法 Insert.on_duplicate_key_update()
,实现了 MySQL 的语法:
from sqlalchemy.dialects.mysql import insert
insert_stmt = insert(my_table).values(id="some_id", data="some data to insert")
on_conflict_stmt = insert_stmt.on_duplicate_key_update(
data=insert_stmt.inserted.data, status="U"
)
conn.execute(on_conflict_stmt)
以上内容将呈现为:
INSERT INTO my_table (id, data)
VALUES (:id, :data)
ON DUPLICATE KEY UPDATE data=VALUES(data), status=:status_1
另请参阅
INSERT…ON DUPLICATE KEY UPDATE (Upsert)
#4009 ### 支持 INSERT..ON DUPLICATE KEY UPDATE
MySQL 支持的 INSERT
的 ON DUPLICATE KEY UPDATE
子句现在可以使用 MySQL 特定版本的 Insert
对象来支持,通过 sqlalchemy.dialects.mysql.dml.insert()
。这个 Insert
子类添加了一个新方法 Insert.on_duplicate_key_update()
,实现了 MySQL 的语法:
from sqlalchemy.dialects.mysql import insert
insert_stmt = insert(my_table).values(id="some_id", data="some data to insert")
on_conflict_stmt = insert_stmt.on_duplicate_key_update(
data=insert_stmt.inserted.data, status="U"
)
conn.execute(on_conflict_stmt)
以上内容将呈现为:
INSERT INTO my_table (id, data)
VALUES (:id, :data)
ON DUPLICATE KEY UPDATE data=VALUES(data), status=:status_1
另请参阅
INSERT…ON DUPLICATE KEY UPDATE (Upsert)
方言改进和变更 - Oracle
cx_Oracle 方言、类型系统的重大重构
随着 cx_Oracle DBAPI 的 6.x 系列的推出,SQLAlchemy 的 cx_Oracle 方言已经重新设计和简化,以利用 cx_Oracle 的最新改进,并放弃了在 cx_Oracle 的 5.x 系列之前更相关的模式的支持。
-
支持的最低 cx_Oracle 版本现在是 5.1.3;推荐使用 5.3 或最新的 6.x 系列。
-
数据类型的处理已经重构。
cursor.setinputsizes()
方法不再用于除 LOB 类型之外的任何数据类型,根据 cx_Oracle 的开发人员的建议。因此,参数auto_setinputsizes
和exclude_setinputsizes
已被弃用,不再起作用。 -
当
coerce_to_decimal
标志设置为 False 时,表示不应发生对具有精度和标度的数值类型进行到Decimal
的强制转换,仅影响未定义类型(例如,没有TypeEngine
对象的普通字符串)的语句。现在,包含Numeric
类型或子类型的 Core 表达式将遵循该类型的十进制强制转换规则。 -
“两阶段”事务支持在方言中已经在 cx_Oracle 的 6.x 系列中被删除,现在已完全移除,因为这个功能从未正确工作过,也不太可能被用于生产环境。因此,
allow_twophase
方言标志已被弃用,也不再起作用。 -
修复了涉及 RETURNING 中存在的列键的错误。给定如下语句:
result = conn.execute(table.insert().values(x=5).returning(table.c.a, table.c.b))
以前,结果中每行的键是
ret_0
和ret_1
,这些是 cx_Oracle RETURNING 实现内部的标识符。现在,键将是a
和b
,这是其他方言所期望的。 -
cx_Oracle 的 LOB 数据类型将返回值表示为
cx_Oracle.LOB
对象,这是一个与游标关联的代理,通过.read()
方法返回最终数据值。在历史上,如果在这些 LOB 对象被消耗之前读取了更多行(特别是在读取了比 cursor.arraysize 值更多的行,导致读取了一批新行),这些 LOB 对象会引发错误“在后续获取后 LOB 变量不再有效”。SQLAlchemy 通过在其类型系统内部自动调用.read()
,以及使用一个特殊的BufferedColumnResultSet
来解决这个问题,该对象将确保在使用cursor.fetchmany()
或cursor.fetchall()
这样的调用时,这些数据被缓冲。方言现在使用 cx_Oracle outputtypehandler 来处理这些
.read()
调用,以便无论获取多少行,它们始终被立即调用,因此不再会发生此错误。因此,BufferedColumnResultSet
的使用,以及一些其他特定于此用例的 CoreResultSet
内部机制已被移除。类型对象也变得更简化,因为它们不再需要处理二进制列结果。另外,cx_Oracle 6.x 已经删除了此错误发生的条件,因此不再可能发生此错误。在 SQLAlchemy 中,如果很少(如果有的话)使用了
auto_convert_lobs=False
选项,并且在 LOB 对象可以被消耗之前读取了更多行,则可能会发生错误。升级到 cx_Oracle 6.x 将解决这个问题。### Oracle 唯一性、检查约束现在已反映。
UNIQUE 和 CHECK 约束现在通过 Inspector.get_unique_constraints()
和 Inspector.get_check_constraints()
反映出来。反映的 Table
对象现在也将包括 CheckConstraint
对象。有关此处行为怪癖的信息,请参阅 约束反射,包括大多数 Table
对象仍不会包括任何 UniqueConstraint
对象,因为这些通常通过 Index
表示。
另请参见
约束反射
#4003 ### Oracle 外键约束名称现在是“名称标准化”
在表反射期间传递给 ForeignKeyConstraint
对象的外键约束名称以及在 Inspector.get_foreign_keys()
方法中将会“名称标准化”,即以小写形式表示以进行大小写不敏感的命名,而不是 Oracle 使用的原始大写格式:
>>> insp.get_indexes("addresses")
[{'unique': False, 'column_names': [u'user_id'],
'name': u'address_idx', 'dialect_options': {}}]
>>> insp.get_pk_constraint("addresses")
{'name': u'pk_cons', 'constrained_columns': [u'id']}
>>> insp.get_foreign_keys("addresses")
[{'referred_table': u'users', 'referred_columns': [u'id'],
'referred_schema': None, 'name': u'user_id_fk',
'constrained_columns': [u'user_id']}]
以前,外键约束的结果看起来像:
[
{
"referred_table": "users",
"referred_columns": ["id"],
"referred_schema": None,
"name": "USER_ID_FK",
"constrained_columns": ["user_id"],
}
]
上述内容可能会在特别是与 Alembic autogenerate 一起创建问题。
#3276 ### cx_Oracle 方言、类型系统的重大重构
随着 cx_Oracle DBAPI 推出的 6.x 系列,SQLAlchemy 的 cx_Oracle 方言已经进行了重构和简化,以利用 cx_Oracle 的最新改进,并放弃了在 cx_Oracle 5.x 系列之前更相关的模式的支持。
-
支持的最低 cx_Oracle 版本现在是 5.1.3;建议使用 5.3 或最新的 6.x 系列。
-
数据类型的处理已经重构。
cursor.setinputsizes()
方法现在仅用于 LOB 类型,根据 cx_Oracle 的开发人员的建议。因此,参数auto_setinputsizes
和exclude_setinputsizes
已被弃用,不再起作用。 -
当设置为 False 时,
coerce_to_decimal
标志表示不应进行具有精度和标度的数字类型到Decimal
的强制转换,仅影响未类型化的(例如,没有TypeEngine
对象的普通字符串)语句。包含Numeric
类型或子类型的 Core 表达式现在将遵循该类型的十进制强制转换规则。 -
方言中的“两阶段”事务支持已经在 cx_Oracle 的 6.x 系列中删除,因为这个功能从未正确工作过,并且不太可能已经投入生产使用。因此,
allow_twophase
方言标志已被弃用,也不再起作用。 -
修复了涉及 RETURNING 的列键存在的错误。给定以下语句:
result = conn.execute(table.insert().values(x=5).returning(table.c.a, table.c.b))
以前,结果中每行的键是
ret_0
和ret_1
,这是 cx_Oracle RETURNING 实现的内部标识符。现在,键将是a
和b
,这是其他方言所期望的。 -
cx_Oracle 的 LOB 数据类型将返回值表示为
cx_Oracle.LOB
对象,它是一个与游标关联的代理,通过.read()
方法返回最终数据值。历史上,如果在这些 LOB 对象被消耗之前读取了更多的行(具体来说,如果读取的行数超过了 cursor.arraysize 的值,这会导致读取一批新的行),这些 LOB 对象将引发错误“在后续获取后 LOB 变量不再有效”。SQLAlchemy 解决了这个问题,通过在其类型系统中自动调用.read()
来处理这些 LOB,并使用特殊的BufferedColumnResultSet
确保这些数据被缓冲,以防使用了cursor.fetchmany()
或cursor.fetchall()
这样的调用。方言现在使用 cx_Oracle 的 outputtypehandler 来处理这些
.read()
调用,以便无论读取多少行,它们都始终被提前调用,因此不再会发生此错误。因此,已删除了BufferedColumnResultSet
的使用,以及一些特定于此用例的 CoreResultSet
的其他内部内容。由于不再需要处理二进制列结果,类型对象也变得简化了。另外,cx_Oracle 6.x 已经删除了此错误在任何情况下发生的条件,因此该错误不再可能发生。在 SQLAlchemy 中,该错误可能发生在很少(如果有的话)使用了
auto_convert_lobs=False
选项,并且与之前的 cx_Oracle 5.x 系列一起使用,以及在 LOB 对象可以被消耗之前读取了更多行的情况下。升级到 cx_Oracle 6.x 将解决该问题。
Oracle 唯一性、检查约束现在已反映
唯一约束和检查约束现在通过 Inspector.get_unique_constraints()
和 Inspector.get_check_constraints()
反映出来。一个反映的 Table
对象现在也将包括 CheckConstraint
对象。请参阅 Constraint Reflection 中的注意事项,了解这里的行为怪癖,包括大多数 Table
对象仍将不包括任何 UniqueConstraint
对象,因为这些通常通过 Index
表示。
另请参阅
Constraint Reflection
Oracle 外键约束名称现在已经“名称标准化”
在表反射期间传递给 ForeignKeyConstraint
对象以及在 Inspector.get_foreign_keys()
方法内部的外键约束的名称现在将被“名称标准化”,即,以小写形式表示以便于不区分大小写的名称,而不是 Oracle 使用的原始大写格式:
>>> insp.get_indexes("addresses")
[{'unique': False, 'column_names': [u'user_id'],
'name': u'address_idx', 'dialect_options': {}}]
>>> insp.get_pk_constraint("addresses")
{'name': u'pk_cons', 'constrained_columns': [u'id']}
>>> insp.get_foreign_keys("addresses")
[{'referred_table': u'users', 'referred_columns': [u'id'],
'referred_schema': None, 'name': u'user_id_fk',
'constrained_columns': [u'user_id']}]
以前,外键结果看起来像是:
[
{
"referred_table": "users",
"referred_columns": ["id"],
"referred_schema": None,
"name": "USER_ID_FK",
"constrained_columns": ["user_id"],
}
]
上述情况可能会特别在 Alembic autogenerate 方面造成问题。
方言改进和更改 - SQL Server
支持具有嵌入点的 SQL Server 架构名称
SQL Server 方言有一种行为,即假定带有点的架构名称是“数据库”。“所有者”标识符对,这在表和组件反射操作以及在呈现架构名称的引用时必须在这些单独的组件之间进行拆分,以使这两个符号分别引用。现在可以使用方括号传递架构参数以手动指定此拆分发生的位置,允许包含一个或多个点的数据库和/或所有者名称:
Table("some_table", metadata, Column("q", String(50)), schema="[MyDataBase.dbo]")
上表将考虑“owner”为MyDataBase.dbo
,在呈现时也将被引用,并且“database”为 None。要分别引用数据库名称和所有者,请使用两对括号:
Table(
"some_table",
metadata,
Column("q", String(50)),
schema="[MyDataBase.SomeDB].[MyDB.owner]",
)
此外,当传递给 SQL Server 方言的“schema”时,现在会尊重quoted_name
构造;如果引号标志为 True,则给定的符号不会在点上拆分,并且将被解释为“owner”。
另请参见
多部分模式名称
支持 AUTOCOMMIT 隔离级别
PyODBC 和 pymssql 方言现在都支持由Connection.execution_options()
设置的“AUTOCOMMIT”隔离级别,这将在 DBAPI 连接对象上建立正确的标志。
支持带有嵌入点的 SQL Server 模式名称
SQL Server 方言具有这样的行为,即假定具有其中的点的模式名称是“数据库”。“所有者”标识符对,这在表和组件反射操作以及在呈现模式名称的引用时必须将这两个符号分开时发生,以便分别引用这两个符号。现在可以使用括号传递模式参数以手动指定此拆分发生的位置,允许数据库和/或所有者名称本身包含一个或多个点:
Table("some_table", metadata, Column("q", String(50)), schema="[MyDataBase.dbo]")
上表将考虑“owner”为MyDataBase.dbo
,在呈现时也将被引用,并且“database”为 None。要分别引用数据库名称和所有者,请使用两对括号:
Table(
"some_table",
metadata,
Column("q", String(50)),
schema="[MyDataBase.SomeDB].[MyDB.owner]",
)
此外,当传递给 SQL Server 方言的“schema”时,现在会尊重quoted_name
构造;如果引号标志为 True,则给定的符号不会在点上拆分,并且将被解释为“owner”。
另请参见
多部分模式名称
支持 AUTOCOMMIT 隔离级别
PyODBC 和 pymssql 方言现在都支持由Connection.execution_options()
设置的“AUTOCOMMIT”隔离级别,这将在 DBAPI 连接对象上建立正确的标志。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?