SqlAlchemy 2.0 中文文档(七十五)(2)

本文涉及的产品
RDS SQL Server Serverless,2-4RCU 50GB 3个月
推荐场景:
云数据库 RDS SQL Server,基础系列 2核4GB
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
简介: SqlAlchemy 2.0 中文文档(七十五)

SqlAlchemy 2.0 中文文档(七十五)(1)https://developer.aliyun.com/article/1562369


方言改进和变化 - SQL Server

为 SQL Server 添加了事务隔离级别支持

所有 SQL Server 方言都支持通过create_engine.isolation_levelConnection.execution_options.isolation_level参数设置事务隔离级别。支持四个标准级别以及SNAPSHOT

engine = create_engine(
    "mssql+pyodbc://scott:tiger@ms_2008", isolation_level="REPEATABLE READ"
)

另请参见

事务隔离级别

#3534 ### 字符串/可变长度类型在反射中不再明确表示“max”

当反射类型如StringTextClause等包含长度时,在 SQL Server 下,一个“无长度”的类型会将“length”参数复制为值"max"

>>> from sqlalchemy import create_engine, inspect
>>> engine = create_engine("mssql+pyodbc://scott:tiger@ms_2008", echo=True)
>>> engine.execute("create table s (x varchar(max), y varbinary(max))")
>>> insp = inspect(engine)
>>> for col in insp.get_columns("s"):
...     print(col["type"].__class__, col["type"].length)
<class 'sqlalchemy.sql.sqltypes.VARCHAR'> max
<class 'sqlalchemy.dialects.mssql.base.VARBINARY'> max

基本类型中的“length”参数预期只能是整数值或 None;None 表示无界长度,SQL Server 方言将其解释为“max”。因此,修复这些长度为 None,以便类型对象在非 SQL Server 上下文中工作:

>>> for col in insp.get_columns("s"):
...     print(col["type"].__class__, col["type"].length)
<class 'sqlalchemy.sql.sqltypes.VARCHAR'> None
<class 'sqlalchemy.dialects.mssql.base.VARBINARY'> None

可能一直依赖于将“length”值直接与字符串“max”进行比较的应用程序应该考虑将None的值视为相同。

#3504

支持在主键上“非聚集”以允许在其他地方进行聚集

UniqueConstraintPrimaryKeyConstraintIndex上现在默认为Nonemssql_clustered标志,可以设置为 False,这将特别为主键渲染 NONCLUSTERED 关键字,允许使用不同的索引作为“clustered”。

另请参见

聚集索引支持

legacy_schema_aliasing 标志现在设置为 False

SQLAlchemy 1.0.5 引入了legacy_schema_aliasing标志到 MSSQL 方言,允许关闭所谓的“传统模式”别名。这种别名试图将模式限定的表转换为别名;给定一个表如下:

account_table = Table(
    "account",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("info", String(100)),
    schema="customer_schema",
)

传统行为模式将尝试将模式限定的表名转换为别名:

>>> eng = create_engine("mssql+pymssql://mydsn", legacy_schema_aliasing=True)
>>> print(account_table.select().compile(eng))
SELECT  account_1.id,  account_1.info
FROM  customer_schema.account  AS  account_1 

然而,这种别名已被证明是不必要的,在许多情况下会产生不正确的 SQL。

在 SQLAlchemy 1.1 中,legacy_schema_aliasing标志现在默认为 False,禁用这种行为模式,允许 MSSQL 方言正常处理模式限定的表。对于可能依赖于此行为的应用程序,请将标志设置回 True。

#3434

方言改进和变化 - Oracle

支持 SKIP LOCKED

新参数GenerativeSelect.with_for_update.skip_locked在 Core 和 ORM 中都会为“SELECT…FOR UPDATE”或“SELECT… FOR SHARE”查询生成“SKIP LOCKED”后缀。

介绍

本指南介绍了 SQLAlchemy 1.1 版本的新功能,并记录了影响从 SQLAlchemy 1.0 系列迁移应用程序的用户的更改。

请仔细查看关于行为变更的部分,可能会有影响向后不兼容的行为变更。

平台/安装程序更改

现在安装需要 Setuptools

多年来,SQLAlchemy 的setup.py文件一直支持安装 Setuptools 和不安装  Setuptools 两种操作方式;支持一种使用纯 Distutils 的“回退”模式。由于现在几乎听不到没有安装 Setuptools 的  Python 环境了,并且为了更充分地支持 Setuptools 的功能集,特别是为了支持 py.test  与其集成以及诸如“extras”之类的功能,setup.py现在完全依赖于 Setuptools。

另请参阅

安装指南

#3489

仅通过环境变量启用/禁用 C 扩展构建

默认情况下,在安装过程中会构建 C 扩展,只要可能。要禁用 C 扩展构建,从 SQLAlchemy 0.8.6 / 0.9.4 版本开始,可以使用DISABLE_SQLALCHEMY_CEXT环境变量。以前使用--without-cextensions参数的方法已被移除,因为它依赖于 setuptools 的已弃用功能。

另请参阅

构建 Cython 扩展

#3500

现在安装需要 Setuptools

多年来,SQLAlchemy 的setup.py文件一直支持安装 Setuptools 和不安装  Setuptools 两种操作方式;支持一种使用纯 Distutils 的“回退”模式。由于现在几乎听不到没有安装 Setuptools 的  Python 环境了,并且为了更充分地支持 Setuptools 的功能集,特别是为了支持 py.test  与其集成以及诸如“extras”之类的功能,setup.py现在完全依赖于 Setuptools。

另请参阅

安装指南

#3489

仅通过环境变量启用/禁用 C 扩展构建

默认情况下,在安装过程中会构建 C 扩展,只要可能。要禁用 C 扩展构建,从 SQLAlchemy 0.8.6 / 0.9.4 版本开始,可以使用DISABLE_SQLALCHEMY_CEXT环境变量。以前使用--without-cextensions参数的方法已被移除,因为它依赖于 setuptools 的已弃用功能。

另请参阅

构建 Cython 扩展

#3500

新功能和改进 - ORM

新的会话生命周期事件

Session长期以来一直支持事件,允许在对象状态变化方面进行一定程度的跟踪,包括SessionEvents.before_attach()SessionEvents.after_attach()SessionEvents.before_flush()。会话文档还在快速介绍对象状态中记录了主要对象状态。然而,从未有过一种系统来跟踪对象特别是当它们通过这些转换时。此外,“已删除”对象的状态历来是模糊的,因为对象在“持久”状态和“分离”状态之间的行为之间存在某种程度的不确定性。

为了清理这个领域并使会话状态转换的领域完全透明,已经添加了一系列新事件,旨在涵盖对象可能在状态之间转换的每种可能方式,并且“已删除”状态还在会话对象状态领域内被赋予了自己的官方状态名称。

新状态转换事件

现在可以拦截对象的所有状态之间的转换,例如持久、挂起等,以便覆盖特定转换的会话级事件。对象进入Session、离开Session以及甚至在使用Session.rollback()回滚事务时发生的所有转换都明确地出现在SessionEvents的接口中。

总共有十个新事件。这些事件的摘要在新编写的文档部分对象生命周期事件中。

新对象状态“已删除”已添加,已删除对象不再是“持久”状态。

Session中对象的持久状态一直被记录为具有有效的数据库标识的对象;然而,在刷新中被删除的对象的情况下,它们一直处于一个灰色地带,它们并不真正“分离”于Session,因为它们仍然可以在回滚中恢复,但也不真正“持久”,因为它们的数据库标识已被删除,并且它们不在标识映射中。

为了解决这种灰色地带的新事件,引入了一个新的对象状态 deleted。这种状态存在于“持久”和“分离”状态之间。通过Session.delete()标记为删除的对象将保持在“持久”状态,直到进行刷新;在那时,它将从标识映射中移除,转移到“已删除”状态,并调用SessionEvents.persistent_to_deleted()钩子。如果Session对象的事务被回滚,对象将恢复为持久状态;将调用SessionEvents.deleted_to_persistent()转换。否则,如果Session对象的事务被提交,将调用SessionEvents.deleted_to_detached()转换。

此外,InstanceState.persistent访问器不再返回 True,表示对象处于新的“已删除”状态;相反,InstanceState.deleted访问器已经增强,可可靠地报告这种新状态。当对象被分离时,InstanceState.deleted返回 False,而InstanceState.detached访问器返回 True。要确定对象是在当前事务中还是在以前的事务中被删除,请使用InstanceState.was_deleted访问器。

强身份映射已被弃用

