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

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

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


安装现在完全支持 PEP-517

源代码分发现在包含一个pyproject.toml文件,以允许完全支持PEP 517。特别是,这允许使用pip进行本地源构建时自动安装可选依赖Cython

#7311

C 扩展现在转移到了 Cython

SQLAlchemy 的 C 扩展已被全部用Cython编写的新扩展替换。虽然 Cython 在 2010 年评估过,当时创建了 C 扩展,但今天使用的 C 扩展的性质和重点与当时相比已经发生了很大变化。同时,Cython 显然已经有了很大发展,Python 构建/分发工具链也使我们重新审视它成为可能。

迁移到 Cython 提供了明显的新优势,而没有明显的缺点:

  • 用 Cython 替换特定 C 扩展的 Cython 扩展都经过了基准测试,通常比 SQLAlchemy 以前包含的几乎所有 C 代码都更快,有时显着快。虽然这看起来很神奇,但似乎是 Cython 实现中的一些非明显优化的结果,这些优化在直接将函数从 Python 转换为 C 时不会存在,特别是对于添加到 C 扩展的许多自定义集合类型的情况。
  • 与原始 C 代码相比,Cython 扩展更容易编写、维护和调试,在大多数情况下与 Python 代码是逐行等效的。预计在即将发布的版本中,SQLAlchemy 的许多元素都将被转移到 Cython 中,这将打开许多以前无法实现的性能改进的新门路。
  • Cython 非常成熟且被广泛使用,包括成为 SQLAlchemy 支持的一些显著数据库驱动程序的基础,包括asyncpgpsycopg3asyncmy

像以前的 C 扩展一样,Cython 扩展被预先构建在 SQLAlchemy 的 wheel 分发中,这些分发可以自动从 PyPi 中的pip获得。手动构建说明也没有变化,除了 Cython 要求。

另请参阅

构建 Cython 扩展

#7256

数据库反射的重大架构、性能和 API 增强

Table 对象及其组件被反射的内部系统已经被完全重新架构,以允许参与方言一次性高性能地大量反射数千个表。目前,PostgreSQLOracle 方言参与了新的架构,其中 PostgreSQL 方言现在可以将大量 Table 对象反射得快近三倍,而 Oracle 方言现在可以将大量 Table 对象反射得快十倍。

重新架构最直接适用于利用 SELECT 查询系统目录表以反射表的方言,并且剩下的包括可以受益于这种方法的方言将是 SQL Server  方言。相比之下,MySQL/MariaDB 和 SQLite 方言利用非关系系统反射数据库表,并且没有受到现有性能问题的影响。

新的 API 与之前的系统向后兼容,并且不需要对第三方方言进行任何更改以保持兼容性;第三方方言也可以通过实现批量查询来选择加入新系统以进行模式反射。

除了这一变化,Inspector 对象的 API 和行为已经改进和增强,具有更一致的跨方言行为以及新的方法和性能特性。

性能概览

源分发包括一个脚本test/perf/many_table_reflection.py,它对现有的反射功能和新功能进行基准测试。其中一部分测试可以在较旧版本的 SQLAlchemy 上运行,在这里我们使用它来说明性能差异,调用metadata.reflect()一次性反射 250 个 Table 对象在本地网络连接上:

方言 操作 SQLA 1.4 时间(秒) SQLA 2.0 时间(秒)
postgresql+psycopg2 metadata.reflect(),250 个表 8.2 3.3
oracle+cx_oracle metadata.reflect(),250 个表 60.4 6.8

Inspector() 的行为变化

对于包含在 SQLite、PostgreSQL、MySQL/MariaDB、Oracle 和 SQL Server 中的 SQLAlchemy 方言,Inspector.has_table()Inspector.has_sequence()Inspector.has_index()Inspector.get_table_names()Inspector.get_sequence_names()现在在缓存方面都表现一致:在第一次为特定的Inspector对象调用后,它们都完全缓存其结果。当调用相同的Inspector对象创建或删除表/序列时,程序将不会在数据库状态发生更改后收到更新的状态。当要执行 DDL 更改时,应使用调用Inspector.clear_cache()或新的Inspector。之前,Inspector.has_table()Inspector.has_sequence()方法没有实现缓存,也没有Inspector支持这些方法的缓存,而Inspector.get_table_names()Inspector.get_sequence_names()方法是,导致两种类型的方法之间的结果不一致。

