SqlAlchemy 2.0 中文文档(五十七)(8)https://developer.aliyun.com/article/1563170
替换具有相同名称和键的 Table 对象中的 Columns 的更严格规则
对于将 Column
对象附加到 Table
对象,有更严格的规则,将一些先前的弃用警告转移到异常中,并防止一些先前可能导致表中出现重复列的情况,当 Table.extend_existing
设置为 True
时,对于编程方式的 Table
构建以及在反射操作期间。
- 无论如何,
Table
对象都不应该具有两个或更多具有相同名称的Column
对象,无论它们的 .key 如何。已经确定并修复了仍然可能发生此情况的边缘情况。 - 向具有与现有
Column
相同名称或键的Table
添加Column
将始终引发DuplicateColumnError
(在 2.0.0b4 中是ArgumentError
的新子类),除非存在额外参数;对于Table.append_column()
,使用Table.append_column.replace_existing
,以及对于构建具有与现有Table
相同名称的Table
(使用或不使用反射)时使用Table.extend_existing
。此前,针对此情况已经放置了弃用警告。 - 如果创建了一个包含
Table.extend_existing
的Table
,并且存在一个没有单独的Column.key
的传入Column
,该传入的Column
将完全替换具有键的现有Column
,这表明操作不是用户所期望的。这在特别是在次要反射步骤期间可能发生,例如metadata.reflect(extend_existing=True)
。警告建议将Table.autoload_replace
参数设置为False
以防止这种情况发生。在 1.4 及以前的版本中,传入的列会额外添加到现有列中。这是一个错误,在 2.0(截至 2.0.0b4)中是一种行为变更,因为在这种情况发生时,以前的键将不再存在于列集合中。
ORM 声明式不同的列顺序应用方式;使用sort_order
控制行为
声明式已更改了来自混合或抽象基类的映射列与声明类本身上的列一起排序的系统,以便首先放置来自声明类的列,然后是混合列。以下映射:
class Foo: col1 = mapped_column(Integer) col3 = mapped_column(Integer) class Bar: col2 = mapped_column(Integer) col4 = mapped_column(Integer) class Model(Base, Foo, Bar): id = mapped_column(Integer, primary_key=True) __tablename__ = "model"
在 1.4 上产生的 CREATE TABLE 如下:
CREATE TABLE model ( col1 INTEGER, col3 INTEGER, col2 INTEGER, col4 INTEGER, id INTEGER NOT NULL, PRIMARY KEY (id) )
而在 2.0 上它产生:
CREATE TABLE model ( id INTEGER NOT NULL, col1 INTEGER, col3 INTEGER, col2 INTEGER, col4 INTEGER, PRIMARY KEY (id) )
对于上述特定情况,这可以视为一种改进,因为Model
上的主键列现在位于人们通常更喜欢的位置。然而,对于以相反方式定义模型的应用程序来说,这并没有什么安慰,因为:
class Foo: id = mapped_column(Integer, primary_key=True) col1 = mapped_column(Integer) col3 = mapped_column(Integer) class Model(Foo, Base): col2 = mapped_column(Integer) col4 = mapped_column(Integer) __tablename__ = "model"
现在,这将产生以下 CREATE TABLE 输出:
CREATE TABLE model ( col2 INTEGER, col4 INTEGER, id INTEGER NOT NULL, col1 INTEGER, col3 INTEGER, PRIMARY KEY (id) )
为解决此问题,SQLAlchemy 2.0.4 引入了mapped_column()
上的一个新参数,称为mapped_column.sort_order
,它是一个整数值,默认为0
,可以设置为正值或负值,以便将列放置在其他列之前或之后,如下例所示:
class Foo: id = mapped_column(Integer, primary_key=True, sort_order=-10) col1 = mapped_column(Integer, sort_order=-1) col3 = mapped_column(Integer) class Model(Foo, Base): col2 = mapped_column(Integer) col4 = mapped_column(Integer) __tablename__ = "model"
上述模型将“id”放置在所有其他列之前,将“col1”放置在“id”之后:
CREATE TABLE model ( id INTEGER NOT NULL, col1 INTEGER, col2 INTEGER, col4 INTEGER, col3 INTEGER, PRIMARY KEY (id) )
未来的 SQLAlchemy 版本可能选择为mapped_column
构造提供显式排序提示,因为此排序是 ORM 特定的。
Sequence
构造不再具有任何显式默认的“start”值;影响 MS SQL Server
在 SQLAlchemy 1.4 之前,Sequence
构造将仅在未指定其他参数时发出简单的 CREATE SEQUENCE
DDL:
>>> # SQLAlchemy 1.3 (and 2.0) >>> from sqlalchemy import Sequence >>> from sqlalchemy.schema import CreateSequence >>> print(CreateSequence(Sequence("my_seq"))) CREATE SEQUENCE my_seq
但是,由于为 MS SQL Server 添加了 Sequence
支持,其中默认起始值不方便地设置为 -2**63
,版本 1.4 决定将 DDL 默认为发出起始值为 1,如果未提供 Sequence.start
:
>>> # SQLAlchemy 1.4 (only) >>> from sqlalchemy import Sequence >>> from sqlalchemy.schema import CreateSequence >>> print(CreateSequence(Sequence("my_seq"))) CREATE SEQUENCE my_seq START WITH 1
此更改引入了其他复杂性,包括当包含 Sequence.min_value
参数时,默认值为 1
实际上应默认为 Sequence.min_value
所述的内容,否则,将看到低于 start_value 的 min_value 可能被视为矛盾。由于研究此问题开始变得有点复杂,包括各种其他边缘情况,我们决定撤消此更改,并恢复 Sequence
的原始行为,即不表达任何观点,只是发出 CREATE SEQUENCE,允许数据库本身决定如何处理 SEQUENCE
的各种参数之间的交互。
因此,为了确保所有后端的起始值都为 1,可能需要显式指定起始值为 1,如下所示:
>>> # All SQLAlchemy versions >>> from sqlalchemy import Sequence >>> from sqlalchemy.schema import CreateSequence >>> print(CreateSequence(Sequence("my_seq", start=1))) CREATE SEQUENCE my_seq START WITH 1
此外,在现代后端(包括 PostgreSQL、Oracle、SQL Server)上自动生成整数主键时,应优先使用 Identity
构造,在 1.4 和 2.0 中也以相同方式工作,行为没有任何更改。
“with_variant()” 克隆原始 TypeEngine 而不是更改类型
TypeEngine.with_variant()
方法用于将特定类型应用于数据库的备用行为,现在返回原始 TypeEngine
对象的副本,并在内部存储变体信息,而不是将其包装在 Variant
类中。
虽然以前的 Variant
方法能够使用动态属性获取器维护原始类型的所有 Python 行为,但这里的改进是,当调用变体时,返回的类型仍然是原始类型的实例,这与诸如 mypy 和 pylance 的类型检查器更加顺畅地配合。给定以下程序:
import typing from sqlalchemy import String from sqlalchemy.dialects.mysql import VARCHAR type_ = String(255).with_variant(VARCHAR(255, charset="utf8mb4"), "mysql", "mariadb") if typing.TYPE_CHECKING: reveal_type(type_)
类型检查器如 pyright 现在将报告类型为:
info: Type of "type_" is "String"
此外,如上所示,对于单个类型,可以传递多个方言名称,特别是对于被视为分开的"mysql"
和"mariadb"
方言的成对,这对于 SQLAlchemy 1.4 很有帮助。
Python 除法运算符对所有后端执行真除法;添加地板除法
核心表达式语言现在支持“真除法”(即 /
Python 运算符)和“地板除法”(即 //
Python 运算符),包括标准化此方面不同数据库的后端特定行为。
给定两个整数值进行“真除法”操作:
expr = literal(5, Integer) / literal(10, Integer)
PostgreSQL 上的 SQL 除法运算符通常在对整数进行操作时作为“地板除法”(floor division)操作,意味着以上结果将返回整数“0”。对于此类后端,SQLAlchemy 现在使用等效于以下形式的 SQL 来渲染 SQL:
%(param_1)s / CAST(%(param_2)s AS NUMERIC)
使用 param_1=5
,param_2=10
,以便返回表达式将是 NUMERIC 类型,通常为 Python 值 decimal.Decimal("0.5")
。
给定两个整数值进行“地板除法”操作:
expr = literal(5, Integer) // literal(10, Integer)
MySQL 和 Oracle 上的 SQL 除法运算符通常在对整数进行操作时作为“真除法”(true division)操作,意味着以上结果将返回浮点值“0.5”。对于这些和类似的后端,SQLAlchemy 现在使用等效于以下形式的 SQL 渲染 SQL:
FLOOR(%(param_1)s / %(param_2)s)
使用 param_1=5,param_2=10,以便返回表达式将是整数类型,就像 Python 值 0
一样。
此处的不兼容变更将是,如果一个应用程序使用 PostgreSQL、SQL Server 或 SQLite,并且依赖于 Python 的“truediv”运算符在所有情况下返回整数值。依赖于此行为的应用程序应该使用 Python 的“地板除法”运算符 //
进行这些操作,或者在使用之前的 SQLAlchemy 版本时进行前向兼容,使用 floor 函数:
expr = func.floor(literal(5, Integer) / literal(10, Integer))
在 SQLAlchemy 版本 2.0 之前的任何版本中,将需要上述形式以提供与后端无关的地板除法。
Session 主动提出当检测到非法并发或重入访问时
Session
现在可以更全面地捕获与多线程或其他并发场景中的非法并发状态更改相关的错误,以及执行意外状态更改的事件钩子。
已知的一个错误是当一个Session
同时在多个线程中使用时会出现AttributeError: 'NoneType' object has no attribute 'twophase'
,这完全是神秘的。当一个线程调用Session.commit()
时,内部调用SessionTransaction.close()
方法来结束事务上下文,与此同时另一个线程正在运行一个查询,如Session.execute()
。在Session.execute()
中,获取当前事务的数据库连接的内部方法首先开始断言会话是“活动的”,但在此断言通过后,同时进行的对Session.close()
的调用干扰了这种状态,导致上述未定义的条件。
更改将应用到围绕SessionTransaction
对象的所有更改状态方法的保护措施,以便在上述情况下,Session.commit()
方法将会失败,因为它将试图将状态更改为在已经进行中的方法期间不允许的状态,而该方法希望获取当前连接以运行数据库查询。
使用在#7433中展示的测试脚本,先前的错误案例如下:
Traceback (most recent call last): File "/home/classic/dev/sqlalchemy/test3.py", line 30, in worker sess.execute(select(A)).all() File "/home/classic/tmp/sqlalchemy/lib/sqlalchemy/orm/session.py", line 1691, in execute conn = self._connection_for_bind(bind) File "/home/classic/tmp/sqlalchemy/lib/sqlalchemy/orm/session.py", line 1532, in _connection_for_bind return self._transaction._connection_for_bind( File "/home/classic/tmp/sqlalchemy/lib/sqlalchemy/orm/session.py", line 754, in _connection_for_bind if self.session.twophase and self._parent is None: AttributeError: 'NoneType' object has no attribute 'twophase'
当_connection_for_bind()
方法无法继续运行时,因为并发访问使其处于无效状态。使用新方法,状态更改的发起者会抛出错误:
File "/home/classic/dev/sqlalchemy/lib/sqlalchemy/orm/session.py", line 1785, in close self._close_impl(invalidate=False) File "/home/classic/dev/sqlalchemy/lib/sqlalchemy/orm/session.py", line 1827, in _close_impl transaction.close(invalidate) File "<string>", line 2, in close File "/home/classic/dev/sqlalchemy/lib/sqlalchemy/orm/session.py", line 506, in _go raise sa_exc.InvalidRequestError( sqlalchemy.exc.InvalidRequestError: Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and this would cause an unexpected state change to symbol('CLOSED')
状态转换检查故意不使用显式锁来检测并发线程活动,而是依赖于简单的属性设置/值测试操作,当发生意外的并发更改时会自然失败。其理念是该方法可以检测到完全发生在单个线程内的非法状态更改,例如运行在会话事务事件上的事件处理程序调用了一个未预期的改变状态的方法,或者在 asyncio 中,如果一个特定的Session
被多个 asyncio 任务共享,以及在使用类似 gevent 的补丁式并发方法时。
SQLite 方言使用 QueuePool 用于基于文件的数据库
当使用基于文件的数据库时,SQLite 方言现在默认为 QueuePool
。这与将 check_same_thread
参数设置为 False
一起设置。已观察到以前默认为 NullPool
的方法,在释放连接后不保留数据库连接,事实上会产生可测量的负面性能影响。与以往一样,池类可通过 create_engine.poolclass
参数进行自定义。
另请参阅
线程/池行为
新的 Oracle FLOAT 类型,带有二进制精度;不直接接受十进制精度
Oracle 方言现已添加了新的数据类型 FLOAT
,以配合 Double
和数据库特定的 DOUBLE
、DOUBLE_PRECISION
和 REAL
数据类型的添加。Oracle 的 FLOAT
接受所谓的“二进制精度”参数,根据 Oracle 文档,这大致是标准“精度”值除以 0.3103:
from sqlalchemy.dialects import oracle Table("some_table", metadata, Column("value", oracle.FLOAT(126)))
二进制精度值 126 等同于使用 DOUBLE_PRECISION
数据类型,而值 63 相当于使用 REAL
数据类型。其他精度值是特定于 FLOAT
类型本身的。
SQLAlchemy Float
数据类型也接受“precision”参数,但这是十进制精度,Oracle 不接受。与其试图猜测转换,Oracle 方言现在将在针对 Oracle 后端使用带有精度值的 Float
时引发一个信息性错误。要为支持的后端指定带有显式精度值的 Float
数据类型,同时还支持其他后端,请使用以下方法:TypeEngine.with_variant()
。
from sqlalchemy.types import Float from sqlalchemy.dialects import oracle Table( "some_table", metadata, Column("value", Float(5).with_variant(oracle.FLOAT(16), "oracle")), )
PostgreSQL 后端的新 RANGE / MULTIRANGE 支持和更改
对于 psycopg2、psycopg3 和 asyncpg 方言,已完全实现了 RANGE / MULTIRANGE 支持。新的支持使用了一个新的 SQLAlchemy 特定的 Range
对象,该对象对不同的后端是不可知的,不需要使用特定于后端的导入或扩展步骤。对于多范围支持,使用 Range
对象的列表。
使用之前的 psycopg2 特定类型的代码应该修改为使用 Range
,这提供了一个兼容的接口。
Range
对象还具有与 PostgreSQL 相同的比较支持。目前已实现的是 Range.contains()
和 Range.contained_by()
方法,其工作方式与 PostgreSQL 中的 @>
和 <@
相同。将来的版本可能会添加更多的运算符支持。
请查看范围和多范围类型的文档,了解如何使用这个新功能的背景知识。
另请参阅
范围和多范围类型
PostgreSQL 上的 match()
运算符使用 plainto_tsquery()
而不是 to_tsquery()
Operators.match()
函数现在在 PostgreSQL 后端上呈现为 col @@ plainto_tsquery(expr)
,而不是 col @@ to_tsquery()
。plainto_tsquery()
接受纯文本,而 to_tsquery()
接受专用的查询符号,因此与其他后端的兼容性较差。
所有 PostgreSQL 搜索函数和运算符都可以通过使用 func
来生成 PostgreSQL 特定的函数和 Operators.bool_op()
(Operators.op()
的布尔类型版本)来生成任意运算符,方式与之前的版本中可用的方式相同。请参阅全文搜索中的示例。
现有的使用Operators.match()
内置 PG 指令的 SQLAlchemy 项目应直接使用func.to_tsquery()
。要以与 1.4 中相同的形式呈现 SQL,请参阅使用 match() 进行简单纯文本匹配的版本说明。
eger))
在 SQLAlchemy 版本 2.0 之前的任何版本中,将需要上述形式以提供与后端无关的地板除法。 [#4926](https://www.sqlalchemy.org/trac/ticket/4926) ### Session 主动提出当检测到非法并发或重入访问时 `Session` 现在可以更全面地捕获与多线程或其他并发场景中的非法并发状态更改相关的错误,以及执行意外状态更改的事件钩子。 已知的一个错误是当一个`Session`同时在多个线程中使用时会出现`AttributeError: 'NoneType' object has no attribute 'twophase'`,这完全是神秘的。当一个线程调用`Session.commit()`时,内部调用`SessionTransaction.close()`方法来结束事务上下文,与此同时另一个线程正在运行一个查询,如`Session.execute()`。在`Session.execute()`中,获取当前事务的数据库连接的内部方法首先开始断言会话是“活动的”,但在此断言通过后,同时进行的对`Session.close()`的调用干扰了这种状态,导致上述未定义的条件。 更改将应用到围绕`SessionTransaction`对象的所有更改状态方法的保护措施,以便在上述情况下,`Session.commit()`方法将会失败,因为它将试图将状态更改为在已经进行中的方法期间不允许的状态,而该方法希望获取当前连接以运行数据库查询。 使用在[#7433](https://www.sqlalchemy.org/trac/ticket/7433)中展示的测试脚本,先前的错误案例如下: ```py Traceback (most recent call last): File "/home/classic/dev/sqlalchemy/test3.py", line 30, in worker sess.execute(select(A)).all() File "/home/classic/tmp/sqlalchemy/lib/sqlalchemy/orm/session.py", line 1691, in execute conn = self._connection_for_bind(bind) File "/home/classic/tmp/sqlalchemy/lib/sqlalchemy/orm/session.py", line 1532, in _connection_for_bind return self._transaction._connection_for_bind( File "/home/classic/tmp/sqlalchemy/lib/sqlalchemy/orm/session.py", line 754, in _connection_for_bind if self.session.twophase and self._parent is None: AttributeError: 'NoneType' object has no attribute 'twophase'
当_connection_for_bind()
方法无法继续运行时,因为并发访问使其处于无效状态。使用新方法,状态更改的发起者会抛出错误:
File "/home/classic/dev/sqlalchemy/lib/sqlalchemy/orm/session.py", line 1785, in close self._close_impl(invalidate=False) File "/home/classic/dev/sqlalchemy/lib/sqlalchemy/orm/session.py", line 1827, in _close_impl transaction.close(invalidate) File "<string>", line 2, in close File "/home/classic/dev/sqlalchemy/lib/sqlalchemy/orm/session.py", line 506, in _go raise sa_exc.InvalidRequestError( sqlalchemy.exc.InvalidRequestError: Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and this would cause an unexpected state change to symbol('CLOSED')
状态转换检查故意不使用显式锁来检测并发线程活动,而是依赖于简单的属性设置/值测试操作,当发生意外的并发更改时会自然失败。其理念是该方法可以检测到完全发生在单个线程内的非法状态更改,例如运行在会话事务事件上的事件处理程序调用了一个未预期的改变状态的方法,或者在 asyncio 中,如果一个特定的Session
被多个 asyncio 任务共享,以及在使用类似 gevent 的补丁式并发方法时。
SQLite 方言使用 QueuePool 用于基于文件的数据库
当使用基于文件的数据库时,SQLite 方言现在默认为 QueuePool
。这与将 check_same_thread
参数设置为 False
一起设置。已观察到以前默认为 NullPool
的方法,在释放连接后不保留数据库连接,事实上会产生可测量的负面性能影响。与以往一样,池类可通过 create_engine.poolclass
参数进行自定义。
另请参阅
线程/池行为
新的 Oracle FLOAT 类型,带有二进制精度;不直接接受十进制精度
Oracle 方言现已添加了新的数据类型 FLOAT
,以配合 Double
和数据库特定的 DOUBLE
、DOUBLE_PRECISION
和 REAL
数据类型的添加。Oracle 的 FLOAT
接受所谓的“二进制精度”参数,根据 Oracle 文档,这大致是标准“精度”值除以 0.3103:
from sqlalchemy.dialects import oracle Table("some_table", metadata, Column("value", oracle.FLOAT(126)))
二进制精度值 126 等同于使用 DOUBLE_PRECISION
数据类型,而值 63 相当于使用 REAL
数据类型。其他精度值是特定于 FLOAT
类型本身的。
SQLAlchemy Float
数据类型也接受“precision”参数,但这是十进制精度,Oracle 不接受。与其试图猜测转换,Oracle 方言现在将在针对 Oracle 后端使用带有精度值的 Float
时引发一个信息性错误。要为支持的后端指定带有显式精度值的 Float
数据类型,同时还支持其他后端,请使用以下方法:TypeEngine.with_variant()
。
from sqlalchemy.types import Float from sqlalchemy.dialects import oracle Table( "some_table", metadata, Column("value", Float(5).with_variant(oracle.FLOAT(16), "oracle")), )
PostgreSQL 后端的新 RANGE / MULTIRANGE 支持和更改
对于 psycopg2、psycopg3 和 asyncpg 方言,已完全实现了 RANGE / MULTIRANGE 支持。新的支持使用了一个新的 SQLAlchemy 特定的 Range
对象,该对象对不同的后端是不可知的,不需要使用特定于后端的导入或扩展步骤。对于多范围支持,使用 Range
对象的列表。
使用之前的 psycopg2 特定类型的代码应该修改为使用 Range
,这提供了一个兼容的接口。
Range
对象还具有与 PostgreSQL 相同的比较支持。目前已实现的是 Range.contains()
和 Range.contained_by()
方法,其工作方式与 PostgreSQL 中的 @>
和 <@
相同。将来的版本可能会添加更多的运算符支持。
请查看范围和多范围类型的文档,了解如何使用这个新功能的背景知识。
另请参阅
范围和多范围类型
PostgreSQL 上的 match()
运算符使用 plainto_tsquery()
而不是 to_tsquery()
Operators.match()
函数现在在 PostgreSQL 后端上呈现为 col @@ plainto_tsquery(expr)
,而不是 col @@ to_tsquery()
。plainto_tsquery()
接受纯文本,而 to_tsquery()
接受专用的查询符号,因此与其他后端的兼容性较差。
所有 PostgreSQL 搜索函数和运算符都可以通过使用 func
来生成 PostgreSQL 特定的函数和 Operators.bool_op()
(Operators.op()
的布尔类型版本)来生成任意运算符,方式与之前的版本中可用的方式相同。请参阅全文搜索中的示例。
现有的使用Operators.match()
内置 PG 指令的 SQLAlchemy 项目应直接使用func.to_tsquery()
。要以与 1.4 中相同的形式呈现 SQL,请参阅使用 match() 进行简单纯文本匹配的版本说明。