目录

SQLAlchemy 1.4 有什么新功能?

优质
小牛编辑
131浏览
2023-12-01

关于此文档

本文档描述了SQLAlchemy版本1.3和SQLAlchemy版本1.4之间的更改。

与其他SQLAlchemy版本相比,1.4版本的重点有所不同,因为它在许多方面都试图作为一个潜在的迁移点,为当前计划用于SQLAlchemy 2.0版的一系列更为引人注目的API更改提供支持。sqlAlchemy 2.0的重点是一个现代化和精简的API,它删除了许多长期以来不受欢迎的使用模式,并将sqlAlchemy中的最佳思想作为一流的API功能主流化,其目标是在如何使用API方面不存在太多的歧义,此外,还将删除一系列隐式行为和很少使用的使内部复杂化和妨碍性能的API标志。

有关SQLAlchemy 2.0的当前状态,请参见 迁移到Alchemy .

API的主要变化和特性-概述

Python3.6是Python3的最低版本;Python2.7仍然受支持

随着Python3.5在2020年9月达到EOL,SQLAlchemy 1.4现在将3.6版作为Python3的最低版本。Python2.7仍然受支持,但是SQLAlchemy 1.4系列将是支持Python2的最后一个系列。

ORM查询内部统一,支持select、update、delete;2.0风格执行

SQLAlchemy在2.0版和1.4版中最大的概念变化是 Select 核心构造和 Query 对象已被移除,以及 Query.update()Query.delete() 方法如何与 UpdateDelete .

关于 SelectQuery ,这两个对象在许多版本中都具有相似的、很大程度上重叠的api,甚至可以在一个和另一个之间进行更改,但在使用模式和行为方面仍然存在很大差异。历史背景是 Query 对象的引入是为了克服 Select 对象,它曾经是ORM对象查询的核心,但是它们必须根据 Table 仅元数据。然而 Query 它只有一个用于加载对象的简单接口,并且只有在许多主要版本中,它才最终获得了 Select 对象,这导致了持续的尴尬,这两个对象变得高度相似,但仍然基本上彼此不兼容。

在版本1.4中,所有Core和ORM SELECT语句都是从 Select 直接反对;当 Query 对象,在语句调用时将其状态复制到 Select 然后使用 2.0 style 执行。向前看 Query 对象将仅成为遗留对象,并且将鼓励应用程序移动到 2.0 style 允许对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)

关于上述示例需要注意的事项:

在整个SQLAlchemy的文档中,将有许多引用 1.x style2.0 style 执行。这是为了区分这两种查询样式,并尝试向前转发新调用样式的文档。在SQL2中 Query 对象可能保留为遗留构造,它将不再是大多数文档中的特征。

对“批量更新和删除”进行了类似的调整,使核心 update()delete() 可用于批量操作。批量更新如下所示:

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

现在可以在 2.0 style (实际上,上面的内容是这样的)如下所示:

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中更加普遍,许多与ORM相关的方法 Query 执行(请参见 Query.execution_options() 一些例子)。

参见

迁移到Alchemy

#5159

透明的SQL编译缓存添加到Core、ORM中的所有DQL、DML语句中

在一个SQLAlchemy版本中,对所有查询系统进行了长达数月的重组和重构,从Core的基础一直到ORM,现在大多数Python计算都涉及到生成SQL字符串和相关的语句元数据,从用户构造的语句到缓存在内存中,这样以后调用相同的语句结构将使用35-60%的CPU资源。

这种缓存超出了SQL字符串的构造,还包括将SQL构造链接到结果集的结果获取结构的构造,并且在ORM中,它包括支持ORM的属性加载器、关系渴望加载器和其他选项,以及必须建立的对象构造例程每次ORM查询从结果集运行和构造ORM对象时。

为了介绍这个特性的一般思想,给出了 性能 它将调用一个非常简单的查询“n”次,默认值为n=10000。查询只返回一行,因为我们希望减少的开销是 许多小查询 . 对于返回许多行的查询,优化没有那么重要:

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 lambdas来延迟查询本身的构造。这是“Baked Query”扩展所使用的方法的一个更复杂的变体,它是在版本1.0.0中引入的。“lambda”特性可以用与baked查询非常相似的样式使用,只是它以特别的方式可用于任何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查询,结合缓存整个构造的完整“烘焙”样式调用,可以在 快60% 并授予的性能与baked查询系统大致相同,后者现在已被本机缓存系统取代。

新系统利用现有的 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 包括:

此外 instrument_declarative() 函数已弃用,被取代 registry.map_declaratively() . 这个 ConcreteBaseAbstractConcreteBaseDeferredReflection 类作为扩展保留在 声明性扩展 包裹。

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

现有的经典映射函数 mapper() 仍然存在,但不赞成调用 mapper() 直接的;新的 registry.map_imperatively() 方法现在通过 sqlalchemy.orm.registry() 这样它就可以明确地与其他声明性映射集成。

新方法与第三方类检测系统进行互操作,这些系统必须在映射过程之前发生在类上,允许声明性映射通过修饰符而不是声明性基来工作,从而使包 dataclassesattrs 除了使用经典映射之外,还可以与声明性映射一起使用。

声明性文档现在已经完全集成到ormmapper配置文档中,并包括组织到一个位置的所有映射样式的示例。参见章节 映射Python类 对于新重组文档的开始。

参见

映射Python类

Python数据类,attr支持,带有声明性、命令式映射

#5508

Python数据类,attr支持,带有声明性、命令式映射

以及 声明式现在集成到ORM中,具有新的特性 , the Mapper 现在明确知道Python dataclasses 模块和将识别以这种方式配置的属性,并继续映射它们,而不会像以前那样跳过它们。如果 attrs 模块, attrs 已经从类中删除了它自己的属性,因此已经与SQLAlchemy经典映射兼容。加上 registry.mapped() decorator,这两个属性系统现在也可以与声明性映射进行互操作。

参见

具有数据类和属性的声明性映射

具有数据类和属性的命令式映射

#5027

对内核和ORM的异步IO支持

SQLAlchemy现在支持Python asyncio -兼容的数据库驱动程序使用全新的异步前端接口 Connection 用于核心用途以及 Session 对于ORM使用,使用 AsyncConnectionAsyncSession 物体。

注解

应该考虑新的异步特性 α水平 对于SQLAlchemy 1.4的初始版本。这是一个全新的东西,使用了一些以前不熟悉的编程技术。

支持的初始数据库API是 异步PG PostgreSQL的异步驱动程序。

SQLAlchemy的内部特性通过使用 greenlet 库来调整SQLAlchemy内部的执行流来传播异步 await 关键字从数据库驱动程序向外扩展到最终用户API,其中 async 方法。使用这种方法,asyncpg驱动程序可以在SQLAlchemy自己的测试套件中完全运行,并且具有与大多数psycopg2特性兼容的特性。greenlet项目的开发人员对该方法进行了审查和改进,SQLAlchemy对此表示赞赏。

面向用户 async API本身关注于面向IO的方法,例如 AsyncEngine.connect()AsyncConnection.execute() . 新核心结构严格支持 2.0 style 仅限用法;在本例中,这意味着必须调用给定连接对象的所有语句 AsyncConnection .

在ORM中, 2.0 style 支持查询执行,使用 select()AsyncSession.execute() ;遗产 Query 对象本身不受 AsyncSession 班级。

ORM特性(如延迟加载相关属性以及过期属性的未过期)在传统的asyncial编程模型中是不允许的,因为它们表示将在Python范围内隐式运行的IO操作 getattr() 操作。为了克服这个问题 传统的 异步应用程序应该明智地使用 eager loading 技术以及放弃使用诸如 expire on commit 这样就不需要这样的负荷了。

对于asyncio应用程序开发人员 选择打破 与传统API相比,新API提供了 严格可选功能 这样,希望使用这种ORM特性的应用程序可以选择将与数据库相关的代码组织成函数,然后可以使用 AsyncSession.run_sync() 方法。见 greenlet_orm.py 示例 异步集成 做个示范。