对于第三方方言的行为取决于它们是否实现了“反射缓存”装饰器来实现这些方法的方言级实现。

新的方法和改进Inspector()的行为

  • 添加了一个方法Inspector.has_schema(),用于返回目标数据库中是否存在模式。
  • 添加了一个方法Inspector.has_index(),用于返回表是否具有特定索引。
  • 诸如Inspector.get_columns()之类的检查方法现在在一次只处理一个表时,如果未找到表或视图,将一致引发NoSuchTableError;此更改特定于各个方言,因此对于现有的第三方方言可能不适用。
  • 将“视图”和“物化视图”的处理分开,因为在实际用例中,这两个构造使用不同的 DDL 来进行 CREATE 和 DROP;现在有单独的Inspector.get_view_names()Inspector.get_materialized_view_names()方法。

#4379

性能概述

源代码分发包括一个脚本test/perf/many_table_reflection.py,该脚本对现有的反射功能以及新功能进行基准测试。其一部分测试可以在较旧版本的 SQLAlchemy 上运行,我们在这里使用它来说明在本地网络连接上一次性反射 250 个Table对象时调用metadata.reflect()的性能差异:

方言 操作 SQLA 1.4 时间(秒) SQLA 2.0 时间(秒)
postgresql+psycopg2 metadata.reflect(), 250 tables 8.2 3.3
oracle+cx_oracle metadata.reflect(), 250 tables 60.4 6.8

Inspector()的行为变化

对于包含在 SQLite、PostgreSQL、MySQL/MariaDB、Oracle 和 SQL Server 中的 SQLAlchemy 内置方言,Inspector.has_table()Inspector.has_sequence()Inspector.has_index()Inspector.get_table_names()Inspector.get_sequence_names()现在在缓存方面行为一致:它们在第一次为特定Inspector对象调用后完全缓存其结果。在调用相同的Inspector对象时创建或删除表/序列的程序在数据库状态发生变化后将不会接收到更新的状态。当要执行 DDL 更改时应使用Inspector.clear_cache()或一个新的Inspector。先前,Inspector.has_table()Inspector.has_sequence()方法未实现缓存,Inspector也不支持这些方法的缓存,而Inspector.get_table_names()Inspector.get_sequence_names()方法则是,导致两种方法之间结果不一致。

第三方方言的行为取决于它们是否实现了这些方法的方言级实现的“反射缓存”装饰器。

新的方法和改进对于Inspector()而言

  • 添加了一个方法Inspector.has_schema(),用于返回目标数据库中是否存在模式
  • 添加了一个方法Inspector.has_index(),用于返回表是否具有特定索引。
  • 诸如Inspector.get_columns()之类的检查方法现在在一次只处理一个表时应一致地引发NoSuchTableError,如果未找到表或视图,则此更改特定于各个方言,因此对于现有的第三方方言可能不适用。
  • 将“视图”和“物化视图”的处理分开,因为在实际用例中,这两个构造使用不同的 DDL 来进行 CREATE 和 DROP;现在有单独的Inspector.get_view_names()Inspector.get_materialized_view_names()方法。

#4379

为 psycopg 3(又名“psycopg”)添加方言支持

psycopg 3 DBAPI 添加了方言支持,尽管现在以包名psycopg取代了之前的psycopg2包,后者目前仍然是 SQLAlchemy“默认”驱动程序的postgresql方言。 psycopg是一个完全重做和现代化的用于 PostgreSQL 的数据库适配器,支持诸如准备语句和 Python asyncio 等概念。

psycopg是 SQLAlchemy 支持的第一个 DBAPI,它提供了 pep-249 同步 API 和一个 asyncio 驱动程序。 可以使用相同的psycopg数据库 URL 与create_engine()create_async_engine()引擎创建函数,并且相应的同步或 asyncio 版本的方言将自动选择。

另请参阅

psycopg

