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

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

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


行为变化

本节涵盖了 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会传播“回滚”调用但不会传播“提交”调用,但不会自行开始一个新的保存点。这种行为被默认选择是因为它与旧版本的  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.extend_existing设置为True时,无论是在编程式Table构建还是在反射操作期间。

  • 无论如何,Table对象永远不应该具有两个或更多具有相同名称的Column对象,无论它们有什么.key。已经确定并修复了一个仍然可能发生这种情况的边缘情况。
  • 向具有与现有 Column 相同名称或键的 Table 添加 Column 将始终引发 DuplicateColumnError(在 2.0.0b4 中是 ArgumentError 的新子类),除非存在其他参数;Table.append_column.replace_existing 用于 Table.append_column(),以及 Table.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 Declarative 对列顺序的应用方式不同;使用 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 声明的内容,否则,一个低于起始值的 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"

此外,如上所示,可以为单个类型传递多个方言名称,特别是对于被视为 SQLAlchemy 1.4 的"mysql""mariadb"方言对来说,这是有帮助的。

#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 在多个线程同时使用时,已知会发生的一个错误是 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 支持和更改
RANGE / MULTIRANGE 支持已完全实现为 psycopg2、psycopg3 和 asyncpg 方言。新支持使用了一个新的 SQLAlchemy 特定的 `Range` 对象,它对不同的后端是不可知的,不需要使用后端特定的导入或扩展步骤。对于多范围支持,使用 `Range` 对象的列表。
之前使用的特定于 psycopg2 的类型的代码应修改为使用`Range`,它提供了兼容的接口。
`Range` 对象还具有与 PostgreSQL 相同的比较支持。到目前为止已实现了 `Range.contains()` 和 `Range.contained_by()` 方法,它们的工作方式与 PostgreSQL 的 `@>` 和 `<@` 相同。未来的版本可能会添加其他运算符支持。
请参阅范围和多范围类型处的文档,了解如何使用新功能的背景信息。
另请参阅
范围和多范围类型
[#7156](https://www.sqlalchemy.org/trac/ticket/7156) [#8706](https://www.sqlalchemy.org/trac/ticket/8706)  ### 在 PostgreSQL 上使用`match()`操作符使用`plainto_tsquery()`而不是`to_tsquery()`
`Operators.match()`函数现在在 PostgreSQL 后端上呈现为`col @@ plainto_tsquery(expr)`,而不是`col @@ to_tsquery()`。`plainto_tsquery()`接受纯文本,而`to_tsquery()`接受专门的查询符号,因此与其他后端的兼容性较差。
通过使用`func`生成 PostgreSQL 特定函数和`Operators.bool_op()`(`Operators.op()`的布尔类型版本)来生成任意运算符,以与之前版本相同的方式,所有 PostgreSQL 搜索函数和操作符都可用。请参阅全文搜索中的示例。
现有的 SQLAlchemy 项目如果在`Operators.match()`中使用了 PG 特定的指令,应该直接使用`func.to_tsquery()`。要以与 1.4 版本完全相同的形式呈现 SQL,请参阅使用 match()进行简单纯文本匹配的版本说明。
[#7086](https://www.sqlalchemy.org/trac/ticket/7086)
## Core 和 ORM 中的新类型支持-不再使用存根/扩展
与 1.4 版本中通过[sqlalchemy2-stubs](https://github.com/sqlalchemy/sqlalchemy2-stubs)包提供的临时方法相比,Core 和 ORM 的类型化方法已完全重写。新方法始于 SQLAlchemy 中最基本的元素,即`Column`,更准确地说是底层所有具有类型的 SQL 表达式的`ColumnElement`。然后,这个表达式级别的类型化扩展到语句构造、语句执行和结果集的领域,最终扩展到 ORM,其中新的声明性形式允许完全类型化的 ORM 模型,从语句到结果集全部集成。
提示
对于 2.0 系列,类型支持应被视为**beta 级别**软件。类型详细信息可能会发生变化,但不计划进行重大不兼容更改。
### SQL 表达式/语句/结果集类型化
本节提供了 SQLAlchemy 的新 SQL 表达类型方法的背景和示例,它从基本的`ColumnElement`构造扩展到 SQL 语句和结果集,以及 ORM 映射的领域。
#### 原因和概述
提示
本节是一个架构讨论。 跳转到 SQL 表达类型 - 示例以查看新的类型化外观。
在[sqlalchemy2-stubs](https://github.com/sqlalchemy/sqlalchemy2-stubs)中,SQL 表达式被类型化为[泛型](https://peps.python.org/pep-0484/#generics),然后引用`TypeEngine`对象,如`Integer`,`DateTime`,或`String`作为它们的泛型参数(如`Column[Integer]`)。 这本身就是与最初的 Dropbox [sqlalchemy-stubs](https://github.com/dropbox/sqlalchemy-stubs)包不同的地方,其中`Column`及其基本构造直接以 Python 类型为泛型,例如`int`,`datetime`和`str`。 希望由于`Integer` / `DateTime` / `String`本身是对`int` / `datetime` / `str`的泛型,因此有方法可以保持两个级别的信息,并且可以通过`TypeEngine`从列表达式中提取 Python 类型作为中间构造。 但是,事实并非如此,因为[**PEP 484**](https://peps.python.org/pep-0484/)没有足够丰富的功能集使其可行,缺乏诸如[higher kinded TypeVars](https://github.com/python/typing/issues/548)之类的功能。
经过对[当前能力的深入评估](https://github.com/python/typing/discussions/999),SQLAlchemy 2.0 实现了[**PEP 484**](https://peps.python.org/pep-0484/)的原始智慧,直接将列表达式与 Python 类型进行关联。这意味着,如果有不同子类型的 SQL 表达式,比如`Column(VARCHAR)`和`Column(Unicode)`,那么这两个`String`子类型的具体信息不会随类型一起传递,因为类型只会携带`str`,但实际上这通常不是问题,通常更有用的是 Python 类型立即出现,因为它代表了将直接存储和接收的 Python 数据。
具体来说,这意味着像`Column('id', Integer)`这样的表达式被类型化为`Column[int]`。这允许建立一个可行的 SQLAlchemy 构造 -> Python 数据类型的管道,而无需使用类型插件。至关重要的是,它允许与 ORM 使用`select()`和`Row`构造的完全互操作性,这些构造引用 ORM 映射的类类型(例如,包含用户映射实例的`Row`,如我们教程中使用的`User`和`Address`示例)。虽然 Python 类型当前对元组类型的定制支持非常有限(其中[**PEP 646**](https://peps.python.org/pep-0646/)是第一个试图处理类似元组对象的 pep,[故意在功能上受到限制](https://mail.python.org/archives/list/typing-sig@python.org/message/G2PNHRR32JMFD3JR7ACA2NDKWTDSEPUG/),并且本身尚不适用于任意元组操作),但已经设计出了一个相当不错的方法,允许基本的`select()` -> `Result` -> `Row`类型化功能,包括 ORM 类,其中在将`Row`对象解包为单独列条目时,添加了一个小的面向类型的访问器,允许各个 Python 值保持与其来源的 SQL 表达式相关联的 Python 类型(翻译:它有效)。
#### SQL 表达式类型化 - 示例
简要介绍了类型行为。注释指示在[vscode](https://code.visualstudio.com/)中悬停在代码上会看到什么(或者使用[reveal_type()](https://mypy.readthedocs.io/en/latest/common_issues.html?highlight=reveal_type#reveal-type)辅助工具时大致会显示什么):
+   简单的 Python 类型分配给 SQL 表达式
    ```py
    # (variable) str_col: ColumnClause[str]
    str_col = column("a", String)
    # (variable) int_col: ColumnClause[int]
    int_col = column("a", Integer)
    # (variable) expr1: ColumnElement[str]
    expr1 = str_col + "x"
    # (variable) expr2: ColumnElement[int]
    expr2 = int_col + 10
    # (variable) expr3: ColumnElement[bool]
    expr3 = int_col == 15
    ```
+   分配给`select()`构造的单个 SQL 表达式以及任何返回行的构造,包括返回行的 DML,如带有`Insert.returning()`的`Insert`,都打包成一个保留每个元素 Python 类型的`Tuple[]`类型。
    ```py
    # (variable) stmt: Select[Tuple[str, int]]
    stmt = select(str_col, int_col)
    # (variable) stmt: ReturningInsert[Tuple[str, int]]
    ins_stmt = insert(table("t")).returning(str_col, int_col)
    ```
+   任何返回行的结构中的`Tuple[]`类型,在调用`.execute()`方法时,传递到`Result`和`Row`。为了将`Row`对象解包为元组,`Row.tuple()`或`Row.t`访问器本质上将`Row`转换为相应的`Tuple[]`(但在运行时仍保持相同的`Row`对象)。
    ```py
    with engine.connect() as conn:
        # (variable) stmt: Select[Tuple[str, int]]
        stmt = select(str_col, int_col)
        # (variable) result: Result[Tuple[str, int]]
        result = conn.execute(stmt)
        # (variable) row: Row[Tuple[str, int]] | None
        row = result.first()
        if row is not None:
            # for typed tuple unpacking or indexed access,
            # use row.tuple() or row.t  (this is the small typing-oriented accessor)
            strval, intval = row.t
            # (variable) strval: str
            strval
            # (variable) intval: int
            intval
    ```
+   对于单列语句的标量值,使用`Connection.scalar()`、`Result.scalars()`等方法是正确的。
    ```py
    # (variable) data: Sequence[str]
    data = connection.execute(select(str_col)).scalars().all()
    ```
+   对于 ORM 映射类,上述对返回行构造的支持与其最配套,因为映射类可以列出其成员的特定类型。下面的示例设置了一个使用新的类型感知语法的类,将在下一节中描述:
    ```py
    from sqlalchemy.orm import DeclarativeBase
    from sqlalchemy.orm import Mapped
    from sqlalchemy.orm import mapped_column
    class Base(DeclarativeBase):
        pass
    class User(Base):
        __tablename__ = "user_account"
        id: Mapped[int] = mapped_column(primary_key=True)
        name: Mapped[str]
        addresses: Mapped[List["Address"]] = relationship()
    class Address(Base):
        __tablename__ = "address"
        id: Mapped[int] = mapped_column(primary_key=True)
        email_address: Mapped[str]
        user_id = mapped_column(ForeignKey("user_account.id"))
    ```
    通过上述映射,属性从语句到结果集一路类型化并表达自己:
    ```py
    with Session(engine) as session:
        # (variable) stmt: Select[Tuple[int, str]]
        stmt_1 = select(User.id, User.name)
        # (variable) result_1: Result[Tuple[int, str]]
        result_1 = session.execute(stmt_1)
        # (variable) intval: int
        # (variable) strval: str
        intval, strval = result_1.one().t
    ```
    映射类本身也是类型,并且行为相同,例如针对两个映射类进行 SELECT 查询:
    ```py
    with Session(engine) as session:
        # (variable) stmt: Select[Tuple[User, Address]]
        stmt_2 = select(User, Address).join_from(User, Address)
        # (variable) result_2: Result[Tuple[User, Address]]
        result_2 = session.execute(stmt_2)
        # (variable) user_obj: User
        # (variable) address_obj: Address
        user_obj, address_obj = result_2.one().t
    ```
    当选择映射类时,像`aliased`这样的构造也能正常工作,保持原始映射类的列级属性以及语句期望的返回类型:
    ```py
    with Session(engine) as session:
        # this is in fact an Annotated type, but typing tools don't
        # generally display this
        # (variable) u1: Type[User]
        u1 = aliased(User)
        # (variable) stmt: Select[Tuple[User, User, str]]
        stmt = select(User, u1, User.name).filter(User.id == 5)
        # (variable) result: Result[Tuple[User, User, str]]
        result = session.execute(stmt)
    ```
+   核心表(Core Table)目前还没有一个合适的方法来在通过`Table.c`访问时维护`Column`对象的类型。
    由于`Table`被设置为类的实例,并且`Table.c`访问器通常通过名称动态访问`Column`对象,因此尚未为此建立类型化方法;需要一些替代语法。
+   ORM 类、标量等工作得很好。
    选择 ORM 类作为标量或元组的典型用例都有效,无论是 2.0 还是 1.x 风格的查询,都可以获得确切的类型,无论是单独还是包含在适当的容器中,如`Sequence[]`、`List[]`或`Iterator[]`:
    ```py
    # (variable) users1: Sequence[User]
    users1 = session.scalars(select(User)).all()
    # (variable) user: User
    user = session.query(User).one()
    # (variable) user_iter: Iterator[User]
    user_iter = iter(session.scalars(select(User)))
    ```
+   传统的`Query`也获得了元组类型。
    对于`Query`的类型支持远远超出了[sqlalchemy-stubs](https://github.com/dropbox/sqlalchemy-stubs)或[sqlalchemy2-stubs](https://github.com/sqlalchemy/sqlalchemy2-stubs)提供的范围,其中标量对象以及元组类型的`Query`对象将保留大多数情况下的结果级别类型:
    ```py
    # (variable) q1: RowReturningQuery[Tuple[int, str]]
    q1 = session.query(User.id, User.name)
    # (variable) rows: List[Row[Tuple[int, str]]]
    rows = q1.all()
    # (variable) q2: Query[User]
    q2 = session.query(User)
    # (variable) users: List[User]
    users = q2.all()
    ```
#### 注意 - 所有存根必须被卸载
类型支持的一个关键警告是**必须卸载所有 SQLAlchemy 存根包**才能使类型化工作。在针对 Python 虚拟环境运行[mypy](https://mypy.readthedocs.io/en/stable/)时,只需卸载这些包。但是,SQLAlchemy 存根包目前也是[typeshed](https://github.com/python/typeshed)的一部分,它本身捆绑在一些类型工具中,如[Pylance](https://github.com/microsoft/pylance-release),因此在某些情况下可能需要定位这些包的文件并删除它们,如果它们实际上干扰了新的类型化正确工作。
一旦 SQLAlchemy 2.0 以最终状态发布,typeshed 将从其自己的存根源中删除 SQLAlchemy。
### ORM 声明模型
SQLAlchemy 1.4 引入了第一个使用[sqlalchemy2-stubs](https://github.com/sqlalchemy/sqlalchemy2-stubs)和 Mypy Plugin 组合的 SQLAlchemy 本机 ORM 类型支持。在 SQLAlchemy 2.0 中,Mypy 插件**仍然可用,并已更新以与 SQLAlchemy 2.0 的类型系统一起使用**。但是,现在应该将其视为**已弃用**,因为应用程序现在有一条直接的路径来采用新的类型支持,而不使用插件或存根。
#### 概述
新系统的基本方法是,当使用完全声明式模型(即不使用混合声明式或命令式配置,这些配置不变)时,映射列声明首先在运行时通过检查每个属性声明左侧的类型注释来推导,如果存在的话。左手类型注释应该包含在`Mapped`泛型类型中,否则该属性不被视为映射属性。然后属性声明可以引用右侧的`mapped_column()`构造,用于提供有关要生成和映射的`Column`的附加核心级模式信息。如果左侧存在`Mapped`注释,则此右侧声明是可选的;如果左侧没有注释,则`mapped_column()`可以用作`Column`指令的精确替代,其中它将提供更准确(但不精确)的属性类型行为,即使没有注释存在。
这种方法受到 Python [dataclasses](https://docs.python.org/3/library/dataclasses.html)方法的启发,它从左边开始注释,然后允许右边的可选`dataclasses.field()`规范;与 dataclasses 方法的关键区别在于 SQLAlchemy 的方法是严格的**选择加入**,其中使用`Column`的现有映射如果没有任何类型注释,将继续像以往一样工作,并且`mapped_column()`构造可以直接替换`Column`而不需要任何显式类型注释。只有在确切的属性级 Python 类型存在时,才需要使用带有`Mapped`的显式注释。这些注释可以根据需要,按属性基础使用,对于那些特定类型有帮助的属性;使用`mapped_column()`的未注释属性将在实例级别被标记为`Any`。
#### 迁移现有映射
迁移到新的 ORM 方法开始时更加冗长,但随着可用的新功能的充分利用,变得比以前更简洁。以下步骤详细说明了典型的过渡,然后继续说明了一些更多的选项。
##### 第一步 - `declarative_base()`被`DeclarativeBase`取代。
Python 类型中观察到的一个限制是似乎没有能力从函数动态生成一个类,然后被类型工具理解为新类的基础。为了解决这个问题而不使用插件,通常对`declarative_base()`的调用可以替换为使用`DeclarativeBase`类,它产生与通常相同的`Base`对象,只是类型工具理解它:
```py
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
    pass


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

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