新系列过渡事件的灵感之一是实现对物体的无泄漏跟踪,使其在身份映射中进出时可以保持“强引用”,从而反映物体在此映射中的移动。有了这种新功能,不再需要Session.weak_identity_map参数和相应的StrongIdentityMap对象。这个选项在 SQLAlchemy 中已经存在多年,因为“强引用”行为曾经是唯一可用的行为,许多应用程序都假定了这种行为。长期以来,强引用跟踪对象不应该是Session的固有工作,而应该是应用程序级别的构造,根据应用程序的需要构建;新的事件模型甚至允许复制强身份映射的确切行为。查看 Session Referencing Behavior 以获取一个新的示例,说明如何替换强身份映射。

#2677 ### 新的 init_scalar()事件在 ORM 级别拦截默认值

当首次访问未设置的属性时,ORM 会为非持久对象产生一个值为None的值:

>>> obj = MyObj()
>>> obj.some_value
None

即使在对象持久化之前,这种在 Python 中的值与 Core 生成的默认值对应的用例也是存在的。为了适应这种用例,新增了一个名为 AttributeEvents.init_scalar() 的事件。在 Attribute Instrumentation 中的新示例 active_column_defaults.py 展示了一个样例用法,所以效果可以是:

>>> obj = MyObj()
>>> obj.some_value
"my default"

#1311 ### 关于“不可哈希”类型的更改,影响 ORM 行的去重

Query 对象具有“去重”返回的行的良好行为,其中包含至少一个 ORM 映射实体(例如,一个完全映射的对象,而不是单个列值)。这主要是为了确保实体的处理与标识映射一起顺利进行,包括在连接的急加载中通常表示的重复实体,以及当用于过滤附加列时使用连接时。

这种去重依赖于行中元素的可哈希性。随着 PostgreSQL 的特殊类型(如 ARRAYHSTOREJSON)的引入,行内类型不可哈希并在这里遇到问题的经历比以前更加普遍。

实际上,自 SQLAlchemy 版本 0.8  起,已经在被标记为“不可哈希”的数据类型上包含了一个标志,然而这个标志在内置类型上并不一致。如 ARRAY 和 JSON  类型现在正确指定“不可哈希” 中描述的那样,现在这个标志已经一致地设置在了所有 PostgreSQL 的“结构”类型上。

NullType 类型上也设置了“不可哈希”标志,因为 NullType 用于指代任何未知类型的表达式。

由于大多数使用 func 的地方都应用了 NullType,因为在大多数情况下,func 实际上并不了解给定的函数名称,使用 func() 通常会禁用行去重,除非显式类型化。以下示例说明了将 func.substr() 应用于字符串表达式和将 func.date() 应用于日期时间表达式;两个示例都将由于连接的急加载而返回重复行,除非应用了显式类型化:

result = (
    session.query(func.substr(A.some_thing, 0, 4), A).options(joinedload(A.bs)).all()
)
users = (
    session.query(
        func.date(User.date_created, "start of month").label("month"),
        User,
    )
    .options(joinedload(User.orders))
    .all()
)

为了保留去重,上述示例应指定为:

result = (
    session.query(func.substr(A.some_thing, 0, 4, type_=String), A)
    .options(joinedload(A.bs))
    .all()
)
users = (
    session.query(
        func.date(User.date_created, "start of month", type_=DateTime).label("month"),
        User,
    )
    .options(joinedload(User.orders))
    .all()
)

此外,对所谓的“不可哈希”类型的处理与之前的版本略有不同;在内部,我们使用id()函数从这些结构中获取“哈希值”,就像我们对待任何普通的映射对象一样。这取代了之前将计数器应用于对象的方法。

#3499 ### 添加了针对传递映射类、实例作为 SQL 字面值的特定检查

现在,类型系统对在上下文中传递 SQLAlchemy“可检查”对象进行了特定检查,否则它们将被处理为字面值。任何 SQLAlchemy 内置对象,只要作为 SQL 值传递是合法的(不是已经是ClauseElement实例),都包括一个方法__clause_element__(),为该对象提供有效的 SQL 表达式。对于不提供此方法的 SQLAlchemy 对象,如映射类、映射器和映射实例,会发出更具信息性的错误消息,而不是允许 DBAPI 接收对象并稍后失败。下面举例说明,其中基于字符串的属性User.nameUser()的完整实例进行比较,而不是与字符串值进行比较:

>>> some_user = User()
>>> q = s.query(User).filter(User.name == some_user)
sqlalchemy.exc.ArgumentError: Object <__main__.User object at 0x103167e90> is not legal as a SQL literal value

当进行User.name == some_user比较时,异常现在是立即的。以前,类似上面的比较会产生一个 SQL 表达式,只有在解析为 DBAPI 执行调用时才会失败;映射的User对象最终会成为一个被 DBAPI 拒绝的绑定参数。

请注意,在上面的示例中,表达式失败是因为User.name是基于字符串的(例如基于列的)属性。此更改不会影响将多对一关系属性与对象进行比较的常规情况,这是另外处理的:

>>> # Address.user refers to the User mapper, so
>>> # this is of course still OK!
>>> q = s.query(Address).filter(Address.user == some_user)

#3321 ### 新的可索引 ORM 扩展

可索引扩展是混合属性功能的一个扩展,允许构建引用“可索引”数据类型(如数组或 JSON 字段)特定元素的属性:

class Person(Base):
    __tablename__ = "person"
    id = Column(Integer, primary_key=True)
    data = Column(JSON)
    name = index_property("data", "name")

上面,name属性将读取/写入 JSON 列data中的字段"name",在初始化为空字典后:

>>> person = Person(name="foobar")
>>> person.name
foobar

该扩展还在修改属性时触发更改事件,因此无需使用MutableDict来跟踪此更改。

另请参见

可索引 ### 新选项允许显式持久化 NULL 覆盖默认值

与 PostgreSQL 中添加的新 JSON-NULL 支持相关,作为 JSON “null”在 ORM 操作中如预期般插入,当不存在时被省略的一部分,基础TypeEngine类现在支持一个方法TypeEngine.evaluates_none(),允许将属性上的None值的正值集合持久化为 NULL,而不是从 INSERT 语句中省略列,这会导致使用列级默认值。这允许对现有对象级技术分配null()到属性的映射器级配置。

另请参阅

强制在具有默认值的列上使用 NULL

#3250 ### 进一步修复了单表继承查询问题

继续从 1.0 的在使用 from_self(),count()时更改单表继承条件,Query在查询针对子查询表达式时,如 exists 时,不应再不当地添加“单一继承”条件:

class Widget(Base):
    __tablename__ = "widget"
    id = Column(Integer, primary_key=True)
    type = Column(String)
    data = Column(String)
    __mapper_args__ = {"polymorphic_on": type}
class FooWidget(Widget):
    __mapper_args__ = {"polymorphic_identity": "foo"}
q = session.query(FooWidget).filter(FooWidget.data == "bar").exists()
session.query(q).all()

生成:

SELECT  EXISTS  (SELECT  1
FROM  widget
WHERE  widget.data  =  :data_1  AND  widget.type  IN  (:type_1))  AS  anon_1

内部的 IN 子句是合适的,以限制为 FooWidget 对象,但以前 IN 子句也会在子查询的外部生成第二次。

#3582 ### 当数据库取消 SAVEPOINT 时改进的 Session 状态

MySQL 的一个常见情况是,在事务中发生死锁时,SAVEPOINT 被取消。Session已经修改以更优雅地处理这种失败模式,使得外部的非 SAVEPOINT 事务仍然可用:

s = Session()
s.begin_nested()
s.add(SomeObject())
try:
    # assume the flush fails, flush goes to rollback to the
    # savepoint and that also fails
    s.flush()
except Exception as err:
    print("Something broke, and our SAVEPOINT vanished too")
# this is the SAVEPOINT transaction, marked as
# DEACTIVE so the rollback() call succeeds
s.rollback()
# this is the outermost transaction, remains ACTIVE
# so rollback() or commit() can succeed
s.rollback()

这个问题是#2696的延续,在 Python 2 上运行时我们发出警告,即使 SAVEPOINT 异常优先。在 Python 3 上,异常被链接,因此两个失败都会被单独报告。

#3680 ### 修复了错误的“新实例 X 与持久实例 Y 冲突”刷新错误

