SQLAlchemy 2.0 文档
变更和迁移
- SQLAlchemy 2.0 - 主要迁移指南
- SQLAlchemy 2.0 的新特性?
- 2.0 更新日志
- 1.4 更新日志
- 1.3 更新日志
- 1.2 更新日志
- 1.1 更新日志
- 1.0 更新日志
- 0.9 更新日志
- 0.8 更新日志
- 0.7 更新日志
- 0.6 更新日志
- 0.5 更新日志
- 0.4 更新日志
- 0.3 更新日志
- 0.2 更新日志
- 0.1 更新日志
- SQLAlchemy 1.4 的新特性?¶
- 主要 API 变更和特性 - 概述
- Python 3.6 是最低 Python 3 版本;仍然支持 Python 2.7
- ORM Query 在内部与 select、update、delete 统一;2.0 风格的执行方式可用
- ORM
Session.execute()
在所有情况下都使用 “future” 风格的Result
集合 - 为核心、ORM 中的所有 DQL、DML 语句添加了透明 SQL 编译缓存
- 声明式现在已集成到 ORM 中,并具有新特性
- Python Dataclasses、attrs 支持声明式、命令式映射
- 核心和 ORM 的异步 IO 支持
- 许多核心和 ORM 语句对象现在在编译阶段执行其大部分构造和验证
- 修复了内部导入约定,以便代码检查工具可以正常工作
- 支持 SQL 正则表达式运算符
- SQLAlchemy 2.0 弃用模式
- API 和行为变更 - 核心
- SELECT 语句不再隐式地被视为 FROM 子句
- select().join() 和 outerjoin() 将 JOIN 条件添加到当前查询,而不是创建子查询
- URL 对象现在是不可变的
- select(), case() 现在接受位置表达式
- 所有 IN 表达式都为列表中的每个值动态渲染参数(例如,扩展参数)
- 内置的 FROM 检查将警告 SELECT 语句中任何潜在的笛卡尔积
- 新的 Result 对象
- RowProxy 不再是 “代理”;现在称为 Row,其行为类似于增强的命名元组
- SELECT 对象和派生的 FROM 子句允许重复的列和列标签
- 改进了使用 CAST 或类似方法的简单列表达式的列标签
- 新的 “后编译” 绑定参数用于 Oracle、SQL Server 中的 LIMIT/OFFSET
- 连接级事务现在可以基于子事务变为非活动状态
- Enum 和 Boolean 数据类型不再默认为 “创建约束”
- 新特性 - ORM
- 行为变更 - ORM
- 方言变更
- 主要 API 变更和特性 - 概述
- SQLAlchemy 1.3 的新特性?
- SQLAlchemy 1.2 的新特性?
- SQLAlchemy 1.1 的新特性?
- SQLAlchemy 1.0 的新特性?
- SQLAlchemy 0.9 的新特性?
- SQLAlchemy 0.8 的新特性?
- SQLAlchemy 0.7 的新特性?
- SQLAlchemy 0.6 的新特性?
- SQLAlchemy 0.5 的新特性?
- SQLAlchemy 0.4 的新特性?
项目版本
- 上一篇: 0.1 更新日志
- 下一篇: SQLAlchemy 1.3 的新特性?
- 上级: 首页
- 在本页
- SQLAlchemy 1.4 的新特性?
- 主要 API 变更和特性 - 概述
- Python 3.6 是最低 Python 3 版本;仍然支持 Python 2.7
- ORM Query 在内部与 select、update、delete 统一;2.0 风格的执行方式可用
- ORM
Session.execute()
在所有情况下都使用 “future” 风格的Result
集合 - 为核心、ORM 中的所有 DQL、DML 语句添加了透明 SQL 编译缓存
- 声明式现在已集成到 ORM 中,并具有新特性
- Python Dataclasses、attrs 支持声明式、命令式映射
- 核心和 ORM 的异步 IO 支持
- 许多核心和 ORM 语句对象现在在编译阶段执行其大部分构造和验证
- 修复了内部导入约定,以便代码检查工具可以正常工作
- 支持 SQL 正则表达式运算符
- SQLAlchemy 2.0 弃用模式
- API 和行为变更 - 核心
- SELECT 语句不再隐式地被视为 FROM 子句
- select().join() 和 outerjoin() 将 JOIN 条件添加到当前查询,而不是创建子查询
- URL 对象现在是不可变的
- select(), case() 现在接受位置表达式
- 所有 IN 表达式都为列表中的每个值动态渲染参数(例如,扩展参数)
- 内置的 FROM 检查将警告 SELECT 语句中任何潜在的笛卡尔积
- 新的 Result 对象
- RowProxy 不再是 “代理”;现在称为 Row,其行为类似于增强的命名元组
- SELECT 对象和派生的 FROM 子句允许重复的列和列标签
- 改进了使用 CAST 或类似方法的简单列表达式的列标签
- 新的 “后编译” 绑定参数用于 Oracle、SQL Server 中的 LIMIT/OFFSET
- 连接级事务现在可以基于子事务变为非活动状态
- Enum 和 Boolean 数据类型不再默认为 “创建约束”
- 新特性 - ORM
- 行为变更 - ORM
- 方言变更
- 主要 API 变更和特性 - 概述
SQLAlchemy 1.4 的新特性?¶
关于本文档
本文档描述了 SQLAlchemy 1.3 版本和 SQLAlchemy 1.4 版本之间的变更。
版本 1.4 的重点与其他 SQLAlchemy 版本不同,因为它在许多方面都试图充当计划在 SQLAlchemy 2.0 版本中发布的一系列更重大的 API 变更的潜在迁移点。SQLAlchemy 2.0 的重点是现代化和精简的 API,它移除了许多长期以来不鼓励使用的使用模式,并将 SQLAlchemy 中最好的想法主流化为一流的 API 特性,目标是 API 的使用方式不再那么模棱两可,并且将移除一系列使内部结构复杂化并阻碍性能的隐式行为和极少使用的 API 标志。
有关 SQLAlchemy 2.0 的当前状态,请参阅 SQLAlchemy 2.0 - 主要迁移指南。
主要 API 变更和特性 - 概述¶
Python 3.6 是最低 Python 3 版本;仍然支持 Python 2.7¶
由于 Python 3.5 在 2020 年 9 月达到 EOL,SQLAlchemy 1.4 现在将版本 3.6 作为最低 Python 3 版本。仍然支持 Python 2.7,但是 SQLAlchemy 1.4 系列将是最后一个支持 Python 2 的系列。
ORM Query 在内部与 select、update、delete 统一;2.0 风格的执行方式可用¶
版本 2.0 以及本质上在 1.4 中的 SQLAlchemy 的最大概念性变化是,核心中的 Select
构造和 ORM 中的 Query
对象之间,以及 Query.update()
和 Query.delete()
方法在它们与 Update
和 Delete
的关系方面存在的巨大差异已被移除。
关于 Select
和 Query
,这两个对象在许多版本中都具有相似的、很大程度上重叠的 API,甚至在两者之间进行更改的一些能力,同时在它们的使用模式和行为方面仍然非常不同。这方面的历史背景是,引入 Query
对象是为了克服 Select
对象的缺点,Select
对象曾经是 ORM 对象查询方式的核心,但它们必须根据 Table
元数据进行查询。然而,Query
仅具有用于加载对象的简单接口,并且仅在经历了许多主要版本之后,它最终获得了 Select
对象的大部分灵活性,这导致了持续的尴尬,即这两个对象变得高度相似,但仍然在很大程度上彼此不兼容。
在版本 1.4 中,所有核心和 ORM SELECT 语句都直接从 Select
对象呈现;当使用 Query
对象时,在语句调用时,它将其状态复制到 Select
,然后使用 2.0 风格 执行在内部调用它。展望未来,Query
对象将仅成为遗留对象,并且将鼓励应用程序迁移到 2.0 风格 执行,这允许将核心构造自由地用于 ORM 实体
with Session(engine, future=True) as sess:
stmt = (
select(User)
.where(User.name == "sandy")
.join(User.addresses)
.where(Address.email_address.like("%gmail%"))
)
result = sess.execute(stmt)
for user in result.scalars():
print(user)
关于上述示例的注意事项
Session
和sessionmaker
对象现在具有完整的上下文管理器(即with:
语句)功能;有关示例,请参阅 打开和关闭 Session 中修订的文档。在 1.4 系列中,所有 2.0 风格 ORM 调用都使用将
Session.future
标志设置为True
的Session
;此标志指示Session
应具有 2.0 风格的行为,其中包括可以从execute
调用 ORM 查询以及事务功能的一些更改。在版本 2.0 中,此标志将始终为True
。select()
构造不再需要在列子句周围使用括号;有关此改进的背景信息,请参阅 select(), case() 现在接受位置表达式。select()
/Select
对象具有Select.join()
方法,该方法的行为类似于Query
的方法,甚至可以容纳 ORM 关系属性(而不会破坏核心和 ORM 之间的分离!)- 有关此方面的背景信息,请参阅 select().join() 和 outerjoin() 将 JOIN 条件添加到当前查询,而不是创建子查询。使用
Session.execute()
调用处理 ORM 实体并期望返回 ORM 结果的语句。有关入门知识,请参阅 查询。另请参阅 ORM Session.execute() 在所有情况下都使用 “future” 风格的 Result 集合 中的以下注释。返回一个
Result
对象,而不是纯列表,它本身是以前的ResultProxy
对象的更复杂的版本;此对象现在同时用于核心和 ORM 结果。有关这方面的信息,请参阅 新的 Result 对象、RowProxy 不再是 “代理”;现在称为 Row,其行为类似于增强的命名元组 和 Query 返回的 “KeyedTuple” 对象被 Row 替换。
在 SQLAlchemy 的文档中,将有很多对 1.x 风格 和 2.0 风格 执行的引用。这是为了区分两种查询风格,并尝试向前文档化未来的新调用风格。在 SQLAlchemy 2.0 中,虽然 Query
对象可能作为遗留构造保留,但它将不再在大多数文档中出现。
对 “批量更新和删除” 进行了类似的调整,以便可以将核心 update()
和 delete()
用于批量操作。像下面这样的批量更新
session.query(User).filter(User.name == "sandy").update(
{"password": "foobar"}, synchronize_session="fetch"
)
现在可以使用 2.0 风格 实现(实际上,以上内容在内部以这种方式运行),如下所示
with Session(engine, future=True) as sess:
stmt = (
update(User)
.where(User.name == "sandy")
.values(password="foobar")
.execution_options(synchronize_session="fetch")
)
sess.execute(stmt)
请注意使用 Executable.execution_options()
方法来传递 ORM 相关的选项。“执行选项” 的使用现在在核心和 ORM 中都更加普遍,并且 Query
中的许多 ORM 相关方法现在都作为执行选项实现(有关一些示例,请参阅 Query.execution_options()
)。
ORM Session.execute()
在所有情况下都使用 “future” 风格的 Result
集合¶
如 RowProxy 不再是 “代理”;现在称为 Row,其行为类似于增强的命名元组 中所述,Result
和 Row
对象现在具有 “命名元组” 行为,当与包含设置为 True
的 create_engine.future
参数的 Engine
一起使用时。这些 “命名元组” 行尤其包含一个行为更改,即使用 in
的 Python 包含表达式,例如
>>> engine = create_engine("...", future=True)
>>> conn = engine.connect()
>>> row = conn.execute.first()
>>> "name" in row
True
上面的包含测试将使用值包含,而不是键包含;row
需要具有 “name” 的值才能返回 True
。
在 SQLAlchemy 1.4 下,当 create_engine.future
参数设置为 False
时,将返回旧式 LegacyRow
对象,这些对象具有先前 SQLAlchemy 版本的 partial-named-tuple 行为,其中包含检查继续使用键包含;如果行具有名为 “name” 的列,而不是值,则 "name" in row
将返回 True。
当使用 Session.execute()
时,无条件地启用完整的命名元组样式,这意味着 "name" in row
将使用值包含作为测试,而不是键包含。这是为了适应 Session.execute()
现在返回一个 Result
,它也适用于 ORM 结果,即使是旧版 ORM 结果行(例如 Query.all()
返回的那些行)也使用值包含。
这是从 SQLAlchemy 1.3 到 1.4 的行为更改。要继续接收键包含集合,请使用 Result.mappings()
方法来接收一个 MappingResult
,它将行作为字典返回
for dict_row in session.execute(text("select id from table")).mappings():
assert "id" in dict_row
为核心、ORM 中的所有 DQL、DML 语句添加了透明 SQL 编译缓存¶
这是 SQLAlchemy 版本有史以来最广泛的更改之一,对从核心一直到 ORM 的所有查询系统进行了为期数月的重组和重构,现在可以将涉及从用户构造的语句生成 SQL 字符串和相关语句元数据的大部分 Python 计算缓存在内存中,这样,后续调用相同的语句构造将减少 35-60% 的 CPU 资源。
这种缓存超越了 SQL 字符串的构造,还包括结果获取结构的构造,这些结构将 SQL 构造链接到结果集,并且在 ORM 中,它包括容纳 ORM 支持的属性加载器、关系预先加载器和其他选项,以及每次 ORM 查询尝试运行并从结果集中构造 ORM 对象时都必须构建的对象构造例程。
为了介绍该特性的总体思路,给定 性能 套件中的代码如下,它将调用一个非常简单的查询 “n” 次,默认值为 n=10000。该查询仅返回单行,因为我们要减少的开销是许多小型查询。对于返回多行的查询,优化效果不那么显着
session = Session(bind=engine)
for id_ in random.sample(ids, n):
result = session.query(Customer).filter(Customer.id == id_).one()
在 SQLAlchemy 1.3 版本中,在运行 Linux 的 Dell XPS13 上,此示例完成如下
test_orm_query : (10000 iterations); total time 3.440652 sec
在 1.4 中,未经修改的上述代码完成如下
test_orm_query : (10000 iterations); total time 2.367934 sec
第一个测试表明,当使用缓存时,常规 ORM 查询可以在许多迭代中以快 30% 的速度运行。
该特性的第二个变体是可选地使用 Python lambda 来延迟查询本身的构造。这是 “Baked Query” 扩展使用的更复杂变体,该扩展在版本 1.0.0 中引入。“lambda” 特性可以以非常类似于 baked query 的风格使用,但它以临时方式用于任何 SQL 构造。此外,它还包括扫描每次 lambda 调用中绑定文字值(每次调用都会更改)以及其他构造(例如,每次从不同的实体或列查询)更改的能力,同时仍然不必每次都运行实际代码。
使用此 API 如下所示
session = Session(bind=engine)
for id_ in random.sample(ids, n):
stmt = lambda_stmt(lambda: future_select(Customer))
stmt += lambda s: s.where(Customer.id == id_)
session.execute(stmt).scalar_one()
上面的代码完成如下
test_orm_query_newstyle_w_lambdas : (10000 iterations); total time 1.247092 sec
此测试表明,使用较新的 “select()” 风格的 ORM 查询,结合完整的 “baked” 风格调用(缓存整个构造),可以在许多迭代中以快 60% 的速度运行,并且性能与 baked query 系统大致相同,而 baked query 系统现在已被本机缓存系统取代。
新系统利用现有的 Connection.execution_options.compiled_cache
执行选项,并在 Engine
中直接添加缓存,该缓存使用 Engine.query_cache_size
参数配置。
1.4 中整个 API 和行为变更的很大一部分是为了支持这个新特性而驱动的。
另请参阅
声明式现在已集成到 ORM 中,并具有新特性¶
经过大约十年的流行,sqlalchemy.ext.declarative
包现在已集成到 sqlalchemy.orm
命名空间中,除了声明式 “扩展” 类仍然是声明式扩展之外。
添加到 sqlalchemy.orm
的新类包括
registry
- 一个新类,它取代了 “声明式基类” 的角色,充当映射类的注册表,可以通过字符串名称在relationship()
调用中引用,并且与任何特定类的映射风格无关。declarative_base()
- 这是在声明式系统的整个过程中一直使用的相同声明式基类,只是它现在在内部引用registry
对象,并且由可以从registry
直接调用的registry.generate_base()
方法实现。declarative_base()
函数会自动创建此注册表,因此对现有代码没有影响。sqlalchemy.ext.declarative.declarative_base
名称仍然存在,当启用 2.0 弃用模式 时,会发出 2.0 弃用警告。declared_attr()
- 相同的 “declared attr” 函数调用现在是sqlalchemy.orm
的一部分。sqlalchemy.ext.declarative.declared_attr
名称仍然存在,当启用 2.0 弃用模式 时,会发出 2.0 弃用警告。其他名称已移至
sqlalchemy.orm
,包括has_inherited_table()
、synonym_for()
、DeclarativeMeta
和as_declarative()
。
此外,instrument_declarative()
函数已被弃用,已被 registry.map_declaratively()
取代。ConcreteBase
、 AbstractConcreteBase
和 DeferredReflection
类仍然作为扩展保留在 Declarative Extensions 包中。
映射样式现在已组织起来,使其全部从 registry
对象扩展,并归入以下类别
- 声明式映射
- 使用
registry.mapped()
声明式装饰器 声明式表
- 命令式表(混合)
- 使用
现有的经典映射函数 sqlalchemy.orm.mapper()
仍然保留,但是直接调用 sqlalchemy.orm.mapper()
已被弃用;新的 registry.map_imperatively()
方法现在通过 sqlalchemy.orm.registry()
路由请求,以便它与其他声明式映射明确地集成。
新方法与必须在映射过程之前在类上发生的第三方类检测系统互操作,允许声明式映射通过装饰器而不是声明式基类工作,以便像 dataclasses 和 attrs 这样的包可以与声明式映射一起使用,除了与经典映射一起使用。
声明式文档现在已完全集成到 ORM 映射器配置文档中,并包括组织在一个位置的所有映射样式的示例。请参阅 ORM 映射类概述 部分,了解新重组文档的开始。
Python 数据类、attrs 支持声明式、命令式映射¶
除了 声明式现在已集成到 ORM 中,并具有新功能 中引入的新声明式装饰器样式外,Mapper
现在显式地意识到 Python dataclasses
模块,并将识别以这种方式配置的属性,并继续映射它们,而不会像以前那样跳过它们。在 attrs
模块的情况下,attrs
已经从类中删除了自己的属性,因此已经与 SQLAlchemy 经典映射兼容。随着 registry.mapped()
装饰器的添加,这两种属性系统现在也可以与声明式映射互操作。
核心和 ORM 的异步 IO 支持¶
SQLAlchemy 现在支持 Python asyncio
兼容的数据库驱动程序,使用全新的 asyncio 前端接口连接到 Connection
以供核心使用,以及 Session
以供 ORM 使用,使用 AsyncConnection
和 AsyncSession
对象。
注意
对于 SQLAlchemy 1.4 的初始版本,新的 asyncio 功能应被视为 alpha 级别。这是非常新的东西,使用了一些以前不熟悉的编程技术。
最初支持的数据库 API 是用于 PostgreSQL 的 asyncpg asyncio 驱动程序。
SQLAlchemy 的内部功能通过使用 greenlet 库完全集成,以便调整 SQLAlchemy 内部的执行流程,以将 asyncio await
关键字从数据库驱动程序向外传播到最终用户 API,该 API 具有 async
方法。使用这种方法,asyncpg 驱动程序在 SQLAlchemy 自己的测试套件中完全可操作,并且具有与大多数 psycopg2 功能的兼容性。这种方法经过了 greenlet 项目的开发人员的审查和改进,SQLAlchemy 对此表示感谢。
面向用户的 async
API 本身侧重于面向 IO 的方法,例如 AsyncEngine.connect()
和 AsyncConnection.execute()
。新的核心构造严格支持 2.0 风格 的用法;这意味着所有语句都必须在给定连接对象的情况下调用,在本例中为 AsyncConnection
。
在 ORM 中,支持 2.0 风格 的查询执行,使用 select()
构造并结合 AsyncSession.execute()
;AsyncSession
类不支持传统的 Query
对象本身。
ORM 功能(如相关属性的延迟加载以及过期属性的取消过期)在传统的 asyncio 编程模型中按定义是不允许的,因为它们指示 IO 操作将在 Python getattr()
操作的范围内隐式运行。为了克服这一点,传统的 asyncio 应用程序应明智地使用 预先加载 技术,并放弃使用诸如 提交时过期 之类的功能,以便不需要此类加载。
对于 选择打破 传统的 asyncio 应用程序开发人员,新 API 提供了一个 严格可选的功能,以便希望使用此类 ORM 功能的应用程序可以选择将数据库相关代码组织到可以使用 AsyncSession.run_sync()
方法在 greenlets 中运行的函数中。有关演示,请参阅 Asyncio 集成 中的 greenlet_orm.py
示例。
还使用新方法 AsyncConnection.stream()
和 AsyncSession.stream()
提供了对异步游标的支持,这些方法支持新的 AsyncResult
对象,该对象本身提供了常见方法的可等待版本,例如 AsyncResult.all()
和 AsyncResult.fetchmany()
。核心和 ORM 都与该功能集成,该功能对应于传统 SQLAlchemy 中“服务器端游标”的使用。
许多核心和 ORM 语句对象现在在编译阶段执行其大部分构造和验证¶
1.4 系列中的一项主要举措是接近核心 SQL 语句和 ORM Query 的模型,以允许语句创建和编译的有效、可缓存模型,其中编译步骤将基于由创建的语句对象生成的缓存键进行缓存,该语句对象本身是为每次使用新创建的。为了实现此目标,语句构造中发生的许多 Python 计算(特别是 ORM Query
以及用于调用 ORM 查询的 select()
构造)正在被移动到语句的编译阶段中发生,该阶段仅在语句被调用后发生,并且仅当语句的编译形式尚未缓存时发生。
从最终用户的角度来看,这意味着某些基于传递给对象的参数而可能出现的错误消息将不再立即引发,而是在首次调用语句时才会发生。这些条件始终是结构性的,而不是数据驱动的,因此不存在由于缓存语句而错过这种情况的风险。
属于此类别的错误情况包括
当构造
_selectable.CompoundSelect
(例如 UNION、EXCEPT 等)并且传递的 SELECT 语句没有相同数量的列时,现在会引发CompileError
以达到此效果;以前,在语句构造时会立即引发ArgumentError
。在调用
Query.join()
时可能出现的各种错误条件将在语句编译时而不是在首次调用该方法时进行评估。
其他可能更改的事项直接涉及 Query
对象
在调用
Query.statement
访问器时,行为可能会略有不同。Select
对象返回的现在是Query
中存在的相同状态的直接副本,而未执行任何 ORM 特定的编译(这意味着它速度大大提高)。但是,Select
将不具有与其在 1.3 中相同的内部状态,包括诸如 FROM 子句之类的东西(如果它们在Query
中没有明确声明)。这意味着依赖于操作此Select
语句的代码(例如调用诸如Select.with_only_columns()
之类的方法)可能需要适应 FROM 子句。
修复了内部导入约定,以便代码检查工具可以正确工作¶
长期以来,SQLAlchemy 一直使用参数注入装饰器来帮助解决相互依赖的模块导入,如下所示
@util.dependency_for("sqlalchemy.sql.dml")
def insert(self, dml, *args, **kw): ...
上面的函数将被重写,不再在外部具有 dml
参数。这将使代码检查工具混淆,将函数视为缺少参数。内部已实施一种新方法,使得不再修改函数的签名,而是在函数内部获取模块对象。
支持 SQL 正则表达式运算符¶
一个期待已久的功能是添加对数据库正则表达式运算符的基本支持,以补充 ColumnOperators.like()
和 ColumnOperators.match()
操作套件。新功能包括 ColumnOperators.regexp_match()
,它实现了一个类似正则表达式匹配的函数,以及 ColumnOperators.regexp_replace()
,它实现了一个正则表达式字符串替换函数。
支持的后端包括 SQLite、PostgreSQL、MySQL / MariaDB 和 Oracle。SQLite 后端仅支持“regexp_match”,但不支持“regexp_replace”。
正则表达式语法和标志 不是后端无关的。未来的功能将允许一次指定多个正则表达式语法,以便在不同的后端之间动态切换。
对于 SQLite,Python 的 re.search()
函数(不带任何附加参数)被确立为实现。
SQLAlchemy 2.0 弃用模式¶
1.4 版本的首要目标之一是提供一个“过渡”版本,以便应用程序可以逐步迁移到 SQLAlchemy 2.0。为此,1.4 版本的主要功能是“2.0 弃用模式”,这是一系列弃用警告,针对每个可检测到的 API 模式发出警告,这些模式在 2.0 版本中将以不同的方式工作。所有警告都使用了 RemovedIn20Warning
类。由于这些警告影响了包括 select()
和 Engine
构造在内的基础模式,即使是简单的应用程序也可能生成大量警告,直到进行了适当的 API 更改。因此,在开发人员启用环境变量 SQLALCHEMY_WARN_20=1
之前,警告模式默认情况下处于关闭状态。
有关使用 2.0 弃用模式的完整演练,请参阅 迁移到 2.0 第二步 - 启用 RemovedIn20Warnings。
API 和行为更改 - 核心¶
SELECT 语句不再隐式地被视为 FROM 子句¶
此更改是 SQLAlchemy 多年来较大的概念性更改之一,但希望最终用户的影响相对较小,因为此更改更接近 MySQL 和 PostgreSQL 等数据库在任何情况下的要求。
最直接明显的冲击是,select()
不能再直接嵌入到另一个 select()
内部,而无需先将内部 select()
转换为子查询。这在历史上是通过使用 SelectBase.alias()
方法执行的,该方法仍然保留,但更明确地适用于使用新方法 SelectBase.subquery()
;这两种方法都做同样的事情。返回的对象现在是 Subquery
,它与 Alias
对象非常相似,并共享一个公共基类 AliasedReturnsRows
。
也就是说,这现在会引发错误
stmt1 = select(user.c.id, user.c.name)
stmt2 = select(addresses, stmt1).select_from(addresses.join(stmt1))
引发错误
sqlalchemy.exc.ArgumentError: Column expression or FROM clause expected,
got <...Select object ...>. To create a FROM clause from a <class
'sqlalchemy.sql.selectable.Select'> object, use the .subquery() method.
正确的调用形式是(还要注意 select() 不再需要括号)
sq1 = select(user.c.id, user.c.name).subquery()
stmt2 = select(addresses, sq1).select_from(addresses.join(sq1))
上面注意到 SelectBase.subquery()
方法本质上等同于使用 SelectBase.alias()
方法。
此更改的基本原理如下
为了支持
Select
与Query
的统一,Select
对象需要具有Select.join()
和Select.outerjoin()
方法,这些方法实际上将 JOIN 条件添加到现有的 FROM 子句中,就像用户一直期望它在任何情况下都做的那样。之前的行为必须与FromClause
的行为保持一致,即它会生成一个未命名的子查询,然后 JOIN 到它,这是一个完全无用的功能,只会让那些不幸尝试它的用户感到困惑。此更改在 select().join() 和 outerjoin() 将 JOIN 条件添加到当前查询,而不是创建子查询 中进行了讨论。在另一个 SELECT 的 FROM 子句中包含 SELECT 而不首先创建别名或子查询的行为是,它会创建一个未命名的子查询。虽然标准 SQL 确实支持此语法,但在实践中,大多数数据库都拒绝它。例如,MySQL 和 PostgreSQL 都完全拒绝使用未命名的子查询
# MySQL / MariaDB: MariaDB [(none)]> select * from (select 1); ERROR 1248 (42000): Every derived table must have its own alias # PostgreSQL: test=> select * from (select 1); ERROR: subquery in FROM must have an alias LINE 1: select * from (select 1); ^ HINT: For example, FROM (SELECT ...) [AS] foo.
像 SQLite 这样的数据库接受它们,但通常情况下,从此类子查询生成的名称仍然过于模糊而无法使用
sqlite> CREATE TABLE a(id integer); sqlite> CREATE TABLE b(id integer); sqlite> SELECT * FROM a JOIN (SELECT * FROM b) ON a.id=id; Error: ambiguous column name: id sqlite> SELECT * FROM a JOIN (SELECT * FROM b) ON a.id=b.id; Error: no such column: b.id # use a name sqlite> SELECT * FROM a JOIN (SELECT * FROM b) AS anon_1 ON a.id=anon_1.id;
由于 SelectBase
对象不再是 FromClause
对象,因此像 .c
属性以及 .select()
之类的方法现在已被弃用,因为它们暗示了子查询的隐式生成。.join()
和 .outerjoin()
方法现在 被重新用于将 JOIN 条件附加到现有查询,其方式与 Query.join()
类似,这正是用户一直期望这些方法在任何情况下都做的事情。
为了取代 .c
属性,添加了一个新的属性 SelectBase.selected_columns
。此属性解析为列集合,这正是大多数人希望 .c
执行的操作(但实际上并非如此),即引用 SELECT 语句的列子句中的列。一个常见的初学者错误是以下代码
stmt = select(users)
stmt = stmt.where(stmt.c.name == "foo")
上面的代码看起来很直观,并且它会生成“SELECT * FROM users WHERE name=’foo’”,但是经验丰富的 SQLAlchemy 用户会认识到,它实际上生成了一个无用的子查询,类似于“SELECT * FROM (SELECT * FROM users) WHERE name=’foo’”。
但是,新的 SelectBase.selected_columns
属性 确实 适合上面的用例,就像在上面的情况下,它直接链接到 users.c
集合中存在的列
stmt = select(users)
stmt = stmt.where(stmt.selected_columns.name == "foo")
select().join() 和 outerjoin() 将 JOIN 条件添加到当前查询,而不是创建子查询¶
为了统一 Query
和 Select
,特别是对于 2.0 风格 的 Select
用法,至关重要的是要有一个可用的 Select.join()
方法,其行为类似于 Query.join()
方法,将额外的条目添加到现有 SELECT 的 FROM 子句中,然后返回新的 Select
对象以进行进一步修改,而不是将对象包装在未命名的子查询中并从该子查询返回 JOIN,这种行为一直几乎毫无用处,并且完全误导用户。
为了实现这一点,首先实施了 SELECT 语句不再隐式地被视为 FROM 子句,它将 Select
从必须是 FromClause
中分离出来;这消除了 Select.join()
需要返回 Join
对象,而不是返回包含 FROM 子句中新 JOIN 的 Select
对象的新版本的需求。
从那时起,由于 Select.join()
和 Select.outerjoin()
确实有现有的行为,最初的计划是这些方法将被弃用,而这些方法的新“有用”版本将在备用的“未来” Select
对象上提供,该对象作为单独的导入提供。
然而,在花费一些时间使用这个特定的代码库后,决定拥有两种不同的 Select
对象到处漂浮,每种对象都具有 95% 相同的行为,除了某些方法行为上的一些细微差别,这将比简单地硬性改变这两种方法的行为更具误导性和不方便,考虑到 Select.join()
和 Select.outerjoin()
的现有行为基本上从未使用过,并且只会引起混乱。
因此,考虑到当前行为是如此无用,以及新行为将是如此极其有用和重要,因此决定在这个领域做出一个硬性的行为改变,而不是再等待一年并在过渡期间拥有一个更笨拙的 API。 SQLAlchemy 开发人员不会轻易做出像这样的完全破坏性的更改,但这确实是一个非常特殊的案例,并且极不可能有人正在使用这些方法的先前实现;正如在 SELECT 语句不再隐式地被视为 FROM 子句 中指出的,像 MySQL 和 PostgreSQL 这样的大型数据库在任何情况下都不允许未命名的子查询,并且从语法的角度来看,来自未命名子查询的 JOIN 几乎不可能有用,因为它很难明确地引用其中的列。
在新实现中,Select.join()
和 Select.outerjoin()
现在表现得非常类似于 Query.join()
,通过匹配左侧实体将 JOIN 条件添加到现有语句中
stmt = select(user_table).join(
addresses_table, user_table.c.id == addresses_table.c.user_id
)
产生
SELECT user.id, user.name FROM user JOIN address ON user.id=address.user_id
与 Join
的情况一样,如果可行,ON 子句会自动确定
stmt = select(user_table).join(addresses_table)
当在语句中使用 ORM 实体时,这本质上是使用 2.0 风格 调用构建 ORM 查询的方式。ORM 实体将在内部为语句分配一个“插件”,以便在将语句编译为 SQL 字符串时,将执行与 ORM 相关的编译规则。更直接地说,Select.join()
方法可以容纳 ORM 关系,而不会破坏 Core 和 ORM 内部结构之间的硬性分离
stmt = select(User).join(User.addresses)
还添加了另一个新方法 Select.join_from()
,它允许更轻松地一次指定连接的左侧和右侧
stmt = select(Address.email_address, User.name).join_from(User, Address)
产生
SELECT address.email_address, user.name FROM user JOIN address ON user.id == address.user_id
URL 对象现在是不可变的¶
URL
对象已被正式确定,现在它将自身呈现为具有固定数量的不可变字段的 namedtuple
。此外,由 URL.query
属性表示的字典也是一个不可变的映射。修改 URL
对象不是正式支持或记录的用例,这导致了一些开放式的用例,使得很难拦截不正确的用法,最常见的是修改 URL.query
字典以包含非字符串元素。这也导致了在基本数据对象中允许可变性的所有常见问题,即其他地方不必要的修改泄漏到不期望 URL 更改的代码中。最后,namedtuple 设计的灵感来自 Python 的 urllib.parse.urlparse()
,它将解析后的对象作为命名元组返回。
彻底更改 API 的决定是基于权衡弃用路径的不可行性(这将涉及将 URL.query
字典更改为在调用任何类型的标准库修改方法时发出弃用警告的特殊字典,此外,当字典包含任何类型的元素列表时,该列表也必须在修改时发出弃用警告)与项目首先已经修改 URL
对象的不太可能的用例,以及像 #5341 这样的小改动在任何情况下都会造成向后不兼容。修改 URL
对象的主要情况是在 CreateEnginePlugin
扩展点内解析插件参数,它本身是一个相当新的添加,基于 Github 代码搜索,它被两个存储库使用,但这两个存储库实际上都没有修改 URL 对象。
URL
对象现在提供了一个丰富的接口,用于检查和生成新的 URL
对象。现有的创建 URL
对象机制,即 make_url()
函数,保持不变
>>> from sqlalchemy.engine import make_url
>>> url = make_url("postgresql+psycopg2://user:pass@host/dbname")
对于程序化构造,如果参数作为关键字参数而不是精确的 7 元组传递,则可能直接使用 URL
构造函数或 __init__
方法的代码将收到弃用警告。关键字样式构造函数现在可通过 URL.create()
方法获得
>>> from sqlalchemy.engine import URL
>>> url = URL.create("postgresql", "user", "pass", host="host", database="dbname")
>>> str(url)
'postgresql://user:pass@host/dbname'
字段通常可以使用 URL.set()
方法进行更改,该方法返回一个新的 URL
对象,并应用更改
>>> mysql_url = url.set(drivername="mysql+pymysql")
>>> str(mysql_url)
'mysql+pymysql://user:pass@host/dbname'
要更改 URL.query
字典的内容,可以使用诸如 URL.update_query_dict()
之类的方法
>>> url.update_query_dict({"sslcert": "/path/to/crt"})
postgresql://user:***@host/dbname?sslcert=%2Fpath%2Fto%2Fcrt
要升级直接修改这些字段的代码,向后和向前兼容的方法是使用鸭子类型,如下样式所示
def set_url_drivername(some_url, some_drivername):
# check for 1.4
if hasattr(some_url, "set"):
return some_url.set(drivername=some_drivername)
else:
# SQLAlchemy 1.3 or earlier, mutate in place
some_url.drivername = some_drivername
return some_url
def set_ssl_cert(some_url, ssl_cert):
# check for 1.4
if hasattr(some_url, "update_query_dict"):
return some_url.update_query_dict({"sslcert": ssl_cert})
else:
# SQLAlchemy 1.3 or earlier, mutate in place
some_url.query["sslcert"] = ssl_cert
return some_url
查询字符串保留其现有格式,即字符串到字符串的字典,使用字符串序列来表示多个参数。例如
>>> from sqlalchemy.engine import make_url
>>> url = make_url(
... "postgresql://user:pass@host/dbname?alt_host=host1&alt_host=host2&sslcert=%2Fpath%2Fto%2Fcrt"
... )
>>> url.query
immutabledict({'alt_host': ('host1', 'host2'), 'sslcert': '/path/to/crt'})
要使用 URL.query
属性的内容,以便将所有值规范化为序列,请使用 URL.normalized_query
属性
>>> url.normalized_query
immutabledict({'alt_host': ('host1', 'host2'), 'sslcert': ('/path/to/crt',)})
查询字符串可以通过诸如 URL.update_query_dict()
、URL.update_query_pairs()
、URL.update_query_string()
等方法追加
>>> url.update_query_dict({"alt_host": "host3"}, append=True)
postgresql://user:***@host/dbname?alt_host=host1&alt_host=host2&alt_host=host3&sslcert=%2Fpath%2Fto%2Fcrt
另请参阅
CreateEnginePlugin 的更改¶
CreateEnginePlugin
也受到此更改的影响,因为自定义插件的文档表明应使用 dict.pop()
方法从 URL 对象中删除已使用的参数。现在应该使用 CreateEnginePlugin.update_url()
方法来实现。向后兼容的方法如下所示
from sqlalchemy.engine import CreateEnginePlugin
class MyPlugin(CreateEnginePlugin):
def __init__(self, url, kwargs):
# check for 1.4 style
if hasattr(CreateEnginePlugin, "update_url"):
self.my_argument_one = url.query["my_argument_one"]
self.my_argument_two = url.query["my_argument_two"]
else:
# legacy
self.my_argument_one = url.query.pop("my_argument_one")
self.my_argument_two = url.query.pop("my_argument_two")
self.my_argument_three = kwargs.pop("my_argument_three", None)
def update_url(self, url):
# this method runs in 1.4 only and should be used to consume
# plugin-specific arguments
return url.difference_update_query(["my_argument_one", "my_argument_two"])
有关如何使用此类的完整详细信息,请参阅 CreateEnginePlugin
的文档字符串。
select()、case() 现在接受位置表达式¶
正如本文档其他地方所见,select()
构造现在将接受“列子句”参数作为位置参数,而不是要求它们作为列表传递
# new way, supports 2.0
stmt = select(table.c.col1, table.c.col2, ...)
当以位置方式发送参数时,不允许使用其他关键字参数。在 SQLAlchemy 2.0 中,上述调用样式将是唯一支持的调用样式。
在 1.4 版本期间,以前的调用样式将继续起作用,它将列或其他表达式的列表作为列表传递
# old way, still works in 1.4
stmt = select([table.c.col1, table.c.col2, ...])
上述旧式调用风格也接受旧的关键字参数,这些关键字参数自那时以来已从大多数叙述性文档中删除。这些关键字参数的存在是最初将列子句作为列表传递的原因
# very much the old way, but still works in 1.4
stmt = select([table.c.col1, table.c.col2, ...], whereclause=table.c.col1 == 5)
两种样式之间的检测基于第一个位置参数是否为列表。不幸的是,仍然可能有一些用法看起来像下面这样,其中省略了“whereclause”的关键字
# very much the old way, but still works in 1.4
stmt = select([table.c.col1, table.c.col2, ...], table.c.col1 == 5)
作为此更改的一部分,Select
构造也获得了 2.0 风格的“未来” API,其中包括更新的 Select.join()
方法以及诸如 Select.filter_by()
和 Select.join_from()
之类的方法。
在相关的更改中,case()
构造也已修改为接受其 WHEN 子句列表作为位置参数,对于旧式调用风格也具有类似的弃用轨迹
stmt = select(users_table).where(
case(
(users_table.c.name == "wendy", "W"),
(users_table.c.name == "jack", "J"),
else_="E",
)
)
SQLAlchemy 构造接受 *args
与值列表的约定,就像 ColumnOperators.in_()
这样的构造的后一种情况一样,是位置参数用于结构规范,列表用于数据规范。
所有 IN 表达式都为列表中的每个值动态呈现参数(例如,扩展参数)¶
“扩展 IN”功能,首次在 后期扩展的 IN 参数集允许带有缓存语句的 IN 表达式 中引入,已经足够成熟,以至于它明显优于以前呈现 IN 表达式的方法。随着该方法得到改进以处理空值列表,它现在是 Core / ORM 将用于呈现 IN 参数列表的唯一方法。
自 SQLAlchemy 首次发布以来就存在的先前方法是,当值列表传递给 ColumnOperators.in_()
方法时,该列表将在语句构造时扩展为一系列单独的 BindParameter
对象。这存在局限性,即不可能在语句执行时根据参数字典更改参数列表,这意味着字符串 SQL 语句不能独立于其参数进行缓存,参数字典也不能完全用于通常包含 IN 表达式的语句。
为了服务于 Baked Queries 中描述的“baked query”功能,需要一个可缓存的 IN 版本,这就是“扩展 IN”功能的由来。与现有行为相反,现有行为是在语句构造时将参数列表扩展为单独的 BindParameter
对象,该功能改为使用单个 BindParameter
,它一次存储值列表;当语句由 Engine
执行时,它会根据传递给 Connection.execute()
调用的参数“动态扩展”为单独的绑定参数位置,并且先前执行中可能已检索到的现有 SQL 字符串使用正则表达式进行修改,以适应当前的参数集。这允许相同的 Compiled
对象(它存储呈现的字符串语句)针对不同的参数集多次调用,这些参数集修改传递给 IN 表达式的列表内容,同时仍然保持传递给 DBAPI 的单个标量参数的行为。虽然某些 DBAPI 直接支持此功能,但它通常不可用;“扩展 IN”功能现在为所有后端一致地支持该行为。
由于 1.4 的一个主要重点是允许在 Core 和 ORM 中进行真正的语句缓存,而没有“baked”系统的笨拙性,并且由于“扩展 IN”功能在任何情况下都代表了一种更简单的构建表达式的方法,因此,每当值列表传递给 IN 表达式时,它现在都会自动调用
stmt = select(A.id, A.data).where(A.id.in_([1, 2, 3]))
预执行字符串表示形式是
>>> print(stmt)
SELECT a.id, a.data
FROM a
WHERE a.id IN ([POSTCOMPILE_id_1])
要直接呈现值,请使用 literal_binds
,就像以前的情况一样
>>> print(stmt.compile(compile_kwargs={"literal_binds": True}))
SELECT a.id, a.data
FROM a
WHERE a.id IN (1, 2, 3)
添加了一个新标志“render_postcompile”,作为帮助程序,允许将当前绑定值呈现为将传递给数据库的形式
>>> print(stmt.compile(compile_kwargs={"render_postcompile": True}))
SELECT a.id, a.data
FROM a
WHERE a.id IN (:id_1_1, :id_1_2, :id_1_3)
引擎日志输出也显示了最终呈现的语句
INFO sqlalchemy.engine.base.Engine SELECT a.id, a.data
FROM a
WHERE a.id IN (?, ?, ?)
INFO sqlalchemy.engine.base.Engine (1, 2, 3)
作为此更改的一部分,“空 IN”表达式(其中列表参数为空)的行为现在标准化为使用 IN 运算符对所谓的“空集”。由于没有用于空集的标准 SQL 语法,因此使用了返回零行的 SELECT,针对每个后端以特定方式定制,以便数据库将其视为空集;此功能首次在 1.3 版本中引入,并在 扩展 IN 功能现在支持空列表 中进行了描述。create_engine.empty_in_strategy
参数在 1.2 版本中引入,作为迁移如何处理先前 IN 系统情况的一种手段,现在已被弃用,并且此标志不再具有任何影响;如 IN / NOT IN 运算符的空集合行为现在可配置;默认表达式简化 中所述,此标志允许方言在将列与自身进行比较的原始系统(事实证明这是一个巨大的性能问题)和比较“1 != 1”以生成“false”表达式的新系统之间切换。1.3 引入的行为现在在所有情况下都发生,比这两种方法都更正确,因为仍然使用了 IN 运算符,并且没有原始系统的性能问题。
此外,“扩展”参数系统已被推广,以便它也服务于其他方言特定的用例,其中 DBAPI 或后备数据库无法容纳参数;有关详细信息,请参阅 用于 Oracle、SQL Server 中 LIMIT/OFFSET 的新“后编译”绑定参数。
内置的 FROM linting 将警告 SELECT 语句中任何潜在的笛卡尔积¶
由于 Core 表达式语言和 ORM 都建立在“隐式 FROM”模型之上,其中如果查询的任何部分引用了特定的 FROM 子句,则会自动添加该子句,因此一个常见问题是 SELECT 语句(顶级语句或嵌入式子查询)包含未连接到查询中其余 FROM 元素的 FROM 元素的情况,从而导致结果集中所谓的“笛卡尔积”,即来自每个未以其他方式连接的 FROM 元素的行的每种可能的组合。在关系数据库中,这几乎总是不希望的结果,因为它会产生一个包含重复、不相关数据的庞大结果集。
SQLAlchemy,尽管具有所有出色的功能,但特别容易发生此类问题,因为 SELECT 语句将自动从其他子句中看到的任何表中向其 FROM 子句添加元素。一个典型的场景如下所示,其中两个表连接在一起,但是 WHERE 子句中的一个附加条目可能无意中与这两个表不一致,将创建一个额外的 FROM 条目
address_alias = aliased(Address)
q = (
session.query(User)
.join(address_alias, User.addresses)
.filter(Address.email_address == "foo")
)
上面的查询从 User
和 address_alias
的 JOIN 中选择,后者是 Address
实体的别名。但是,Address
实体直接在 WHERE 子句中使用,因此上面将导致 SQL
SELECT
users.id AS users_id, users.name AS users_name,
users.fullname AS users_fullname,
users.nickname AS users_nickname
FROM addresses, users JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id
WHERE addresses.email_address = :email_address_1
在上面的 SQL 中,我们可以看到 SQLAlchemy 开发人员所说的“可怕的逗号”,因为我们在 FROM 子句中看到了“FROM addresses, users JOIN addresses”,这是笛卡尔积的经典标志;其中查询正在使用 JOIN 来连接 FROM 子句,但是由于其中一个未连接,因此它使用逗号。上面的查询将返回连接“user”和“addresses”表在“id / user_id”列上的完整行集,然后将所有这些行应用到直接针对“addresses”表中每一行的笛卡尔积中。也就是说,如果用户行有 10 行,地址行有 100 行,则上面的查询将返回其预期结果行,很可能是 100 行,因为将选择所有地址行,再次乘以 100,因此总结果大小将为 10000 行。
“table1, table2 JOIN table3”模式也是 SQLAlchemy ORM 中经常发生的模式,这归因于 ORM 功能的细微误用,特别是与连接的预加载或连接的表继承相关的那些功能,以及这些系统中 SQLAlchemy ORM 错误的结果。类似的问题也适用于使用“隐式连接”的 SELECT 语句,其中未使用 JOIN 关键字,而是通过 WHERE 子句将每个 FROM 元素与另一个元素链接起来。
多年来,Wiki 上有一个配方,该配方在查询执行时将图算法应用于 select()
构造,并检查查询的结构以查找这些未链接的 FROM 子句,解析 WHERE 子句和所有 JOIN 子句以确定 FROM 元素是如何链接在一起的,并确保所有 FROM 元素都连接在单个图中。该配方现在已改编为 SQLCompiler
本身的一部分,如果检测到这种情况,它现在可以选择性地为语句发出警告。警告使用 create_engine.enable_from_linting
标志启用,并且默认情况下启用。linter 的计算开销非常低,此外,它仅在语句编译期间发生,这意味着对于缓存的 SQL 语句,它仅发生一次。
使用此功能,我们上面的 ORM 查询将发出警告
>>> q.all()
SAWarning: SELECT statement has a cartesian product between FROM
element(s) "addresses_1", "users" and FROM element "addresses".
Apply join condition(s) between each element to resolve.
linter 功能不仅适应通过 JOIN 子句链接在一起的表,而且还适应通过 WHERE 子句链接在一起的表。在上面,我们可以添加一个 WHERE 子句,将新的 Address
实体与之前的 address_alias
实体链接起来,这将消除警告
q = (
session.query(User)
.join(address_alias, User.addresses)
.filter(Address.email_address == "foo")
.filter(Address.id == address_alias.id)
) # resolve cartesian products,
# will no longer warn
笛卡尔积警告认为 FROM 子句之间的任何类型的链接都是解决方案,即使最终结果集仍然浪费,因为 linter 仅旨在检测完全出乎意料的 FROM 子句的常见情况。如果 FROM 子句在其他地方被显式引用并链接到其他 FROM 子句,则不会发出警告
q = (
session.query(User)
.join(address_alias, User.addresses)
.filter(Address.email_address == "foo")
.filter(Address.id > address_alias.id)
) # will generate a lot of rows,
# but no warning
如果明确声明,也允许完全笛卡尔积;例如,如果我们想要 User
和 Address
的笛卡尔积,我们可以 JOIN on true()
,以便每一行都与另一行匹配;以下查询将返回所有行且不产生警告
from sqlalchemy import true
# intentional cartesian product
q = session.query(User).join(Address, true()) # intentional cartesian product
默认情况下,仅当语句由 Connection
编译以供执行时才生成警告;调用 ClauseElement.compile()
方法不会发出警告,除非提供了 linting 标志
>>> from sqlalchemy.sql import FROM_LINTING
>>> print(q.statement.compile(linting=FROM_LINTING))
SAWarning: SELECT statement has a cartesian product between FROM element(s) "addresses" and FROM element "users". Apply join condition(s) between each element to resolve.
SELECT users.id, users.name, users.fullname, users.nickname
FROM addresses, users JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id
WHERE addresses.email_address = :email_address_1
新的 Result 对象¶
SQLAlchemy 2.0 的一个主要目标是统一 ORM 和 Core 之间“结果”的处理方式。为了实现这一目标,1.4 版本引入了自 SQLAlchemy 诞生之初就一直是 SQLAlchemy 一部分的 ResultProxy
和 RowProxy
对象的新版本。
新对象记录在 Result
和 Row
中,不仅用于 Core 结果集,还用于 ORM 中的 2.0 风格 结果。
此结果对象与 ResultProxy
完全兼容,并包含许多新功能,这些功能现在同样应用于 Core 和 ORM 结果,包括诸如以下方法:
Result.one()
- 返回恰好一行,否则引发异常
with engine.connect() as conn:
row = conn.execute(table.select().where(table.c.id == 5)).one()
Result.one_or_none()
- 相同,但没有行时也返回 None
Result.all()
- 返回所有行
Result.partitions()
- 分块获取行
with engine.connect() as conn:
result = conn.execute(
table.select().order_by(table.c.id),
execution_options={"stream_results": True},
)
for chunk in result.partitions(500):
# process up to 500 records
...
Result.columns()
- 允许对行进行切片和重组
with engine.connect() as conn:
# requests x, y, z
result = conn.execute(select(table.c.x, table.c.y, table.c.z))
# iterate rows as y, x
for y, x in result.columns("y", "x"):
print("Y: %s X: %s" % (y, x))
Result.scalars()
- 返回标量对象列表,默认情况下从第一列返回,但也可以选择其他列
result = session.execute(select(User).order_by(User.id))
for user_obj in result.scalars():
...
Result.mappings()
- 返回字典而不是命名元组行
with engine.connect() as conn:
result = conn.execute(select(table.c.x, table.c.y, table.c.z))
for map_ in result.mappings():
print("Y: %(y)s X: %(x)s" % map_)
当使用 Core 时,Connection.execute()
返回的对象是 CursorResult
的实例,它继续具有与 ResultProxy
相同的 API 特性,关于插入的主键、默认值、行数等等。对于 ORM,将返回 Result
子类,它执行 Core 行到 ORM 行的转换,然后允许所有相同的操作发生。
另请参阅
ORM Query 与 Core Select 统一 - 在 2.0 迁移文档中
RowProxy 不再是 “proxy”;现在称为 Row,其行为类似于增强的命名元组¶
在 Core 结果集中表示单个数据库结果行的 RowProxy
类,现在称为 Row
,并且不再是 “proxy” 对象;这意味着当返回 Row
对象时,该行是一个简单的元组,其中包含最终形式的数据,已经由与数据类型关联的结果行处理函数处理过(示例包括将数据库中的日期字符串转换为 datetime
对象,将 JSON 字符串转换为 Python json.loads()
结果等等)。
这样做的直接理由是使行更像 Python 命名元组,而不是映射,其中元组中的值是元组上 __contains__
运算符的主题,而不是键。通过使 Row
的行为类似于命名元组,它就适合用作 ORM 的 KeyedTuple
对象的替代品,从而最终形成一个 API,其中 ORM 和 Core 都提供行为相同的的结果集。统一 ORM 和 Core 中的主要模式是 SQLAlchemy 2.0 的主要目标,而 1.4 版本旨在使大多数或所有底层架构模式到位,以支持此过程。Query 返回的 “KeyedTuple” 对象被 Row 替换 中的注释描述了 ORM 对 Row
类的使用。
对于 1.4 版本,Row
类提供了一个额外的子类 LegacyRow
,Core 使用它并提供 RowProxy
的向后兼容版本,同时为那些将被移动的 API 特性和行为发出弃用警告。ORM Query
现在直接使用 Row
作为 KeyedTuple
的替代品。
LegacyRow
类是一个过渡类,其中 __contains__
方法仍然针对键而不是值进行测试,同时在操作成功时发出弃用警告。此外,之前 RowProxy
上的所有其他类似映射的方法都已弃用,包括 LegacyRow.keys()
、LegacyRow.items()
等。对于来自 Row
对象的类似映射的行为,包括对这些方法以及面向键的 __contains__
运算符的支持,未来的 API 将首先访问一个特殊的属性 Row._mapping
,然后它将为该行提供完整的映射接口,而不是元组接口。
理由:更像命名元组而不是映射¶
命名元组和映射在布尔运算符方面的差异可以概括。给定伪代码中的 “命名元组” 作为
row = (id: 5, name: 'some name')
最大的跨不兼容性差异是 __contains__
的行为
"id" in row # True for a mapping, False for a named tuple
"some name" in row # False for a mapping, True for a named tuple
在 1.4 中,当 Core 结果集返回 LegacyRow
时,上面的 "id" in row
比较将继续成功,但会发出弃用警告。要将 “in” 运算符用作映射,请使用 Row._mapping
属性
"id" in row._mapping
SQLAlchemy 2.0 的结果对象将具有 .mappings()
修饰符,以便可以直接接收这些映射
# using sqlalchemy.future package
for row in result.mappings():
row["id"]
代理行为消失了,在现代用法中也是不必要的¶
将 Row
重构为类似于元组的行为要求所有数据值都预先完全可用。这是与 RowProxy
的内部行为更改,其中结果行处理函数将在访问行的元素时调用,而不是在首次获取行时调用。这意味着例如,当从 SQLite 检索 datetime 值时,RowProxy
对象中存在的行数据以前看起来像
row_proxy = (1, "2019-12-31 19:56:58.272106")
然后在通过 __getitem__
访问时,datetime.strptime()
函数将动态地用于将上面的字符串日期转换为 datetime
对象。使用新的架构,datetime()
对象在返回的元组中存在,datetime.strptime()
函数只预先调用一次
row = (1, datetime.datetime(2019, 12, 31, 19, 56, 58, 272106))
SQLAlchemy 中的 RowProxy
和 Row
对象是 SQLAlchemy 大部分 C 扩展代码发生的地方。此代码已得到高度重构,以高效地提供新行为,并且整体性能得到了提高,因为 Row
的设计现在相当简单。
先前行为背后的理由假设了一种使用模型,其中结果行可能存在数十个或数百个列,其中大多数列不会被访问,并且其中大多数列将需要一些结果值处理函数。通过仅在需要时调用处理函数,目的是减少不必要的结果处理函数,从而提高性能。
上述假设有很多原因不成立
调用的大多数行处理函数是在 Python 2 下将字节串 Unicode 解码为 Python Unicode 字符串。这正值 Python Unicode 开始被使用,以及 Python 3 出现之前。一旦 Python 3 被引入,在几年内,所有 Python DBAPI 都承担起正确支持直接传递 Python Unicode 对象的作用,在 Python 2 和 Python 3 下都是如此,前者作为一种选择,后者作为唯一的前进方向。最终,在大多数情况下,它也成为 Python 2 的默认设置。SQLAlchemy 的 Python 2 支持仍然为某些 DBAPI(如 cx_Oracle)启用显式的字符串到 Unicode 的转换,但是它现在是在 DBAPI 级别执行的,而不是作为标准的 SQLAlchemy 结果行处理函数。
上述字符串转换(如果使用)是通过 C 扩展使其性能极高,以至于即使在 1.4 中,SQLAlchemy 的字节到 Unicode 编解码器钩子也插入到 cx_Oracle 中,据观察,它比 cx_Oracle 自己的钩子性能更高;这意味着在任何情况下,转换行中所有字符串的开销都不像最初那样显着。
行处理函数在大多数其他情况下都不使用;例外情况是 SQLite 的 datetime 支持、某些后端的 JSON 支持、一些数字处理程序(如字符串到
Decimal
)。在Decimal
的情况下,Python 3 也标准化了高性能的cdecimal
实现,而 Python 2 则继续使用性能差得多的纯 Python 版本。在实际用例中,获取只需要几列的完整行并不常见。在 SQLAlchemy 的早期,来自其他语言的数据库代码形式 “row = fetch(‘SELECT * FROM table’)” 很常见;然而,使用 SQLAlchemy 的表达式语言,在实践中观察到的代码通常会使用所需的特定列。
SELECT 对象和派生的 FROM 子句允许重复的列和列标签¶
此更改允许 select()
构造现在允许重复的列标签以及重复的列对象本身,以便结果元组以与所选列相同的方式组织和排序。ORM Query
已经以这种方式工作,因此此更改允许两者之间更大的交叉兼容性,这是 2.0 过渡的关键目标
>>> from sqlalchemy import column, select
>>> c1, c2, c3, c4 = column("c1"), column("c2"), column("c3"), column("c4")
>>> stmt = select(c1, c2, c3.label("c2"), c2, c4)
>>> print(stmt)
SELECT c1, c2, c3 AS c2, c2, c4
为了支持此更改,ColumnCollection
被 SelectBase
以及派生的 FROM 子句(如子查询)使用,也支持重复的列;这包括新的 SelectBase.selected_columns
属性,已弃用的 SelectBase.c
属性,以及在 FromClause.c
属性,例如在 Subquery
和 Alias
等构造中看到的。
>>> list(stmt.selected_columns)
[
<sqlalchemy.sql.elements.ColumnClause at 0x7fa540bcca20; c1>,
<sqlalchemy.sql.elements.ColumnClause at 0x7fa540bcc9e8; c2>,
<sqlalchemy.sql.elements.Label object at 0x7fa540b3e2e8>,
<sqlalchemy.sql.elements.ColumnClause at 0x7fa540bcc9e8; c2>,
<sqlalchemy.sql.elements.ColumnClause at 0x7fa540897048; c4>
]
>>> print(stmt.subquery().select())
SELECT anon_1.c1, anon_1.c2, anon_1.c2, anon_1.c2, anon_1.c4
FROM (SELECT c1, c2, c3 AS c2, c2, c4) AS anon_1
ColumnCollection
也允许通过整数索引访问,以支持字符串 “key” 不明确的情况
>>> stmt.selected_columns[2]
<sqlalchemy.sql.elements.Label object at 0x7fa540b3e2e8>
为了适应在 ColumnCollection
在 Table
和 PrimaryKeyConstraint
等对象中的使用,对于这些对象更关键的旧的 “去重” 行为在新的类 DedupeColumnCollection
中保留。
此更改包括熟悉的警告 "Column %r on table %r being replaced by %r, which has the same key. Consider use_labels for select() statements."
被移除;Select.apply_labels()
仍然可用,并且仍然被 ORM 用于所有 SELECT 操作,但是它并不意味着列对象的去重,尽管它确实意味着隐式生成的标签的去重
>>> from sqlalchemy import table
>>> user = table("user", column("id"), column("name"))
>>> stmt = select(user.c.id, user.c.name, user.c.id).apply_labels()
>>> print(stmt)
SELECT "user".id AS user_id, "user".name AS user_name, "user".id AS id_1
FROM "user"
最后,此更改通过确保 SELECT 语句中列的数量和位置与给定的内容镜像,从而更容易创建 UNION 和其他 _selectable.CompoundSelect
对象,例如在以下用例中
>>> s1 = select(user, user.c.id)
>>> s2 = select(c1, c2, c3)
>>> from sqlalchemy import union
>>> u = union(s1, s2)
>>> print(u)
SELECT "user".id, "user".name, "user".id
FROM "user" UNION SELECT c1, c2, c3
改进了使用 CAST 或类似方法的简单列表达式的列标签¶
一位用户指出,PostgreSQL 数据库在使用像 CAST 这样的函数对命名列进行操作时,有一个方便的行为,即结果列名与内部表达式的名称相同
test=> SELECT CAST(data AS VARCHAR) FROM foo;
data
------
5
(1 row)
这允许人们将 CAST 应用于表列,同时不会在结果行中丢失列名(上面使用了名称 "data"
)。与 MySQL/MariaDB 以及大多数其他数据库相比,后者的列名取自完整的 SQL 表达式,并且可移植性不高
MariaDB [test]> SELECT CAST(data AS CHAR) FROM foo;
+--------------------+
| CAST(data AS CHAR) |
+--------------------+
| 5 |
+--------------------+
1 row in set (0.003 sec)
在 SQLAlchemy Core 表达式中,我们从不处理像上面那样的原始生成名称,因为 SQLAlchemy 将自动标签应用于这些表达式,直到现在,这些表达式始终是所谓的 “匿名” 表达式
>>> print(select(cast(foo.c.data, String)))
SELECT CAST(foo.data AS VARCHAR) AS anon_1 # old behavior
FROM foo
这些匿名表达式是必要的,因为 SQLAlchemy 的 ResultProxy
大量使用结果列名来匹配数据类型,例如 String
数据类型,它曾经具有结果行处理行为,以匹配正确的列,因此最重要的是,名称必须既易于以数据库无关的方式确定,又在所有情况下都是唯一的。在 SQLAlchemy 1.0 中,作为 #918 的一部分,对结果行中命名列的这种依赖(特别是 PEP-249 cursor 的 cursor.description
元素)被缩减为对于大多数 Core SELECT 构造来说不是必需的;在 1.4 版本中,整个系统对于具有重复列或标签名称的 SELECT 语句变得更加适应,例如在 SELECT 对象和派生的 FROM 子句允许重复的列和列标签 中。因此,我们现在模拟 PostgreSQL 对于单个列的简单修改的合理行为,最突出的是 CAST
>>> print(select(cast(foo.c.data, String)))
SELECT CAST(foo.data AS VARCHAR) AS data
FROM foo
对于针对没有名称的表达式的 CAST,以前的逻辑用于生成通常的 “匿名” 标签
>>> print(select(cast("hi there," + foo.c.data, String)))
SELECT CAST(:data_1 + foo.data AS VARCHAR) AS anon_1
FROM foo
针对 Label
的 cast()
,尽管必须省略标签表达式,因为这些表达式不会在 CAST 内部呈现,但仍将使用给定的名称
>>> print(select(cast(("hi there," + foo.c.data).label("hello_data"), String)))
SELECT CAST(:data_1 + foo.data AS VARCHAR) AS hello_data
FROM foo
当然,与以前一样,可以将 Label
应用于外部的表达式,以直接应用 “AS <name>” 标签
>>> print(select(cast(("hi there," + foo.c.data), String).label("hello_data")))
SELECT CAST(:data_1 + foo.data AS VARCHAR) AS hello_data
FROM foo
用于 Oracle、SQL Server 中 LIMIT/OFFSET 的新 “编译后” 绑定参数¶
1.4 系列的主要目标是确立所有 Core SQL 构造都是完全可缓存的,这意味着特定的 Compiled
结构将生成相同的 SQL 字符串,而与使用的任何 SQL 参数无关,这尤其包括用于指定 LIMIT 和 OFFSET 值的参数,这些参数通常用于分页和 “top N” 样式的结果。
虽然 SQLAlchemy 多年来一直使用绑定参数用于 LIMIT/OFFSET 方案,但仍然存在一些不允许此类参数的异常情况,包括 SQL Server “TOP N” 语句,例如
SELECT TOP 5 mytable.id, mytable.data FROM mytable
以及 Oracle,其中 FIRST_ROWS() 提示(如果将 optimize_limits=True
参数传递给 create_engine()
和 Oracle URL)不允许它们,而且据报道,将绑定参数与 ROWNUM 比较一起使用会产生较慢的查询计划
SELECT anon_1.id, anon_1.data FROM (
SELECT /*+ FIRST_ROWS(5) */
anon_2.id AS id,
anon_2.data AS data,
ROWNUM AS ora_rn FROM (
SELECT mytable.id, mytable.data FROM mytable
) anon_2
WHERE ROWNUM <= :param_1
) anon_1 WHERE ora_rn > :param_2
为了允许所有语句在编译级别上无条件地可缓存,添加了一种称为 “编译后” 参数的新形式的绑定参数,它使用与 “扩展 IN 参数” 相同的机制。这是一个 bindparam()
,其行为与任何其他绑定参数完全相同,只是参数值将在发送到 DBAPI cursor.execute()
方法之前按字面呈现到 SQL 字符串中。新的参数在 SQL Server 和 Oracle 方言中内部使用,以便驱动程序接收字面呈现的值,但 SQLAlchemy 的其余部分仍然可以将此视为绑定参数。上面两个语句在使用 str(statement.compile(dialect=<dialect>))
字符串化时,现在看起来像
SELECT TOP [POSTCOMPILE_param_1] mytable.id, mytable.data FROM mytable
和
SELECT anon_1.id, anon_1.data FROM (
SELECT /*+ FIRST_ROWS([POSTCOMPILE__ora_frow_1]) */
anon_2.id AS id,
anon_2.data AS data,
ROWNUM AS ora_rn FROM (
SELECT mytable.id, mytable.data FROM mytable
) anon_2
WHERE ROWNUM <= [POSTCOMPILE_param_1]
) anon_1 WHERE ora_rn > [POSTCOMPILE_param_2]
[POSTCOMPILE_<param>]
格式也是在使用 “扩展 IN” 时看到的格式。
当查看 SQL 日志输出时,将看到语句的最终形式
SELECT anon_1.id, anon_1.data FROM (
SELECT /*+ FIRST_ROWS(5) */
anon_2.id AS id,
anon_2.data AS data,
ROWNUM AS ora_rn FROM (
SELECT mytable.id AS id, mytable.data AS data FROM mytable
) anon_2
WHERE ROWNUM <= 8
) anon_1 WHERE ora_rn > 3
“编译后参数” 功能通过 bindparam.literal_execute
参数作为公共 API 公开,但目前不打算用于通用用途。字面值是使用底层数据类型的 TypeEngine.literal_processor()
呈现的,在 SQLAlchemy 中,其范围非常有限,仅支持整数和简单的字符串值。
连接级事务现在可以基于子事务变为非活动状态¶
Connection
现在包含以下行为:由于内部事务的回滚,Transaction
可以变为非活动状态,但是 Transaction
在自身回滚之前不会清除。
这本质上是一个新的错误条件,如果内部 “子” 事务已回滚,则将禁止在 Connection
上继续执行语句。此行为与 ORM Session
的行为非常相似,如果外部事务已开始,则需要回滚以清除无效事务;此行为在 “由于先前的刷新期间的异常,此 Session 的事务已回滚。”(或类似) 中描述。
虽然 Connection
的行为模式不如 Session
严格,但此更改是为了帮助识别子事务何时回滚了 DBAPI 事务,但外部代码没有意识到这一点并尝试继续进行,这实际上是在新事务上运行操作。将 Session 加入到外部事务中(例如用于测试套件) 中描述的 “测试工具” 模式是这种情况的常见发生地。
Core 和 ORM 的 “子事务” 功能本身已弃用,并且在 2.0 版本中将不再存在。因此,这个新的错误条件本身是暂时的,因为它在子事务被删除后将不再适用。
为了使用不包含子事务的 2.0 风格的行为,请在 create_engine()
上使用 create_engine.future
参数。
错误消息在 此连接处于非活动事务中。请完全 rollback() 后再继续 中的错误页面中描述。
Enum 和 Boolean 数据类型不再默认 “创建约束”¶
Enum.create_constraint
和 Boolean.create_constraint
参数现在默认为 False,指示当创建这两个数据类型的所谓 “非原生” 版本时,默认情况下将不生成 CHECK 约束。这些 CHECK 约束带来了模式管理维护的复杂性,应该选择加入,而不是默认打开。
要确保为这些类型发出 CREATE CONSTRAINT,请将这些标志设置为 True
class Spam(Base):
__tablename__ = "spam"
id = Column(Integer, primary_key=True)
boolean = Column(Boolean(create_constraint=True))
enum = Column(Enum("a", "b", "c", create_constraint=True))
新功能 - ORM¶
Columns 的 Raiseload¶
“raiseload” 功能,当访问未加载的属性时,它会引发 InvalidRequestError
,现在可用于面向列的属性,使用 defer.raiseload
参数 defer()
。这与关系加载使用的 raiseload()
选项的工作方式相同
book = session.query(Book).options(defer(Book.summary, raiseload=True)).first()
# would raise an exception
book.summary
要在映射上配置列级 raiseload,可以使用 deferred.raiseload
参数 deferred()
。然后可以在查询时使用 undefer()
选项来急切加载属性
class Book(Base):
__tablename__ = "book"
book_id = Column(Integer, primary_key=True)
title = Column(String(200), nullable=False)
summary = deferred(Column(String(2000)), raiseload=True)
excerpt = deferred(Column(Text), raiseload=True)
book_w_excerpt = session.query(Book).options(undefer(Book.excerpt)).first()
最初考虑将现有的 raiseload()
选项扩展到也支持面向列的属性,该选项目前适用于 relationship()
属性。然而,这将破坏 raiseload()
的“通配符”行为,其文档说明允许用户阻止所有关系的加载。
session.query(Order).options(joinedload(Order.items), raiseload("*"))
如上所述,如果我们扩展 raiseload()
以容纳列,则通配符也会阻止列的加载,从而成为向后不兼容的更改;此外,如果 raiseload()
同时涵盖列表达式和关系,那么在不添加新 API 的情况下,将如何实现仅阻止关系加载的上述效果尚不清楚。因此,为了保持简单,列的选项仍然保留在 defer()
上。
raiseload()
- 用于在关系加载时引发异常的查询选项
defer.raiseload
- 用于在列表达式加载时引发异常的查询选项
作为此更改的一部分,“deferred”与属性过期结合的行为已更改。以前,当一个对象被标记为过期,然后通过访问其中一个过期属性而变为未过期时,在映射器级别被映射为 “deferred” 的属性也会加载。现在已更改为,在映射中被 deferred 的属性将永远不会“变为未过期”,它仅在作为 deferral 加载器的一部分被访问时加载。
然而,一个未在映射中被映射为 “deferred” 的属性,但在查询时通过 defer()
选项被 deferred,将在对象或属性过期时被重置;也就是说,deferred 选项被移除。这与之前的行为相同。
另请参阅
ORM 批量插入与 psycopg2 现在在大多数情况下批量处理带有 RETURNING 的语句¶
psycopg2 方言功能默认情况下对 INSERT 语句使用 “execute_values” 和 RETURNING 中的更改增加了对 Core 中同时使用 “executemany” + “RETURNING” 的支持,现在默认情况下为 psycopg2 方言启用此功能,使用了 psycopg2 的 execute_values()
扩展。ORM flush 过程现在利用此功能,以便在不损失批量处理 INSERT 语句的性能优势的情况下,可以检索新生成的主键值和服务器默认值。此外,psycopg2 的 execute_values()
扩展本身比 psycopg2 的默认 “executemany” 实现提供了五倍的性能提升,它通过重写 INSERT 语句以在一个语句中包含多个 “VALUES” 表达式,而不是重复调用相同的语句来实现,因为 psycopg2 缺乏预先 PREPARE 语句的能力,而这通常是这种方法获得高性能所期望的。
SQLAlchemy 在其示例中包含了一个 性能套件,我们可以在其中比较针对 1.3 和 1.4 版本的 “batch_inserts” 运行器生成的时间,揭示了大多数类型的批量插入速度提高了 3 倍到 5 倍
# 1.3
$ python -m examples.performance bulk_inserts --dburl postgresql://scott:tiger@localhost/test
test_flush_no_pk : (100000 iterations); total time 14.051527 sec
test_bulk_save_return_pks : (100000 iterations); total time 15.002470 sec
test_flush_pk_given : (100000 iterations); total time 7.863680 sec
test_bulk_save : (100000 iterations); total time 6.780378 sec
test_bulk_insert_mappings : (100000 iterations); total time 5.363070 sec
test_core_insert : (100000 iterations); total time 5.362647 sec
# 1.4 with enhancement
$ python -m examples.performance bulk_inserts --dburl postgresql://scott:tiger@localhost/test
test_flush_no_pk : (100000 iterations); total time 3.820807 sec
test_bulk_save_return_pks : (100000 iterations); total time 3.176378 sec
test_flush_pk_given : (100000 iterations); total time 4.037789 sec
test_bulk_save : (100000 iterations); total time 2.604446 sec
test_bulk_insert_mappings : (100000 iterations); total time 1.204897 sec
test_core_insert : (100000 iterations); total time 0.958976 sec
请注意,execute_values()
扩展在 psycopg2 层修改了 INSERT 语句,在 SQLAlchemy 记录日志之后。因此,通过 SQL 日志记录,将看到参数集被批量处理在一起,但在应用程序端看不到多个 “values” 的连接。
2020-06-27 19:08:18,166 INFO sqlalchemy.engine.Engine INSERT INTO a (data) VALUES (%(data)s) RETURNING a.id
2020-06-27 19:08:18,166 INFO sqlalchemy.engine.Engine [generated in 0.00698s] ({'data': 'data 1'}, {'data': 'data 2'}, {'data': 'data 3'}, {'data': 'data 4'}, {'data': 'data 5'}, {'data': 'data 6'}, {'data': 'data 7'}, {'data': 'data 8'} ... displaying 10 of 4999 total bound parameter sets ... {'data': 'data 4998'}, {'data': 'data 4999'})
2020-06-27 19:08:18,254 INFO sqlalchemy.engine.Engine COMMIT
最终的 INSERT 语句可以通过在 PostgreSQL 端启用语句日志记录来查看。
2020-06-27 19:08:18.169 EDT [26960] LOG: statement: INSERT INTO a (data)
VALUES ('data 1'),('data 2'),('data 3'),('data 4'),('data 5'),('data 6'),('data
7'),('data 8'),('data 9'),('data 10'),('data 11'),('data 12'),
... ('data 999'),('data 1000') RETURNING a.id
2020-06-27 19:08:18.175 EDT
[26960] LOG: statement: INSERT INTO a (data) VALUES ('data 1001'),('data
1002'),('data 1003'),('data 1004'),('data 1005 '),('data 1006'),('data
1007'),('data 1008'),('data 1009'),('data 1010'),('data 1011'), ...
该功能默认情况下将行批量处理为 1000 行一组,可以使用 Psycopg2 快速执行助手 中记录的 executemany_values_page_size
参数来影响此设置。
ORM 批量更新和删除在可用时对 “fetch” 策略使用 RETURNING¶
使用 “fetch” 策略的 ORM 批量更新或删除
sess.query(User).filter(User.age > 29).update(
{"age": User.age - 10}, synchronize_session="fetch"
)
如果后端数据库支持,现在将使用 RETURNING;目前包括 PostgreSQL 和 SQL Server(Oracle 方言不支持 RETURNING 多行)。
UPDATE users SET age_int=(users.age_int - %(age_int_1)s) WHERE users.age_int > %(age_int_2)s RETURNING users.id
[generated in 0.00060s] {'age_int_1': 10, 'age_int_2': 29}
Col ('id',)
Row (2,)
Row (4,)
对于不支持 RETURNING 多行的后端,仍然使用之前预先发出 SELECT 获取主键的方法。
SELECT users.id FROM users WHERE users.age_int > %(age_int_1)s
[generated in 0.00043s] {'age_int_1': 29}
Col ('id',)
Row (2,)
Row (4,)
UPDATE users SET age_int=(users.age_int - %(age_int_1)s) WHERE users.age_int > %(age_int_2)s
[generated in 0.00102s] {'age_int_1': 10, 'age_int_2': 29}
此更改的复杂挑战之一是支持诸如水平分片扩展之类的用例,其中单个批量更新或删除可能在一些支持 RETURNING 和一些不支持 RETURNING 的后端之间进行多路复用。新的 1.4 执行架构支持这种情况,以便 “fetch” 策略可以保持不变,并优雅地降级为使用 SELECT,而不是必须添加一个新的 “returning” 策略,该策略将不具有后端无关性。
作为此更改的一部分,“fetch” 策略也变得更加高效,因为它将不再使匹配行的对象过期,对于 SET 子句中使用的可以在 Python 中评估的 Python 表达式;这些表达式将以与 “evaluate” 策略相同的方式直接分配到对象上。只有对于无法评估的 SQL 表达式,它才会回退到使属性过期。“evaluate” 策略也得到了增强,对于无法评估的值,它也会回退到 “expire”。
行为变更 - ORM¶
Query 返回的 “KeyedTuple” 对象被 Row 替换¶
正如在 RowProxy 不再是 “proxy”;现在称为 Row,其行为类似于增强的命名元组 中讨论的那样,Core RowProxy
对象现在被一个名为 Row
的类替换。Row
基类现在的行为更像一个完整的命名元组,因此它现在被用作 Query
对象返回的类似元组的结果的基础,而不是之前的 “KeyedTuple” 类。
这样做的理由是为了在 SQLAlchemy 2.0 中,Core 和 ORM SELECT 语句都将使用相同的 Row
对象返回结果行,该对象表现得像一个命名元组。字典式功能可以通过 Row
的 Row._mapping
属性获得。在此期间,Core 结果集将使用 Row
子类 LegacyRow
,它保持了之前的 dict/tuple 混合行为以实现向后兼容性,而 Row
类将直接用于 Query
对象返回的 ORM 元组结果。
已努力使 Row
的大多数功能在 ORM 中可用,这意味着通过字符串名称以及实体/列的访问都应该有效。
row = s.query(User, Address).join(User.addresses).first()
row._mapping[User] # same as row[0]
row._mapping[Address] # same as row[1]
row._mapping["User"] # same as row[0]
row._mapping["Address"] # same as row[1]
u1 = aliased(User)
row = s.query(u1).only_return_tuples(True).first()
row._mapping[u1] # same as row[0]
row = s.query(User.id, Address.email_address).join(User.addresses).first()
row._mapping[User.id] # same as row[0]
row._mapping["id"] # same as row[0]
row._mapping[users.c.id] # same as row[0]
Session 具有新的 “autobegin” 行为¶
以前,默认模式为 autocommit=False
的 Session
将在构造时立即在内部开始一个 SessionTransaction
对象,并在每次调用 Session.rollback()
或 Session.commit()
后创建一个新的。
新的行为是,SessionTransaction
对象现在仅在需要时创建,当调用诸如 Session.add()
或 Session.execute()
等方法时。但是,现在也可以显式调用 Session.begin()
以开始事务,即使在 autocommit=False
模式下也是如此,从而与未来风格的 _base.Connection
的行为相匹配。
这表明的行为变更包括:
Session
现在可以处于未开始事务的状态,即使在autocommit=False
模式下也是如此。以前,此状态仅在 “autocommit” 模式下可用。在此状态下,
Session.commit()
和Session.rollback()
方法是空操作。依赖这些方法使所有对象过期的代码应显式使用Session.begin()
或Session.expire_all()
以适应其用例。SessionEvents.after_transaction_create()
事件钩子不会在Session
创建时立即发出,也不会在Session.rollback()
或Session.commit()
完成后立即发出。Session.close()
方法也不意味着隐式开始新的SessionTransaction
。
另请参阅
理由¶
Session
对象的默认行为 autocommit=False
历史上意味着始终有一个 SessionTransaction
对象在运行,通过 Session.transaction
属性与 Session
关联。当给定的 SessionTransaction
完成时,由于提交、回滚或关闭,它会立即被新的事务替换。SessionTransaction
本身并不意味着任何面向连接的资源的使用,因此这种长期存在的行为具有特殊的优雅之处,因为 Session.transaction
的状态始终可预测为非 None。
然而,作为 #5056 中大幅减少引用循环的倡议的一部分,此假设意味着调用 Session.close()
会导致 Session
对象仍然具有引用循环,并且清理起来更加昂贵,更不用说构造 SessionTransaction
对象会带来少量开销,这意味着对于例如调用 Session.commit()
然后 Session.close()
的 Session
将产生不必要的开销。
因此,决定 Session.close()
应该将 self.transaction
的内部状态(现在在内部称为 self._transaction
)设置为 None,并且新的 SessionTransaction
应该仅在需要时创建。为了保持一致性和代码覆盖率,此行为也扩展到包括所有预期 “autobegin” 的点,而不仅仅是在调用 Session.close()
时。
特别是,这会导致订阅 SessionEvents.after_transaction_create()
事件钩子的应用程序的行为发生变化;以前,当首次构造 Session
时,以及对于大多数关闭先前事务并发出 SessionEvents.after_transaction_end()
的操作,都会发出此事件。新的行为是,当 SessionEvents.after_transaction_create()
在需要时发出,当 Session
尚未创建新的 SessionTransaction
对象,并且映射的对象通过诸如 Session.add()
和 Session.delete()
等方法与 Session
关联时,当调用 Session.transaction
属性时,当 Session.flush()
方法有任务要完成时等等。
此外,依赖 Session.commit()
或 Session.rollback()
方法无条件地使所有对象过期的代码将无法再这样做。需要使所有对象在未发生更改时过期的代码应调用 Session.expire_all()
来处理这种情况。
除了 SessionEvents.after_transaction_create()
事件的发出时间发生变化以及 Session.commit()
或 Session.rollback()
的空操作性质外,此更改应该对 Session
对象的行为没有其他用户可见的影响;Session
将继续具有在调用 Session.close()
后仍然可用于新操作的行为,并且 Session
与 Engine
和数据库本身交互的顺序也应保持不受影响,因为这些操作已经在按需方式运行。
只读关系不同步反向引用¶
在 1.3.14 的 #5149 中,当在目标关系上同时使用 relationship.backref
或 relationship.back_populates
关键字和 relationship.viewonly
标志时,SQLAlchemy 开始发出警告。这是因为 “viewonly” 关系实际上不会持久化对其所做的更改,这可能会导致一些误导性行为。然而,在 #5237 中,我们试图改进此行为,因为在只读关系上设置反向引用存在合理的用例,包括反向填充属性在某些情况下被关系延迟加载器用于确定在另一个方向上不需要额外的急切加载,以及反向填充可以用于映射器内省,并且 backref()
可以是设置双向关系的便捷方法。
因此,解决方案是将反向引用发生的 “mutation” 设为可选,使用 relationship.sync_backref
标志。在 1.4 中,对于也设置了 relationship.viewonly
的关系目标,relationship.sync_backref
的值默认为 False。这表明对 viewonly 关系所做的任何更改都不会以任何方式影响另一侧或 Session
的状态
class User(Base):
# ...
addresses = relationship(Address, backref=backref("user", viewonly=True))
class Address(Base): ...
u1 = session.query(User).filter_by(name="x").first()
a1 = Address()
a1.user = u1
在上面,a1
对象将不会被添加到 u1.addresses
集合中,a1
对象也不会被添加到会话中。以前,这两者都为真。当 relationship.viewonly
为 False
时,不应将 relationship.sync_backref
设置为 False
的警告不再发出,因为这现在是默认行为。
cascade_backrefs 行为已弃用,将在 2.0 中移除¶
长期以来,SQLAlchemy 一直具有基于反向引用赋值将对象级联到 Session
中的行为。给定下面已经在 Session
中的 User
,将其分配给 Address
对象的 Address.user
属性,假设设置了双向关系,这意味着 Address
也将在此时被放入 Session
中。
u1 = User()
session.add(u1)
a1 = Address()
a1.user = u1 # <--- adds "a1" to the Session
上述行为是反向引用行为的意外副作用,因为 a1.user
意味着 u1.addresses.append(a1)
,a1
将被级联到 Session
中。这仍然是 1.4 中的默认行为。在某个时候,添加了一个新的标志 relationship.cascade_backrefs
以禁用上述行为,以及 backref.cascade_backrefs
,以便在通过 relationship.backref
指定关系时设置此标志,因为它可能令人惊讶,并且也会妨碍某些操作,在这些操作中,对象会过早地放置在 Session
中并被过早地 flush。
在 2.0 中,默认行为将是 “cascade_backrefs” 为 False,此外将不会有 “True” 行为,因为这通常不是理想的行为。当启用 2.0 弃用警告时,当实际发生 “backref cascade” 时,将发出警告。要获得新行为,请在任何目标关系上将 relationship.cascade_backrefs
和 backref.cascade_backrefs
设置为 False
,这在 1.3 和更早版本中已经支持,或者使用 Session.future
标志进入 2.0 风格 模式。
Session = sessionmaker(engine, future=True)
with Session() as session:
u1 = User()
session.add(u1)
a1 = Address()
a1.user = u1 # <--- will not add "a1" to the Session
急切加载器在未过期操作期间发出¶
一个长期寻求的行为是,当访问过期的对象时,配置的急切加载器将运行,以便在对象刷新或以其他方式变为未过期时,急切加载过期对象上的关系。此行为现已添加,因此 joinedloaders 将像往常一样添加内联 JOIN,selectin/subquery 加载器将在对象变为未过期或对象刷新时,为给定的关系运行 “immediateload” 操作。
>>> a1 = session.query(A).options(joinedload(A.bs)).first()
>>> a1.data = "new data"
>>> session.commit()
如上所述,A
对象加载了 joinedload()
选项,以便预先加载 bs
集合。在 session.commit()
之后,对象的状态将过期。访问 .data
列属性时,对象将被刷新,这将包括 joinedload 操作。
>>> a1.data
SELECT a.id AS a_id, a.data AS a_data, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id
FROM a LEFT OUTER JOIN b AS b_1 ON a.id = b_1.a_id
WHERE a.id = ?
此行为既适用于直接应用于 relationship()
的加载器策略,也适用于与 Query.options()
一起使用的选项,前提是对象最初是由该查询加载的。
对于“辅助”预先加载器 “selectinload” 和 “subqueryload”,这些加载器的 SQL 策略对于预先加载单个对象上的属性不是必需的;因此,它们将在刷新场景中调用 “immediateload” 策略,该策略类似于 “lazyload” 发出的查询,作为额外的查询发出。
>>> a1 = session.query(A).options(selectinload(A.bs)).first()
>>> a1.data = "new data"
>>> session.commit()
>>> a1.data
SELECT a.id AS a_id, a.data AS a_data
FROM a
WHERE a.id = ?
(1,)
SELECT b.id AS b_id, b.a_id AS b_a_id
FROM b
WHERE ? = b.a_id
(1,)
请注意,加载器选项不适用于以不同方式引入 Session
的对象。也就是说,如果 a1
对象刚刚在此 Session
中持久化,或者在应用 eager 选项之前使用不同的查询加载,则该对象不具有与之关联的 eager 加载选项。 这不是一个新概念,但是寻找刷新行为的 eagerload 的用户可能会发现这一点更明显。
列加载器,例如 deferred()
、with_expression()
仅在最外层的完整实体查询中指示时才生效¶
注意
此更改说明未出现在本文档的早期版本中,但是与所有 SQLAlchemy 1.4 版本相关。
在 1.3 和更早版本中从未支持但在某种程度上会产生特定效果的一种行为是,重新利用列加载器选项,例如 defer()
和 with_expression()
在子查询中,以控制每个子查询的 columns 子句中将包含哪些 SQL 表达式。一个典型的例子是构造 UNION 查询,例如
q1 = session.query(User).options(with_expression(User.expr, literal("u1")))
q2 = session.query(User).options(with_expression(User.expr, literal("u2")))
q1.union_all(q2).all()
在版本 1.3 中,with_expression()
选项将对 UNION 的每个元素生效,例如
SELECT anon_1.anon_2 AS anon_1_anon_2, anon_1.user_account_id AS anon_1_user_account_id,
anon_1.user_account_name AS anon_1_user_account_name
FROM (
SELECT ? AS anon_2, user_account.id AS user_account_id, user_account.name AS user_account_name
FROM user_account
UNION ALL
SELECT ? AS anon_3, user_account.id AS user_account_id, user_account.name AS user_account_name
FROM user_account
) AS anon_1
('u1', 'u2')
SQLAlchemy 1.4 的加载器选项概念已变得更加严格,因此仅应用于 **查询的最外层部分**,即旨在填充要返回的实际 ORM 实体的 SELECT;1.4 中的上述查询将产生
SELECT ? AS anon_1, anon_2.user_account_id AS anon_2_user_account_id,
anon_2.user_account_name AS anon_2_user_account_name
FROM (
SELECT user_account.id AS user_account_id, user_account.name AS user_account_name
FROM user_account
UNION ALL
SELECT user_account.id AS user_account_id, user_account.name AS user_account_name
FROM user_account
) AS anon_2
('u1',)
也就是说,Query
的选项取自 UNION 的第一个元素,因为所有加载器选项都只能位于最顶层。第二个查询中的选项被忽略了。
理由¶
此行为现在更紧密地匹配其他类型的加载器选项的行为,例如关系加载器选项,如 joinedload()
,在所有 SQLAlchemy 版本(包括 1.3 和更早版本)中,在 UNION 情况下,这些选项已经被复制到查询的最顶层,并且仅从 UNION 的第一个元素中获取,丢弃查询其他部分的任何选项。
上面展示的这种隐式复制和选择性忽略选项(相当随意)是一种遗留行为,仅是 Query
的一部分,并且是 Query
及其应用 Query.union_all()
的方式不足的一个特殊例子,因为将单个 SELECT 转换为自身和另一个查询的 UNION,以及加载器选项应如何应用于新语句是含糊不清的。
对于更常见的 defer()
用例,SQLAlchemy 1.4 的行为通常优于 1.3,可以证明这一点。以下查询
q1 = session.query(User).options(defer(User.name))
q2 = session.query(User).options(defer(User.name))
q1.union_all(q2).all()
在 1.3 中,会笨拙地将 NULL 添加到内部查询,然后 SELECT 它
SELECT anon_1.anon_2 AS anon_1_anon_2, anon_1.user_account_id AS anon_1_user_account_id
FROM (
SELECT NULL AS anon_2, user_account.id AS user_account_id
FROM user_account
UNION ALL
SELECT NULL AS anon_2, user_account.id AS user_account_id
FROM user_account
) AS anon_1
如果所有查询都没有设置相同的选项,则由于无法形成正确的 UNION,上述场景将引发错误。
而在 1.4 中,该选项仅应用于顶层,省略了 User.name
的获取,从而避免了这种复杂性
SELECT anon_1.user_account_id AS anon_1_user_account_id
FROM (
SELECT user_account.id AS user_account_id, user_account.name AS user_account_name
FROM user_account
UNION ALL
SELECT user_account.id AS user_account_id, user_account.name AS user_account_name
FROM user_account
) AS anon_1
正确方法¶
使用 2.0 风格 查询,目前不会发出警告,但是嵌套的 with_expression()
选项始终被忽略,因为它们不适用于正在加载的实体,并且不会隐式复制到任何地方。以下查询不会为 with_expression()
调用生成任何输出
s1 = select(User).options(with_expression(User.expr, literal("u1")))
s2 = select(User).options(with_expression(User.expr, literal("u2")))
stmt = union_all(s1, s2)
session.scalars(select(User).from_statement(stmt)).all()
生成 SQL
SELECT user_account.id, user_account.name
FROM user_account
UNION ALL
SELECT user_account.id, user_account.name
FROM user_account
要正确地将 with_expression()
应用于 User
实体,应将其应用于查询的最外层,在每个 SELECT 的 columns 子句中使用普通的 SQL 表达式
s1 = select(User, literal("u1").label("some_literal"))
s2 = select(User, literal("u2").label("some_literal"))
stmt = union_all(s1, s2)
session.scalars(
select(User)
.from_statement(stmt)
.options(with_expression(User.expr, stmt.selected_columns.some_literal))
).all()
这将产生预期的 SQL
SELECT user_account.id, user_account.name, ? AS some_literal
FROM user_account
UNION ALL
SELECT user_account.id, user_account.name, ? AS some_literal
FROM user_account
User
对象本身将在其内容中包含此表达式,位于 User.expr
下。
访问瞬态对象上未初始化的集合属性不再改变 __dict__¶
SQLAlchemy 的行为一直是,访问新创建对象上的映射属性会返回隐式生成的值,而不是引发 AttributeError
,例如标量属性的 None
或列表保持关系的 []
>>> u1 = User()
>>> u1.name
None
>>> u1.addresses
[]
上述行为的理由最初是为了使 ORM 对象更易于使用。由于 ORM 对象在首次创建时表示一个没有任何状态的空行,因此其未访问的属性将解析为标量的 None
(或 SQL NULL)和关系的空集合,这是很直观的。 特别是,它使一种极其常见的模式成为可能,即能够改变新集合,而无需手动创建和首先分配一个空集合
>>> u1 = User()
>>> u1.addresses.append(Address()) # no need to assign u1.addresses = []
直到 SQLAlchemy 1.0 版本,标量属性和集合的此初始化系统的行为都是 None
或空集合将被 *填充* 到对象的状态中,例如 __dict__
。这意味着以下两个操作是等效的
>>> u1 = User()
>>> u1.name = None # explicit assignment
>>> u2 = User()
>>> u2.name # implicit assignment just by accessing it
None
在上面,u1
和 u2
都将在 name
属性的值中填充 None
值。由于这是一个 SQL NULL,因此 ORM 将跳过在 INSERT 中包含这些值,以便进行 SQL 级别的默认设置(如果有),否则该值在数据库端默认为 NULL。
在版本 1.0 中,作为 关于没有预先存在值的属性的属性事件和其他操作的更改 的一部分,此行为得到了改进,以便 None
值不再填充到 __dict__
中,仅返回。除了删除 getter 操作的改变副作用外,此更改还使得可以通过实际分配 None
将具有服务器默认值的列设置为值 NULL,这现在与仅读取它有所区别。
但是,此更改不适用于集合,其中返回未分配的空集合意味着此可变集合每次都会有所不同,并且也无法正确适应对其调用的改变操作(例如 append、add 等)。虽然该行为继续通常不会妨碍任何人,但在 #4519 中最终确定了一个边缘案例,其中此空集合可能有害,即当对象合并到会话中时
>>> u1 = User(id=1) # create an empty User to merge with id=1 in the database
>>> merged1 = session.merge(
... u1
... ) # value of merged1.addresses is unchanged from that of the DB
>>> u2 = User(id=2) # create an empty User to merge with id=2 in the database
>>> u2.addresses
[]
>>> merged2 = session.merge(u2) # value of merged2.addresses has been emptied in the DB
在上面,merged1
上的 .addresses
集合将包含数据库中已有的所有 Address()
对象。merged2
则不会;因为它隐式分配了一个空列表,所以 .addresses
集合将被擦除。这是一个改变副作用实际上可以改变数据库本身的例子。
虽然有人认为属性系统或许应该开始使用严格的“纯 Python”行为,对于非持久对象上不存在的属性在所有情况下都引发 AttributeError
,并要求显式分配所有集合,但对于多年来一直依赖此行为的大量应用程序而言,这种更改可能过于极端,从而导致复杂 的推出/向后兼容性问题,并且很可能恢复旧行为的解决方法会变得普遍,因此在任何情况下都使整个更改无效。
然后,更改是保持默认的生成行为,但最终通过在集合系统中添加额外的机制,使标量属性的非改变行为也成为集合的现实。当访问空属性时,将创建新集合并将其与状态关联,但在实际改变之前,不会将其添加到 __dict__
中
>>> u1 = User()
>>> l1 = u1.addresses # new list is created, associated with the state
>>> assert u1.addresses is l1 # you get the same list each time you access it
>>> assert (
... "addresses" not in u1.__dict__
... ) # but it won't go into __dict__ until it's mutated
>>> from sqlalchemy import inspect
>>> inspect(u1).attrs.addresses.history
History(added=None, unchanged=None, deleted=None)
当列表更改时,它将成为要持久化到数据库的跟踪更改的一部分
>>> l1.append(Address())
>>> assert "addresses" in u1.__dict__
>>> inspect(u1).attrs.addresses.history
History(added=[<__main__.Address object at 0x7f49b725eda0>], unchanged=[], deleted=[])
预计此更改对现有应用程序 *几乎* 没有任何影响,只是已观察到某些应用程序可能依赖于此集合的隐式分配,例如断言对象基于其 __dict__
包含某些值
>>> u1 = User()
>>> u1.addresses
[]
# this will now fail, would pass before
>>> assert {k: v for k, v in u1.__dict__.items() if not k.startswith("_")} == {
... "addresses": []
... }
或确保集合不需要延迟加载即可继续,现在以下(不得不承认很笨拙的)代码也会失败
>>> u1 = User()
>>> u1.addresses
[]
>>> s.add(u1)
>>> s.flush()
>>> s.close()
>>> u1.addresses # <-- will fail, .addresses is not loaded and object is detached
依赖于集合的隐式改变行为的应用程序需要进行更改,以便它们显式分配所需的集合
>>> u1.addresses = []
“新实例与现有标识冲突”错误现在是一个警告¶
SQLAlchemy 始终具有逻辑来检测 Session
中要插入的对象是否与已存在的对象具有相同的主键
class Product(Base):
__tablename__ = "product"
id = Column(Integer, primary_key=True)
session = Session(engine)
# add Product with primary key 1
session.add(Product(id=1))
session.flush()
# add another Product with same primary key
session.add(Product(id=1))
s.commit() # <-- will raise FlushError
更改是 FlushError
已更改为仅是一个警告
sqlalchemy/orm/persistence.py:408: SAWarning: New instance <Product at 0x7f1ff65e0ba8> with identity key (<class '__main__.Product'>, (1,), None) conflicts with persistent instance <Product at 0x7f1ff60a4550>
在此之后,该条件将尝试将行插入数据库,这将发出 IntegrityError
,这与主键标识尚未存在于 Session
中时会引发的错误相同
sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: product.id
理由是允许使用 IntegrityError
来捕获重复项的代码能够正常工作,而与 Session
的现有状态无关,这通常是使用保存点完成的
# add another Product with same primary key
try:
with session.begin_nested():
session.add(Product(id=1))
except exc.IntegrityError:
print("row already exists")
上述逻辑在早期并非完全可行,因为在 Product
对象具有现有标识且已在 Session
中的情况下,代码还必须捕获 FlushError
,此外,该错误并未针对完整性问题的特定条件进行筛选。 随着更改,上述代码块的行为保持一致,除了还会发出警告。
由于所讨论的逻辑处理主键,因此在 INSERT 中发生主键冲突时,所有数据库都会发出完整性错误。在更早版本中不会引发错误的情况是,映射定义了映射可选对象上的主键,该主键比数据库模式中实际配置的主键更具限制性,例如在映射到表的连接时,或者在定义附加列作为数据库模式中实际上未约束的复合主键的一部分时。但是,这些情况也更加一致地工作,因为 INSERT 理论上无论现有标识是否仍在数据库中都会继续进行。 也可以配置警告以使用 Python 警告过滤器引发异常。
viewonly=True 禁止与持久性相关的级联操作¶
当使用 relationship.viewonly
标志将 relationship()
设置为 viewonly=True
时,它表示此关系应仅用于从数据库加载数据,而不应被改变或参与持久性操作。为了确保此约定成功运行,关系不能再指定在“viewonly”方面没有意义的 relationship.cascade
设置。
此处的主要目标是 “delete, delete-orphan” 级联,即使 viewonly 为 True,它们在 1.3 版本中仍然会影响持久性,这是一个错误;即使 viewonly 为 True,如果父对象被删除或对象被分离,对象仍会将这两个操作级联到相关对象。与其修改级联操作以检查 viewonly,不如简单地禁止将两者配置在一起
class User(Base):
# ...
# this is now an error
addresses = relationship("Address", viewonly=True, cascade="all, delete-orphan")
以上将引发
sqlalchemy.exc.ArgumentError: Cascade settings
"delete, delete-orphan, merge, save-update" apply to persistence
operations and should not be combined with a viewonly=True relationship.
自 SQLAlchemy 1.3.12 起,遇到此问题的应用程序应发出警告,对于上述错误,解决方案是删除 viewonly 关系的级联设置。
使用自定义查询查询继承映射时,行为更严格¶
此更改适用于查询连接或单表继承子类实体的情况,给定一个已完成的 SELECT 子查询以从中选择。如果给定的子查询返回的行与请求的多态标识或标识不对应,则会引发错误。以前,在连接表继承下,此条件将静默通过,返回无效的子类,而在单表继承下,Query
将针对子查询添加其他条件以限制结果,这可能会不适当地干扰查询的意图。
给定 Employee
、Engineer(Employee)
、Manager(Employee)
的示例映射,在 1.3 系列中,如果我们针对连接继承映射发出以下查询
s = Session(e)
s.add_all([Engineer(), Manager()])
s.commit()
print(s.query(Manager).select_entity_from(s.query(Employee).subquery()).all())
子查询选择了 Engineer
和 Manager
行,即使外部查询是针对 Manager
的,我们也会得到一个非 Manager
对象返回
SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id
FROM (SELECT employee.type AS type, employee.id AS id
FROM employee) AS anon_1
2020-01-29 18:04:13,524 INFO sqlalchemy.engine.base.Engine ()
[<__main__.Engineer object at 0x7f7f5b9a9810>, <__main__.Manager object at 0x7f7f5b9a9750>]
新行为是此条件引发错误
sqlalchemy.exc.InvalidRequestError: Row with identity key
(<class '__main__.Employee'>, (1,), None) can't be loaded into an object;
the polymorphic discriminator column '%(140205120401296 anon)s.type'
refers to mapped class Engineer->engineer, which is not a sub-mapper of
the requested mapped class Manager->manager
仅当该实体的主键列为非 NULL 时,才会引发上述错误。 如果行中给定实体没有主键,则不会尝试构造实体。
在单表继承映射的情况下,行为的更改稍微复杂一些; 如果上面的 Engineer
和 Manager
使用单表继承进行映射,则在 1.3 中,将发出以下查询,并且仅返回 Manager
对象
SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id
FROM (SELECT employee.type AS type, employee.id AS id
FROM employee) AS anon_1
WHERE anon_1.type IN (?)
2020-01-29 18:08:32,975 INFO sqlalchemy.engine.base.Engine ('manager',)
[<__main__.Manager object at 0x7ff1b0200d50>]
Query
将 “单表继承” 条件添加到子查询,对最初由其设置的意图进行润色。此行为在 1.0 版本中添加到 #3891 中,并在 “连接” 和 “单” 表继承之间创建了行为不一致,并且还会修改给定查询的意图,该查询可能旨在返回与继承实体对应的列为 NULL 的其他行,这是一个有效的用例。 现在,该行为等效于连接表继承的行为,其中假定子查询返回正确的行,并且如果遇到意外的多态标识,则会引发错误
SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id
FROM (SELECT employee.type AS type, employee.id AS id
FROM employee) AS anon_1
2020-01-29 18:13:10,554 INFO sqlalchemy.engine.base.Engine ()
Traceback (most recent call last):
# ...
sqlalchemy.exc.InvalidRequestError: Row with identity key
(<class '__main__.Employee'>, (1,), None) can't be loaded into an object;
the polymorphic discriminator column '%(140700085268432 anon)s.type'
refers to mapped class Engineer->employee, which is not a sub-mapper of
the requested mapped class Manager->employee
适用于 1.3 的上述情况的正确调整是调整给定的子查询,以基于鉴别器列正确筛选行
print(
s.query(Manager)
.select_entity_from(
s.query(Employee).filter(Employee.discriminator == "manager").subquery()
)
.all()
)
SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id
FROM (SELECT employee.type AS type, employee.id AS id
FROM employee
WHERE employee.type = ?) AS anon_1
2020-01-29 18:14:49,770 INFO sqlalchemy.engine.base.Engine ('manager',)
[<__main__.Manager object at 0x7f70e13fca90>]
方言更改¶
pg8000 最低版本为 1.16.6,仅支持 Python 3¶
在项目维护者的帮助下,pg8000 方言的支持得到了极大的改进。
由于 API 更改,pg8000 方言现在需要 1.16.6 或更高版本。pg8000 系列自 1.13 系列起已放弃对 Python 2 的支持。需要 pg8000 的 Python 2 用户应确保他们的要求固定在 SQLAlchemy<1.4
。
PostgreSQL psycopg2 方言需要 psycopg2 版本 2.7 或更高版本¶
psycopg2 方言依赖于过去几年发布的 psycopg2 的许多功能。为了简化方言,2017 年 3 月发布的 2.7 版本现在是所需的最低版本。
psycopg2 方言不再对绑定参数名称有限制¶
SQLAlchemy 1.3 无法容纳在 psycopg2 方言下包含百分号或括号的绑定参数名称。反过来,这意味着包含这些字符的列名也存在问题,因为 INSERT 和其他 DML 语句将生成与列名匹配的参数名称,这将导致失败。解决方法是使用 Column.key
参数,以便使用备用名称来生成参数,或者必须在 create_engine()
级别更改方言的参数样式。 从 SQLAlchemy 1.4.0beta3 开始,所有命名限制都已删除,并且参数在所有场景中都已完全转义,因此不再需要这些解决方法。
psycopg2 方言默认情况下为 INSERT 语句提供带有 RETURNING 的 “execute_values” 功能¶
作为在使用 Core 和 ORM 时 PostgreSQL 性能显着增强的一半,psycopg2 方言现在默认对编译的 INSERT 语句使用 psycopg2.extras.execute_values()
,并且还在此模式下实现了 RETURNING 支持。 此更改的另一半是 ORM 批量插入与 psycopg2 现在在大多数情况下批量处理带有 RETURNING 的语句,这允许 ORM 利用带有 executemany 的 RETURNING(即,批量处理 INSERT 语句),以便使用 psycopg2 的 ORM 批量插入速度提高高达 400%,具体取决于具体情况。
此扩展方法允许在单个语句中插入多行,语句使用扩展的 VALUES 子句。虽然 SQLAlchemy 的 insert()
构造已通过 Insert.values()
方法支持此语法,但扩展方法允许在语句作为 “executemany” 执行执行时动态构造 VALUES 子句,这是在将参数字典列表传递给 Connection.execute()
时发生的情况。 它也发生在缓存边界之外,以便可以在呈现 VALUES 之前缓存 INSERT 语句。
在 性能 示例套件中使用 bulk_inserts.py
脚本对 execute_values()
方法进行快速测试,结果显示性能大约 **提高了五倍**
$ python -m examples.performance bulk_inserts --test test_core_insert --num 100000 --dburl postgresql://scott:tiger@localhost/test
# 1.3
test_core_insert : A single Core INSERT construct inserting mappings in bulk. (100000 iterations); total time 5.229326 sec
# 1.4
test_core_insert : A single Core INSERT construct inserting mappings in bulk. (100000 iterations); total time 0.944007 sec
版本 1.2 在 对批处理模式/快速执行助手的支持 中添加了对 “batch” 扩展的支持,并在 1.3 版本中增强了对 execute_values
扩展的支持,如 #4623 中所述。 在 1.4 中,execute_values
扩展现在默认对 INSERT 语句启用;UPDATE 和 DELETE 的 “batch” 扩展默认保持关闭状态。
此外,execute_values
扩展函数支持返回由 RETURNING 生成的行作为聚合列表。 如果给定的 insert()
构造通过 Insert.returning()
方法或旨在返回生成默认值的类似方法请求返回,则 psycopg2 方言现在将检索此列表;然后将行安装在结果中,以便像它们直接来自游标一样检索它们。 这允许像 ORM 这样的工具在所有情况下都使用批量插入,这有望提供显着的性能改进。
psycopg2 方言的 executemany_mode
功能已修订,具有以下更改
添加了一个新模式
"values_only"
。 此模式对使用 executemany() 运行的编译 INSERT 语句使用非常高性能的psycopg2.extras.execute_values()
扩展方法,但不将execute_batch()
用于 UPDATE 和 DELETE 语句。 此新模式现在是 psycopg2 方言的默认设置。现有的
"values"
模式现在命名为"values_plus_batch"
。 此模式将对 INSERT 语句使用execute_values
,对 UPDATE 和 DELETE 语句使用execute_batch
。 默认情况下未启用该模式,因为它禁用了与executemany()
一起执行的 UPDATE 和 DELETE 语句的cursor.rowcount
的正常功能。对于 INSERT 语句,
"values_only"
和"values"
启用了 RETURNING 支持。 psycopg2 方言将使用 fetch=True 标志从 psycopg2 接收返回的行,并将它们安装到结果集中,就像它们直接来自游标一样(它们最终确实来自游标,但 psycopg2 的扩展函数已将多个批次聚合到一个列表中)。execute_values
的默认 “page_size” 设置已从 100 增加到 1000。execute_batch
函数的默认值仍为 100。 这些参数都可以像以前一样修改。作为 1.2 版本功能一部分的
use_batch_mode
标志已删除;该行为仍然可以通过 1.3 中添加的executemany_mode
标志控制。Core 引擎和方言已得到增强,以支持 executemany 加返回模式,目前仅适用于 psycopg2,通过提供新的
CursorResult.inserted_primary_key_rows
和CursorResult.returned_default_rows
访问器。
另请参阅
从 SQLite 方言中删除 “join rewriting” 逻辑;更新导入¶
删除对右嵌套 join rewriting 的支持,以支持 2013 年发布的 3.7.16 之前的旧 SQLite 版本。预计任何现代 Python 版本都不会依赖此限制。
该行为首次在 0.9 中引入,并且是允许右嵌套连接的更大更改的一部分,如 许多 JOIN 和 LEFT OUTER JOIN 表达式将不再包装在 (SELECT * FROM ..) AS ANON_1 中 中所述。 但是,由于其复杂性,SQLite 解决方法在 2013-2014 年期间产生了许多回归。 2016 年,方言被修改为仅当 SQLite 版本早于 3.7.16 时才会发生 join rewriting 逻辑,在使用二分法来确定 SQLite 在何处修复了对此构造的支持之后,并且没有进一步报告针对该行为的问题(即使在内部发现了一些错误)。 现在预计,对于 Python 2.7 或 3.5 及更高版本(受支持的 Python 版本),很少或没有 Python 构建会包含早于 3.7.17 的 SQLite 版本,并且该行为仅在更复杂的 ORM 连接场景中才是必需的。 如果安装的 SQLite 版本早于 3.7.16,则现在会发出警告。
在相关更改中,SQLite 的模块导入不再尝试在 Python 3 上导入 “pysqlite2” 驱动程序,因为此驱动程序在 Python 3 上不存在; 还删除了关于旧 pysqlite2 版本的非常古老的警告。
为 MariaDB 10.3 添加了 Sequence 支持¶
截至 10.3 版本的 MariaDB 数据库支持序列。SQLAlchemy 的 MySQL 方言现在实现了对针对此数据库的 Sequence
对象 的支持,这意味着对于 Sequence
,将发出 “CREATE SEQUENCE” DDL,该 Table
或 MetaData
集合中存在该 Sequence
,这与 PostgreSQL、Oracle 等后端的工作方式相同,当方言的服务器版本检查确认数据库为 MariaDB 10.3 或更高版本时。此外,当以这些方式使用时,Sequence
将充当列默认值和主键生成对象。
由于此更改将影响当前部署在 MariaDB 10.3 上的应用程序的 DDL 假设以及 INSERT 语句的行为,并且该应用程序恰好在其表定义中显式使用了 Sequence
构造,因此务必注意 Sequence
支持一个标志 Sequence.optional
,该标志用于限制 Sequence
生效的场景。当在表的整数主键列中存在的 Sequence
上使用 “optional” 时
Table(
"some_table",
metadata,
Column(
"id", Integer, Sequence("some_seq", start=1, optional=True), primary_key=True
),
)
仅当目标数据库不支持任何其他为列生成整数主键值的方法时,上述 Sequence
才用于 DDL 和 INSERT 语句。也就是说,上面的 Oracle 数据库将使用序列,而 PostgreSQL 和 MariaDB 10.3 数据库则不会。对于升级到 SQLAlchemy 1.4 的现有应用程序而言,这可能很重要,因为该应用程序可能没有为其后端数据库发出此 Sequence
的 DDL,因为如果 INSERT 语句试图使用未创建的序列,则会失败。
另请参阅
为 SQL Server 添加了与 IDENTITY 不同的序列支持¶
Sequence
构造现在在 Microsoft SQL Server 中完全可用。当应用于 Column
时,表的 DDL 将不再包含 IDENTITY 关键字,而是将依赖于 “CREATE SEQUENCE” 来确保序列存在,然后该序列将用于表上的 INSERT 语句。
版本 1.3 之前的 Sequence
用于控制 SQL Server 中 IDENTITY 列的参数;此用法在 1.3 版本中发出弃用警告,现在已在 1.4 版本中删除。为了控制 IDENTITY 列的参数,应使用 mssql_identity_start
和 mssql_identity_increment
参数;请参阅下面链接的 MSSQL 方言文档。
另请参阅
flambé! 龙和 The Alchemist 图像设计由 Rotem Yaari 创作并慷慨捐赠。
使用 Sphinx 7.2.6 创建。文档最后生成时间:2025 年 3 月 11 日星期二下午 02:40:17 EDT