SqlAlchemy 2.0 中文文档(七十三)(1)

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介: SqlAlchemy 2.0 中文文档(七十三)


原文:docs.sqlalchemy.org/en/20/contents.html

SQLAlchemy 1.3 有什么新功能?

原文:docs.sqlalchemy.org/en/20/changelog/migration_13.html

关于本文档

本文描述了 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 与非主映射器的关系取代了

#4393

新功能和改进 - 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()直接引用AliasedClassAliasedClass已经做了我们希望非主映射器做的一切;它允许从替代可选择项加载现有映射类,继承现有映射器的所有属性和关系,与加载器选项非常配合,提供一个类似类的对象,可以像类本身一样混入查询中。通过这种改变,以前针对非主映射器的配方在配置关系连接方式中被更改为别名类。

在关系到别名类时,原始的非主映射器看起来像:

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可调用对象,以便根据给定的条件在多个分片上运行更新/删除操作。

#4196

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 类。

#4351

关联代理现在强引用父对象

关联代理集合仅维护对父对象的弱引用的长期行为被还原;代理现在将在代理集合本身也在内存中的情况下维护对父对象的强引用,从而消除“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中是持久的,它将保持在该会话的状态中,直到被垃圾回收。

请注意,如果此更改导致问题,可能会对其进行修订。

#4268

为集合和关联代理实现了批量替换

将集合或字典分配给关联代理集合现在应该能正常工作了,而以前会为现有键重新创建关联代理成员,导致由于相同对象的删除+插入而导致潜在刷新失败的问题,现在应该只在适当的情况下创建新的关联对象:

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,导致在刷新期间删除AB对象之间的链接。相同的问题也可以使用普通重复项来演示:

>>> 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.searchlist.__contains__

最初认为需要在集合内部使用基于事件的引用计数方案,以便在整个集合的生命周期中跟踪所有重复的实例,这将对所有集合操作产生性能/内存/复杂性影响,包括非常频繁的加载和追加操作。相反采取的方法将额外的开销限制在集合移除和批量替换这些不太常见的操作上,并且线性扫描的观察开销是可以忽略的;在工作单元内以及在集合进行批量替换时,已经在关系绑定集合中使用了线性扫描。

#1103

关键行为变化 - 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_namecolumn_0_N_namecolumn_0N_keycolumn_0_N_keyreferred_column_0N_namereferred_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() 方法现在可以工作,允许方言以及 TypeDecoratorVariant 使用这些方法。

以下示例说明了一个将 SQL 时间转换函数应用于 LargeBinaryTypeDecorator。为了使此类型在 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


SqlAlchemy 2.0 中文文档(七十三)(2)https://developer.aliyun.com/article/1562208

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
3月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(七十四)(5)
SqlAlchemy 2.0 中文文档(七十四)
41 6
|
3月前
|
SQL JSON 测试技术
SqlAlchemy 2.0 中文文档(七十五)(2)
SqlAlchemy 2.0 中文文档(七十五)
38 3
|
3月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(七十四)(3)
SqlAlchemy 2.0 中文文档(七十四)
39 1
|
3月前
|
SQL JSON 关系型数据库
SqlAlchemy 2.0 中文文档(七十五)(4)
SqlAlchemy 2.0 中文文档(七十五)
45 1
|
3月前
|
存储 Java 测试技术
SqlAlchemy 2.0 中文文档(七十三)(3)
SqlAlchemy 2.0 中文文档(七十三)
31 8
|
3月前
|
SQL Python
SqlAlchemy 2.0 中文文档(七十四)(4)
SqlAlchemy 2.0 中文文档(七十四)
27 6
|
3月前
|
SQL 关系型数据库 测试技术
SqlAlchemy 2.0 中文文档(七十三)(2)
SqlAlchemy 2.0 中文文档(七十三)
38 4
|
3月前
|
SQL JSON 关系型数据库
SqlAlchemy 2.0 中文文档(七十五)(1)
SqlAlchemy 2.0 中文文档(七十五)
62 4
|
3月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(七十三)(4)
SqlAlchemy 2.0 中文文档(七十三)
31 2
|
3月前
|
SQL JSON 关系型数据库
SqlAlchemy 2.0 中文文档(七十三)(5)
SqlAlchemy 2.0 中文文档(七十三)
38 1