Session.rollback() 方法负责移除在数据库中被 INSERT 的对象,例如在那个现在被回滚的事务中从挂起状态移动到持久状态的对象。进行此状态更改的对象在一个弱引用集合中被跟踪,如果一个对象从该集合中被垃圾回收,Session  将不再关心它(否则对于在事务中插入许多新对象的操作不会扩展)。然而,如果应用程序在回滚发生之前重新加载了同一被垃圾回收的行,那么会出现问题;如果对这个对象的强引用仍然存在于下一个事务中,那么这个对象未被插入且应该被移除的事实将丢失,并且  flush 将错误地引发错误:

from sqlalchemy import Column, create_engine
from sqlalchemy.orm import Session
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)
s = Session(e)
# persist an object
s.add(A(id=1))
s.flush()
# rollback buffer loses reference to A
# load it again, rollback buffer knows nothing
# about it
a1 = s.query(A).first()
# roll back the transaction; all state is expired but the
# "a1" reference remains
s.rollback()
# previous "a1" conflicts with the new one because we aren't
# checking that it never got committed
s.add(A(id=1))
s.commit()

上述程序将引发:

FlushError: New instance <User at 0x7f0287eca4d0> with identity key
(<class 'test.orm.test_transaction.User'>, ('u1',)) conflicts
with persistent instance <User at 0x7f02889c70d0>

问题在于当引发上述异常时,工作单元正在处理原始对象,假设它是一个活动行,而实际上该对象已过期,并在测试中显示它已经消失。修复现在测试这个条件,因此在 SQL 日志中我们看到:

BEGIN  (implicit)
INSERT  INTO  a  (id)  VALUES  (?)
(1,)
SELECT  a.id  AS  a_id  FROM  a  LIMIT  ?  OFFSET  ?
(1,  0)
ROLLBACK
BEGIN  (implicit)
SELECT  a.id  AS  a_id  FROM  a  WHERE  a.id  =  ?
(1,)
INSERT  INTO  a  (id)  VALUES  (?)
(1,)
COMMIT

在上述情况下,工作单元现在会为我们即将报告为冲突的行执行一个 SELECT,看到它不存在,然后正常进行。这个 SELECT 的开销只在我们本来会在任何情况下错误地引发异常时才会发生。

#3677 ### 被动删除功能用于连接继承映射

现在,一个连接表继承映射现在可以允许 DELETE 操作继续进行,作为 Session.delete() 的结果,它只为基本表发出 DELETE,而不是子类表,允许配置的 ON DELETE CASCADE 为配置的外键发生。这是使用 mapper.passive_deletes 选项配置的:

from sqlalchemy import Column, Integer, String, ForeignKey, create_engine
from sqlalchemy.orm import Session
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class A(Base):
    __tablename__ = "a"
    id = Column("id", Integer, primary_key=True)
    type = Column(String)
    __mapper_args__ = {
        "polymorphic_on": type,
        "polymorphic_identity": "a",
        "passive_deletes": True,
    }
class B(A):
    __tablename__ = "b"
    b_table_id = Column("b_table_id", Integer, primary_key=True)
    bid = Column("bid", Integer, ForeignKey("a.id", ondelete="CASCADE"))
    data = Column("data", String)
    __mapper_args__ = {"polymorphic_identity": "b"}

使用上述映射,mapper.passive_deletes 选项在基本映射器上进行配置;它对所有具有该选项设置的映射器的非基本映射器生效。对于类型为 B 的对象的 DELETE 不再需要检索 b_table_id 的主键值(如果未加载),也不需要为表本身发出 DELETE 语句:

session.delete(some_b)
session.commit()

将生成的 SQL 如下:

DELETE  FROM  a  WHERE  a.id  =  %(id)s
-- {'id': 1}
COMMIT

一如既往,目标数据库必须支持启用 ON DELETE CASCADE 的外键支持。

#2349 ### 同名反向引用应用于具体继承子类时不会引发错误

以下映射一直是可以无问题地进行的:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    b = relationship("B", foreign_keys="B.a_id", backref="a")
class A1(A):
    __tablename__ = "a1"
    id = Column(Integer, primary_key=True)
    b = relationship("B", foreign_keys="B.a1_id", backref="a1")
    __mapper_args__ = {"concrete": True}
class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))
    a1_id = Column(ForeignKey("a1.id"))

上述情况下,即使类 A 和类 A1 有一个名为 b 的关系,也不会出现冲突警告或错误,因为类 A1 被标记为“具体”。

然而,如果关系配置反过来,将会出现错误:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
class A1(A):
    __tablename__ = "a1"
    id = Column(Integer, primary_key=True)
    __mapper_args__ = {"concrete": True}
class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))
    a1_id = Column(ForeignKey("a1.id"))
    a = relationship("A", backref="b")
    a1 = relationship("A1", backref="b")

修复增强了反向引用功能,以便不会发出错误,同时在映射器逻辑中增加了额外的检查,以避免替换属性时发出警告。

#3630 ### 不再对继承映射器上的同名关系发出警告

在继承场景中创建两个映射器时,在两者上都放置同名关系会发出警告“关系’‘在映射器上取代了继承映射器’'上的相同关系;这可能会在刷新期间引起依赖问题”。示例如下:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    bs = relationship("B")
class ASub(A):
    __tablename__ = "a_sub"
    id = Column(Integer, ForeignKey("a.id"), primary_key=True)
    bs = relationship("B")
class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

这个警告可以追溯到 2007 年的 0.4  系列,基于一个自那时起已完全重写的工作单元代码版本。目前,没有关于在基类和派生类上放置同名关系的已知问题,因此警告已被取消。但是,请注意,由于警告,这种用例在现实世界中可能并不常见。虽然为这种用例添加了基本的测试支持,但可能会发现这种模式的一些新问题。

1.1.0b3 版本中新增。

#3749 ### 混合属性和方法现在也传播文档字符串以及.info

现在,混合方法或属性将反映原始文档字符串中存在的__doc__值:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    name = Column(String)
    @hybrid_property
    def some_name(self):
  """The name field"""
        return self.name

现在,A.some_name.__doc__的上述值将被尊重:

>>> A.some_name.__doc__
The name field

然而,为了实现这一点,混合属性的机制必然变得更加复杂。以前,混合的类级访问器将是一个简单的传递,也就是说,这个测试将成功:

>>> assert A.name is A.some_name

随着这一变化,A.some_name返回的表达式现在被包装在自己的QueryableAttribute包装器中:

>>> A.some_name
<sqlalchemy.orm.attributes.hybrid_propertyProxy object at 0x7fde03888230>

为了确保这个包装器能够正确工作,进行了大量测试,包括对自定义值对象配方的复杂方案,但我们将继续关注用户是否出现其他退化情况。

作为这一变化的一部分,hybrid_property.info集合现在也从混合描述符本身传播,而不是从底层表达式传播。也就是说,访问A.some_name.info现在返回与inspect(A).all_orm_descriptors['some_name'].info相同的字典:

>>> A.some_name.info["foo"] = "bar"
>>> from sqlalchemy import inspect
>>> inspect(A).all_orm_descriptors["some_name"].info
{'foo': 'bar'}

请注意,这个.info字典独立于可能直接代理的混合描述符的映射属性的字典;这是从 1.0 版本开始的行为变化。包装器仍将代理镜像属性的其他有用属性,如QueryableAttribute.propertyQueryableAttribute.class_

#3653 ### Session.merge 解决挂起冲突与持久性相同

Session.merge() 方法现在将跟踪给定图中对象的标识,以在发出 INSERT 之前维护主键唯一性。当遇到相同标识的重复对象时,非主键属性会在遇到对象时被覆盖,这本质上是非确定性的。这种行为与持久对象的处理方式相匹配,即通过主键已经位于数据库中的对象,因此这种行为更具内部一致性。

给定:

u1 = User(id=7, name="x")
u1.orders = [
    Order(description="o1", address=Address(id=1, email_address="a")),
    Order(description="o2", address=Address(id=1, email_address="b")),
    Order(description="o3", address=Address(id=1, email_address="c")),
]
sess = Session()
sess.merge(u1)

在上面的例子中,我们将一个User对象与三个新的Order对象合并,每个对象都引用一个不同的Address对象,但每个对象都具有相同的主键。Session.merge() 的当前行为是在标识映射中查找这个Address对象,并将其用作目标。如果对象存在,意味着数据库已经有了主键为“1”的Address行,我们可以看到Addressemail_address字段将被覆盖三次,在这种情况下分别为 a、b 和最后是 c。

然而,如果主键为“1”的Address行不存在,Session.merge() 将创建三个单独的Address实例,然后在插入时会出现主键冲突。新的行为是,这些Address对象的拟议主键被跟踪在一个单独的字典中,以便我们将三个拟议的Address对象的状态合并到一个要插入的Address对象上。