为 oracledb 添加方言支持

oracledb DBAPI 添加了方言支持,这是流行的 cx_Oracle 驱动程序的重命名、新主要版本。

另请参阅

python-oracledb

新的条件 DDL 用于约束和索引

一个新的方法Constraint.ddl_if()Index.ddl_if()允许像CheckConstraintUniqueConstraintIndex这样的构造在给定的Table上有条件地渲染,基于与DDLElement.execute_if()方法接受的相同类型的条件。在下面的示例中,CHECK 约束和索引只会针对 PostgreSQL 后端生成:

meta = MetaData()
my_table = Table(
    "my_table",
    meta,
    Column("id", Integer, primary_key=True),
    Column("num", Integer),
    Column("data", String),
    Index("my_pg_index", "data").ddl_if(dialect="postgresql"),
    CheckConstraint("num > 5").ddl_if(dialect="postgresql"),
)
e1 = create_engine("sqlite://", echo=True)
meta.create_all(e1)  # will not generate CHECK and INDEX
e2 = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
meta.create_all(e2)  # will generate CHECK and INDEX

另请参见

控制约束和索引的 DDL 生成

#7631

DATE、TIME、DATETIME 数据类型现在在所有后端上支持文字渲染

现在已经为后端特定编译实现了日期和时间类型的文字渲染,包括 PostgreSQL 和 Oracle:

>>> import datetime
>>> from sqlalchemy import DATETIME
>>> from sqlalchemy import literal
>>> from sqlalchemy.dialects import oracle
>>> from sqlalchemy.dialects import postgresql
>>> date_literal = literal(datetime.datetime.now(), DATETIME)
>>> print(
...     date_literal.compile(
...         dialect=postgresql.dialect(), compile_kwargs={"literal_binds": True}
...     )
... )
'2022-12-17 11:02:13.575789'
>>> print(
...     date_literal.compile(
...         dialect=oracle.dialect(), compile_kwargs={"literal_binds": True}
...     )
... )
TO_TIMESTAMP('2022-12-17 11:02:13.575789',  'YYYY-MM-DD HH24:MI:SS.FF') 

