SqlAlchemy 2.0 中文文档(七十二)(1)

本文涉及的产品
云原生数据库 PolarDB PostgreSQL 版,企业版 4核16GB
推荐场景:
HTAP混合负载
云数据库 RDS MySQL,集群版 2核4GB 100GB
推荐场景:
搭建个人博客
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
简介: SqlAlchemy 2.0 中文文档(七十二)


原文:docs.sqlalchemy.org/en/20/contents.html

SQLAlchemy 1.4 有什么新特性?

原文:docs.sqlalchemy.org/en/20/changelog/migration_14.html

关于本文档

本文描述了 SQLAlchemy 版本 1.3 和 SQLAlchemy 版本 1.4 之间的变化。

版本 1.4 的重点与其他 SQLAlchemy 版本不同,它在很多方面试图作为潜在的迁移点,用于当前计划发布的 SQLAlchemy  2.0 中更为重大的一系列 API 更改。SQLAlchemy 2.0 的重点是现代化和精简的  API,删除了长期以来被不鼓励的许多使用模式,并将 SQLAlchemy 中的最佳思想作为一流 API 功能,目标是 API  的使用方式更加明确,以及删除一系列隐式行为和很少使用的 API 标志,这些都会使内部复杂化并阻碍性能。

有关 SQLAlchemy 2.0 的当前状态,请参阅 SQLAlchemy 2.0 - 主要迁移指南。

主要 API 更改和功能 - 通用

Python 3.6 是最低要求的 Python 3 版本;仍支持 Python 2.7

由于 Python 3.5 在 2020 年 9 月已达到生命周期终点,因此  SQLAlchemy 1.4 现在将版本 3.6 作为最低要求的 Python 3 版本。Python 2.7 仍然受支持,但是  SQLAlchemy 1.4 系列将是最后一个支持 Python 2 的系列。### ORM 查询在内部与  select、update、delete 统一;2.0 风格的执行可用

对于 SQLAlchemy 2.0 和本质上也是 1.4 版本的最大概念变化是,在 Core 中的Select构造和 ORM 中的Query对象之间的巨大分离已被移除,以及在它们与UpdateDelete之间的Query.update()Query.delete()方法之间的分离。

关于SelectQuery,这两个对象在许多版本中具有类似的、大部分重叠的 API,甚至有一些能够在两者之间切换的能力,但在使用模式和行为上仍然有很大的不同。这一历史背景是,Query对象是为了克服Select对象的缺点而引入的,后者曾经是 ORM 对象查询的核心,只是它们必须以Table元数据的形式进行查询。然而,Query只有一个简单的接口来加载对象,只有在许多主要版本的发布过程中,它最终才获得了大部分Select对象的灵活性,这导致这两个对象变得非常相似,但仍然在很大程度上不兼容。

在版本 1.4 中,所有核心和 ORM SELECT 语句都直接从Select对象呈现;当使用Query对象时,在语句调用时,它会将其状态复制到一个Select对象中,然后使用 2.0 风格执行。未来,Query对象将仅成为传统,应用程序将被鼓励转向 2.0 风格执行,允许核心构造自由地针对 ORM 实体使用:

with Session(engine, future=True) as sess:
    stmt = (
        select(User)
        .where(User.name == "sandy")
        .join(User.addresses)
        .where(Address.email_address.like("%gmail%"))
    )
    result = sess.execute(stmt)
    for user in result.scalars():
        print(user)

以上示例的注意事项:

  • Sessionsessionmaker对象现在具有完整的上下文管理器(即with:语句)功能;请参阅打开和关闭会话的修订文档以获取示例。
  • 在 1.4 系列中,所有 2.0 风格的 ORM 调用都使用一个包含Session的对象,其中包括设置为TrueSession.future标志;这个标志表示Session应该具有 2.0 风格的行为,其中包括 ORM 查询可以从execute中调用,以及一些事务特性的变化。在 2.0 版本中,这个标志将始终为True
  • select()构造不再需要在列子句周围加括号;有关此改进的背景,请参阅 select(), case()现在接受位置表达式。
  • select() / Select对象具有一个Select.join()方法,其行为类似于Query的方法,甚至可以容纳 ORM 关系属性(而不会破坏 Core 和 ORM 之间的分离!)- 有关此内容,请参阅 select().join()和 outerjoin()向当前查询添加 JOIN 条件,而不是创建子查询。
  • 与 ORM 实体一起工作并预计返回 ORM 结果的语句是使用Session.execute()来调用的。查看查询以获取入门指南。另请参阅 ORM Session.execute()在所有情况下使用“future”风格结果集中的以下注意事项。
  • 返回一个Result对象,而不是一个普通列表,这本身是以前ResultProxy对象的一个更复杂的版本;这个对象现在用于  Core 和 ORM 结果。有关此信息,请参阅新的 Result 对象,RowProxy 不再是“代理”;现在称为  Row,并且行为类似于增强的命名元组,以及 Query 返回的“KeyedTuple”对象被 Row 替换。