如果原始情况发出某种警告,表明单个合并树中存在冲突数据可能更好,然而多年来,对于持久情况,值的非确定性合并一直是行为;现在对于挂起情况也是如此。警告存在冲突值的功能仍然对于两种情况都是可行的,但会增加相当大的性能开销,因为在合并过程中每个列值都必须进行比较。

#3601 ### 修复涉及用户发起的外键操作的多对一对象移动问题

已修复涉及用另一个对象替换对对象的多对一引用的机制的错误。在属性操作期间,先前引用的对象的位置现在使用数据库提交的外键值,而不是当前的外键值。修复的主要效果是,当进行多对一更改时,向集合发出的反向引用事件将更准确地触发,即使在之前手动将外键属性移动到新值。假设类ParentSomeClass的映射,其中SomeClass.parent指向ParentParent.items指向SomeClass对象的集合:

some_object = SomeClass()
session.add(some_object)
some_object.parent_id = some_parent.id
some_object.parent = some_parent

在上面的例子中,我们创建了一个待处理的对象some_object,将其外键指向Parent以引用它,然后我们实际设置了关系。在修复错误之前,反向引用不会触发:

# before the fix
assert some_object not in some_parent.items

现在的修复是,当我们试图定位some_object.parent的先前值时,我们忽略了先前手动设置的父 id,并寻找数据库提交的值。在这种情况下,它是 None,因为对象是待处理的,所以事件系统将some_object.parent记录为净变化:

# after the fix, backref fired off for some_object.parent = some_parent
assert some_object in some_parent.items

尽管不鼓励操纵由关系管理的外键属性,但对于这种用例有有限的支持。为了允许加载继续进行,经常会使用Session.enable_relationship_loading()RelationshipProperty.load_on_pending功能,这些功能会导致基于内存中未持久化的外键值发出惰性加载的关系。无论是否使用这些功能,这种行为改进现在将变得明显。

#3708 ### 改进 Query.correlate 方法与多态实体

在最近的 SQLAlchemy 版本中,许多形式的“多态”查询生成的 SQL 比以前更“扁平化”,其中多个表的 JOIN 不再无条件地捆绑到子查询中。为了适应这一变化,Query.correlate()方法现在从这样的多态可选择中提取各个表,并确保它们都是子查询的“相关”部分。假设映射文档中的Person/Manager/Engineer->Company设置,使用with_polymorphic

sess.query(Person.name).filter(
    sess.query(Company.name)
    .filter(Company.company_id == Person.company_id)
    .correlate(Person)
    .as_scalar()
    == "Elbonia, Inc."
)

上述查询现在会产生:

SELECT  people.name  AS  people_name
FROM  people
LEFT  OUTER  JOIN  engineers  ON  people.person_id  =  engineers.person_id
LEFT  OUTER  JOIN  managers  ON  people.person_id  =  managers.person_id
WHERE  (SELECT  companies.name
FROM  companies
WHERE  companies.company_id  =  people.company_id)  =  ?

在修复之前,调用correlate(Person)会无意中尝试将PersonEngineerManager的连接作为一个单元进行关联,因此Person不会被关联:

-- old, incorrect query
SELECT  people.name  AS  people_name
FROM  people
LEFT  OUTER  JOIN  engineers  ON  people.person_id  =  engineers.person_id
LEFT  OUTER  JOIN  managers  ON  people.person_id  =  managers.person_id
WHERE  (SELECT  companies.name
FROM  companies,  people
WHERE  companies.company_id  =  people.company_id)  =  ?

对多态映射使用相关子查询仍然存在一些未完善的地方。例如,如果Person多态链接到所谓的“具体多态联合”查询,上述子查询可能无法正确引用此子查询。在所有情况下,完全引用“多态”实体的一种方法是首先从中创建一个aliased()对象:

# works with all SQLAlchemy versions and all types of polymorphic
# aliasing.
paliased = aliased(Person)
sess.query(paliased.name).filter(
    sess.query(Company.name)
    .filter(Company.company_id == paliased.company_id)
    .correlate(paliased)
    .as_scalar()
    == "Elbonia, Inc."
)

aliased()构造保证了“多态可选择”被包装在一个子查询中。通过在相关子查询中明确引用它,多态形式将被正确使用。

#3662 ### 查询的字符串化将查询会话以获取正确的方言

Query对象调用str()将会查询Session以获取正确的“绑定”,以便渲染将传递给数据库的 SQL。特别是,这允许引用特定于方言的 SQL 构造的Query可呈现,假设Query与适当的Session相关联。以前,只有当映射关联到的MetaData本身绑定到目标Engine时,此行为才会生效。

如果底层的MetaDataSession都未与任何绑定的Engine相关联,则将使用“默认”方言回退来生成 SQL 字符串。

另请参见

“友好”的核心 SQL 构造的字符串化,没有方言

#3081 ### 在一行中多次出现相同实体的连接贪婪加载

已修复了一个情况,即通过连接贪婪加载加载属性,即使实体已经从不包括属性的不同“路径”上的行加载。这是一个难以复现的深层用例,但一般思路如下:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    b_id = Column(ForeignKey("b.id"))
    c_id = Column(ForeignKey("c.id"))
    b = relationship("B")
    c = relationship("C")
class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    c_id = Column(ForeignKey("c.id"))
    c = relationship("C")
class C(Base):
    __tablename__ = "c"
    id = Column(Integer, primary_key=True)
    d_id = Column(ForeignKey("d.id"))
    d = relationship("D")
class D(Base):
    __tablename__ = "d"
    id = Column(Integer, primary_key=True)
c_alias_1 = aliased(C)
c_alias_2 = aliased(C)
q = s.query(A)
q = q.join(A.b).join(c_alias_1, B.c).join(c_alias_1.d)
q = q.options(
    contains_eager(A.b).contains_eager(B.c, alias=c_alias_1).contains_eager(C.d)
)
q = q.join(c_alias_2, A.c)
q = q.options(contains_eager(A.c, alias=c_alias_2))

上述查询生成的 SQL 如下:

SELECT
  d.id  AS  d_id,
  c_1.id  AS  c_1_id,  c_1.d_id  AS  c_1_d_id,
  b.id  AS  b_id,  b.c_id  AS  b_c_id,
  c_2.id  AS  c_2_id,  c_2.d_id  AS  c_2_d_id,
  a.id  AS  a_id,  a.b_id  AS  a_b_id,  a.c_id  AS  a_c_id
FROM
  a
  JOIN  b  ON  b.id  =  a.b_id
  JOIN  c  AS  c_1  ON  c_1.id  =  b.c_id
  JOIN  d  ON  d.id  =  c_1.d_id
  JOIN  c  AS  c_2  ON  c_2.id  =  a.c_id

我们可以看到c表被选择两次;一次在A.b.c -> c_alias_1的上下文中,另一次在A.c -> c_alias_2的上下文中。此外,我们可以看到对于单行来说,C标识很可能对于c_alias_1c_alias_2相同的,这意味着一行中的两组列导致只有一个新对象被添加到标识映射中。

上述查询选项仅要求在c_alias_1的上下文中加载属性C.d,而不是c_alias_2。因此,我们在标识映射中得到的最终C对象是否加载了C.d属性取决于映射如何遍历,尽管不完全是随机的,但基本上是不确定的。修复的方法是,即使对于它们都引用相同标识的单行,c_alias_1的加载器在c_alias_2的加载器之后处理,C.d元素仍将被加载。以前,加载器不寻求修改已通过不同路径加载的实体的加载。首先到达实体的加载器一直是不确定的,因此在某些情况下,这种修复可能会被检测为行为变化,而在其他情况下则不会。

修复包括两种“多条路径到一个实体”的情况的测试,并且修复应该希望覆盖所有其他类似情况。

#3431

新增了对变异跟踪扩展的 MutableList 和 MutableSet 助手

新的助手类MutableListMutableSet已添加到变异跟踪扩展中,以补充现有的MutableDict助手。

#3297

新的“raise” / “raise_on_sql”加载策略

为了帮助防止一系列对象加载后发生不必要的延迟加载,可以将新的“lazy=‘raise’”和“lazy=‘raise_on_sql’”策略以及相应的加载器选项raiseload()应用于关系属性,当访问非急切加载属性进行读取时,将引发InvalidRequestError。这两个变体测试任何类型的延迟加载,包括那些只会返回 None 或从标识映射中检索的延迟加载:

>>> from sqlalchemy.orm import raiseload
>>> a1 = s.query(A).options(raiseload(A.some_b)).first()
>>> a1.some_b
Traceback (most recent call last):
...
sqlalchemy.exc.InvalidRequestError: 'A.some_b' is not available due to lazy='raise'