还使用新方法提供对异步游标的支持 AsyncConnection.stream()AsyncSession.stream() 一个新的支持 AsyncResult 对象,该对象本身提供了诸如 AsyncResult.all()AsyncResult.fetchmany() . Core和ORM都与传统SQLAlchemy中“服务器端游标”的使用相对应的特性集成在一起。

参见

异步I/O(异步)

异步集成

#3414

许多Core和ORM语句对象现在在编译阶段执行大部分构造和验证

1.4系列中的一个主要的创新点是接近核心SQL语句和ORM查询的模型,以便建立一个高效的、可缓存的语句创建和编译模型,其中编译步骤将基于created statement对象生成的缓存键进行缓存,后者本身是为每次使用而新创建的。为了实现这个目标,很多Python计算都发生在语句的构造中,尤其是ORM的构造中 Query 以及 select() 构造用于调用ORM查询时,将被移动到语句的编译阶段中发生,该阶段仅在调用语句之后发生,并且仅当语句的编译形式尚未缓存时发生。

从最终用户的角度来看,这意味着基于传递给对象的参数而产生的一些错误消息将不再立即引发,而是只在第一次调用语句时发生。这些条件总是结构化的,而不是数据驱动的,因此不存在由于缓存语句而丢失此类条件的风险。

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

  • 当A _selectable.CompoundSelect 是构造的(例如,UNION、EXCEPT等),并且传递的SELECT语句的列数不是相同的,a CompileError 现在提出了这个意思;以前,一个 ArgumentError 将在报表构造后立即提出。

  • 调用时可能出现的各种错误情况 Query.join() 将在语句编译时而不是第一次调用方法时计算。

其他可能发生变化的事情涉及 Query 直接对象:

  • 方法时,行为可能略有不同。 Query.statement 访问者。这个 Select 对象中返回的状态现在是同一状态的直接副本。 Query ,而无需执行任何特定于ORM的编译(这意味着它的速度要快得多)。然而, Select 将不具有与1.3中相同的内部状态,包括像FROM子句这样的内容,如果它们没有在 Query 。这意味着依赖于对此进行操作的代码 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步骤2-打开RemovedIn20警告 .

参见

迁移到Alchemy

迁移到2.0步骤2-打开RemovedIn20警告

API和行为变化-核心

select语句不再被隐式地视为FROM子句

这一变化是多年来SQL炼金术中较大的概念性变化之一,但希望最终用户的影响相对较小,因为在任何情况下,变化都更接近于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.

正确的调用形式是 brackets are no longer required for select() ):

sq1 = select(user.c.id, user.c.name).subquery()
stmt2 = select(addresses, sq1).select_from(addresses.join(sq1))

上面提到 SelectBase.subquery() 方法本质上等同于使用 SelectBase.alias() 方法。

这一变化的依据如下:

  • 为了支持统一 Select 具有 Query , the Select 对象需要有 Select.join()Select.outerjoin() 方法,这些方法实际上将连接条件添加到现有的FROM子句中,这是用户一直期望它在任何情况下执行的操作。以前的行为,必须与 FromClause 它将生成一个未命名的子查询,然后加入它,这是一个完全无用的功能,只会让那些不幸尝试此功能的用户感到困惑。有关此更改的讨论,请参阅 select().join()和outerjoin()将连接条件添加到当前查询,而不是创建子查询 .

  • 在另一个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;

AS SelectBase 对象不再是 FromClause 对象、属性,如 .c 属性和方法 .select() 现在已弃用,因为它们意味着隐式生成子查询。这个 .join().outerjoin() 方法现在 repurposed to append JOIN criteria to the existing query 以类似于 Query.join() ,这也是用户一直希望这些方法在任何情况下都能做到的。

代替 .c 属性,新属性 SelectBase.selected_columns 已添加。此属性解析为大多数人希望的列集合 .c 是(但不是),即引用select语句的columns子句中的列。初学者常犯的错误是以下代码:

stmt = select(users)
stmt = stmt.where(stmt.c.name == 'foo')

上面的代码看起来很直观,它会生成“select * FROM users WHERE name='foo'", however veteran SQLAlchemy users will recognize that it in fact generates a useless subquery resembling "SELECT * 从(select*from users)其中name='foo'”。

新的 SelectBase.selected_columns 但是属性 does 适合上面的用例,与上面类似的情况一样,它直接链接到 users.c 收藏:

stmt = select(users)
stmt = stmt.where(stmt.selected_columns.name == 'foo')

#4617

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

朝着统一的目标前进 QuerySelect 尤其是 2.0 style 使用 Select ,这是至关重要的,有一个工作 Select.join() 方法的行为类似于 Query.join() 方法,向现有SELECT的FROM子句添加其他条目,然后返回新的 Select 对象进行进一步的修改,而不是将对象包装在未命名的子查询中并从该子查询返回联接,这种行为实际上一直是无用的,并且完全误导用户。

允许这种情况发生, select语句不再被隐式地视为FROM子句 首先实现了分离 Select 从必须成为 FromClause ;这就取消了 Select.join() 需要返回一个 Join 而不是一个新版本 Select 在其FROM子句中包含新联接的。

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

然而,在使用这个特定的代码库一段时间后,决定使用两种不同的 Select 每个对象都有95%相同的行为,除了一些方法的行为方式有一些细微的差别之外,这些对象将比简单地改变这两个方法的行为方式更具误导性和不方便性 Select.join()Select.outerjoin() 基本上从未使用过,只会引起混乱。

所以我们决定,考虑到当前的行为是多么的无用,新的行为是多么的有用、重要和有用,我们决定 难以改变的行为 在这一领域,而不是再等一年,在过渡期间有一个更尴尬的API。SQLAlchemy开发人员不会轻易地做出这样一个完全破坏性的更改,但是这是一个非常特殊的情况,使用这些方法以前的实现是极不可能的;如中所述 select语句不再被隐式地视为FROM子句 ,主要的数据库如MySQL和PostgreSQL在任何情况下都不允许使用未命名的子查询,从语法的角度来看,从未命名的子查询中进行连接几乎不可能有用,因为很难明确地引用其中的列。

随着新的实施, Select.join()Select.outerjoin() 现在的行为和 Query.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 style 调用。ORM实体将在内部为语句分配一个“插件”,这样当语句被编译成SQL字符串时,与ORM相关的编译规则就会发生。更直接地说 Select.join() 方法可以适应ORM关系,而不会破坏核心和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 dictionary是一个特殊的字典,它在调用任何类型的标准库变异方法时发出弃用警告,此外,当字典将保存任何类型的元素列表时,该列表还必须针对已经发生变化的项目的不太可能的用例发出弃用警告) URL 对象,以及诸如 #5341 在任何情况下都会造成向后不兼容。a基因突变的原发病例 URL 对象中解析插件参数的对象 CreateEnginePlugin extensionpoint本身是最近添加的一个基于Github代码搜索的新功能,两个存储库都在使用,这两个库实际上都没有改变URL对象。

这个 URL 对象现在提供了一个丰富的接口来检查和生成新的 URL 物体。现有机制来创建 URL 对象 make_url() 功能,保持不变:

>>> 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

要升级直接改变这些字段的代码,一个 前后兼容方法 是使用duck类型,如下所示:

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)
postgresql://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"]
        )

查看docstring CreateEnginePlugin 有关如何使用该类的完整详细信息。

#5526

select(),case()现在接受位置表达式

如本文件其他部分所述 select() 构造现在将按位置接受“columns子句”参数,而不是要求它们作为列表传递:

# 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子句首先作为列表传递的原因:

# 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 construct还获得了2.0风格的“未来”API,其中包括一个更新的 Select.join() 方法以及类似的方法 Select.filter_by()Select.join_from() .