在整个 SQLAlchemy 的文档中,将会有许多关于 1.x 风格 和 2.0 风格 执行的引用。这是为了区分这两种查询风格,并尝试在前进过程中向前文档化新的调用风格。在 SQLAlchemy 2.0 中,虽然 Query 对象可能仍然保留为遗留构造,但它将不再在大多数文档中显示。

类似的调整已经针对“批量更新和删除”进行了,以便 Core 的 update()delete() 可用于批量操作。如下所示的批量更新:

session.query(User).filter(User.name == "sandy").update(
    {"password": "foobar"}, synchronize_session="fetch"
)

可以以 2.0 风格(事实上,上述代码在内部以此方式运行)实现如下:

with Session(engine, future=True) as sess:
    stmt = (
        update(User)
        .where(User.name == "sandy")
        .values(password="foobar")
        .execution_options(synchronize_session="fetch")
    )
    sess.execute(stmt)

请注意使用 Executable.execution_options() 方法传递 ORM 相关选项。现在“执行选项”的使用在 Core 和 ORM 中更为普遍,并且许多来自 Query 的 ORM 相关方法现在被实现为执行选项(请参阅 Query.execution_options() 查看一些示例)。

另请参阅

SQLAlchemy 2.0 - 主要迁移指南

#5159 ### ORM Session.execute() 在所有情况下都使用“future”风格的 Result

如 RowProxy 现在不再是“代理”;现在被称为 Row 并且像增强型命名元组一样行为 所述,当与一个包含 create_engine.future 参数设置为 TrueEngine 一起使用时,ResultRow 对象现在具有“命名元组”行为。这些“命名元组”行特别包括一种行为变化,即使用 in 的 Python 包含表达式,如下所示:

>>> engine = create_engine("...", future=True)
>>> conn = engine.connect()
>>> row = conn.execute.first()
>>> "name" in row
True