或仅在 SQL 会被发出的情况下进行延迟加载:

>>> from sqlalchemy.orm import raiseload
>>> a1 = s.query(A).options(raiseload(A.some_b, sql_only=True)).first()
>>> a1.some_b
Traceback (most recent call last):
...
sqlalchemy.exc.InvalidRequestError: 'A.bs' is not available due to lazy='raise_on_sql'

#3512 ### Mapper.order_by 已弃用

这个来自 SQLAlchemy 最早版本的旧参数是 ORM 的原始设计的一部分,其中包括Mapper对象作为一个公共查询结构。这个角色早已被Query对象取代,我们使用Query.order_by()来指示结果的排序方式,这种方式对于任何组合的 SELECT 语句、实体和 SQL 表达式都能一致工作。有许多情况下Mapper.order_by不按预期工作(或者预期的结果不清楚),比如当查询组合成联合时;这些情况不受支持。

#3394 ### 新的会话生命周期事件

Session长期以来一直支持事件,允许在某种程度上跟踪对象状态的变化,包括SessionEvents.before_attach()SessionEvents.after_attach()SessionEvents.before_flush()。会话文档还记录了主要对象状态在对象状态快速入门。然而,从来没有一种系统可以跟踪对象特别是当它们通过这些转换时。此外,“已删除”对象的状态历来是模糊的,因为对象在“持久”状态和“分离”状态之间的行为。

为了清理这个领域并使会话状态转换的领域完全透明,已经添加了一系列新的事件,旨在涵盖对象可能在状态之间转换的每种可能方式,此外,“已删除”状态已在会话对象状态领域内被赋予了自己的官方状态名称。

新的状态转换事件

现在可以拦截对象的所有状态之间的转换,比如 persistent、pending 等,以便覆盖特定转换的会话级事件。对象进入Session、离开Session,甚至在使用Session.rollback()回滚事务时发生的所有转换都明确地出现在SessionEvents的接口中。

总共有十个新事件。这些事件的摘要在新编写的文档部分对象生命周期事件中。

添加了新的对象状态“deleted”,被删除的对象不再是“persistent”。

对象在Session中的 persistent 状态一直被记录为具有有效的数据库标识的对象;然而,在刷新时被删除的对象的情况下,它们一直处于一个灰色地带,因为它们并没有真正“分离”出Session,因为它们仍然可以在回滚时恢复,但又不真正“持久”,因为它们的数据库标识已被删除,而且它们不在标识映射中。

为了解决这个新事件所带来的灰色地带,引入了一个新的对象状态 deleted。此状态存在于“持久”状态和“分离”状态之间。通过 Session.delete() 标记为删除的对象将保持在“持久”状态,直到进行刷新为止;在那时,它将从标识映射中移除,转移到“已删除”状态,并调用 SessionEvents.persistent_to_deleted() 钩子。如果 Session 对象的事务被回滚,则对象将被恢复为持久状态;将调用 SessionEvents.deleted_to_persistent() 过渡。否则,如果 Session 对象的事务被提交,则调用 SessionEvents.deleted_to_detached() 过渡。

另外,InstanceState.persistent 访问器不再返回 True以表示处于新“已删除”状态的对象;相反,InstanceState.deleted 访问器已经增强,可可靠地报告此新状态。当对象被分离时,InstanceState.deleted 返回 False,而 InstanceState.detached 访问器则返回 True。要确定对象是在当前事务中还是在以前的事务中被删除,请使用 InstanceState.was_deleted 访问器。

强身份映射已不推荐使用。

新系列转换事件的灵感之一是实现对象在进出标识映射时的无泄漏跟踪,以便维护一个“强引用”,反映对象在此映射中的进出情况。有了这种新的功能,就不再需要Session.weak_identity_map参数和相应的StrongIdentityMap对象。多年来,此选项一直保留在 SQLAlchemy 中,因为“强引用”行为曾经是唯一可用的行为,并且许多应用程序都是根据这种行为编写的。长期以来,已建议不要将对象的强引用跟踪作为Session的内在工作,并且应该作为应用程序需要时由应用程序构建的构造体;新的事件模型甚至允许复制强标识映射的确切行为。请参阅会话引用行为以了解如何替换强标识映射的新方法。

#2677

新的状态转换事件

所有对象状态之间的转换,如 persistent、pending 等,现在都可以通过会话级事件进行拦截,以涵盖特定转换。对象转换为Session时,移出Session时,甚至在使用Session.rollback()回滚事务时发生的所有转换,在SessionEvents的接口中都明确存在。

总共有十个新事件。这些事件的摘要在新编写的文档部分对象生命周期事件中。

添加了新的对象状态“已删除”,已删除的对象不再“持久”。

对象在Session中的持久状态一直被记录为具有有效的数据库标识符;然而,在被删除的对象的情况下,在刷新时它们一直处于一个灰色地带,它们并不真正“分离”于Session,因为它们仍然可以在回滚中恢复,但并不真正“持久”,因为它们的数据库标识已被删除,并且不在标识映射中。

为了解决这个灰色地带,引入了一个新的对象状态删除。这种状态存在于“持久”和“分离”状态之间。通过Session.delete()标记为删除的对象将保持在“持久”状态,直到刷新进行;在那时,它将从标识映射中移除,转移到“删除”状态,并调用SessionEvents.persistent_to_deleted()钩子。如果Session对象的事务被回滚,对象将恢复为持久状态;调用SessionEvents.deleted_to_persistent()转换。否则,如果Session对象的事务被提交,将调用SessionEvents.deleted_to_detached()转换。

此外,InstanceState.persistent访问器不再返回 True,用于处于新“已删除”状态的对象;相反,InstanceState.deleted访问器已经增强,可可靠地报告这种新状态。当对象被分离时,InstanceState.deleted返回 False,而InstanceState.detached访问器返回 True。要确定对象是在当前事务中删除还是在以前的事务中删除,使用InstanceState.was_deleted访问器。

强标识映射已弃用

新系列过渡事件的灵感之一是为了实现对象在进出标识映射时的无泄漏跟踪,以便维护“强引用”,反映对象在此映射中进出的情况。有了这种新能力,就不再需要Session.weak_identity_map参数和相应的StrongIdentityMap对象。这个选项在 SQLAlchemy 中已经存在多年,因为“强引用”行为曾经是唯一可用的行为,许多应用程序都假定了这种行为。长期以来,强引用跟踪对象不是Session的固有工作,而是一个应用程序级别的构造,根据应用程序的需要构建;新的事件模型甚至允许复制强标识映射的确切行为。查看 Session Referencing Behavior 以获取一个新的示例,说明如何替换强标识映射。

#2677

新的init_scalar()事件在 ORM 级别拦截默认值

当首次访问未设置的属性时,ORM 会为非持久对象生成一个值为None

>>> obj = MyObj()
>>> obj.some_value
None

在对象持久化之前,有一个用例是使此 Python 值对应于 Core 生成的默认值。为了适应这种用例,添加了一个新的事件AttributeEvents.init_scalar()。在属性仪器化中的新示例active_column_defaults.py说明了一个示例用法,因此效果可以是:

>>> obj = MyObj()
>>> obj.some_value
"my default"

#1311

关于“不可哈希”类型的更改,影响 ORM 行的去重

Query对象具有“去重”返回行的良好行为,其中包含至少一个 ORM 映射实体(例如,完全映射对象,而不是单独的列值)。这主要是为了使实体的处理与标识映射平滑配合,包括适应通常在连接的急加载中表示的重复实体,以及在使用连接以过滤其他列的目的时。

此去重依赖于行内元素的可哈希性。随着 PostgreSQL 引入特殊类型如ARRAYHSTOREJSON,行内类型不可哈希且在此遇到问题的情况比以往更普遍。

实际上,自 SQLAlchemy 版本 0.8  以来,已经在被标记为“不可哈希”的数据类型上包含了一个标志,然而这个标志在内置类型上并没有一致使用。正如 ARRAY 和 JSON  类型现在正确指定“不可哈希”所述,这个标志现在已经为所有 PostgreSQL 的“结构”类型一致设置。

“不可哈希”标志也设置在NullType类型上,因为NullType用于引用任何未知类型的表达式。

由于NullType应用于大多数func的用法,因为func实际上并不知道在大多数情况下给定的函数名称,使用 func()通常会禁用行去重,除非应用了显式类型。以下示例说明了将func.substr()应用于字符串表达式,以及将func.date()应用于日期时间表达式;这两个示例将由于连接的急加载而返回重复行,除非应用了显式类型:

result = (
    session.query(func.substr(A.some_thing, 0, 4), A).options(joinedload(A.bs)).all()
)
users = (
    session.query(
        func.date(User.date_created, "start of month").label("month"),
        User,
    )
    .options(joinedload(User.orders))
    .all()
)

为了保留去重,上面的示例应指定为:

result = (
    session.query(func.substr(A.some_thing, 0, 4, type_=String), A)
    .options(joinedload(A.bs))
    .all()
)
users = (
    session.query(
        func.date(User.date_created, "start of month", type_=DateTime).label("month"),
        User,
    )
    .options(joinedload(User.orders))
    .all()
)

另外,对于所谓的“不可哈希”类型的处理略有不同,与之前的发布版本有些不同;在内部,我们使用 id() 函数从这些结构中获取“哈希值”,就像我们对任何普通映射对象一样。这取代了以前的方法,该方法对对象应用了一个计数器。

#3499

添加了用于传递映射类、实例作为 SQL 文字的特定检查

现在,类型系统对于在否则会被处理为文字值的上下文中传递 SQLAlchemy “可检查”对象具有特定检查。任何可以作为 SQL 值传递的 SQLAlchemy 内置对象(它不是已经是 ClauseElement 实例的对象)都包含一个方法 __clause_element__(),该方法为该对象提供一个有效的 SQL 表达式。对于不提供此功能的 SQLAlchemy 对象,例如映射类、映射器和映射实例,将发出更详细的错误消息,而不是允许 DBAPI 接收对象并稍后失败。下面举例说明了一个示例,其中将字符串属性 User.nameUser() 的完整实例进行比较,而不是与字符串值进行比较:

>>> some_user = User()
>>> q = s.query(User).filter(User.name == some_user)
sqlalchemy.exc.ArgumentError: Object <__main__.User object at 0x103167e90> is not legal as a SQL literal value

现在,当比较User.name == some_user时,异常立即发生。以前,类似上述的比较会产生一个 SQL 表达式,只有在解析为 DBAPI 执行调用时才会失败;映射的 User 对象最终会变成一个被 DBAPI 拒绝的绑定参数。

请注意,在上面的示例中,表达式失败是因为 User.name 是基于字符串的(例如列导向)属性。此更改不会影响通常情况下将多对一关系属性与对象进行比较的情况,这是单独处理的:

>>> # Address.user refers to the User mapper, so
>>> # this is of course still OK!
>>> q = s.query(Address).filter(Address.user == some_user)

#3321

新的可索引 ORM 扩展

可索引 扩展是对混合属性功能的扩展,它允许构建引用特定元素的属性,这些元素属于“可索引”数据类型,例如数组或 JSON 字段:

class Person(Base):
    __tablename__ = "person"
    id = Column(Integer, primary_key=True)
    data = Column(JSON)
    name = index_property("data", "name")

在上面的示例中,name 属性将从 JSON 列 data 读取/写入字段 "name",在将其初始化为空字典之后:

>>> person = Person(name="foobar")
>>> person.name
foobar

该扩展还在修改属性时触发更改事件,因此无需使用 MutableDict 来跟踪此更改。

另请参阅

可索引

新选项允许明确持久化 NULL 覆盖默认值

与 PostgreSQL 中新增的 JSON-NULL 支持相关,作为 JSON “null” is inserted as expected with ORM operations, omitted when not present 的一部分,基础 TypeEngine 类现在支持一个方法 TypeEngine.evaluates_none(),允许将属性上的 None 值设置为 NULL,而不是在 INSERT 语句中省略该列,这会导致使用列级默认值。这允许在映射器级别配置现有的对象级别将 null() 分配给属性的技术。

另请参见

强制在具有默认值的列上使用 NULL

#3250

进一步修复单表继承查询

继续从 1.0 的 Change to single-table-inheritance criteria when using from_self(), count() 中,Query 在查询针对子查询表达式(如 exists)时不应再不适当地添加“单一继承”条件:

class Widget(Base):
    __tablename__ = "widget"
    id = Column(Integer, primary_key=True)
    type = Column(String)
    data = Column(String)
    __mapper_args__ = {"polymorphic_on": type}
class FooWidget(Widget):
    __mapper_args__ = {"polymorphic_identity": "foo"}
q = session.query(FooWidget).filter(FooWidget.data == "bar").exists()
session.query(q).all()

产生:

SELECT  EXISTS  (SELECT  1
FROM  widget
WHERE  widget.data  =  :data_1  AND  widget.type  IN  (:type_1))  AS  anon_1

内部的 IN 子句是适当的,以限制为 FooWidget 对象,但以前 IN 子句也会在子查询的外部生成第二次。

#3582

当数据库取消 SAVEPOINT 时改进的 Session 状态

MySQL 的一个常见情况是在事务中发生死锁时取消 SAVEPOINT。Session 已经修改以更优雅地处理这种失败模式,使得外部的非 SAVEPOINT 事务仍然可用:

s = Session()
s.begin_nested()
s.add(SomeObject())
try:
    # assume the flush fails, flush goes to rollback to the
    # savepoint and that also fails
    s.flush()
except Exception as err:
    print("Something broke, and our SAVEPOINT vanished too")
# this is the SAVEPOINT transaction, marked as
# DEACTIVE so the rollback() call succeeds
s.rollback()
# this is the outermost transaction, remains ACTIVE
# so rollback() or commit() can succeed
s.rollback()

这个问题是 #2696 的延续,在 Python 2 上运行时我们发出警告,以便可以看到原始错误,即使 SAVEPOINT 异常优先。在 Python 3 上,异常被链接在一起,因此两个失败都会被单独报告。

#3680

修复了错误的“新实例 X 与持久实例 Y 冲突”刷新错误

Session.rollback() 方法负责移除在数据库中被插入的对象,例如从挂起状态移动到持久状态的对象,在被回滚的事务中。使得这种状态改变的对象被跟踪在一个弱引用集合中,如果一个对象从该集合中被垃圾回收,Session  就不再关心它(否则对于在事务中插入许多新对象的操作来说,这种方式不会扩展)。然而,如果在回滚发生之前,应用程序重新加载了同一个被垃圾回收的行;如果对这个对象仍然存在强引用到下一个事务中,那么这个对象没有被插入并且应该被移除的事实将会丢失,刷新将会错误地引发一个错误:

from sqlalchemy import Column, create_engine
from sqlalchemy.orm import Session
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)
s = Session(e)
# persist an object
s.add(A(id=1))
s.flush()
# rollback buffer loses reference to A
# load it again, rollback buffer knows nothing
# about it
a1 = s.query(A).first()
# roll back the transaction; all state is expired but the
# "a1" reference remains
s.rollback()
# previous "a1" conflicts with the new one because we aren't
# checking that it never got committed
s.add(A(id=1))
s.commit()

上述程序将引发:

FlushError: New instance <User at 0x7f0287eca4d0> with identity key
(<class 'test.orm.test_transaction.User'>, ('u1',)) conflicts
with persistent instance <User at 0x7f02889c70d0>

这个 bug 是当上述异常被引发时,工作单元正在处理原始对象,假设它是一个活动行,而实际上该对象已过期,并在测试中显示它已经消失。修复现在测试这个条件,所以在 SQL 日志中我们看到:

BEGIN  (implicit)
INSERT  INTO  a  (id)  VALUES  (?)
(1,)
SELECT  a.id  AS  a_id  FROM  a  LIMIT  ?  OFFSET  ?
(1,  0)
ROLLBACK
BEGIN  (implicit)
SELECT  a.id  AS  a_id  FROM  a  WHERE  a.id  =  ?
(1,)
INSERT  INTO  a  (id)  VALUES  (?)
(1,)
COMMIT

上面,工作单元现在对我们即将报告���冲突的行进行 SELECT,看到它不存在,然后正常进行。这个 SELECT 的开销只在我们本来会错误地引发异常的情况下才会发生。

#3677

联接继承映射的被动删除功能

一个联接表继承映射现在可能允许一个 DELETE 操作继续进行,作为 Session.delete() 的结果,它只对基表发出 DELETE,而不是子类表,允许配置的 ON DELETE CASCADE 为配置的外键发生。这是使用 mapper.passive_deletes 选项进行配置的:

from sqlalchemy import Column, Integer, String, ForeignKey, create_engine
from sqlalchemy.orm import Session
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class A(Base):
    __tablename__ = "a"
    id = Column("id", Integer, primary_key=True)
    type = Column(String)
    __mapper_args__ = {
        "polymorphic_on": type,
        "polymorphic_identity": "a",
        "passive_deletes": True,
    }