以前,这种文字渲染仅在没有给定方言的情况下将语句字符串化时起作用;当尝试使用特定于方言的类型进行渲染时,会引发NotImplementedError,直到 SQLAlchemy 1.4.45,这变为���CompileError(属于#8800的一部分)。

当使用 PostgreSQL、MySQL、MariaDB、MSSQL、Oracle 方言提供的 SQL 编译器和literal_binds时,默认渲染为修改后的 ISO-8601 渲染(即将 T 转换为空格的 ISO-8601),对于 Oracle,ISO 格式被包装在适当的 TO_DATE() 函数调用中。对于 SQLite,渲染保持不变,因为该方言始终包含日期值的字符串渲染。

#5052

ResultAsyncResult 的上下文管理器支持

Result 对象现在支持上下文管理器使用,这将确保对象及其底层游标在块结束时关闭。这在特定于服务器端游标的情况下特别有用,其中重要的是在操作结束时关闭打开的游标对象,即使发生了用户定义的异常:

with engine.connect() as conn:
    with conn.execution_options(yield_per=100).execute(
        text("select * from table")
    ) as result:
        for row in result:
            print(f"{row}")

在使用 asyncio 时,AsyncResultAsyncConnection 已经修改,以提供可选的异步上下文管理器使用,例如:

async with async_engine.connect() as conn:
    async with conn.execution_options(yield_per=100).execute(
        text("select * from table")
    ) as result:
        for row in result:
            print(f"{row}")

#8710

行为变更

本节涵盖了在 SQLAlchemy 2.0 中进行的行为更改,这些更改在主要的 1.4->2.0 迁移路径中不是主要的一部分;这里的更改不应对向后兼容性产生重大影响。

Session 的新事务加入模式

“将外部事务加入会话”的行为已经进行了修订和改进,允许显式控制Session将如何适应已经建立了事务和可能已经建立了保存点的传入Connection。新参数Session.join_transaction_mode包括一系列选项值,可以以多种方式适应现有事务,最重要的是允许Session仅使用保存点以完全事务化的方式运行,同时在任何情况下都保持外部启动的事务为非提交且处于活动状态,允许测试套件回滚测试中发生的所有更改。

这一主要改进允许文档中记录的将会话加入外部事务的方法(例如用于测试套件)的步骤,也从 SQLAlchemy 1.3 到 1.4 进行了更改,现在简化为不再需要显式使用事件处理程序或提及显式保存点;通过使用join_transaction_mode="create_savepoint"Session永远不会影响传入事务的状态,而是创建一个保存点(即“嵌套事务”)作为其根事务。

以下是在将会话加入外部事务的方法(例如用于测试套件)给出的示例的部分内容;查看该部分以获取完整示例:

class SomeTest(TestCase):
    def setUp(self):
        # connect to the database
        self.connection = engine.connect()
        # begin a non-ORM transaction
        self.trans = self.connection.begin()
        # bind an individual Session to the connection, selecting
        # "create_savepoint" join_transaction_mode
        self.session = Session(
            bind=self.connection, join_transaction_mode="create_savepoint"
        )
    def tearDown(self):
        self.session.close()
        # rollback non-ORM transaction
        self.trans.rollback()
        # return connection to the Engine
        self.connection.close()

Session.join_transaction_mode的默认模式选择为"conditional_savepoint",如果给定的Connection本身已经在保存点上,则使用"create_savepoint"行为。如果给定的Connection处于事务中但不在保存点上,则Session将传播“rollback”调用但不会传播“commit”调用,但不会自行开始新的保存点。此行为被默认选择,因为它与旧版  SQLAlchemy 版本的兼容性最大,并且它不会启动新的 SAVEPOINT,除非给定的驱动程序已经在使用 SAVEPOINT,因为对  SAVEPOINT 的支持不仅取决于特定的后端和驱动程序,还取决于配置。

以下是一个案例,该案例在 SQLAlchemy 1.3 中有效,在 SQLAlchemy 1.4 中停止工作,现在在 SQLAlchemy 2.0 中已经恢复:

engine = create_engine("...")
# setup outer connection with a transaction and a SAVEPOINT
conn = engine.connect()
trans = conn.begin()
nested = conn.begin_nested()
# bind a Session to that connection and operate upon it, including
# a commit
session = Session(conn)
session.connection()
session.commit()
session.close()
# assert both SAVEPOINT and transaction remain active
assert nested.is_active
nested.rollback()
trans.rollback()

在上述情况下,Session加入到已经启动保存点的Connection中;在Session处理事务后,这两个单元的状态保持不变。在 SQLAlchemy 1.3 中,上述案例有效,因为Session会在Connection上开始一个“子事务”,这将允许外部保存点/事务保持不受影响,就像上面的简单情况一样。由于子事务在 1.4 中已被弃用并在 2.0 中已被移除,因此此行为不再可用。新的默认行为通过使用真正的第二个 SAVEPOINT 来改进“子事务”的行为,因此即使调用Session.rollback()也会阻止Session“突破”到外部启动的 SAVEPOINT 或事务。

将已启动事务的Connection加入到Session中的新代码应明确选择Session.join_transaction_mode,以便明确定义所��的行为。

#9015 ### str(engine.url) 将默认混淆密码

为了避免数据库密码泄露,对 URL 调用 str() 现在默认启用密码混淆功能。以前,这种混淆将在 __repr__() 调用中生效,但不会在 __str__() 中生效。此更改将影响试图从另一个引擎的字符串化 URL 调用 create_engine() 的应用程序和测试套件,例如:

>>> e1 = create_engine("postgresql+psycopg2://scott:tiger@localhost/test")
>>> e2 = create_engine(str(e1.url))

上述引擎 e2 将不会有正确的密码;它将有混淆的字符串 "***"

上述模式的首选方法是直接传递 URL 对象,无需进行字符串化:

>>> e1 = create_engine("postgresql+psycopg2://scott:tiger@localhost/test")
>>> e2 = create_engine(e1.url)

否则,对于具有明文密码的字符串化 URL,请使用 URL.render_as_string() 方法,并将 URL.render_as_string.hide_password 参数设置为 False

>>> e1 = create_engine("postgresql+psycopg2://scott:tiger@localhost/test")
>>> url_string = e1.url.render_as_string(hide_password=False)
>>> e2 = create_engine(url_string)

#8567 ### 对具有相同名称、键的 Table 对象中列的替换有更严格的规则

对于将 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,以及对于使用反射或不使用反射的构建一个具有相同名称的 TableTable.extend_existing。此前,该情况已经有了废弃警告。
  • 创建 Table 时现在会发出警告,如果其中包含 Table.extend_existing,其中一个没有单独的 Column.key 的传入 Column 会完全替换具有键的现有 Column,这表明操作并非用户意图。这种情况可能特别发生在次要反射步骤期间,例如 metadata.reflect(extend_existing=True)。警告建议将 Table.autoload_replace 参数设置为 False 以防止这种情况发生。在之前的版本中(1.4 及更早),传入的列会额外添加到现有列中。这是一个错误,并且在 2.0 中(截至 2.0.0b4)是一种行为变化,因为此时先前的键将不再存在于列集合中。

#8925 ### ORM 声明式应用列顺序不同;使用 sort_order 控制行为

声明式已更改了从 mixin 或抽象基类产生的映射列与声明类本身上的列一起排序的系统,以便先将声明类的列放在前面,然后是 mixin 列。以下映射:

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 中的行为没有变化。

#7211 ### “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 中是有帮助的。

#6980 ### Python 除法运算符对所有后端执行真除法;添加了地板除法。

核心表达式语言现在支持“真除法”(即 Python 操作符/)和“地板除法”(即 Python 操作符//),包括后端特定的行为以规范化这方面不同数据库的行为。

给定两个整数值进行“真除法”操作:

expr = literal(5, Integer) / literal(10, Integer)

例如,在 PostgreSQL 上,SQL 除法运算符通常在对整数使用时作为“地板除法”运行,这意味着上述结果将返回整数“0”。对于这些和类似的后端,SQLAlchemy 现在使用等效于以下形式的 SQL 来呈现:

%(param_1)s  /  CAST(%(param_2)s  AS  NUMERIC)

param_1=5param_2=10时,返回表达式将是NUMERIC类型,通常作为 Python 值decimal.Decimal("0.5")

给定两个整数值进行“地板除法”操作:

expr = literal(5, Integer) // literal(10, Integer)

例如,在 MySQL 和 Oracle 上,SQL 除法运算符通常在对整数使用时作为“真除法”运行,这意味着上述结果将返回浮点值“0.5”。对于这些和类似的后端,SQLAlchemy 现在使用等效于以下形式的 SQL 来呈现:

FLOOR(%(param_1)s  /  %(param_2)s)

param_1=5param_2=10时,返回表达式将是INTEGER类型,就像 Python 值0一样。

这里的不兼容变化是,如果一个应用程序使用 PostgreSQL、SQL Server 或 SQLite,并依赖于 Python  的“truediv”运算符在所有情况下返回整数值。依赖于这种行为的应用程序应该使用 Python 的“floor division”运算符 // 进行这些操作,或者在使用之前的 SQLAlchemy 版本时,使用 floor 函数以确保向前兼容性。

expr = func.floor(literal(5, Integer) / literal(10, Integer))

在任何 SQLAlchemy 版本 2.0 之前的版本中,都需要上述形式来提供与后端无关的地板除法。

#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中说明的测试脚本,前面的错误案例看起来像这样:

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 之类的补丁样式并发方法时。

#7433 ### SQLite 方言在基于文件的数据库中使用 QueuePool

当使用基于文件的数据库时,SQLite 方言现在默认为QueuePool。这是在将check_same_thread参数设置为False的同时进行的。已经观察到,以前默认为NullPool的方法,在释放连接后不会保留数据库连接,实际上确实对性能产生了可测量的负面影响。像往常一样,可以通过create_engine.poolclass参数自定义池类。

另请参阅

线程/池行为

#7490 ### 具有二进制精度的新 Oracle FLOAT 类型;不能直接接受十进制精度

Oracle 方言添加了新的数据类型FLOAT,以配合Double和数据库特定的DOUBLEDOUBLE_PRECISIONREAL数据类型的添加。 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 数据类型还接受“精度”参数,但这是十进制精度,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 的 `@>` 和 `<@` 相同。未来的版本可能会增加其他操作符支持。
请参阅 Range and Multirange Types 文档,了解使用这一新功能的背景。
另请参阅
范围和多范围类型
[#7156](https://www.sqlalchemy.org/trac/ticket/7156) [#8706](https://www.sqlalchemy.org/trac/ticket/8706)  ### 在 PostgreSQL 上,`match()` 运算符使用 `plainto_tsquery()` 而不是 `to_tsquery()`
在 PostgreSQL 后端上,`Operators.match()` 函数现在呈现 `col @@ plainto_tsquery(expr)`,而不是 `col @@ to_tsquery()`。`plainto_tsquery()` 接受纯文本,而 `to_tsquery()` 接受专用查询符号,因此与其他后端的兼容性较差。
通过使用 `func` 生成 PostgreSQL 特定函数和 `Operators.bool_op()`(`Operators.op()` 的布尔类型版本)生成任意运算符,可以使用所有 PostgreSQL 搜索函数和操作符,方式与先前版本中提供的方式相同。请参阅 全文搜索 中的示例。
在使用 `Operators.match()` 内的 PG 特定指令的现有 SQLAlchemy 项目应直接使用 `func.to_tsquery()`。要以与 1.4 中相同的形式呈现 SQL,请参阅 使用 match() 进行简单的纯文本匹配 的版本说明。
[#7086](https://www.sqlalchemy.org/trac/ticket/7086)  ### `Session` 的新事务连接模式
“将外部事务加入到会话中”的行为已经修订和改进,允许对 `Session` 如何适应已经建立了事务和可能已经建立了保存点的传入 `Connection` 进行显式控制。新的参数 `Session.join_transaction_mode` 包括一系列选项值,可以以几种方式适应现有的事务,最重要的是允许一个 `Session` 以完全事务性的方式操作,专门使用保存点,同时在所有情况下保持外部启动的事务未提交且处于活动状态,从而允许测试套件回滚在测试中发生的所有更改。
这样做的主要改进是,文档中记录的 将会话加入外部事务(例如测试套件) 的配方,也从 SQLAlchemy 1.3 更改为 1.4,现在简化为不再需要显式使用事件处理程序或任何提及显式保存点;通过使用 `join_transaction_mode="create_savepoint"`,`Session` 将永远不会影响传入事务的状态,而是创建一个保存点(即“嵌套事务”)作为其根事务。
以下是在 将会话加入外部事务(例如测试套件) 中给出的示例的一部分说明;请参阅该部分以获取完整示例:
```py
class SomeTest(TestCase):
    def setUp(self):
        # connect to the database
        self.connection = engine.connect()
        # begin a non-ORM transaction
        self.trans = self.connection.begin()
        # bind an individual Session to the connection, selecting
        # "create_savepoint" join_transaction_mode
        self.session = Session(
            bind=self.connection, join_transaction_mode="create_savepoint"
        )
    def tearDown(self):
        self.session.close()
        # rollback non-ORM transaction
        self.trans.rollback()
        # return connection to the Engine
        self.connection.close()

Session.join_transaction_mode 的默认模式选择是 "conditional_savepoint",如果给定的 Connection 已经处于保存点上,则使用 "create_savepoint" 行为。如果给定的 Connection 处于事务中但不在保存点中,则 Session 将传播“回滚”调用但不传播“提交”调用,但不会自行开始新的保存点。此行为被默认选择,因为它最大程度地与旧版本的 SQLAlchemy  兼容,并且它不会启动新的 SAVEPOINT,除非给定的驱动程序已经在使用 SAVEPOINT,因为 SAVEPOINT  的支持不仅与特定的后端和驱动程序有关,还与配置有关。

以下是一个案例的示例,在 SQLAlchemy 1.3 中有效,在 SQLAlchemy 1.4 中停止工作,并在 SQLAlchemy 2.0 中恢复:

engine = create_engine("...")
# setup outer connection with a transaction and a SAVEPOINT
conn = engine.connect()
trans = conn.begin()
nested = conn.begin_nested()
# bind a Session to that connection and operate upon it, including
# a commit
session = Session(conn)
session.connection()
session.commit()
session.close()
# assert both SAVEPOINT and transaction remain active
assert nested.is_active
nested.rollback()
trans.rollback()

在上述情况下,Session 加入到具有已启动保存点的 Connection 中后;这两个单元的状态在 Session 处理事务后保持不变。在 SQLAlchemy 1.3 中,上述情况有效,因为 Session 将在 Connection 上开始一个“子事务”,这将允许外部保存点 / 事务保持不受影响,对于上述简单情况而言。由于子事务在 1.4 中已弃用并在 2.0 中已删除,因此此行为不再可用。新的默认行为通过使用真正的第二个 SAVEPOINT 改进了“子事务”的行为,以便即使调用 Session.rollback() 也会阻止 Session “突破”到外部启动的 SAVEPOINT 或事务。

新代码将 Connection 加入到 Session 的事务中时,应明确选择一个 Session.join_transaction_mode ,以明确定义所需的行为。

#9015

str(engine.url) 将默认混淆密码

为了避免数据库密码泄露,现在在 URL 上调用 str() 将默认启用密码混淆功能。之前,此混淆对于 __repr__() 调用有效,但对于 __str__() 调用无效。此更改将影响尝试使用来自另一个引擎的字符串化 URL 调用 create_engine() 的应用程序和测试套件,例如:

>>> e1 = create_engine("postgresql+psycopg2://scott:tiger@localhost/test")
>>> e2 = create_engine(str(e1.url))

上述引擎 e2 将不会有正确的密码;它将有混淆的字符串 "***"

上述模式的首选方法是直接传递 URL 对象,无需转换为字符串:

>>> e1 = create_engine("postgresql+psycopg2://scott:tiger@localhost/test")
>>> e2 = create_engine(e1.url)

否则,对于具有明文密码的字符串化 URL,请使用 URL.render_as_string() 方法,并将 URL.render_as_string.hide_password 参数设置为 False

>>> e1 = create_engine("postgresql+psycopg2://scott:tiger@localhost/test")
>>> url_string = e1.url.render_as_string(hide_password=False)
>>> e2 = create_engine(url_string)

#8567


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

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
4月前
|
SQL 关系型数据库 API
SqlAlchemy 2.0 中文文档(五十四)(5)
SqlAlchemy 2.0 中文文档(五十四)
45 1
|
4月前
|
SQL 缓存 关系型数据库
SqlAlchemy 2.0 中文文档(五十四)(2)
SqlAlchemy 2.0 中文文档(五十四)
99 1
|
4月前
|
SQL 关系型数据库 测试技术
SqlAlchemy 2.0 中文文档(五十四)(4)
SqlAlchemy 2.0 中文文档(五十四)
37 1
|
4月前
|
SQL 缓存 关系型数据库
SqlAlchemy 2.0 中文文档(五十四)(3)
SqlAlchemy 2.0 中文文档(五十四)
29 1
|
4月前
|
SQL Oracle 关系型数据库
SqlAlchemy 2.0 中文文档(五十七)(3)
SqlAlchemy 2.0 中文文档(五十七)
29 0
|
4月前
|
SQL API Python
SqlAlchemy 2.0 中文文档(五十七)(6)
SqlAlchemy 2.0 中文文档(五十七)
32 0
|
4月前
|
SQL Oracle 关系型数据库
SqlAlchemy 2.0 中文文档(五十七)(9)
SqlAlchemy 2.0 中文文档(五十七)
80 0
|
4月前
|
SQL 存储 测试技术
SqlAlchemy 2.0 中文文档(五十七)(4)
SqlAlchemy 2.0 中文文档(五十七)
34 0
|
4月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(五十七)(2)
SqlAlchemy 2.0 中文文档(五十七)
26 0
|
4月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(五十七)(1)
SqlAlchemy 2.0 中文文档(五十七)
42 0