上述包含测试将使用 值包含,而不是 键包含row 需要具有“name”的 value 才能返回 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 对象时必须构建的对象构造例程。
为了介绍该功能的一般概念,给出来自 Performance 套件的代码如下,它将调用一个非常简单的查询“n”次,默认值为 n=10000。查询仅返回一行,因为我们要减少的开销是**许多小查询**的开销。对于返回许多行的查询,优化并不那么显著:
```py
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 查询,结合完整的“baked”风格调用,可以在许多迭代中运行得快 60%,并且提供与现在被本地缓存系统取代的烘焙查询系统大致相同的性能。

新系统利用现有的Connection.execution_options.compiled_cache执行选项,并直接向Engine添加了一个缓存,该缓存使用Engine.query_cache_size参数进行配置。

1.4 版本中的 API 和行为变化的重要部分是为了支持这一新功能。

另请参阅

SQL 编译缓存

#4639 #5380 #4645 #4808 #5004 ### 声明式现在已经与新功能整合到 ORM 中

大约十年后,sqlalchemy.ext.declarative包现在已经整合到sqlalchemy.orm命名空间中,除了保留为声明式扩展的声明式“extension”类。

新添加到sqlalchemy.orm的类包括:

  • registry - 一个新类,取代了“declarative base”类的角色,作为映射类的注册表,可以通过字符串名称在relationship()调用中引用,并且不受任何特定类映射样式的限制。
  • declarative_base() - 这是在声明系统跨度期间一直在使用的相同声明基类,只是现在在内部引用了一个registry对象,并由registry.generate_base()方法实现,可以直接从registry调用。declarative_base()函数会自动创建此注册表,因此对现有代码没有影响。当启用 2.0 deprecations mode 时,sqlalchemy.ext.declarative.declarative_base名称仍然存在,发出 2.0 弃用警告。
  • declared_attr() - 同样是“declared attr”函数调用现在成为sqlalchemy.orm的一部分。当启用 2.0 deprecations mode 时,sqlalchemy.ext.declarative.declared_attr名称仍然存在,发出 2.0 弃用警告。
  • 其他名称移至sqlalchemy.orm,包括has_inherited_table()synonym_for()DeclarativeMetaas_declarative()

另外,instrument_declarative()函数已被弃用,被registry.map_declaratively()取代。ConcreteBaseAbstractConcreteBaseDeferredReflection类仍然作为声明性扩展包中的扩展。

映射样式现在已经组织起来,它们都从registry对象扩展,并分为以下几类:

  • 声明性映射
  • 使用declarative_base()基类与元类
  • 具有 mapped_column()的声明性表
  • 命令式表(又名“混合表”)
  • 使用registry.mapped()声明性装饰器
  • 声明性表
  • 命令式表(混合)
  • 将 ORM 映射应用于现有数据类(传统数据类用法)
  • 命令式(又名“经典”映射)
  • 使用registry.map_imperatively()
  • 使用命令式映射映射预先存在的数据类

现有的经典映射函数sqlalchemy.orm.mapper()仍然存在,但不建议直接调用sqlalchemy.orm.mapper();新的registry.map_imperatively()方法现在通过sqlalchemy.orm.registry()路由请求,以便与其他声明性映射明确集成。

这种新方法与第三方类仪器系统相互操作,这些系统必须在映射过程之前对类进行必要的操作,允许声明性映射通过装饰器而不是声明性基础工作,以便像dataclassesattrs这样的包可以与声明性映射一起使用,除了与经典映射一起使用。

声明文档现在已完全集成到 ORM 映射器配置文档中,并包括对所有样式映射的示例,组织到一个地方。请参阅 ORM 映射类概述部分,开始新的重新组织的文档。

另请参阅

ORM 映射类概述

Python Dataclasses、attrs 支持声明性、命令式映射

#5508 ### 使用 Python Dataclasses、attrs 支持声明性、命令式映射

随着声明性现在已经与新特性集成到 ORM 中的新装饰器样式,Mapper现在明确地了解 Python 的dataclasses模块,并将识别配置为此方式的属性,并继续映射它们,而不是像以前那样跳过它们。对于attrs模块,attrs已经从类中删除了自己的属性,因此已经与 SQLAlchemy 的经典映射兼容。通过添加registry.mapped()装饰器,两种属性系统现在也可以与声明性映射互操作。

另请参阅

将 ORM 映射应用于现有的数据类(传统数据类使用)

使用命令式映射映射预先存在的数据类

#5027 ### 核心和 ORM 的异步 IO 支持

SQLAlchemy 现在支持使用全新的异步 IO 前端接口来使用 Python asyncio兼容的数据库驱动程序,用于 Core 使用的Connection以及用于 ORM 使用的Session,使用AsyncConnectionAsyncSession对象。

注意

新的 asyncio 功能应该被视为alpha 级别,适用于 SQLAlchemy 1.4 的初始版本。这是一些使用了一些以前不熟悉的编程技术的全新东西。

初始支持的数据库 API 是 asyncpg 用于 PostgreSQL 的 asyncio 驱动程序。

SQLAlchemy 的内部功能完全集成了greenlet库,以便调整执行流程,从数据库驱动程序向最终用户 API 传播 asyncio await 关键字,该 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类支持。

ORM 功能,如延迟加载相关属性以及过期属性的取消,根据定义在传统的 asyncio 编程模型中是不允许的,因为它们表示会在 Python getattr() 操作的范围内隐式运行的 IO 操作。为了克服这一点,传统的 asyncio 应用程序应该巧妙地利用 eager loading 技术,并放弃使用诸如 expire on commit 之类的功能,以便不需要这样的加载。

对于选择与传统决裂的 asyncio 应用程序开发人员,新的 API 提供了一个严格可选的功能,使希望利用此类 ORM 功能的应用程序可以选择将与数据库相关的代码组织成函数,然后使用 AsyncSession.run_sync() 方法在 greenlets 中运行。查看 Asyncio Integration 中的 greenlet_orm.py 示例以进行演示。

还提供了对异步游标的支持,使用新方法 AsyncConnection.stream()AsyncSession.stream(),支持一个新的 AsyncResult 对象,该对象本身提供了常见方法的可等待版本,如 AsyncResult.all()AsyncResult.fetchmany()。核心和 ORM 都与传统 SQLAlchemy 中“服务器端游标”的使用对应的功能集成在一起。

另请参阅

异步 I/O(asyncio)

Asyncio Integration

#3414 ### 许多核心和 ORM 语句对象现在在编译阶段执行大部分构建和验证操作

1.4 系列中的一个重要举措是接近核心 SQL 语句和 ORM  查询的模型,以实现高效、可缓存的语句创建和编译模型,其中编译步骤将被缓存,基于创建的语句对象生成的缓存键,该对象本身为每次使用新创建。为实现这一目标,特别是在构建语句时发生的大部分  Python 计算,特别是 ORM Queryselect() 构造在用于调用 ORM 查询时,正在移动到语句的编译阶段中,该阶段仅在调用语句后发生,且仅在语句的编译形式尚未被缓存时才会发生。

从最终用户的角度来看,这意味着基于传递给对象的参数可能引发的某些错误消息将不再立即引发,而是仅在首次调用语句时发生。这些条件始终是结构性的,而不是数据驱动的,因此不会因为缓存语句而错过这种条件。

属于此类别的错误条件包括:

  • 当构造_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参数。这会让代码检查工具看到函数缺少参数而感到困惑。已经内部实现了一种新方法,使函数的签名不再被修改,而是在函数内部获取模块对象。

#4656

#4689 ### 支持 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 实现注意事项

#1390 ### SQLAlchemy 2.0 弃用模式

1.4 版本的主要目标之一是提供一个“过渡”版本,以便应用程序可以逐渐迁移到 SQLAlchemy 2.0。为此,1.4  版本的一个主要特性是“2.0 弃用模式”,这是一系列针对每个可检测到的 API 模式发出的弃用警告,在 2.0  版本中将以不同方式工作。所有警告都使用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()方法。

这一变化的理由如下:

  • 为了支持SelectQuery的统一,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")

#4617 ### select().join()和 outerjoin()将 JOIN 条件添加到当前查询,而不是创建子查询

为了实现 2.0 风格对Select的使用,特别是统一QuerySelect的目标,关键是有一个工作的Select.join()方法,其行为类似于Query.join()方法,向现有 SELECT 的 FROM 子句添加额外条目,然后返回新的Select对象以进行进一步修改,而不是将对象包装在未命名的子查询中并从该子查询返回 JOIN,这种行为对用户来说一直是几乎无用和完全误导的。

为了实现这一点,不再将 SELECT 语句隐式视为 FROM 子句首先实现了这一点,将Select从必须是FromClause中分离出来;这消除了Select.join()需要返回一个Join对象而不是包含新 JOIN 的 FROM 子句的新版本Select对象的要求。

从那时起,由于Select.join()Select.outerjoin()具有现有行为,最初的计划是这些方法将被弃用,并且这些方法的新“有用”版本将在一个备用的“未来”Select对象上作为单独的导入可用。

然而,在与这个特定代码库一段时间后,决定有两种不同类型的Select对象漂浮在周围,每个对象的行为几乎相同,只是某些方法的行为略有不同,这将比简单地改变这两种方法的行为更具误导性和不便,因为Select.join()Select.outerjoin() 的现有行为基本上从未被使用,只会引起混乱。

因此,决定在这个领域做出严格的行为改变,而不是等待另一年并在此期间拥有更尴尬的  API,考虑到当前行为是多么无用,新行为将会是多么极其有用和重要。SQLAlchemy  开发人员并不轻易做出像这样完全破坏性的改变,然而这是一个非常特殊的情况,以前的这些方法实现几乎不太可能被使用;正如在 SELECT  语句不再隐式视为 FROM 子句 中所指出的,主要数据库如 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")

对于编程构造,如果代码可能直接使用URL构造函数或__init__方法,如果参数作为关键字参数而不是精确的 7 元组传递,将收到弃用警告。现在可以通过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)
postgresl://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() 构造现在将接受“columns clause”参数作为位置参数,而不需要将它们作为列表传递:

# 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, ...])

上述传统调用风格还接受自那时起已从大多数叙述文档中删除的旧关键字参数。这些关键字参数的存在是为什么首先将 columns clause 作为列表传递的原因:

# 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 风格的“future” 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",
    )
)

对于 SQLAlchemy 构造函数接受*args与接受值列表的约定,例如ColumnOperators.in_()这样的构造函数,位置参数用于结构规范,列表用于数据规范

另请参阅

select()不再接受各种构造函数参数,列按位置传递

在“遗留”模式中创建的 select()构造函数;关键字参数等。

#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 代码检查将警告任何 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")
)

上面的查询从Useraddress_alias的 JOIN 中选择,后者是Address实体的别名。然而,Address实体在 WHERE 子句中直接使用,因此上述将导致 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 元素与另一个元素链接起来。

多年来,维基上有一篇关于应用图算法到查询执行时的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

完整的笛卡尔积也是允许的,如果明确说明;例如,如果我们想要UserAddress的笛卡尔积,我们可以在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 开始就存在的ResultProxyRowProxy对象的新版本。

新对象的文档位于ResultRow,不仅用于核心结果集,还用于 ORM 中的 2.0 风格结果。

此结果对象与ResultProxy完全兼容,并包括许多新功能,现在对核心和 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_)

在使用核心时,由Connection.execute()返回的对象是CursorResult的实例,其继续具有与ResultProxy相同的 API 功能,关于插入的主键、默认值、行数等。对于 ORM,将返回Result的子类,执行核心行到 ORM 行的转换,然后允许进行所有相同的操作。

另请参见

ORM 查询与核心选择统一 - 在 2.0 迁移文档中

#5087

#4395

#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的内部行为变化不同,RowProxy中的结果行处理函数将在访问行的元素时被调用,而不是在首次获取行时被调用。这意味着例如从 SQLite 检索日期时间值时,以前在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 中的RowProxyRow对象是大部分 SQLAlchemy 的 C 扩展代码发生的地方。这些代码已经经过高度重构,以有效地提供新的行为,并且整体性能已经得到改善,因为Row的设计现在相当简单。

之前行为背后的理念假设了一个结果行可能有几十甚至几百列存在的使用模型,其中大多数列不会被访问,并且其中大多数列需要一些结果值处理函数。通过仅在需要时调用处理函数,目标是不需要大量的结果处理函数,从而提高性能。

有许多原因导致上述假设不成立:

  1. 调用绝大多数行处理函数是为了将字节字符串解码为 Python Unicode 字符串,在 Python 2 下。这是因为  Python Unicode 开始被使用并且在 Python 3 存在之前。一旦引入了 Python 3,在几年内,所有 Python  DBAPIs 都开始正确地支持直接传递 Python Unicode 对象,在 Python 2 和 Python 3  下都是如此,在前一种情况下是作为选项,在后一种情况下是唯一的前进方式。最终,在大多数情况下,它也成为了 Python 2  的默认选项。SQLAlchemy 的 Python 2 支持仍然支持一些 DBAPIs,比如 cx_Oracle,但现在是在 DBAPI  级别执行而不是作为标准 SQLAlchemy 结果行处理函数。
  2. 上述字符串转换,在使用时,通过 C  扩展被制作得非常高效,以至于即使在 1.4 版中,SQLAlchemy 的字节到 Unicode 编解码挂钩被插入到 cx_Oracle  中,观察到它比 cx_Oracle 自己的挂钩更高效;这意味着在任何情况下将所有字符串转换为行的开销都不像最初那样显着。
  3. 在大多数其他情况下不使用行处理函数;例外情况包括 SQLite 的日期时间支持,某些后端的 JSON 支持,一些数字处理程序例如字符串到 Decimal 的转换。在 Decimal 的情况下,Python 3 也标准化了高性能的 cdecimal 实现,而在 Python 2 中则继续使用性能远远不及的纯 Python 版本。
  4. 在实际使用案例中,很少会出现只需要少数列的情况在 SQLAlchemy 的早期,来自其他语言的数据库代码形式“row = fetch(‘SELECT * FROM table’)”很常见;然而,观察到的野外代码通常使用了需要的特定列的表达式语言。

另请参阅

查询返回的“KeyedTuple”对象已被“Row”替换

ORM 会话.execute() 在所有情况下都使用“future”风格的结果集

#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属性,以及在诸如SubqueryAlias等构造中看到的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>

为了适应ColumnCollection在诸如TablePrimaryKeyConstraint等对象中的使用,保留了旧的“去重”行为,这对于这些对象更为关键,它被保存在一个新的类DedupeColumnCollection中。

此更改包括删除了熟悉的警告"Column %r on table %r being replaced by %r, which has the same key. Consider use_labels for select() statements."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 或类似方法对简单列表达式进行列标记

有用户指出,当针对命名列使用类似 CAST 的函数时,PostgreSQL 数据库具有方便的行为,即结果列名与内部表达式相同:

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 的一部分,对于大多数核心 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 

对于没有名称的表达式,使用先前的逻辑来生成通常的“匿名”标签:

>>> print(select(cast("hi there," + foo.c.data, String)))
SELECT  CAST(:data_1  +  foo.data  AS  VARCHAR)  AS  anon_1
FROM  foo 

对于 Labelcast(),尽管必须省略标签表达式,因为这些表达式不会在 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 ### 新的用于 LIMIT/OFFSET 的 “后编译” 绑定参数在 Oracle、SQL Server 中使用

1.4 系列的一个主要目标是确保所有核心 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 参数,SQLAlchemy 将使用 FIRST_ROWS() 提示,这不允许它们,但也有报道称使用绑定参数与 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 公开,但目前不打算供一般使用。字面值是使用底层数据类型的 TypeEngine.literal_processor() 渲染的,在 SQLAlchemy 中具有极其有限的范围,仅支持整数和简单字符串值。

#4808 ### 基于子事务,现在可以根据连接级事务是否处于非活动状态

现在,Connection 包括了一个行为,即由于内部事务的回滚,Transaction 可以变为非活动状态,但是 Transaction 在自身被回滚之前不会清除。

这本质上是一种新的错误条件,如果内部“子”事务已回滚,则不允许在 Connection 上继续执行语句。该行为与 ORM Session 的行为非常相似,如果已启动外部事务,则需要回滚以清除无效事务;此行为在 “由于刷新期间的前一个异常,此会话的事务已回滚。”(或类似内容) 中有描述。

虽然 Connection 的行为模式比 Session 更宽松,但由于它有助于确定子事务何时回滚了 DBAPI 事务,但外部代码并不知道此事并尝试继续进行,实际上是在新事务上运行操作,因此进行了更改。在 将会话加入外部事务(例如用于测试套件) 中描述的“测试套件”模式是这种情况的普遍发生地点。

Core 和 ORM 的“子事务”功能本身已被弃用,并且在 2.0 版本中将不再存在。因此,这种新的错误条件本身是临时的,因为一旦删除子事务,它就不再适用。

为了使用不包括子事务的 2.0 样式行为,请在 create_engine() 上使用 create_engine.future 参数。

错误消息在错误页面中描述为 此连接处于非活动事务中。 请在继续之前完全回滚()。### 枚举和布尔数据类型不再默认为“创建约束”

Enum.create_constraintBoolean.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

新功能 - ORM

列的 Raiseload

“raiseload”功能会在访问未加载属性时引发InvalidRequestError,现在可以通过defer.raiseload参数来为基于列的属性提供支持。这与关系加载中使用的raiseload()选项的工作方式相同:

book = session.query(Book).options(defer(Book.summary, raiseload=True)).first()
# would raise an exception
book.summary

要在映射上配置列级 raiseload,可以使用deferred.raiseload参数来为deferred()。然后可以在查询时使用undefer()选项来急切加载属性:

class Book(Base):
    __tablename__ = "book"
    book_id = Column(Integer, primary_key=True)
    title = Column(String(200), nullable=False)
    summary = deferred(Column(String(2000)), raiseload=True)
    excerpt = deferred(Column(Text), raiseload=True)
book_w_excerpt = session.query(Book).options(undefer(Book.excerpt)).first()

最初考虑扩展现有的为relationship()属性工作的raiseload()选项,以支持基于列的属性。然而,这将破坏raiseload()的“通配符”行为,该行为被记录为允许阻止所有关系加载:

session.query(Order).options(joinedload(Order.items), raiseload("*"))

如果我们扩展了raiseload()以适应列,通配符也将阻止列加载,从而导致向后不兼容的更改;此外,不清楚raiseload()是否同时涵盖列表达式和关系,如何实现上述仅阻止关系加载的效果,而不添加新的 API。因此,为了保持简单,列的选项仍然在defer()上:

raiseload() - 查询选项,用于关系加载时引发异常

defer.raiseload - 查询选项,用于列表达式加载时引发异常

作为此更改的一部分,“deferred”与属性过期的行为已更改。以前,当对象被标记为过期,然后通过访问其中一个过期属性来取消过期时,映射为“deferred”的属性也会加载。现在已更改为映射中延迟的属性永远不会“取消过期”,只有在作为延迟加载器的一部分访问时才会加载。

一个未映射为“deferred”的属性,但在查询时通过defer()选项延迟,当对象或属性过期时将被重置;也就是说,延迟选项被移除。这与以前的行为相同。

另请参阅

使用 raiseload 防止延迟列加载

#4826 ### ORM Batch inserts with psycopg2 now batch statements with RETURNING in most cases

psycopg2 方言特性“execute_values”现在默认为 INSERT 语句添加 RETURNING,在 Core 中同时支持“executemany” + “RETURNING”,现在默认情况下使用 psycopg2 的 execute_values() 扩展为 psycopg2 方言启用。ORM 刷新过程现在利用此功能,以便在不丢失能够将 INSERT 语句批处理在一起的性能优势的同时实现新生成的主键值和服务器默认值的检索。此外,psycopg2 的 execute_values() 扩展本身通过将一个 INSERT 语句重写为包含许多“VALUES”表达式的单个语句而不是重复调用相同语句,提供了五倍的性能改进,因为 psycopg2 缺乏预先准备语句的能力,这通常是为了使这种方法具有高性能而预期的。

SQLAlchemy 在其示例中包含一个性能套件,我们可以比较“batch_inserts”运行程序在 1.3 和 1.4 中生成的时间,显示大多数批量插入的速度提升了 3 倍至 5 倍:

# 1.3
$ python -m examples.performance bulk_inserts --dburl postgresql://scott:tiger@localhost/test
test_flush_no_pk : (100000 iterations); total time 14.051527 sec
test_bulk_save_return_pks : (100000 iterations); total time 15.002470 sec
test_flush_pk_given : (100000 iterations); total time 7.863680 sec
test_bulk_save : (100000 iterations); total time 6.780378 sec
test_bulk_insert_mappings :  (100000 iterations); total time 5.363070 sec
test_core_insert : (100000 iterations); total time 5.362647 sec
# 1.4 with enhancement
$ python -m examples.performance bulk_inserts --dburl postgresql://scott:tiger@localhost/test
test_flush_no_pk : (100000 iterations); total time 3.820807 sec
test_bulk_save_return_pks : (100000 iterations); total time 3.176378 sec
test_flush_pk_given : (100000 iterations); total time 4.037789 sec
test_bulk_save : (100000 iterations); total time 2.604446 sec
test_bulk_insert_mappings : (100000 iterations); total time 1.204897 sec
test_core_insert : (100000 iterations); total time 0.958976 sec

注意,execute_values() 扩展会修改在 psycopg2 层中由 SQLAlchemy 记录的 INSERT 语句之后。因此,在 SQL 记录中,可以看到参数集被批处理在一起,但多个“values”的连接在应用程序端不可见:

2020-06-27 19:08:18,166 INFO sqlalchemy.engine.Engine INSERT INTO a (data) VALUES (%(data)s) RETURNING a.id
2020-06-27 19:08:18,166 INFO sqlalchemy.engine.Engine [generated in 0.00698s] ({'data': 'data 1'}, {'data': 'data 2'}, {'data': 'data 3'}, {'data': 'data 4'}, {'data': 'data 5'}, {'data': 'data 6'}, {'data': 'data 7'}, {'data': 'data 8'}  ... displaying 10 of 4999 total bound parameter sets ...  {'data': 'data 4998'}, {'data': 'data 4999'})
2020-06-27 19:08:18,254 INFO sqlalchemy.engine.Engine COMMIT

可以通过在 PostgreSQL 端启用语句记录来查看最终的 INSERT 语句:

2020-06-27 19:08:18.169 EDT [26960] LOG:  statement: INSERT INTO a (data)
VALUES ('data 1'),('data 2'),('data 3'),('data 4'),('data 5'),('data 6'),('data
7'),('data 8'),('data 9'),('data 10'),('data 11'),('data 12'),
... ('data 999'),('data 1000') RETURNING a.id
2020-06-27 19:08:18.175 EDT
[26960] LOG:  statement: INSERT INTO a (data) VALUES ('data 1001'),('data
1002'),('data 1003'),('data 1004'),('data 1005 '),('data 1006'),('data
1007'),('data 1008'),('data 1009'),('data 1010'),('data 1011'), ...

该功能默认将行分组为每组 1000 行,可以使用文档中记录的 executemany_values_page_size 参数来影响。

#5263 ### ORM 批量更新和删除在可用时使用 RETURNING 作为“fetch”策略

使用“fetch”策略的 ORM 批量更新或删除:

sess.query(User).filter(User.age > 29).update(
    {"age": User.age - 10}, synchronize_session="fetch"
)

现在如果后端数据库支持,将使用 RETURNING;目前包括 PostgreSQL 和 SQL Server(Oracle 方言不支持返回多行):

UPDATE users SET age_int=(users.age_int - %(age_int_1)s) WHERE users.age_int > %(age_int_2)s RETURNING users.id
[generated in 0.00060s] {'age_int_1': 10, 'age_int_2': 29}
Col ('id',)
Row (2,)
Row (4,)

对于不支持返回多行的后端,仍然使用先前的方法在事先发出主键的 SELECT:

SELECT users.id FROM users WHERE users.age_int > %(age_int_1)s
[generated in 0.00043s] {'age_int_1': 29}
Col ('id',)
Row (2,)
Row (4,)
UPDATE users SET age_int=(users.age_int - %(age_int_1)s) WHERE users.age_int > %(age_int_2)s
[generated in 0.00102s] {'age_int_1': 10, 'age_int_2': 29}

这种变化的一个复杂挑战之一是支持水平分片扩展等情况,其中单个批量更新或删除可能在一些支持 RETURNING  的后端之间复用,而另一些则不支持。新的 1.4 执行架构支持这种情况,以便“fetch”策略可以保持不变,优雅地降级到使用  SELECT,而不是必须添加一个不具备后端通用性的新“returning”策略。

作为这一变化的一部分,“fetch”策略也变得更加高效,它不再使与匹配行对应的对象过期,对于可以在 Python 中求值的用于 SET  子句的 Python 表达式;相反,这些直接分配到对象上,就像“evaluate”策略一样。只有对于无法求值的 SQL  表达式,它才会退回到使属性过期。对于无法求值的值,“evaluate”策略也已经增强为退回到“expire”。


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

相关文章
|
10天前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(七十二)(5)
SqlAlchemy 2.0 中文文档(七十二)
15 1
|
10天前
|
SQL Oracle 关系型数据库
SqlAlchemy 2.0 中文文档(七十)(3)
SqlAlchemy 2.0 中文文档(七十)
14 1
|
10天前
|
SQL Oracle 关系型数据库
SqlAlchemy 2.0 中文文档(七十)(5)
SqlAlchemy 2.0 中文文档(七十)
12 1
|
10天前
|
SQL Oracle 关系型数据库
SqlAlchemy 2.0 中文文档(七十)(1)
SqlAlchemy 2.0 中文文档(七十)
11 1
|
10天前
|
SQL Oracle 关系型数据库
SqlAlchemy 2.0 中文文档(七十)(4)
SqlAlchemy 2.0 中文文档(七十)
11 1
|
6天前
|
SQL 数据库 数据安全/隐私保护
SqlAlchemy 2.0 中文文档(五十)(6)
SqlAlchemy 2.0 中文文档(五十)
12 0
|
6天前
|
SQL 数据库 数据安全/隐私保护
SqlAlchemy 2.0 中文文档(五十)(3)
SqlAlchemy 2.0 中文文档(五十)
9 0
|
6天前
|
JSON 数据库 数据格式
SqlAlchemy 2.0 中文文档(五十)(5)
SqlAlchemy 2.0 中文文档(五十)
10 0
|
6天前
|
SQL 算法 数据库
SqlAlchemy 2.0 中文文档(五十)(1)
SqlAlchemy 2.0 中文文档(五十)
12 0
|
6天前
|
存储 JSON 数据格式
SqlAlchemy 2.0 中文文档(五十)(2)
SqlAlchemy 2.0 中文文档(五十)
8 0