class B(A):
    __tablename__ = "b"
    b_table_id = Column("b_table_id", Integer, primary_key=True)
    bid = Column("bid", Integer, ForeignKey("a.id", ondelete="CASCADE"))
    data = Column("data", String)
    __mapper_args__ = {"polymorphic_identity": "b"}

在上述映射中,mapper.passive_deletes 选项被配置在基本映射器上;它对于所有具有该选项设置的映射器的非基本映射器生效。对于类型为 B 的对象的 DELETE 不再需要检索 b_table_id 的主键值(如果未加载),也不需要为表本身发出 DELETE 语句:

session.delete(some_b)
session.commit()

将会发出 SQL 语句:

DELETE  FROM  a  WHERE  a.id  =  %(id)s
-- {'id': 1}
COMMIT

一如既往,目标数据库必须支持启用 ON DELETE CASCADE 的外键支持。

#2349

同名反向引用应用于具体继承子类时不会引发错误

以下映射一直以来都是可能的而没有问题:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    b = relationship("B", foreign_keys="B.a_id", backref="a")
class A1(A):
    __tablename__ = "a1"
    id = Column(Integer, primary_key=True)
    b = relationship("B", foreign_keys="B.a1_id", backref="a1")
    __mapper_args__ = {"concrete": True}
class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))
    a1_id = Column(ForeignKey("a1.id"))

上面,即使类 A 和类 A1 有一个名为 b 的关系,也不会发生冲突警告或错误,因为类 A1 被标记为“具体”。

然而,如果关系被配置为另一种方式,将会发生错误:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
class A1(A):
    __tablename__ = "a1"
    id = Column(Integer, primary_key=True)
    __mapper_args__ = {"concrete": True}
class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))
    a1_id = Column(ForeignKey("a1.id"))
    a = relationship("A", backref="b")
    a1 = relationship("A1", backref="b")

此修复增强了 backref 特性,以便不发出错误,以及在映射器逻辑中进一步检查是否应该绕过替换属性的警告。

#3630

在继承映射器上具有相同名称的关系不再发出警告

在继承情景中创建两个映射器时,在两者上放置具有相同名称的关系将发出警告:“关系’‘在映射器上取代了继承的映射器’'上的相同关系;这可能会在刷新时引起依赖问题”。示例如下:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    bs = relationship("B")
class ASub(A):
    __tablename__ = "a_sub"
    id = Column(Integer, ForeignKey("a.id"), primary_key=True)
    bs = relationship("B")
class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

这个警告可以追溯到 2007 年的 0.4  系列,基于一个自那时完全重写的工作单元代码版本。目前,将同名关系放置在基类和派生类上没有已知问题,因此警告已解除。然而,请注意,由于警告,这种用例在现实世界中可能并不普遍。尽管为此用例添加了基本的测试支持,但可能会发现这种模式的一些新问题。

版本 1.1.0b3 中的新功能。

#3749

现在混合属性和方法也会传播文档字符串以及.info

现在混合方法或属性将反映原始文档字符串中存在的__doc__值:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    name = Column(String)
    @hybrid_property
    def some_name(self):
  """The name field"""
        return self.name

上述值A.some_name.__doc__现在被尊重:

>>> A.some_name.__doc__
The name field

然而,为了实现这一点,混合属性的机制必然变得更加复杂。以前,混合的类级访问器是一个简单的透传,也就是说,这个测试会成功:

>>> assert A.name is A.some_name

随着变化,由A.some_name返回的表达式现在被包装在其自己的QueryableAttribute包装器中:

>>> A.some_name
<sqlalchemy.orm.attributes.hybrid_propertyProxy object at 0x7fde03888230>

已经进行了大量测试,以确保此包装器能够正常工作,包括对自定义值对象配方的复杂方案,但我们将继续关注用户是否出现其他退化。

作为这个改变的一部分,hybrid_property.info集合现在也从混合描述符本身传播,而不是从底层表达式传播。也就是说,现在访问A.some_name.info会返回与inspect(A).all_orm_descriptors['some_name'].info相同的字典:

>>> A.some_name.info["foo"] = "bar"
>>> from sqlalchemy import inspect
>>> inspect(A).all_orm_descriptors["some_name"].info
{'foo': 'bar'}

请注意,此.info字典由混合描述符可能直接代理的映射属性的字典不同;这是从 1.0 开始的行为变更。包装器仍将代理来自镜像属性的其他有用属性,例如QueryableAttribute.propertyQueryableAttribute.class_

#3653

Session.merge 解决未解决的冲突与持久性相同

现在,Session.merge()方法将跟踪给定图中对象的标识,以维护主键的唯一性,然后再发出 INSERT。当遇到相同标识的重复对象时,非主键属性会被覆盖,因为对象被遇到时,这基本上是非确定性的。这种行为与持久对象的处理方式相匹配,即通过主键已经位于数据库中的对象,因此这种行为更加内部一致。

给定:

u1 = User(id=7, name="x")
u1.orders = [
    Order(description="o1", address=Address(id=1, email_address="a")),
    Order(description="o2", address=Address(id=1, email_address="b")),
    Order(description="o3", address=Address(id=1, email_address="c")),
]
sess = Session()
sess.merge(u1)

在上面的例子中,我们将一个User对象与三个新的Order对象合并,每个对象都引用一个不同的Address对象,但每个对象都具有相同的主键。Session.merge()的当前行为是在标识映射中查找这个Address对象,并将其用作目标。如果对象存在,意味着数据库已经有一个主键为“1”的Address行,我们可以看到Addressemail_address字段将在这种情况下被三次覆盖,分别为值 a、b 和最后的 c。

然而,如果主键“1”对应的Address行不存在,Session.merge()将会创建三个独立的Address实例,然后在插入时会出现主键冲突。新的行为是,这些Address对象的拟议主键被跟踪在一个单独的字典中,这样我们就可以将三个拟议的Address对象的状态合并到一个要插入的Address对象上。

如果原始情况下发出某种警告,指出在单个合并树中存在冲突数据可能更好,然而,多年来,对于持久情况,非确定性值的合并一直是行为,现在也适用于挂起情况。警告存在冲突值的功能仍然适用于两种情况,但会增加相当大的性能开销,因为在合并过程中必须比较每个列值。

#3601

修复涉及用户发起的外键操作的多对一对象移动

修复了涉及将对对象的多对一引用替换为另一个对象的机制的错误。在属性操作期间,先前引用的对象的位置现在使用数据库提交的外键值,而不是当前的外键值。修复的主要效果是,当进行多对一更改时,即使在之前手动将外键属性移动到新值之前,也将更准确地触发对集合的  backref 事件。假设类ParentSomeClass的映射,其中SomeClass.parent指向Parent,而Parent.items指向SomeClass对象的集合:

some_object = SomeClass()
session.add(some_object)
some_object.parent_id = some_parent.id
some_object.parent = some_parent

上面,我们创建了一个待处理的对象some_object,并将其外键指向Parent以引用它,然后我们实际设置了关系。在修复错误之前,backref 不会被触发:

# before the fix
assert some_object not in some_parent.items

现在修复的问题是,当我们试图定位some_object.parent的先前值时,我们会忽略手动设置的父 id,并寻找数据库提交的值。在这种情况下,它是 None,因为对象是待处理的,所以事件系统将some_object.parent记录为净变化:

# after the fix, backref fired off for some_object.parent = some_parent
assert some_object in some_parent.items

虽然不鼓励操纵由关系管理的外键属性,但对于这种用例有一定的支持。为了允许加载继续进行,经常会使用Session.enable_relationship_loading()RelationshipProperty.load_on_pending功能,这会导致基于内存中尚未持久化的外键值的惰性加载关系。无论是否使用了这些功能,这种行为改进现在都会显而易见。

#3708

改进查询中的 Query.correlate 方法与多态实体

在最近的 SQLAlchemy 版本中,许多形式的“多态”查询生成的 SQL 比以前更“扁平化”,其中多个表的 JOIN 不再无条件地捆绑到子查询中。为了适应这一点,Query.correlate()方法现在会从这样一个多态可选择的地方提取各个表,并确保它们都是子查询的“相关部分”。假设从映射文档中的Person/Manager/Engineer->Company设置开始,使用with_polymorphic

sess.query(Person.name).filter(
    sess.query(Company.name)
    .filter(Company.company_id == Person.company_id)
    .correlate(Person)
    .as_scalar()
    == "Elbonia, Inc."
)

上述查询现在会产生:

SELECT  people.name  AS  people_name
FROM  people
LEFT  OUTER  JOIN  engineers  ON  people.person_id  =  engineers.person_id
LEFT  OUTER  JOIN  managers  ON  people.person_id  =  managers.person_id
WHERE  (SELECT  companies.name
FROM  companies
WHERE  companies.company_id  =  people.company_id)  =  ?

在修复之前,调用correlate(Person)会错误地尝试将PersonEngineerManager的连接作为一个单元进行关联,因此Person不会被关联:

-- old, incorrect query
SELECT  people.name  AS  people_name
FROM  people
LEFT  OUTER  JOIN  engineers  ON  people.person_id  =  engineers.person_id
LEFT  OUTER  JOIN  managers  ON  people.person_id  =  managers.person_id
WHERE  (SELECT  companies.name
FROM  companies,  people
WHERE  companies.company_id  =  people.company_id)  =  ?

对多态映射使用相关子查询仍然存在一些未完善的地方。例如,如果Person多态链接到所谓的“具体多态联合”查询,上述子查询可能无法正确引用此子查询。在所有情况下,完全引用“多态”实体的一种方法是首先从中创建一个aliased()对象:

# works with all SQLAlchemy versions and all types of polymorphic
# aliasing.
paliased = aliased(Person)
sess.query(paliased.name).filter(
    sess.query(Company.name)
    .filter(Company.company_id == paliased.company_id)
    .correlate(paliased)
    .as_scalar()
    == "Elbonia, Inc."
)

aliased() 构造保证了“多态可选择性”被包裹在子查询中。通过在相关子查询中明确引用它,多态形式被正确使用。

#3662

查询的字符串化将向会话咨询正确的方言

Query对象调用str()将向Session咨询要使用的正确“绑定”,以便呈现将传递给数据库的 SQL。特别是,这允许引用特定于方言的 SQL 结构的Query可呈现,假设Query与适当的Session相关联。以前,只有当映射关联到的MetaData本身绑定到目标Engine时,此行为才会生效。

如果底层的MetaDataSession都没有与任何绑定的Engine相关联,则会使用“默认”方言回退以生成 SQL 字符串。

另请参见

没有方言的核心 SQL 结构的“友好”字符串化

#3081

在一行中多次出现相同实体的连接式预加载

已对通过连接式预加载加载属性的情况进行了修复,即使实体已经从不包括属性的不同“路径”上的行加载。这是一个难以复现的深层用例,但一般思路如下:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    b_id = Column(ForeignKey("b.id"))
    c_id = Column(ForeignKey("c.id"))
    b = relationship("B")
    c = relationship("C")
class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    c_id = Column(ForeignKey("c.id"))
    c = relationship("C")
class C(Base):
    __tablename__ = "c"
    id = Column(Integer, primary_key=True)
    d_id = Column(ForeignKey("d.id"))
    d = relationship("D")
class D(Base):
    __tablename__ = "d"
    id = Column(Integer, primary_key=True)
c_alias_1 = aliased(C)
c_alias_2 = aliased(C)
q = s.query(A)
q = q.join(A.b).join(c_alias_1, B.c).join(c_alias_1.d)
q = q.options(
    contains_eager(A.b).contains_eager(B.c, alias=c_alias_1).contains_eager(C.d)
)
q = q.join(c_alias_2, A.c)
q = q.options(contains_eager(A.c, alias=c_alias_2))

上述查询生成的 SQL 如下:

SELECT
  d.id  AS  d_id,
  c_1.id  AS  c_1_id,  c_1.d_id  AS  c_1_d_id,
  b.id  AS  b_id,  b.c_id  AS  b_c_id,
  c_2.id  AS  c_2_id,  c_2.d_id  AS  c_2_d_id,
  a.id  AS  a_id,  a.b_id  AS  a_b_id,  a.c_id  AS  a_c_id
FROM
  a
  JOIN  b  ON  b.id  =  a.b_id
  JOIN  c  AS  c_1  ON  c_1.id  =  b.c_id
  JOIN  d  ON  d.id  =  c_1.d_id
  JOIN  c  AS  c_2  ON  c_2.id  =  a.c_id

我们可以看到 c 表被选中了两次;一次是在 A.b.c -> c_alias_1 的上下文中,另一次是在 A.c -> c_alias_2 的上下文中。此外,我们可以看到对于单个行来说,C 的标识很可能对于 c_alias_1c_alias_2相同的,这意味着一行中的两组列只会导致将一个新对象添加到标识映射中。

上面的查询选项只要求在 c_alias_1 的上下文中加载属性 C.d,而不是在 c_alias_2 中加载。因此,我们在标识映射中得到的最终 C 对象是否加载了 C.d 属性取决于映射是如何遍历的,虽然不是完全随机的,但基本上是不确定的。修复方法是,即使对于已通过不同路径加载的实体,加载器也会对 C.d 元素进行加载。先到达实体的加载器一直是不确定的,所以这个修复在某些情况下可能会被检测到是一种行为上的改变,而在其他情况下则不会。

修复包括两种“多个路径指向一个实体”的情况的测试,并且修复希望能够涵盖此类其他场景的问题。

#3431

新增了 MutableList 和 MutableSet 辅助类到变化跟踪扩展

变化跟踪扩展中新增了新的辅助类MutableListMutableSet,以补充现有的MutableDict助手。

#3297

新的“raise”/“raise_on_sql”加载策略

为了帮助防止在加载一系列对象后发生不需要的惰性加载,可以将新的“lazy=’raise’”和“lazy=’raise_on_sql’”策略及相应的加载器选项raiseload()应用于关系属性,这将导致在读取非急切加载的属性时引发InvalidRequestError。两种变体测试任何类型的惰性加载,包括那些只返回 None 或从标识映射中检索的加载:

>>> from sqlalchemy.orm import raiseload
>>> a1 = s.query(A).options(raiseload(A.some_b)).first()
>>> a1.some_b
Traceback (most recent call last):
...
sqlalchemy.exc.InvalidRequestError: 'A.some_b' is not available due to lazy='raise'

或者只有在会发出 SQL 时才进行惰性加载:

>>> from sqlalchemy.orm import raiseload
>>> a1 = s.query(A).options(raiseload(A.some_b, sql_only=True)).first()
>>> a1.some_b
Traceback (most recent call last):
...
sqlalchemy.exc.InvalidRequestError: 'A.bs' is not available due to lazy='raise_on_sql'

#3512

Mapper.order_by 已弃用

这个参数是 SQLAlchemy 最初版本的一部分,它是 ORM 的原始设计的一部分,其中包含Mapper对象作为公共面向的查询结构。这个角色早已被Query对象取代,我们在这里使用Query.order_by()来指示结果的排序方式,这种方式对于任何组合的 SELECT 语句、实体和 SQL 表达式都是一致的。有许多情况下,Mapper.order_by不像预期那样工作(或者预期的结果不清楚),比如当查询组合成联合时;这些情况是不受支持的。

#3394


SqlAlchemy 2.0 中文文档(七十五)(3)https://developer.aliyun.com/article/1562371

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
3月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(七十四)(5)
SqlAlchemy 2.0 中文文档(七十四)
46 6
|
3月前
|
SQL JSON 关系型数据库
SqlAlchemy 2.0 中文文档(七十五)(4)
SqlAlchemy 2.0 中文文档(七十五)
50 1
|
3月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(七十四)(3)
SqlAlchemy 2.0 中文文档(七十四)
43 1
|
3月前
|
SQL Python
SqlAlchemy 2.0 中文文档(七十四)(4)
SqlAlchemy 2.0 中文文档(七十四)
30 6
|
3月前
|
SQL JSON 关系型数据库
SqlAlchemy 2.0 中文文档(七十五)(1)
SqlAlchemy 2.0 中文文档(七十五)
69 4
|
3月前
|
SQL 缓存 关系型数据库
SqlAlchemy 2.0 中文文档(七十四)(2)
SqlAlchemy 2.0 中文文档(七十四)
32 1
|
3月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(七十四)(1)
SqlAlchemy 2.0 中文文档(七十四)
60 1
|
3月前
|
SQL JSON 关系型数据库
SqlAlchemy 2.0 中文文档(七十五)(5)
SqlAlchemy 2.0 中文文档(七十五)
25 0
|
3月前
|
SQL JSON 关系型数据库
SqlAlchemy 2.0 中文文档(七十五)(3)
SqlAlchemy 2.0 中文文档(七十五)
31 0
|
3月前
|
SQL Oracle 关系型数据库
SqlAlchemy 2.0 中文文档(六十七)(2)
SqlAlchemy 2.0 中文文档(六十七)
19 0