SqlAlchemy 2.0 中文文档(七十三)(1)https://developer.aliyun.com/article/1562202
核心关键变化
完全移除将字符串 SQL 片段强制转换为 text()
首次添加于版本 1.0 的警告,描述在将完整 SQL 片段强制转换为 text() 时发出的警告,现已转换为异常。 对于像Query.filter()
和Select.order_by()
这样的方法传递的字符串片段自动转换为text()
构造的自动转换引发了持续的担忧,尽管这已发出警告。 在Select.order_by()
、Query.order_by()
、Select.group_by()
和Query.group_by()
的情况下,字符串标签或列名仍然会解析为相应的表达式构造,但如果解析失败,则会引发CompileError
,从而防止直接呈现原始 SQL 文本。
#4481 ### “threadlocal”引擎策略已弃用
“线程本地引擎策略”是在 SQLAlchemy 0.2 左右添加的,作为解决在 SQLAlchemy 0.1 中操作的标准方式存在问题的解决方案,可以总结为“一切都是线程本地”。回顾来看,似乎相当荒谬,SQLAlchemy 的首次发布在各方面都是“alpha”版本,已经担心太多用户已经定居在现有 API 上,无法简单地更改它。
SQLAlchemy 的原始使用模型如下:
engine.begin() table.insert().execute(parameters) result = table.select().execute() table.update().execute(parameters) engine.commit()
经过几个月的实际使用,很明显,假装“连接”或“事务”是一个隐藏的实现细节是一个坏主意,特别是当有人需要同时处理多个数据库连接时。因此,我们今天看到的使用范式被引入,减去了上下文管理器,因为它们在 Python 中尚不存在:
conn = engine.connect() try: trans = conn.begin() conn.execute(table.insert(), parameters) result = conn.execute(table.select()) conn.execute(table.update(), parameters) trans.commit() except: trans.rollback() raise finally: conn.close()
上述范式是人们所需要的,但由于仍然有点啰嗦(因为没有上下文管理器),旧的工作方式也被保留了下来,并成为了线程本地引擎策略。
今天,与 Core 一起工作要简洁得多,甚至比原始模式更简洁,这要归功于上下文管理器:
with engine.begin() as conn: conn.execute(table.insert(), parameters) result = conn.execute(table.select()) conn.execute(table.update(), parameters)
此时,任何仍然依赖“threadlocal”风格的代码都将通过此弃用被鼓励进行现代化改造 - 该功能应在下一个主要系列的 SQLAlchemy 中完全移除,例如 1.4 版。连接池参数Pool.use_threadlocal
也已弃用,因为在大多数情况下实际上没有任何效果,Engine.contextual_connect()
方法也已弃用,该方法通常与Engine.connect()
方法是同义的,除非使用了线程本地引擎。
#4393 ### convert_unicode 参数已弃用
参数String.convert_unicode
和create_engine.convert_unicode
已被弃用。这些参数的目的是指示 SQLAlchemy 确保在 Python 2 中传递给数据库之前将传入的 Python Unicode 对象编码为字节字符串,并期望从数据库接收的字节字符串转换回 Python Unicode 对象。在 Python 3 之前的时代,这是一个巨大的挑战,因为几乎所有的 Python DBAPI 默认情况下都没有启用 Unicode 支持,并且大多数都存在与它们提供的 Unicode 扩展相关的主要问题。最终,SQLAlchemy 添加了 C 扩展,其中一个主要目的是加快结果集中的 Unicode 解码过程。
一旦引入了 Python 3,DBAPI 开始更全面地支持 Unicode,并且更重要的是,默认情况下支持 Unicode。然而,特定 DBAPI 在何种条件下返回 Unicode 数据以及接受 Python Unicode 值作为参数的条件仍然非常复杂。这标志着“convert_unicode”标志开始过时,因为它们不再足以确保编码/解码仅在需要时发生,而不是在不需要时发生。相反,“convert_unicode”开始被方言自动检测。这可以在引擎第一次连接时发出的“SELECT ‘test plain returns’”和“SELECT ‘test_unicode_returns’”SQL 中看到;方言正在测试当前 DBAPI 及其当前设置和后端数据库连接是否默认返回 Unicode。
最终结果是,终端用户在任何情况下都不再需要使用“convert_unicode”标志,如果需要,SQLAlchemy 项目需要知道这些情况及原因。目前,在所有主要数据库上,数百个 Unicode 往返测试通过,而不使用此标志,因此可以相当有信心地说它们不再需要,除非在争议的非使用情况下,例如访问来自传统数据库的错误编码数据,这种情况最好使用自定义类型。
方言改进和更改 - PostgreSQL
为 PostgreSQL 分区表添加基本反射支持
SQLAlchemy 可以使用版本 1.2.6 中添加的postgresql_partition_by
标志,在 PostgreSQL 的 CREATE TABLE 语句中呈现“PARTITION BY”序列。然而,'p'
类型直到现在都不是反射查询的一部分。
给定一个类似于以下的模式:
dv = Table( "data_values", metadata_obj, Column("modulus", Integer, nullable=False), Column("data", String(30)), postgresql_partition_by="range(modulus)", ) sa.event.listen( dv, "after_create", sa.DDL( "CREATE TABLE data_values_4_10 PARTITION OF data_values " "FOR VALUES FROM (4) TO (10)" ), )
两个表名 'data_values'
和 'data_values_4_10'
将从 Inspector.get_table_names()
中返回,此外,列也将从 Inspector.get_columns('data_values')
以及 Inspector.get_columns('data_values_4_10')
中返回。这也适用于对这些表使用 Table(..., autoload=True)
。
方言改进和变更 - MySQL
协议级别的 ping 现在用于预 ping
包括 mysqlclient、python-mysql、PyMySQL 和 mysql-connector-python 在内的 MySQL 方言现在使用 connection.ping()
方法进行池预 ping 功能,详情请参阅 Disconnect Handling - Pessimistic。这比以前在连接上发出 “SELECT 1” 的方法更轻量级。### 控制 ON DUPLICATE KEY UPDATE 中参数的排序
可以通过传递一个 2 元组列表来显式地为 ON DUPLICATE KEY UPDATE
子句中的 UPDATE 参数进行排序:
from sqlalchemy.dialects.mysql import insert insert_stmt = insert(my_table).values(id="some_existing_id", data="inserted value") on_duplicate_key_stmt = insert_stmt.on_duplicate_key_update( [ ("data", "some data"), ("updated_at", func.current_timestamp()), ], )
另请参阅
INSERT…ON DUPLICATE KEY UPDATE(Upsert)
方言改进和变更 - SQLite
添加对 SQLite JSON 的支持
添加了新的数据类型 JSON
,它代表了 JSON
基本数据类型的 SQLite 的 json 成员访问函数。实现使用 SQLite 的 JSON_EXTRACT
和 JSON_QUOTE
函数提供基本的 JSON 支持。
请注意,数据库中呈现的数据类型本身的名称为“JSON”。这将创建一个具有“numeric”亲和性的 SQLite 数据类型,通常情况下不应该成为问题,除非是由单个整数值组成的 JSON 值的情况。尽管如此,根据 SQLite 自己文档中的示例,JSON 的名称仍然被用于其熟悉性。参见 www.sqlite.org/json1.html
#3850 ### 添加对约束中 SQLite ON CONFLICT 的支持
SQLite 支持非标准的 ON CONFLICT 子句,可为独立约束以及一些列内约束(如 NOT NULL)指定。通过向 UniqueConstraint
等对象添加 sqlite_on_conflict
关键字,已对这些子句进行了支持,以及几种 Column
-特定变体:
some_table = Table( "some_table", metadata_obj, Column("id", Integer, primary_key=True, sqlite_on_conflict_primary_key="FAIL"), Column("data", Integer), UniqueConstraint("id", "data", sqlite_on_conflict="IGNORE"), )
上表将在 CREATE TABLE 语句中呈现为:
CREATE TABLE some_table ( id INTEGER NOT NULL, data INTEGER, PRIMARY KEY (id) ON CONFLICT FAIL, UNIQUE (id, data) ON CONFLICT IGNORE )
另请参阅
对约束的 ON CONFLICT 支持
方言改进和变化 - Oracle
国家字符数据类型弱化以支持通用 Unicode,可通过选项重新启用
现在,默认情况下,Unicode
和UnicodeText
数据类型现在对应于 Oracle 上的VARCHAR2
和CLOB
数据类型,而不是NVARCHAR2
和NCLOB
(也称为“国家”字符集类型)。这将在CREATE TABLE
语句中的呈现行为中看到,以及当使用Unicode
或UnicodeText
的绑定参数时,不会传递类型对象给setinputsizes()
;cx_Oracle 会原生处理字符串值。这种变化基于 cx_Oracle 维护者的建议,即 Oracle 中的“国家”数据类型在很大程度上已经过时且性能不佳。它们还会在某些情况下干扰,比如应用于trunc()
等函数的格式说明符时。
当数据库不使用符合 Unicode 标准的字符集时,可能需要使用NVARCHAR2
和相关类型的情况。在这种情况下,可以通过向create_engine()
传递标志use_nchar_for_unicode
来重新启用旧行为。
始终明确使用NVARCHAR2
和NCLOB
数据类型将继续使用NVARCHAR2
和NCLOB
,包括在 DDL 中以及在处理绑定参数时使用 cx_Oracle 的setinputsizes()
。
在读取方面,在 Python 2 下已添加了对 CHAR/VARCHAR/CLOB 结果行的自动 Unicode 转换,以匹配 Python 3 下 cx_Oracle 的行为。为了减轻 cx_Oracle 方言在 Python 2 下先前具有的性能问题,SQLAlchemy 在 Python 2 下使用非常高效(当构建 C 扩展时)的本机 Unicode 处理程序。可以通过将coerce_to_unicode
标志设置为 False 来禁用自动 Unicode 强制转换。此标志现在默认为 True,并适用于所有在结果集中返回的不明确为Unicode
或 Oracle 的 NVARCHAR2/NCHAR/NCLOB 数据类型的字符串数据。
#4242 ### cx_Oracle 连接参数现代化,已弃用的参数已移除
�� cx_oracle 方言接受的参数以及 URL 字符串进行了一系列现代化处理:
- 弃用的参数
auto_setinputsizes
、allow_twophase
、exclude_setinputsizes
已被移除。 threaded
参数的值,在 SQLAlchemy 方言中一直默认为 True,现在不再默认生成。SQLAlchemyConnection
对象本身不被视为线程安全,因此不需要传递此标志。- 将
threaded
传递给create_engine()
本身已被弃用。要将threaded
的值设置为True
,请将其传递给create_engine.connect_args
字典或使用查询字符串,例如oracle+cx_oracle://...?threaded=true
。 - 所有传递到 URL 查询字符串的参数,除非另有特殊处理,现在都传递给 cx_Oracle.connect()函数。其中一些也被强制转换为 cx_Oracle 常量或布尔值,包括
mode
、purity
、events
和threaded
。 - 与以往一样,所有 cx_Oracle
.connect()
参数都通过create_engine.connect_args
字典接受,文档对此描述不准确。
方言改进和变化 - SQL Server
支持 pyodbc fast_executemany
Pyodbc 最近添加的“fast_executemany”模式,在使用 Microsoft ODBC 驱动程序时可用,现在是 pyodbc / mssql 方言的选项。通过create_engine()
传递:
engine = create_engine( "mssql+pyodbc://scott:tiger@mssql2017:1433/test?driver=ODBC+Driver+13+for+SQL+Server", fast_executemany=True, )
另请参阅
快速 Executemany 模式
#4158 ### 新参数影响 IDENTITY 的起始和增量,使用 Sequence 已被弃用
SQL Server 自 SQL Server 2012 起现在支持具有真实 CREATE SEQUENCE
语法的序列。在 #4235 中,SQLAlchemy 将添加对这些序列的支持,使用 Sequence
,方式与其他任何方言一样。然而,目前的情况是 Sequence
已经在 SQL Server 上重新用途,以影响主键列的 IDENTITY
规范的 “start” 和 “increment” 参数。为了向普通序列也可用过渡,使用 Sequence
将在 1.3 系列中的整个过渡期间发出弃用警告。为了影响 “start” 和 “increment”,请在 Column
上使用新的 mssql_identity_start
和 mssql_identity_increment
参数:
test = Table( "test", metadata_obj, Column( "id", Integer, primary_key=True, mssql_identity_start=100, mssql_identity_increment=10, ), Column("name", String(20)), )
要在非主键列上发出 IDENTITY
,这是一个很少使用但有效的 SQL Server 情况,请使用 Column.autoincrement
标志,在目标列上将其设置为 True
,在任何整数主键列上设置为 False
:
test = Table( "test", metadata_obj, Column("id", Integer, primary_key=True, autoincrement=False), Column("number", Integer, autoincrement=True), )
另请参阅
自动增量行为 / IDENTITY 列
#4235 ## 更改的 StatementError 格式(换行符和 %s)
对于 StatementError
的字符串表示引入了两个更改。字符串表示的“详细信息”和“SQL”部分现在由换行符分隔,并且保留了原始 SQL 语句中存在的换行符。目标是在保持原始错误消息单行记录的同时提高可读性。
这意味着以前看起来像这样的错误消息:
sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError) A value is required for bind parameter 'id' [SQL: 'select * from reviews\nwhere id = ?'] (Background on this error at: https://sqlalche.me/e/cd3x)
现在将如下所示:
sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError) A value is required for bind parameter 'id' [SQL: select * from reviews where id = ?] (Background on this error at: https://sqlalche.me/e/cd3x)
此更改的主要影响是消费者不再能假定完整的异常消息在单行上,但是来自 DBAPI 驱动程序或 SQLAlchemy 内部生成的原始 “错误” 部分仍将位于第一行。
介绍
本指南介绍了 SQLAlchemy 版本 1.3 中的新功能,并记录了对将应用程序从 SQLAlchemy 1.2 系列迁移到 1.3 系列的用户产生影响的更改。
请仔细查看行为更改部分,了解可能的向后不兼容的行为更改。
一般
为所有弃用元素发出弃用警告;添加新的弃用
发行版 1.3 确保所有被弃用的行为和 API,包括所有长期被列为“遗留”的行为和 API,都会发出 DeprecationWarning
警告。这包括在使用诸如 Session.weak_identity_map
这样的参数和 MapperExtension
这样的类时。虽然所有弃用已在文档中注明,但通常它们没有使用正确的重新构造文本指令,或者包含它们被弃用的版本。一个特定的 API 功能是否实际发出弃用警告并不一致。一般的态度是,大多数或所有这些弃用功能都被视为长期遗留功能,没有计划删除它们。
此次变更包括所有文档化的弃用现在都在文档中使用了正确的重新构造文本指令,并附有版本号,明确指出该功能或用例将在将来的版本中删除(例如,不再有永久使用的旧用例),以及使用任何此类功能或用例都将明确发出 DeprecationWarning
警告,在 Python 3 中以及使用像 Pytest 这样的现代测试工具时,现在在标准错误流中更加明确。目标是,这些长时间弃用的功能,回溯到版本 0.7 或 0.6,应该开始被完全删除,而不是保留它们作为“遗留”功能。此外,从版本 1.3 开始,一些重大的新弃用将被添加。由于 SQLAlchemy 在数千开发人员的实际使用中已有 14 年,因此可以指出一种混合得很好的用例流,并且修剪掉与这种单一工作方式相违背的功能和模式。
更大的背景是 SQLAlchemy 试图适应即将到来的仅支持 Python 3 的世界,以及类型注解的世界,为此,初步计划对 SQLAlchemy 进行重大改版,希望大大减少 API 的认知负担,以及对 Core 和 ORM 之间众多实现和使用差异的重大调整。由于这两个系统在 SQLAlchemy 首次发布后发生了巨大变化,特别是 ORM 仍保留了许多“后期添加的”行为,这使得 Core 和 ORM 之间的隔离墙过高。通过提前将 API 集中在每个支持的用例的单一模式上,将来对显着改变的 API 进行迁移的工作变得更加简单。
对于在 1.3 中添加的最重大的弃用,请参见下面的链接部分。
另请参阅
“threadlocal” 引擎策略已弃用
convert_unicode 参数已弃用
关于 AliasedClass 的关系取代了非主映射器的需要
#4393 ### 所有弃用元素都会发出弃用警告;新增弃用
发布 1.3 确保所有被弃用的行为和 API,包括那些多年来一直被列为“遗留”的行为,都会发出DeprecationWarning
警告。这包括当使用参数如Session.weak_identity_map
和类似MapperExtension
时。虽然所有弃用都已在文档中记录,但通常它们没有使用适当的重构文本指令,或者包含它们被弃用的版本。特定 API 功能是否实际发出弃用警告并不一致。一般的态度是,大多数或所有这些弃用功能都被视为长期遗留功能,没有计划删除它们。
此更改包括,所有记录的弃用现在在文档中使用适当的重构文本指令,并附有版本号,明确说明该功能或用例将在将来的版本中被移除(例如,不再有永久的遗留用例),并且使用任何此类功能或用例肯定会发出DeprecationWarning
,在 Python 3 中以及使用现代测试工具如 Pytest 时,现在在标准错误流中更加明确。目标是,这些长期弃用的功能,可以追溯到版本 0.7 或 0.6,应该开始被完全移除,而不是将它们保留为“遗留”功能。此外,一些重大的新弃用功能正在版本 1.3 中添加。由于 SQLAlchemy 在数千开发人员的实际使用中已经有了 14 年的历史,可以指出一个混合在一起的使用案例流,以及修剪掉与这种单一工作方式相悖的功能和模式。
SQLAlchemy 的更大背景是,它试图适应即将到来的 Python 3-only 世界,以及一个类型注释的世界,为了实现这个目标,有暂定计划对 SQLAlchemy 进行重大改造,希望能大大减少 API 的认知负荷,并对 Core 和 ORM 之间的实现和使用之间的许多差异进行重大调整。由于这两个系统在 SQLAlchemy 首次发布后发生了巨大变化,特别是 ORM 仍然保留了许多“外挂”行为,使得 Core 和 ORM 之间的隔离墙过高。通过提前将 API 集中在每个支持的用例的单一模式上,将来迁移到显著改变的 API 的工作变得更简单。
对于 1.3 中添加的最重要的弃用功能,请参见下面的链接部分。
另请参阅
“threadlocal” engine strategy deprecated
convert_unicode parameters deprecated
与别名类的关系替代了非主要映射器的需求
新功能和改进 - ORM
与别名类的关系替代了非主要映射器的需求
“非主要映射器”是以命令式映射风格创建的Mapper
,它充当已经映射的类的额外映射器,针对不同类型的可选择对象。非主要映射器起源于 SQLAlchemy 的 0.1、0.2 系列,那时预计Mapper
对象将是主要的查询构建接口,之后才有Query
对象的存在。
随着Query
的出现,以及后来的AliasedClass
构造,大多数非主要映射器的用例都消失了。这是一件好事,因为 SQLAlchemy 在 0.5 系列左右也完全放弃了“经典”映射,转而采用了声明式系统。
当意识到一些非常难以定义的relationship()
配置可能会成为可能时,仍然保留了非主要映射器的一个用例。当使用非主要映射器作为映射目标时,可以使用替代可选择项,而不是尝试构建一个relationship.primaryjoin
,该关系涵盖了特定对象间关系的所有复杂性。
随着这个用例变得越来越流行,它的局限性也变得明显,包括非主要映射器难以配置以适应添加新列的可选择项,映射器不继承原始映射的关系,明确配置在非主要映射器上的关系与加载器选项不兼容,非主要映射器还不能提供可在查询中使用的基于列的属性的完全功能命名空间(在旧的 0.1 - 0.4 时代,人们会直接使用Table
对象与 ORM)。
缺失的部分是允许 relationship()
直接引用 AliasedClass
。AliasedClass
已经做了非主映射器所要做的一切;它允许从备用可选择中加载现有映射的类,它继承现有映射器的所有属性和关系,它与加载器选项非常配合,还提供了一个可以像类本身一样混入查询的类似对象。通过这个改变,原来针对非主映射器的配置关系连接的配方被更改为别名类。
在关系到别名类处,原始的非主映射器如下所示:
j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id) B_viacd = mapper( B, j, non_primary=True, primary_key=[j.c.b_id], properties={ "id": j.c.b_id, # so that 'id' looks the same as before "c_id": j.c.c_id, # needed for disambiguation "d_c_id": j.c.d_c_id, # needed for disambiguation "b_id": [j.c.b_id, j.c.d_b_id], "d_id": j.c.d_id, }, ) A.b = relationship(B_viacd, primaryjoin=A.b_id == B_viacd.c.b_id)
这些属性是必需的,以便重新映射额外的列,使其不与已映射到B
的现有列发生冲突,同时还需要定义一个新的主键。
使用新方法,所有这些冗长都会消失,并且在建立关系时可以直接引用额外的列:
j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id) B_viacd = aliased(B, j, flat=True) A.b = relationship(B_viacd, primaryjoin=A.b_id == j.c.b_id)
非主映射器现已不推荐使用,最终目标是经典映射作为一种特性完全消失。声明性 API 将成为唯一的映射方式,这有望带来内部改进和简化,以及更清晰的文档说明。
#4423 ### selectin 加载不再使用 JOIN 来进行简单的一对多加载
1.2 版中添加的“selectin”加载功能引入了一种极其高效的新方法来急切加载集合,在许多情况下比“subquery”急切加载要快得多,因为它不依赖于重复原始 SELECT 查询,而是使用一个简单的 IN 子句。然而,“selectin”加载仍然依赖于在父表和相关表之间渲染 JOIN,因为它需要在行中使用父主键值来匹配行。在 1.3 中,添加了一个新的优化,将在简单的一对多加载的最常见情况下省略这个 JOIN,其中相关的行已经包含了其外键列中表达的父行的主键。这再次提供了显着的性能改进,因为 ORM 现在可以一次性加载大量集合,而完全不使用 JOIN 或子查询。
给定一个映射:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) bs = relationship("B", lazy="selectin") class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id"))
在 1.2 版本的“selectin”加载中,从 A 到 B 的加载如下所示:
SELECT a.id AS a_id FROM a SELECT a_1.id AS a_1_id, b.id AS b_id, b.a_id AS b_a_id FROM a AS a_1 JOIN b ON a_1.id = b.a_id WHERE a_1.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY a_1.id (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
使用新行为,加载如下:
SELECT a.id AS a_id FROM a SELECT b.a_id AS b_a_id, b.id AS b_id FROM b WHERE b.a_id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY b.a_id (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
该行为被自动释放,使用类似于延迟加载的启发式方法,以确定相关实体是否可以直接从标识映射中获取。然而,与大多数查询功能一样,由于关于多态加载的高级场景,该功能的实现变得更加复杂。如果遇到问题,用户应该报告错误,但是该更改还包括一个标志relationship.omit_join
,可以在relationship()
上设置为False
以禁用优化。
#4340 ### 改进多对一查询表达式的行为
当构建一个查询,将一个多对一的关系与一个对象值进行比较时,比如:
u1 = session.query(User).get(5) query = session.query(Address).filter(Address.user == u1)
上述表达式Address.user == u1
最终编译成一个 SQL 表达式,通常基于User
对象的主键列,如"address.user_id = 5"
,使用延迟可调用来在绑定表达式中尽可能晚地检索值5
。这是为了适应两种情况:Address.user == u1
表达式可能针对尚未刷新的User
对象,依赖于服务器生成的主键值,以及即使自表达式创建以来u1
的主键值已更改,表达式始终返回正确结果的情况。
然而,这种行为的一个副作用是,如果u1
在表达式被评估时已经过期,就会导致额外的 SELECT 语句,而且如果u1
也已经从Session
中分离,就会引发错误:
u1 = session.query(User).get(5) query = session.query(Address).filter(Address.user == u1) session.expire(u1) session.expunge(u1) query.all() # <-- would raise DetachedInstanceError
当Session
提交并且u1
实例超出范围时,对象的过期/清除可能会隐式发生,因为Address.user == u1
表达式并不强烈引用对象本身,只引用其InstanceState
。
修复方法是允许 Address.user == u1
表达式根据尝试在表达式编译时正常检索或加载值的结果来评估值 5
,就像现在一样,但如果对象已分离并已过期,则从一个新的机制中检索它 InstanceState
,该机制将在属性过期时对该状态上的特定属性的最后已知值进行备忘录。此机制仅在表达式功能需要时为特定属性/ InstanceState
启用,以节省性能/内存开销。
最初,尝试了诸如立即评估表达式并尝试稍后加载值的各种安排等更简单的方法,但困难的边缘情况是正在更改的列属性(通常是自然主键)的值。为了确保诸如 Address.user == u1
的表达式始终返回 u1
当前状态的正确答案,它将返回持久对象的当前数据库持久值,如果需要通过 SELECT 查询取消过期,并且对于已分离的对象,它将返回最近已知的值,而不管对象何时使用 InstanceState
来跟踪列属性的最后已知值,无论何时属性将被过期。
当无法评估值时,现代属性 API 功能用于指示特定的错误消息,这两种情况是当列属性从未设置时,以及当第一次进行评估时对象已过期时。在所有情况下,不再引发 DetachedInstanceError
。
#4359 ### 多对一替换不会对“raiseload”或“old”对象引发异常
考虑到延迟加载将继续在多对一关系上进行,以加载“old”值,如果关系未指定 relationship.active_history
标志,则不会对分离的对象引发断言:
a1 = session.query(Address).filter_by(id=5).one() session.expunge(a1) a1.user = some_user
上面,在分离的 a1
对象上替换 .user
属性时,如果属性试图从标识映射中检索 .user
的先前值,则会引发 DetachedInstanceError
。变化在于该操作现在在不加载旧值的情况下继续进行。
同样的改变也适用于lazy="raise"
加载器策略:
class Address(Base): # ... user = relationship("User", ..., lazy="raise")
以前,对a1.user
的关联将引发“raiseload”异常,因为属性试图检索先前的值。在加载“旧”值的情况下,现在会跳过此断言。
#4353 ### 为 ORM 属性实现了“del”
Python del
操作实际上对于映射属性(标量列或对象引用)并不可用。已添加支持,使其可以正常工作,其中del
操作大致等同于将属性设置为None
值:
some_object = session.query(SomeObject).get(5) del some_object.some_attribute # from a SQL perspective, works like "= None"
#4354 ### info 字典添加到 InstanceState
将.info
字典添加到InstanceState
类,这个对象是调用一个映射对象上的inspect()
而来。这允许自定义配方添加有关对象的其他信息,这些信息将随着对象在内存中的完整生命周期一起传递:
from sqlalchemy import inspect u1 = User(id=7, name="ed") inspect(u1).info["user_info"] = "7|ed"
#4257 ### 水平分片扩展支持批量更新和删除方法
ShardedQuery
扩展对象支持Query.update()
和Query.delete()
批量更新/删除方法。在调用它们时,将咨询query_chooser
可调用对象,以便根据给定的条件跨多个分片运行更新/删除。
关联代理改进
尽管没有任何特定的原因,但本周期的 Association Proxy 扩展有许多改进。
关联代理有新的 cascade_scalar_deletes 标志
给定一个映射为:
class A(Base): __tablename__ = "test_a" id = Column(Integer, primary_key=True) ab = relationship("AB", backref="a", uselist=False) b = association_proxy( "ab", "b", creator=lambda b: AB(b=b), cascade_scalar_deletes=True ) class B(Base): __tablename__ = "test_b" id = Column(Integer, primary_key=True) ab = relationship("AB", backref="b", cascade="all, delete-orphan") class AB(Base): __tablename__ = "test_ab" a_id = Column(Integer, ForeignKey(A.id), primary_key=True) b_id = Column(Integer, ForeignKey(B.id), primary_key=True)
对A.b
的赋值将生成一个AB
对象:
a.b = B()
A.b
关联是标量的,并包含一个新标志AssociationProxy.cascade_scalar_deletes
。设置时,将A.b
设置为None
将同时删除A.ab
。默认行为仍然是保留a.ab
不变:
a.b = None assert a.ab is None
虽然乍一看这个逻辑应该只看现有关系的“级联”属性,但仅从那个属性本身来看不清楚应该删除代理对象,因此行为被作为一个显式选项提供。
此外,del
现在对标量的工作方式类似于设置为None
:
del a.b assert a.ab is None
#4308 #### 关联代理在每个类上都存储特定于类的状态
AssociationProxy
对象根据其关联的父映射类做出许多决策。虽然 AssociationProxy
在历史上起初是一个相对简单的‘getter’,但很快就明显需要它也需要对其引用的属性类型做出决策——比如标量或集合、映射对象或简单值等等。为了实现这一点,它需要检查映射属性或其他引用的描述符或属性,如从其父类引用的那样。然而,在 Python 的描述符机制中,描述符仅在其在该类的上下文中被访问时才了解其“父”类,例如调用 MyClass.some_descriptor
,这会调用 __get__()
方法并传入类。AssociationProxy
对象因此会存储特定于该类的状态,但只有在调用此方法后才会这样做;尝试在首先将 AssociationProxy
作为描述符之前预先检查此状态将会引发错误。此外,它会假设由 __get__()
首先看到的第一个类是它需要了解的唯一父类。尽管事实上,如果特定类具有继承的子类,那么关联代理实际上是代表不止一个父类工作的,尽管没有明确地重新使用它。尽管即使存在这样的缺点,关联代理仍然会通过其当前行为取得相当大的进展,但在某些情况下仍存在缺陷以及确定最佳“所有者”类的复杂问题。
这些问题现在已经在AssociationProxy
中得到解决,当调用__get__()
时不再修改其自身的内部状态;相反,每个已知类别都会生成一个名为AssociationProxyInstance
的新对象,该对象处理特定映射父类的所有状态(当父类未映射时,不会生成AssociationProxyInstance
)。 单个“拥有类”的概念用于关联代理,尽管在 1.1 中有所改进,但基本上已被替换为现在的方法,其中 AP 现在可以平等地处理任意数量的“拥有”类。
为了适应希望检查此状态的应用程序而不必调用__get__()
的情况,添加了一个新方法AssociationProxy.for_class()
,它提供了直接访问特定类别AssociationProxyInstance
的功能,如下所示:
class User(Base): # ... keywords = association_proxy("kws", "keyword") proxy_state = inspect(User).all_orm_descriptors["keywords"].for_class(User)
一旦我们有了AssociationProxyInstance
对象,在上面的示例中存储在proxy_state
变量中,我们可以查看特定于User.keywords
代理的属性,例如target_class
:
>>> proxy_state.target_class Keyword
#3423 #### AssociationProxy 现在为基于列的目标提供了标准列操作符
鉴于AssociationProxy
的目标是数据库列,并且不是对象引用或另一个关联代理的情况:
class User(Base): # ... elements = relationship("Element") # column-based association proxy values = association_proxy("elements", "value") class Element(Base): # ... value = Column(String)
User.values
关联代理指的是Element.value
列。现在可用标准列操作,例如like
:
>>> print(s.query(User).filter(User.values.like("%foo%"))) SELECT "user".id AS user_id FROM "user" WHERE EXISTS (SELECT 1 FROM element WHERE "user".id = element.user_id AND element.value LIKE :value_1)
equals
:
>>> print(s.query(User).filter(User.values == "foo")) SELECT "user".id AS user_id FROM "user" WHERE EXISTS (SELECT 1 FROM element WHERE "user".id = element.user_id AND element.value = :value_1)
在与None
比较时,IS NULL
表达式增加了一个测试,即相关行根本不存在;这与以前的行为相同:
>>> print(s.query(User).filter(User.values == None)) SELECT "user".id AS user_id FROM "user" WHERE (EXISTS (SELECT 1 FROM element WHERE "user".id = element.user_id AND element.value IS NULL)) OR NOT (EXISTS (SELECT 1 FROM element WHERE "user".id = element.user_id))
注意ColumnOperators.contains()
操作符实际上是一个字符串比较操作符;这是一个行为上的改变,之前,关联代理只将.contains
作为列表包含操作符使用。现在,通过列比较,它现在的行为类似于“like”:
>>> print(s.query(User).filter(User.values.contains("foo"))) SELECT "user".id AS user_id FROM "user" WHERE EXISTS (SELECT 1 FROM element WHERE "user".id = element.user_id AND (element.value LIKE '%' || :value_1 || '%'))
为了测试User.values
集合是否包含值"foo"
,应该使用等于操作符(例如User.values == 'foo'
);这在之前的版本中也是有效的。
当使用基于对象的关联代理与集合时,行为与以前相同,即测试集合成员资格,例如给定一个映射:
class User(Base): __tablename__ = "user" id = Column(Integer, primary_key=True) user_elements = relationship("UserElement") # object-based association proxy elements = association_proxy("user_elements", "element") class UserElement(Base): __tablename__ = "user_element" id = Column(Integer, primary_key=True) user_id = Column(ForeignKey("user.id")) element_id = Column(ForeignKey("element.id")) element = relationship("Element") class Element(Base): __tablename__ = "element" id = Column(Integer, primary_key=True) value = Column(String)
.contains()
方法产生与以前相同的表达式,测试User.elements
列表中是否存在Element
对象:
>>> print(s.query(User).filter(User.elements.contains(Element(id=1)))) SELECT "user".id AS user_id FROM "user" WHERE EXISTS (SELECT 1 FROM user_element WHERE "user".id = user_element.user_id AND :param_1 = user_element.element_id)
总的来说,这个改变是基于 AssociationProxy stores class-specific state on a per-class basis 的架构变化而启用的;因为代理现在在生成表达式时会衍生出额外的状态,所以AssociationProxyInstance
类现在有对象目标和列目标版本。
关联代理现在强引用父对象
长期以来,关联代理集合仅保持对父对象的弱引用的行为被恢复;代理现在将保持对父对象的强引用,只要代理集合本身也在内存中,消除了“过时的关联代理”错误。这个改变是基于实验性基础进行的,以查看是否会出现任何导致副作用的用例。
举例来说,给定一个带有关联代理的映射:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) bs = relationship("B") b_data = association_proxy("bs", "data") class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id")) data = Column(String) a1 = A(bs=[B(data="b1"), B(data="b2")]) b_data = a1.b_data
之前,如果a1
超出范围被删除:
del a1
在a1
超出范围被删除后尝试迭代b_data
集合会引发错误“过时的关联代理,父对象已经超出范围”。这是因为关联代理需要访问实际的a1.bs
集合以产生视图,在这个改变之前,它只保持对a1
的弱引用。特别是,用户在执行内联操作时经常会遇到这个错误,比如:
collection = session.query(A).filter_by(id=1).first().b_data
上面的例子中,因为A
对象在实际使用b_data
集合之前已被垃圾回收。
改变是b_data
集合现在保持对a1
对象的强引用,以便它保持存在:
assert b_data == ["b1", "b2"]
此更改引入的副作用是,如果一个应用程序像上面一样传递集合,父对象在集合被丢弃之前不会被垃圾回收。正如以往一样,如果 a1
在特定的 Session
内是持久化的,它将保持为该会话的状态直到被垃圾回收。
请注意,如果这种变化导致问题,可能会对此进行修订。
为集合,具有 AssociationProxy 的字典实现批量替换
现在,将集合代理分配给集合代理集合的集合应该正常工作,而以前会为现有键重新创建集合代理成员,导致潜在的刷新失败问题,因为删除+插入相同对象,现在应该只在适当的情况下创建新的关联对象:
class A(Base): __tablename__ = "test_a" id = Column(Integer, primary_key=True) b_rel = relationship( "B", collection_class=set, cascade="all, delete-orphan", ) b = association_proxy("b_rel", "value", creator=lambda x: B(value=x)) class B(Base): __tablename__ = "test_b" __table_args__ = (UniqueConstraint("a_id", "value"),) id = Column(Integer, primary_key=True) a_id = Column(Integer, ForeignKey("test_a.id"), nullable=False) value = Column(String) # ... s = Session(e) a = A(b={"x", "y", "z"}) s.add(a) s.commit() # re-assign where one B should be deleted, one B added, two # B's maintained a.b = {"x", "z", "q"} # only 'q' was added, so only one new B object. previously # all three would have been re-created leading to flush conflicts # against the deleted ones. assert len(s.new) == 1
#2642 ### 多对一反向引用在删除操作期间检查集合重复项
当一个 ORM 映射的集合存在作为 Python 序列时,通常是 Python list
,作为 relationship()
的默认值,包含重复项,并且对象从其中一个位置被移除但其他位置没有移除时,一个多对一的反向引用会将其属性设置为 None
,即使一对多的一侧仍然表示对象存在。即使一对多的集合在关系模型中不能有重复项,在内存中使用序列集合的 ORM 映射的 relationship()
可以包含其中的重复项,但限制是这种重复状态既不能持久化也不能从数据库中检索。特别是,在列表中临时存在重复项是 Python “swap” 操作的固有特性。考虑到标准的一对多/多对一设置:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) bs = relationship("B", backref="a") class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id"))
如果我们有一个带有两个 B
成员的 A
对象,并执行交换:
a1 = A(bs=[B(), B()]) a1.bs[0], a1.bs[1] = a1.bs[1], a1.bs[0]
在上述操作期间,拦截标准的 Python __setitem__
__delitem__
方法提供了一个临时状态,其中第二个 B()
对象在集合中出现两次。当从一个位置移除 B()
对象时,B.a
反向引用将引用设置为 None
,导致刷新期间删除 A
和 B
对象之间的链接。同样的问题也可以使用普通重复项演示:
>>> a1 = A() >>> b1 = B() >>> a1.bs.append(b1) >>> a1.bs.append(b1) # append the same b1 object twice >>> del a1.bs[1] >>> a1.bs # collection is unaffected so far... [<__main__.B object at 0x7f047af5fb70>] >>> b1.a # however b1.a is None >>> >>> session.add(a1) >>> session.commit() # so upon flush + expire.... >>> a1.bs # the value is gone []
修复确保在触发反向引用之前,即在修改集合之前,对集合进行检查,确保目标项的实例数量为零或一,然后取消多对一的一侧,使用线性搜索,目前使用 list.search
和 list.__contains__
。
最初认为在集合内部需要使用基于事件的引用计数方案,以便在整个集合的生命周期中跟踪所有重复实例,这将对所有集合操作产生性能/内存/复杂性影响,包括加载和追加这些非常频繁的操作。相反采取的方法将额外的开销限制在较少常见的集合移除和批量替换操作上,线性扫描的观察开销是可以忽略的;在工作单元中已经使用了与关系绑定集合的线性扫描,以及在集合进行批量替换时。
#1103 ### 与 AliasedClass 的关系取代了非主映射器的需求
“非主映射器”是以 Imperative Mapping 风格创建的Mapper
,它充当已经映射类的另一种可选择的附加映射器。非主映射器起源于 SQLAlchemy 的 0.1、0.2 系列,当时预期Mapper
对象将是主要的查询构造接口,而Query
对象还不存在。
随着Query
的出现,以及后来的AliasedClass
构造,大多数非主映射器的用例都消失了。这是一件好事,因为 SQLAlchemy 在 0.5 系列左右也完全摆脱了“经典”映射,转而采用了声明式系统。
当意识到一些非常难以定义的relationship()
配置可能成为可能时,仍然存在一个非主映射器的用例,当一个具有替代可选择项的非主映射器被作为映射目标时,而不是尝试构建一个包含特定对象间关系所有复杂性的relationship.primaryjoin
。
随着这种使用情况越来越普遍,它的局限性变得明显,包括非主映射器难以配置到可选的添加新列的地方,映射器不继承原始映射的关系,非主映射器上明确配置的关系在加载器选项中表现不佳,非主映射器也不提供可以在查询中使用的基于列的属性的完整功能命名空间(在旧的 0.1 至 0.4 版本中,人们可以直接使用Table
对象与 ORM)。
缺失的部分是允许relationship()
直接引用AliasedClass
。 AliasedClass
已经实现了我们希望非主映射器实现的所有功能;它允许从替代选择加载现有映射类,它继承了现有映射器的所有属性和关系,它与加载器选项非常配合,它提供了一个可以像类本身一样混入查询的类似类对象。通过这种改变,以前针对非主映射器的配方在配置关联加入处被改为别名类。
在关联到别名类,原始的非主映射器如下所示:
j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id) B_viacd = mapper( B, j, non_primary=True, primary_key=[j.c.b_id], properties={ "id": j.c.b_id, # so that 'id' looks the same as before "c_id": j.c.c_id, # needed for disambiguation "d_c_id": j.c.d_c_id, # needed for disambiguation "b_id": [j.c.b_id, j.c.d_b_id], "d_id": j.c.d_id, }, ) A.b = relationship(B_viacd, primaryjoin=A.b_id == B_viacd.c.b_id)
这些属性是必需的,以便重新映射附加列,以便它们不与映射到B
的现有列发生冲突,同时还需要定义一个新的主键。
采用新方法,所有这些冗长都消失了,并且在建立关系时直接引用附加列:
j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id) B_viacd = aliased(B, j, flat=True) A.b = relationship(B_viacd, primaryjoin=A.b_id == j.c.b_id)
非主映射器现在已经被弃用,最终目标是将经典映射作为一种功能完全取消。声明性 API 将成为映射的唯一手段,这希望能够实现内部改进和简化,以及更清晰的文档编写。
selectin 加载不再对简单的一对多使用 JOIN
在 1.2 中添加的“selectin”加载功能引入了一种极其高效的新方法来急切加载集合,在许多情况下比“subquery”急切加载要快得多,因为它不依赖于重新声明原始 SELECT 查询,而是使用一个简单的 IN 子句。然而,“selectin”加载仍然依赖于在父表和相关表之间渲染 JOIN,因为它需要行中的父主键值以匹配行。在 1.3 中,添加了一种新的优化,将在简单的一对多加载的最常见情况下省略此 JOIN,其中相关行已经包含了父行的主键值,表达为其外键列。这再次提供了显著的性能改进,因为 ORM 现在可以一次性加载大量集合,而无需使用 JOIN 或子查询。
给定一个映射:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) bs = relationship("B", lazy="selectin") class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id"))
在“selectin”加载的 1.2 版本中,从 A 到 B 的加载看起来像:
SELECT a.id AS a_id FROM a SELECT a_1.id AS a_1_id, b.id AS b_id, b.a_id AS b_a_id FROM a AS a_1 JOIN b ON a_1.id = b.a_id WHERE a_1.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY a_1.id (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
使用新行为后,负载看起来像:
SELECT a.id AS a_id FROM a SELECT b.a_id AS b_a_id, b.id AS b_id FROM b WHERE b.a_id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY b.a_id (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
该行为被释放为自动的,使用了类似于延迟加载的启发式方法,以确定是否可以直接从标识映射中获取相关实体。然而,与大多数查询功能一样,由于涉及多态加载的高级场景,该功能的实现变得更加复杂。如果遇到问题,用户应该报告错误,但是该更改还包括一个标志relationship.omit_join
,可以在relationship()
上设置为False
,以禁用优化。
SqlAlchemy 2.0 中文文档(七十三)(4)https://developer.aliyun.com/article/1562219