在相关变更中, case() construct也被修改为以位置方式接受WHEN子句的列表,对于旧的调用样式具有类似的弃用跟踪:

stmt = select(users_table).where(
    case(
        (users_table.c.name == 'wendy', 'W'),
        (users_table.c.name == 'jack', 'J'),
        else_='E'
    )
)

SQLAlchemy构造的约定接受 *args vs.一个值列表,对于类似于 ColumnOperators.in_() ,是吗 位置参数用于结构规范,列表用于数据规范 .

参见

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

在“传统”模式下创建的select()构造;关键字参数等。

#5284

All-IN表达式动态呈现列表中每个值的参数(例如展开参数)

在中首次引入的“扩展”功能 参数集后期扩展允许在具有缓存语句的表达式中使用 ,已经足够成熟,明显优于以前的表达式呈现方法。由于该方法已改进为处理空值列表,因此它现在是Core/ORM用来呈现IN参数列表的唯一方法。

SQLAlchemy自第一次发布以来一直存在的前一种方法是,当一个值列表传递给 ColumnOperators.in_() 方法,则将列表展开为一系列单独的 BindParameter 对象在语句构造时。这受到了一个限制,即不能根据参数字典在语句执行时改变参数列表,这意味着字符串SQL语句不能独立于其参数进行缓存,参数字典也不能完全用于表达式中通常包含的语句。

为了提供中所述的“烘焙查询”功能 烘焙查询 ,需要一个IN的可缓存版本,这就是“扩展IN”特性的原因。与现有的行为不同的是,参数列表在语句构造时被扩展为单独的 BindParameter 对象,则该功能使用单个 BindParameter 一次存储值列表;当语句由 Engine ,它会根据传递给调用的参数动态“扩展”到单个绑定参数位置 Connection.execute() ,并使用正则表达式修改可能已从先前执行中检索到的现有SQL字符串,以适合当前参数集。这也考虑到了这一点 Compiled 对象,该对象存储呈现的字符串语句,将针对不同的参数集调用多次,这些参数集修改在表达式中传递给的列表内容,同时仍保持传递给DBAPI的单个标量参数的行为。虽然有些dbapi确实直接支持此功能,但它通常不可用;“扩展”特性现在支持所有后端的一致行为。

由于1.4的一个主要焦点是允许在Core和ORM中进行真正的语句缓存,而不会出现“baked”系统的尴尬,而且由于“expanding 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)

作为此更改的一部分,“empty-IN”表达式(其中list参数为空)的行为现在在针对所谓的“empty set”使用IN运算符时得到了标准化。由于空集没有标准的SQL语法,因此使用了一个不返回行的SELECT,它以特定的方式为每个后端定制,以便数据库将其视为空集;此功能在版本1.3中首次引入,并在中进行了描述 扩展功能现在支持空列表 . 这个 create_engine.empty_in_strategy 参数在版本1.2中引入,作为迁移系统中以前的实例处理方式的方法,现在已弃用,此标志不再有效;如中所述 现在可以配置in/not in运算符的空集合行为;简化了默认表达式 ,这个标志允许方言在原始的比较列和自身的系统之间切换,结果发现这是一个巨大的性能问题,而一个更新的比较“1!=1“,以产生“假”表达式。现在在所有情况下发生的1.3引入的行为比这两种方法都更正确,因为仍然使用in操作符,并且没有原始系统的性能问题。

此外,“扩展”参数系统已被通用化,因此它还可以为DBAPI或后台数据库无法容纳参数的其他方言特定用例提供服务;请参阅 新的“后编译”绑定参数用于Oracle、SQL Server中的限制/偏移 有关详细信息。

参见

新的“后编译”绑定参数用于Oracle、SQL Server中的限制/偏移

扩展功能现在支持空列表

BindParameter

#4645

从linting内置将在SELECT语句中警告任何潜在的笛卡尔积

由于核心表达式语言和ORM都建立在一个“隐式FROMs”模型上,如果查询的任何部分引用了一个特定的FROM子句,那么一个常见的问题是SELECT语句,无论是顶级语句还是嵌入式子查询,包含未联接到查询中其余FROM元素的FROM元素,导致在结果集中出现所谓的“笛卡尔积”,即每个FROM元素中的所有可能的行组合,而不是以其他方式联接。在关系数据库中,这几乎总是一个不理想的结果,因为它会产生大量重复的、不相关的数据的结果集。

尽管SQLAlchemy有很多优秀的特性,但它特别容易发生这种问题,因为SELECT语句会自动从其他子句中看到的任何表中添加元素到FROM子句中。一个典型的场景如下所示,其中两个表被连接在一起,但是where子句中的一个附加条目可能无意中没有与这两个表对齐,它将创建一个附加的FROM条目:

address_alias = aliased(Address)

q = session.query(User).\
    join(address_alias, User.addresses).\
    filter(Address.email_address == 'foo')

上面的查询从 Useraddress_alias ,后者是 Address 实体。但是 Address entity直接在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”表中的每一行的笛卡尔积中。也就是说,如果地址中有10个用户行和100个行,上面的查询将返回其预期的结果行,很可能是100,因为将选择所有地址行,再乘以100,因此总结果大小将为10000行。

“table1,table2-JOIN-table3”模式在SQLAlchemy ORM中也经常出现,这是由于ORM特性(尤其是与joined-eager-loading或joined表继承相关的特性)的细微错误应用,以及这些系统中SQLAlchemy ORM错误的结果。类似的问题也适用于使用“隐式联接”的SELECT语句,其中不使用JOIN关键字,而是通过where子句将每个FROM元素与另一个元素链接起来。

几年来,Wiki上有一个诀窍,它将图形算法应用于 select() 在查询执行时构造并检查这些未链接的FROM子句的查询结构,通过WHERE子句和all 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

笛卡尔积警告考虑 any 两个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() 这样,每一行都将相互匹配;下面的查询将返回所有行而不生成警告:

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

新对象结果

sqlalchemy2.0的一个主要目标是统一ORM和核心之间如何处理“结果”。为了实现这个目标,版本1.4引入了两个 ResultProxyRowProxy 从一开始就成为SQLAlchemy的一部分的对象。

新对象记录在 ResultRow ,和不仅用于核心结果集,而且用于 2.0 style ORM中也有结果。

此结果对象与完全兼容 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_)

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

参见

与Core Select统一的ORM查询 -在2.0迁移文档中

#5087

#4395

#4959

RowProxy不再是“代理”;现在称为Row,其行为类似于增强的命名元组

这个 RowProxy 类,它表示核心结果集中的单个数据库结果行,现在调用 Row 并且不再是“代理”对象;这意味着当 Row 对象时,该行是一个简单的元组,它包含最终形式的数据,已经由与数据类型相关联的结果行处理函数处理(例如,将数据库中的日期字符串转换为 datetime 对象,将JSON字符串转换为Python json.loads() 结果等)。

这样做的直接原因是,行可以更像一个名为tuple的Python,而不是映射,其中tuple中的值是 __contains__ 元组上的运算符,而不是键。与 Row 它的作用类似于命名元组,因此适合用作ORM的替换 KeyedTuple 对象,这将导致一个最终的API,其中ORM和核心都提供行为相同的结果集。ORM和Core中主要模式的统一是sqlalchemy2.0的一个主要目标,而版本1.4的目标是将大部分或所有底层架构模式都准备就绪,以支持这个过程。在 查询返回的“KeyedTuple”对象被行替换 描述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的result对象将具有 .mappings() 这样就可以直接接收这些映射了:

# using sqlalchemy.future package
for row in result.mappings():
    row["id"]

代理行为消失了,在现代使用中也是不必要的

的重构 Row 要像元组一样工作,需要所有的数据值预先完全可用。这是一个内部行为的改变 RowProxy ,其中结果行处理函数将在访问行的元素时调用,而不是在第一次提取行时调用。这意味着,例如,当从SQLite检索日期时间值时,该行的数据如 RowProxy 对象以前应该是这样的:

row_proxy = (1, '2019-12-31 19:56:58.272106')

然后通过 __getitem__ , the datetime.strptime() 函数将动态地将上述字符串日期转换为 datetime 对象。在新架构下 datetime() 当对象在tuple中时,返回它 datetime.strptime() 前面只调用了一次函数::

row = (1, datetime.datetime(2019, 12, 31, 19, 56, 58, 272106))

这个 RowProxyRow SQLAlchemy中的对象是SQLAlchemy的大部分C扩展代码发生的地方。此代码经过高度重构,以高效的方式提供新的行为,并且随着 Row 现在要简单得多。

前面的行为背后的基本原理是假设一个使用模型,其中一个结果行可能有几十个或几百个列,其中大多数列将不会被访问,并且其中大多数列需要一些结果值处理功能。通过只在需要时调用处理函数,目标是不需要很多结果处理函数,从而提高性能。

上述假设不成立的原因有很多:

  1. 调用的绝大多数行处理函数都是在python2下将bytestring解码为pythonunicode字符串。这是正确的,因为pythonunicode已经开始使用python3了。在引入python3之后,在几年内,所有Python dbapi都承担起了适当的角色,在python2和python3下直接支持pythonunicode对象的交付,在前一种情况下是一种选择,在后一种情况下是唯一的前进方向。最终,在大多数情况下,它也成为Python2的默认值。SQLAlchemy的Python2支持仍然支持某些DBAPI(如cx_Oracle)的字符串到Unicode的显式转换,但是现在它是在DBAPI级别执行的,而不是作为标准的SQLAlchemy结果行处理函数执行的。

  2. 上面的字符串转换在使用时,通过C扩展实现了非常高的性能,以至于即使在1.4版本中,SQLAlchemy的字节到Unicode编解码器钩子也被插入到了cx Oracle中,在那里它被观察到比cx®Oracle自己的钩子更具性能;这意味着,在任何情况下,转换一行中所有字符串的开销都不如原来那么大。

  3. 行处理函数在其他大多数情况下不使用;例外情况是SQLite的datetime支持、某些后端的JSON支持、一些数字处理程序(如string to) Decimal . 在下列情况下 Decimal ,Python3也在高性能 cdecimal 实现,而Python2则不是这样,它继续使用性能差得多的纯Python版本。

  4. 在SQLAlchemy早期的实际用例中,获取只需要少数列的完整行并不常见,来自其他语言的“row=fetch('SELECT*from table')”形式的数据库代码很常见;但是使用SQLAlchemy的表达式语言,在野外观察到的代码通常使用所需的特定列。

参见

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

#4710

select对象和派生自子句允许重复的列和列标签

这一变化使得 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

为了支持这一变化, ColumnCollection 被使用 SelectBase 以及派生自子句(如子查询)也支持重复列;这包括 SelectBase.selected_columns 属性,已弃用 SelectBase.c 属性,以及 FromClause.c 在诸如 SubqueryAlias ::

>>> 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 当字符串“key”不明确时,还允许通过整数索引访问以支持:

>>> 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仍将其用于所有选择操作,但是它并不意味着列对象的重复数据消除,尽管它确实意味着隐式生成的标签的重复数据消除:

>>> 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"

最后,这种变化使得创建联合和其他 _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)

