SqlAlchemy 2.0 中文文档(七十二)(2)https://developer.aliyun.com/article/1561011
ORM Session.execute()
在所有情况下都使用“future”风格的Result
集
如在 RowProxy 不再是“代理”;现在称为 Row 并且行为类似增强命名元组中所述,当与设置了create_engine.future
参数为True
的Engine
一起使用时,Result
和Row
对象现在具有“命名元组”行为。特别是这些“命名元组”行现在包括一个行为变化,即使用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 版本的部分命名元组行为,其中包含检查仍然使用键包含性;如果行中有名为“name”的列,则"name" in row
将返回 True,而不是值。
在使用Session.execute()
时,完整的命名元组样式被无条件地启用,这意味着"name" in row
将使用值包含性作为测试,而不是键包含性。这是为了适应Session.execute()
现在返回一个Result
,该结果还适用于 ORM 结果,即使是由Query.all()
返回的传统 ORM 结果行也使用值包含性。
这是从 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
透明 SQL 编译缓存添加到 Core、ORM 中的所有 DQL、DML 语句
这是 SQLAlchemy 版本中最广泛的变化之一,经过数月的重新组织和重构,从 Core 的基础一直到 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()
在运行 Linux 的 Dell XPS13 上的 SQLAlchemy 1.3 版本中,此示例完成如下:
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”功能可以以与烘焙查询非常相似的方式使用,只是它以一种临时方式可用于任何 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 查询,结合完全“烘焙”风格的调用,可以在许多迭代中运行速度快60%,并且性能与现在被本地缓存系统取代的烘焙查询系统大致相同。
新系统利用现有的Connection.execution_options.compiled_cache
执行选项,并直接向Engine
添加缓存,该缓存使用Engine.query_cache_size
参数进行配置。
1.4 版本中的 API 和行为变化的一个重要部分是为了支持这一新功能。
另请参阅
SQL 编译缓存
声明式现在已经与 ORM 集成,并具有新功能
大约十年后,sqlalchemy.ext.declarative
包现在已经集成到sqlalchemy.orm
命名空间中,除了保留为声明式扩展的声明式“extension”类。
添加到sqlalchemy.orm
的新类包括:
registry
- 一个新的类,取代了“声明基类”的角色,作为映射类的注册表,可以通过字符串名称在relationship()
调用中引用,并且不受任何特定类被映射的风格的影响。declarative_base()
- 这是在声明系统中一直在使用的相同声明基类,只是现在在内部引用了一个registry
对象,并由registry.generate_base()
方法实现,可以直接从registry
调用。declarative_base()
函数会自动生成这个注册表,因此不会影响现有代码。sqlalchemy.ext.declarative.declarative_base
名称仍然存在,在启用 2.0 弃用模式时会发出 2.0 弃用警告。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
类仍然作为声明式扩展包中的扩展。
映射样式现在已经组织起来,它们都从registry
对象扩展,并分为以下类别:
- 声明式映射
- 使用
declarative_base()
基类与元类
- 具有 mapped_column()的声明式表
- 命令式表(又称“混合表”)
- 使用
registry.mapped()
声明式装饰器
- 声明式表
- 命令式表(混合)
- 将 ORM 映射应用于现有数据类(传统数据类用法)
- 命令式(又称“经典”映射)
- 使用
registry.map_imperatively()
- 使用命令式映射映射预先存在的数据类
现有的经典映射函数sqlalchemy.orm.mapper()
仍然存在,但不建议直接调用sqlalchemy.orm.mapper()
;新的registry.map_imperatively()
方法现在通过sqlalchemy.orm.registry()
路由请求,以便与其他声明式映射无歧义地集成。
新方法与第三方类仪器系统互操作,这些系统必须在映射过程之前对类进行操作,允许通过装饰器而不是声明性基类工作的声明性映射,以便像 dataclasses 和 attrs 这样的包可以与声明性映射一起使用,除了与经典映射一起使用。
声明式文档现已完全整合到 ORM 映射器配置文档中,并包括所有样式映射的示例,组织在一个地方。请查看重新组织文档的开始部分 ORM 映射类概述。
另请参阅
ORM 映射类概述
Python Dataclasses, attrs 支持声明式、命令式映射
Python Dataclasses, attrs 支持声明式、命令式映射
除了在 声明式现在与新功能一起集成到 ORM 中 中引入的新声明式装饰器样式外,Mapper
现在明确了解 Python dataclasses
模块,并将识别以这种方式配置的属性,并继续映射它们,而不像以前那样跳过它们。对于 attrs
模块,attrs
已经从类中删除了自己的属性,因此已经与 SQLAlchemy 经典映射兼容。通过 registry.mapped()
装饰器的添加,两个属性系统现在也可以与声明性映射互操作。
另请参阅
将 ORM 映射应用于现有数据类(传统数据类用法)
使用命令式映射映射预先存在的数据类
Core 和 ORM 的异步 IO 支持
SQLAlchemy 现在支持使用全新的 asyncio 前端接口来支持 Python asyncio
兼容的数据库驱动程序,用于 Core 使用的 Connection
和用于 ORM 使用的 Session
,使用 AsyncConnection
和 AsyncSession
对象。
注意
新的 asyncio 功能在 SQLAlchemy 1.4 的初始版本中应被视为alpha 级别。这是一些以前不熟悉的编程技术的全新内容。
初始数据库 API 支持的是 asyncpg PostgreSQL 的 asyncio 驱动程序。
SQLAlchemy 的内部特性完全集成,通过使用 greenlet 库来调整在 SQLAlchemy 内部的执行流程,以将 asyncio 的 await
关键字从数据库驱动程序传播到最终用户 API,该 API 具有 async
方法。使用这种方法,asyncpg 驱动程序在 SQLAlchemy 自己的测试套件中完全可操作,并与大多数 psycopg2 特性兼容。这种方法经过了 greenlet 项目的开发人员的审查和改进,对此 SQLAlchemy 表示感谢。
用户面向的 async
API 本身主要围绕 IO 导向的方法,如 AsyncEngine.connect()
和 AsyncConnection.execute()
。新的 Core 结构严格只支持 2.0 风格 的用法;这意味着所有语句必须在给定连接对象的情况下调用,本例中为 AsyncConnection
。
在 ORM 中,支持 2.0 风格 的查询执行,使用 select()
结构与 AsyncSession.execute()
结合使用;传统的 Query
对象本身不受 AsyncSession
类支持。
传统的 asyncio 编程模型不允许诸如延迟加载相关属性以及过期属性的非 ORM 特性,因为它们表示 IO 操作,这些操作会在 Python 的 getattr()
操作的范围内隐式运行。为了克服这一点,传统 asyncio 应用程序应该审慎使用 急加载 技术,并放弃使用诸如 提交时过期 等特性,以避免需要这样的加载。
对于选择打破传统的 asyncio 应用程序开发人员,新的 API 提供了一个严格可选的功能,使希望使用此类 ORM 功能的应用程序可以选择将与数据库相关的代码组织到函数中,然后可以使用AsyncSession.run_sync()
方法在 greenlets 中运行。请参阅 Asyncio 集成中的greenlet_orm.py
示例进行演示。
支持异步游标的方法也提供了新的方法AsyncConnection.stream()
和AsyncSession.stream()
,支持一个新的AsyncResult
对象,该对象本身提供了常见方法的可等待版本,如AsyncResult.all()
和AsyncResult.fetchmany()
。Core 和 ORM 都与这一特性集成,对应于传统 SQLAlchemy 中“服务器端游标”的使用。
另请参阅
异步 I/O(asyncio)
Asyncio 集成
许多 Core 和 ORM 语句对象现在在编译阶段执行大部分构建和验证工作
1.4 系列中的一个重要举措是处理 Core SQL 语句以及 ORM 查询的模型,以允许有效的、可缓存的语句创建和编译模型,其中编译步骤将被缓存,基于由创建的语句对象生成的缓存键,该对象本身为每次使用新创建。为实现这一目标,构建语句中发生的大部分 Python 计算,特别是 ORM Query
以及在调用 ORM 查询时使用的select()
构造时的计算,正在被移至语句的编译阶段,该阶段仅在语句被调用后发生,并且仅在语句的编译形式尚未被缓存时才会发生。
从最终用户的角度来看,这意味着基于传递给对象的参数可能引发的某些错误消息将不再立即引发,而是仅在首次调用语句时发生。这些条件始终是结构性的,而不是数据驱动的,因此不会因为缓存语句而错过这种条件。
属于此类别的错误条件包括:
- 当构建
_selectable.CompoundSelect
(例如 UNION、EXCEPT 等)时,传递的 SELECT 语句列数不相同时,现在会引发CompileError
;之前,在语句构建时会立即引发ArgumentError
。 - 在调用
Query.join()
时可能出现的各种错误条件将在语句编译时进行评估,而不是在首次调用方法时。
其他可能发生变化的事情涉及直接的Query
对象:
- 在调用
Query.statement
访问器时,行为可能会有所不同。现在返回的Select
对象是与Query
中存在的相同状态的直接副本,而不执行任何 ORM 特定的编译(这意味着速度大大提高)。但是,Select
将不具有与 1.3 版本中相同的内部状态,包括如果在Query
中未明确声明,则明确拼写出 FROM 子句。这意味着依赖于操作此Select
语句的代码,例如调用Select.with_only_columns()
等方法可能需要适应 FROM 子句。
另请参阅
透明 SQL 编译缓存添加到 Core、ORM 中的所有 DQL、DML 语句
修复了内部导入约定,使代码检查工具可以正常工作
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()
函数在没有额外参数的情况下被确定为实现。
另请参阅
ColumnOperators.regexp_match()
ColumnOperators.regexp_replace()
正则表达式支持 - SQLite 实现说明
SQLAlchemy 2.0 弃用模式
1.4 版本的主要目标之一是提供一个“过渡”版本,以便应用程序可以逐渐迁移到 SQLAlchemy 2.0。为此,1.4 版本的一个主要特性是“2.0 弃用模式”,它是一系列弃用警告,针对每个在 2.0 版本中会有不同工作方式的可检测 API 模式发出。所有警告都使用RemovedIn20Warning
类。由于这些警告影响到基础模式,包括select()
和Engine
构造,即使是简单的应用程序也可能生成大量警告,直到适当的 API 更改完成。因此,默认情况下警告模式被关闭,直到开发人员启用环境变量SQLALCHEMY_WARN_20=1
。
要了解如何使用 2.0 弃用模式的完整步骤,请参阅 迁移至 2.0 第二步 - 打开 RemovedIn20Warnings。
另请参阅
SQLAlchemy 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
对象需要具有实际将 JOIN 条件添加到现有 FROM 子句的Select.join()
和Select.outerjoin()
方法,正如用户一直期望它所做的那样。之前的行为是,必须与FromClause
的行为相一致,它会生成一个未命名的子查询,然后再与之 JOIN,这是一个完全无用的功能,只会混淆那些不幸尝试此操作的用户。这一变更在 select().join() 和 outerjoin() 将 JOIN 条件添加到当前查询,而不是创建子查询 中进行了讨论。 - 将一个 SELECT 包含在另一个 SELECT 的 FROM 子句中,而不先创建别名或子查询的行为会导致创建一个未命名的子查询。虽然标准 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()
类似,这正是用户一直期望这些方法做的事情。
新的属性 SelectBase.selected_columns
替代了 .c
属性。该属性解析为一个列集合,大多数人希望 .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")
#4617 ### select().join() 和 outerjoin() 将 JOIN 条件添加到当前查询,而不是创建子查询
为了实现统一Query
和Select
的目标,特别是对于 2.0 风格使用Select
,至关重要的是有一个工作的Select.join()
方法,其行为类似于Query.join()
方法,向现有 SELECT 的 FROM 子句添加额外条目,然后返回新的Select
对象以进行进一步修改,而不是将对象包装在一个无名子查询中并从该子查询返回 JOIN,这种行为对用户来说一直是几乎无用和完全误导的。
为了实现这一点,首先实现了 A SELECT statement is no longer implicitly considered to be a FROM clause,这将Select
从必须是FromClause
中分离出来;这消除了Select.join()
需要返回一个Join
对象而不是包含新 JOIN 的新版本的Select
对象的要求。
从那时起,由于Select.join()
和Select.outerjoin()
已经存在行为,最初的计划是这些方法将被弃用,并且新的“有用”版本的方法将在一个备用的“未来”Select
对象上可用,作为一个单独的导入。
然而,经过一段时间与这个特定代码库一起工作后,决定让两种不同类型的Select
对象漂浮,每个对象的行为几乎相同,只是一些方法的行为略有不同,这将比简单地改变这两种方法的行为更具误导性和不便,因为Select.join()
和Select.outerjoin()
的现有行为几乎从不被使用,只会造成混乱。
因为当前行为非常无用,而新行为将非常有用和重要,因此决定在这一领域做出严格的行为更改,而不是等待另一年并在此期间拥有更加尴尬的 API。 SQLAlchemy 开发人员并不轻易进行完全破坏性的更改,然而这是一个非常特殊的情况,以前几乎不太可能使用这些方法的实现;正如在 A SELECT statement is no longer implicitly considered to be a FROM clause 中所指出的,主要数据库如 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](https://www.sqlalchemy.org/trac/ticket/5341) 这样的小改变在任何情况下都会造成向后不兼容性。对 `URL` 对象进行变异的主要用例是在 `CreateEnginePlugin` 扩展点内解析插件参数,这本身是一个相当新的添加,根据 Github 代码搜索的结果,有两个仓库在使用它,但都没有实际变异 URL 对象。 `URL`对象现在提供了一个丰富的接口来检查和生成新的`URL`对象。用于创建`URL`对象的现有机制,即`make_url()`函数,保持不变: ```py >>> 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
另请参阅
URL
更改 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
中的文档字符串,了解此类的使用方法。
#5526 ### 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", ) )
对于接受*args
与值列表的 SQLAlchemy 构造的约定,就像ColumnOperators.in_()
这样的构造中的后者情况一样,位置参数用于结构规范,列表用于数据规范。
另请参阅
select() 不再接受不同的构造参数,列按位置传递
select() 构造以“legacy”模式创建;关键字参数等
#5284 ### 所有 IN 表达式会动态为列表中的每个值渲染参数(例如,扩展参数)
首次引入的“扩展 IN”功能,见延迟扩展的 IN 参数集允许具有缓存语句的 IN 表达式,已经足够成熟,明显优于以前的渲染 IN 表达式的方法。随着这种方法的改进以处理空值列表,它现在是 Core / ORM 渲染 IN 参数列表的唯一方法。
SQLAlchemy 自首次发布以来一直存在的先前方法是,当一系列值传递给ColumnOperators.in_()
方法时,在语句构造时,列表将被扩展为一系列单独的BindParameter
对象。这受到的限制是,在语句执行时无法根据参数字典变化参数列表,这意味着无法独立缓存字符串 SQL 语句及其参数,也无法完全用于包含 IN 表达式的语句。
为了支持 Baked Queries 中描述的“烘焙查询”功能,需要一个可缓存版本的 IN,这就带来了“扩展 IN”功能。与现有行为相反,在语句构造时将参数列表展开为单独的BindParameter
对象,该功能使用一个存储值列表的单个BindParameter
;当语句由Engine
执行时,它会根据传递给Connection.execute()
调用的参数,在执行过程中动态将其“展开”为基于参数传递的单个绑定参数位置,并且已从先前执行中检索到的现有 SQL 字符串将使用正则表达式进行修改,以适应当前参数集。这允许相同的Compiled
对象,存储渲染的字符串语句,针对修改传递给 IN 表达式的列表内容的不同参数集多次调用,同时仍保持将单个标量参数传递给 DBAPI 的行为。虽然一些 DBAPI 直接支持此功能,但通常不可用;“扩展 IN”功能现在为所有后端一致支持此行为。
1.4 的一个主要重点是在 Core 和 ORM 中允许真正的语句缓存,而不需要“烘焙”系统的尴尬,而且由于“扩展 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 功能现在支持空列表中描述。在版本 1.2 中引入的create_engine.empty_in_strategy
参数,作为迁移以前 IN 系统处理方式的手段,现已弃用,此标志不再起作用;如 IN / NOT IN 运算符的空集合行为现在可配置;默认表达式简化中所述,此标志允许方言在原始系统比较列与自身的情况下切换,这种情况被证明是一个巨大的性能问题,以及比较“1 != 1” 以生成“false” 表达式的新系统。1.3 引入的行为现在在所有情况下都更加正确,因为仍然使用 IN 运算符,并且不具有原始系统的性能问题。
此外,“扩展”参数系统已经泛化,以便为其他特定方言的用例提供服务,其中参数无法由 DBAPI 或后端数据库容纳;详细信息请参见在 Oracle、SQL Server 中用于 LIMIT/OFFSET 的新“编译后”绑定参数。
另请参见
在 Oracle、SQL Server 中用于 LIMIT/OFFSET 的新“编译后”绑定参数
扩展 IN 功能现在支持空列表
BindParameter
#4645 ### 内置的 FROM linting 将警告任何 SELECT 语句中潜在的笛卡尔积
由于核心表达式语言以及 ORM 建立在“隐式 FROMs”模型上,如果查询的任何部分引用了它,那么特定的 FROM 子句将自动添加,一个常见问题是 SELECT 语句,无论是顶级语句还是嵌套子查询,包含了未与查询中的其他 FROM 元素连接的 FROM 元素,导致结果集中出现所谓的“笛卡尔积”,即每个未连接的 FROM 元素之间的所有可能组合的行。在关系数据库中,这几乎总是一个不希望的结果,因为它会产生一个充满重复、不相关数据的巨大结果集。
尽管 SQLAlchemy 具有许多出色的功能,但特别容易发生这种问题,因为 SELECT 语句将自动从其他子句中看到的任何表添加到其 FROM 子句中。典型情况如下,其中两个表被 JOIN 在一起,但是 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
实体的别名。然而,在 WHERE 子句中直接使用了Address
实体,因此上述查询将导致以下 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”表中的每一行的笛卡尔积。也就是说,如果有十个用户行和 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
的笛卡尔积,我们可以在true()
上进行 JOIN,以便每一行都与其他每一行匹配;以下查询将返回所有行并不会产生警告:
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
#4737 ### 新的 Result 对象
SQLAlchemy 2.0 的一个主要目标是统一 ORM 和 Core 之间如何处理“结果”。为实现这一目标,版本 1.4 引入了自 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 查询与 Core Select 统一 - 在 2.0 迁移文档中
#4959 ### RowProxy 不再是“代理”;现在称为 Row,并且像增强型命名元组一样运行
RowProxy
类,代表 Core 结果集中的单个数据库结果行,现在被称为Row
,不再是一个“代理”对象;这意味着当返回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 中,当核心结果集返回一个 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 Unicode 字符串,这是在 Python 2 下开始使用 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 自己的挂接更高效;这意味着在任何情况下,将所有字符串转换为 Unicode 字符串的开销不再像最初那样显著。
- 大多数情况下不使用行处理函数;例外情况包括 SQLite 的日期时间支持,某些后端的 JSON 支持,一些数值处理程序,如字符串转换为
Decimal
。在Decimal
的情况下,Python 3 也标准化了高性能的cdecimal
实现,而在 Python 2 中并非如此,Python 2 仍然使用性能较低的纯 Python 版本。 - 在实际用例中,很少有需要获取完整行而只需要少数列的情况。在 SQLAlchemy 的早期,来自其他语言的数据库代码形式“row = fetch(‘SELECT * FROM table’)”很常见;然而,使用 SQLAlchemy 的表达式语言,实际观察到的代码通常使用所需的特定列。
另请参见
Query 返回的“KeyedTuple”对象被 Row 替换
ORM Session.execute() uses “future” style Result sets in all cases
#4710 ### 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
为了支持这一变化,SelectBase
使用的ColumnCollection
以及派生的 FROM 子句(如子查询)也支持重复列;这包括新的SelectBase.selected_columns
属性,已弃用的SelectBase.c
属性,以及在诸如Subquery
和Alias
等构造中看到的FromClause.c
属性:
>>> 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
还允许通过整数索引访问,以支持当字符串“键”不明确时:
>>> stmt.selected_columns[2] <sqlalchemy.sql.elements.Label object at 0x7fa540b3e2e8>
为了适应Table
和PrimaryKeyConstraint
等对象中对ColumnCollection
的使用,保留了更适用于这些对象的旧的“去重”行为,这一行为在新的DedupeColumnCollection
类中得以保留。
这一变化包括移除了熟悉的警告“表%r 上的列%r 被%r 替换,具有相同的键。考虑为 select()语句使用 use_labels。”
;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"
最后,这一变化使得更容易创建 UNION 和其他_selectable.CompoundSelect
对象,通过确保 SELECT 语句中的列数和位置与给定的相符,例如:
>>> 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
#4753 ### 使用 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的一部分,对于大多数 Core SELECT 构造,不再需要依赖结果行中的命名列(特别是 PEP-249 游标的cursor.description
元素);在 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 ”标签:
>>> 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
#4449 ### 用于 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 中,如果向create_engine()
传递了optimize_limits=True
参数并使用 Oracle URL,那么 FIRST_ROWS()提示(SQLAlchemy 将使用该提示)将不允许它们,但是使用绑定参数与 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=))
将上述两个语句字符串化后看起来如下:
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]
当使用“扩展 IN”时,也会看到[POSTCOMPILE_]
格式。
查看 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 公开,但目前不打算供一般使用。在 SQLAlchemy 中,字面值使用底层数据类型的TypeEngine.literal_processor()
进行呈现,其范围极其有限,仅支持整数和简单字符串值。
#4808 ### 连接级事务现在可以基于子事务处于非活动状态
一个Connection
现在包括了一个行为,即由于内部事务的回滚而使得Transaction
变为非活动状态,但是Transaction
直到自身被回滚之前都不会清除。
这本质上是一个新的错误条件,如果内部“子”事务已被回滚,则将禁止语句执行继续在Connection
上进行。该行为与 ORM Session
的行为非常相似,如果已开始外部事务,则需要将其回滚以清除无效事务;此行为在“由于刷新期间的先前异常,此会话的事务已被回滚。”(或类似)中有描述。
虽然Connection
的行为模式比Session
更宽松,但由于它有助于识别子事务已回滚 DBAPI 事务,但外部代码不知道这一点并尝试继续进行,实际上在新事务上运行操作,因此进行了此更改。在将会话加入外部事务(例如用于测试套件)中描述的“测试工具”模式是发生这种情况的常见地方。
Core 和 ORM 的“子事务”功能本身已被弃用,并将不再出现在 2.0 版本中。因此,这种新的错误条件本身是临时的,一旦子事务被移除,它将不再适用。
为了与不包括子事务的 2.0 风格行为一起工作,请在create_engine()
上使用create_engine.future
参数。
错误消息在错误页面中描述为此连接处于非活动事务状态。请在继续之前完全回滚()。 ### 枚举和布尔数据类型不再默认为“创建约束”
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))
#5367 ### 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
对象需要具有实际添加 JOIN 条件到现有 FROM 子句的Select.join()
和Select.outerjoin()
方法,正如用户一直期望它们做的那样。先前的行为,需要与FromClause
一致,即生成一个无名称的子查询,然后 JOIN 到它,这是一个完全没有用的功能,只会让那些不幸尝试的用户感到困惑。这一变化在 select().join() and outerjoin() add JOIN criteria to the current query, rather than creating a subquery 中讨论。 - 在另一个 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,这种行为对用户来说一直是几乎无用和完全误导的。
为了使这成为可能,首先实现了 A SELECT statement is no longer implicitly considered to be a FROM clause,这将Select
从必须是FromClause
的要求中分离出来;这消除了Select.join()
需要返回一个Join
对象而不是包含新 JOIN 的 FROM 子句的新版本Select
对象的要求。
从那时起,由于 Select.join()
和 Select.outerjoin()
有着现有的行为,最初的计划是这些方法将被弃用,而新的“有用”版本的方法将在一个备用的“未来” Select
对象上作为一个单独的导入可用。
然而,经过一段时间的与这个特定的代码库一起工作后,决定有两种不同类型的 Select
对象在周围漂浮,每个对象的行为都相似,除了某些细微的方法行为上的差异,这比起简单地对这两种方法的行为进行硬性更改更加误导和不便,因为 Select.join()
和 Select.outerjoin()
的现有行为基本上是不会被使用的,只会造成混淆。
所以决定在这一个领域做出一个硬性行为更改,而不是等待另一年并在此期间产生更加尴尬的 API。SQLAlchemy 开发人员不轻易做出像这样完全破坏性的更改,然而这是一个非常特殊的情况,以前的这些方法的实现几乎肯定不会被使用;如 A SELECT statement is no longer implicitly considered to be a FROM clause 中所述,主要数据库如 MySQL 和 PostgreSQL 在任何情况下都不允许未命名的子查询,从语法上来说,从未命名的子查询中进行 JOIN 几乎是不可能有用的,因为在其中明确引用列非常困难。
有了新的实现,Select.join()
和 Select.outerjoin()
现在的行为与 Query.join()
非常相似,通过将 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 关系,而不会破坏核心和 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
SqlAlchemy 2.0 中文文档(七十二)(4)https://developer.aliyun.com/article/1561033