这允许对表列应用强制转换,同时不丢失列名(上面使用该名称 "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核心表达式中,我们从不处理像上面这样的原始生成的名称,因为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 ,这依赖于结果行中的命名列(特别是 cursor.description (PEP-249游标的元素)被缩小到对于大多数核心SELECT构造来说都是不必要的;在版本1.4中,系统总体上对具有重复列或标签名称的SELECT语句(如in)越来越满意 select对象和派生自子句允许重复的列和列标签 . 因此,我们现在模拟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

A cast() 反对 Label ,尽管必须省略label表达式,因为这些表达式不会在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<name>”标签:

>>> 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

新的“后编译”绑定参数用于Oracle、SQL Server中的限制/偏移

1.4系列的一个主要目标是建立所有核心SQL构造都是完全可缓存的,这意味着 Compiled 结构将生成一个完全相同的SQL字符串,而不管它使用了什么SQL参数,其中特别包括那些用于指定限制和偏移值的参数,这些参数通常用于分页和“top N”样式的结果。

虽然SQLAlchemy多年来一直在限制/偏移量方案中使用绑定参数,但仍有一些不允许使用这些参数的异常值,包括SQL Server“TOP N”语句,例如:

SELECT TOP 5 mytable.id, mytable.data FROM mytable

与Oracle一样,其中的第一个_ROWS()提示(如果 optimize_limits=True 参数传递给 create_engine() 但也有报道称,使用绑定参数进行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

为了允许所有语句在编译级别无条件缓存,添加了一种新的绑定参数形式,称为“后编译”参数,它使用与“扩展参数”相同的机制。这是一个 bindparam() 它的行为与任何其他绑定参数的行为相同,但参数值将在发送给DBAPI之前按字面方式呈现为SQL字符串 cursor.execute() 方法。新参数由sqlserver和Oracle方言在内部使用,因此驱动程序接收到文本呈现的值,但是SQLAlchemy的其他成员仍然可以将其视为绑定参数。当使用 str(statement.compile(dialect=<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]

这个 [POSTCOMPILE_<param>] 格式也是使用“扩展”时看到的。

在查看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 参数,但当前不打算用于一般用途。使用 TypeEngine.literal_processor() 在SQLAlchemy中 极其有限 作用域,仅支持整数和简单字符串值。

#4808

连接级事务现在可以基于子事务处于非活动状态

A Connection 现在包括以下行为: Transaction 由于内部事务上的回滚,可以使其处于非活动状态,但是 Transaction 直到它自己被回滚时才会清除。

这实际上是一个新的错误条件,它将禁止语句执行继续执行 Connection 如果内部“子”事务已回滚。这种行为与ORM的行为非常相似。 Session ,其中,如果外部事务已开始,则需要回滚以清除无效事务;此行为在 “由于在刷新过程中出现以前的异常,此会话的事务已回滚。”(或类似) .

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

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

要使用不包含子事务的2.0样式行为,请使用 create_engine.future 参数对 create_engine() .

错误消息在的错误页中进行了描述 此连接处于非活动事务上。请在继续之前完全回滚()。 .

枚举和布尔数据类型不再默认为“创建约束”

这个 Enum.create_constraintBoolean.create_constraint 参数现在默认为False,表示在创建这两个数据类型的所谓“非本机”版本时,CHECK约束将 not 默认生成。这些检查约束呈现了模式管理维护的复杂性,应该选择使用,而不是默认打开。

要确保为这些类型发出创建约束,请将这些标志设置为 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

立柱的标高

“拉伊洛德”的特点 InvalidRequestError 当属性被卸载时,可以使用面向属性访问列 defer.raiseload 参数 defer() . 其工作方式与 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()

最初认为 raiseload() 适用于 relationship() 属性被扩展以支持面向列的属性。然而,这将打破“通配符”行为 raiseload() ,它被记录为允许阻止加载所有关系:

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

上面,如果我们扩张的话 raiseload() 为了适应列,通配符也会阻止列加载,因此是一个向后不兼容的更改;此外,不清楚是否 raiseload() 涵盖了列表达式和关系,以及如何在不添加新API的情况下实现上述仅阻塞关系加载的效果。所以为了简单起见,columns的选项仍然是开的 defer()

raiseload() -为关系加载引发的查询选项

defer.raiseload -为列表达式加载引发的查询选项

作为此更改的一部分,“deferred”与属性过期一起的行为发生了更改。以前,当一个对象被标记为过期,然后通过访问其中一个过期属性而未过期时,在映射器级别映射为“延迟”的属性也将被加载。这一点已经改变,映射中延迟的属性永远不会“未过期”,它只在作为延迟加载程序的一部分访问时加载。

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

参见

延迟列的Raiseload

#4826

ORM批插入与psycopg2现在批处理语句在大多数情况下返回

变化 psycopg2方言的特性是“execute_values”,默认情况下返回for INSERT语句 在Core中同时添加对“executemany”+“RETURNING”的支持,现在默认情况下使用psycopg2为psycopg2方言启用 execute_values() 扩展。ORM flush进程现在利用了这个特性,这样就可以实现对新生成的主键值和服务器默认值的检索,同时又不损失能够一起批插入语句的性能优势。另外,psycopg2的 execute_values() 与psycopg2的默认“executemany”实现相比,扩展本身提供了五倍的性能改进,方法是重写INSERT语句,将多个“值”表达式全部包含在一个语句中,而不是重复调用同一语句,因为psycopg2缺乏像通常那样提前准备语句的能力希望这种方法能够执行。

SQLAlchemy包括一个 performance suite 我们可以在第三批产品中插入第三批产品的第三批产品

# 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() extension修改psycopg2层中的INSERT语句, 之后 它被SQLAlchemy记录下来了。因此,使用SQL日志记录,可以看到参数集被批处理在一起,但多个“值”的连接在应用程序端将不可见:

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

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

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 参数记录在 Psycopg2快速执行助手 .

#5263

ORM批量更新和删除使用返回“获取”策略(如果可用)

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

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

如果后端数据库支持RETURNING,现在将使用RETURNING;这当前包括PostgreSQL和sqlserver(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}

这种变化带来的复杂挑战之一是支持水平分片扩展等情况,在这种情况下,单个批量更新或删除可以在后端之间进行多路复用,有些后端支持返回,而有些则不支持。新的1.4执行架构支持这种情况,这样“获取”策略可以保持原样,并保持优雅降级为使用SELECT,而不是必须添加一个新的“returning”策略,而不是后端不可知论。

作为此更改的一部分,“fetch”策略也变得更加高效,因为对于SET子句中使用的Python表达式(可以在Python中求值),它将不再使与行匹配的对象过期;相反,这些对象将以与“evaluate”策略相同的方式直接分配到对象上。只有对于无法求值的SQL表达式,它才会回到使属性过期的状态。“evaluate”策略也得到了增强,可以针对无法评估的值返回到“expire”。

行为改变-ORM

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

如上所述 RowProxy不再是“代理”;现在称为Row,其行为类似于增强的命名元组 核心 RowProxy 对象现在被一个名为 Row . 基地 Row 对象现在的行为更像命名元组,因此它现在被用作 Query 对象,而不是以前的“KeyedTuple”类。

其基本原理是,在sqlalchemy2.0中,Core和ormselect语句都将使用相同的方法返回结果行 Row 对象,其行为类似于命名元组。字典式功能可从 Row 通过 Row._mapping 属性。在此期间,核心结果集将使用 Row 子类 LegacyRow 它保持以前的dict/tuple混合行为以实现向后兼容性,而 Row 类将直接用于 Query 对象。

我们已经努力获得 Row 要在ORM中可用,这意味着通过字符串名和实体/列进行访问应该有效:

row = s.query(User, Address).join(User.addresses).first()

row._mapping[User]  # same as row[0]
row._mapping[Address]  # same as row[1]
row._mapping["User"]  # same as row[0]
row._mapping["Address"]  # same as row[1]

u1 = aliased(User)
row = s.query(u1).only_return_tuples(True).first()
row._mapping[u1]  # same as row[0]


row = (
    s.query(User.id, Address.email_address)
    .join(User.addresses)
    .first()
)

row._mapping[User.id]  # same as row[0]
row._mapping["id"]  # same as row[0]
row._mapping[users.c.id]  # same as row[0]

参见

RowProxy不再是“代理”;现在称为Row,其行为类似于增强的命名元组

#4710 .

会话具有新的“自动注册”行为

在此之前, Session 在其默认模式 autocommit=False 将在内部开始一个 SessionTransaction 对象,并且还将在每次调用 Session.rollback()Session.commit()

新的行为是,这 SessionTransaction 对象现在仅按需创建,当方法(如 Session.add()Session.execute() 都被称为。但是,现在也可以调用 Session.begin() 以显式开始事务,即使在 autocommit=False 模式,因此与未来风格的行为相匹配 _base.Connection

这表明行为发生了以下变化:

参见

自动开始

基本原理

这个 Session 对象的默认行为 autocommit=False 历史上的意思是 SessionTransaction 正在播放的对象,与 Session 通过 Session.transaction 属性。当给定的 SessionTransaction 已完成,由于提交、回滚或关闭,它立即被替换为新的。这个 SessionTransaction 它本身并不意味着使用任何面向连接的资源,因此这种长期存在的行为在 Session.transaction 总是可以预见的。

但是,作为 #5056 为了大大减少参考周期,这个假设意味着 Session.close() 结果是 Session 对象,它仍然具有引用循环,并且清理成本更高,更不用说在构造 SessionTransaction 对象,这意味着将为 Session 举个例子 Session.commit() 然后 Session.close() .

因此,决定 Session.close() 应该离开内部状态 self.transaction ,现在内部称为 self._transaction ,因为没有,这是一个新的 SessionTransaction 只应在需要时创建。为了保持一致性和代码覆盖率,这种行为也被扩展到包括“autobegin”所期望的所有点,而不仅仅是在什么时候 Session.close() 被叫来了。

特别是,这会导致订阅 SessionEvents.after_transaction_create() 事件钩子;以前,当 Session 以及大多数关闭前一个事务并将发出的操作 SessionEvents.after_transaction_end() . 新的行为是 SessionEvents.after_transaction_create()Session 尚未创建新的 SessionTransaction 对象和映射对象与 Session 通过像 Session.add()Session.delete() ,当 Session.transactionSession.flush() 方法有任务要完成等。

此外,依赖于 Session.commit()Session.rollback() 方法无条件地使所有对象过期,则不能再这样做。在未发生任何更改时需要使所有对象过期的代码应调用 Session.expire_all() 为了这个案子。

除了时间上的变化外, SessionEvents.after_transaction_create() 事件以及 Session.commit()Session.rollback() ,则该更改不应对用户可见的 Session 对象的行为; Session 将继续具有这样的行为,即它在以下情况下仍可用于新操作 Session.close() 被调用,以及 SessionEngine 而且数据库本身也应该保持不受影响,因为这些操作已经在按需方式运行。

#5074

仅查看关系不同步backref

#5149 在1.3.14中,SQLAlchemy在 relationship.backrefrelationship.back_populates 关键字将与 relationship.viewonly 标记目标关系。这是因为“viewonly”关系实际上并不持久化对其所做的更改,这可能会导致一些误导行为的发生。但是,在 #5237 ,我们试图改进这种行为,因为有合法的用例在viewonly关系上设置backref,包括关系惰性加载程序在某些情况下使用back-populates属性来确定不需要在另一个方向上进行额外的紧急加载,以及back-populates可以用于mapper反省等等 backref() 可以方便地建立双向关系。

然后,解决方案是使backref中发生的“突变”成为可选的,使用 relationship.sync_backref 旗子。在1.4中 relationship.sync_backref 对于同时设置 relationship.viewonly . 这表示对与viewonly的关系所做的任何更改都不会影响另一方或 Session 无论如何:

class User(Base):
    # ...

    addresses = relationship(Address, backref=backref("user", viewonly=True))

class Address(Base):
    # ...


u1 = session.query(User).filter_by(name="x").first()

a1 = Address()
a1.user = u1

上面, a1 对象意志 not 添加到 u1.addresses 收藏,也不会 a1 对象添加到会话中。以前,这两件事都是真的。警告说 relationship.sync_backref 应设置为 False 什么时候? relationship.viewonlyFalse 不再发射,因为这现在是默认行为。

#5237

cascade_backrefs行为在2.0中不推荐删除

SQLAlchemy长期以来一直有将对象级联到 Session 基于backref赋值。鉴于 User 下面已经是 Session ,将其分配给 Address.user AN属性 Address 对象,假设建立了双向关系,则意味着 Address 也被放进 Session 在这一点上:

u1 = User()
session.add(u1)

a1 = Address()
a1.user = u1  # <--- adds "a1" to the Session

上述行为是反引用行为的意外副作用,因为 a1.user 暗示 u1.addresses.append(a1)a1 会被级联到 Session 。在整个1.4中,这仍然是默认行为。在某种程度上,一面新的旗帜 relationship.cascade_backrefs 已添加以禁用上述行为,以及 backref.cascade_backrefs 要在由指定关系时设置此项,请执行以下操作 relationship.backref ,因为它可能会令人惊讶,而且还会妨碍一些操作,其中对象将被放置在 Session 太早了,脸红得太早了。

在2.0中,默认行为是“CASCADE_BACKREFS”为FALSE,另外不会有“True”行为,因为这通常不是理想的行为。如果启用了2.0弃用警告,则在实际发生“反向引用级联”时将发出警告。若要获得新行为,请将 relationship.cascade_backrefsbackref.cascade_backrefsFalse 在任何目标关系上,正如1.3和更早版本中已经支持的那样,或者使用 Session.future 标志为 2.0-style 模式::

Session = sessionmaker(engine, future=True)

with Session() as session:
  u1 = User()
  session.add(u1)

  a1 = Address()
  a1.user = u1  # <--- will not add "a1" to the Session

#5150

加载器在未到期的操作期间发出信号

一个长期寻求的行为是,当一个过期的对象被访问时,配置好的急切加载程序将运行,以便在对象刷新或以其他方式未过期时,急切地加载过期对象上的关系。现在已经添加了此行为,因此joinedLoader将像往常一样添加内联联接,当过期对象未过期或对象刷新时,selectin/subquery加载程序将对给定关系运行“immediateload”操作:

>>> a1 = session.query(A).options(joinedload(A.bs)).first()
>>> a1.data = 'new data'
>>> session.commit()

上面, A 对象加载了 joinedload() 急切地加载选项 bs 收藏。在 session.commit() ,对象的状态为过期。访问 .data 属性,对象将被刷新,现在还将包括joinedload操作:

>>> a1.data
SELECT a.id AS a_id, a.data AS a_data, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id
FROM a LEFT OUTER JOIN b AS b_1 ON a.id = b_1.a_id
WHERE a.id = ?

该行为同时适用于应用于 relationship() 直接,以及与一起使用的选项 Query.options() ,前提是对象最初是由该查询加载的。

对于“secondary”eager loader“selectinload”和“subqueryload”,这些加载器的SQL策略并不需要,以便在单个对象上急切地加载属性;因此它们将在刷新场景中调用“immediateload”策略,类似于“lazyload”发出的查询,作为附加查询发出:

>>> a1 = session.query(A).options(selectinload(A.bs)).first()
>>> a1.data = 'new data'
>>> session.commit()
>>> a1.data
SELECT a.id AS a_id, a.data AS a_data
FROM a
WHERE a.id = ?
(1,)
SELECT b.id AS b_id, b.a_id AS b_a_id
FROM b
WHERE ? = b.a_id
(1,)

请注意,loader选项不适用于引入到 Session 以不同的方式。也就是说,如果 a1 对象被持久化了 Session ,或在应用eager选项之前使用其他查询加载,则该对象没有与其关联的eager load选项。这并不是一个新的概念,但是用户谁正在寻找急切的负载刷新行为可能会发现这是更明显的。

#1763

访问临时对象上未初始化的集合属性不再发生变化 __dict__

访问新创建的对象上的映射属性会返回隐式生成的值,而不是引发 AttributeError ,如 None 对于标量属性或 [] 对于列表持有关系:

>>> u1 = User()
>>> u1.name
None
>>> u1.addresses
[]

上述行为的基本原理最初是为了使ORM对象更易于使用。由于ORM对象在第一次创建时代表一个空行,没有任何状态,因此很直观地,它的未访问属性将解析为 None (或sql空)用于scalars,用于关系的空集合。特别是,它使一种极为常见的模式成为可能,即在不首先手动创建和分配空集合的情况下改变新集合:

>>> u1 = User()
>>> u1.addresses.append(Address())  # no need to assign u1.addresses = []

直到1.0版的sqlAlchemy,这个初始化系统对于标量属性和集合的行为都是 None 或者是空的集合 密集的 进入对象的状态,例如 __dict__ .这意味着以下两个操作是等效的:

>>> u1 = User()
>>> u1.name = None  # explicit assignment

>>> u2 = User()
>>> u2.name  # implicit assignment just by accessing it
None

在上面,两个 u1u2 会有价值的 None 填充在 name 属性。因为这是一个SQL空值,所以ORM将跳过在插入中包含这些值,以便发生SQL级别的默认值(如果有的话),否则数据库端的值默认为空值。

在1.0版中作为 对属性事件的更改以及与没有预先存在值的属性相关的其他操作 ,这种行为被改进,以便 None 值不再填充到 __dict__ ,仅返回。除了删除getter操作的可变副作用之外,这种更改还可以通过实际分配,将具有服务器默认值的列设置为空值。 None 它现在已经区别于仅仅阅读它了。

但是,更改不适用于集合,因为返回未分配的空集合意味着此可变集合将每次都不同,并且也无法正确适应对其调用的可变操作(例如追加、添加等)。虽然这种行为通常不会妨碍任何人,但最终在年发现了一个边缘案例。 #4519 如果此空集合可能有害,则当对象合并到会话中时:

>>> u1 = User(id=1)  # create an empty User to merge with id=1 in the database
>>> merged1 = session.merge(u1)  # value of merged1.addresses is unchanged from that of the DB

>>> u2 = User(id=2) # create an empty User to merge with id=2 in the database
>>> u2.addresses
[]
>>> merged2 = session.merge(u2)  # value of merged2.addresses has been emptied in the DB

上面, .addresses 收集关于 merged1 将包含所有 Address() 数据库中已存在的对象。 merged2 不会;因为它有一个隐式分配的空列表, .addresses 集合将被删除。这是一个例子,说明这种变化的副作用实际上会改变数据库本身。

虽然有人认为属性系统应该开始使用严格的“纯Python”行为,但是 AttributeError 在所有情况下,对于非持久对象上不存在的属性以及要求显式分配所有集合的情况,这种更改对于多年来依赖此行为的大量应用程序来说可能过于极端,从而导致复杂的卷展/后退兼容性问题以及解决方法恢复旧行为的可能性将变得普遍,从而在任何情况下都使整个更改无效。

然后,改变是保持默认的生成行为,但最终通过在收集系统中添加额外的机制,使scalar的非变化行为成为集合的现实。访问空属性时,将创建新集合并与状态关联,但不会将其添加到 __dict__ 直到它真正变异:

>>> u1 = User()
>>> l1 = u1.addresses  # new list is created, associated with the state
>>> assert u1.addresses is l1  # you get the same list each time you access it
>>> assert "addresses" not in u1.__dict__  # but it won't go into __dict__ until it's mutated
>>> from sqlalchemy import inspect
>>> inspect(u1).attrs.addresses.history
History(added=None, unchanged=None, deleted=None)

更改列表后,它将成为要持久化到数据库的跟踪更改的一部分:

>>> l1.append(Address())
>>> assert "addresses" in u1.__dict__
>>> inspect(u1).attrs.addresses.history
History(added=[<__main__.Address object at 0x7f49b725eda0>], unchanged=[], deleted=[])

预计这一变化 几乎 不会以任何方式影响现有应用程序,除非已观察到某些应用程序可能依赖于此集合的隐式赋值,例如断言对象包含基于其 __dict__ ::

>>> u1 = User()
>>> u1.addresses
[]
# this will now fail, would pass before
>>> assert {k: v for k, v in u1.__dict__.items() if not k.startswith("_")} == {"addresses": []}

或者,为了确保集合不需要进行延迟加载,下面的(公认是笨拙的)代码现在也将失败::

>>> u1 = User()
>>> u1.addresses
[]
>>> s.add(u1)
>>> s.flush()
>>> s.close()
>>> u1.addresses  # <-- will fail, .addresses is not loaded and object is detached

依赖集合的隐式变化行为的应用程序将需要更改,以便它们显式分配所需的集合:

>>> u1.addresses = []

#4519

“new instance conflicts with existing identity”错误现在是一个警告

SQLAlchemy始终具有逻辑来检测 Session 要插入的对象与已存在的对象具有相同的主键::

class Product(Base):
    __tablename__ = 'product'

    id = Column(Integer, primary_key=True)

session = Session(engine)

# add Product with primary key 1
session.add(Product(id=1))
session.flush()

# add another Product with same primary key
session.add(Product(id=1))
s.commit()  # <-- will raise FlushError

变化是 FlushError 更改为仅警告::

sqlalchemy/orm/persistence.py:408: SAWarning: New instance <Product at 0x7f1ff65e0ba8> with identity key (<class '__main__.Product'>, (1,), None) conflicts with persistent instance <Product at 0x7f1ff60a4550>

在此之后,条件将尝试将该行插入将发出的数据库中。 IntegrityError ,如果主键标识不在 Session ::

sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: product.id

其基本原理是允许使用 IntegrityError 无论 Session ,就像通常使用保存点所做的那样:

# add another Product with same primary key
try:
    with session.begin_nested():
        session.add(Product(id=1))
except exc.IntegrityError:
    print("row already exists")

上述逻辑在早期并不完全可行,例如 Product 具有现有标识的对象已在 Session ,代码还必须捕获 FlushError 另外,不会针对完整性问题的特定条件进行筛选。通过更改,上述块的行为保持一致,但也会发出警告。

由于所讨论的逻辑处理主键,因此在插入时主键发生冲突的情况下,所有数据库都会发出完整性错误。以前不会出现错误的情况是,在映射的可选对象上定义主键的映射非常不寻常,这种映射比数据库模式中实际配置的更严格,例如映射到表的联接或将附加列定义为复合主键的一部分,而复合主键在数据库模式中实际上不受约束。然而,这些情况也更加一致地工作,因为无论数据库中是否仍然存在现有的标识,插入都将在理论上继续进行。警告还可以配置为使用python warnings过滤器引发异常。

#4662

viewonly=True时不允许持久性相关的级联操作

当A relationship() 设置为 viewonly=True 使用 relationship.viewonly 标志,它指示此关系只应用于从数据库加载数据,不应更改或涉及持久性操作。为了确保此合同成功运行,关系不再指定 relationship.cascade 在“仅查看”方面没有意义的设置。

这里的主要目标是“delete,delete orphan”级联,在1.3版本中,即使viewonly为True,它仍然会影响持久性,这是一个bug;即使viewonly为True,如果父对象被删除或对象被分离,对象仍会将这两个操作级联到相关对象上。与其修改级联操作来检查viewonly,还不如将这两种操作一起配置:

class User(Base):
    # ...

    # this is now an error
    addresses = relationship(
        "Address", viewonly=True, cascade="all, delete-orphan")

以上将提出:

sqlalchemy.exc.ArgumentError: Cascade settings
"delete, delete-orphan, merge, save-update" apply to persistence
operations and should not be combined with a viewonly=True relationship.

从SQLAlchemy 1.3.12开始,存在此问题的应用程序应该发出警告,对于上述错误,解决方案是删除viewonly关系的级联设置。

#4993 #4994

使用自定义查询查询继承映射时的更严格行为

此更改适用于查询联接或单表继承子类实体的场景,给定一个完整的SELECT子查询进行选择。如果给定的子查询返回的行与所请求的多态标识不对应,则会引发错误。以前,这个条件将在联接表继承下静默传递,返回无效的子类,而在单表继承下,则 Query 将针对子查询添加附加条件,以限制可能不适当地干扰查询意图的结果。

给出的示例映射 EmployeeEngineer(Employee)Manager(Employee) ,在1.3系列中,如果我们要针对联接继承映射发出以下查询:

s = Session(e)

s.add_all([Engineer(), Manager()])

s.commit()

print(
    s.query(Manager).select_entity_from(s.query(Employee).subquery()).all()
)

子查询同时选择 Engineer 以及 Manager 行,即使外部查询针对 Manager ,我们得到了一个 Manager 对象背面:

SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id
FROM (SELECT employee.type AS type, employee.id AS id
FROM employee) AS anon_1
2020-01-29 18:04:13,524 INFO sqlalchemy.engine.base.Engine ()
[<__main__.Engineer object at 0x7f7f5b9a9810>, <__main__.Manager object at 0x7f7f5b9a9750>]

新的行为是这种情况会引发一个错误:

sqlalchemy.exc.InvalidRequestError: Row with identity key
(<class '__main__.Employee'>, (1,), None) can't be loaded into an object;
the polymorphic discriminator column '%(140205120401296 anon)s.type'
refers to mapped class Engineer->engineer, which is not a sub-mapper of
the requested mapped class Manager->manager

只有当实体的主键列为非空时,才会出现上述错误。如果行中没有给定实体的主键,则不会尝试构造实体。

在单继承映射的情况下,行为的更改稍微涉及更多;如果 EngineerManager 以上都是用单表继承映射的,在1.3中,将发出以下查询,并且只有 Manager 返回对象::

SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id
FROM (SELECT employee.type AS type, employee.id AS id
FROM employee) AS anon_1
WHERE anon_1.type IN (?)
2020-01-29 18:08:32,975 INFO sqlalchemy.engine.base.Engine ('manager',)
[<__main__.Manager object at 0x7ff1b0200d50>]

这个 Query 向子查询添加了“单表继承”条件,编辑了最初由它设置的意图。此行为已在中的版本1.0中添加 #3891 ,并在“joined”和“single”表继承之间创建行为不一致,并另外修改给定查询的意图,该查询可能会返回与继承实体对应的列为NULL的其他行,这是一个有效的用例。该行为现在等效于联接表继承的行为,其中假定子查询返回正确的行,如果遇到意外的多态标识,则会引发错误:

SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id
FROM (SELECT employee.type AS type, employee.id AS id
FROM employee) AS anon_1
2020-01-29 18:13:10,554 INFO sqlalchemy.engine.base.Engine ()
Traceback (most recent call last):
# ...
sqlalchemy.exc.InvalidRequestError: Row with identity key
(<class '__main__.Employee'>, (1,), None) can't be loaded into an object;
the polymorphic discriminator column '%(140700085268432 anon)s.type'
refers to mapped class Engineer->employee, which is not a sub-mapper of
the requested mapped class Manager->employee

对上面1.3中提到的情况的正确调整是调整给定的子查询,以根据鉴别器列正确地过滤行:

print(
    s.query(Manager).select_entity_from(
        s.query(Employee).filter(Employee.discriminator == 'manager').
        subquery()).all()
)

SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id
FROM (SELECT employee.type AS type, employee.id AS id
FROM employee
WHERE employee.type = ?) AS anon_1
2020-01-29 18:14:49,770 INFO sqlalchemy.engine.base.Engine ('manager',)
[<__main__.Manager object at 0x7f70e13fca90>]

#5122

方言变化

pg8000最低版本为1.16.6,仅支持Python 3

在项目维护人员的帮助下,对pg8000方言的支持得到了显著改善。

由于API更改,pg8000方言现在需要版本1.16.6或更高版本。从1.13系列开始,pg8000系列已经放弃了对Python2的支持。需要pg8000的Python 2用户应确保其要求固定在 SQLAlchemy<1.4

#5451

PostgreSQL psycopg2方言需要psycopg2版本2.7或更高版本

psycopg2方言依赖于过去几年发布的psycopg2的许多特性。为了简化方言,2017年3月发布的2.7版现在是最低版本。

mental copg2方言在绑定参数名称方面不再有限制

SQLAlChemy 1.3不能支持包含百分号或括号的Mental copg2方言中的绑定参数名称。这反过来意味着包含这些字符的列名也有问题,因为INSERT和其他DML语句将生成与列的参数名匹配的参数名,这将导致失败。解决方法是利用 Column.key 参数,以使将用于生成该参数的备用名称或方言的参数样式必须在 create_engine() 标高。从SQLAlChemy 1.4.0beta3开始,所有命名限制都已删除,所有场景中的参数都完全转义,因此不再需要这些变通方法。

#5941

#5653

psycopg2方言的特性是“execute_values”,默认情况下返回for INSERT语句

psycopg2方言现在使用的是PostgreSQL在使用Core和ORM时显著提高性能的前半部分 psycopg2.extras.execute_values() 默认为已编译的INSERT语句,并在此模式下实现返回支持。另一半的变化是 ORM批插入与psycopg2现在批处理语句在大多数情况下返回 这使得ORM可以利用executemany返回(即批处理INSERT语句),这样ORM使用psycopg2进行大容量插入可以根据具体情况提高400%。

此扩展方法允许在一个语句中插入多行,使用扩展的VALUES子句。而SQLAlchemy的 insert() 构造已经通过 Insert.values() 方法时,扩展方法允许VALUES子句的构造在语句作为“executemany”执行时动态发生,也就是将参数字典列表传递给 Connection.execute() . 它也发生在缓存边界之外,因此可以在呈现值之前缓存INSERT语句。

快速测试 execute_values() 使用 bulk_inserts.py 脚本在 性能 示例套件揭示了一个近似值 性能提升五倍 ::

$ python -m examples.performance bulk_inserts --test test_core_insert --num 100000 --dburl postgresql://scott:tiger@localhost/test

# 1.3
test_core_insert : A single Core INSERT construct inserting mappings in bulk. (100000 iterations); total time 5.229326 sec

# 1.4
test_core_insert : A single Core INSERT construct inserting mappings in bulk. (100000 iterations); total time 0.944007 sec

在的1.2版中添加了对“批处理”扩展的支持 支持批处理模式/快速执行助手 ,并进行了增强,以包括对 execute_values 延伸1.3英寸 #4623 . 在1.4中 execute_values 默认情况下,INSERT语句的扩展是打开的;UPDATE和DELETE的“批处理”扩展默认保持关闭。

此外, execute_values 扩展函数支持返回作为聚合列表返回生成的行。psycopg2方言现在将检索这个列表,如果 insert() 构造通过返回的请求 Insert.returning() 方法或类似的方法,用于返回生成的默认值;然后将行安装到结果中,以便像直接从游标中获取行一样进行检索。在这种情况下,类似于批处理的插入工具可以显著提高性能。

这个 executemany_mode psycopg2方言的特点经过了以下修改:

  • 新模式 "values_only" 已添加。这种模式使用的是 psycopg2.extras.execute_values() 已编译INSERT语句的扩展方法使用executemany()运行,但不使用 execute_batch() 更新和删除语句。这个新模式现在是psycopg2方言的默认设置。

  • 现有的 "values" 模式现在命名为 "values_plus_batch" . 此模式将使用 execute_values 对于INSERT语句和 execute_batch 更新和删除语句。默认情况下不会启用该模式,因为它会禁用 cursor.rowcount 使用执行UPDATE和DELETE语句 executemany() .

  • 已为启用返回支持 "values_only""values" 对于INSERT语句。psycopg2方言将使用fetch=True标志从psycopg2接收返回的行,并将它们安装到结果集中,就像它们直接来自游标一样(它们最终做到了,但是psycopg2的扩展函数将多个批聚合到一个列表中)。

  • 的默认“页面大小”设置 execute_values 从100增加到1000。默认值保持为100 execute_batch 功能。这些参数都可以像以前一样修改。

  • 这个 use_batch_mode 该特性的1.2版本的一部分的标志将被删除;通过 executemany_mode 1.3中添加的标志。

  • 核心引擎和方言通过提供新的 CursorResult.inserted_primary_key_rowsCursorResult.returned_default_rows 访问器。

参见

Psycopg2快速执行助手

#5401

删除了SQLite方言中的“join-rewriting”逻辑;更新了导入

放弃了对右嵌套连接重写的支持,以支持2013年发布的3.7.16之前的旧SQLite版本。预计任何现代Python版本都不会依赖于此限制。

该行为在0.9中首次引入,是允许右嵌套联接这一更大变化的一部分,如中所述 许多联接和左外部联接表达式将不再作为anon_1包装在(select*from..)中。 . 然而,由于SQLite在2013-2014年期间的工作环境复杂,导致其在2013-2014年期间出现了许多问题。2016年,方言进行了修改,使得只有在使用二分法来确定SQLite修复了对该构造的支持的位置之后,才会对3.7.16之前的SQLite版本使用联接重写逻辑,并且没有针对该行为的进一步问题报告(即使在内部发现了一些错误)。现在预计,几乎没有针对python2.7或3.5及更高版本(支持的Python版本)的Python构建,其中包括3.7.17之前的SQLite版本,并且只有在更复杂的ORM连接场景中才需要这种行为。如果安装的SQLite版本早于3.7.16,则会发出警告。

在相关的更改中,SQLite的模块导入不再尝试导入Python3上的“pysqlite2”驱动程序,因为Python3上不存在该驱动程序;对于旧的pysqlite2版本,也会删除一个非常旧的警告。

#4895

增加了对MariaDB 10.3的序列支持

从10.3开始,MariaDB数据库支持序列。SQLAlchemy的MySQL方言现在实现了对 Sequence 对象,这意味着将为 Sequence 在一个 TableMetaData 当方言的服务器版本检查确认数据库是MariaDB 10.3或更高版本时,收集的方式与用于PostgreSQL、Oracle等后端的方法相同。此外 Sequence 以这些方式使用时,将充当列默认值和主键生成对象。

由于此更改将影响DDL的假设以及当前针对mariabd10.3部署的应用程序的INSERT语句的行为,而mariabd10.3恰好也明确使用了 Sequence 在其表定义中,需要注意的是 Sequence 支持标志 Sequence.optional 用于限制 Sequence 生效。当“可选”用于 Sequence 存在于表的整数主键列中:

Table(
    "some_table", metadata,
    Column("id", Integer, Sequence("some_seq", optional=True), primary_key=True)
)

以上 Sequence 仅当目标数据库不支持为列生成整数主键值的任何其他方法时,才用于DDL和INSERT语句。也就是说,上面的Oracle数据库将使用序列,而PostgreSQL和MariaDB 10.3数据库则不会使用。这对于正在升级到SQLAlchemy 1.4的现有应用程序可能很重要,因为它可能没有为此发出DDL Sequence 如果它试图使用未创建的序列,则as INSERT语句将失败。

参见

定义序列

#4976

向SQL Server添加了不同于标识的序列支持

这个 Sequence 构造现在可以与Microsoft SQL Server一起完全正常工作。当应用于 Column ,表的DDL将不再包含IDENTITY关键字,而是依赖“createsequence”来确保存在一个序列,然后该序列将用于表上的INSERT语句。

这个 Sequence 在版本1.3之前,用于控制SQLServer中标识列的参数;此用法在整个1.3中发出了不推荐使用的警告,现在在1.4中已删除。对于标识列的参数控制 mssql_identity_startmssql_identity_increment 应该使用参数;请参阅下面链接的MSSQL方言文档。

参见

自动增量行为/标识列

#4235

#4633