SqlAlchemy-2-0-中文文档-三十二-

SqlAlchemy 2.0 中文文档(三十二)

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

常见问题

原文:docs.sqlalchemy.org/en/20/faq/index.html

常见问题部分是一系列常见问题的不断增长的集合,涵盖了众多已知问题。

  • 安装

    • 当我尝试使用 asyncio 时,为什么会出现关于未安装 greenlet 的错误?
  • 连接 / 引擎

    • 如何配置日志记录?

    • 如何池化数据库连接?我的连接是否被池化?

    • 如何将自定义连接参数传递给我的数据库 API?

    • “MySQL 服务器已断开连接”

    • “命令不同步;你现在无法运行此命令” / “此结果对象不返回行。它已被自动关闭”

    • 如何自动“重试”语句执行?

    • 为什么 SQLAlchemy 发出了这么多 ROLLBACKs?

    • 我正在使用 SQLite 数据库的多个连接(通常用于测试事务操作),但我的测试程序无法工作!

    • 在使用 Engine 时,如何获取原始的 DBAPI 连接?

    • 如何在 Python 多进程或 os.fork() 中使用引擎 / 连接 / 会话?

  • 元数据 / 模式

    • 当我使用 table.drop() / metadata.drop_all() 时,我的程序挂起了

    • SQLAlchemy 是否支持 ALTER TABLE、CREATE VIEW、CREATE TRIGGER、模式升级功能?

    • 如何按依赖顺序对 Table 对象进行排序?

    • 如何将 CREATE TABLE / DROP TABLE 输出作为字符串获取?

    • 如何派生 Table/Column 以提供某些行为/配置?

  • SQL 表达式

    • 如何将 SQL 表达式呈现为字符串,可能包含内联的绑定参数?

    • 当将 SQL 语句字符串化时,为什么百分号会被加倍?

    • 我正在使用 op() 生成自定义运算符,但我的括号不正确

  • ORM 配置

    • 如何映射没有主键的表?

    • 如何配置一个列,该列是 Python 保留字或类似的?

    • 如何在给定映射类的情况下获取所有列、关系、映射属性等的列表?

    • 我收到关于“隐式组合列 X 在属性 Y 下”的警告或错误

    • 我使用声明性,并使用 and_()or_() 设置 primaryjoin/secondaryjoin,并且收到有关外键的错误消息。

    • 为什么推荐 LIMIT 结合 ORDER BY(尤其是与 subqueryload() 一起)?

  • 性能

    • 为什么我升级到 1.4 和/或 2.x 后应用程序变慢?

    • 我如何对基于 SQLAlchemy 的应用程序进行性能分析?

    • 我正在使用 ORM 插入 400,000 行,但速度非常慢!

  • 会话 / 查询

    • 我正在使用我的会话重新加载数据,但它没有看到我在其他地方提交的更改

    • “由于 flush 期间的前一个异常,此会话的事务已回滚。”(或类似的)

    • 如何制作一个查询,始终向每个查询添加特定的过滤器?

    • 我的查询没有返回与 query.count() 告诉我的相同数量的对象 - 为什么?

    • 我已经创建了一个对外连接的映射,虽然查询返回了行,但没有返回对象。为什么?

    • 我使用 joinedload()lazy=False 创建了一个 JOIN/OUTER JOIN,但是当我尝试添加 WHERE、ORDER BY、LIMIT 等条件时,SQLAlchemy 并没有构造正确的查询(依赖于 (OUTER) JOIN)。

    • 查询没有 __len__(),为什么?

    • 如何在 ORM 查询中使用文本 SQL?

    • 我调用 Session.delete(myobject),但它没有从父集合中删除!

    • 为什么在加载对象时我的 __init__() 没有被调用?

    • 我如何在 SA 的 ORM 中使用 ON DELETE CASCADE?

    • 我将我的实例的“foo_id”属性设置为“7”,但“foo”属性仍然是None - 难道它不应该加载 id 为 #7 的 Foo 吗?

    • 如何遍历与给定对象相关的所有对象?

    • 是否有一种方法可以自动地只拥有唯一的关键词(或其他类型的对象),而不必查询关键词并获得包含该关键词的行的引用?

    • 为什么 post_update 除了第一个 UPDATE 外还会发出 UPDATE?

  • 第三方集成问题

    • 我遇到了与“numpy.int64”、“numpy.bool_”等相关的错误。

    • SQL 表达式中期望 WHERE/HAVING 角色,实际得到了 True

安装

原文:docs.sqlalchemy.org/en/20/faq/installation.html

  • 当我尝试使用 asyncio 时,出现了关于未安装 greenlet 的错误

当我尝试使用 asyncio 时,出现了关于未安装 greenlet 的错误

对于不提供预构建二进制轮的 CPU 架构,默认情况下不会安装 greenlet 依赖项。特别是,这包括 Apple M1。要安装包括 greenlet 的内容,请将 asyncio setuptools 额外内容添加到 pip install 命令中:

pip install sqlalchemy[asyncio]

欲了解更多背景信息,请参阅 Asyncio 平台安装说明(包括 Apple M1)。

另请参阅

Asyncio 平台安装说明(包括 Apple M1) ## 当我尝试使用 asyncio 时,出现了关于未安装 greenlet 的错误

对于不提供预构建二进制轮的 CPU 架构,默认情况下不会安装 greenlet 依赖项。特别是,这包括 Apple M1。要安装包括 greenlet 的内容,请将 asyncio setuptools 额外内容添加到 pip install 命令中:

pip install sqlalchemy[asyncio]

欲了解更多背景信息,请参阅 Asyncio 平台安装说明(包括 Apple M1)。

另请参阅

Asyncio 平台安装说明(包括 Apple M1)

连接 / 引擎

原文:docs.sqlalchemy.org/en/20/faq/connections.html

  • 我如何配置日志记录?

  • 我如何池化数据库连接?我的连接被池化了吗?

  • 我如何传递自定义连接参数给我的数据库 API?

  • “MySQL 服务器已断开连接”

  • “命令不同步;您现在无法运行此命令” / “此结果对象不返回行。它已被自动关闭”

  • 如何自动“重试”语句执行?

    • 使用 DBAPI 自动提交允许透明重连的只读版本
  • 为什么 SQLAlchemy 发出那么多回滚?

    • 我正在使用 MyISAM - 如何关闭它?

    • 我正在使用 SQL Server - 如何将那些回滚变成提交?

  • 我正在使用 SQLite 数据库的多个连接(通常用于测试事务操作),但我的测试程序不起作用!

  • 在使用引擎时如何获取原始 DBAPI 连接?

    • 访问 asyncio 驱动程序的底层连接
  • 如何在 Python 多进程或 os.fork() 中使用引擎 / 连接 / 会话?

我如何配置日志记录?

参见 配置日志记录。

我如何池化数据库连接?我的连接被池化了吗?

SQLAlchemy 在大多数情况下会自动执行应用程序级别的连接池。对于所有包含的方言(除了在使用“内存”数据库时的 SQLite 外),Engine 对象都指向 QueuePool 作为连接的来源。

更多细节,请参阅 引擎配置 和 连接池。

我如何传递自定义连接参数给我的数据库 API?

create_engine() 调用可以通过 connect_args 关键字参数直接接受附加参数:

e = create_engine(
    "mysql+mysqldb://scott:tiger@localhost/test", connect_args={"encoding": "utf8"}
)

或者对于基本的字符串和整数参数,它们通常可以在 URL 的查询字符串中指定:

e = create_engine("mysql+mysqldb://scott:tiger@localhost/test?encoding=utf8")

另请参见

自定义 DBAPI connect() 参数 / 连接时例程

“MySQL 服务器已断开连接”

此错误的主要原因是 MySQL 连接已超时并已被服务器关闭。 MySQL 服务器会关闭空闲了一段时间(默认为八小时)的连接。 为了适应此情况,可立即设置为启用 create_engine.pool_recycle 设置,这将确保比一定时间旧的连接在下次检出时将被丢弃并替换为新连接。

对于更一般的情况,如适应数据库重新启动和由于网络问题而导致的临时连接丢失,池中的连接可能会在响应更广泛的断开连接检测技术时进行回收利用。 章节 处理断开连接 提供了关于“悲观”(例如预检)和“乐观”(例如优雅恢复)技术的背景。 现代 SQLAlchemy 倾向于采用“悲观”方法。

另请参见

处理断开连接

“命令不同步;您现在无法运行此命令” / “此结果对象不返回行。 它已被自动关闭”

MySQL 驱动程序存在一类失败模式,其中与服务器的连接状态处于无效状态。 通常,当再次使用连接时,将出现这两种错误消息之一。 原因是服务器的状态已更改为客户端库不期望的状态,因此当客户端库在连接上发出新语句时,服务器不会如预期地响应。

在 SQLAlchemy 中,由于数据库连接是池化的,连接上的消息不同步的问题变得更加重要,因为当操作失败时,如果连接本身处于不可用状态,如果它再次返回到连接池中,那么在再次检出时将会发生故障。 对此问题的缓解措施是当发生这种故障模式时连接被作废,以便底层 MySQL 数据库连接被丢弃。 对于许多已知的故障模式,此作废会自动发生,也可以通过 Connection.invalidate() 方法显式调用。

在此类别中还存在第二类故障模式,其中上下文管理器(例如with session.begin_nested():)希望在发生错误时“回滚”事务; 但是在某些连接的故障模式中,回滚本身(也可以是 RELEASE SAVEPOINT 操作)也会失败,导致误导性的堆栈跟踪。

最初,此错误的原因相当简单,它意味着多线程程序从多个线程调用单个连接上的命令。 这适用于原始的“MySQLdb”本机 C 驱动程序,这几乎是唯一使用的驱动程序。 但是,随着纯 Python 驱动程序(如 PyMySQL 和 MySQL-connector-Python)的引入,以及诸如 gevent/eventlet、多处理(通常与 Celery 一起使用)等工具的增加使用,已知有一整套因素会导致这个问题,其中一些因素已经在 SQLAlchemy 的不同版本中得到改进,但其他因素是无法避免的:

  • 在线程之间共享连接 - 这是这类错误发生的最初原因。 程序在同一时间在两个或多个线程中使用同一个连接,这意味着多组消息在连接上混合在一起,将服务器端会话置于客户端不再知道如何解释的状态。 但是,如今通常更有可能出现其他原因。

  • 在进程之间共享连接的文件句柄 - 这通常发生在程序使用os.fork()生成新进程时,父进程中存在的 TCP 连接被共享到一个或多个子进程。 由于多个进程现在向本质上是相同文件句柄的服务器发送消息,因此服务器接收到交错的消息并破坏连接的状态。

    如果程序使用 Python 的“multiprocessing”模块,并使用在父进程中创建的Engine,则此场景可能非常容易发生。 使用工具如 Celery 时通常会使用“multiprocessing”。 正确的方法应该是在子进程首次启动时生成一个新的Engine,丢弃从父进程传递下来的任何Engine; 或者,从父进程继承的Engine可以通过调用Engine.dispose()来处理其内部连接池。

  • 使用 Greenlet Monkeypatching w/ Exits - 当使用像 gevent 或 eventlet 这样的库对 Python 网络 API 进行 monkeypatch 时,像 PyMySQL 这样的库现在以异步模式运行,即使它们并没有明确针对这种模型开发。一个常见问题是 greenthread 被中断,通常是由于应用程序中的超时逻辑。这导致引发GreenletExit异常,并且纯 Python MySQL 驱动程序被中断了其工作,可能是正在接收来自服务器的响应或准备以其他方式重置连接状态。当异常中断所有这些工作时,客户端和服务器之间的对话现在不同步,后续使用连接可能会失败。从版本 1.1.0 开始,SQLAlchemy 知道如何防范这种情况,如果数据库操作被所谓的“退出异常”中断,其中包括GreenletExit和任何不是Exception的 Python BaseException的子类,连接将被作废。

  • 回滚/SAVEPOINT 释放失败 - 某些类别的错误导致连接在事务上下文中无法使用,以及在“SAVEPOINT”块中操作时。在这些情况下,连接上的失败使任何 SAVEPOINT 不再存在,但当 SQLAlchemy 或应用程序尝试“回滚”此保存点时,“RELEASE SAVEPOINT”操作失败,通常会显示类似“savepoint does not exist”的消息。在这种情况下,在 Python 3 下会输出一系列异常,其中最终的错误“原因”也将被显示。在 Python 2 下,没有“链接”异常,但是最近的 SQLAlchemy 版本将尝试发出警告,说明原始失败原因,同时仍会抛出立即错误,即 ROLLBACK 的失败。## 如何自动“重试”语句执行?

文档部分处理断开连接讨论了对已自上次检查特定连接以来已断开的连接可用的策略。在这方面最现代的功能是create_engine.pre_ping参数,它允许在从池中检索数据库连接时发出“ping”,如果当前连接已断开,则重新连接。

需要注意的是,“ping” 仅在连接实际用于操作之前发出。一旦连接交付给调用方,根据 Python DBAPI 规范,它现在将受到 自动启动 操作的影响,这意味着当首次使用连接时将自动开始一个新事务,该事务将在后续语句中保持有效,直到调用 DBAPI 级别的 connection.commit()connection.rollback() 方法。

在 SQLAlchemy 的现代用法中,一系列 SQL 语句始终在这个事务状态下调用,假设未启用 DBAPI 自动提交模式(关于此后面会有更多介绍),这意味着没有单个语句会自动提交;如果操作失败,当前事务中所有语句的效果将丢失。

这对于“重试”语句的概念意味着在默认情况下,当连接丢失时,整个事务都将丢失。数据库无法“重新连接和重试”并继续之前的操作,因为数据已经丢失。因此,SQLAlchemy 没有一个在事务中途重新连接的透明“重连”功能。处理中途断开连接的操作的标准方法是从事务开始处重新尝试整个操作,通常通过使用一个自定义的 Python 装饰器,该装饰器会多次“重试”特定函数直到成功,或以其他方式设计应用程序以使其能够抵御因事务断开而导致操作失败。

还有一个扩展概念,可以跟踪事务中执行的所有语句,然后在新事务中重播它们以近似“重试”操作。SQLAlchemy 的 事件系统 确实允许构建这样一个系统,但这种方法通常也不太有用,因为无法保证这些 DML 语句是否针对相同的状态进行操作,一旦事务结束,新事务中的数据库状态可能完全不同。在事务操作开始和提交的地方显式地构建“重试”到应用程序中仍然是更好的方法,因为应用程序级别的事务方法最了解如何重新运行它们的步骤。

否则,如果 SQLAlchemy 提供了一个在事务中途自动且悄无声息地“重新连接”连接的功能,那么效果将是数据被悄无声息地丢失。通过试图隐藏问题,SQLAlchemy 将使情况变得更糟。

但是,如果我们 使用事务,那么就会有更多的选项可用,下一节将描述这些选项。

使用 DBAPI 自动提交允许透明重新连接的只读版本

在说明不具有透明重新连接机制的基础上,上一节假设应用实际上正在使用 DBAPI 级别的事务。由于大多数 DBAPI 现在提供了本机的“自动提交”设置,我们可以利用这些特性为只读,仅自动提交操作提供有限的透明重新连接形式。可以将透明语句重试应用于 DBAPI 的cursor.execute()方法,但是仍然不安全应用于 DBAPI 的cursor.executemany()方法,因为语句可能已经消耗了给定参数的任何部分。

警告

不应将以下方案用于编写数据的操作。用户应该仔细阅读并了解该方案的工作原理,并在将该方案投入生产使用之前对特定的目标 DBAPI 驱动程序非常仔细地测试故障模式。重试机制不能保证在所有情况下防止断开连接错误。

可以通过利用DialectEvents.do_execute()DialectEvents.do_execute_no_params()钩子向 DBAPI 级别的cursor.execute()方法应用简单的重试机制,该机制将能够在语句执行期间拦截断开连接。它将不会拦截在结果集获取操作期间的连接失败,对于那些不完全缓冲结果集的 DBAPI。该方案要求数据库支持 DBAPI 级别的自动提交,并且不能保证适用于特定的后端。提供了一个名为reconnecting_engine()的单一函数,它将事件钩子应用于给定的Engine对象,返回一个总是自动提交的版本,该版本支持 DBAPI 级别的自动提交。连接将在单参数和无参数语句执行时自动重新连接:

import time

from sqlalchemy import event

def reconnecting_engine(engine, num_retries, retry_interval):
    def _run_with_retries(fn, context, cursor_obj, statement, *arg, **kw):
        for retry in range(num_retries + 1):
            try:
                fn(cursor_obj, statement, context=context, *arg)
            except engine.dialect.dbapi.Error as raw_dbapi_err:
                connection = context.root_connection
                if engine.dialect.is_disconnect(raw_dbapi_err, connection, cursor_obj):
                    if retry > num_retries:
                        raise
                    engine.logger.error(
                        "disconnection error, retrying operation",
                        exc_info=True,
                    )
                    connection.invalidate()

                    # use SQLAlchemy 2.0 API if available
                    if hasattr(connection, "rollback"):
                        connection.rollback()
                    else:
                        trans = connection.get_transaction()
                        if trans:
                            trans.rollback()

                    time.sleep(retry_interval)
                    context.cursor = cursor_obj = connection.connection.cursor()
                else:
                    raise
            else:
                return True

    e = engine.execution_options(isolation_level="AUTOCOMMIT")

    @event.listens_for(e, "do_execute_no_params")
    def do_execute_no_params(cursor_obj, statement, context):
        return _run_with_retries(
            context.dialect.do_execute_no_params, context, cursor_obj, statement
        )

    @event.listens_for(e, "do_execute")
    def do_execute(cursor_obj, statement, parameters, context):
        return _run_with_retries(
            context.dialect.do_execute, context, cursor_obj, statement, parameters
        )

    return e

给定上述方案,可以使用以下概念验证脚本演示事务中的重新连接。运行一次后,它将每五秒向数据库发出一个SELECT 1语句:

from sqlalchemy import create_engine
from sqlalchemy import select

if __name__ == "__main__":
    engine = create_engine("mysql+mysqldb://scott:tiger@localhost/test", echo_pool=True)

    def do_a_thing(engine):
        with engine.begin() as conn:
            while True:
                print("ping: %s" % conn.execute(select([1])).scalar())
                time.sleep(5)

    e = reconnecting_engine(
        create_engine("mysql+mysqldb://scott:tiger@localhost/test", echo_pool=True),
        num_retries=5,
        retry_interval=2,
    )

    do_a_thing(e)

在脚本运行时重新启动数据库,以演示透明重新连接操作:

$ python reconnect_test.py
ping: 1
ping: 1
disconnection error, retrying operation
Traceback (most recent call last):
  ...
MySQLdb._exceptions.OperationalError: (2006, 'MySQL server has gone away')
2020-10-19 16:16:22,624 INFO sqlalchemy.pool.impl.QueuePool Invalidate connection <_mysql.connection open to 'localhost' at 0xf59240>
ping: 1
ping: 1
...

上述方案已针对 SQLAlchemy 1.4 进行了测试。

为什么 SQLAlchemy 会发出那么多的 ROLLBACK?

SQLAlchemy 目前假定 DBAPI 连接处于“非自动提交”模式 - 这是 Python 数据库 API 的默认行为,这意味着必须假定事务始终在进行中。当连接返回时,连接池会发出connection.rollback()。这样可以释放连接上剩余的任何事务资源。在像 PostgreSQL 或 MSSQL 这样的数据库中,表资源会被积极锁定,这一点至关重要,以防止行和表在不再使用的连接中保持锁定。否则应用程序可能会挂起。然而,这不仅仅是为了锁定,对于任何具有任何类型事务隔离的数据库,包括具有 InnoDB 的 MySQL,这同样至关重要。如果任何连接仍在旧事务中,那么该连接返回的数据将是过时的,如果在隔离中已经在该连接上查询了该数据。有关为什么甚至在 MySQL 上可能看到过时数据的背景,请参阅dev.mysql.com/doc/refman/5.1/en/innodb-transaction-model.html

我在 MyISAM 上 - 如何关闭它?

连接池的连接返回行为的行为可以使用reset_on_return进行配置:

from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool

engine = create_engine(
    "mysql+mysqldb://scott:tiger@localhost/myisam_database",
    pool=QueuePool(reset_on_return=False),
)

我在 SQL Server 上 - 如何将那些 ROLLBACKs 转换为 COMMITs?

reset_on_return接受commitrollback的值,以及TrueFalseNone。设置为commit将导致任何连接返回到池时进行 COMMIT:

engine = create_engine(
    "mssql+pyodbc://scott:tiger@mydsn", pool=QueuePool(reset_on_return="commit")
)

我正在使用 SQLite 数据库的多个连接(通常用于测试事务操作),但我的测试程序不起作用!

如果使用 SQLite 的:memory:数据库,默认连接池是SingletonThreadPool,每个线程保持一个 SQLite 连接。因此,在同一线程中使用两个连接实际上是相同的 SQLite 连接。确保您不使用:memory:数据库,以便引擎将使用QueuePool(当前 SQLAlchemy 版本中非内存数据库的默认值)。

另请参见

线程/池行为 - 有关 PySQLite 行为的信息。

当使用引擎时,如何访问原始 DBAPI 连接?

使用常规的 SA 引擎级 Connection,您可以通过Connection.connection属性在Connection上获取一个经过池代理的 DBAPI 连接版本,并且对于真正的 DBAPI 连接,您可以在其上调用PoolProxiedConnection.dbapi_connection属性。在常规的同步驱动程序中,通常不需要访问非经过池代���的 DBAPI 连接,因为所有方法都经过代理:

engine = create_engine(...)
conn = engine.connect()

# pep-249 style PoolProxiedConnection (historically called a "connection fairy")
connection_fairy = conn.connection

# typically to run statements one would get a cursor() from this
# object
cursor_obj = connection_fairy.cursor()
# ... work with cursor_obj

# to bypass "connection_fairy", such as to set attributes on the
# unproxied pep-249 DBAPI connection, use .dbapi_connection
raw_dbapi_connection = connection_fairy.dbapi_connection

# the same thing is available as .driver_connection (more on this
# in the next section)
also_raw_dbapi_connection = connection_fairy.driver_connection

自版本 1.4.24 起发生了变化:添加了PoolProxiedConnection.dbapi_connection属性,取代了先前的PoolProxiedConnection.connection属性,但该属性仍然可用;该属性始终提供一个符合 pep-249 同步风格的连接对象。还添加了PoolProxiedConnection.driver_connection属性,它将始终引用真实的驱动程序级连接,无论它展示了什么 API。

访问 asyncio 驱动程序的基础连接

当使用 asyncio 驱动程序时,上述方案有两个变化。首先是当使用AsyncConnection时,必须使用可等待方法AsyncConnection.get_raw_connection()来访问PoolProxiedConnection。在这种情况下返回的PoolProxiedConnection保留了一个同步风格的 pep-249 使用模式,而PoolProxiedConnection.dbapi_connection属性指的是一个将 asyncio 连接适配为同步风格 pep-249 API 的 SQLAlchemy 适配连接对象,换句话说,在使用 asyncio 驱动程序时会有两层代理。实际的 asyncio 连接可以从driver_connection属性中获取。以 asyncio 方式重新表述前面的示例如下:

async def main():
    engine = create_async_engine(...)
    conn = await engine.connect()

    # pep-249 style ConnectionFairy connection pool proxy object
    # presents a sync interface
    connection_fairy = await conn.get_raw_connection()

    # beneath that proxy is a second proxy which adapts the
    # asyncio driver into a pep-249 connection object, accessible
    # via .dbapi_connection as is the same with a sync API
    sqla_sync_conn = connection_fairy.dbapi_connection

    # the really-real innermost driver connection is available
    # from the .driver_connection attribute
    raw_asyncio_connection = connection_fairy.driver_connection

    # work with raw asyncio connection
    result = await raw_asyncio_connection.execute(...)

从版本 1.4.24 开始更改:添加了PoolProxiedConnection.dbapi_connectionPoolProxiedConnection.driver_connection属性,以允许通过一致的接口访问 pep-249 连接、pep-249 适配层和底层驱动程序连接。

使用 asyncio 驱动程序时,上述“DBAPI”连接实际上是一个经过 SQLAlchemy 适配的连接形式,它呈现同步风格的 pep-249 风格 API。要访问实际的 asyncio 驱动程序连接,可以通过PoolProxiedConnectionPoolProxiedConnection.driver_connection属性进行访问。对于标准的 pep-249 驱动程序,PoolProxiedConnection.dbapi_connectionPoolProxiedConnection.driver_connection是同义词。

在将连接返回到池之前,您必须确保将任何隔离级别设置或其他特定操作设置恢复为正常状态。

作为恢复设置的替代方法,您可以在Connection或代理连接上调用Connection.detach()方法,这将使连接与池解除关联,从而在调用Connection.close()时关闭并丢弃连接:

conn = engine.connect()
conn.detach()  # detaches the DBAPI connection from the connection pool
conn.connection.<go nuts>
conn.close()  # connection is closed for real, the pool replaces it with a new connection

如何在 Python 多进程或 os.fork()中使用引擎/连接/会话?

这在使用连接池与多进程或 os.fork()一节中有所涉及。

如何配置日志记录?

参见配置日志记录。

如何池化数据库连接?我的连接是否被池化了?

SQLAlchemy 在大多数情况下会自动执行应用程序级别的连接池。对于所有包含的方言(除了使用“内存”数据库的 SQLite),Engine 对象指的是一个 QueuePool 作为连接的来源。

更多详细信息,请参阅 引擎配置 和 连接池。

如何向我的数据库 API 传递自定义连接参数?

create_engine() 调用可以通过 connect_args 关键字参数直接接受附加参数:

e = create_engine(
    "mysql+mysqldb://scott:tiger@localhost/test", connect_args={"encoding": "utf8"}
)

或者对于基本的字符串和整数参数,它们通常可以在 URL 的查询字符串中指定:

e = create_engine("mysql+mysqldb://scott:tiger@localhost/test?encoding=utf8")

另请参见

自定义 DBAPI connect() 参数 / 连接时例程

“MySQL 服务器已关闭连接”

此错误的主要原因是 MySQL 连接已超时并已被服务器关闭。MySQL 服务器会关闭空闲一段时间(默认为八小时)的连接。为了适应这一点,立即设置是启用 create_engine.pool_recycle 设置,这将确保超过一定秒数的连接在下次检出时被丢弃并替换为新连接。

对于更一般的情况,即适应数据库重新启动和由于网络问题导致的临时连接丢失,池中的连接可能会根据更广义的断开连接检测技术进行回收。章节 处理断开连接 提供了关于“悲观”(例如,预先 ping)和“乐观”(例如,优雅恢复)技术的背景。现代 SQLAlchemy 倾向于采用“悲观”方法。

另请参见

处理断开连接

“命令不同步;您现在无法运行此命令” / “此结果对象不返回行。它已被自动关闭”

MySQL 驱动程序存在一类相当广泛的故障模式,其中与服务器的连接状态处于无效状态。通常情况下,当再次使用连接时,将出现以下两个错误消息之一。原因是因为服务器的状态已更改为客户端库不期望的状态,因此当客户端库在连接上发出新语句时,服务器不会如预期地响应。

在 SQLAlchemy 中,由于数据库连接是池化的,连接上的消息不同步的问题变得更加重要,因为当一个操作失败时,如果连接本身处于不可用状态,如果它重新进入连接池,当再次检出时将发生故障。对于这个问题的缓解措施是,当出现这种故障模式时,连接被作废,以便底层数据库连接到 MySQL 被丢弃。这种作废对于许多已知的故障模式会自动发生,也可以通过Connection.invalidate()方法显式调用。

在这个类别中还有第二类故障模式,其中上下文管理器(如with session.begin_nested():)在发生错误时希望“回滚”事务;然而在某些连接的故障模式中,回滚本身(也可以是一个 RELEASE SAVEPOINT 操作)也会失败,导致误导性的堆栈跟踪。

最初,这种错误的原因通常很简单,意味着一个多线程程序从多个线程调用单个连接上的命令。这适用于最初几乎是唯一使用的原始“MySQLdb”本机 C 驱动程序。然而,随着纯 Python 驱动程序(如 PyMySQL 和 MySQL-connector-Python)的引入,以及诸如 gevent/eventlet、多进程(通常与 Celery 一起使用)等工具的增加使用,已知存在一整套因素会导致这个问题,其中一些已经在 SQLAlchemy 版本中得到改进,但另一些是不可避免的:

  • 在线程之间共享连接 - 这是这类错误发生的最初原因。程序在两个或多个线程中同时使用相同的连接,意味着多组消息在连接上混在一起,使得服务器端会话进入一个客户端不再知道如何解释的状态。然而,今天通常更可能出现其他原因。

  • 在进程之间共享连接的文件句柄 - 这通常发生在程序使用os.fork()生成新进程时,父进程中存在的 TCP 连接被共享到一个或多个子进程中。由于多个进程现在向基本上相同的文件句柄发送消息,服务器接收到交错的消息并破坏连接的状态。

    如果程序使用 Python 的“multiprocessing”模块,并且使用了在父进程中创建的 Engine,则可能会很容易发生此情况。在使用 Celery 等工具时,使用“multiprocessing”是很常见的。正确的方法应该是在子进程第一次启动时生成一个新的 Engine,丢弃从父进程继承下来的任何 Engine;或者,从父进程继承的 Engine 可以通过调用 Engine.dispose() 来处理其内部的连接池。

  • 使用 Exit 的 Greenlet Monkeypatching - 当使用类似 gevent 或 eventlet 的库对 Python 网络 API 进行 monkeypatch 时,像 PyMySQL 这样的库现在以异步模式运行,即使它们并没有明确针对此模型开发。一个常见问题是 greenthread 被中断,通常是由于应用程序中的超时逻辑。这导致引发GreenletExit异常,并且纯 Python MySQL 驱动程序被中断了其工作,可能是正在接收来自服务器的响应或准备重新设置连接的状态。当异常中断了所有这些工作时,客户端和服务器之间的对话现在不再同步,连接的后续使用可能会失败。截至版本 1.1.0,SQLAlchemy 知道如何防范这种情况,因为如果数据库操作被所谓的“退出异常”中断,这包括GreenletExit和任何不是也是Exception子类的 Python BaseException的子类,则连接将无效。

  • 回滚 / SAVEPOINT 释放失败 - 某些类别的错误会导致连接在事务上下文中无法使用,以及在“SAVEPOINT”块中操作时无法使用。在这些情况下,连接上的故障使任何 SAVEPOINT 都不再存在,然而当 SQLAlchemy 或应用程序尝试“回滚”此 savepoint 时,“RELEASE SAVEPOINT”操作会失败,通常会出现“savepoint 不存在”的消息。在这种情况下,在 Python 3 下将输出一系列异常,其中最终的错误“原因”也将被显示出来。在 Python 2 下,没有“链接”异常,但是 SQLAlchemy 的最新版本将尝试发出警告,说明原始故障原因,同时仍然抛出 ROLLBACK 失败的立即错误。

如何自动“重试”语句执行?

文档部分 处理断开连接 讨论了对已经断开连接的池化连接可用的策略。在这方面最现代的特性是 create_engine.pre_ping 参数,它允许在从池中检索数据库连接时发出“ping”,如果当前连接已断开,则重新连接。

需要注意的是,此“ping”仅在连接实际用于操作之前发出。一旦连接被提供给调用者,根据 Python DBAPI 规范,它现在已经受到autobegin操作的影响,这意味着当首次使用时,它将自动开始一个新事务,该事务在后续语句中仍然有效,直到调用 DBAPI 级别的 connection.commit()connection.rollback() 方法。

在现代使用 SQLAlchemy 中,一系列 SQL 语句总是在事务状态下调用,假设未启用 DBAPI 自动提交模式(下一节将详细介绍),这意味着没有单个语句会自动提交;如果操作失败,当前事务内所有语句的影响都将丢失。

对于“重试”语句的含义是,默认情况下,当连接丢失时,整个事务都将丢失。数据库无法以有用的方式“重新连接和重试”,并继续上次执行的位置,因为数据已经丢失。因此,SQLAlchemy 没有一个能在事务进行中工作时透明地进行“重新连接”的功能,以处理数据库连接在使用过程中断开的情况。处理中途断开连接的规范方法是从事务开始处重试整个操作,通常通过使用自定义 Python 装饰器多次“重试”特定函数直到成功,或者以其他方式设计应用程序,使其能够抵御事务被中断而导致操作失败的情况。

还有一个概念,即扩展程序可以跟踪事务中已经执行的所有语句,然后在新事务中重新执行它们,以近似实现“重试”操作。SQLAlchemy 的事件系统确实允许构建这样一个系统,但这种方法通常也不实用,因为没有办法保证这些 DML 语句将针对相同的状态进行操作,一旦事务结束,数据库在新事务中的状态可能会完全不同。在事务操作开始和提交的点明确将“重试”架构化到应用程序中仍然是更好的方法,因为应用程序级别的事务方法最了解如何重新运行它们的步骤。

否则,如果 SQLAlchemy 提供了一个透明且静默地在事务中重新连接连接的功能,则效果将是数据被静默丢失。通过试图隐藏问题,SQLAlchemy 将使情况变得更糟。

然而,如果我们使用事务,则会有更多的选择,如下一节所述。

使用 DBAPI 自动提交允许只读版本的透明重新连接

由于没有透明的重新连接机制的理由已经说明,上一节建立在这样一个假设之上,即应用程序实际上正在使用 DBAPI 级别的事务。由于大多数 DBAPI 现在提供了本地的“自动提交”设置,我们可以利用这些特性来为只读、自动提交的操作提供有限形式的透明重新连接。可以将透明的语句重试应用于 DBAPI 的cursor.execute()方法,但仍然不安全应用于 DBAPI 的cursor.executemany()方法,因为该语句可能已经消耗了给定参数的任何部分。

警告

下面的方法应用于写入数据的操作。用户应该仔细阅读和理解该方法的工作原理,并仔细针对具体目标的 DBAPI 驱动程序测试故障模式,然后再在生产中使用该方法。重试机制不能保证在所有情况下都防止断开连接错误。

可以通过利用DialectEvents.do_execute()DialectEvents.do_execute_no_params()钩子来应用于 DBAPI 级别的cursor.execute()方法的简单重试机制,这将能够在语句执行期间拦截断开连接。对于那些不完全缓冲结果集的 DBAPI,它不会拦截在结果集获取操作期间的连接故障。该配方要求数据库支持 DBAPI 级别的自动提交,并且对于特定后端不能保证。提供了一个名为reconnecting_engine()的单个函数,它将事件钩子应用于给定的Engine对象,返回一个始终自动提交的版本,该版本启用了 DBAPI 级别的自动提交。连接将透明地重新连接以进行单参数和无参数语句执行:

import time

from sqlalchemy import event

def reconnecting_engine(engine, num_retries, retry_interval):
    def _run_with_retries(fn, context, cursor_obj, statement, *arg, **kw):
        for retry in range(num_retries + 1):
            try:
                fn(cursor_obj, statement, context=context, *arg)
            except engine.dialect.dbapi.Error as raw_dbapi_err:
                connection = context.root_connection
                if engine.dialect.is_disconnect(raw_dbapi_err, connection, cursor_obj):
                    if retry > num_retries:
                        raise
                    engine.logger.error(
                        "disconnection error, retrying operation",
                        exc_info=True,
                    )
                    connection.invalidate()

                    # use SQLAlchemy 2.0 API if available
                    if hasattr(connection, "rollback"):
                        connection.rollback()
                    else:
                        trans = connection.get_transaction()
                        if trans:
                            trans.rollback()

                    time.sleep(retry_interval)
                    context.cursor = cursor_obj = connection.connection.cursor()
                else:
                    raise
            else:
                return True

    e = engine.execution_options(isolation_level="AUTOCOMMIT")

    @event.listens_for(e, "do_execute_no_params")
    def do_execute_no_params(cursor_obj, statement, context):
        return _run_with_retries(
            context.dialect.do_execute_no_params, context, cursor_obj, statement
        )

    @event.listens_for(e, "do_execute")
    def do_execute(cursor_obj, statement, parameters, context):
        return _run_with_retries(
            context.dialect.do_execute, context, cursor_obj, statement, parameters
        )

    return e

给定上述配方,可以使用以下概念验证脚本演示事务中的重新连接。运行后,它将每五秒向数据库发出一个SELECT 1语句:

from sqlalchemy import create_engine
from sqlalchemy import select

if __name__ == "__main__":
    engine = create_engine("mysql+mysqldb://scott:tiger@localhost/test", echo_pool=True)

    def do_a_thing(engine):
        with engine.begin() as conn:
            while True:
                print("ping: %s" % conn.execute(select([1])).scalar())
                time.sleep(5)

    e = reconnecting_engine(
        create_engine("mysql+mysqldb://scott:tiger@localhost/test", echo_pool=True),
        num_retries=5,
        retry_interval=2,
    )

    do_a_thing(e)

在脚本运行时重新启动数据库以演示透明重连接操作:

$ python reconnect_test.py
ping: 1
ping: 1
disconnection error, retrying operation
Traceback (most recent call last):
  ...
MySQLdb._exceptions.OperationalError: (2006, 'MySQL server has gone away')
2020-10-19 16:16:22,624 INFO sqlalchemy.pool.impl.QueuePool Invalidate connection <_mysql.connection open to 'localhost' at 0xf59240>
ping: 1
ping: 1
...

上述配方已在 SQLAlchemy 1.4 中进行了测试。### 使用 DBAPI 自动提交允许透明重连接的只读版本

在未说明透明重连接机制的理由的情况下,前一节基于这样一种假设,即应用程序实际上正在使用 DBAPI 级别的事务。由于大多数 DBAPI 现在提供本地“自动提交”设置,我们可以利用这些特性为只读,仅自动提交操作提供一种有限形式的透明重连接。透明语句重试可以应用于 DBAPI 的cursor.execute()方法,但是仍然不安全应用于 DBAPI 的cursor.executemany()方法,因为该语句可能已经消耗了给定参数的任何部分。

警告

不应将以下配方用于写入数据的操作。用户应仔细阅读和理解配方的工作原理,并在生产使用此配方之前针对特定的 DBAPI 驱动程序非常仔细地测试故障模式。重试机制不能保证在所有情况下防止断开连接错误。

可以通过使用DialectEvents.do_execute()DialectEvents.do_execute_no_params()钩子向 DBAPI 级别的 cursor.execute() 方法应用简单的重试机制,这些钩子将能够在语句执行期间拦截断开连接。对于那些不完全缓冲结果集的 DBAPI,它将不会拦截结果集获取操作期间的连接故障。该方案要求数据库支持 DBAPI 级别的自动提交,并且不能保证适用于特定的后端。提供了一个名为 reconnecting_engine() 的单个函数,它将事件钩子应用于给定的 Engine 对象,返回一个始终启用 DBAPI 级别自动提交的版本。连接将自动重新连接以用于单参数和无参数语句执行:

import time

from sqlalchemy import event

def reconnecting_engine(engine, num_retries, retry_interval):
    def _run_with_retries(fn, context, cursor_obj, statement, *arg, **kw):
        for retry in range(num_retries + 1):
            try:
                fn(cursor_obj, statement, context=context, *arg)
            except engine.dialect.dbapi.Error as raw_dbapi_err:
                connection = context.root_connection
                if engine.dialect.is_disconnect(raw_dbapi_err, connection, cursor_obj):
                    if retry > num_retries:
                        raise
                    engine.logger.error(
                        "disconnection error, retrying operation",
                        exc_info=True,
                    )
                    connection.invalidate()

                    # use SQLAlchemy 2.0 API if available
                    if hasattr(connection, "rollback"):
                        connection.rollback()
                    else:
                        trans = connection.get_transaction()
                        if trans:
                            trans.rollback()

                    time.sleep(retry_interval)
                    context.cursor = cursor_obj = connection.connection.cursor()
                else:
                    raise
            else:
                return True

    e = engine.execution_options(isolation_level="AUTOCOMMIT")

    @event.listens_for(e, "do_execute_no_params")
    def do_execute_no_params(cursor_obj, statement, context):
        return _run_with_retries(
            context.dialect.do_execute_no_params, context, cursor_obj, statement
        )

    @event.listens_for(e, "do_execute")
    def do_execute(cursor_obj, statement, parameters, context):
        return _run_with_retries(
            context.dialect.do_execute, context, cursor_obj, statement, parameters
        )

    return e

根据上述方案,可以使用以下概念证明脚本演示事务中重新连接。运行一次后,它将每五秒向数据库发出一个SELECT 1语句:

from sqlalchemy import create_engine
from sqlalchemy import select

if __name__ == "__main__":
    engine = create_engine("mysql+mysqldb://scott:tiger@localhost/test", echo_pool=True)

    def do_a_thing(engine):
        with engine.begin() as conn:
            while True:
                print("ping: %s" % conn.execute(select([1])).scalar())
                time.sleep(5)

    e = reconnecting_engine(
        create_engine("mysql+mysqldb://scott:tiger@localhost/test", echo_pool=True),
        num_retries=5,
        retry_interval=2,
    )

    do_a_thing(e)

在脚本运行时重新启动数据库以演示透明的重新连接操作:

$ python reconnect_test.py
ping: 1
ping: 1
disconnection error, retrying operation
Traceback (most recent call last):
  ...
MySQLdb._exceptions.OperationalError: (2006, 'MySQL server has gone away')
2020-10-19 16:16:22,624 INFO sqlalchemy.pool.impl.QueuePool Invalidate connection <_mysql.connection open to 'localhost' at 0xf59240>
ping: 1
ping: 1
...

上述方案已经在 SQLAlchemy 1.4 上进行了测试。

为什么 SQLAlchemy 发出了那么多个 ROLLBACK?

SQLAlchemy 目前假设 DBAPI 连接处于“非自动提交”模式 - 这是 Python 数据库 API 的默认行为,这意味着必须假定事务始终在进行中。连接池在连接返回时发出 connection.rollback()。这是为了释放连接上仍然存在的任何事务资源。在像 PostgreSQL 或 MSSQL 这样的数据库上,表资源被积极地锁定,这一点至关重要,以确保行和表不会在不再使用的连接中保持锁定状态。否则,应用程序可能会挂起。然而,这不仅仅是为了锁定,并且在具有任何类型的事务隔离的任何数据库上同样关键,包括具有 InnoDB 的 MySQL。如果在隔离内在连接上已经查询了该数据,任何仍然处于旧事务中的连接将返回陈旧的数据。有关为什么即使在 MySQL 上也可能看到陈旧数据的背景,请参阅dev.mysql.com/doc/refman/5.1/en/innodb-transaction-model.html

我使用的是 MyISAM - 如何关闭它?

连接池的连接返回行为的行为可以使用 reset_on_return 进行配置:

from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool

engine = create_engine(
    "mysql+mysqldb://scott:tiger@localhost/myisam_database",
    pool=QueuePool(reset_on_return=False),
)

我使用的是 SQL Server - 如何将那些 ROLLBACKs 转换为 COMMITs?

reset_on_return 接受值 commitrollback,除了 TrueFalseNone。设置为 commit 将导致任何连接返回到池时进行 COMMIT:

engine = create_engine(
    "mssql+pyodbc://scott:tiger@mydsn", pool=QueuePool(reset_on_return="commit")
)

我正在使用 MyISAM - 如何关闭它?

可以使用 reset_on_return 配置连接池的连接返回行为:

from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool

engine = create_engine(
    "mysql+mysqldb://scott:tiger@localhost/myisam_database",
    pool=QueuePool(reset_on_return=False),
)

我正在使用 SQL Server - 如何将那些 ROLLBACKs 转换为 COMMITs?

reset_on_return 接受值 commitrollback,除了 TrueFalseNone。设置为 commit 将导致任何连接返回到池时进行 COMMIT:

engine = create_engine(
    "mssql+pyodbc://scott:tiger@mydsn", pool=QueuePool(reset_on_return="commit")
)

我正在使用 SQLite 数据库的多个连接(通常用于测试事务操作),但我的测试程序不起作用!

如果使用 SQLite 的 :memory: 数据库,默认连接池是 SingletonThreadPool,它每个线程维护一个 SQLite 连接。因此,在同一线程中使用两个连接实际上是相同的 SQLite 连接。确保您不是使用 :memory: 数据库,以便引擎将使用 QueuePool(当前 SQLAlchemy 版本中非内存数据库的默认值)。

另请参阅

线程/池行为 - 有关 PySQLite 行为的信息。

在使用 Engine 时如何访问原始的 DBAPI 连接?

使用常规的 SA 引擎级 Connection,您可以通过 Connection.connection 属性获取到一个池代理版本的 DBAPI 连接,并且对于真正的 DBAPI 连接,您可以在此调用 PoolProxiedConnection.dbapi_connection 属性。在常规的同步驱动程序中,通常不需要访问非池代理的 DBAPI 连接,因为所有方法都是通过代理的:

engine = create_engine(...)
conn = engine.connect()

# pep-249 style PoolProxiedConnection (historically called a "connection fairy")
connection_fairy = conn.connection

# typically to run statements one would get a cursor() from this
# object
cursor_obj = connection_fairy.cursor()
# ... work with cursor_obj

# to bypass "connection_fairy", such as to set attributes on the
# unproxied pep-249 DBAPI connection, use .dbapi_connection
raw_dbapi_connection = connection_fairy.dbapi_connection

# the same thing is available as .driver_connection (more on this
# in the next section)
also_raw_dbapi_connection = connection_fairy.driver_connection

在版本 1.4.24 中更改:添加了 PoolProxiedConnection.dbapi_connection 属性,它取代了以前的 PoolProxiedConnection.connection 属性,后者仍然可用;此属性始终提供 pep-249 同步风格的连接对象。还添加了 PoolProxiedConnection.driver_connection 属性,它将始终引用真正的驱动程序级连接,无论它呈现什么 API。

访问 asyncio 驱动程序的底层连接

在使用 asyncio 驱动程序时,上述方案有两个变化。首先是在使用AsyncConnection时,必须使用可等待方法AsyncConnection.get_raw_connection()来访问PoolProxiedConnection。在这种情况下返回的PoolProxiedConnection保留了同步风格的 pep-249 使用模式,而PoolProxiedConnection.dbapi_connection属性指的是一个将 asyncio 连接适配为同步风格 pep-249 API 的 SQLAlchemy 适配连接对象,换句话说,在使用 asyncio 驱动程序时存在两层代理。实际的 asyncio 连接可以从driver_connection属性中获取。将上述示例重新阐述为 asyncio 的形式如下:

async def main():
    engine = create_async_engine(...)
    conn = await engine.connect()

    # pep-249 style ConnectionFairy connection pool proxy object
    # presents a sync interface
    connection_fairy = await conn.get_raw_connection()

    # beneath that proxy is a second proxy which adapts the
    # asyncio driver into a pep-249 connection object, accessible
    # via .dbapi_connection as is the same with a sync API
    sqla_sync_conn = connection_fairy.dbapi_connection

    # the really-real innermost driver connection is available
    # from the .driver_connection attribute
    raw_asyncio_connection = connection_fairy.driver_connection

    # work with raw asyncio connection
    result = await raw_asyncio_connection.execute(...)

自版本 1.4.24 起更改:添加了PoolProxiedConnection.dbapi_connectionPoolProxiedConnection.driver_connection属性,以允许通过一致的接口访问 pep-249 连接、pep-249 适配层和底层驱动程序连接。

在使用 asyncio 驱动程序时,上述“DBAPI”连接实际上是 SQLAlchemy 适配的连接形式,它呈现了同步风格的 pep-249 风格 API。要访问实际的 asyncio 驱动程序连接,可以通过PoolProxiedConnection.driver_connection属性来访问PoolProxiedConnection。对于标准的 pep-249 驱动程序,PoolProxiedConnection.dbapi_connectionPoolProxiedConnection.driver_connection是同义词。

在将连接返回到池之前,必须确保将连接上的任何隔离级别设置或其他操作特定设置恢复为正常状态。

作为恢复设置的替代方案,您可以在 Connection 或代理连接上调用 Connection.detach() 方法,这将使连接与池解除关联,从而在调用 Connection.close() 时关闭和丢弃它:

conn = engine.connect()
conn.detach()  # detaches the DBAPI connection from the connection pool
conn.connection.<go nuts>
conn.close()  # connection is closed for real, the pool replaces it with a new connection

使用 asyncio 驱动程序访问底层连接

当使用 asyncio 驱动程序时,对上述方案有两个变化。首先是当使用 AsyncConnection 时,必须使用可等待方法 AsyncConnection.get_raw_connection() 访问 PoolProxiedConnection。在这种情况下返回的 PoolProxiedConnection 保留了同步样式 pep-249 使用模式,并且 PoolProxiedConnection.dbapi_connection 属性指向一个 SQLAlchemy 适配的连接对象,将 asyncio 连接适配为同步样式 pep-249 API,换句话说,当使用 asyncio 驱动程序时会有两层代理。实际的 asyncio 连接可以从 driver_connection 属性获得。在 asyncio 方面重新表述上一个示例如下:

async def main():
    engine = create_async_engine(...)
    conn = await engine.connect()

    # pep-249 style ConnectionFairy connection pool proxy object
    # presents a sync interface
    connection_fairy = await conn.get_raw_connection()

    # beneath that proxy is a second proxy which adapts the
    # asyncio driver into a pep-249 connection object, accessible
    # via .dbapi_connection as is the same with a sync API
    sqla_sync_conn = connection_fairy.dbapi_connection

    # the really-real innermost driver connection is available
    # from the .driver_connection attribute
    raw_asyncio_connection = connection_fairy.driver_connection

    # work with raw asyncio connection
    result = await raw_asyncio_connection.execute(...)

从版本 1.4.24 开始更改:添加了 PoolProxiedConnection.dbapi_connectionPoolProxiedConnection.driver_connection 属性,以允许使用一致的接口访问 pep-249 连接、pep-249 适配层和底层驱动程序连接。

在使用 asyncio 驱动程序时,上述“DBAPI”连接实际上是一个经过 SQLAlchemy 适配的连接形式,它呈现了一个同步风格的 pep-249 风格 API。要访问实际的 asyncio 驱动程序连接,它将呈现所使用驱动程序的原始 asyncio API,可以通过PoolProxiedConnectionPoolProxiedConnection.driver_connection属性进行访问。对于标准的 pep-249 驱动程序,PoolProxiedConnection.dbapi_connectionPoolProxiedConnection.driver_connection 是同义词。

在将连接返回到池之前,您必须确保将任何隔离级别设置或其他特定操作设置恢复为正常状态。

作为恢复设置的替代方案,您可以在Connection或代理连接上调用Connection.detach()方法,这将使连接与池解除关联,从而在调用Connection.close()时关闭并丢弃连接:

conn = engine.connect()
conn.detach()  # detaches the DBAPI connection from the connection pool
conn.connection.<go nuts>
conn.close()  # connection is closed for real, the pool replaces it with a new connection

我如何在 Python 多进程或 os.fork() 中使用引擎/连接/会话?

这在使用连接池与多进程或 os.fork()一节中有详细介绍。

元数据 / 模式

原文:docs.sqlalchemy.org/en/20/faq/metadata_schema.html

  • 当我说 table.drop() / metadata.drop_all() 时,我的程序挂起了

  • SQLAlchemy 支持 ALTER TABLE、CREATE VIEW、CREATE TRIGGER、Schema 升级功能吗?

  • 我如何按照它们的依赖关系对 Table 对象进行排序?

  • 我如何将 CREATE TABLE/ DROP TABLE 输出作为字符串获取?

  • 我如何子类化 Table/Column 以提供某些行为/配置?

当我说 table.drop() / metadata.drop_all() 时,我的程序挂起了

这通常对应两种情况:1. 使用 PostgreSQL,它对表锁非常严格,2. 您仍然打开了一个包含对表的锁定的连接,并且与用于 DROP 语句的连接不同。这是模式的最简化版本:

connection = engine.connect()
result = connection.execute(mytable.select())

mytable.drop(engine)

上述,连接池连接仍然被检出;此外,上述结果对象还保持对此连接的链接。如果使用“隐式执行”,结果将保持此连接打开,直到结果对象关闭或所有行都被耗尽。

调用 mytable.drop(engine) 试图在从 Engine 获取的第二连接上发出 DROP TABLE 操作,这将会被锁定。

解决方法是在发出 DROP TABLE 前关闭所有连接:

connection = engine.connect()
result = connection.execute(mytable.select())

# fully read result sets
result.fetchall()

# close connections
connection.close()

# now locks are removed
mytable.drop(engine)

SQLAlchemy 支持 ALTER TABLE、CREATE VIEW、CREATE TRIGGER、Schema 升级功能吗?

SQLAlchemy 直接不支持通用 ALTER。对于特殊的 ad-hoc 基础 DDL,可以使用 DDL 和相关构造。有关此主题的讨论,请参阅 自定义 DDL。

更全面的选择是使用模式迁移工具,例如 Alembic 或 SQLAlchemy-Migrate;有关此问题的讨论,请参阅 通过迁移修改数据库对象。

我如何按照它们的依赖关系对 Table 对象进行排序?

这可以通过 MetaData.sorted_tables 函数获得:

metadata_obj = MetaData()
# ... add Table objects to metadata
ti = metadata_obj.sorted_tables
for t in ti:
    print(t)

如何将 CREATE TABLE/ DROP TABLE 输出作为字符串获取?

现代的 SQLAlchemy 具有表示 DDL 操作的从句构造。这些可以像任何其他 SQL 表达式一样渲染为字符串:

from sqlalchemy.schema import CreateTable

print(CreateTable(mytable))

要获得特定于某个引擎的字符串:

print(CreateTable(mytable).compile(engine))

还有一种特殊形式的Engine可通过create_mock_engine()访问,允许将整个元数据创建序列转储为字符串,使用以下方法:

from sqlalchemy import create_mock_engine

def dump(sql, *multiparams, **params):
    print(sql.compile(dialect=engine.dialect))

engine = create_mock_engine("postgresql+psycopg2://", dump)
metadata_obj.create_all(engine, checkfirst=False)

Alembic 工具还支持一种“离线”SQL 生成模式,将数据库迁移呈现为 SQL 脚本。

如何对表/列进行子类化以提供特定的行为/配置?

TableColumn 不是直接子类化的良好目标。但是,可以使用创建函数来获取构造时的行为,并使用附加事件来处理与模式对象之间的链接,例如约束约定或命名约定。可以在 命名约定 中看到许多这些技术的示例。

当我说table.drop() / metadata.drop_all()时,我的程序挂起了。

这通常对应于两个条件:1. 使用 PostgreSQL,它对表锁非常严格,2. 你有一个仍然打开的连接,其中包含对表的锁,并且与用于 DROP 语句的连接不同。以下是该模式的最简版本:

connection = engine.connect()
result = connection.execute(mytable.select())

mytable.drop(engine)

上面,连接池连接仍然被检出;此外,上述结果对象还维护对此连接的链接。如果使用“隐式执行”,结果将保持此连接打开,直到关闭结果对象或耗尽所有行。

mytable.drop(engine)的调用尝试在从Engine获取的第二个连接上发出 DROP TABLE,这会导致锁定。

解决方案是在发出 DROP TABLE 前关闭所有连接:

connection = engine.connect()
result = connection.execute(mytable.select())

# fully read result sets
result.fetchall()

# close connections
connection.close()

# now locks are removed
mytable.drop(engine)

SQLAlchemy 支持 ALTER TABLE、CREATE VIEW、CREATE TRIGGER、Schema 升级功能吗?

SQLAlchemy 并不直接支持一般的 ALTER。对于特殊的按需 DDL,可以使用DDL和相关构造。有关此主题的讨论,请参阅 自定义 DDL。

更全面的选项是使用模式迁移工具,例如 Alembic 或 SQLAlchemy-Migrate;请参阅 通过迁移更改数据库对象 以讨论此问题。

如何按其依赖顺序对 Table 对象进行排序?

可通过MetaData.sorted_tables函数进行访问:

metadata_obj = MetaData()
# ... add Table objects to metadata
ti = metadata_obj.sorted_tables
for t in ti:
    print(t)

如何将 CREATE TABLE/DROP TABLE 输出为字符串?

现代的 SQLAlchemy 有表示 DDL 操作的子句构造。这些可以像任何其他 SQL 表达式一样渲染为字符串:

from sqlalchemy.schema import CreateTable

print(CreateTable(mytable))

要获取特定引擎的字符串:

print(CreateTable(mytable).compile(engine))

还有一种特殊形式的 Engine 可以通过 create_mock_engine() 获得,它允许将整个元数据创建序列转储为字符串,使用以下方法:

from sqlalchemy import create_mock_engine

def dump(sql, *multiparams, **params):
    print(sql.compile(dialect=engine.dialect))

engine = create_mock_engine("postgresql+psycopg2://", dump)
metadata_obj.create_all(engine, checkfirst=False)

Alembic 工具还支持一种“离线”SQL 生成模式,将数据库迁移呈现为 SQL 脚本。

我如何子类化 Table/Column 以提供某些行为/配置?

TableColumn 不适合直接进行子类化。但是,可以使用创建函数来获得在构造时的行为,以及使用附加事件来处理模式对象之间的链接行为,比如约束惯例或命名惯例。许多这些技术的示例可以在 命名约定 中看到。

SQL 表达式

原文:docs.sqlalchemy.org/en/20/faq/sqlexpressions.html

  • 如何将 SQL 表达式呈现为字符串,可能包含内联的绑定参数?

    • 针对特定数据库进行字符串化

    • 内联呈现绑定参数

    • 将“POSTCOMPILE”参数呈现为绑定参数

  • 在字符串化 SQL 语句时为什么百分号会被双倍显示?

  • 我正在使用 op() 生成自定义运算符,但我的括号没有正确显示

    • 为什么括号规则是这样的?

如何将 SQL 表达式呈现为字符串,可能包含内联的绑定参数?

SQLAlchemy Core 语句对象或表达式片段的“字符串化”,以及 ORM Query 对象,在大多数简单情况下都可以简单地使用 str() 内置函数来实现,如下所示,当与 print 函数一起使用时(请注意 Python print 函数如果不显式使用 str(),也会自动调用它):

>>> from sqlalchemy import table, column, select
>>> t = table("my_table", column("x"))
>>> statement = select(t)
>>> print(str(statement))
SELECT  my_table.x
FROM  my_table 

str() 内置函数或等效函数,可在 ORM Query 对象上调用,也可在诸如 select()insert() 等语句上调用,还可在任何表达式片段上调用,例如:

>>> from sqlalchemy import column
>>> print(column("x") == "some value")
x  =  :x_1 

针对特定数据库进行字符串化

当我们要将语句或片段字符串化时,如果包含具有特定于数据库的字符串格式的元素,或者包含仅在某种类型的数据库中可用的元素,则会出现复杂情况。在这些情况下,我们可能会得到一个不符合我们目标数据库正确语法的字符串化语句,或者操作可能会引发一个UnsupportedCompilationError异常。在这些情况下,有必要使用ClauseElement.compile()方法将语句字符串化,同时传递一个代表目标数据库的EngineDialect对象。例如,如果我们有一个 MySQL 数据库引擎,我们可以按照 MySQL 方言字符串化一个语句:

from sqlalchemy import create_engine

engine = create_engine("mysql+pymysql://scott:tiger@localhost/test")
print(statement.compile(engine))

更直接地,不需要构建一个Engine对象,我们可以直接实例化一个Dialect对象,如下所示,我们使用一个 PostgreSQL 方言:

from sqlalchemy.dialects import postgresql

print(statement.compile(dialect=postgresql.dialect()))

请注意,任何方言都可以使用create_engine()本身组装,使用一个虚拟 URL,然后访问Engine.dialect属性,比如如果我们想要一个 psycopg2 的方言对象:

e = create_engine("postgresql+psycopg2://")
psycopg2_dialect = e.dialect

给定一个 ORM Query对象时,为了访问ClauseElement.compile()方法,我们只需要首先访问Query.statement访问器:

statement = query.statement
print(statement.compile(someengine))

内联渲染绑定参数

警告

永远不要使用这些技术处理来自不受信任输入的字符串内容,比如来自 Web 表单或其他用户输入应用程序。SQLAlchemy 将 Python 值强制转换为直接 SQL 字符串值的功能不安全,并且不验证传递的数据类型。在针对关系数据库编程调用非 DDL SQL 语句时,始终使用绑定参数。

上述形式将渲染 SQL 语句,因为它被传递到 Python DBAPI,其中包括绑定参数不会内联渲染。SQLAlchemy 通常不会对绑定参数进行字符串化处理,因为这由 Python DBAPI 适当处理,更不用说绕过绑定参数可能是现代 Web 应用中被广泛利用的安全漏洞之一了。SQLAlchemy 在某些情况下有限的能力执行此字符串化,例如发出 DDL 时。为了访问此功能,可以使用传递给compile_kwargsliteral_binds标志:

from sqlalchemy.sql import table, column, select

t = table("t", column("x"))

s = select(t).where(t.c.x == 5)

# **do not use** with untrusted input!!!
print(s.compile(compile_kwargs={"literal_binds": True}))

# to render for a specific dialect
print(s.compile(dialect=dialect, compile_kwargs={"literal_binds": True}))

# or if you have an Engine, pass as first argument
print(s.compile(some_engine, compile_kwargs={"literal_binds": True}))

此功能主要用于日志记录或调试目的,其中获得查询的原始 sql 字符串可能会很有用。

上述方法的注意事项是,它仅支持基本类型,如整数和字符串,而且如果直接使用未设置预设值的bindparam(),它也无法对其进行字符串化处理。下面详细介绍了无条件对所有参数进行字符串化的方法。

提示

SQLAlchemy 不支持对所有数据类型进行完全字符串化的原因有三个:

  1. 当正常使用 DBAPI 时,该功能已被当前 DBAPI 支持。SQLAlchemy 项目不能被要求为所有后端的所有数据类型重复这种功能,因为这是多余的工作,还带来了重大的测试和持续支持开销。

  2. 对于特定数据库的绑定参数进行字符串化建议一种实际上将这些完全字符串化的语句传递给数据库以进行执行的用法。这是不必要和不安全的,SQLAlchemy 不希望以任何方式鼓励这种用法。

  3. 渲染字面值的区域是最有可能报告安全问题的地方。SQLAlchemy 尽量将安全参数字符串化的区域留给 DBAPI 驱动程序,这样每个 DBAPI 的具体细节可以得到适当和安全地处理。

由于 SQLAlchemy 故意不支持对所有数据类型的完全字符串化,因此在特定调试场景下执行此操作的技术包括以下内容。作为示例,我们将使用 PostgreSQL 的UUID数据类型:

import uuid

from sqlalchemy import Column
from sqlalchemy import create_engine
from sqlalchemy import Integer
from sqlalchemy import select
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import declarative_base

Base = declarative_base()

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    data = Column(UUID)

stmt = select(A).where(A.data == uuid.uuid4())

给定上述模型和语句,将比较一列与单个 UUID 值,将此语句与内联值一起进行字符串化的选项包括:

  • 一些 DBAPI,如 psycopg2,支持像mogrify()这样的辅助函数,提供对它们的字面渲染功能的访问。要使用此类功能,请渲染 SQL 字符串而不使用literal_binds,并通过SQLCompiler.params访问器分别传递参数:

    e = create_engine("postgresql+psycopg2://scott:tiger@localhost/test")
    
    with e.connect() as conn:
        cursor = conn.connection.cursor()
        compiled = stmt.compile(e)
    
        print(cursor.mogrify(str(compiled), compiled.params))
    

    上述代码将生成 psycopg2 的原始字节串:

    b"SELECT a.id, a.data \nFROM a \nWHERE a.data = 'a511b0fc-76da-4c47-a4b4-716a8189b7ac'::uuid"
    
  • 直接将SQLCompiler.params渲染到语句中,使用目标 DBAPI 的适当paramstyle。例如,psycopg2 DBAPI 使用命名的pyformat样式。render_postcompile的含义将在下一节中讨论。警告,这是不安全的,请勿使用不受信任的输入

    e = create_engine("postgresql+psycopg2://")
    
    # will use pyformat style, i.e. %(paramname)s for param
    compiled = stmt.compile(e, compile_kwargs={"render_postcompile": True})
    
    print(str(compiled) % compiled.params)
    

    这将产生一个非工作的字符串,但仍然适合调试:

    SELECT  a.id,  a.data
    FROM  a
    WHERE  a.data  =  9eec1209-50b4-4253-b74b-f82461ed80c1
    

    另一个示例使用位置参数风格,如qmark,我们可以使用SQLCompiler.positiontup集合与SQLCompiler.params一起编译我们上面的语句,以便按其位置顺序检索语句的参数:

    import re
    
    e = create_engine("sqlite+pysqlite://")
    
    # will use qmark style, i.e. ? for param
    compiled = stmt.compile(e, compile_kwargs={"render_postcompile": True})
    
    # params in positional order
    params = (repr(compiled.params[name]) for name in compiled.positiontup)
    
    print(re.sub(r"\?", lambda m: next(params), str(compiled)))
    

    上述片段打印:

    SELECT  a.id,  a.data
    FROM  a
    WHERE  a.data  =  UUID('1bd70375-db17-4d8c-94f1-fc2ef3aada26')
    
  • 使用自定义 SQL 构造和编译扩展扩展,在用户定义的标志存在时以自定义方式渲染BindParameter对象。这个标志通过compile_kwargs字典发送,就像任何其他标志一样:

    from sqlalchemy.ext.compiler import compiles
    from sqlalchemy.sql.expression import BindParameter
    
    @compiles(BindParameter)
    def _render_literal_bindparam(element, compiler, use_my_literal_recipe=False, **kw):
        if not use_my_literal_recipe:
            # use normal bindparam processing
            return compiler.visit_bindparam(element, **kw)
    
        # if use_my_literal_recipe was passed to compiler_kwargs,
        # render the value directly
        return repr(element.value)
    
    e = create_engine("postgresql+psycopg2://")
    print(stmt.compile(e, compile_kwargs={"use_my_literal_recipe": True}))
    

    上述配方将打印:

    SELECT  a.id,  a.data
    FROM  a
    WHERE  a.data  =  UUID('47b154cd-36b2-42ae-9718-888629ab9857')
    
  • 对于内置于模型或语句中的特定类型的字符串化,可以使用TypeDecorator类使用TypeDecorator.process_literal_param()方法来提供任何数据类型的自定义字符串化:

    from sqlalchemy import TypeDecorator
    
    class UUIDStringify(TypeDecorator):
        impl = UUID
    
        def process_literal_param(self, value, dialect):
            return repr(value)
    

    上述数据类型需要在模型内部明确使用,或者在语句内部使用type_coerce(),例如

    from sqlalchemy import type_coerce
    
    stmt = select(A).where(type_coerce(A.data, UUIDStringify) == uuid.uuid4())
    
    print(stmt.compile(e, compile_kwargs={"literal_binds": True}))
    

    再次打印相同形式:

    SELECT  a.id,  a.data
    FROM  a
    WHERE  a.data  =  UUID('47b154cd-36b2-42ae-9718-888629ab9857')
    

将“POSTCOMPILE”参数渲染为绑定参数

SQLAlchemy 包含一个称为 BindParameter.expanding 的绑定参数变体,这是一个“延迟评估”的参数,当 SQL 构造编译时以中间状态呈现,然后在语句执行时进一步处理,当传递实际已知值时。默认情况下,通过 ColumnOperators.in_() 表达式使用“扩展”参数,以便 SQL 字符串可以安全地独立缓存,而不受传递给 ColumnOperators.in_() 的特定调用的实际值列表的影响:

>>> stmt = select(A).where(A.id.in_([1, 2, 3]))

若要将 IN 子句呈现为真实的绑定参数符号,请在 ClauseElement.compile() 中使用 render_postcompile=True 标志:

>>> e = create_engine("postgresql+psycopg2://")
>>> print(stmt.compile(e, compile_kwargs={"render_postcompile": True}))
SELECT  a.id,  a.data
FROM  a
WHERE  a.id  IN  (%(id_1_1)s,  %(id_1_2)s,  %(id_1_3)s) 

在关于呈现绑定参数的前一节中描述的 literal_binds 标志会自动将 render_postcompile 设置为 True,因此对于带有简单整数/字符串的语句,这些可以直接转换为字符串:

# render_postcompile is implied by literal_binds
>>> print(stmt.compile(e, compile_kwargs={"literal_binds": True}))
SELECT  a.id,  a.data
FROM  a
WHERE  a.id  IN  (1,  2,  3) 

SQLCompiler.paramsSQLCompiler.positiontup 也与 render_postcompile 兼容,因此在这里以相同的方式工作,例如 SQLite 的位置形式:

>>> u1, u2, u3 = uuid.uuid4(), uuid.uuid4(), uuid.uuid4()
>>> stmt = select(A).where(A.data.in_([u1, u2, u3]))

>>> import re
>>> e = create_engine("sqlite+pysqlite://")
>>> compiled = stmt.compile(e, compile_kwargs={"render_postcompile": True})
>>> params = (repr(compiled.params[name]) for name in compiled.positiontup)
>>> print(re.sub(r"\?", lambda m: next(params), str(compiled)))
SELECT  a.id,  a.data
FROM  a
WHERE  a.data  IN  (UUID('aa1944d6-9a5a-45d5-b8da-0ba1ef0a4f38'),  UUID('a81920e6-15e2-4392-8a3c-d775ffa9ccd2'),  UUID('b5574cdb-ff9b-49a3-be52-dbc89f087bfa')) 

警告

请记住,所有上述代码配方都是用于字符串化字面值,在将语句发送到数据库时绕过绑定参数的情况下,仅适用于:

  1. 使用仅限于调试目的

  2. 字符串不应传递到活动的生产数据库

  3. 仅与本地、可信赖的输入一起使用

上述用于字符串化字面值的配方在任何情况下都不安全,绝不应该用于生产数据库。## 字符串化 SQL 语句时为什么要双倍百分号?

许多 DBAPI 实现使用 pyformatformat paramstyle,其语法中必然涉及百分号。大多数这样做的 DBAPI 期望在用于语句的字符串形式中,百分号用于其他目的时应该是双倍的(即转义),例如:

SELECT  a,  b  FROM  some_table  WHERE  a  =  %s  AND  c  =  %s  AND  num  %%  modulus  =  0

当 SQL 语句由 SQLAlchemy 传递给底层的 DBAPI 时,绑定参数的替换方式与 Python 字符串插值运算符 % 相同,在许多情况下,DBAPI 实际上直接使用此运算符。以上,绑定参数的替换看起来像:

SELECT  a,  b  FROM  some_table  WHERE  a  =  5  AND  c  =  10  AND  num  %  modulus  =  0

像 PostgreSQL(默认 DBAPI 是 psycopg2)和 MySQL(默认 DBAPI 是 mysqlclient)这样的数据库的默认编译器将具有百分号转义行为:

>>> from sqlalchemy import table, column
>>> from sqlalchemy.dialects import postgresql
>>> t = table("my_table", column("value % one"), column("value % two"))
>>> print(t.select().compile(dialect=postgresql.dialect()))
SELECT  my_table."value %% one",  my_table."value %% two"
FROM  my_table 

当使用此类方言时,如果需要非 DBAPI 语句,而这些语句不包括绑定的参数符号,则可通过直接使用 Python 的 % 运算符来简单地替换空参数集来删除百分号:

>>> strstmt = str(t.select().compile(dialect=postgresql.dialect()))
>>> print(strstmt % ())
SELECT  my_table."value % one",  my_table."value % two"
FROM  my_table 

另一种方法是在使用的方言上设置不同的参数样式;所有 Dialect 实现都接受一个参数 paramstyle,将导致该方言的编译器使用给定的参数样式。下面,在用于编译的方言中设置了非常常见的 named 参数样式,以便百分号在 SQL 的编译形式中不再具有重要意义,并且将不再被转义:

>>> print(t.select().compile(dialect=postgresql.dialect(paramstyle="named")))
SELECT  my_table."value % one",  my_table."value % two"
FROM  my_table 
```  ## 我使用 op() 来生成自定义操作符,但是我的括号没有正确显示

`Operators.op()` 方法允许创建自定义数据库操作符,否则 SQLAlchemy 不会识别:

```py
>>> print(column("q").op("->")(column("p")))
q  ->  p 

但是,当在复合表达式的右侧使用时,它不会按我们的预期生成括号:

>>> print((column("q1") + column("q2")).op("->")(column("p")))
q1  +  q2  ->  p 

在上面的情况下,我们可能希望 (q1 + q2) -> p

对于此情况的解决方案是设置操作符的优先级,使用 Operators.op.precedence 参数,将其设置为一个较高的数字,其中 100 是最大值,而 SQLAlchemy 当前使用的任何操作符的最高数字为 15

>>> print((column("q1") + column("q2")).op("->", precedence=100)(column("p")))
(q1  +  q2)  ->  p 

我们还可以使用 ColumnElement.self_group() 方法通常强制将二元表达式(例如具有左/右操作数和运算符的表达式)括在括号中:

>>> print((column("q1") + column("q2")).self_group().op("->")(column("p")))
(q1  +  q2)  ->  p 

为什么括号规则是这样的?

当存在过多的括号或括号处于它们不期望的不寻常位置时,很多数据库都会出现问题,因此 SQLAlchemy 不会基于分组生成括号,它使用操作符优先级,如果操作符已知是可结合的,那么生成的括号将最小化。否则,像下面这样的表达式:

column("a") & column("b") & column("c") & column("d")

将产生:

(((a  AND  b)  AND  c)  AND  d)

这是可以接受的,但可能会让人们感到恼火(并被报告为错误)。在其他情况下,它会导致更容易混淆数据库或至少可读性更差的事物,例如:

column("q", ARRAY(Integer, dimensions=2))[5][6]

将产生:

((q[5])[6])

也有一些边缘情况,我们会得到类似"(x) = 7"这样的东西,数据库真的不喜欢这样。所以括号化并不是简单地加括号,它使用运算符优先级和结合性来确定分组。

对于Operators.op(),优先级的值默认为零。

如果我们将Operators.op.precedence的值默认为 100,例如最高值,会怎么样?然后这个表达式会加更多括号,但其他方面都没问题,也就是说,这两个是等价的:

>>> print((column("q") - column("y")).op("+", precedence=100)(column("z")))
(q  -  y)  +  z
>>> print((column("q") - column("y")).op("+")(column("z")))
q  -  y  +  z 

但这两个不是:

>>> print(column("q") - column("y").op("+", precedence=100)(column("z")))
q  -  y  +  z
>>> print(column("q") - column("y").op("+")(column("z")))
q  -  (y  +  z) 

目前,尚不清楚只要我们根据运算符优先级和结合性进行括号化,是否真的有一种方法可以自动为没有给定优先级的通用运算符进行括号化,以使其在所有情况下都有效,因为有时您希望自定义运算符具有比其他运算符更低的优先级,有时您希望它更高。

可能如果上面的“二元”表达式在调用op()时强制使用self_group()方法,假设左侧的复合表达式总是可以无害地加括号。也许这种改变可以在某个时候实现,但是目前保持括号化规则更加内部一致似乎是更安全的方法。 ## 如何将 SQL 表达式呈现为字符串,可能包含内联的绑定参数?

在大多数简单情况下,将 SQLAlchemy Core 语句对象或表达式片段以及 ORM Query 对象“字符串化”,就像在使用str()内置函数时一样简单,如下所示,当与print函数一起使用时(请注意 Python 的print函数如果我们不显式使用str(),也会自动调用它):

>>> from sqlalchemy import table, column, select
>>> t = table("my_table", column("x"))
>>> statement = select(t)
>>> print(str(statement))
SELECT  my_table.x
FROM  my_table 

内置函数str(),或者等效函数,可以在 ORM Query 对象上调用,也可以在任何语句上调用,比如select()insert()等,以及任何表达式片段,比如:

>>> from sqlalchemy import column
>>> print(column("x") == "some value")
x  =  :x_1 

针对特定数据库的字符串化

当我们要字符串化的语句或片段包含具有数据库特定字符串格式的元素,或者包含仅在某种类型的数据库中可用的元素时,会出现一个复杂性。在这些情况下,我们可能会得到一个不符合我们所针对的数据库的正确语法的字符串化语句,或者该操作可能会引发一个UnsupportedCompilationError异常。在这些情况下,必须使用ClauseElement.compile()方法对语句进行字符串化,同时传递一个表示目标数据库的EngineDialect对象。例如,如果我们有一个 MySQL 数据库引擎,我们可以如下将语句字符串化为 MySQL 方言:

from sqlalchemy import create_engine

engine = create_engine("mysql+pymysql://scott:tiger@localhost/test")
print(statement.compile(engine))

更直接地,不需要构建Engine对象,我们可以直接实例化一个Dialect对象,如下所示,我们使用 PostgreSQL 方言:

from sqlalchemy.dialects import postgresql

print(statement.compile(dialect=postgresql.dialect()))

请注意,可以使用create_engine()本身来组装任何方言,只需使用一个虚拟 URL 并访问Engine.dialect属性即可,例如,如果我们想要 psycopg2 的方言对象:

e = create_engine("postgresql+psycopg2://")
psycopg2_dialect = e.dialect

给定一个 ORM Query 对象,为了获取ClauseElement.compile()方法,我们只需要先访问Query.statement访问器:

statement = query.statement
print(statement.compile(someengine))

将绑定参数嵌入渲染

警告

永远不要将这些技术与来自不受信任输入的字符串内容一起使用,例如来自 Web 表单或其他用户输入应用程序。SQLAlchemy 将 Python 值强制转换为直接 SQL 字符串值的设施不安全,不安全地针对不受信任的输入,并且不验证传递的数据类型。在针对关系数据库程序化地调用非 DDL SQL 语句时,始终使用绑定参数。

上述形式将呈现 SQL 语句,就像它传递给 Python DBAPI 一样,其中绑定参数不会被内联呈现。SQLAlchemy 通常不会将绑定参数字符串化,因为这由 Python DBAPI 适当处理,更不用说绕过绑定参数可能是现代 Web 应用程序中最广泛利用的安全漏洞之一。SQLAlchemy 在某些情况下有限地能够执行此字符串化,比如发出 DDL。为了访问此功能,可以使用传递给 compile_kwargsliteral_binds 标志:

from sqlalchemy.sql import table, column, select

t = table("t", column("x"))

s = select(t).where(t.c.x == 5)

# **do not use** with untrusted input!!!
print(s.compile(compile_kwargs={"literal_binds": True}))

# to render for a specific dialect
print(s.compile(dialect=dialect, compile_kwargs={"literal_binds": True}))

# or if you have an Engine, pass as first argument
print(s.compile(some_engine, compile_kwargs={"literal_binds": True}))

此功能主要用于记录或调试目的,其中查询的原始 SQL 字符串可能会证明有用。

上述方法的注意事项是它仅支持基本类型,如整数和字符串,而且如果直接使用没有预设值的 bindparam(),它也无法将其字符串化。无条件地将所有参数字符串化的方法如下所述。

提示

SQLAlchemy 不支持所有数据类型的完全字符串化的原因有三个:

  1. 当正常使用 DBAPI 时,这是已经受支持的功能。SQLAlchemy 项目无法被要求为所有后端的每种数据类型复制这种功能,因为这是多余的工作,还会带来重大的测试和持续支持开销。

  2. 使用内联绑定参数进行字符串化,针对特定数据库,表明了一种实际将这些完全字符串化的语句传递到数据库执行的用法。这是不必要且不安全的,SQLAlchemy 不希望以任何方式鼓励这种用法。

  3. 渲染字面值的领域是最有可能报告安全问题的领域。SQLAlchemy 尽量将安全参数字符串化的问题留给 DBAPI 驱动程序处理,其中每个 DBAPI 的具体情况可以得到适当和安全的处理。

由于 SQLAlchemy 故意不支持对字面值的完全字符串化,因此在特定调试场景中执行此操作的技术包括以下内容。作为示例,我们将使用 PostgreSQL 的 UUID 数据类型:

import uuid

from sqlalchemy import Column
from sqlalchemy import create_engine
from sqlalchemy import Integer
from sqlalchemy import select
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import declarative_base

Base = declarative_base()

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    data = Column(UUID)

stmt = select(A).where(A.data == uuid.uuid4())

鉴于上述模型和语句,将比较列与单个 UUID 值,将此语句与内联值字符串化的选项包括:

  • 一些 DBAPI,如 psycopg2,支持像 mogrify() 这样的辅助函数,提供对它们的字面渲染功能的访问。要使用这些功能,渲染 SQL 字符串时不要使用 literal_binds,并通过 SQLCompiler.params 访问器单独传递参数:

    e = create_engine("postgresql+psycopg2://scott:tiger@localhost/test")
    
    with e.connect() as conn:
        cursor = conn.connection.cursor()
        compiled = stmt.compile(e)
    
        print(cursor.mogrify(str(compiled), compiled.params))
    

    上述代码将生成 psycopg2 的原始字节串:

    b"SELECT a.id, a.data \nFROM a \nWHERE a.data = 'a511b0fc-76da-4c47-a4b4-716a8189b7ac'::uuid"
    
  • 直接将SQLCompiler.params渲染到语句中,使用目标 DBAPI 的适当paramstyle。例如,psycopg2 DBAPI 使用命名的pyformat样式。render_postcompile的含义将在下一节中讨论。警告这不安全,请勿使用不受信任的输入

    e = create_engine("postgresql+psycopg2://")
    
    # will use pyformat style, i.e. %(paramname)s for param
    compiled = stmt.compile(e, compile_kwargs={"render_postcompile": True})
    
    print(str(compiled) % compiled.params)
    

    这将产生一个非工作的字符串,但适合用于调试:

    SELECT  a.id,  a.data
    FROM  a
    WHERE  a.data  =  9eec1209-50b4-4253-b74b-f82461ed80c1
    

    另一个例子是使用位置参数风格,例如qmark,我们可以结合使用SQLCompiler.positiontup集合和SQLCompiler.params来在 SQLite 中呈现上述语句,以便按照编译后的顺序检索参数:

    import re
    
    e = create_engine("sqlite+pysqlite://")
    
    # will use qmark style, i.e. ? for param
    compiled = stmt.compile(e, compile_kwargs={"render_postcompile": True})
    
    # params in positional order
    params = (repr(compiled.params[name]) for name in compiled.positiontup)
    
    print(re.sub(r"\?", lambda m: next(params), str(compiled)))
    

    上述片段打印:

    SELECT  a.id,  a.data
    FROM  a
    WHERE  a.data  =  UUID('1bd70375-db17-4d8c-94f1-fc2ef3aada26')
    
  • 当存在用户定义的标志时,使用自定义 SQL 构造和编译扩展扩展以自定义方式呈现BindParameter对象。此标志通过compile_kwargs字典像其他标志一样发送:

    from sqlalchemy.ext.compiler import compiles
    from sqlalchemy.sql.expression import BindParameter
    
    @compiles(BindParameter)
    def _render_literal_bindparam(element, compiler, use_my_literal_recipe=False, **kw):
        if not use_my_literal_recipe:
            # use normal bindparam processing
            return compiler.visit_bindparam(element, **kw)
    
        # if use_my_literal_recipe was passed to compiler_kwargs,
        # render the value directly
        return repr(element.value)
    
    e = create_engine("postgresql+psycopg2://")
    print(stmt.compile(e, compile_kwargs={"use_my_literal_recipe": True}))
    

    上述配方将打印:

    SELECT  a.id,  a.data
    FROM  a
    WHERE  a.data  =  UUID('47b154cd-36b2-42ae-9718-888629ab9857')
    
  • 用于内置于模型或语句的特定类型字符串化的TypeDecorator类可使用TypeDecorator.process_literal_param()方法来提供任何数据类型的自定义字符串化:

    from sqlalchemy import TypeDecorator
    
    class UUIDStringify(TypeDecorator):
        impl = UUID
    
        def process_literal_param(self, value, dialect):
            return repr(value)
    

    上述数据类型需要在模型内明确使用或在语句内部使用type_coerce(),例如

    from sqlalchemy import type_coerce
    
    stmt = select(A).where(type_coerce(A.data, UUIDStringify) == uuid.uuid4())
    
    print(stmt.compile(e, compile_kwargs={"literal_binds": True}))
    

    再次打印相同形式:

    SELECT  a.id,  a.data
    FROM  a
    WHERE  a.data  =  UUID('47b154cd-36b2-42ae-9718-888629ab9857')
    

将“POSTCOMPILE”参数呈现为绑定参数

SQLAlchemy 包括一个变体绑定参数,称为 BindParameter.expanding,它是一个“延迟评估”的参数,在编译 SQL 构造时呈现为中间状态,然后在语句执行时进一步处理,当实际已知值传递时。 “扩展”参数默认用于 ColumnOperators.in_() 表达式,以便 SQL 字符串可以安全地独立于传递给 ColumnOperators.in_() 的特定值列表进行缓存:

>>> stmt = select(A).where(A.id.in_([1, 2, 3]))

要使用实际的绑定参数符号呈现 IN 子句,请在 ClauseElement.compile() 中使用 render_postcompile=True 标志:

>>> e = create_engine("postgresql+psycopg2://")
>>> print(stmt.compile(e, compile_kwargs={"render_postcompile": True}))
SELECT  a.id,  a.data
FROM  a
WHERE  a.id  IN  (%(id_1_1)s,  %(id_1_2)s,  %(id_1_3)s) 

前一节中关于渲染绑定参数的 literal_binds 标志自动将 render_postcompile 设置为 True,因此对于具有简单整数/字符串的语句,可以直接进行字符串化:

# render_postcompile is implied by literal_binds
>>> print(stmt.compile(e, compile_kwargs={"literal_binds": True}))
SELECT  a.id,  a.data
FROM  a
WHERE  a.id  IN  (1,  2,  3) 

SQLCompiler.paramsSQLCompiler.positiontup 也与 render_postcompile 兼容,因此以前的渲染内联绑定参数的方法在这里也可以正常工作,例如 SQLite 的位置形式:

>>> u1, u2, u3 = uuid.uuid4(), uuid.uuid4(), uuid.uuid4()
>>> stmt = select(A).where(A.data.in_([u1, u2, u3]))

>>> import re
>>> e = create_engine("sqlite+pysqlite://")
>>> compiled = stmt.compile(e, compile_kwargs={"render_postcompile": True})
>>> params = (repr(compiled.params[name]) for name in compiled.positiontup)
>>> print(re.sub(r"\?", lambda m: next(params), str(compiled)))
SELECT  a.id,  a.data
FROM  a
WHERE  a.data  IN  (UUID('aa1944d6-9a5a-45d5-b8da-0ba1ef0a4f38'),  UUID('a81920e6-15e2-4392-8a3c-d775ffa9ccd2'),  UUID('b5574cdb-ff9b-49a3-be52-dbc89f087bfa')) 

警告

请记住,所有上述代码示例,用于将字面值字符串化,将语句发送到数据库时绕过绑定参数的使用,仅在以下情况下使用

  1. 仅用于调试目的

  2. 字符串不应传递给生产数据库

  3. 仅用于本地、可信的输入

上述对字面值字符串化的方法在任何情况下都不安全,绝不应该用于生产数据库

针对特定数据库的字符串化

当我们要将要串化的语句或片段包含有特定于数据库的字符串格式的元素,或者当它包含有仅在某种类型的数据库中可用的元素时,就会出现一些复杂情况。在这些情况下,我们可能会得到一个串化的语句,该语句不符合我们所针对的数据库的正确语法,或者该操作可能会引发一个 UnsupportedCompilationError 异常。在这些情况下,有必要使用 ClauseElement.compile() 方法串化该语句,同时传递一个代表目标数据库的 EngineDialect 对象。如下,如果我们有一个 MySQL 数据库引擎,我们可以根据 MySQL 方言串化一个语句:

from sqlalchemy import create_engine

engine = create_engine("mysql+pymysql://scott:tiger@localhost/test")
print(statement.compile(engine))

更直接地,我们可以在不构建 Engine 对象的情况下直接实例化一个 Dialect 对象,如下所示,我们使用了一个 PostgreSQL 方言:

from sqlalchemy.dialects import postgresql

print(statement.compile(dialect=postgresql.dialect()))

注意,任何方言都可以使用 create_engine() 方法与一个虚拟 URL 配合组装,然后访问 Engine.dialect 属性,比如说如果我们想要一个 psycopg2 的方言对象:

e = create_engine("postgresql+psycopg2://")
psycopg2_dialect = e.dialect

当给定一个 ORM Query 对象时,为了获取到 ClauseElement.compile() 方法,我们只需要先访问 Query.statement 属性:

statement = query.statement
print(statement.compile(someengine))

将绑定参数内联渲染

警告

永远不要使用这些技术处理来自不受信任输入的字符串内容,比如来自网络表单或其他用户输入应用程序。SQLAlchemy 将 Python 值强制转换为直接的 SQL 字符串值的能力不安全且不验证传递的数据类型。在针对关系数据库进行非 DDL SQL 语句的编程调用时,始终使用绑定参数。

上述形式将渲染传递给 Python DBAPI 的 SQL 语句,其中包括绑定参数不会内联渲染。SQLAlchemy 通常不会字符串化绑定参数,因为这由 Python DBAPI 适当处理,更不用说绕过绑定参数可能是现代 Web 应用程序中被广泛利用的安全漏洞之一。SQLAlchemy 在某些情况下(如发出 DDL)有限地执行此字符串化。为了访问此功能,可以使用传递给 compile_kwargsliteral_binds 标志:

from sqlalchemy.sql import table, column, select

t = table("t", column("x"))

s = select(t).where(t.c.x == 5)

# **do not use** with untrusted input!!!
print(s.compile(compile_kwargs={"literal_binds": True}))

# to render for a specific dialect
print(s.compile(dialect=dialect, compile_kwargs={"literal_binds": True}))

# or if you have an Engine, pass as first argument
print(s.compile(some_engine, compile_kwargs={"literal_binds": True}))

此功能主要用于日志记录或调试目的,其中查询的原始 SQL 字符串可能会证明有用。

上述方法的注意事项是,它仅支持基本类型,如整数和字符串,而且如果直接使用没有预设值的 bindparam(),它也无法将其字符串化。在下面详细描述了无条件字符串化所有参数的方法。

提示

SQLAlchemy 不支持所有数据类型的完全字符串化的原因有三:

  1. 当正常使用 DBAPI 时,已经支持此功能。SQLAlchemy 项目不能被要求为所有后端的每种数据类型复制此功能,因为这是多余的工作,还会产生重大的测试和持续支持开销。

  2. 对于特定数据库,将边界参数内联化字符串化建议使用实际将这些完全字符串化的语句传递给数据库执行。这是不必要且不安全的,SQLAlchemy 不希望以任何方式鼓励这种用法。

  3. 字面值渲染领域是最有可能报告安全问题的领域。SQLAlchemy 尽量使安全参数字符串化领域成为 DBAPI 驱动程序的问题,其中每个 DBAPI 的具体情况都可以得到适当和安全地处理。

由于 SQLAlchemy 故意不支持对字面值的完全字符串化,因此在特定调试场景中进行这样的技术包括以下内容。例如,我们将使用 PostgreSQL 的 UUID 数据类型:

import uuid

from sqlalchemy import Column
from sqlalchemy import create_engine
from sqlalchemy import Integer
from sqlalchemy import select
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import declarative_base

Base = declarative_base()

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    data = Column(UUID)

stmt = select(A).where(A.data == uuid.uuid4())

针对以上模型和语句将比较一列与单个 UUID 值的情况,使用内联值对该语句进行字符串化的选项包括:

  • 一些 DBAPI(如 psycopg2)支持像 mogrify() 这样的辅助函数,提供对它们的字面值渲染功能的访问。要使用这些特性,渲染 SQL 字符串时不要使用 literal_binds,而是通过 SQLCompiler.params 访问器分别传递参数:

    e = create_engine("postgresql+psycopg2://scott:tiger@localhost/test")
    
    with e.connect() as conn:
        cursor = conn.connection.cursor()
        compiled = stmt.compile(e)
    
        print(cursor.mogrify(str(compiled), compiled.params))
    

    上述代码将产生 psycopg2 的原始字节字符串:

    b"SELECT a.id, a.data \nFROM a \nWHERE a.data = 'a511b0fc-76da-4c47-a4b4-716a8189b7ac'::uuid"
    
  • 直接将 SQLCompiler.params 渲染到语句中,使用目标 DBAPI 的适当 paramstyle。例如,psycopg2 DBAPI 使用命名的 pyformat 样式。 render_postcompile 的含义将在下一节中讨论。 警告:这不安全,请不要使用不受信任的输入

    e = create_engine("postgresql+psycopg2://")
    
    # will use pyformat style, i.e. %(paramname)s for param
    compiled = stmt.compile(e, compile_kwargs={"render_postcompile": True})
    
    print(str(compiled) % compiled.params)
    

    这将产生一个无效的字符串,尽管它适用于调试:

    SELECT  a.id,  a.data
    FROM  a
    WHERE  a.data  =  9eec1209-50b4-4253-b74b-f82461ed80c1
    

    另一个示例使用了位置参数风格,如 qmark,我们还可以使用 SQLCompiler.positiontup 集合与 SQLCompiler.params 结合使用,以便按编译后的语句中的位置顺序检索参数:

    import re
    
    e = create_engine("sqlite+pysqlite://")
    
    # will use qmark style, i.e. ? for param
    compiled = stmt.compile(e, compile_kwargs={"render_postcompile": True})
    
    # params in positional order
    params = (repr(compiled.params[name]) for name in compiled.positiontup)
    
    print(re.sub(r"\?", lambda m: next(params), str(compiled)))
    

    上述代码段将打印:

    SELECT  a.id,  a.data
    FROM  a
    WHERE  a.data  =  UUID('1bd70375-db17-4d8c-94f1-fc2ef3aada26')
    
  • 当存在用户定义的标志时,使用 自定义 SQL 构造和编译扩展 扩展以自定义方式渲染 BindParameter 对象。此标志通过 compile_kwargs 字典发送,就像发送任何其他标志一样:

    from sqlalchemy.ext.compiler import compiles
    from sqlalchemy.sql.expression import BindParameter
    
    @compiles(BindParameter)
    def _render_literal_bindparam(element, compiler, use_my_literal_recipe=False, **kw):
        if not use_my_literal_recipe:
            # use normal bindparam processing
            return compiler.visit_bindparam(element, **kw)
    
        # if use_my_literal_recipe was passed to compiler_kwargs,
        # render the value directly
        return repr(element.value)
    
    e = create_engine("postgresql+psycopg2://")
    print(stmt.compile(e, compile_kwargs={"use_my_literal_recipe": True}))
    

    上述示例将打印:

    SELECT  a.id,  a.data
    FROM  a
    WHERE  a.data  =  UUID('47b154cd-36b2-42ae-9718-888629ab9857')
    
  • 对于内置于模型或语句中的特定类型字符串化,可以使用 TypeDecorator 类来使用 TypeDecorator.process_literal_param() 方法提供任何数据类型的自定义字符串化:

    from sqlalchemy import TypeDecorator
    
    class UUIDStringify(TypeDecorator):
        impl = UUID
    
        def process_literal_param(self, value, dialect):
            return repr(value)
    

    上述数据类型需要在模型内或在语句中本地使用 type_coerce() 明确使用,例如

    from sqlalchemy import type_coerce
    
    stmt = select(A).where(type_coerce(A.data, UUIDStringify) == uuid.uuid4())
    
    print(stmt.compile(e, compile_kwargs={"literal_binds": True}))
    

    再次打印相同的形式:

    SELECT  a.id,  a.data
    FROM  a
    WHERE  a.data  =  UUID('47b154cd-36b2-42ae-9718-888629ab9857')
    

将 “POSTCOMPILE” 参数呈现为绑定参数

SQLAlchemy 包含一个称为BindParameter.expanding的绑定参数变体,这是一个“延迟评估”的参数,当编译 SQL 结构时以中间状态呈现,然后在语句执行时进一步处理,当实际已知值被传递时。 “扩展”参数默认用于ColumnOperators.in_()表达式,以便 SQL 字符串可以安全地独立于传递给ColumnOperators.in_()的特定调用的实际值被缓存:

>>> stmt = select(A).where(A.id.in_([1, 2, 3]))

要使用实际的绑定参数符号呈现 IN 子句,请在ClauseElement.compile()中使用render_postcompile=True标志:

>>> e = create_engine("postgresql+psycopg2://")
>>> print(stmt.compile(e, compile_kwargs={"render_postcompile": True}))
SELECT  a.id,  a.data
FROM  a
WHERE  a.id  IN  (%(id_1_1)s,  %(id_1_2)s,  %(id_1_3)s) 

在先前有关渲染绑定参数的部分中描述的literal_binds标志会自动将render_postcompile设置为 True,因此对于具有简单 int/字符串的语句,可以直接将它们字符串化:

# render_postcompile is implied by literal_binds
>>> print(stmt.compile(e, compile_kwargs={"literal_binds": True}))
SELECT  a.id,  a.data
FROM  a
WHERE  a.id  IN  (1,  2,  3) 

SQLCompiler.paramsSQLCompiler.positiontuprender_postcompile兼容,因此在此处渲染内联绑定参数的先前方法也将以相同的方式工作,例如 SQLite 的位置形式:

>>> u1, u2, u3 = uuid.uuid4(), uuid.uuid4(), uuid.uuid4()
>>> stmt = select(A).where(A.data.in_([u1, u2, u3]))

>>> import re
>>> e = create_engine("sqlite+pysqlite://")
>>> compiled = stmt.compile(e, compile_kwargs={"render_postcompile": True})
>>> params = (repr(compiled.params[name]) for name in compiled.positiontup)
>>> print(re.sub(r"\?", lambda m: next(params), str(compiled)))
SELECT  a.id,  a.data
FROM  a
WHERE  a.data  IN  (UUID('aa1944d6-9a5a-45d5-b8da-0ba1ef0a4f38'),  UUID('a81920e6-15e2-4392-8a3c-d775ffa9ccd2'),  UUID('b5574cdb-ff9b-49a3-be52-dbc89f087bfa')) 

警告

请记住,所有上述字符串化文字值的代码示例,当将语句发送到数据库时绕过绑定参数的使用,只能在以下情况下使用

  1. 仅用于调试目的

  2. 该字符串不应传递给实时生产数据库

  3. 仅限于本地,可信任的输入

上述用于将文字值字符串化的方法在任何情况下都不安全,绝对不应该用于生产数据库

为什么在将 SQL 语句字符串化时百分号会被加倍?

许多 DBAPI 实现采用pyformatformat paramstyle,这在其语法中必然涉及百分号。这样做的大多数 DBAPI 都希望在使用的语句的字符串形式中,用于其他目的的百分号被双倍化(即转义),例如:

SELECT  a,  b  FROM  some_table  WHERE  a  =  %s  AND  c  =  %s  AND  num  %%  modulus  =  0

当 SQL 语句通过 SQLAlchemy 传递给底层 DBAPI 时,绑定参数的替换方式与 Python 字符串插值运算符%相同,在许多情况下,DBAPI 实际上直接使用这个运算符。上面,绑定参数的替换看起来像是:

SELECT  a,  b  FROM  some_table  WHERE  a  =  5  AND  c  =  10  AND  num  %  modulus  =  0

像 PostgreSQL(默认 DBAPI 是 psycopg2)和 MySQL(默认 DBAPI 是 mysqlclient)这样的数据库的默认编译器将具有这种百分号转义行为:

>>> from sqlalchemy import table, column
>>> from sqlalchemy.dialects import postgresql
>>> t = table("my_table", column("value % one"), column("value % two"))
>>> print(t.select().compile(dialect=postgresql.dialect()))
SELECT  my_table."value %% one",  my_table."value %% two"
FROM  my_table 

当使用这样的方言时,如果需要不包含绑定参数符号的非 DBAPI 语句,一种快速删除百分号的方法是直接使用 Python 的%运算符替换一个空的参数集:

>>> strstmt = str(t.select().compile(dialect=postgresql.dialect()))
>>> print(strstmt % ())
SELECT  my_table."value % one",  my_table."value % two"
FROM  my_table 

另一种方法是在使用的方言上设置不同的参数样式;所有Dialect实现都接受一个paramstyle参数,该参数将导致该方言的编译器使用给定的参数样式。下面,非常常见的named参数样式在用于编译的方言中设置,以便百分号在 SQL 的编译形式中不再重要,并且不再被转义:

>>> print(t.select().compile(dialect=postgresql.dialect(paramstyle="named")))
SELECT  my_table."value % one",  my_table."value % two"
FROM  my_table 

我正在使用 op()生成自定义运算符,但我的括号没出来正确

Operators.op()方法允许创建一个 SQLAlchemy 中未知的自定义数据库操作符:

>>> print(column("q").op("->")(column("p")))
q  ->  p 

然而,当将其用于复合表达式的右侧时,它不会生成我们期望的括号:

>>> print((column("q1") + column("q2")).op("->")(column("p")))
q1  +  q2  ->  p 

在上面的情况下,我们可能想要(q1 + q2) -> p

对于这种情况的解决方案是设置运算符的优先级,使用Operators.op.precedence参数,设置为一个高数字,其中 100 是最大值,当前任何 SQLAlchemy 运算符使用的最高数字是 15:

>>> print((column("q1") + column("q2")).op("->", precedence=100)(column("p")))
(q1  +  q2)  ->  p 

我们还可以通常通过使用ColumnElement.self_group()方法强制在二元表达式(例如具有左/右操作数和运算符的表达式)周围加上括号:

>>> print((column("q1") + column("q2")).self_group().op("->")(column("p")))
(q1  +  q2)  ->  p 

为什么括号的规则是这样的?

当存在过多的括号或者括号处于数据库不期望的不寻常位置时,许多数据库会报错,因此 SQLAlchemy 不基于分组生成括号,它使用操作符优先级以及如果操作符已知是可结合的,则生成最小的括号。否则,表达式如下:

column("a") & column("b") & column("c") & column("d")

将产生:

(((a  AND  b)  AND  c)  AND  d)

这样做可能会让人们感到不爽(并被报告为错误)。在其他情况下,它会导致更容易让数据库混淆或至少降低可读性,比如:

column("q", ARRAY(Integer, dimensions=2))[5][6]

将产生:

((q[5])[6])

还有一些边界情况,我们会得到像"(x) = 7"这样的东西,数据库真的不喜欢这样。因此,括号化不是简单地添加括号,而是使用运算符优先级和结合性来确定分组。

对于Operators.op(),优先级的值默认为零。

如果我们将Operators.op.precedence的值默认为 100,即最高值,会怎样呢?然后这个表达式会多加括号,但除此之外还是可以的,也就是说,这两个表达式是等价的:

>>> print((column("q") - column("y")).op("+", precedence=100)(column("z")))
(q  -  y)  +  z
>>> print((column("q") - column("y")).op("+")(column("z")))
q  -  y  +  z 

但是这两种情况不是:

>>> print(column("q") - column("y").op("+", precedence=100)(column("z")))
q  -  y  +  z
>>> print(column("q") - column("y").op("+")(column("z")))
q  -  (y  +  z) 

目前来看,只要我们根据运算符的优先级和结合性进行括号化,如果真的有一种方法可以自动为没有给定优先级的通用运算符进行括号化,从而在所有情况下都能正常工作,这还不清楚,因为有时您希望自定义的运算符具有比其他运算符更低的优先级,有时您希望它更高。

如果上面的“binary”表达式强制在调用op()时使用self_group()方法,假设左侧的复合表达式总是可以无害地加上括号,那么这种可能性是存在的。也许这种改变以后可以实现,但是目前来看,保持括号规则在内部更一致似乎是更安全的方法。

为什么括号规则会是这样?

当括号过多或者括号出现在它们不期望的不寻常位置时,许多数据库会抛出错误,因此 SQLAlchemy 不基于分组生成括号,而是使用运算符优先级,如果运算符已知为结合性,那么会尽量生成最少的括号。否则,表达式如下:

column("a") & column("b") & column("c") & column("d")

会产生:

(((a  AND  b)  AND  c)  AND  d)

这是可以的,但可能会让人们感到烦恼(并报告为错误)。在其他情况下,它会导致更容易让数据库混淆,或者至少影响可读性,比如:

column("q", ARRAY(Integer, dimensions=2))[5][6]

会产生:

((q[5])[6])

还有一些边界情况,我们会得到像"(x) = 7"这样的东西,数据库真的不喜欢这样。因此,括号化不是简单地添加括号,而是使用运算符优先级和结合性来确定分组。

对于Operators.op(),优先级的值默认为零。

如果我们将Operators.op.precedence的值默认为 100,即最高值,会怎样呢?然后这个表达式会多加括号,但除此之外还是可以的,也就是说,这两个表达式是等价的:

>>> print((column("q") - column("y")).op("+", precedence=100)(column("z")))
(q  -  y)  +  z
>>> print((column("q") - column("y")).op("+")(column("z")))
q  -  y  +  z 

但是这两种情况不是:

>>> print(column("q") - column("y").op("+", precedence=100)(column("z")))
q  -  y  +  z
>>> print(column("q") - column("y").op("+")(column("z")))
q  -  (y  +  z) 

现在,尚不清楚只要我们基于操作符优先级和结合性进行括号化,是否真的有一种方法可以自动为没有给定优先级的通用运算符添加括号,以便在所有情况下都能正常工作,因为有时您希望自定义操作符的优先级低于其他操作符,有时您希望它更高。

也许,如果上面的“二元”表达式在调用op()时强制使用了self_group()方法,假设左侧的复合表达式总是可以无害地加括号。也许这种改变可以在某个时候实现,然而就目前而言,保持括号规则更加内部一致似乎是更安全的做法。

ORM 配置

原文:docs.sqlalchemy.org/en/20/faq/ormconfiguration.html

  • 如何映射没有主键的表?

  • 如何配置一个与 Python 保留字或类似的列?

  • 如何在给定映射类的情况下获取所有列、关系、映射属性等的列表?

  • 我收到关于“在属性 Y 下隐式组合列 X”的警告或错误

  • 我正在使用声明式并使用 and_()or_() 设置 primaryjoin/secondaryjoin,但我收到了关于外键的错误消息。

  • 为什么推荐在 LIMIT 中使用 ORDER BY(特别是在 subqueryload() 中)?

如何映射没有主键的表?

为了映射到特定表,SQLAlchemy ORM 需要至少有一个列被标记为主键列;当然,多列,即复合主键,也是完全可行的。这些列不需要实际被数据库知道为主键列,尽管最好是这样。只需要这些列 行为 象主键一样,例如,作为行的唯一且非空的标识符。

大多数 ORM 都要求对象有某种形式的主键定义,因为内存中的对象必须对应于数据库表中的唯一可识别行;至少,这允许对象可以被定位用于仅影响该对象行而不影响其他行的 UPDATE 和 DELETE 语句。然而,主键的重要性远不止于此。在 SQLAlchemy 中,所有 ORM 映射的对象始终使用称为 身份映射 的模式与它们的特定数据库行唯一链接在一起,这是 SQLAlchemy 使用的工作单元系统的核心模式,也是最常见的(和不那么常见的) ORM 使用模式的关键。

注意

需要注意的是,我们只讨论 SQLAlchemy ORM;一个基于 Core 构建并且只处理 Table 对象、select() 构造等的应用程序 不需要 在任何方式上存在或关联表上有任何主键(尽管再次强调,在 SQL 中,所有表应该真的有某种主键,以免您实际上需要更新或删除特定行)。

在几乎所有情况下,表确实有所谓的 候选键,它是一列或一系列列,可以唯一标识一行。如果一张表真的没有这个,而且有实际完全重复的行,那么该表就不符合 第一范式,也不能被映射。否则,组成最佳候选键的任何列都可以直接应用到映射器上:

class SomeClass(Base):
    __table__ = some_table_with_no_pk
    __mapper_args__ = {
        "primary_key": [some_table_with_no_pk.c.uid, some_table_with_no_pk.c.bar]
    }

当使用完全声明的表元数据时,最好在这些列上使用 primary_key=True 标志:

class SomeClass(Base):
    __tablename__ = "some_table_with_no_pk"

    uid = Column(Integer, primary_key=True)
    bar = Column(String, primary_key=True)

关系数据库中的所有表都应该有主键。即使是多对多关联表 - 主键将是两个关联列的组合:

CREATE  TABLE  my_association  (
  user_id  INTEGER  REFERENCES  user(id),
  account_id  INTEGER  REFERENCES  account(id),
  PRIMARY  KEY  (user_id,  account_id)
)

如何配置一个 Python 保留字或类似的 Column?

基于列的属性可以在映射中被赋予任何所需的名称。请参阅明确命名声明式映射的列。

如何在给定一个映射类的情况下获取所有列、关系、映射属性等列表?

所有这些信息都可以从 Mapper 对象中获得。

要获取特定映射类的 Mapper,请对其调用 inspect() 函数:

from sqlalchemy import inspect

mapper = inspect(MyClass)

从那里,关于类的所有信息都可以通过属性访问,例如:

  • Mapper.attrs - 所有映射属性的命名空间。属性本身是 MapperProperty 的实例,如果适用的话,它们包含可以导致映射的 SQL 表达式或列的其他属性。

  • Mapper.column_attrs - 限于列和 SQL 表达式属性的映射属性命名空间。您可能想直接使用 Mapper.columns 来获取 Column 对象。

  • Mapper.relationships - 所有RelationshipProperty属性的命名空间。

  • Mapper.all_orm_descriptors - 所有映射属性的命名空间,以及使用hybrid_propertyAssociationProxy等系统定义的用户定义属性。

  • Mapper.columns - Column对象和与映射相关的其他命名 SQL 表达式的命名空间。

  • Mapper.mapped_table - 该映射器所映射到的Table或其他可选择的对象。

  • Mapper.local_table - 此映射器“本地”的Table;这在将映射器映射到组合可选择项的情况下与Mapper.mapped_table不同。

我收到关于“隐式将列 X 组合到属性 Y 下”的警告或错误

此条件指的是当映射包含两列,这两列由于名称而被映射到同一属性名下,但没有表明这是有意的。映射的类需要为每个要存储独立值的属性明确指定名称;当两列具有相同的名称并且没有消歧时,它们就属于同一属性,其效果是将一列的值复制到另一列,根据哪一列首先分配给属性。

这种行为通常是可取的,在继承映射中通过外键关系将两列链接在一起时是允许的,而不会发出警告。当出现警告或异常时,可以通过将列分配给名称不同的属性来解决问题,或者如果希望将它们组合在一起,则使用column_property()使其明确。

给出如下示例:

from sqlalchemy import Integer, Column, ForeignKey
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

class B(A):
    __tablename__ = "b"

    id = Column(Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("a.id"))

自 SQLAlchemy 版本 0.9.5 起,将检测到上述条件,并将警告说ABid列正在组合为同名属性id,这是一个严重的问题,因为这意味着B对象的主键将始终与其A的主键相同。

解决此问题的映射如下:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

class B(A):
    __tablename__ = "b"

    b_id = Column("id", Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("a.id"))

假设我们确实希望A.idB.id互为镜像,尽管B.a_idA.id相关的地方。我们可以使用column_property()将它们组合在一起:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

class B(A):
    __tablename__ = "b"

    # probably not what you want, but this is a demonstration
    id = column_property(Column(Integer, primary_key=True), A.id)
    a_id = Column(Integer, ForeignKey("a.id"))

我正在使用声明式语法,并使用and_()or_()设置primaryjoin/secondaryjoin,但是我收到了关于外键的错误消息。

你这样做了吗?:

class MyClass(Base):
    # ....

    foo = relationship(
        "Dest", primaryjoin=and_("MyClass.id==Dest.foo_id", "MyClass.foo==Dest.bar")
    )

那是两个字符串表达式的and_(),而 SQLAlchemy 不能对其应用任何映射。声明式允许将relationship()参数指定为字符串,这些字符串将使用eval()转换为表达式对象。但这不会发生在and_()表达式内部 - 这是声明式仅对作为字符串传递给primaryjoin或其他参数的整体应用的特殊操作:

class MyClass(Base):
    # ....

    foo = relationship(
        "Dest", primaryjoin="and_(MyClass.id==Dest.foo_id, MyClass.foo==Dest.bar)"
    )

或者,如果您需要的对象已经可用,请跳过字符串:

class MyClass(Base):
    # ....

    foo = relationship(
        Dest, primaryjoin=and_(MyClass.id == Dest.foo_id, MyClass.foo == Dest.bar)
    )

相同的想法也适用于所有其他参数,比如foreign_keys

# wrong !
foo = relationship(Dest, foreign_keys=["Dest.foo_id", "Dest.bar_id"])

# correct !
foo = relationship(Dest, foreign_keys="[Dest.foo_id, Dest.bar_id]")

# also correct !
foo = relationship(Dest, foreign_keys=[Dest.foo_id, Dest.bar_id])

# if you're using columns from the class that you're inside of, just use the column objects !
class MyClass(Base):
    foo_id = Column(...)
    bar_id = Column(...)
    # ...

    foo = relationship(Dest, foreign_keys=[foo_id, bar_id])

为什么推荐使用ORDER BYLIMIT(特别是与subqueryload()一起)?

当 SELECT 语句返回行时未使用 ORDER BY 时,关系数据库可以以任意顺序返回匹配的行。虽然这种排序很常见,对应于表中行的自然顺序,但并不是所有数据库和所有查询都是如此。这样做的结果是,任何使用LIMITOFFSET限制行,或者仅选择结果的第一行,而放弃其余部分的查询,在返回结果行时不是确定性的,假设有多个行匹配查询的条件。

尽管我们可能不会注意到这一点,因为对于通常以其自然顺序返回行的数据库上的简单查询,它更多地成为问题,如果我们还使用subqueryload()来加载相关集合,并且我们可能无法按预期加载集合。

SQLAlchemy 通过发出单独的查询来实现subqueryload(),其结果与第一个查询的结果匹配。我们会看到像这样发出两个查询:

>>> session.scalars(select(User).options(subqueryload(User.addresses))).all()
-- the "main" query
SELECT  users.id  AS  users_id
FROM  users
-- the "load" query issued by subqueryload
SELECT  addresses.id  AS  addresses_id,
  addresses.user_id  AS  addresses_user_id,
  anon_1.users_id  AS  anon_1_users_id
FROM  (SELECT  users.id  AS  users_id  FROM  users)  AS  anon_1
JOIN  addresses  ON  anon_1.users_id  =  addresses.user_id
ORDER  BY  anon_1.users_id 

第二个查询将第一个查询嵌入为行的来源。当内部查询使用OFFSET和/或LIMIT而没有排序时,这两个查询可能不会看到相同的结果:

>>> user = session.scalars(
...     select(User).options(subqueryload(User.addresses)).limit(1)
... ).first()
-- the "main" query
SELECT  users.id  AS  users_id
FROM  users
  LIMIT  1
-- the "load" query issued by subqueryload
SELECT  addresses.id  AS  addresses_id,
  addresses.user_id  AS  addresses_user_id,
  anon_1.users_id  AS  anon_1_users_id
FROM  (SELECT  users.id  AS  users_id  FROM  users  LIMIT  1)  AS  anon_1
JOIN  addresses  ON  anon_1.users_id  =  addresses.user_id
ORDER  BY  anon_1.users_id 

根据数据库的具体情况,我们可能会得到如下两个查询的结果:

-- query #1
+--------+
|users_id|
+--------+
|       1|
+--------+

-- query #2
+------------+-----------------+---------------+
|addresses_id|addresses_user_id|anon_1_users_id|
+------------+-----------------+---------------+
|           3|                2|              2|
+------------+-----------------+---------------+
|           4|                2|              2|
+------------+-----------------+---------------+

在上面的例子中,我们为user.id为 2 的用户接收到了两行addresses,而对于 id 为 1 的用户却没有。我们浪费了两行,并且未能实际加载集合。这是一个隐匿的错误,因为不查看 SQL 和结果,ORM 将不会显示任何问题;如果我们访问已有的Useraddresses,它会对集合进行惰性加载,我们将看不到任何实际错误发生。

解决此问题的方法是始终指定确定性排序顺序,以便主查询始终返回相同的行集。这通常意味着您应该在表的唯一列上进行 Select.order_by() 排序。主键是一个不错的选择:

session.scalars(
    select(User).options(subqueryload(User.addresses)).order_by(User.id).limit(1)
).first()

注意,joinedload() 这种预加载策略不会遇到相同的问题,因为只会发出一个查询,所以加载查询不会与主查询不同。同样,selectinload() 这种预加载策略也不会有此问题,因为它将其集合加载直接链接到刚刚加载的主键值。

另请参阅

子查询预加载 ## 我如何映射一个没有主键的表?

SQLAlchemy ORM 为了映射到特定表,需要至少有一个列被指定为主键列;多列,即复合主键,当然也是完全可行的。这些列需要实际上被数据库知道为主键列,尽管它们是主键列是个好主意。只需要这些列表现出主键的行为即可,例如作为行的唯一标识符和不可为空的标识符。

大多数 ORM 要求对象定义某种主键,因为内存中的对象必须对应于数据库表中的唯一可识别行;至少,这允许对象可以成为 UPDATE 和 DELETE 语句的目标,这些语句将仅影响该对象的行,而不会影响其他行。但是,主键的重要性远不止于此。在 SQLAlchemy 中,所有 ORM 映射的对象始终通过称为标识映射的模式与其特定数据库行唯一链接到一个 Session 中,该模式是 SQLAlchemy 使用的工作单元系统的核心,并且也是最常见(以及不那么常见)的 ORM 使用模式的关键。

注意

需要注意的是,我们只谈论 SQLAlchemy ORM;一个建立在 Core 之上、仅处理Table对象、select()构造等的应用程序,不需要在任何方式上要求主键存在于或与表相关联(尽管在 SQL 中,所有表实际上都应该具有某种主键,否则你可能需要实际更新或删除特定行)。

几乎在所有情况下,表都具有所谓的 候选键,这是一列或一系列列,唯一标识一行。如果表确实没有这个,且具有实际完全重复的行,则该表不符合第一范式,无法进行映射。否则,组成最佳候选键的任何列都可以直接应用于映射器:

class SomeClass(Base):
    __table__ = some_table_with_no_pk
    __mapper_args__ = {
        "primary_key": [some_table_with_no_pk.c.uid, some_table_with_no_pk.c.bar]
    }

当使用完全声明的表元数据时,最好在这些列上使用primary_key=True标志:

class SomeClass(Base):
    __tablename__ = "some_table_with_no_pk"

    uid = Column(Integer, primary_key=True)
    bar = Column(String, primary_key=True)

关系数据库中的所有表都应该有主键。即使是多对多的关联表 - 主键也将是两个关联列的组合:

CREATE  TABLE  my_association  (
  user_id  INTEGER  REFERENCES  user(id),
  account_id  INTEGER  REFERENCES  account(id),
  PRIMARY  KEY  (user_id,  account_id)
)

如何配置一个是 Python 保留字或类似的列?

在映射中,基于列的属性可以赋予任何所需的名称。参见显式命名声明式映射的列。

如何获取给定映射类的所有列、关系、映射属性等列表?

所有这些信息都可以从Mapper对象中获取。

要获取特定映射类的Mapper,请在其上调用inspect()函数:

from sqlalchemy import inspect

mapper = inspect(MyClass)

从那里,可以通过诸如以下属性之类的属性访问有关类的所有信息:

  • Mapper.attrs - 所有映射属性的命名空间。这些属性本身是MapperProperty的实例,其中包含了可导致映射的 SQL 表达式或列的其他属性(如果适用)。

  • Mapper.column_attrs - 仅限于列和 SQL 表达式属性的映射属性命名空间。你可能想使用Mapper.columns直接获取 Column对象。

  • Mapper.relationships - 所有 RelationshipProperty 属性的命名空间。

  • Mapper.all_orm_descriptors - 所有映射属性的命名空间,以及使用诸如 hybrid_propertyAssociationProxy 等系统定义的用户定义属性等。

  • Mapper.columns - 与映射相关联的 Column 对象和其他命名 SQL 表达式的命名空间。

  • Mapper.mapped_table - 此映射器映射到的 Table 或其他可选择的对象。

  • Mapper.local_table - 此映射器“本地”的 Table;在映射器使用继承映射到组合选择时,这与 Mapper.mapped_table 不同。

我收到了一个关于“隐式组合列 X 在属性 Y 下”的警告或错误

这种情况指的是映射包含两个列,这两个列由于它们的名称而被映射到同一属性名称下,但没有迹象表明这是有意的。映射类需要为每个要存储独立值的属性指定明确的名称;当两个列具有相同的名称并且没有消歧义时,它们就会落入同一个属性下,效果是从一个列中的值被复制到另一个列中,取决于哪个列首先分配给属性。

这种行为通常是可取的,在继承映射内部通过外键关系链接两个列时,无需警告即可允许。当出现警告或异常时,可以通过将列分配给不同命名的属性来解决问题,或者如果希望将它们组合在一起,则可以使用column_property()来明确表示这一点。

给出如下示例:

from sqlalchemy import Integer, Column, ForeignKey
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

class B(A):
    __tablename__ = "b"

    id = Column(Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("a.id"))

截至 SQLAlchemy 版本 0.9.5,检测到上述条件,并将警告ABid列正在合并到同名属性id下,上面是一个严重问题,因为这意味着B对象的主键将始终反映其A的主键。

解决此问题的映射如下:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

class B(A):
    __tablename__ = "b"

    b_id = Column("id", Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("a.id"))

假设我们确实希望A.idB.id彼此镜像,尽管B.a_idA.id相关的地方。我们可以使用column_property()将它们合并在一起:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

class B(A):
    __tablename__ = "b"

    # probably not what you want, but this is a demonstration
    id = column_property(Column(Integer, primary_key=True), A.id)
    a_id = Column(Integer, ForeignKey("a.id"))

我正在使用声明式并使用and_()or_()设置 primaryjoin/secondaryjoin,并且收到有关外键的错误消息。

您是这样做的吗?:

class MyClass(Base):
    # ....

    foo = relationship(
        "Dest", primaryjoin=and_("MyClass.id==Dest.foo_id", "MyClass.foo==Dest.bar")
    )

这是两个字符串表达式的and_(),SQLAlchemy 无法对其应用任何映射。声明式允许将relationship()参数指定为字符串,并使用eval()将其转换为表达式对象。但这不会发生在and_()表达式内部 - 它是声明式仅适用于作为字符串传递给 primaryjoin 或其他参数的整体的特殊操作:

class MyClass(Base):
    # ....

    foo = relationship(
        "Dest", primaryjoin="and_(MyClass.id==Dest.foo_id, MyClass.foo==Dest.bar)"
    )

或者如果您需要的对象已经可用,请跳过字符串:

class MyClass(Base):
    # ....

    foo = relationship(
        Dest, primaryjoin=and_(MyClass.id == Dest.foo_id, MyClass.foo == Dest.bar)
    )

相同的想法适用于所有其他参数,例如foreign_keys

# wrong !
foo = relationship(Dest, foreign_keys=["Dest.foo_id", "Dest.bar_id"])

# correct !
foo = relationship(Dest, foreign_keys="[Dest.foo_id, Dest.bar_id]")

# also correct !
foo = relationship(Dest, foreign_keys=[Dest.foo_id, Dest.bar_id])

# if you're using columns from the class that you're inside of, just use the column objects !
class MyClass(Base):
    foo_id = Column(...)
    bar_id = Column(...)
    # ...

    foo = relationship(Dest, foreign_keys=[foo_id, bar_id])

为什么推荐在LIMIT中使用ORDER BY(特别是在subqueryload()中)?

当没有为返回行的 SELECT 语句使用 ORDER BY 时,关系数据库可以以任意的顺序返回匹配的行。虽然这种排序往往对应于表内行的自然顺序,但并非所有数据库和所有查询都是如此。这样做的结果是,任何使用LIMITOFFSET限制行数的查询,或者仅选择结果的第一行,丢弃其余行的查询,在返回哪个结果行时不是确定性的,假设查询的条件有多个匹配行。

尽管我们可能在通常按照它们的自然顺序返回行的数据库上的简单查询中没有注意到这一点,但如果我们还使用subqueryload()来加载相关集合,这就更成为一个问题,我们可能不会按预期加载集合。

SQLAlchemy 通过发出单独的查询来实现subqueryload(),其结果与第一个查询的结果匹配。我们看到像这样发出的两个查询:

>>> session.scalars(select(User).options(subqueryload(User.addresses))).all()
-- the "main" query
SELECT  users.id  AS  users_id
FROM  users
-- the "load" query issued by subqueryload
SELECT  addresses.id  AS  addresses_id,
  addresses.user_id  AS  addresses_user_id,
  anon_1.users_id  AS  anon_1_users_id
FROM  (SELECT  users.id  AS  users_id  FROM  users)  AS  anon_1
JOIN  addresses  ON  anon_1.users_id  =  addresses.user_id
ORDER  BY  anon_1.users_id 

第二个查询将第一个查询嵌入为行的源。当内部查询使用OFFSET和/或LIMIT而没有排序时,这两个查询可能不会看到相同的结果:

>>> user = session.scalars(
...     select(User).options(subqueryload(User.addresses)).limit(1)
... ).first()
-- the "main" query
SELECT  users.id  AS  users_id
FROM  users
  LIMIT  1
-- the "load" query issued by subqueryload
SELECT  addresses.id  AS  addresses_id,
  addresses.user_id  AS  addresses_user_id,
  anon_1.users_id  AS  anon_1_users_id
FROM  (SELECT  users.id  AS  users_id  FROM  users  LIMIT  1)  AS  anon_1
JOIN  addresses  ON  anon_1.users_id  =  addresses.user_id
ORDER  BY  anon_1.users_id 

根据数据库的具体情况,我们可能会得到以下两个查询的结果:

-- query #1
+--------+
|users_id|
+--------+
|       1|
+--------+

-- query #2
+------------+-----------------+---------------+
|addresses_id|addresses_user_id|anon_1_users_id|
+------------+-----------------+---------------+
|           3|                2|              2|
+------------+-----------------+---------------+
|           4|                2|              2|
+------------+-----------------+---------------+

如上所述,我们对于user.id为 2 的两个addresses行,却没有对于 1 的。我们浪费了两行,并且未能实际加载集合。这是一个潜在的错误,因为如果不查看 SQL 和结果,ORM 将不会显示任何问题;如果我们访问我们拥有的Useraddresses,它将对集合进行惰性加载,并且我们将看不到任何实际出错的情况。

解决这个问题的方法是始终指定确定性的排序顺序,以便主查询始终返回相同的行集合。这通常意味着你应该在表上的一个唯一列上使用Select.order_by()。主键是一个不错的选择:

session.scalars(
    select(User).options(subqueryload(User.addresses)).order_by(User.id).limit(1)
).first()

请注意,joinedload() 急加载策略不会遭受相同的问题,因为只发出一次查询,因此加载查询不能与主查询不同。类似地,selectinload() 急加载策略也不会有此问题,因为它将其集合加载直接链接到刚刚加载的主键值。

另请参阅

子查询的急加载

性能

原文:docs.sqlalchemy.org/en/20/faq/performance.html

  • 为什么升级到 1.4 和/或 2.x 后我的应用程序变慢了?

    • 第一步 - 打开 SQL 日志记录并确认缓存是否正常工作

    • 第二步 - 确定哪些构造阻止启用缓存

    • 第三步 - 为给定的对象启用缓存和/或寻找替代方案

  • 如何对基于 SQLAlchemy 的应用程序进行性能分析?

    • 查询性能分析

    • 代码性能分析

    • 执行速度慢

    • 结果获取慢 - 核心

    • 结果获取慢 - ORM

  • 我正在使用 ORM 插入 400,000 行,速度非常慢!

为什么升级到 1.4 和/或 2.x 后我的应用程序变慢了?

SQLAlchemy 自 1.4 版本起包含一个 SQL 编译缓存功能,它将允许核心和 ORM SQL 构造缓存它们的字符串形式,以及用于从语句中获取结果的其他结构信息,当下次使用另一个结构上等同的构造时,可以跳过相对昂贵的字符串编译过程。这个系统依赖于为所有 SQL 构造实现的功能,包括诸如 Columnselect()TypeEngine 等对象,以生成完全代表它们状态的缓存键,至于它们对 SQL 编译过程产生的影响程度。

缓存系统使得 SQLAlchemy 1.4 及以上版本在将 SQL 构造重复转换为字符串所花费的时间方面比 SQLAlchemy 1.3 更高效。然而,这仅在为使用的方言和 SQL 构造启用缓存时才有效;如果没有启用缓存,则字符串编译通常类似于 SQLAlchemy 1.3,但在某些情况下速度略有下降。

然而,有一种情况是,如果 SQLAlchemy 的新缓存系统已被禁用(出于以下原因),则 ORM 的性能实际上可能明显低于 1.3 或其他之前的版本,这是由于 ORM 惰性加载器和对象刷新查询中缺乏缓存,而在 1.3 版本和更早版本中使用了现在已经过时的 BakedQuery 系统。如果应用程序在切换到 1.4 时看到了显著(30% 或更高)的性能下降(以操作完成所需的时间为度量),这可能是问题的根本原因,下面有缓解措施。

参见

SQL 编译缓存 - 缓存系统概述

对象将不生成缓存键,性能影响 - 关于不启用缓存的元素生成的警告的额外信息。

第一步 - 打开 SQL 记录并确认缓存是否起作用

在这里,我们想要使用引擎日志记录中描述的技术,查找带有[no key]指示器的语句,甚至是带有[dialect does not support caching]的语句。对于首次调用语句时成功参与缓存系统的 SQL 语句,我们会看到[generated in Xs]指示器,随后对于绝大多数后续语句会看到[cached since Xs ago]指示器。如果对于特定的 SELECT 语句主要存在[no key],或者如果由于[dialect does not support caching]完全禁用了缓存,这可能是导致性能严重下降的原因。

参见

使用日志估算缓存性能

第二步 - 确定哪些构造物阻止了缓存的启用

假设语句没有被缓存,在应用程序的日志中会及早发出警告(仅适用于 SQLAlchemy 1.4.28 及以上版本),指示不参与缓存的方言、TypeEngine 对象和 SQL 构造物。

对于诸如扩展TypeDecoratorUserDefinedType的用户定义数据类型,警告将如下所示:

sqlalchemy.ext.SAWarning: MyType will not produce a cache key because the
``cache_ok`` attribute is not set to True. This can have significant
performance implications including some performance degradations in
comparison to prior SQLAlchemy versions. Set this attribute to True if this
type object's state is safe to use in a cache key, or False to disable this
warning.

对于自定义和第三方 SQL 元素,例如那些使用自定义 SQL 构造和编译扩展中描述的技术构建的元素,这些警告会看起来像:

sqlalchemy.exc.SAWarning: Class MyClass will not make use of SQL
compilation caching as it does not set the 'inherit_cache' attribute to
``True``. This can have significant performance implications including some
performance degradations in comparison to prior SQLAlchemy versions. Set
this attribute to True if this object can make use of the cache key
generated by the superclass. Alternatively, this attribute may be set to
False which will disable this warning.

对于使用Dialect类层次结构的自定义和第三方方言,警告将如下所示:

sqlalchemy.exc.SAWarning: Dialect database:driver will not make use of SQL
compilation caching as it does not set the 'supports_statement_cache'
attribute to ``True``. This can have significant performance implications
including some performance degradations in comparison to prior SQLAlchemy
versions. Dialect maintainers should seek to set this attribute to True
after appropriate development and testing for SQLAlchemy 1.4 caching
support. Alternatively, this attribute may be set to False which will
disable this warning.

第三步 - 为给定对象启用缓存和/或寻求替代方案

缓存不足的缓解步骤包括:

  • ExternalType.cache_ok 设置为 True,用于所有继承自 TypeDecoratorUserDefinedType 的自定义类型,以及这些类型的子类,如 PickleType。仅当自定义类型不包含任何影响其渲染 SQL 的额外状态属性时才设置这个 属性

    class MyCustomType(TypeDecorator):
        cache_ok = True
        impl = String
    

    如果使用的类型来自第三方库,请与该库的维护者联系,以便进行调整和发布。

    另请参阅

    ExternalType.cache_ok - 启用自定义数据类型缓存的要求背景。

  • 确保第三方方言将 Dialect.supports_statement_cache 设置为 True。这表明第三方方言的维护者确保其方言与 SQLAlchemy 1.4 或更高版本兼容,并且其方言不包含可能干扰缓存的编译特性。由于存在一些常见的编译模式可能会干扰缓存,因此方言维护者务必仔细检查和测试,调整任何与缓存不兼容的传统模式。

    另请参阅

    第三方方言的缓存 - 第三方方言参与 SQL 语句缓存的背景和示例。

  • 自定义 SQL 类,包括使用 自定义 SQL 构造和编译扩展 创建的所有 DQL / DML 构造,以及对象的临时子类,如 ColumnTable。对于简单子类,可以将 HasCacheKey.inherit_cache 属性设置为 True,该属性不包含影响 SQL 编译的子类特定状态信息。

    另请参阅

    为自定义构造启用缓存支持 - 应用 HasCacheKey.inherit_cache 属性的指南。

另请参阅

SQL 编译缓存 - 缓存系统概述

对象不会生成缓存键,性能影响 - 背景是在为特定结构和/或方言未启用缓存时发出警告的情况。## 如何分析一个使用 SQLAlchemy 的应用程序?

寻找性能问题通常涉及两种策略。一种是查询性能分析,另一种是代码性能分析。

查询性能分析

有时仅仅记录普通的 SQL(通过 python 的 logging 模块启用,或者通过create_engine()上的echo=True参数启用)就能让你了解到事情花费了多长时间。例如,如果在 SQL 操作之后记录一些内容,你会在日志中看到类似这样的内容:

17:37:48,325 INFO  [sqlalchemy.engine.base.Engine.0x...048c] SELECT ...
17:37:48,326 INFO  [sqlalchemy.engine.base.Engine.0x...048c] {<params>}
17:37:48,660 DEBUG [myapp.somemessage]

如果你在操作之后记录了myapp.somemessage,你就知道完成 SQL 部分花费了 334ms。

记录 SQL 还会说明是否发出了数十/数百个查询,这些查询可以更好地组织成更少的查询。当使用 SQLAlchemy ORM 时,“eager loading”特性提供了部分(contains_eager())或完全(joinedload()subqueryload())自动化此活动,但是没有 ORM 的“eager loading”通常意味着使用连接,以便结果可以在一个结果集中加载而不是随着更多深度的添加而增加查询的数量(即 r + r*r2 + r*r2*r3 …)

对于更长期的查询性能分析,或者实现应用程序端的“慢查询”监视器,可以使用事件来拦截游标执行,使用以下类似的配方:

from sqlalchemy import event
from sqlalchemy.engine import Engine
import time
import logging

logging.basicConfig()
logger = logging.getLogger("myapp.sqltime")
logger.setLevel(logging.DEBUG)

@event.listens_for(Engine, "before_cursor_execute")
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    conn.info.setdefault("query_start_time", []).append(time.time())
    logger.debug("Start Query: %s", statement)

@event.listens_for(Engine, "after_cursor_execute")
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    total = time.time() - conn.info["query_start_time"].pop(-1)
    logger.debug("Query Complete!")
    logger.debug("Total Time: %f", total)

在上述例子中,我们使用ConnectionEvents.before_cursor_execute()ConnectionEvents.after_cursor_execute()事件来在执行语句时建立拦截点。我们使用info字典在连接上附加一个计时器;我们在这里使用堆栈,因为偶尔会出现游标执行事件嵌套的情况。

代码性能分析

如果日志显示单个查询花费的时间太长,你需要分解在数据库内处理查询、通过网络发送结果、被 DBAPI 处理以及最终由 SQLAlchemy 的结果集和/或 ORM 层接收的时间。每个阶段都可能存在自己的瓶颈,具体取决于具体情况。

为此,您需要使用Python Profiling Module。以下是一个将分析集成到上下文管理器中的简单示例:

import cProfile
import io
import pstats
import contextlib

@contextlib.contextmanager
def profiled():
    pr = cProfile.Profile()
    pr.enable()
    yield
    pr.disable()
    s = io.StringIO()
    ps = pstats.Stats(pr, stream=s).sort_stats("cumulative")
    ps.print_stats()
    # uncomment this to see who's calling what
    # ps.print_callers()
    print(s.getvalue())

要对代码段进行分析:

with profiled():
    session.scalars(select(FooClass).where(FooClass.somevalue == 8)).all()

分析的输出可以用来了解时间花在哪里。分析输出的一部分如下所示:

13726 function calls (13042 primitive calls) in 0.014 seconds

Ordered by: cumulative time

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
222/21    0.001    0.000    0.011    0.001 lib/sqlalchemy/orm/loading.py:26(instances)
220/20    0.002    0.000    0.010    0.001 lib/sqlalchemy/orm/loading.py:327(_instance)
220/20    0.000    0.000    0.010    0.000 lib/sqlalchemy/orm/loading.py:284(populate_state)
   20    0.000    0.000    0.010    0.000 lib/sqlalchemy/orm/strategies.py:987(load_collection_from_subq)
   20    0.000    0.000    0.009    0.000 lib/sqlalchemy/orm/strategies.py:935(get)
    1    0.000    0.000    0.009    0.009 lib/sqlalchemy/orm/strategies.py:940(_load)
   21    0.000    0.000    0.008    0.000 lib/sqlalchemy/orm/strategies.py:942(<genexpr>)
    2    0.000    0.000    0.004    0.002 lib/sqlalchemy/orm/query.py:2400(__iter__)
    2    0.000    0.000    0.002    0.001 lib/sqlalchemy/orm/query.py:2414(_execute_and_instances)
    2    0.000    0.000    0.002    0.001 lib/sqlalchemy/engine/base.py:659(execute)
    2    0.000    0.000    0.002    0.001 lib/sqlalchemy/sql/elements.py:321(_execute_on_connection)
    2    0.000    0.000    0.002    0.001 lib/sqlalchemy/engine/base.py:788(_execute_clauseelement)

...

在上面的例子中,我们可以看到 instances() SQLAlchemy 函数被调用了 222 次(递归调用,外部调用 21 次),总共花费了 .011 秒来执行所有调用。

执行慢

这些调用的具体情况可以告诉我们时间花在哪里。例如,如果您看到时间花在 cursor.execute() 内部,例如针对 DBAPI:

2    0.102    0.102    0.204    0.102 {method 'execute' of 'sqlite3.Cursor' objects}

这将表示数据库需要很长时间才能开始返回结果,这意味着您的查询应该进行优化,可以通过添加索引或重构查询和/或底层架构来实现。对于这项任务,应该使用数据库后端提供的 EXPLAIN、SHOW PLAN 等系统来分析查询计划。

结果获取慢 - 核心

另一方面,如果你看到与获取行相关的许多调用,或者 fetchall() 的调用时间非常长,这可能意味着查询返回的行数超出了预期,或者获取行本身很慢。ORM 本身通常使用 fetchall() 来获取行(如果使用 Query.yield_per() 选项,则使用 fetchmany())。

通过在 DBAPI 级别使用 fetchall(),会导致调用非常慢,这可能表示行数过多:

2    0.300    0.600    0.300    0.600 {method 'fetchall' of 'sqlite3.Cursor' objects}

即使最终结果似乎没有很多行,但意外地大量行数可能是笛卡尔积的结果 - 当多组行未适当地连接在一起时。如果在复杂查询中使用了错误的Column 对象,从而引入了意外的额外 FROM 子句,那么使用 SQLAlchemy Core 或 ORM 查询往往很容易产生这种行为。

另一方面,在 DBAPI 级别使用 fetchall() 快速,但当 SQLAlchemy 的CursorResult 被要求执行 fetchall() 时却很慢,可能表示数据类型处理慢,比如 Unicode 转换等:

# the DBAPI cursor is fast...
2    0.020    0.040    0.020    0.040 {method 'fetchall' of 'sqlite3.Cursor' objects}

...

# but SQLAlchemy's result proxy is slow, this is type-level processing
2    0.100    0.200    0.100    0.200 lib/sqlalchemy/engine/result.py:778(fetchall)

在某些情况下,后端可能正在进行不需要的类型级处理。更具体地说,看到类型 API 中的慢调用是更好的指标 - 下面是使用此类类型时的情况:

from sqlalchemy import TypeDecorator
import time

class Foo(TypeDecorator):
    impl = String

    def process_result_value(self, value, thing):
        # intentionally add slowness for illustration purposes
        time.sleep(0.001)
        return value

这个有意慢操作的分析输出可以看起来像这样:

200    0.001    0.000    0.237    0.001 lib/sqlalchemy/sql/type_api.py:911(process)
200    0.001    0.000    0.236    0.001 test.py:28(process_result_value)
200    0.235    0.001    0.235    0.001 {time.sleep}

也就是说,我们在 type_api 系统中看到了许多昂贵的调用,而实际耗时的是 time.sleep() 调用。

请确保查阅方言文档以了解关于这个级别已知的性能调优建议的说明,特别是对于像 Oracle 这样的数据库。在这种情况下可能有一些关于确保数字精度或字符串处理的系统不需要在所有情况下都需要的系统。

在行提取性能受影响的更低级别可能还有更多的点;例如,如果花费的时间似乎集中在像 socket.receive() 这样的调用上,这可能表明除了实际的网络连接之外,一切都很快,而且花费了太多时间在数据在网络上传输上。

结果获取速度慢 - ORM

要检测 ORM 提取行的慢速(这是性能关注的最常见领域),调用如 populate_state()_instance() 将说明单个 ORM 对象的填充情况:

# the ORM calls _instance for each ORM-loaded row it sees, and
# populate_state for each ORM-loaded row that results in the population
# of an object's attributes
220/20    0.001    0.000    0.010    0.000 lib/sqlalchemy/orm/loading.py:327(_instance)
220/20    0.000    0.000    0.009    0.000 lib/sqlalchemy/orm/loading.py:284(populate_state)

ORM 将行转换为 ORM 映射对象的速度慢是这个操作的复杂性与 cPython 的开销的产物。减轻这种情况的常见策略包括:

  • 获取单个列而不是完整的实体,即:

    select(User.id, User.name)
    

    而不是:

    select(User)
    
  • 使用Bundle对象来组织基于列的结果:

    u_b = Bundle("user", User.id, User.name)
    a_b = Bundle("address", Address.id, Address.email)
    
    for user, address in session.execute(select(u_b, a_b).join(User.addresses)):
        ...
    
  • 使用结果缓存 - 参见 Dogpile Caching 了解深入示例。

  • 考虑使用像 PyPy 这样的更快的解释器。

分析结果可能有点令人生畏,但经过一些实践后,它们就会变得非常容易阅读。

另请参阅

性能 - 一套具有捆绑式分析功能的性能演示。

我正在使用 ORM 插入 40 万行,但速度真的很慢!

ORM 插入的性质已经发生了变化,因为大多数包含的驱动程序在 SQLAlchemy 2.0 中都使用了 RETURNING 和 insertmanyvalues 支持。请参阅 除了 MySQL 外的所有后端现在都已实现优化的 ORM 批量插入 一节了解详情。

总的来说,SQLAlchemy 内置的驱动程序,除了 MySQL 外,现在应该提供非常快的 ORM 批量插入性能。

第三方驱动程序也可以选择使用一些小的代码更改来使用新的批量基础架构,假设他们的后端支持所需的语法。SQLAlchemy 开发人员鼓励第三方方言的用户发布关于这些驱动程序的问题,以便他们可以联系 SQLAlchemy 开发人员寻求帮助。

为什么我升级到 1.4 和/或 2.x 后我的应用程序变慢了?

截至版本 1.4,SQLAlchemy 包含一个 SQL 编译缓存设施,它允许核心和 ORM SQL 构造缓存它们的字符串形式,以及用于从语句中获取结果的其他结构信息,从而在下次使用另一个结构上等价的构造时跳过相对昂贵的字符串编译过程。该系统依赖于为所有 SQL 构造实现的功能,包括诸如Columnselect()TypeEngine对象等,以生成完全代表它们状态的缓存键,在影响 SQL 编译过程的程度上。

缓存系统使得 SQLAlchemy 1.4 及以上版本在将 SQL 构造反复转换为字符串方面比 SQLAlchemy 1.3 更高效。但是,这仅在启用了使用的方言和 SQL 构造的缓存时才有效;如果没有启用,字符串编译通常类似于 SQLAlchemy 1.3,某些情况下速度略有降低。

但是,有一种情况,即如果禁用了 SQLAlchemy 的新缓存系统(由于以下原因),则 ORM 的性能实际上可能显着低于 1.3 或其他先前版本,原因是在 1.3 和以前的版本中,ORM 惰性加载器和对象刷新查询中没有缓存,而是使用了现在已经过时的BakedQuery系统。如果应用程序在切换到 1.4 后性能显着下降(测量操作完成所需的时间为 30%或更高),则这可能是问题的主要原因,以下是缓解措施。

另请参阅

SQL 编译缓存 - 缓存系统概述

对象不会产生缓存键,性能影响 - 关于未启用缓存的元素生成警告的附加信息。

第一步 - 打开 SQL 日志并确认缓存是否正常工作

在这里,我们希望使用引擎日志记录中描述的技术,寻找带有[no key]指示器或甚至[dialect does not support caching]的语句。对于成功参与缓存系统的 SQL 语句,我们将看到首次调用语句时指示为[generated in Xs],对于绝大多数后续语句,将看到[cached since Xs ago]。如果特别是对于 SELECT 语句,或者如果由于[dialect does not support caching]而完全禁用缓存,则这可能是性能显著下降的原因。

另请参阅

使用日志估算缓存性能

第二步 - 确定是哪些构造物阻止了缓存的启用

假设语句未被缓存,应该会在应用程序日志的早期(仅适用于 SQLAlchemy 1.4.28 及以上版本)中发出警告,指示不参与缓存的方言、TypeEngine 对象和 SQL 构造。

对于像那些扩展 TypeDecoratorUserDefinedType 的用户定义数据类型,警告将如下所示:

sqlalchemy.ext.SAWarning: MyType will not produce a cache key because the
``cache_ok`` attribute is not set to True. This can have significant
performance implications including some performance degradations in
comparison to prior SQLAlchemy versions. Set this attribute to True if this
type object's state is safe to use in a cache key, or False to disable this
warning.

对于自定义和第三方 SQL 元素,比如那些使用 自定义 SQL 构造和编译扩展 中描述的技术构建的元素,这些警告将如下所示:

sqlalchemy.exc.SAWarning: Class MyClass will not make use of SQL
compilation caching as it does not set the 'inherit_cache' attribute to
``True``. This can have significant performance implications including some
performance degradations in comparison to prior SQLAlchemy versions. Set
this attribute to True if this object can make use of the cache key
generated by the superclass. Alternatively, this attribute may be set to
False which will disable this warning.

对于使用 Dialect 类层次结构的自定义和第三方方言,警告将如下所示:

sqlalchemy.exc.SAWarning: Dialect database:driver will not make use of SQL
compilation caching as it does not set the 'supports_statement_cache'
attribute to ``True``. This can have significant performance implications
including some performance degradations in comparison to prior SQLAlchemy
versions. Dialect maintainers should seek to set this attribute to True
after appropriate development and testing for SQLAlchemy 1.4 caching
support. Alternatively, this attribute may be set to False which will
disable this warning.

第三步 - 为给定对象启用缓存和/或寻求替代方案

缓解缓存不足的步骤包括:

  • 检查并设置 ExternalType.cache_okTrue,用于所有继承自 TypeDecoratorUserDefinedType 的自定义类型,以及这些类型的子类,如 PickleType。只有在自定义类型不包含影响其呈现 SQL 的其他状态属性时才设置此项:

    class MyCustomType(TypeDecorator):
        cache_ok = True
        impl = String
    

    如果使用的类型来自第三方库,请与该库的维护者联系,以便进行调整和发布。

    另请参阅

    ExternalType.cache_ok - 关于启用自定义数据类型缓存的要求背景信息。

  • 确保第三方方言将 Dialect.supports_statement_cache 设置为 True。这表示第三方方言的维护者已确保其方言与 SQLAlchemy 1.4 或更高版本兼容,并且他们的方言不包含可能妨碍缓存的任何编译特性。由于有一些常见的编译模式实际上可能会干扰缓存,因此方言维护者必须仔细检查和测试此内容,并针对任何无法与缓存一起使用的旧模式进行调整。

    另请参见

    第三方方言的缓存 - 第三方方言参与 SQL 语句缓存的背景和示例。

  • 自定义 SQL 类,包括使用自定义 SQL 构造和编译扩展可能创建的所有 DQL / DML 构造,以及对象的临时子类,如ColumnTableHasCacheKey.inherit_cache 属性可以设置为 True,用于简单的子类,这些子类不包含影响 SQL 编译的子类特定状态信息。

    另请参见

    为自定义构造启用缓存支持 - 应用HasCacheKey.inherit_cache 属性的指南。

另请参见

SQL 编译缓存 - 缓存系统概述

对象不会生成缓存键,性能影响 - 在未为特定构造和/或方言启用缓存时发出警告的背景信息。

步骤一 - 打开 SQL 日志记录并确认缓存是否起作用

在这里,我们想要使用引擎日志记录中描述的技术,查找具有 [no key] 指示器或甚至 [dialect does not support caching] 的语句。当首次调用语句时,我们将看到参与缓存系统的 SQL 语句指示 [generated in Xs],然后对于绝大多数后续语句,指示为 [cached since Xs ago]。如果 [no key] 特别是对于 SELECT 语句普遍存在,或者如果由于 [dialect does not support caching] 而完全禁用缓存,这可能是导致性能严重下降的原因。

另请参见

使用日志估算缓存性能

第二步 - 确定哪些构造阻止了缓存的启用

假设语句未被缓存,则应在应用程序的日志中尽早发出警告(仅适用于 SQLAlchemy 1.4.28 及以上版本),指示不参与缓存的方言、TypeEngine 对象和 SQL 构造。

对于用户定义的数据类型,比如那些扩展了TypeDecoratorUserDefinedType的数据类型,警告信息如下:

sqlalchemy.ext.SAWarning: MyType will not produce a cache key because the
``cache_ok`` attribute is not set to True. This can have significant
performance implications including some performance degradations in
comparison to prior SQLAlchemy versions. Set this attribute to True if this
type object's state is safe to use in a cache key, or False to disable this
warning.

对于自定义和第三方 SQL 元素,例如使用 自定义 SQL 构造和编译扩展 中描述的技术构造的元素,这些警告信息如下:

sqlalchemy.exc.SAWarning: Class MyClass will not make use of SQL
compilation caching as it does not set the 'inherit_cache' attribute to
``True``. This can have significant performance implications including some
performance degradations in comparison to prior SQLAlchemy versions. Set
this attribute to True if this object can make use of the cache key
generated by the superclass. Alternatively, this attribute may be set to
False which will disable this warning.

对于使用 Dialect 类层次结构的自定义和第三方方言,警告信息如下:

sqlalchemy.exc.SAWarning: Dialect database:driver will not make use of SQL
compilation caching as it does not set the 'supports_statement_cache'
attribute to ``True``. This can have significant performance implications
including some performance degradations in comparison to prior SQLAlchemy
versions. Dialect maintainers should seek to set this attribute to True
after appropriate development and testing for SQLAlchemy 1.4 caching
support. Alternatively, this attribute may be set to False which will
disable this warning.

第三步 - 为给定的对象启用缓存和/或寻找替代方案

缓存缺失的缓解步骤包括:

  • 查看并设置 ExternalType.cache_ok,对于所有扩展自 TypeDecoratorUserDefinedType 的自定义类型,以及这些类型的子类,例如 PickleType。仅在自定义类型不包含影响其呈现 SQL 的其他状态属性时才设置此选项:

    class MyCustomType(TypeDecorator):
        cache_ok = True
        impl = String
    

    如果使用的类型来自第三方库,请咨询该库的维护者,以便进行调整并发布。

    另请参阅

    ExternalType.cache_ok - 启用自定义数据类型缓存的要求背景。

  • 确保第三方方言设置Dialect.supports_statement_cacheTrue。这表示第三方方言的维护者已确保他们的方言与 SQLAlchemy 1.4 或更高版本兼容,并且他们的方言不包含任何可能干扰缓存的编译特性。由于有一些常见的编译模式实际上可能会干扰缓存,因此方言维护者需要仔细检查和测试,并调整任何不适用于缓存的旧模式。

    另请参阅

    第三方方言的缓存 - 第三方方言参与 SQL 语句缓存的背景和示例。

  • 自定义 SQL 类,包括使用 自定义 SQL 构造和编译扩展 创建的所有 DQL / DML 构造,以及对象的临时子类,例如 ColumnTable。对于不包含影响 SQL 编译的子类特定状态信息的简单子类,可以将 HasCacheKey.inherit_cache 属性设置为 True

    另请参阅

    为自定义结构启用缓存支持 - 应用HasCacheKey.inherit_cache属性的指南。

另请参阅

SQL 编译缓存 - 缓存系统概述

对象不会生成缓存密钥,性能影响 - 当为特定构造和/或方言禁用缓存时发出警告的背景信息。

如何对由 SQLAlchemy 驱动的应用进行性能分析?

寻找性能问题通常涉及两种策略。一种是查询分析,另一种是代码分析。

查询分析

有时候,仅仅通过普通的 SQL 记录(通过 python 的 logging 模块启用,或者通过create_engine()上的 echo=True 参数启用)就可以了解操作花费的时间。例如,如果在 SQL 操作之后记录了一些内容,你会在日志中看到像这样的信息:

17:37:48,325 INFO  [sqlalchemy.engine.base.Engine.0x...048c] SELECT ...
17:37:48,326 INFO  [sqlalchemy.engine.base.Engine.0x...048c] {<params>}
17:37:48,660 DEBUG [myapp.somemessage]

如果你在操作之后记录了 myapp.somemessage,你就知道完成 SQL 部分花费了 334ms。

记录 SQL 也会说明是否发出了数十/数百个查询,这些查询可以更好地组织成更少的查询。在使用 SQLAlchemy ORM 时,“急加载”功能提供了部分 (contains_eager()) 或完全 (joinedload(), subqueryload()) 自动化此活动,但是没有 ORM 的“急加载”通常意味着使用连接,以便跨多个表加载结果集,而不是随着深度的增加而增加查询次数(即 r + r*r2 + r*r2*r3 …)

对于更长期的查询分析,或者实现应用程序端的“慢查询”监视器,可以使用事件拦截游标执行,使用以下类似的方法:

from sqlalchemy import event
from sqlalchemy.engine import Engine
import time
import logging

logging.basicConfig()
logger = logging.getLogger("myapp.sqltime")
logger.setLevel(logging.DEBUG)

@event.listens_for(Engine, "before_cursor_execute")
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    conn.info.setdefault("query_start_time", []).append(time.time())
    logger.debug("Start Query: %s", statement)

@event.listens_for(Engine, "after_cursor_execute")
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    total = time.time() - conn.info["query_start_time"].pop(-1)
    logger.debug("Query Complete!")
    logger.debug("Total Time: %f", total)

在上面的例子中,我们使用 ConnectionEvents.before_cursor_execute()ConnectionEvents.after_cursor_execute() 事件来建立在执行语句时的拦截点。我们使用 info 字典在连接上附加一个计时器;我们在这里使用堆栈,以处理游标执行事件可能嵌套的偶发情况。

代码分析

如果日志显示单个查询花费的时间过长,您需要了解在数据库内部处理查询、通过网络发送结果、由 DBAPI 处理以及最终由 SQLAlchemy 的结果集和/或 ORM 层接收的时间分别花费了多少。每个阶段都可能出现自己的瓶颈,具体取决于具体情况。

为此,您需要使用 Python Profiling Module。以下是一个将分析集成到上下文管理器中的简单示例:

import cProfile
import io
import pstats
import contextlib

@contextlib.contextmanager
def profiled():
    pr = cProfile.Profile()
    pr.enable()
    yield
    pr.disable()
    s = io.StringIO()
    ps = pstats.Stats(pr, stream=s).sort_stats("cumulative")
    ps.print_stats()
    # uncomment this to see who's calling what
    # ps.print_callers()
    print(s.getvalue())

要对代码段进行分析:

with profiled():
    session.scalars(select(FooClass).where(FooClass.somevalue == 8)).all()

分析的输出可以用来了解时间花费在哪里。分析输出的一部分看起来像这样:

13726 function calls (13042 primitive calls) in 0.014 seconds

Ordered by: cumulative time

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
222/21    0.001    0.000    0.011    0.001 lib/sqlalchemy/orm/loading.py:26(instances)
220/20    0.002    0.000    0.010    0.001 lib/sqlalchemy/orm/loading.py:327(_instance)
220/20    0.000    0.000    0.010    0.000 lib/sqlalchemy/orm/loading.py:284(populate_state)
   20    0.000    0.000    0.010    0.000 lib/sqlalchemy/orm/strategies.py:987(load_collection_from_subq)
   20    0.000    0.000    0.009    0.000 lib/sqlalchemy/orm/strategies.py:935(get)
    1    0.000    0.000    0.009    0.009 lib/sqlalchemy/orm/strategies.py:940(_load)
   21    0.000    0.000    0.008    0.000 lib/sqlalchemy/orm/strategies.py:942(<genexpr>)
    2    0.000    0.000    0.004    0.002 lib/sqlalchemy/orm/query.py:2400(__iter__)
    2    0.000    0.000    0.002    0.001 lib/sqlalchemy/orm/query.py:2414(_execute_and_instances)
    2    0.000    0.000    0.002    0.001 lib/sqlalchemy/engine/base.py:659(execute)
    2    0.000    0.000    0.002    0.001 lib/sqlalchemy/sql/elements.py:321(_execute_on_connection)
    2    0.000    0.000    0.002    0.001 lib/sqlalchemy/engine/base.py:788(_execute_clauseelement)

...

在上面的例子中,我们可以看到 instances() SQLAlchemy 函数被调用了 222 次(递归调用,并且从外部调用了 21 次),所有调用总共花费了 .011 秒。

执行速度慢

这些调用的具体信息可以告诉我们时间花费在哪里。例如,如果您看到时间花费在 cursor.execute() 内部,例如针对 DBAPI:

2    0.102    0.102    0.204    0.102 {method 'execute' of 'sqlite3.Cursor' objects}

这表明数据库启动返回结果花费了很长时间,这意味着你的查询应该进行优化,可以通过添加索引或重构查询和/或底层模式来实现。对于这项任务,有必要分析查询计划,使用诸如 EXPLAIN、SHOW PLAN 等数据库后端提供的系统。

结果获取缓慢 - Core

另一方面,如果您看到与获取行相关的成千上万次调用,或者对 fetchall() 的调用非常长,这可能意味着您的查询返回的行数比预期的多,或者获取行本身很慢。ORM 本身通常使用 fetchall() 来获取行(或者如果使用了 Query.yield_per() 选项,则使用 fetchmany())。

非常大量的行数将通过在 DBAPI 级别非常慢的调用 fetchall() 来表示:

2    0.300    0.600    0.300    0.600 {method 'fetchall' of 'sqlite3.Cursor' objects}

即使最终结果似乎没有很多行,但出现意外地大量行的情况可能是笛卡尔积的结果 - 当多组行未经适当连接地组合在一起时。如果在复杂查询中使用了错误的 Column 对象,导致引入意外的额外 FROM 子句,那么用 SQLAlchemy Core 或 ORM 查询往往很容易产生这种行为。

另一方面,在 DBAPI 级别快速调用 fetchall(),但当 SQLAlchemy 的 CursorResult 要求执行 fetchall() 时出现缓慢,可能表示数据类型的处理速度较慢,例如 unicode 转换等:

# the DBAPI cursor is fast...
2    0.020    0.040    0.020    0.040 {method 'fetchall' of 'sqlite3.Cursor' objects}

...

# but SQLAlchemy's result proxy is slow, this is type-level processing
2    0.100    0.200    0.100    0.200 lib/sqlalchemy/engine/result.py:778(fetchall)

在某些情况下,后端可能正在进行不需要的类型级处理。更具体地说,看到类型 API 内的调用很慢更好,下面是我们使用这样一个类型时的情况:

from sqlalchemy import TypeDecorator
import time

class Foo(TypeDecorator):
    impl = String

    def process_result_value(self, value, thing):
        # intentionally add slowness for illustration purposes
        time.sleep(0.001)
        return value

这个有意慢操作的分析结果可以看起来像这样:

200    0.001    0.000    0.237    0.001 lib/sqlalchemy/sql/type_api.py:911(process)
200    0.001    0.000    0.236    0.001 test.py:28(process_result_value)
200    0.235    0.001    0.235    0.001 {time.sleep}

也就是说,我们在 type_api 系统内看到了很多昂贵的调用,而实际耗时的事情是 time.sleep() 调用。

确保检查 Dialect documentation 以了解此级别已知的性能调整建议,特别是对于像 Oracle 这样的数据库。可能有关于确保数值精度或字符串处理的系统可能并不在所有情况下都需要。

还可能存在更低级别的点导致行获取性能下降;例如,如果时间主要花在像 socket.receive() 这样的调用上,这可能表明除了网络连接本身外,其他所有东西都很快,而且花费了太多时间在网络上传输数据。

结果获取缓慢 - ORM

要检测 ORM 获取行的速度慢(这是性能关注的最常见领域),像 populate_state()_instance() 这样的调用将说明单个 ORM 对象的加载情况:

# the ORM calls _instance for each ORM-loaded row it sees, and
# populate_state for each ORM-loaded row that results in the population
# of an object's attributes
220/20    0.001    0.000    0.010    0.000 lib/sqlalchemy/orm/loading.py:327(_instance)
220/20    0.000    0.000    0.009    0.000 lib/sqlalchemy/orm/loading.py:284(populate_state)

ORM 将行转换为 ORM 映射对象的速度慢是由于此操作的复杂性与 cPython 的开销相结合造成的。缓解这种情况的常见策略包括:

  • 获取单个列而不是完整的实体,即:

    select(User.id, User.name)
    

    而不是:

    select(User)
    
  • 使用 Bundle 对象组织基于列的结果:

    u_b = Bundle("user", User.id, User.name)
    a_b = Bundle("address", Address.id, Address.email)
    
    for user, address in session.execute(select(u_b, a_b).join(User.addresses)):
        ...
    
  • 使用结果缓存 - 有关此的详细示例,请参见 Dogpile Caching。

  • 考虑使用像 PyPy 这样的更快的解释器。

一次性分析的输出可能有点令人生畏,但经过一些练习后,它们会变得非常容易阅读。

另请参阅

性能 - 一套具有捆绑分析功能的性能演示。

查询分析

有时,仅仅记录 SQL(通过 Python 的 logging 模块启用或通过 create_engine()echo=True 参数启用)可以让人了解到事情花费的时间。例如,如果在 SQL 操作之后记录了某些内容,则在日志中会看到类似于以下内容:

17:37:48,325 INFO  [sqlalchemy.engine.base.Engine.0x...048c] SELECT ...
17:37:48,326 INFO  [sqlalchemy.engine.base.Engine.0x...048c] {<params>}
17:37:48,660 DEBUG [myapp.somemessage]

如果在操作之后记录了 myapp.somemessage,则知道完成 SQL 部分花费了 334ms。

记录 SQL 还会说明是否发出了数十个/数百个查询,这些查询可以更好地组织为更少的查询。在使用 SQLAlchemy ORM 时,“急加载”功能提供了部分(contains_eager())或完全(joinedload()subqueryload())自动化此活动,但在没有 ORM 的“急加载”时,通常意味着使用连接以便在一个结果集中加载多个表的结果,而不是随着深度的增加而增加查询次数(即 r + r*r2 + r*r2*r3 …)

为了更长期地分析查询,或者实现应用程序端的“慢查询”监视器,可以使用事件拦截游标执行,使用以下类似的方法:

from sqlalchemy import event
from sqlalchemy.engine import Engine
import time
import logging

logging.basicConfig()
logger = logging.getLogger("myapp.sqltime")
logger.setLevel(logging.DEBUG)

@event.listens_for(Engine, "before_cursor_execute")
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    conn.info.setdefault("query_start_time", []).append(time.time())
    logger.debug("Start Query: %s", statement)

@event.listens_for(Engine, "after_cursor_execute")
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    total = time.time() - conn.info["query_start_time"].pop(-1)
    logger.debug("Query Complete!")
    logger.debug("Total Time: %f", total)

以上,我们使用ConnectionEvents.before_cursor_execute()ConnectionEvents.after_cursor_execute()事件在语句执行时建立拦截点。我们在连接上附加一个计时器,使用info字典;在这里我们使用堆栈,偶尔情况下游标执行事件可能是嵌套的。

代码性能分析

如果日志显示个别查询花费了太长时间,您需要详细了解在数据库内部处理查询、通过网络发送结果、由 DBAPI 处理以及最终由 SQLAlchemy 的结果集和/或 ORM 层接收的时间。每个阶段都可能存在自己的瓶颈,具体取决于特定情况。

为此,您需要使用Python 性能分析模块。以下是一个将性能分析嵌入到上下文管理器中的简单示例:

import cProfile
import io
import pstats
import contextlib

@contextlib.contextmanager
def profiled():
    pr = cProfile.Profile()
    pr.enable()
    yield
    pr.disable()
    s = io.StringIO()
    ps = pstats.Stats(pr, stream=s).sort_stats("cumulative")
    ps.print_stats()
    # uncomment this to see who's calling what
    # ps.print_callers()
    print(s.getvalue())

对代码段进行性能分析:

with profiled():
    session.scalars(select(FooClass).where(FooClass.somevalue == 8)).all()

性能分析的输出可以让我们了解时间消耗在哪里。性能分析的一部分输出如下所示:

13726 function calls (13042 primitive calls) in 0.014 seconds

Ordered by: cumulative time

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
222/21    0.001    0.000    0.011    0.001 lib/sqlalchemy/orm/loading.py:26(instances)
220/20    0.002    0.000    0.010    0.001 lib/sqlalchemy/orm/loading.py:327(_instance)
220/20    0.000    0.000    0.010    0.000 lib/sqlalchemy/orm/loading.py:284(populate_state)
   20    0.000    0.000    0.010    0.000 lib/sqlalchemy/orm/strategies.py:987(load_collection_from_subq)
   20    0.000    0.000    0.009    0.000 lib/sqlalchemy/orm/strategies.py:935(get)
    1    0.000    0.000    0.009    0.009 lib/sqlalchemy/orm/strategies.py:940(_load)
   21    0.000    0.000    0.008    0.000 lib/sqlalchemy/orm/strategies.py:942(<genexpr>)
    2    0.000    0.000    0.004    0.002 lib/sqlalchemy/orm/query.py:2400(__iter__)
    2    0.000    0.000    0.002    0.001 lib/sqlalchemy/orm/query.py:2414(_execute_and_instances)
    2    0.000    0.000    0.002    0.001 lib/sqlalchemy/engine/base.py:659(execute)
    2    0.000    0.000    0.002    0.001 lib/sqlalchemy/sql/elements.py:321(_execute_on_connection)
    2    0.000    0.000    0.002    0.001 lib/sqlalchemy/engine/base.py:788(_execute_clauseelement)

...

以上,我们可以看到instances() SQLAlchemy 函数被调用了 222 次(递归调用,从外部调用了 21 次),所有调用总共花了 0.011 秒。

执行速度慢

这些调用的具体细节可以告诉我们时间都花在哪里。例如,如果您看到在cursor.execute()内花费了时间,例如针对 DBAPI:

2    0.102    0.102    0.204    0.102 {method 'execute' of 'sqlite3.Cursor' objects}

这将表明数据库花费了很长时间才开始返回结果,这意味着您的查询应该被优化,可以通过添加索引或重组查询和/或底层架构来完成此任务。对查询计划的分析是有必要的,可以使用像 EXPLAIN、SHOW PLAN 等数据库后端提供的系统。

结果获取速度慢 - 核心

另一方面,如果你看到与获取行有关的成千上万次调用,或者对fetchall()的非常长时间的调用,这可能意味着你的查询返回的行数超出了预期,或者获取行本身的速度很慢。ORM 本身通常使用fetchall()来获取行(如果使用了Query.yield_per()选项,则使用fetchmany())。

非常慢的fetchall()调用会被 DBAPI 级别上指示出异乎寻常的大量行:

2    0.300    0.600    0.300    0.600 {method 'fetchall' of 'sqlite3.Cursor' objects}

如果行数意外地很大,即使最终结果似乎没有很多行,也可能是笛卡尔积的结果 - 当多组行组合在一起而没有适当地连接表时。如果在复杂查询中使用了错误的Column对象,拉入意外的 FROM 子句,很容易在 SQLAlchemy Core 或 ORM 查询中产生这种行为。

另一方面,在 DBAPI 级别快速调用fetchall(),但当要求 SQLAlchemy 的CursorResult执行fetchall()时变慢,可能表明在处理数据类型(如 unicode 转换等)时存在缓慢:

# the DBAPI cursor is fast...
2    0.020    0.040    0.020    0.040 {method 'fetchall' of 'sqlite3.Cursor' objects}

...

# but SQLAlchemy's result proxy is slow, this is type-level processing
2    0.100    0.200    0.100    0.200 lib/sqlalchemy/engine/result.py:778(fetchall)

在某些情况下,后端可能正在进行不必要的类型级处理。更具体地说,看到类型 API 中的调用很慢更好,下面是当我们使用这样的类型时的情况:

from sqlalchemy import TypeDecorator
import time

class Foo(TypeDecorator):
    impl = String

    def process_result_value(self, value, thing):
        # intentionally add slowness for illustration purposes
        time.sleep(0.001)
        return value

这个故意缓慢操作的分析输出看起来像这样:

200    0.001    0.000    0.237    0.001 lib/sqlalchemy/sql/type_api.py:911(process)
200    0.001    0.000    0.236    0.001 test.py:28(process_result_value)
200    0.235    0.001    0.235    0.001 {time.sleep}

也就是说,我们在type_api系统中看到许多昂贵的调用,而实际耗时的是time.sleep()调用。

确保查看 Dialect 文档以获取关于已知性能调优建议的说明,特别是对于像 Oracle 这样的数据库。可能存在确保数字精度或字符串处理的系统,在某些情况下可能不需要。

还可能存在更多低级别的点导致行提取性能下降;例如,如果花费的时间似乎集中在像socket.receive()这样的调用上,那可能表明除了实际的网络连接外,一切都很快,而花费太多时间在数据在网络上传输上。

结果提取缓慢 - ORM

要检测 ORM 提取行的缓慢(这是性能关注的最常见领域),像populate_state()_instance()这样的调用将说明单个 ORM 对象的填充:

# the ORM calls _instance for each ORM-loaded row it sees, and
# populate_state for each ORM-loaded row that results in the population
# of an object's attributes
220/20    0.001    0.000    0.010    0.000 lib/sqlalchemy/orm/loading.py:327(_instance)
220/20    0.000    0.000    0.009    0.000 lib/sqlalchemy/orm/loading.py:284(populate_state)

ORM 在将行转换为 ORM 映射对象时的缓慢是这个操作的复杂性与 cPython 的开销相结合的产物。减轻这种情况的常见策略包括:

  • 获取单个列而不是完整实体,也就是:

    select(User.id, User.name)
    

    而不是:

    select(User)
    
  • 使用Bundle对象来组织基于列的结果:

    u_b = Bundle("user", User.id, User.name)
    a_b = Bundle("address", Address.id, Address.email)
    
    for user, address in session.execute(select(u_b, a_b).join(User.addresses)):
        ...
    
  • 使用结果缓存 - 参见 Dogpile Caching 以获取关于此的深入示例。

  • 考虑使用像 PyPy 这样的更快解释器。

分析的输出可能有点令人生畏,但经过一些练习后,它们非常容易阅读。

另请参阅

性能 - 一套具有捆绑分析功能的性能演示。

我正在使用 ORM 插入 40 万行,速度真的很慢!

ORM 插入的性质已经改变,因为大多数包含的驱动程序在 SQLAlchemy 2.0 中都使用了带有 insertmanyvalues 支持的 RETURNING。详情请参见 优化的 ORM 批量插入现在已经为除 MySQL 外的所有后端实现 部分。

总的来说,除了 MySQL 外,SQLAlchemy 内置的驱动程序现在应该提供非常快速的 ORM 批量插入性能。

第三方驱动程序也可以通过一些小的代码更改选择使用新的批量基础设施,假设它们的后端支持必要的语法。SQLAlchemy 开发人员鼓励第三方方言的用户发布与这些驱动程序相关的问题,以便他们可以联系 SQLAlchemy 开发人员寻求帮助。

会话 / 查询

原文:docs.sqlalchemy.org/en/20/faq/sessions.html

  • 我正在使用 Session 重新加载数据,但它没有看到我在其他地方提交的更改

  • “此会话的事务已由于刷新期间的先前异常而回滚。”(或类似消息)

    • 但为什么 flush() 坚持发出 ROLLBACK?

    • 但为什么一个自动调用 ROLLBACK 不够?为什么我必须再次 ROLLBACK?

  • 如何创建一个始终向每个查询添加特定过滤器的查询?

  • 我的查询返回的对象数与 query.count() 告诉我的不一致 - 为什么?

  • 我已经创建了一个对 Outer Join 的映射,虽然查询返回行,但没有返回对象。为什么?

  • 我使用 joinedload()lazy=False 创建 JOIN/OUTER JOIN,但当我尝试添加 WHERE、ORDER BY、LIMIT 等条件时,SQLAlchemy 没有构造正确的查询(这取决于 (OUTER) JOIN)

  • 查询没有 __len__(),为什么?

  • 如何在 ORM 查询中使用文本 SQL?

  • 调用 Session.delete(myobject) 后,我的对象未从父集合中移除!

  • 加载对象时为什么不调用我的 __init__()

  • 如何在 SA 的 ORM 中使用 ON DELETE CASCADE?

  • 我将实例的“foo_id”属性设置为“7”,但“foo”属性仍然为 None - 应该加载 id 为 #7 的 Foo 吗?

  • 如何遍历与给定对象相关的所有对象?

  • 有没有一种方法可以自动只获取唯一关键字(或其他类型的对象),而不需要查询关键字并获取包含该关键字的行的引用?

  • 为什么 post_update 除了第一个 UPDATE 之外还会发出 UPDATE?

我重新加载了我的会话中的数据,但它没有看到我在其他地方提交的更改

这种行为的主要问题在于,会话表现得好像事务处于可串行化隔离状态一样,即使实际上并非如此(通常也不是)。从实际角度来看,这意味着会话在事务范围内已经读取的数据不会发生任何更改。

如果术语“隔离级别”不熟悉,那么您首先需要阅读此链接:

隔离级别

简而言之,可串行化隔离级通常意味着一旦在事务中选择了一系列行,每次重新发出该 SELECT 时都会获得相同的数据。如果您处于较低的隔离级别“可重复读”,您将看到新添加的行(不再看到已删除的行),但对于您已经加载的行,您不会看到任何更改。只有当您处于较低的隔离级别,例如“读取提交”,才有可能看到数据行更改其值。

有关在使用 SQLAlchemy ORM 时控制隔离级别的信息,请参阅设置事务隔离级别 / DBAPI AUTOCOMMIT。

为了极大地简化事情,Session本身是基于完全隔离的事务运行的,并且不会覆盖已经读取的任何映射属性,除非您告诉它这样做。尝试在进行中的事务中重新读取已加载的数据的用例是一个不常见的用例,在许多情况下没有任何效果,因此这被认为是例外而不是规范;为了在这种例外情况下工作,提供了几种方法允许在进行中的事务上下文中重新加载特定数据。

要理解我们在谈论Session时所说的“事务”是什么意思,您的Session只能在事务内部工作。有关概述,请参阅管理事务。

一旦我们弄清楚了我们的隔离级别是什么,并且我们认为我们的隔离级别设置得足够低,以便如果我们重新选择一行,我们应该能够在我们的 Session 中看到新数据,那么我们该如何看到它?

从最常见到最不常见的三种方式:

  1. 我们只需结束当前事务,并在下一次访问时启动新事务,通过调用 Session.commit()(请注意,如果 Session 处于较少使用的“自动提交”模式,则还会调用 Session.begin())。绝大多数应用和用例不会出现无法“看到”其他事务中的数据的问题,因为它们遵循了这一模式,这是短事务最佳实践的核心。有关此问题的一些想法,请参阅 何时构建 Session,何时提交它,何时关闭它?。

  2. 我们告诉我们的 Session 重新读取它已经读取过的行,要么在下次使用 Session.expire_all()Session.expire() 查询它们时,要么立即在对象上使用 refresh。有关此事的详细信息,请参阅 刷新 / 过期。

  3. 我们可以在设置了“填充现有”选项的情况下运行整个查询,这样它们读取行时就会覆盖已加载的对象。这是一个在 填充现有 中描述的执行选项。

但请记住,如果我们的隔离级别是可重复读或更高,则 ORM 无法看到行中的更改,除非我们开始新的事务。## “此会话的事务由于在 flush 期间发生的先前异常而回滚。”(或类似)

Session.flush() 引发异常、回滚事务,但后续对 Session 的命令未显式调用 Session.rollback()Session.close() 时会出现此错误。

通常对应于在 Session.flush()Session.commit() 上捕获异常并且不正确处理异常的应用程序。例如:

from sqlalchemy import create_engine, Column, Integer
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base(create_engine("sqlite://"))

class Foo(Base):
    __tablename__ = "foo"
    id = Column(Integer, primary_key=True)

Base.metadata.create_all()

session = sessionmaker()()

# constraint violation
session.add_all([Foo(id=1), Foo(id=1)])

try:
    session.commit()
except:
    # ignore error
    pass

# continue using session without rolling back
session.commit()

使用 Session 应该符合类似于此结构:

try:
    # <use session>
    session.commit()
except:
    session.rollback()
    raise
finally:
    session.close()  # optional, depends on use case

除了 flushes 外,很多事情都可能导致 try/except 内部的失败。应用程序应确保对基于 ORM 的过程应用某种“框架”系统,以便连接和事务资源具有明确的边界,并且如果发生任何失败条件,则可以显式地回滚事务。

这并不意味着整个应用程序都应该有 try/except 块,这将不是一种可扩展的架构。相反,一种典型的方法是,当首次调用基于 ORM 的方法和函数时,从最顶层调用函数的过程将处于一个块中,该块在一系列操作成功完成时提交事务,并且在任何原因失败时,包括失败的 flushes 时回滚事务。也有使用函数装饰器或上下文管理器来实现类似结果的方法。采取的方法取决于正在编写的应用程序的类型。

关于如何组织使用 Session 的详细讨论,请参阅何时构造会话,何时提交它,何时关闭它?。

但为什么 flush() 一定要发出 ROLLBACK 呢?

如果 Session.flush() 能够部分完成而不回滚,那将是很好的,但是由于其当前能力有限,因此这超出了它的当前能力范围,因为其内部簿记必须被修改,以便可以随时停止,并且与已刷新到数据库的内容完全一致。虽然从理论上讲这是可能的,但是增强功能的有用性因为许多数据库操作在任何情况下都需要回滚而大大降低。特别是,Postgres 有一些操作,一旦失败,事务就不允许继续进行:

test=> create table foo(id integer primary key);
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "foo_pkey" for table "foo"
CREATE TABLE
test=> begin;
BEGIN
test=> insert into foo values(1);
INSERT 0 1
test=> commit;
COMMIT
test=> begin;
BEGIN
test=> insert into foo values(1);
ERROR:  duplicate key value violates unique constraint "foo_pkey"
test=> insert into foo values(2);
ERROR:  current transaction is aborted, commands ignored until end of transaction block

SQLAlchemy 提供的解决这两个问题的方法是支持 SAVEPOINT,通过 Session.begin_nested()。使用 Session.begin_nested(),您可以在事务内部对可能失败的操作进行框架化,然后在保持封闭事务的同时“回滚”到失败之前的点。

但为什么一个自动调用 ROLLBACK 不够?为什么我必须再次 ROLLBACK?

flush()引起的回滚并不是完整事务块的结束;虽然它结束了正在进行的数据库事务,但从Session的角度来看,仍然存在一个现在处于非活动状态的事务。

给定这样一个块:

sess = Session()  # begins a logical transaction
try:
    sess.flush()

    sess.commit()
except:
    sess.rollback()

在上面,当首次创建一个Session时,假设没有使用“自动提交模式”,在Session内建立了一个逻辑事务。这个事务是“逻辑”的,因为直到调用 SQL 语句时才会实际使用任何数据库资源,此时会启动连接级和 DBAPI 级事务。然而,无论数据库级事务是否是其状态的一部分,逻辑事务将保持不变,直到使用Session.commit()Session.rollback()Session.close()结束它。

当上面的flush()失败时,代码仍然位于由 try/commit/except/rollback 块框定的事务中。如果flush()完全回滚逻辑事务,那么当我们到达except:块时,Session将处于干净状态,准备在全新事务上发出新的 SQL,并且调用Session.rollback()将不按顺序进行。特别是,此时Session已经开始了一个新事务,而Session.rollback()将错误地对其进行操作。在这个正常情况下应该进行回滚的地方,而不是允许 SQL 操作在新事务上继续进行,Session会拒绝继续直到显式回滚实际发生。

换句话说,期望调用代码始终调用 Session.commit()Session.rollback()Session.close() 来对应当前事务块。flush() 保持 Session 在这个事务块内,以便上述代码的行为是可预测和一致的。

如何使一个查询始终对每个查询添加某个过滤器?

请查看 FilteredQuery 的配方。

我的查询返回的对象数与 query.count() 告诉我的不一样 - 为什么?

Query 对象被要求返回一个 ORM 映射对象列表时,将根据主键对对象进行去重。也就是说,如果我们例如使用了在 使用 ORM 声明形式定义表元数据 中描述的 User 映射,并且我们有一个如下所示的 SQL 查询:

q = session.query(User).outerjoin(User.addresses).filter(User.name == "jack")

在教程中使用的示例数据中,addresses 表中有两行数据,其中 name'jack'、主键值为 5 的 users 行。如果我们要求上述查询的 Query.count(),我们将得到答案 2

>>> q.count()
2

但是,如果我们运行 Query.all() 或遍历查询,我们将得到一个元素

>>> q.all()
[User(id=5, name='jack', ...)]

这是因为当 Query 对象返回完整实体时,它们会被去重。如果我们改为请求单个列返回,则不会发生这种情况:

>>> session.query(User.id, User.name).outerjoin(User.addresses).filter(
...     User.name == "jack"
... ).all()
[(5, 'jack'), (5, 'jack')]

Query 将去重的主要原因有两个:

  • 允许联接预加载正常工作 - 联接预加载通过使用与相关表的连接来查询行,然后将这些连接的行路由到主对象的集合中来工作。为了做到这一点,它必须获取主对象主键在每个子条目中重复的行。这种模式可以继续到更深层的子集合,以便为单个主对象(如User(id=5))处理多行。去重允许我们按照查询的方式接收对象,例如所有名为'jack'的User()对象,对于我们来说是一个对象,其中User.addresses集合已经被急加载,就像在relationship()上通过lazy='joined'或通过joinedload()选项指示的那样。为了保持一致性,无论是否建立了联接加载,去重仍然适用,因为急加载背后的关键理念是这些选项永远不会影响结果。

  • 消除关于身份映射的混淆 - 这显然是较不重要的原因。由于Session使用了身份映射,即使我们的 SQL 结果集中有两行主键为 5 的记录,Session 中只有一个User(id=5)对象,必须在其身份上保持唯一,即其主键/类组合。如果一个查询User()对象,多次在列表中获取相同对象实际上并没有太多意义。有序集合可能更好地表示Query 在返回完整对象时所寻求的内容。

Query去重问题仍然存在问题,主要是因为Query.count()方法不一致,并且当前状态是最近的版本中的连接急加载首先被“子查询急加载”策略取代,最近是“select IN 急加载”策略,这两种策略通常更适合于集合急加载。随着这一演变的继续,SQLAlchemy 可能会更改 Query的行为,这也可能涉及新的 API,以更直接地控制此行为,并且也可能更改连接的急加载的行为,以创建更一致的使用模式。

我已经针对外连接创建了映射,但是虽然查询返回行,但没有返回对象。为什么?

由外连接返回的行可能包含主键的部分 NULL,因为主键是两个表的组合。Query对象忽略不具有可接受主键的传入行。根据Mapper上的allow_partial_pks标志的设置,如果该值至少具有一个非 NULL 值,则接受主键,或者如果该值没有 NULL 值,则接受该值。请参见 Mapper上的allow_partial_pks

我正在使用joinedload()lazy=False来创建 JOIN/OUTER JOIN,当我尝试添加 WHERE、ORDER BY、LIMIT 等条件时,SQLAlchemy 没有构造正确的查询。(这依赖于(OUTER)JOIN)

由连接的急加载生成的连接仅用于完全加载相关集合,并设计为不影响查询的主要结果。由于它们是匿名别名,因此不能直接引用。

关于这种行为的详细信息,请参见急加载的禅意。

查询没有__len__(),为什么?

应用于对象的 Python __len__() 魔术方法允许使用len()内置函数来确定集合的长度。直觉上,SQL 查询对象将__len__()链接到Query.count()方法,该方法发出 SELECT COUNT。不可能的原因是评估查询为列表将导致两次 SQL 调用而不是一次:

class Iterates:
    def __len__(self):
        print("LEN!")
        return 5

    def __iter__(self):
        print("ITER!")
        return iter([1, 2, 3, 4, 5])

list(Iterates())

输出:

ITER!
LEN!

如何在 ORM 查询中使用文本 SQL?

参见:

  • 从文本语句获取 ORM 结果 - 使用 Query 进行即席文本块。

  • 在会话中使用 SQL 表达式 - 直接使用 Session 进行文本 SQL 操作。

我调用 Session.delete(myobject),但它没有从父集合中删除!

查看关于删除的注释 - 从集合和标量关系中删除对象以了解此行为的描述。

当加载对象时,为什么我的 __init__() 没有被调用?

查看跨加载保持非映射状态以了解此行为的描述。

我如何在 SA 的 ORM 中使用 ON DELETE CASCADE?

SQLAlchemy 总是对当前加载在 Session 中的依赖行发出 UPDATE 或 DELETE 语句。对于未加载的行,默认情况下会发出 SELECT 语句来加载这些行并更新/删除它们;换句话说,它假定没有配置 ON DELETE CASCADE。要配置 SQLAlchemy 与 ON DELETE CASCADE 协作,请参见使用 ORM 关系的外键 ON DELETE cascade。

我将实例的“foo_id”属性设置为“7”,但“foo”属性仍然为None - 它不应该加载具有 id #7 的 Foo 吗?

ORM 的构建不支持根据外键属性变化驱动的关系的立即填充 - 相反,它被设计成反向工作 - 外键属性由 ORM 在幕后处理,最终用户自然设置对象关系。因此,设置 o.foo 的推荐方式是做到这一点 - 设置它!:

foo = session.get(Foo, 7)
o.foo = foo
Session.commit()

当然,对外键属性进行操作是完全合法的。但是,将外键属性设置为新值当前不会触发 relationship() 中涉及的“expire”事件。这意味着对于以下序列:

o = session.scalars(select(SomeClass).limit(1)).first()

# assume the existing o.foo_id value is None;
# accessing o.foo will reconcile this as ``None``, but will effectively
# "load" the value of None
assert o.foo is None

# now set foo_id to something.  o.foo will not be immediately affected
o.foo_id = 7

当首次访问时,o.foo 会加载其有效的数据库值为 None。将 o.foo_id = 7 设置为挂起更改的值为“7”,但尚未刷新 - 因此 o.foo 仍然为 None

# attribute is already "loaded" as None, has not been
# reconciled with o.foo_id = 7 yet
assert o.foo is None

对于 o.foo 基于外键变化进行加载,通常在提交后自然实现,因为提交既刷新了新的外键值,又使所有状态过期:

session.commit()  # expires all attributes

foo_7 = session.get(Foo, 7)

# o.foo will lazyload again, this time getting the new object
assert o.foo is foo_7

更精简的操作是单独过期属性 - 这可以为任何持久的对象执行,使用Session.expire()

o = session.scalars(select(SomeClass).limit(1)).first()
o.foo_id = 7
Session.expire(o, ["foo"])  # object must be persistent for this

foo_7 = session.get(Foo, 7)

assert o.foo is foo_7  # o.foo lazyloads on access

请注意,如果对象不是持久的但存在于Session中,则被称为待定。这意味着对象的行尚未插入到数据库中。对于这样的对象,设置foo_id在行被插入之前没有意义;否则还没有行:

new_obj = SomeClass()
new_obj.foo_id = 7

Session.add(new_obj)

# returns None but this is not a "lazyload", as the object is not
# persistent in the DB yet, and the None value is not part of the
# object's state
assert new_obj.foo is None

Session.flush()  # emits INSERT

assert new_obj.foo is foo_7  # now it loads

该配方ExpireRelationshipOnFKChange展示了一个使用 SQLAlchemy 事件的示例,以协调设置具有多对一关系的外键属性。

如何遍历所有与给定对象相关联的对象?

具有其他对象相关联的对象将对应于设置在映射器之间的relationship()构造。此代码片段将迭代所有对象,并校正循环:

from sqlalchemy import inspect

def walk(obj):
    deque = [obj]

    seen = set()

    while deque:
        obj = deque.pop(0)
        if obj in seen:
            continue
        else:
            seen.add(obj)
            yield obj
        insp = inspect(obj)
        for relationship in insp.mapper.relationships:
            related = getattr(obj, relationship.key)
            if relationship.uselist:
                deque.extend(related)
            elif related is not None:
                deque.append(related)

该函数可以演示如下:

Base = declarative_base()

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    bs = relationship("B", backref="a")

class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))
    c_id = Column(ForeignKey("c.id"))
    c = relationship("C", backref="bs")

class C(Base):
    __tablename__ = "c"
    id = Column(Integer, primary_key=True)

a1 = A(bs=[B(), B(c=C())])

for obj in walk(a1):
    print(obj)

输出:

<__main__.A object at 0x10303b190>
<__main__.B object at 0x103025210>
<__main__.B object at 0x10303b0d0>
<__main__.C object at 0x103025490>

是否有一种方法可以自动地仅获取唯一的关键词(或其他类型的对象),而不是对关键词进行查询并获取包含该关键词的行的引用?

当人们阅读文档中的多对多示例时,他们会遇到一个事实,即如果您两次创建相同的Keyword,它会被放入数据库两次。这有点不方便。

这个UniqueObject配方是为了解决这个问题而创建的。

为什么 post_update 除了第一个 UPDATE 外还发出 UPDATE?

post_update 功能,在指向自身的行/相互依赖的行文档中记录,涉及在对特定关系绑定的外键进行更改时发出 UPDATE 语句,除了通常会对目标行发出的 INSERT/UPDATE/DELETE 之外。虽然这个 UPDATE 语句的主要目的是与 INSERT 或 DELETE 配对,以便它可以在 INSERT 或 DELETE 操作后设置或取消设置一个外键引用,以断开与相互依赖的外键的循环,但它目前也被捆绑为在目标行本身被更新时发出的第二个 UPDATE。在这种情况下,post_update 发出的 UPDATE 通常 是不必要的,并且通常会显得浪费。

然而,对尝试删除这种“UPDATE / UPDATE”行为的一些研究表明,不仅需要在 post_update 实现中进行重大更改,而且还需要在与 post_update 不相关的区域进行更改,以使其工作,因为在某些情况下需要对非 post_update 部分的操作顺序进行反转,这反过来又会影响其他情况,例如正确处理引用主键值的 UPDATE(参见#1063 以获取概念验证)。

答案是,“post_update”用于打破两个相互依赖的外键之间的循环,并且使得此循环打破仅限于目标表的 INSERT/DELETE 意味着其他地方的 UPDATE 语句的顺序需要变得自由化,导致其他边缘情况的破坏。## 我正在使用我的会话重新加载数据,但它没有看到我在其他地方提交的更改

关于这种行为的主要问题是,会话的行为就像事务处于可串行化隔离状态一样,即使事务并不是(通常情况下并不是)。从实际角度来看,这意味着会话不会更改已经在事务范围内读取的任何数据。

如果术语“隔离级别”不熟悉,那么您首先需要阅读此链接:

隔离级别

简而言之,可串行化隔离级别通常意味着一旦您在事务中选择一系列行,您每次重新发出该 SELECT 时都会得到相同的数据。如果您处于较低的隔离级别,例如“可重复读”,您将看到新添加的行(不再看到删除的行),但对于您已经加载的行,您不会看到任何更改。只有当您处于较低的隔离级别时,例如“读取已提交的”,才有可能看到数据行更改其值。

关于在使用 SQLAlchemy ORM 时控制隔离级别的信息,请参阅设置事务隔离级别 / DBAPI AUTOCOMMIT。

要极大地简化事情,Session 本身是在完全隔离的事务中运行的,并且不会覆盖任何已经读取的映射属性,除非你告诉它这样做。在进行中的事务中尝试重新读取已经加载的数据的用例是一个不常见的用例,在许多情况下没有效果,因此这被认为是例外而不是规范;为了在这个例外中工作,提供了几种方法,允许在进行中的事务的上下文中重新加载特定的数据。

当我们谈论Session时,理解我们所说的“事务”是什么意思,你的Session只能在事务内工作。关于此的概述请参阅管理事务。

一旦我们确定了我们的隔离级别,并且我们认为我们的隔离级别设置得足够低,以至于如果我们重新选择一行,我们应该在我们的Session中看到新数据,那我们如何看到它呢?

三种方式,从最常见到最不常见:

  1. 我们只需结束当前事务,并在下一次访问时通过调用Session.commit()(请注意,如果Session处于较少使用的“自动提交”模式,则还将调用Session.begin())。绝大多数应用程序和用例不会出现无法在其他事务中“看到”数据的问题,因为它们遵循这种模式,这是短事务最佳实践的核心。有关此问题的一些想法,请参阅我何时构造一个会话,何时提交它,何时关闭它?。

  2. 我们告诉我们的Session重新读取已经读取的行,要么在下次查询它们时使用Session.expire_all()Session.expire(),要么立即在对象上使用refresh。有关此操作的详细信息,请参阅刷新/过期。

  3. 我们可以在设置了“填充现有”选项的情况下运行整个查询,以确保在读取行时覆盖已加载的对象。这是一种在填充现有中描述的执行选项。

但请记住,如果我们的隔离级别是可重复读或更高级别,ORM 无法看到行中的更改,除非我们启动一个新的事务

“此会话的事务由于刷新期间的先前异常已被回滚。”(或类似内容)

Session.flush()引发异常,回滚事务,但在未显式调用Session.rollback()Session.close()的情况下调用Session上的进一步命令时,就会发生这种错误。

这通常对应于一个应用程序在Session.flush()Session.commit()上捕获异常,但未正确处理异常。 例如:

from sqlalchemy import create_engine, Column, Integer
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base(create_engine("sqlite://"))

class Foo(Base):
    __tablename__ = "foo"
    id = Column(Integer, primary_key=True)

Base.metadata.create_all()

session = sessionmaker()()

# constraint violation
session.add_all([Foo(id=1), Foo(id=1)])

try:
    session.commit()
except:
    # ignore error
    pass

# continue using session without rolling back
session.commit()

使用Session应该符合类似于这样的结构:

try:
    # <use session>
    session.commit()
except:
    session.rollback()
    raise
finally:
    session.close()  # optional, depends on use case

许多事情除了刷新之外,都可能导致 try/except 中的失败。 应用程序应确保对 ORM 导向的进程应用某种“框架”系统,以便连接和事务资源具有明确定界,并且如果发生任何失败条件,则可以显式回滚事务。

这并不意味着整个应用程序中应该到处都是 try/except 块,这不是可扩展的架构。 相反,一个典型的方法是,当首次调用 ORM 导向的方法和函数时,从最顶层调用函数的进程将在成功完成一系列操作时提交事务,并且如果操作因任何原因失败,包括失败的刷新,则回滚事务。 还有使用函数装饰器或上下文管理器来实现类似结果的方法。 采取的方法取决于正在编写的应用程序的类型。

有关如何组织使用Session的详细讨论,请参见何时构建会话,何时提交会话,何时关闭会话?。

但为什么 flush()坚持发出 ROLLBACK?

如果 Session.flush() 能部分完成然后不回滚,那将会很好,但是由于它当前的能力限制,这是不可能的,因为它的内部记录必须被修改,以便随时停止,并且与已刷新到数据库的内容完全一致。虽然这在理论上是可能的,但增强功能的有用性大大降低了,因为许多数据库操作在任何情况下都需要回滚。特别是 Postgres 有一些操作,一旦失败,事务就不允许继续:

test=> create table foo(id integer primary key);
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "foo_pkey" for table "foo"
CREATE TABLE
test=> begin;
BEGIN
test=> insert into foo values(1);
INSERT 0 1
test=> commit;
COMMIT
test=> begin;
BEGIN
test=> insert into foo values(1);
ERROR:  duplicate key value violates unique constraint "foo_pkey"
test=> insert into foo values(2);
ERROR:  current transaction is aborted, commands ignored until end of transaction block

SQLAlchemy 提供的解决这两个问题的方法是通过支持 SAVEPOINT,通过 Session.begin_nested()。使用 Session.begin_nested(),您可以在事务中设置一个可能会失败的操作,然后在保持封闭事务的同时“回滚”到其失败之前的点。

但为什么一次自动调用 ROLLBACK 不够?为什么我还必须再次 ROLLBACK?

由 flush() 引起的回滚并不是完整事务块的结束;尽管它结束了正在进行的数据库事务,但从 Session 的角度来看,仍然存在一个处于非活动状态的事务。

鉴于这样的代码块:

sess = Session()  # begins a logical transaction
try:
    sess.flush()

    sess.commit()
except:
    sess.rollback()

在上面的例子中,当一个 Session 第一次被创建时,假设没有使用“自动提交模式”,则在 Session 内建立了一个逻辑事务。这个事务是“逻辑”的,因为它实际上并不使用任何数据库资源,直到调用 SQL 语句时,此时会启动一个连接级别和 DBAPI 级别的事务。然而,无论数据库级别的事务是否是其状态的一部分,逻辑事务都会保持不变,直到使用 Session.commit()Session.rollback()Session.close() 结束它。

当上面的flush()失败时,代码仍位于由 try/commit/except/rollback 块框定的事务内。 如果flush()完全回滚逻辑事务,那么当我们到达except:块时,Session将处于干净状态,准备在全新的事务上发出新的 SQL,并且对Session.rollback()的调用将处于不正确的顺序。 特别是,到这一点为止,Session已经开始了一个新的事务,而Session.rollback()将错误地对其进行操作。 与其允许 SQL 操作在此处继续新事务,而正常用法规定要进行回滚的地方,则Session拒绝继续,直到显式回滚实际发生。

换句话说,预期调用代码将始终调用Session.commit()Session.rollback()Session.close()与当前事务块对应。 flush()保持Session在此事务块中,以便上述代码的行为可预测且一致。

但为什么flush()坚持要发出一个 ROLLBACK 呢?

如果Session.flush()可以部分完成然后不回滚,那将是很好的,但是由于其当前能力范围之外,因为其内部记账必须被修改,以便它可以随时停止,并且与已经刷新到数据库的内容完全一致。 尽管理论上可能,但增强功能的实用性大大降低了,因为许多数据库操作无论如何都要求回滚。 特别是,Postgres 有一些操作,一旦失败,就不允许事务继续:

test=> create table foo(id integer primary key);
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "foo_pkey" for table "foo"
CREATE TABLE
test=> begin;
BEGIN
test=> insert into foo values(1);
INSERT 0 1
test=> commit;
COMMIT
test=> begin;
BEGIN
test=> insert into foo values(1);
ERROR:  duplicate key value violates unique constraint "foo_pkey"
test=> insert into foo values(2);
ERROR:  current transaction is aborted, commands ignored until end of transaction block

SQLAlchemy 提供解决这两个问题的方法是通过 Session.begin_nested() 支持 SAVEPOINT。使用 Session.begin_nested(),您可以在事务中执行一个可能会失败的操作,然后在维持封闭事务的同时“回滚”到失败之前的状态。

但为什么一次自动调用 ROLLBACK 不够?为什么我必须再次 ROLLBACK?

由 flush() 引起的回滚不是完整事务块的结束;虽然它结束了正在进行的数据库事务,在Session的视角下仍然存在一个现在处于不活动状态的事务。

给定一个如下的块:

sess = Session()  # begins a logical transaction
try:
    sess.flush()

    sess.commit()
except:
    sess.rollback()

在上述情况中,当首次创建一个Session时,假设没有使用“自动提交模式”,则在Session内建立了一个逻辑事务。该事务是“逻辑”的,因为它实际上不使用任何数据库资源,直到调用 SQL 语句,此时开始连接级和 DBAPI 级的事务。但是,无论数据库级事务是否是其状态的一部分,逻辑事务将保持不变,直到使用Session.commit()Session.rollback()Session.close()结束为止。

当上面的flush()失败时,代码仍然处于由 try/commit/except/rollback 块框定的事务中。如果flush()完全回滚了逻辑事务,这意味着当我们到达except:块时,Session将处于干净的状态,准备在一个全新的事务中发出新的 SQL,并且对Session.rollback()的调用将会处于顺序错误的状态。特别是,Session此时已经开始了一个新的事务,而Session.rollback()将在错误地执行。与其在这个地方允许 SQL 操作在新的事务中进行,而正常使用指示将要进行回滚的地方,则Session拒绝继续,直到显式回滚实际发生为止。

换句话说,期望调用代码始终调用Session.commit()Session.rollback()Session.close()与当前事务块相对应。flush()保持Session在这个事务块内,以便上述代码的行为是可预测且一致的。

如何创建一个始终向每个查询添加特定过滤器的查询?

参见FilteredQuery中的配方。

我的查询返回的对象数量与 query.count() 告诉我的数量不一样 - 为什么?

Query对象被要求返回一个 ORM 映射对象列表时,将根据主键对对象进行去重。也就是说,如果我们例如使用了在使用 ORM 声明形式定义表元数据中描述的User映射,并且我们有一个如下的 SQL 查询:

q = session.query(User).outerjoin(User.addresses).filter(User.name == "jack")

在上面的例子中,教程中使用的样例数据在addresses表中有两行,对应于名为'jack'users行,主键值为 5。如果我们对上述查询使用Query.count(),我们将得到答案2

>>> q.count()
2

然而,如果我们运行Query.all()或者迭代查询,我们会得到一个元素

>>> q.all()
[User(id=5, name='jack', ...)]

这是因为当Query对象返回完整实体时,它们会被去重。如果我们请求单个列返回,则不会发生这种情况:

>>> session.query(User.id, User.name).outerjoin(User.addresses).filter(
...     User.name == "jack"
... ).all()
[(5, 'jack'), (5, 'jack')]

Query会进行去重的两个主要原因有:

  • 允许连接式贪婪加载正常工作 - 连接式贪婪加载通过使用与相关表的连接查询行,然后将这些连接查询行路由到导航对象的集合中来工作。为了做到这一点,它必须获取重复了主导对象主键的行,以便每个子条目。这种模式可以继续到更进一步的子集合,以便为单个主导对象,如User(id=5),处理多行。去重允许我们按照查询时的方式接收对象,例如,所有User()对象其名称为'jack',对我们来说是一个对象,并且User.addresses集合被贪婪加载,就像在relationship()上使用lazy='joined'或通过joinedload()选项指示的那样。为了保持一致性,去重仍然适用于是否已建立连接加载,因为贪婪加载的核心理念是这些选项从不影响结果。

  • 消除关于身份映射的混淆 - 这显然是较不重要的原因。由于Session使用了一个身份映射,即使我们的 SQL 结果集有两行主键为 5 的记录,Session内也只有一个User(id=5)对象,必须以其身份唯一性进行维护,即其主键/类组合。如果查询User()对象,获取相同对象多次在列表中实际上没有太多意义。有序集合可能更能代表Query在返回完整对象时所寻求的内容。

Query 去重的问题仍然存在问题,主要原因是 Query.count() 方法不一致,当前状态是,在最近的发布中,联合急加载首先被“子查询急加载”策略所取代,更近期的是“选择 IN 急加载”策略,这两者通常更适用于集合急加载。随着这种演变的继续,SQLAlchemy 可能会改变 Query 的行为,这也可能涉及到新的 API,以更直接地控制这种行为,并且还可能改变联合急加载的行为,以创建更一致的使用模式。

我已经创建了一个针对 Outer Join 的映射,虽然查询返回了行,但没有返回对象。为什么?

外部连接返回的行可能会对主键的某部分包含 NULL,因为主键是两个表的组合。Query 对象忽略那些没有可接受主键的传入行。根据 Mapperallow_partial_pks 标志的设置,如果值至少有一个非 NULL 值,则接受主键,或者如果值没有 NULL 值,则接受主键。请参阅 Mapper 上的 allow_partial_pks

当我尝试添加 WHERE、ORDER BY、LIMIT 等条件(这依赖于(外部)JOIN)时,我使用 joinedload()lazy=False 创建了一个 JOIN/OUTER JOIN,但 SQLAlchemy 在构造查询时出现了问题。

由联合急加载生成的连接仅用于完全加载相关集合,并且设计为不会影响查询的主要结果。由于它们是匿名别名,因此不能直接引用。

关于这种行为的详细信息,请参见 Joined Eager Loading 的禅意。

Query 没有 __len__(),为什么?

Python 中的 __len__() 魔法方法应用于对象,允许使用 len() 内置函数来确定集合的长度。很直观地,一个 SQL 查询对象会将 __len__() 关联到 Query.count() 方法,该方法会发出一个 SELECT COUNT。然而,不可能做到这一点的原因是因为将查询作为列表进行评估会导致两个 SQL 调用而不是一个:

class Iterates:
    def __len__(self):
        print("LEN!")
        return 5

    def __iter__(self):
        print("ITER!")
        return iter([1, 2, 3, 4, 5])

list(Iterates())

输出:

ITER!
LEN!

如何在 ORM 查询中使用 Textual SQL?

请参阅:

  • 从文本语句获取 ORM 结果 - 使用 Query 进行自定义文本块。

  • 使用 SQL 表达式与会话 - 直接使用文本 SQL 与 Session

我调用Session.delete(myobject)但它没有从父集合中删除!

有关此行为的描述,请参阅 关于删除的说明 - 从集合和标量关系引用的对象删除。

当我加载对象时,为什么我的__init__()没有被调用?

有关此行为的描述,请参阅 跨加载保持非映射状态。

我如何在 SA 的 ORM 中使用 ON DELETE CASCADE?

SQLAlchemy 总是针对当前加载在 Session 中的依赖行发出 UPDATE 或 DELETE 语句。对于未加载的行,默认情况下会发出 SELECT 语句来加载这些行,并对其进行更新/删除;换句话说,它假定未配置 ON DELETE CASCADE。要配置 SQLAlchemy 以配合 ON DELETE CASCADE,请参阅 使用 ORM 关系的外键 ON DELETE cascade。

我将我的实例的“foo_id”属性设置为“7”,但“foo”属性仍然为None - 它不应该加载 ID 为#7 的 Foo 吗?

ORM 并非以支持从外键属性更改驱动的关系的即时填充方式构建的 - 相反,它设计为以相反的方式工作 - 外键属性由 ORM 在幕后处理,最终用户自然设置对象关系。因此,设置o.foo的推荐方法就是这样 - 设置它!:

foo = session.get(Foo, 7)
o.foo = foo
Session.commit()

当然,操作外键属性是完全合法的。但是,目前设置外键属性为新值不会触发其中涉及的 relationship() 的“过期”事件。这意味着对于以下序列:

o = session.scalars(select(SomeClass).limit(1)).first()

# assume the existing o.foo_id value is None;
# accessing o.foo will reconcile this as ``None``, but will effectively
# "load" the value of None
assert o.foo is None

# now set foo_id to something.  o.foo will not be immediately affected
o.foo_id = 7

当首次访问时,o.foo加载为其有效的数据库值None。设置o.foo_id = 7将使值“7”作为挂起更改,但尚未刷新 - 因此o.foo仍然为None

# attribute is already "loaded" as None, has not been
# reconciled with o.foo_id = 7 yet
assert o.foo is None

对于o.foo的加载,基于外键变异通常在提交后自然实现,这既刷新了新的外键值,也使所有状态失效:

session.commit()  # expires all attributes

foo_7 = session.get(Foo, 7)

# o.foo will lazyload again, this time getting the new object
assert o.foo is foo_7

一个更简单的操作是单独使属性过期 - 这可以针对任何 persistent 对象使用Session.expire():

o = session.scalars(select(SomeClass).limit(1)).first()
o.foo_id = 7
Session.expire(o, ["foo"])  # object must be persistent for this

foo_7 = session.get(Foo, 7)

assert o.foo is foo_7  # o.foo lazyloads on access

请注意,如果对象不是持久的但存在于Session中,则称为 pending。这意味着对象的行尚未 INSERT 到数据库中。对于这样的对象,设置foo_id在行被插入之前没有意义;否则还没有行:

new_obj = SomeClass()
new_obj.foo_id = 7

Session.add(new_obj)

# returns None but this is not a "lazyload", as the object is not
# persistent in the DB yet, and the None value is not part of the
# object's state
assert new_obj.foo is None

Session.flush()  # emits INSERT

assert new_obj.foo is foo_7  # now it loads

这个方案ExpireRelationshipOnFKChange提供了一个使用 SQLAlchemy 事件的示例,以便协调与多对一关系中的外键属性的设置。

如何遍历与给定对象相关的所有对象?

与之相关的其他对象的对象将与映射器之间设置的relationship()构造相对应。这段代码片段将迭代所有对象,纠正循环:

from sqlalchemy import inspect

def walk(obj):
    deque = [obj]

    seen = set()

    while deque:
        obj = deque.pop(0)
        if obj in seen:
            continue
        else:
            seen.add(obj)
            yield obj
        insp = inspect(obj)
        for relationship in insp.mapper.relationships:
            related = getattr(obj, relationship.key)
            if relationship.uselist:
                deque.extend(related)
            elif related is not None:
                deque.append(related)

函数可以如下所示演示:

Base = declarative_base()

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    bs = relationship("B", backref="a")

class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))
    c_id = Column(ForeignKey("c.id"))
    c = relationship("C", backref="bs")

class C(Base):
    __tablename__ = "c"
    id = Column(Integer, primary_key=True)

a1 = A(bs=[B(), B(c=C())])

for obj in walk(a1):
    print(obj)

输出:

<__main__.A object at 0x10303b190>
<__main__.B object at 0x103025210>
<__main__.B object at 0x10303b0d0>
<__main__.C object at 0x103025490>

有没有一种方法可以自动地只有唯一的关键词(或其他类型的对象),而不必查询关键词并获取包含该关键词的行的引用呢?

当人们阅读文档中的多对多示例时,他们会发现如果您创建相同的Keyword两次,它会在数据库中出现两次。这有点不方便。

这个UniqueObject方案是为了解决这个问题而创建的。

为什么 post_update 除了第一个 UPDATE 之外还会发出 UPDATE?

该特性,详细说明请参见指向自身的行/相互依赖行,会在特定关系绑定的外键发生更改时发出 UPDATE 语句,除了会针对目标行通常发出的 INSERT/UPDATE/DELETE 之外。虽然此 UPDATE 语句的主要目的是与该行的 INSERT 或 DELETE 配对,以便它可以在后设置或前取消外键引用,以打破与相互依赖的外键的循环,但目前它也被捆绑为第二个 UPDATE,当目标行本身被 UPDATE 时发出。在这种情况下,post_update 发出的 UPDATE 通常 是不必要的,并且通常会显得浪费。

然而,一些研究试图消除这种“UPDATE / UPDATE”行为的努力表明,不仅需要在 post_update 实现中进行重大更改,还需要在与 post_update 无关的领域进行一些变更,以使其生效,因为在某些情况下,非 post_update 方面的操作顺序需要被颠倒,这反过来可能会影响其他情况,比如正确处理引用主键值的 UPDATE(参见#1063以获取概念验证)。

答案是,“post_update”用于打破两个相互依赖的外键之间的循环,并且使得这种循环打破仅限于目标表的 INSERT/DELETE 意味着其他地方 UPDATE 语句的排序需要被放宽,导致其他边缘情况的破坏。

第三方集成问题

原文:docs.sqlalchemy.org/en/20/faq/thirdparty.html

  • 我遇到了与“numpy.int64”、“numpy.bool_”等相关的错误。

  • 预期为 WHERE/HAVING 角色的 SQL 表达式,实际得到了 True

我遇到了与“numpy.int64”、“numpy.bool_”等相关的错误。

numpy包具有其自己的数字数据类型,它们是从 Python 的数字类型扩展而来的,但是其中包含一些行为,在某些情况下使它们无法与 SQLAlchemy 的一些行为以及使用的底层 DBAPI 驱动程序的一些行为协调一致。

可能出现的两个错误是在诸如 psycopg2 这样的后端上出现ProgrammingError: can't adapt type 'numpy.int64',以及在最近版本的 SQLAlchemy 中可能会出现ArgumentError: SQL expression for WHERE/HAVING role expected, got True;在更早的版本中可能会是ArgumentError: SQL expression object expected, got object of type <class 'numpy.bool_'> instead

在第一种情况中,问题是由于 psycopg2 没有为int64数据类型提供适当的查找条目,因此它不能直接被查询接受。这可以通过以下代码进行说明:

import numpy

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    data = Column(Integer)

# .. later
session.add(A(data=numpy.int64(10)))
session.commit()

在后一种情况中,问题是由于numpy.int64数据类型重写了__eq__()方法并强制返回表达式的返回类型为numpy.Truenumpy.False,这破坏了 SQLAlchemy 的表达式语言行为,后者期望从 Python 的等式比较中返回ColumnElement表达式:

>>> import numpy
>>> from sqlalchemy import column, Integer
>>> print(column("x", Integer) == numpy.int64(10))  # works
x  =  :x_1
>>> print(numpy.int64(10) == column("x", Integer))  # breaks
False

这些错误都可以通过相同的方法解决,即需要将特殊的 numpy 数据类型替换为常规的 Python 值。例如,对于诸如numpy.int32numpy.int64之类的类型,应用 Python 的int()函数,对于numpy.float32应用 Python 的float()函数:

data = numpy.int64(10)

session.add(A(data=int(data)))

result = session.execute(select(A.data).where(int(data) == A.data))

session.commit()

预期为 WHERE/HAVING 角色的 SQL 表达式,实际得到了 True。

参见我遇到了与“numpy.int64”、“numpy.bool_”等相关的错误。

我遇到了与“numpy.int64”、“numpy.bool_”等相关的错误。

numpy包具有其自己的数字数据类型,它们是从 Python 的数字类型扩展而来的,但是其中包含一些行为,在某些情况下使它们无法与 SQLAlchemy 的一些行为以及使用的底层 DBAPI 驱动程序的一些行为协调一致。

可能出现的两个错误是在诸如 psycopg2 这样的后端上出现ProgrammingError: can't adapt type 'numpy.int64',以及在最近版本的 SQLAlchemy 中可能会出现ArgumentError: SQL expression for WHERE/HAVING role expected, got True;在更早的版本中可能会是ArgumentError: SQL expression object expected, got object of type <class 'numpy.bool_'> instead

在第一种情况下,问题是因为 psycopg2 没有适当的查找条目来处理 int64 数据类型,因此它不会直接被查询接受。这可以从以下基于代码的示例中说明:

import numpy

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    data = Column(Integer)

# .. later
session.add(A(data=numpy.int64(10)))
session.commit()

在后一种情况下,问题是由于 numpy.int64 数据类型覆盖了 __eq__() 方法,并强制表达式的返回类型为 numpy.Truenumpy.False,这违反了 SQLAlchemy 表达式语言的行为,后者期望从 Python 相等比较中返回ColumnElement 表达式:

>>> import numpy
>>> from sqlalchemy import column, Integer
>>> print(column("x", Integer) == numpy.int64(10))  # works
x  =  :x_1
>>> print(numpy.int64(10) == column("x", Integer))  # breaks
False

这些错误都可以用同样的方法解决,即需要将特殊的 numpy 数据类型替换为常规的 Python 值。例如,对像 numpy.int32numpy.int64 这样的类型应用 Python 的 int() 函数,以及对 numpy.float32 应用 Python 的 float() 函数:

data = numpy.int64(10)

session.add(A(data=int(data)))

result = session.execute(select(A.data).where(int(data) == A.data))

session.commit()

期望 WHERE/HAVING 角色的 SQL 表达式,得到了 True

请参见 I’m getting errors related to “numpy.int64”, “numpy.bool_”, 等。

错误消息

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

本节列出了 SQLAlchemy 引发或发出的常见错误消息和警告的描述和背景。

SQLAlchemy 通常在 SQLAlchemy 特定的异常类的上下文中引发错误。有关这些类的详细信息,请参见核心异常和 ORM 异常。

SQLAlchemy 错误大致可分为两类,即编程时错误运行时错误。编程时错误是由于函数或方法使用不正确的参数而引发的,或者来自于无法解析的其他配置方法,例如无法解析的映射器配置。编程时错误通常是即时且确定的。另一方面,运行时错误表示程序运行时响应某些随机条件发生的失败,例如数据库连接耗尽或发生某些数据相关问题。运行时错误更可能出现在正在运行的应用程序的日志中,因为程序在遇到这些状态时会对负载和遇到的数据做出响应。

由于运行时错误不容易重现,并且通常发生在程序运行时对某些任意条件的响应中,它们更难以调试,也会影响到已经投入生产的程序。

在本节中,目标是尝试提供关于一些最常见的运行时错误以及编程时错误的背景信息。

连接和事务

队列池大小 超出 达到,连接超时,超时

这可能是最常见的运行时错误,直接涉及到应用程序的工作负载超过了一个配置的限制,这个限制通常适用于几乎所有的 SQLAlchemy 应用程序。

以下要点总结了此错误的含义,从大多数 SQLAlchemy 用户应该已经熟悉的最基本的要点开始。

  • SQLAlchemy 引擎对象默认使用一个连接池 - 这意味着当一个Engine对象使用一个 SQL 数据库连接资源,并且然后释放该资源时,数据库连接本身保持连接到数据库,并返回到一个内部队列,可以再次使用。即使代码似乎已经结束了与数据库的对话,在许多情况下,应用程序仍将保持一定数量的数据库连接,直到应用程序结束或池明确释放为止。

  • 由于池的存在,当应用程序使用 SQL 数据库连接时,通常是从使用Engine.connect()或使用 ORMSession进行查询时,此活动不一定会在获取连接对象时立即建立到数据库的新连接;它反而会向连接池查询连接,该连接池通常会从池中检索一个现有的连接以供重用。如果没有可用连接,则池将创建一个新的数据库连接,但仅当池未超过配置的容量时。

  • 在大多数情况下使用的默认池被称为QueuePool。当您请求此池提供连接并且没有可用连接时,它会创建一个新连接如果当前使用的连接总数小于配置的值。这个值等于池大小加上最大溢出。这意味着如果您已将引擎配置为:

    engine = create_engine("mysql+mysqldb://u:p@host/db", pool_size=10, max_overflow=20)
    

    上述Engine将允许最多 30 个连接在任何时候使用,不包括从引擎分离或失效的连接。如果一个新连接的请求到达,而应用程序的其他部分已经使用了 30 个连接,连接池将在固定时间内阻塞,然后超时并引发此错误消息。

    为了允许一次使用更多的连接,可以使用传递给create_engine()函数的create_engine.pool_sizecreate_engine.max_overflow参数来调整池。等待连接可用的超时时间通过create_engine.pool_timeout参数进行配置。

  • 通过将create_engine.max_overflow设置为值“-1”,可以配置池具有无限的溢出。使用此设置,池仍然会维护一组固定的连接,但如果没有可用连接,则绝对会创建一个新连接,而不会阻塞。

    然而,当以这种方式运行时,如果应用程序存在使用所有可用连接资源的问题,最终会达到数据库本身可用连接的配置限制,这将再次返回一个错误。更严重的是,当应用程序耗尽连接数据库的连接时,通常会在失败之前使用大量资源,并且还可能干扰依赖于能够连接到数据库的其他应用程序和数据库状态机制。

    鉴于上述情况,可以将连接池视为连接使用的安全阀,为防止恶意应用程序导致整个数据库对所有其他应用程序不可用提供了关键的保护层。在收到此错误消息时,最好修复使用过多连接的问题和/或适当配置限制,而不是允许无限溢出,因为这实际上并不能解决潜在的问题。

什么导致应用程序使用完所有可用的连接?

  • 应用程序正在处理基于池配置值的太多并发请求以执行工作 - 这是最直接的原因。如果您有一个在允许 30 个并发线程的线程池中运行的应用程序,并且每个线程使用一个连接,如果您的池未配置为允许至少同时检出 30 个连接,那么一旦您的应用程序接收到足够的并发请求,您将收到此错误。解决方案是提高池的限制或降低并发线程数。

  • 应用程序未将连接返回到池中 - 这是下一个最常见的原因,即应用程序正在使用连接池,但程序未能释放这些连接,而是将它们保持打开状态。连接池以及 ORM Session 确实具有逻辑,以便当会话和/或连接对象被垃圾收集时,会导致底层连接资源被释放,但是不能依赖此行为及时释放资源。

    造成这种情况的常见原因是应用程序使用 ORM 会话,但在完成涉及该会话的工作后未调用 Session.close()。解决方法是确保 ORM 会话(如果使用 ORM)或引擎绑定的Connection对象(如果使用 Core)在完成工作后明确关闭,可以通过适当的.close()方法或使用可用的上下文管理器之一(例如,“with:”语句)来正确释放资源。

  • 应用程序试图运行长时间事务 - 数据库事务是非常昂贵的资源,永远不应保持空闲以等待某个事件发生。如果应用程序正在等待用户按下按钮,或者等待长时间运行的作业队列中的结果,或者保持持久连接以向浏览器发送请求,不要在整个时间内保持数据库事务处于打开状态。当应用程序需要与数据库交互并与事件交互时,在该点打开一个短暂的事务,然后关闭它。

  • 应用程序发生死锁 - 也是此错误的常见原因,更难以理解,如果应用程序由于应用程序端或数据库端的死锁而无法完成对连接的使用,则应用程序可能会使用完所有可用连接,从而导致附加请求接收到此错误。造成死锁的原因包括:

    • 当使用隐式异步系统(如 gevent 或 eventlet)时,如果未正确地对所有套接字库和驱动程序进行猴子补丁,或者对所有猴子补丁驱动程序方法的覆盖不完全,或者在异步系统用于 CPU 绑定的工作负载并且使用数据库资源的 greenlets 等待时间过长时,可能会出现问题。通常情况下,隐式或显式的异步编程框架对于绝大多数关系型数据库操作来说通常不是必要的或合适的;如果应用程序必须在某些功能区域使用异步系统,则最好是数据库导向型业务方法在传统线程内运行,而将消息传递给应用程序的异步部分。

    • 数据库端的死锁,例如行相互死锁

    • 线程错误,例如互相死锁的互斥体,或者在同一线程中调用已锁定的互斥体

请记住,使用连接池的另一种选择是完全关闭连接池。有关此问题的背景,请参阅切换池实现一节。然而,要注意,当发生此错误消息时,这总是由于应用程序本身的问题更大;池只是帮助更早地揭示问题。

请参阅

连接池

与引擎和连接一起工作 ### Pool 类不能与 asyncio 引擎一起使用(反之亦然)

QueuePool池类在内部使用thread.Lock对象,与 asyncio 不兼容。如果使用create_async_engine()函数创建AsyncEngine,则适当的队列池类是AsyncAdaptedQueuePool,它会自动使用,无需指定。

除了AsyncAdaptedQueuePool之外,NullPoolStaticPool池类不使用锁,并且也适用于与异步引擎一起使用。

在极少数情况下,如果使用create_engine()函数明确指定AsyncAdaptedQueuePool池类,则也会引发此错误。

另请参阅

连接池 ### 在无效事务回滚之前无法重新连接。请在继续之前完全回滚()

此错误条件指的是Connection被使无效,无论是由于数据库断开连接检测还是由于显式调用Connection.invalidate(),但仍然存在一个事务,该事务是由Connection.begin()方法显式启动,或者由于连接在发出任何 SQL 语句时自动开始事务,如 SQLAlchemy 2.x 系列中发生的情况。当连接被使无效时,任何正在进行的Transaction现在处于无效状态,必须显式回滚以将其从Connection中移除。 ## DBAPI 错误

Python 数据库 API,或者 DBAPI,是一个数据库驱动程序的规范,可以在Pep-249找到。这个 API 指定了一组异常类,适应了数据库的所有故障模式。

SQLAlchemy 不直接生成这些异常。相反,它们被从数据库驱动程序拦截并由 SQLAlchemy 提供的异常 DBAPIError 包装,但异常中的消息 由驱动程序生成,而非 SQLAlchemy

InterfaceError

与数据库本身而非数据库接口相关的错误引发的异常。

此错误是 DBAPI 错误,源自于数据库驱动程序(DBAPI),而非 SQLAlchemy 本身。

InterfaceError 有时会由驱动程序在数据库连接被断开或无法连接到数据库的情况下引发。有关如何处理此问题的提示,请参阅 处理断开连接 部分。 ### DatabaseError

与数据库本身而非接口或传递的数据相关的错误引发的异常。

此错误是 DBAPI 错误,源自于数据库驱动程序(DBAPI),而非 SQLAlchemy 本身。 ### DataError

由于处理数据的问题而引发的错误,例如除以零、数值超出范围等。

此错误是 DBAPI 错误,源自于数据库驱动程序(DBAPI),而非 SQLAlchemy 本身。 ### OperationalError

数据库操作中出现的与程序员控制无关的错误引发的异常,例如出现意外断开连接、找不到数据源名称、无法处理事务、在处理过程中发生内存分配错误等。

此错误是 DBAPI 错误,源自于数据库驱动程序(DBAPI),而非 SQLAlchemy 本身。

在数据库连接被断开或无法连接到数据库的情况下,OperationalError 是驱动程序中最常见(但不是唯一)使用的错误类。有关如何处理此问题的提示,请参阅 处理断开连接 部分。 ### IntegrityError

数据库的关系完整性受到影响时引发的异常,例如外键检查失败。

此错误是 DBAPI 错误,源自于数据库驱动程序(DBAPI),而非 SQLAlchemy 本身。 ### InternalError

数据库遇到内部错误时引发的异常,例如游标不再有效、事务不同步等。

此错误是 DBAPI 错误,源自于数据库驱动程序(DBAPI),而非 SQLAlchemy 本身。

InternalError 有时会由驱动程序在数据库连接被断开或无法连接到数据库的情况下引发。有关如何处理此问题的提示,请参阅 处理断开连接 部分。 ### ProgrammingError

引发编程错误的异常,例如找不到表或已存在,SQL 语句中的语法错误,指定的参数数量错误等。

此错误是 DBAPI 错误,源自数据库驱动程序(DBAPI),而不是 SQLAlchemy 本身。

ProgrammingError有时由驱动程序引发,原因是数据库连接被断开,或者无法连接到数据库。有关如何处理此问题的提示,请参见处理断开连接部分。 ### NotSupportedError

当方法或数据库 API 使用数据库不支持的情况下引发异常,例如在不支持事务或已关闭事务的连接上请求.rollback()

此错误是 DBAPI 错误,源自数据库驱动程序(DBAPI),而不是 SQLAlchemy 本身。

SQL 表达语言

对象不会产生缓存键,性能影响

自 SQLAlchemy 版本 1.4 起,包括 SQL 编译缓存机制在内,将允许 Core 和 ORM SQL 结构缓存其字符串形式,以及用于从语句中提取结果的其他结构信息,从而在下次使用另一个结构等效构造时跳过相对昂贵的字符串编译过程。此系统依赖于为所有 SQL 构造实现的功能,包括对象,如 Columnselect()TypeEngine 对象,以生成完全代表其状态的缓存键,以影响 SQL 编译过程。

如果问题中的警告涉及到广泛使用的对象,例如 Column 对象,并且显示出影响大多数发出的 SQL 结构的情况(使用估计缓存性能使用日志中描述的估算技术),以至于缓存通常不会为应用程序启用,这将对性能产生负面影响,并且在某些情况下,与以前的 SQLAlchemy 版本相比,实际上可能会产生性能降低。为什么升级到 1.4 和/或 2.x 后我的应用程序变慢了? FAQ 对此进行了额外详细的介绍。

如果存在任何疑问,缓存会自行禁用

缓存依赖于能够生成准确表示语句完整结构的缓存键以一致的方式。如果特定的 SQL 结构(或类型)没有适当的指令,允许其生成正确的缓存键,则不能安全地启用缓存:

  • 缓存键必须表示完整的结构:如果两个单独的结构实例的使用可能导致渲染不同的 SQL,则使用不捕捉第一个和第二个元素之间不同之处的缓存键缓存该元素的 SQL 会导致为第二个实例缓存和渲染错误的 SQL。

  • 缓存键必须是一致的:如果某个结构代表的状态每次都会更改,比如文字值,为每个实例生成唯一的 SQL,那么这个结构也不适合缓存,因为重复使用该结构会很快填满语句缓存,其中包含可能不会再次使用的唯一 SQL 字符串,从而达不到缓存的目的。

出于上述两个原因,SQLAlchemy 的缓存系统对于决定是否缓存与对象对应的 SQL 非常谨慎

缓存的断言属性

基于以下标准发出警告。有关每个标准的详细信息,请参阅 为什么在升级到 1.4 和/或 2.x 后我的应用程序变慢了? 部分。

  • Dialect 本身(即由我们传递给 create_engine() 的 URL 的第一部分指定的模块,如 postgresql+psycopg2://)必须指示已经审查并测试以正确支持缓存,这由 Dialect.supports_statement_cache 属性设置为 True 来表示。在使用第三方方言时,请与方言的维护者协商,以便他们可以遵循 确保可以启用缓存的步骤 并发布新版本。

  • 第三方或用户定义的类型,其继承自TypeDecoratorUserDefinedType必须在其定义中包含ExternalType.cache_ok属性,包括所有派生的子类,遵循ExternalType.cache_ok的文档字符串中描述的指南。如前所述,如果这些数据类型是从第三方库导入的,请与该库的维护者联系,以便他们提供必要的更改并发布新版本。

  • 第三方或用户定义的 SQL 构造,它们从诸如ClauseElementColumnInsert 等类继承,包括简单的子类以及设计用于与自定义 SQL 构造和编译扩展一起使用的构造,通常应包括HasCacheKey.inherit_cache 属性设置为 TrueFalse,根据构造的设计而定,遵循启用自定义构造的缓存支持中描述的指南。

参见

使用日志估算缓存性能 - 关于观察缓存行为和效率的背景信息

升级到 1.4 和/或 2.x 后,为什么我的应用变慢了? - 在常见问题解答部分 ### Compiler StrSQLCompiler 无法呈现 <element type> 类型的元素

当尝试对包含不是默认编译的元素的 SQL 表达式构造进行字符串化时,通常会发生此错误;在这种情况下,错误将针对StrSQLCompiler类。在较少见的情况下,当使用错误类型的 SQL 表达式与特定类型的数据库后端时,也可能发生这种情况;在这些情况下,将命名其他类型的 SQL 编译器类,例如 SQLCompilersqlalchemy.dialects.postgresql.PGCompiler。下面的指南更具体地针对“字符串化”用例,但也描述了一般背景。

通常,核心 SQL 结构或 ORM Query 对象可以直接字符串化,例如我们使用 print()

>>> from sqlalchemy import column
>>> print(column("x") == 5)
x  =  :x_1 

当上述 SQL 表达式被字符串化时,会使用StrSQLCompiler 编译器类,这是一个特殊的语句编译器,当一个结构被字符串化而没有任何特定于方言的信息时会被调用。

然而,有许多结构是特定于某种特定类型的数据库方言的,对于这些结构,StrSQLCompiler 并不知道如何转换成字符串,例如 PostgreSQL 的“插入冲突” 结构:

>>> from sqlalchemy.dialects.postgresql import insert
>>> from sqlalchemy import table, column
>>> my_table = table("my_table", column("x"), column("y"))
>>> insert_stmt = insert(my_table).values(x="foo")
>>> insert_stmt = insert_stmt.on_conflict_do_nothing(index_elements=["y"])
>>> print(insert_stmt)
Traceback (most recent call last):

...

sqlalchemy.exc.UnsupportedCompilationError:
Compiler <sqlalchemy.sql.compiler.StrSQLCompiler object at 0x7f04fc17e320>
can't render element of type
<class 'sqlalchemy.dialects.postgresql.dml.OnConflictDoNothing'>

为了字符串化特定于特定后端的结构,必须使用 ClauseElement.compile() 方法,传递一个 Engine 或一个 Dialect 对象,这将调用正确的编译器。 下面我们使用 PostgreSQL 方言:

>>> from sqlalchemy.dialects import postgresql
>>> print(insert_stmt.compile(dialect=postgresql.dialect()))
INSERT  INTO  my_table  (x)  VALUES  (%(x)s)  ON  CONFLICT  (y)  DO  NOTHING 

对于 ORM Query 对象,可以使用 Query.statement 访问器访问语句:

statement = query.statement
print(statement.compile(dialect=postgresql.dialect()))

请查看下面的常见问题解答链接,了解有关直接字符串化/编译 SQL 元素的额外细节。

另请参阅

如何将 SQL 表达式渲染为字符串,可能包含内联的绑定参数?

TypeError: 不支持在 ‘ColumnProperty’ 和 实例之间的操作

这经常发生在尝试在 SQL 表达式的上下文中使用column_property()deferred() 对象时,通常在声明性语句中,例如:

class Bar(Base):
    __tablename__ = "bar"

    id = Column(Integer, primary_key=True)
    cprop = deferred(Column(Integer))

    __table_args__ = (CheckConstraint(cprop > 5),)

在上面的例子中,在映射之前内联使用了 cprop 属性,但是这个 cprop 属性不是一个Column,而是一个ColumnProperty,这是一个临时对象,因此不具备 Column 对象或 InstrumentedAttribute 对象的全部功能,后者将在声明过程完成后映射到 Bar 类上。

虽然 ColumnProperty 确实有一个 __clause_element__() 方法,允许它在某些基于列的上下文中工作,但是它不能在上述开放式比较上下文中工作,因为它没有 Python __eq__() 方法,该方法将允许它将对数字 “5” 的比较解释为 SQL 表达式而不是常规的 Python 比较。

解决方法是直接访问 Column,使用 ColumnProperty.expression 属性:

class Bar(Base):
    __tablename__ = "bar"

    id = Column(Integer, primary_key=True)
    cprop = deferred(Column(Integer))

    __table_args__ = (CheckConstraint(cprop.expression > 5),)

绑定参数 (在参数组 中)需要一个值。

当语句使用 bindparam() 而在执行语句时未提供值时,就会发生此错误:

stmt = select(table.c.column).where(table.c.id == bindparam("my_param"))

result = conn.execute(stmt)

在上面,未提供参数 “my_param” 的值。正确的方法是提供一个值:

result = conn.execute(stmt, {"my_param": 12})

当消息采用“需要参数组 中的绑定参数 的值”形式时,消息是指向 “executemany” 执行方式。在这种情况下,语句通常是 INSERT、UPDATE 或 DELETE,并传递了参数列表。在这种格式中,语句可以动态生成,以包括参数列表中的每个参数的参数位置,其中它将使用 第一组参数 来确定这些参数应该是什么。

例如,下面的语句是基于第一个参数集设置为需要参数 “a”、“b” 和 “c” 而计算的 - 这些名称确定了语句的最终字符串格式,该格式将用于列表中的每组参数。由于第二个条目不包含 “b”,因此会生成此错误:

m = MetaData()
t = Table("t", m, Column("a", Integer), Column("b", Integer), Column("c", Integer))

e.execute(
    t.insert(),
    [
        {"a": 1, "b": 2, "c": 3},
        {"a": 2, "c": 4},
        {"a": 3, "b": 4, "c": 5},
    ],
)
sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError)
A value is required for bind parameter 'b', in parameter group 1
[SQL: u'INSERT INTO t (a, b, c) VALUES (?, ?, ?)']
[parameters: [{'a': 1, 'c': 3, 'b': 2}, {'a': 2, 'c': 4}, {'a': 3, 'c': 5, 'b': 4}]]

由于需要 “b”,因此将其传递为 None,以便 INSERT 可以继续进行:

e.execute(
    t.insert(),
    [
        {"a": 1, "b": 2, "c": 3},
        {"a": 2, "b": None, "c": 4},
        {"a": 3, "b": 4, "c": 5},
    ],
)

另请参阅

发送参数 ### 预期的 FROM 子句,却收到了 Select。要创建 FROM 子句,请使用 .subquery() 方法。

这指的是 SQLAlchemy 1.4 中的一个更改,即由select()等函数生成的 SELECT 语句,但也包括联合和文本 SELECT 表达式等,不再被视为FromClause对象,不能直接放在另一个 SELECT 语句的 FROM 子句中,而必须首先将它们包装在Subquery中。这是 Core 中的一个重大概念变化,完整的原因讨论在不再将 SELECT 语句隐式视为 FROM 子句中。

给出一个示例如下:

m = MetaData()
t = Table("t", m, Column("a", Integer), Column("b", Integer), Column("c", Integer))
stmt = select(t)

在上面,stmt代表一个 SELECT 语句。当我们想直接将stmt作为另一个 SELECT 语句中的 FROM 子句使用时,就会产生错误,比如如果我们尝试从中选择:

new_stmt_1 = select(stmt)

或者如果我们想在 FROM 子句中使用它,比如在 JOIN 中:

new_stmt_2 = select(some_table).select_from(some_table.join(stmt))

在 SQLAlchemy 的早期版本中,在另一个 SELECT 语句中使用 SELECT 会产生一个带括号的无名称子查询。在大多数情况下,这种 SQL 形式并不是很有用,因为像 MySQL 和 PostgreSQL 这样的数据库要求 FROM 子句中的子查询具有命名别名,这意味着使用SelectBase.alias()方法或者从 1.4 版本开始使用SelectBase.subquery()方法来生成这个别名。在其他数据库中,为子查询命名仍然更清晰,以解决子查询内部列名的任何歧义。

除了上述实际原因外,还有许多其他与 SQLAlchemy 相关的原因导致进行了更改。因此,上述两个语句的正确形式要求使用SelectBase.subquery()

subq = stmt.subquery()

new_stmt_1 = select(subq)

new_stmt_2 = select(some_table).select_from(some_table.join(subq))

另请参阅

不再将 SELECT 语句隐式视为 FROM 子句 ### 为原始 clauseelement 自动生成别名

从版本 1.4.26 开始新增。

此废弃警告指的是一个非常古老且可能不太熟知的模式,适用于旧版 Query.join() 方法以及 2.0 风格 Select.join() 方法,其中可以根据 relationship() 来说明连接,但是目标是映射到的 Table 或其他 Core 可选择对象,而不是 ORM 实体,如映射的类或aliased()构造:

a1 = Address.__table__

q = (
    s.query(User)
    .join(a1, User.addresses)
    .filter(Address.email_address == "ed@foo.com")
    .all()
)

上述模式还允许使用任意可选择的对象,例如 Core JoinAlias 对象,但是这个元素没有自动适应,这意味着必须直接引用 Core 元素:

a1 = Address.__table__.alias()

q = (
    s.query(User)
    .join(a1, User.addresses)
    .filter(a1.c.email_address == "ed@foo.com")
    .all()
)

指定连接目标的正确方式始终是使用映射的类本身或一个aliased对象,在后一种情况下,使用 PropComparator.of_type()修饰符设置别名:

# normal join to relationship entity
q = s.query(User).join(User.addresses).filter(Address.email_address == "ed@foo.com")

# name Address target explicitly, not necessary but legal
q = (
    s.query(User)
    .join(Address, User.addresses)
    .filter(Address.email_address == "ed@foo.com")
)

加入到一个别名:

from sqlalchemy.orm import aliased

a1 = aliased(Address)

# of_type() form; recommended
q = (
    s.query(User)
    .join(User.addresses.of_type(a1))
    .filter(a1.email_address == "ed@foo.com")
)

# target, onclause form
q = s.query(User).join(a1, User.addresses).filter(a1.email_address == "ed@foo.com")
```  ### 由于重叠的表而自动生成别名

自版本 1.4.26 新增。

当使用涉及加入表继承的映射进行查询时,通常会生成此警告。问题在于,在两个具有共同基表的加入继承模型之间进行连接时,不能形成适当的 SQL JOIN 而不对其中一侧应用别名;SQLAlchemy 将别名应用于连接的右侧。例如,给定一个加入继承映射如下:

```py
class Employee(Base):
    __tablename__ = "employee"
    id = Column(Integer, primary_key=True)
    manager_id = Column(ForeignKey("manager.id"))
    name = Column(String(50))
    type = Column(String(50))

    reports_to = relationship("Manager", foreign_keys=manager_id)

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": type,
    }

class Manager(Employee):
    __tablename__ = "manager"
    id = Column(Integer, ForeignKey("employee.id"), primary_key=True)

    __mapper_args__ = {
        "polymorphic_identity": "manager",
        "inherit_condition": id == Employee.id,
    }

上述映射包括EmployeeManager类之间的关系。由于这两个类都使用了“employee”数据库表,从 SQL 的角度来看,这是一种自引用关系。如果我们想要使用连接从EmployeeManager模型中查询,那么在 SQL 层面上,“employee”表需要在查询中包含两次,这意味着它必须被别名化。当我们使用 SQLAlchemy ORM 创建这样一个连接时,得到的 SQL 如下所示:

>>> stmt = select(Employee, Manager).join(Employee.reports_to)
>>> print(stmt)
SELECT  employee.id,  employee.manager_id,  employee.name,
employee.type,  manager_1.id  AS  id_1,  employee_1.id  AS  id_2,
employee_1.manager_id  AS  manager_id_1,  employee_1.name  AS  name_1,
employee_1.type  AS  type_1
FROM  employee  JOIN
(employee  AS  employee_1  JOIN  manager  AS  manager_1  ON  manager_1.id  =  employee_1.id)
ON  manager_1.id  =  employee.manager_id 

在上面的 SQL 语句中,选择了employee表,代表了查询中的Employee实体。然后连接到employee AS employee_1 JOIN manager AS manager_1的右嵌套连接,其中employee表再次出现,但作为一个匿名别名employee_1。这就是警告消息所指的“自动生成别名”。

当 SQLAlchemy 加载包含EmployeeManager对象的 ORM 行时,ORM 必须将来自上述employee_1manager_1表别名的行适应为未别名化的Manager类的行。这个过程在内部是复杂的,并且不支持所有 API 特性,特别是当尝试在比这里展示的更深度嵌套的查询中使用contains_eager()等急加载特性时。由于这种模式对于更复杂的情况不可靠,并涉及难以预测和遵循的隐式决策,因此会发出警告,并且这种模式可能被视为传统特性。编写此查询的更好方式是使用适用于任何其他自引用关系的相同模式,即显式使用aliased()构造。对于连接继承和其他基于连接的映射,通常希望添加使用aliased.flat参数,这将允许通过将别名应用于连接中的各个表来对两个或更多表进行连接别名化,而不是将连接嵌入到新的子查询中:

>>> from sqlalchemy.orm import aliased
>>> manager_alias = aliased(Manager, flat=True)
>>> stmt = select(Employee, manager_alias).join(Employee.reports_to.of_type(manager_alias))
>>> print(stmt)
SELECT  employee.id,  employee.manager_id,  employee.name,
employee.type,  manager_1.id  AS  id_1,  employee_1.id  AS  id_2,
employee_1.manager_id  AS  manager_id_1,  employee_1.name  AS  name_1,
employee_1.type  AS  type_1
FROM  employee  JOIN
(employee  AS  employee_1  JOIN  manager  AS  manager_1  ON  manager_1.id  =  employee_1.id)
ON  manager_1.id  =  employee.manager_id 

如果我们想要使用contains_eager()来填充reports_to属性,我们将引用别名:

>>> stmt = (
...     select(Employee)
...     .join(Employee.reports_to.of_type(manager_alias))
...     .options(contains_eager(Employee.reports_to.of_type(manager_alias)))
... )

在某些更嵌套的情况下,如果不使用显式的aliased()对象,contains_eager()选项可能无法获得足够的上下文来确定从哪里获取数据,特别是在 ORM 在非常嵌套的上下文中“自动别名”时。因此,最好不要依赖这个特性,而是尽可能将 SQL 构造明确化。

对象关系映射

IllegalStateChangeError 和并发异常

SQLAlchemy 2.0 引入了一个新系统,详见会话在检测到非法并发或重入访问时主动引发,该系统主动检测在单个 Session 对象的实例以及其扩展的 AsyncSession 代理对象上调用并发方法。这些并发访问调用通常会发生在单个 Session 实例在多个并发线程之间共享而没有进行同步访问时,或者类似地,当单个 AsyncSession 实例在多个并发任务之间共享时(例如使用 asyncio.gather() 这样的函数)。这些使用模式不是这些对象的适当用法,在没有 SQLAlchemy 实现的主动警告系统的情况下,仍然会在对象内部产生无效状态,从而产生难以调试的错误,包括数据库连接本身的驱动程序级错误。

SessionAsyncSession 的实例都是可变、有状态的对象,没有内置的方法调用同步,并且代表着一次单一的数据库事务,该事务在一次特定的 EngineAsyncEngine 绑定的数据库连接上进行(请注意,这些对象都支持同时绑定到多个引擎,但在这种情况下,在事务范围内仍然只会有一个连接与引擎相关)。单个数据库事务不是并发 SQL 命令的适当目标;相反,运行并发数据库操作的应用程序应该使用并发事务。因此,对于这些对象,适当的模式是每个线程一个 Session 或每个任务一个 AsyncSession

有关并发的更多背景信息,请参阅会话是否线程安全?AsyncSession 是否可以在并发任务中共享?一节。 ### 父实例 未绑定到会话;(延迟加载/延迟加载/刷新等)操作无法继续

这很可能是处理 ORM 时最常见的错误消息,并且它是由 ORM 广泛使用的一种技术的性质引起的,这种技术称为延迟加载。延迟加载是一种常见的对象关系模式,其中由 ORM 持久化的对象维护了与数据库本身的代理,以便当访问对象上的各种属性时,可以延迟从数据库中检索其值。这种方法的优点是可以从数据库中检索对象而不必一次加载其所有属性或相关数据,而只能在那时提供所请求的数据。其主要缺点基本上是优点的镜像,即如果正在加载大量对象,这些对象在所有情况下都需要某一组数据,则逐步加载该额外数据是一种浪费。

对于懒加载的另一个警告,除了通常的效率问题之外,还有一个要注意的是,为了进行懒加载,对象必须保持与会话相关联,以便能够检索其状态。这个错误消息意味着一个对象已经与其Session解除关联,并且被要求从数据库中懒加载数据。

对象变为分离状态的最常见原因是会话本身已关闭,通常是通过Session.close()方法关闭的。然后,对象将继续存在以供进一步访问,这在 Web 应用程序中非常常见,其中它们被传递到服务器端模板引擎,并被要求加载更多属性。

减轻这个错误的方法是通过以下技术:

  • 尽量不要有分离的对象;不要过早关闭会话 - 通常,应用程序会在将相关对象传递给其他系统之前关闭事务,然后由于此错误而失败。有时,事务不需要那么快关闭;一个例子是 Web 应用在渲染视图之前关闭了事务。这通常是以“正确性”的名义而做的,但可能被视为“封装”的误用,因为此术语指的是代码组织,而不是实际操作。使用 ORM 对象的模板正在使用代理模式,它将数据库逻辑封装在调用者之外。如果Session可以保持打开状态直到对象的生命周期结束,那么这是最佳方法。

  • 否则,将需要的所有内容一次性加载 - 通常不可能保持事务处于打开状态,特别是在需要将对象传递给其他无法在同一上下文中运行的系统的更复杂的应用程序中。在这种情况下,应用程序应准备处理分离对象,并应尽量恰当地使用急加载来确保对象一开始就拥有所需的内容。

  • 并且重要的是,将 expire_on_commit 设置为 False - 当使用分离的对象时,对象需要重新加载数据的最常见原因是因为它们在上次调用 Session.commit() 时过期了。当处理分离的对象时,不应使用此过期;因此,Session.expire_on_commit 参数应设置为 False。通过防止对象在事务外过期,加载的数据将保持存在,并且在访问该数据时不会产生额外的延迟加载。

    Session.rollback() 方法无条件地使 Session 中的所有内容过期,并且在非错误情况下也应避免使用。

    另请参阅

    关系加载技术 - 关于急加载和其他基于关系的加载技术的详细文档

    提交 - 有关会话提交的背景

    刷新 / 过期 - 属性过期的背景 ### 此 Session 的事务由于在 flush 过程中出现先前的异常而被回滚

Session 的 flush 过程,在遇到错误时会回滚数据库事务,以保持内部一致性。但是,一旦发生这种情况,会话的事务现在处于“不活动”状态,必须由调用方显式地回滚,就像如果没有发生失败,则必须显式地提交一样。

当使用 ORM 时,这是一个常见错误,通常适用于尚未正确围绕其Session操作进行“框架化”的应用程序。更多详细信息请参阅 FAQ 中的“由于刷新期间的先前异常,此会话的事务已被回滚。”(或类似)。### 对于关系,delete-orphan 级联通常仅在一对多关系的“一”侧上配置,而不在多对一或多对多关系的“多”侧上配置。

当在多对一或多对多关系上设置“delete-orphan”级联时,就会出现这个错误,例如:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

    bs = relationship("B", back_populates="a")

class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

    # this will emit the error message when the mapper
    # configuration step occurs
    a = relationship("A", back_populates="bs", cascade="all, delete-orphan")

configure_mappers()

上面,对B.a上的“delete-orphan”设置表示的意图是,当引用特定A的每个B对象被删除时,该A也应该被删除。也就是说,它表达了被删除的“孤儿”将是一个A对象,并且当引用它的每个B被删除时,它就成为一个“孤儿”。

“delete-orphan”级联模型不支持这一功能。“孤儿”考虑仅在删除一个对象时进行,然后该对象将引用零个或多个现在由此单个删除“孤儿化”的对象,这将导致这些对象也被删除。换句话说,它仅设计用于跟踪基于删除一个且仅一个“父”对象每个孤儿的创建,这是一对多关系中的自然情况,其中在“一”侧的对象的删除导致“多”侧的相关项目随后被删除。

为了支持这一功能,上述映射将级联设置放在一对多的一侧,看起来像是:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

    bs = relationship("B", back_populates="a", cascade="all, delete-orphan")

class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

    a = relationship("A", back_populates="bs")

其中表达的意图是,当删除一个A时,它所引用的所有B对象也被删除。

然后错误消息继续建议使用relationship.single_parent标志。该标志可用于强制执行一个关系,该关系能够让许多对象引用特定对象,实际上每次只会有一个对象引用它。它用于传统或其他不太理想的数据库模式,其中外键关系暗示“多”集合,但实际上只有一个对象会引用给定目标对象。这种不常见的情况可以通过上面的示例来演示:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

    bs = relationship("B", back_populates="a")

class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

    a = relationship(
        "A",
        back_populates="bs",
        single_parent=True,
        cascade="all, delete-orphan",
    )

上述配置将安装一个验证器,该验证器将强制执行在B.a关系的范围内,只能有一个B与一个A关联:

>>> b1 = B()
>>> b2 = B()
>>> a1 = A()
>>> b1.a = a1
>>> b2.a = a1
sqlalchemy.exc.InvalidRequestError: Instance <A at 0x7eff44359350> is
already associated with an instance of <class '__main__.B'> via its
B.a attribute, and is only allowed a single parent.

请注意,此验证器的范围有限,并且不会阻止通过其他方向创建多个“父对象”。例如,它不会检测到关于A.bs的相同设置:

>>> a1.bs = [b1, b2]
>>> session.add_all([a1, b1, b2])
>>> session.commit()
INSERT  INTO  a  DEFAULT  VALUES
()
INSERT  INTO  b  (a_id)  VALUES  (?)
(1,)
INSERT  INTO  b  (a_id)  VALUES  (?)
(1,) 

然而,事情不会按预期进行,因为“delete-orphan”级联将继续按照单个主导对象的术语工作,这意味着如果我们删除B对象中的任意一个A就会被删除。另一个B还会留下,ORM 通常足够智能以将外键属性设置为 NULL,但这通常不是预期的结果:

>>> session.delete(b1)
>>> session.commit()
UPDATE  b  SET  a_id=?  WHERE  b.id  =  ?
(None,  2)
DELETE  FROM  b  WHERE  b.id  =  ?
(1,)
DELETE  FROM  a  WHERE  a.id  =  ?
(1,)
COMMIT 

对于上述所有示例,类似的逻辑也适用于多对多关系的微积分;如果一个多对多关系在一侧设置了 single_parent=True,那么该侧可以使用“delete-orphan”级联,但这很可能不是实际想要的,因为多对多关系的目的是使得可以有许多对象引用任一方向的对象。

总的来说,“delete-orphan”级联通常应用于一对多关系的“一”侧,以便删除“多”侧的对象,而不是反过来。

在 1.3.18 版本中更改:当在多对一或多对多关系上使用“delete-orphan”错误消息时,已更新为更具描述性的文本。

另请参阅

级联

delete-orphan

实例已通过其属性与的实例关联,并且仅允许有一个单独的父对象。 ### 实例已通过其属性与的实例关联,并且仅允许有一个单独的父对象。

relationship.single_parent 标志被使用,并且一个对象同时被指定为多个对象的“父对象”时,会发出此错误。

鉴于以下映射:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

    a = relationship(
        "A",
        single_parent=True,
        cascade="all, delete-orphan",
    )

意图表明,不会有多于一个B对象同时引用特定的A对象:

>>> b1 = B()
>>> b2 = B()
>>> a1 = A()
>>> b1.a = a1
>>> b2.a = a1
sqlalchemy.exc.InvalidRequestError: Instance <A at 0x7eff44359350> is
already associated with an instance of <class '__main__.B'> via its
B.a attribute, and is only allowed a single parent.

当此错误出现意外时,通常是因为在对对于关系,delete-orphan 级联通常仅在一对多关系的“一”侧配置,并不在多对一或多对多关系的“多”侧上。描述的错误消息做出响应时应用了relationship.single_parent标志,而实际问题是对“delete-orphan”级联设置的误解。有关详细信息,请参阅该消息。

另请参阅

对于关系,删除孤儿级联通常仅在一对多关系的“一”方配置,并不在多对一或多对多关系的“多”方配置。 ### 关系 X 将列 Q 复制到列 P,与关系‘Y’冲突

此警告指的是在刷新时两个或多个关系将写入相同列的情况,但 ORM 没有任何手段来协调这些关系。根据具体情况,解决方案可能是两个关系需要使用relationship.back_populates相互引用,或者一个或多个关系应该配置为relationship.viewonly以防止冲突写入,有时配置是完全有意的,应该配置relationship.overlaps以消除每个警告。

对于缺少relationship.back_populates的典型示例,给定以下映射:

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)
    children = relationship("Child")

class Child(Base):
    __tablename__ = "child"
    id = Column(Integer, primary_key=True)
    parent_id = Column(ForeignKey("parent.id"))
    parent = relationship("Parent")

上述映射将生成警告:

SAWarning: relationship 'Child.parent' will copy column parent.id to column child.parent_id,
which conflicts with relationship(s): 'Parent.children' (copies parent.id to child.parent_id).

关系Child.parentParent.children似乎存在冲突。解决方案是应用relationship.back_populates:

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)
    children = relationship("Child", back_populates="parent")

class Child(Base):
    __tablename__ = "child"
    id = Column(Integer, primary_key=True)
    parent_id = Column(ForeignKey("parent.id"))
    parent = relationship("Parent", back_populates="children")

对于更自定义的关系,在“重叠”情况可能是有意的且无法解决的情况下,relationship.overlaps参数可以指定不应发生警告的关系名称。这通常发生在对同一底层表的两个或多个关系具有自定义relationship.primaryjoin条件以限制每种情况下相关项目的情况:

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)
    c1 = relationship(
        "Child",
        primaryjoin="and_(Parent.id == Child.parent_id, Child.flag == 0)",
        backref="parent",
        overlaps="c2, parent",
    )
    c2 = relationship(
        "Child",
        primaryjoin="and_(Parent.id == Child.parent_id, Child.flag == 1)",
        overlaps="c1, parent",
    )

class Child(Base):
    __tablename__ = "child"
    id = Column(Integer, primary_key=True)
    parent_id = Column(ForeignKey("parent.id"))

    flag = Column(Integer)

在上述示例中,ORM 将知道Parent.c1Parent.c2Child.parent之间的重叠是有意的。 ### 对象无法转换为‘persistent’状态,因为此标识映射不再有效。

版本 1.4.26 中的新功能。

这条消息是为了处理以下情况而添加的:在原始Session关闭后,或者在调用其Session.expunge_all()方法后,迭代可能会产生 ORM 对象的Result对象。当一个Session一次性地移除所有对象时,该Session使用的内部标识映射将被替换为一个新的映射,并且原始映射将被丢弃。一个未被使用和未被缓冲的Result对象将在内部保留对该现在被丢弃的标识映射的引用。因此,当消耗了Result时,将无法将要产生的对象与该Session关联起来。这种安排是有意的,因为通常不建议在创建它的事务上下文之外迭代未缓冲的Result对象:

# context manager creates new Session
with Session(engine) as session_obj:
    result = sess.execute(select(User).where(User.id == 7))

# context manager is closed, so session_obj above is closed, identity
# map is replaced

# iterating the result object can't associate the object with the
# Session, raises this error.
user = result.first()

使用asyncio ORM 扩展时,上述情况通常不会发生,因为当AsyncSession返回同步风格的Result时,结果在执行语句时已经预先缓冲。这样可以允许次要的急切加载器调用而无需额外的await调用。

要在上述情况下像asyncio扩展一样预先缓冲结果,可以使用prebuffer_rows执行选项,如下所示:

# context manager creates new Session
with Session(engine) as session_obj:
    # result internally pre-fetches all objects
    result = sess.execute(
        select(User).where(User.id == 7), execution_options={"prebuffer_rows": True}
    )

# context manager is closed, so session_obj above is closed, identity
# map is replaced

# pre-buffered objects are returned
user = result.first()

# however they are detached from the session, which has been closed
assert inspect(user).detached
assert inspect(user).session is None

在上述代码块中,所选的 ORM 对象完全在session_obj块内生成,与session_obj关联,并在Result对象中缓冲以供迭代。在块外,session_obj被关闭并且移除了这些 ORM 对象。迭代Result对象将产生这些 ORM 对象,但是由于它们的来源Session已经移除了它们,它们将以分离状态提供。

注意

上面对“预缓冲”与“非缓冲” Result 对象的引用是指 ORM 将来自 DBAPI 的原始数据库行转换为 ORM 对象的过程。这并不意味着底层的 cursor 对象本身是否被缓冲,表示来自 DBAPI 的待处理结果,它本身是被缓冲的还是非缓冲的,因为这本质上是一个更低层的缓冲。关于 cursor 结果本身的缓冲,请参阅使用服务器端游标(也称为流结果)部分。 ### 类型注释无法解释为注释的声明性表单

SQLAlchemy 2.0 引入了一个新的注释声明式表声明系统,该系统从运行时类定义中的 PEP 484 注释中派生 ORM 映射属性信息。此形式的要求是所有 ORM 注释必须使用称为 Mapped 的通用容器进行正确注释。包括显式 PEP 484 类型注释的遗留 SQLAlchemy 映射,例如使用 遗留 Mypy 扩展进行类型支持的映射,可能包括诸如 relationship() 的指令,不包括此通用容器。

为了解决此问题,可以将类标记为 __allow_unmapped__ 布尔属性,直到它们完全迁移到 2.0 语法。请参阅迁移说明,例如 迁移到 2.0 的第六步 - 向显式类型的 ORM 模型添加 allow_unmapped 的示例。

另请参阅

迁移到 2.0 的第六步 - 向显式类型的 ORM 模型添加 allow_unmapped - 在 SQLAlchemy 2.0 - 主要迁移指南 文档中 ### 当将 转换为数据类时,属性(s) 源自非数据类的父类

当使用描述在任何 mixin 类或抽象基类中的 SQLAlchemy ORM 映射数据类特性,该特性本身并未声明为数据类,例如下面的示例所示时,会发生此警告:

from __future__ import annotations

import inspect
from typing import Optional
from uuid import uuid4

from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass

class Mixin:
    create_user: Mapped[int] = mapped_column()
    update_user: Mapped[Optional[int]] = mapped_column(default=None, init=False)

class Base(DeclarativeBase, MappedAsDataclass):
    pass

class User(Base, Mixin):
    __tablename__ = "sys_user"

    uid: Mapped[str] = mapped_column(
        String(50), init=False, default_factory=uuid4, primary_key=True
    )
    username: Mapped[str] = mapped_column()
    email: Mapped[str] = mapped_column()

由于 Mixin 本身不是从 MappedAsDataclass 扩展的,因此会生成以下警告:

SADeprecationWarning: When transforming <class '__main__.User'> to a
dataclass, attribute(s) "create_user", "update_user" originates from
superclass <class
'__main__.Mixin'>, which is not a dataclass. This usage is deprecated and
will raise an error in SQLAlchemy 2.1\. When declaring SQLAlchemy
Declarative Dataclasses, ensure that all mixin classes and other
superclasses which include attributes are also a subclass of
MappedAsDataclass.

修复方法是在 Mixin 的签名中添加 MappedAsDataclass

class Mixin(MappedAsDataclass):
    create_user: Mapped[int] = mapped_column()
    update_user: Mapped[Optional[int]] = mapped_column(default=None, init=False)

Python 的PEP 681规范不支持在数据类的超类上声明的属性,这些超类本身不是数据类;根据 Python 数据类的行为,这些字段将被忽略,如下例所示:

from dataclasses import dataclass
from dataclasses import field
import inspect
from typing import Optional
from uuid import uuid4

class Mixin:
    create_user: int
    update_user: Optional[int] = field(default=None)

@dataclass
class User(Mixin):
    uid: str = field(init=False, default_factory=lambda: str(uuid4()))
    username: str
    password: str
    email: str

上面,User类将不会在其构造函数中包含create_user,也不会尝试将update_user解释为数据类属性。这是因为Mixin不是数据类。

SQLAlchemy 2.0 系列中的数据类功能未正确遵守这一行为;相反,非数据类混合类和超类上的属性将被视为最终数据类配置的一部分。然而,像 Pyright 和 Mypy 这样的类型检查器不会将这些字段视为数据类构造函数的一部分,因为根据PEP 681它们应该被忽略。否则,由于它们的存在是模棱两可的,SQLAlchemy 2.1 将要求在数据类层次结构中具有 SQLAlchemy 映射属性的混合类本身必须是数据类。### 创建<类名>数据类时遇到的 Python 数据类错误

当使用MappedAsDataclass混合类或registry.mapped_as_dataclass()装饰器时,SQLAlchemy 利用 Python 标准库中的实际Python 数据类模块,以将数据类行为应用于目标类。此 API 有其自己的错误场景,其中大部分涉及在用户定义的类上构建__init__()方法;在类上声明的属性的顺序,以及在超类上的顺序决定了__init__()方法将如何构建,还有特定规则规定了属性的组织方式以及它们应如何使用参数如init=Falsekw_only=True等。SQLAlchemy 不控制或实现这些规则。因此,对于这种类型的错误,请参考Python 数据类文档,特别注意应用于继承的规则。

另请参见

声明式数据类映射 - SQLAlchemy 数据类文档

Python 数据类 - 在 python.org 网站上

继承 - 在 python.org 网站上 ### 按主键进行逐行 ORM 批量更新需要记录包含主键值

在不提供给定记录中的主键值的情况下使用 ORM 批量更新 by Primary Key 功能时会发生此错误,例如:

>>> session.execute(
...     update(User).where(User.name == bindparam("u_name")),
...     [
...         {"u_name": "spongebob", "fullname": "Spongebob Squarepants"},
...         {"u_name": "patrick", "fullname": "Patrick Star"},
...     ],
... )

在上述情况中,参数字典列表的存在与使用Session来执行 ORM 启用的 UPDATE 语句会自动使用基于主键的 ORM 批量更新,该方法期望参数字典包含主键值,例如:

>>> session.execute(
...     update(User),
...     [
...         {"id": 1, "fullname": "Spongebob Squarepants"},
...         {"id": 3, "fullname": "Patrick Star"},
...         {"id": 5, "fullname": "Eugene H. Krabs"},
...     ],
... )

要在不提供每个记录的主键值的情况下调用 UPDATE 语句,请使用Session.connection()来获取当前Connection,然后使用该连接进行调用:

>>> session.connection().execute(
...     update(User).where(User.name == bindparam("u_name")),
...     [
...         {"u_name": "spongebob", "fullname": "Spongebob Squarepants"},
...         {"u_name": "patrick", "fullname": "Patrick Star"},
...     ],
... )

另请参阅

ORM 批量更新 by Primary Key

针对具有多个参数集的 UPDATE 语句禁用基于主键的 ORM 批量更新

异步 I/O 异常

需要等待

SQLAlchemy 的异步模式需要使用异步驱动程序连接到数据库。尝试使用不兼容的 DBAPI 的情况下,通常会引发此错误。

另请参阅

异步 I/O(asyncio) ### 丢失 Greenlet

对异步 DBAPI 的调用是在通常由 SQLAlchemy AsyncIO 代理类设置的 greenlet spawn 上下文之外启动的。通常情况下,当尝试在意料之外的位置进行 IO 时,会发生此错误,使用的调用模式不直接提供使用await关键字的情况。在使用 ORM 时,几乎总是由于使用了延迟加载,在不经过额外步骤和/或使用成功所需的替代加载器模式的情况下,不直接支持 asyncio。

另请参阅

在使用 AsyncSession 时防止隐式 IO - 涵盖了大多数可能出现此问题的 ORM 方案以及如何进行缓解,包括与延迟加载场景一起使用的特定模式。 ### 无可用检查

直接在AsyncConnectionAsyncEngine对象上直接使用inspect()函数目前不受支持,因为尚未提供Inspector对象的可等待形式。相反,通过使用inspect()函数获取对象,使其引用AsyncConnection对象的底层AsyncConnection.sync_connection属性;然后通过使用AsyncConnection.run_sync()方法以及执行所需操作的自定义函数以“同步”调用样式使用Inspector

async def async_main():
    async with engine.connect() as conn:
        tables = await conn.run_sync(
            lambda sync_conn: inspect(sync_conn).get_table_names()
        )

另请参阅

使用检查器检查模式对象 - 使用inspect()与 asyncio 扩展的其他示例。

Core 异常类

查看 Core Exceptions 以获取 Core 异常类。

ORM 异常类

查看 ORM Exceptions 以获取 ORM 异常类。

遗留异常

本节中的异常不是由当前 SQLAlchemy 版本生成的,但在此提供以适应异常消息超链接。

在 SQLAlchemy 2.0 中,<某个函数>将不再<某事>。

SQLAlchemy 2.0 代表了 Core 和 ORM 组件中许多关键 SQLAlchemy 使用模式的重大转变。2.0 版本的目标是对 SQLAlchemy 自其早期开始以来的一些最基本假设进行轻微调整,并提供一个新的简化使用模型,希望在 Core 和 ORM 组件之间更加简约一致,同时更具能力。

在 SQLAlchemy 2.0 - 主要迁移指南中介绍的 SQLAlchemy 2.0 项目包含了一个综合的未来兼容性系统,该系统集成到了 SQLAlchemy 1.4 系列中,以便应用程序能够明确、清晰地、逐步地升级路径,将应用程序迁移到完全兼容 2.0 版本。RemovedIn20Warning弃用警告是该系统的基础,用于提供关于现有代码库中需要修改的行为的指导。如何启用此警告的概述在 SQLAlchemy 2.0 弃用模式中。

另请参阅

SQLAlchemy 2.0 - 主要迁移指南 - 从 1.x 系列升级流程的概述,以及 SQLAlchemy 2.0 的当前目标和进展。

SQLAlchemy 2.0 弃用模式 - 如何在 SQLAlchemy 1.4 中使用“2.0 弃用模式”的具体指南。 ### 对象正在通过反向引用级联合并到一个 Session

本消息指的是 SQLAlchemy 中在 2.0 版本中删除的“反向引用级联”行为。这指的是将对象添加到Session中的操作,因为该会话中已经存在的另一个对象与之关联。由于这种行为被证明比有用更令人困惑,因此添加了relationship.cascade_backrefsbackref.cascade_backrefs参数,可以将其设置为False以禁用它,在 SQLAlchemy 2.0 中完全删除了“级联反向引用”行为。

对于较旧的 SQLAlchemy 版本,在当前使用relationship.backref字符串参数配置的反向引用上设置relationship.cascade_backrefsFalse,必须首先使用backref()函数声明该反向引用,以便传递backref.cascade_backrefs参数。

或者,可以通过在“未来”模式下使用Session来完全关闭整个“级联反向引用”行为,通过将True传递给Session.future参数。

另请参阅

cascade_backrefs 行为在 2.0 中被弃用以移除 - 关于 SQLAlchemy 2.0 变更的背景。### select() 构造以“旧”模式创建;关键字参数等。

select() 构造已在 SQLAlchemy 1.4 中更新,以支持在 SQLAlchemy 2.0 中标准的新调用风格。为了向后兼容在 1.4 系列内,该构造接受“旧”风格和“新”风格的参数。

“新”风格的特性是列和表达式只能按位置传递给select() 构造;对象的任何其他修饰符都必须使用后续的方法链传递:

# this is the way to do it going forward
stmt = select(table1.c.myid).where(table1.c.myid == table2.c.otherid)

对比之下,在 SQLAlchemy 的旧形式中,select() 在像 Select.where() 这样的方法甚至添加之前,可能是这样的:

# this is how it was documented in original SQLAlchemy versions
# many years ago
stmt = select([table1.c.myid], whereclause=table1.c.myid == table2.c.otherid)

或者甚至“whereclause”会按位置传递:

# this is also how it was documented in original SQLAlchemy versions
# many years ago
stmt = select([table1.c.myid], table1.c.myid == table2.c.otherid)

近年来,大多数叙述性文档已经删除了接受的“whereclause”和其他参数,导致调用风格更像是作为列参数传递的列列表,但没有进一步的参数:

# this is how it's been documented since around version 1.0 or so
stmt = select([table1.c.myid]).where(table1.c.myid == table2.c.otherid)

select() 不再接受多样的构造参数,列仅按位置传递 中的文档描述了这一变更的 2.0 迁移。

另请参阅

select() 不再接受多样的构造参数,列仅按位置传递

SQLAlchemy 2.0 - 主要迁移指南 ### 通过旧绑定的元数据找到了一个绑定,但由于该 Session 设置了 future=True,因此该绑定被忽略。

直到 SQLAlchemy 1.4,都存在“bound metadata”的概念;从 SQLAlchemy 2.0 开始已经移除。

此错误指的是MetaData.bind参数,该参数位于MetaData对象上,该对象又允许 ORM Session将特定的映射类与Engine关联起来。在 SQLAlchemy 2.0 中,Session必须直接链接到每个Engine。也就是说,不是实例化Sessionsessionmaker而不带任何参数,并将EngineMetaData关联起来:

engine = create_engine("sqlite://")
Session = sessionmaker()
metadata_obj = MetaData(bind=engine)
Base = declarative_base(metadata=metadata_obj)

class MyClass(Base): ...

session = Session()
session.add(MyClass())
session.commit()

Engine必须直接与sessionmakerSession关联。MetaData对象不应再关联任何引擎:

engine = create_engine("sqlite://")
Session = sessionmaker(engine)
Base = declarative_base()

class MyClass(Base): ...

session = Session()
session.add(MyClass())
session.commit()

在 SQLAlchemy 1.4 中,当Session.future标志设置在sessionmakerSession上时,将启用此 2.0 风格行为。 ### 此编译对象未绑定到任何 Engine 或 Connection

此错误指的是“绑定的元数据”概念,这是仅存在于 1.x 版本的传统 SQLAlchemy 模式。当直接在未关联任何Engine的 Core 表达式对象上调用Executable.execute()方法时,会出现此问题:

metadata_obj = MetaData()
table = Table("t", metadata_obj, Column("q", Integer))

stmt = select(table)
result = stmt.execute()  # <--- raises

逻辑期望的是MetaData对象已经绑定到一个Engine上:

engine = create_engine("mysql+pymysql://user:pass@host/db")
metadata_obj = MetaData(bind=engine)

在上述情况下,任何从Table派生的语句,又从MetaData派生的语句将隐式地利用给定的Engine来调用该语句。

请注意,绑定元数据的概念在 SQLAlchemy 2.0 中不存在。调用语句的正确方式是通过Connection.execute()方法的Connection

with engine.connect() as conn:
    result = conn.execute(stmt)

在使用 ORM 时,可以通过Session获得类似的功能:

result = session.execute(stmt)

另请参阅

语句执行基础知识 ### 此连接处于非活动事务状态。请在继续之前完全回滚()

该错误条件是在 SQLAlchemy 版本 1.4 中添加的,不适用于 SQLAlchemy 2.0. 该错误是指将Connection置于使用诸如Connection.begin()之类的方法创建的事务中,然后在该范围内创建进一步的“标记”事务;然后使用Transaction.rollback()回滚或使用Transaction.close()关闭“标记”事务,但是外部事务仍然处于“非活动”状态,必须回滚。

这种模式看起来像:

engine = create_engine(...)

connection = engine.connect()
transaction1 = connection.begin()

# this is a "sub" or "marker" transaction, a logical nesting
# structure based on "real" transaction transaction1
transaction2 = connection.begin()
transaction2.rollback()

# transaction1 is still present and needs explicit rollback,
# so this will raise
connection.execute(text("select 1"))

上述中,transaction2 是一个“标记”事务,表示事务在外部事务内的逻辑嵌套;虽然内部事务可以通过其 rollback() 方法回滚整个事务,但其 commit() 方法除了关闭“标记”事务的范围外,没有其他效果。调用 transaction2.rollback() 的效果是取消激活 transaction1,这意味着它在数据库级别上实际上已回滚,但仍然存在以适应事务的一致嵌套模式。

正确的解决方法是确保外部事务也已回滚:

transaction1.rollback()

这种模式在 Core 中不常用。在 ORM 中,可能会出现类似的问题,这是 ORM 的“逻辑”事务结构的产物;这在“此会话的事务由于刷新期间的先前异常而被回滚。”(或类似内容)的常见问题解答条目中有描述。

SQLAlchemy 2.0 中已删除“子事务”模式,因此不再提供此特定编程模式,从而防止出现此错误消息。

连接和事务

队列池大小限制已达到溢出,连接超时,超时

这可能是最常见的运行时错误,因为它直接涉及应用程序的工作负载超过配置限制,这通常适用于几乎所有 SQLAlchemy 应用程序。

以下几点总结了这个错误的含义,从大多数 SQLAlchemy 用户应该已经熟悉的最基本点开始。

  • SQLAlchemy 引擎对象默认使用连接池 - 这意味着当使用Engine对象的 SQL 数据库连接资源,并且释放该资源后,数据库连接本身仍保持连接状态,并返回到内部队列,可以再次使用。尽管代码可能看起来已经结束了与数据库的交互,但在许多情况下,应用程序仍会保持一定数量的数据库连接,直到应用程序结束或显式处理池。

  • 由于连接池,当应用程序使用 SQL 数据库连接时,通常是通过使用Engine.connect()或使用 ORM Session进行查询时,此活动并不一定在获取连接对象时立即建立新连接到数据库;相反,它会向连接池查询连接,该连接池通常会检索一个现有连接以供重复使用。如果没有可用连接,连接池将创建一个新的数据库连接,但前提是池未超过配置容量。

  • 大多数情况下使用的默认池称为QueuePool。当您要求此池提供连接但没有可用连接时,它将创建一个新连接如果当前连接总数小于配置值。该值等于池大小加上最大溢出。这意味着如果您已将引擎配置为:

    engine = create_engine("mysql+mysqldb://u:p@host/db", pool_size=10, max_overflow=20)
    

    上述Engine将允许最多同时有 30 个连接在使用中,不包括从引擎分离或失效的连接。如果新连接请求到达并且应用程序的其他部分已经使用了 30 个连接,连接池将阻塞一段固定时间,然后超时并引发此错误消息。

    为了允许更多的连接同时使用,可以使用传递给create_engine()函数的create_engine.pool_sizecreate_engine.max_overflow参数来调整池。等待连接可用的超时时间是使用create_engine.pool_timeout参数配置的。

  • 可以通过将create_engine.max_overflow设置为值“-1”来配置池以具有无限溢出。使用此设置,池仍将维护一组固定的连接池,但如果没有可用的连接,则永远不会阻止新连接的请求;相反,如果没有可用的连接,它将无条件地建立一个新连接。

    然而,以这种方式运行时,如果应用程序存在使用所有可用连接资源的问题,它最终将达到数据库本身可用连接的配置限制,这将再次返回错误。更严重的是,当应用程序耗尽数据库连接时,通常会在失败之前使用大量资源,并且还可能干扰其他依赖于能够连接到数据库的应用程序和数据库状态机制。

    根据上述情况,连接池可以被视为连接使用的安全阀,为防止一个恶意应用导致整个数据库对所有其他应用不可用提供了至关重要的保护层。当收到此错误消息时,最好修复使用过多连接和/或适当配置限制的问题,而不是允许无限溢出,因为这实际上并不能解决潜在问题。

应用程序耗尽所有可用连接的原因是什么?

  • 应用程序正在处理过多的并发请求,以根据池的配置值进行工作 - 这是最直接的原因。如果您的应用程序在允许 30 个并发线程的线程池中运行,并且每个线程使用一个连接,则如果您的池未配置为允许同时至少检出 30 个连接,一旦您的应用程序接收到足够的并发请求,您将会收到此错误。解决方法是提高池的限制或降低并发线程数。

  • 应用程序没有将连接归还到池中 - 这是接下来最常见的原因,即应用程序正在使用连接池,但程序未能将这些连接释放并且仍然保持打开状态。连接池以及 ORM Session 都有逻辑,当会话和/或连接对象被垃圾回收时,底层连接资源将被释放,但不能依赖这种行为及时释放资源。

    这种情况发生的常见原因是应用程序使用 ORM 会话但在完成使用该会话的工作后没有调用 Session.close()。解决方案是确保在完成工作时显式关闭 ORM 会话(如果使用 ORM)或引擎绑定的 Connection 对象(如果使用 Core),通过适当的 .close() 方法或使用其中一个可用的上下文管理器(例如,“with:”语句)来正确释放资源。

  • 应用程序试图运行长时间事务 - 数据库事务是一种非常昂贵的资源,绝对不能让其空闲等待某些事件发生。如果一个应用程序正在等待用户按下按钮,或者等待长时间运行的作业队列中的结果,或者正在保持与浏览器的持久连接打开,请不要在整个时间段保持数据库事务处于打开状态。当应用程序需要与数据库交互并处理事件时,在那一点上打开一个短暂的事务,然后关闭它。

  • 应用程序发生死锁 - 这也是这个错误的常见原因之一,也更难以理解,如果一个应用程序无法完成其对连接的使用,无论是由于应用程序端还是数据库端的死锁,应用程序都会使用完所有可用的连接,这样会导致其他请求收到这个错误。死锁的原因包括:

    • 如果使用隐式异步系统(例如 gevent 或 eventlet)而没有正确地 monkeypatch 所有的 socket 库和驱动程序,或者在没有完全覆盖所有被 monkeypatch 的驱动程序方法的情况下存在漏洞,或者更少见的情况是异步系统正在用于 CPU 密集型工作负载且 greenlets 使用数据库资源的等待时间过长。对于绝大多数关系型数据库操作来说,隐式或显式的异步编程框架通常是不必要或不合适的;如果应用程序必须在某些功能区域使用异步系统,最好是数据库导向型业务方法在传统线程中运行,然后将消息传递给应用程序的异步部分。

    • 数据库端死锁,例如行之间相互死锁

    • 线程错误,例如互相死锁的互斥锁,或者在同一线程中调用已锁定的互斥锁

请记住,使用池的另一种选择是完全关闭池。请参阅切换池实现部分以了解相关背景信息。但是,请注意,当出现此错误消息时,总是由应用程序本身的更大问题引起;池只是帮助更快地暴露问题。

另请参见

连接池

使用引擎和连接 ### 池类不能与 asyncio 引擎一起使用(反之亦然)

QueuePool池类在内部使用thread.Lock对象,并且与 asyncio 不兼容。如果使用create_async_engine()函数创建AsyncEngine,则适当的队列池类是AsyncAdaptedQueuePool,它会自动使用,无需指定。

除了AsyncAdaptedQueuePool之外,NullPoolStaticPool池类不使用锁,并且也适用于与异步引擎一起使用。

在极少数情况下,如果使用create_engine()函数显式指定了AsyncAdaptedQueuePool池类,则也会引发此错误。

另请参见

连接池 ### 在无效事务回滚之前无法重新连接。请在继续之前完全回滚()

这个错误条件指的是当一个Connection无效时,无论是因为数据库断开检测还是因为显式调用Connection.invalidate(),但仍然存在一个事务,该事务由Connection.begin()方法明确启动,或者由于连接在发出任何 SQL 语句时自动开始一个事务,如 SQLAlchemy 2.x 系列中发生的情况。当连接无效时,任何正在进行中的Transaction现在处于无效状态,必须明确回滚才能将其从Connection中移除。### QueuePool 大小为的限制溢出,连接超时,超时

这可能是最常见的运行时错误,因为它直接涉及应用程序的工作负载超过了配置的限制,这个限制通常适用于几乎所有的 SQLAlchemy 应用程序。

以下几点总结了这个错误的含义,从大多数 SQLAlchemy 用户应该已经熟悉的最基本的点开始。

  • SQLAlchemy Engine 对象默认使用连接池 - 这意味着当一个Engine对象的 SQL 数据库连接资源被使用,并且释放了该资源后,数据库连接本身仍然保持连接状态,并返回到一个内部队列中,可以再次使用。尽管代码可能看起来已经结束了与数据库的交互,但在许多情况下,应用程序仍将保持一定数量的数据库连接,直到应用程序结束或显式释放池为止。

  • 由于连接池,当应用程序使用 SQL 数据库连接时,通常是通过使用Engine.connect()或通过使用 ORM Session进行查询时,这个活动并不一定在获取连接对象时立即建立到数据库的新连接;它实际上是向连接池查询连接,这通常会从池中检索一个现有的连接以便重新使用。如果没有可用的连接,池将创建一个新的数据库连接,但只有在池没有超过配置容量时才会这样做。

  • 在大多数情况下使用的默认池称为QueuePool。当您要求此池提供连接,而没有可用连接时,它将创建一个新连接如果当前使用的连接总数少于配置值。这个值等于池大小加上最大溢出。这意味着如果您已将引擎配置为:

    engine = create_engine("mysql+mysqldb://u:p@host/db", pool_size=10, max_overflow=20)
    

    上述Engine允许最多 30 个连接同时存在,不包括已从引擎分离或失效的连接。如果请求新连接,并且应用程序的其他部分已使用 30 个连接,连接池将阻塞一段固定时间,然后超时并引发此错误消息。

    为了允许更多连接同时使用,池可以使用传递给create_engine()函数的create_engine.pool_sizecreate_engine.max_overflow参数进行调整。等待可用连接的超时时间由create_engine.pool_timeout参数配置。

  • 通过将create_engine.max_overflow设置为值“-1”,可以配置池具有无限溢出。使用此设置,池仍将维护一组固定的连接,但如果请求新连接时没有可用连接,它将无条件地创建一个新连接。

    然而,在这种方式下运行时,如果应用程序存在使用所有可用连接资源的问题,则最终会达到数据库本身可用连接的配置限制,这将再次返回错误。更严重的是,当应用程序耗尽数据库连接时,通常会在失败之前使用大量资源,并且还可能干扰其他依赖于能够连接到数据库的应用程序和数据库状态机制。

    鉴于上述情况,连接池可以被视为连接使用的安全阀,提供了对于恶意应用程序使整个数据库对于所有其他应用程序不可用的关键保护层。当收到此错误消息时,最好修复使用太多连接和/或适当配置限制的问题,而不是允许无限溢出,这实际上并没有解决潜在问题。

什么原因会导致应用程序耗尽所有可用的连接?

  • 应用程序正在处理太多并发请求以执行基于池配置的工作 - 这是最直接的原因。如果您有一个运行在允许 30 个并发线程的线程池中的应用程序,每个线程使用一个连接,并且如果您的池没有配置为允许至少同时检出 30 个连接,则在您的应用程序接收到足够的并发请求时,您将收到此错误。解决方法是提高池的限制或降低并发线程的数量。

  • 应用程序未将连接返回到池中 - 这是接下来最常见的原因,即应用程序正在使用连接池,但程序未能释放这些连接,而是保持它们处于打开状态。连接池以及 ORM Session确实具有逻辑,使得当会话和/或连接对象被垃圾回收时,导致底层连接资源被释放,但不能依赖此行为及时释放资源。

    这种情况经常发生的原因是应用程序使用 ORM 会话,但在完成涉及该会话的工作后未调用Session.close()。解决方法是确保 ORM 会话(如果使用 ORM)或绑定到引擎的Connection对象(如果使用 Core)在完成工作后明确关闭,可以通过适当的.close()方法或使用其中一个可用的上下文管理器(例如“with:”语句)来正确释放资源。

  • 应用程序正试图运行长时间事务 - 数据库事务是一种非常昂贵的资源,不应该空闲等待某个事件发生。如果应用程序正在等待用户按下按钮,或者等待长时间作业队列的结果,或者保持持久连接打开以与浏览器交互,不要保持数据库事务始终处于打开状态。当应用程序需要与数据库一起工作并与事件交互时,在那一点上打开一个短暂的事务,然后关闭它。

  • 应用程序发生死锁 - 这也是此错误的常见原因,更难以理解。如果应用程序由于应用程序端或数据库端的死锁而无法完成其对连接的使用,那么应用程序可能会耗尽所有可用连接,然后导致其他请求收到此错误。死锁的原因包括:

    • 如果没有正确地对所有套接字库和驱动程序进行猴子补丁,或者对所有猴子补丁驱动程序方法的覆盖不完全,或者在异步系统正在用于 CPU 密集型工作负载并且使用数据库资源的绿色线程等待太长时间时,则使用隐式异步系统,例如 gevent 或 eventlet,会出现问题。通常,隐式或显式异步编程框架通常对绝大多数关系数据库操作都不是必要的或合适的;如果应用程序必须对某些功能区域使用异步系统,则最好是数据库导向的业务方法在传统线程中运行,然后将消息传递到应用程序的异步部分。

    • 数据库端发生死锁,例如行相互死锁

    • 线程错误,例如互斥体在相互死锁,或在同一线程中调用已锁定的互斥体

请记住,除了使用池化技术的替代方法是完全关闭池化技术。请参阅切换池实现部分了解背景信息。但是,请注意,当出现此错误消息时,通常是由于应用程序本身存在更大的问题;池仅帮助更早地暴露问题。

另请参阅

连接池

与引擎和连接一起工作

无法将 Pool 类与 asyncio 引擎一起使用(反之亦然)

QueuePool 池类在内部使用 thread.Lock 对象,并且与 asyncio 不兼容。如果使用 create_async_engine() 函数创建 AsyncEngine,则适当的队列池类是 AsyncAdaptedQueuePool,它会自动使用,无需指定。

除了 AsyncAdaptedQueuePoolNullPoolStaticPool 池类不使用锁,并且也适用于与异步引擎一起使用。

在极少数情况下,如果使用 create_engine() 函数明确指定 AsyncAdaptedQueuePool 池类,则还会引发此错误。

另请参阅

连接池

无法重新连接直到无效事务被完全回滚。请在继续之前完全回滚()

此错误条件指的是 Connection 被作废的情况,可能是由于检测到数据库断开连接或由于显式调用 Connection.invalidate(),但仍然存在已经由 Connection.begin() 方法显式启动的事务,或者由于连接在发出任何 SQL 语句时自动开始事务,这在 SQLAlchemy 的 2.x 系列中发生。当连接被作废时,任何正在进行的 Transaction 现在处于无效状态,必须显式回滚以将其从 Connection 中移除。

DBAPI 错误

Python 数据库 API,或 DBAPI,是一种用于数据库驱动程序的规范,可以在 Pep-249 找到。此 API 指定了一组异常类,以适应数据库的所有故障模式。

SQLAlchemy 不会直接生成这些异常。相反,它们是从数据库驱动程序拦截并由 SQLAlchemy 提供的异常 DBAPIError 包装的,但异常中的消息是由驱动程序生成的,而不是 SQLAlchemy

InterfaceError

与数据库接口而不是数据库本身相关的错误引发的异常。

此错误是 DBAPI 错误 ,源自数据库驱动程序(DBAPI),而不是 SQLAlchemy 本身。

InterfaceError 有时会由驱动程序在数据库连接断开或无法连接到数据库的情况下引发。有关如何处理此问题的提示,请参阅 处理断开连接 部分。 ### DatabaseError

与数据库本身相关而不是接口或传递的数据的错误引发的异常。

此错误是 DBAPI 错误 ,源自数据库驱动程序(DBAPI),而不是 SQLAlchemy 本身。 ### DataError

由于处理的数据问题引发的异常,例如除以零,数值超出范围等。

此错误是 DBAPI 错误,源自数据库驱动程序(DBAPI),而不是 SQLAlchemy 本身。 ### OperationalError

与数据库操作相关的错误引发的异常,不一定在程序员控制之下,例如出现意外断开连接,找不到数据源名称,无法处理事务,处理过程中发生内存分配错误等。

此错误是 DBAPI 错误,源自数据库驱动程序(DBAPI),而不是 SQLAlchemy 本身。

OperationalError 是由驱动程序在数据库连接断开或无法连接到数据库的情况下使用的最常见(但不是唯一)错误类别。有关如何处理此问题的提示,请参阅处理断开连接部分。 ### IntegrityError

当数据库的关系完整性受到影响时引发的异常,例如外键检查失败。

此错误是 DBAPI 错误,源自数据库驱动程序(DBAPI),而不是 SQLAlchemy 本身。 ### InternalError

当数据库遇到内部错误时引发的异常,例如游标不再有效,事务不同步等。

此错误是 DBAPI 错误,源自数据库驱动程序(DBAPI),而不是 SQLAlchemy 本身。

InternalError 有时会由驱动程序在数据库连接断开或无法连接到数据库的情况下引发。有关如何处理此问题的提示,请参阅处理断开连接部分。 ### ProgrammingError

由于编程错误引发的异常,例如未找到表或已存在,SQL 语句中的语法错误,指定的参数数量错误等。

此错误是 DBAPI 错误,源自数据库驱动程序(DBAPI),而不是 SQLAlchemy 本身。

ProgrammingError 有时会由驱动程序在数据库连接断开或无法连接到数据库的情况下引发。有关如何处理此问题的提示,请参阅处理断开连接部分。 ### NotSupportedError

当使用数据库不支持的方法或数据库 API 时引发的异常,例如在不支持事务或已关闭事务的连接上请求.rollback()

此错误是 DBAPI 错误,源自数据库驱动程序(DBAPI),而不是 SQLAlchemy 本身。 ### InterfaceError

与数据库本身而不是数据库接口相关的错误引发的异常。

此错误是 DBAPI 错误,源自数据库驱动程序(DBAPI),而不是 SQLAlchemy 本身。

InterfaceError有时由驱动程序在数据库连接断开或无法连接到数据库的情况下引发。有关如何处理此问题的提示,请参见处理断开连接部分。

DatabaseError

由于与数据库本身相关的错误而引发的异常,而不是与传递的接口或数据相关。

此错误是 DBAPI 错误,源自数据库驱动程序(DBAPI),而不是 SQLAlchemy 本身。

DataError

由于处理数据的问题(例如除零,数值超出范围等)引发的错误。

此错误是 DBAPI 错误,源自数据库驱动程序(DBAPI),而不是 SQLAlchemy 本身。

OperationalError

由于与数据库操作相关的错误而引发的异常,不一定在程序员的控制之下,例如发生意外断开连接,数据源名称未找到,无法处理事务,处理过程中发生内存分配错误等。

此错误是 DBAPI 错误,源自数据库驱动程序(DBAPI),而不是 SQLAlchemy 本身。

OperationalError是驱动程序在数据库连接断开或无法连接到数据库的情况下最常见(但不是唯一)使用的错误类。有关如何处理此问题的提示,请参见处理断开连接部分。

IntegrityError

当数据库的关系完整性受到影响时引发异常,例如外键检查失败。

此错误是 DBAPI 错误,源自数据库驱动程序(DBAPI),而不是 SQLAlchemy 本身。

InternalError

当数据库遇到内部错误时引发异常,例如游标不再有效,事务不同步等。

此错误是 DBAPI 错误,源自数据库驱动程序(DBAPI),而不是 SQLAlchemy 本身。

InternalError有时由驱动程序在数据库连接断开或无法连接到数据库的情况下引发。有关如何处理此问题的提示,请参见处理断开连接部分。

ProgrammingError

由于编程错误而引发的异常,例如表未找到或已存在,在 SQL 语句中存在语法错误,指定的参数数量错误等。

此错误是 DBAPI 错误,源自数据库驱动程序(DBAPI),而不是 SQLAlchemy 本身。

ProgrammingError有时由驱动程序在数据库连接断开或无法连接到数据库的情况下引发。有关如何处理此问题的提示,请参见处理断开连接部分。

NotSupportedError

在使用数据库不支持的方法或数据库 API 时引发异常,例如在不支持事务或已关闭事务的连接上请求 .rollback()。

这个错误是一个 DBAPI 错误,源自数据库驱动程序(DBAPI),而不是 SQLAlchemy 本身。

SQL 表达语言

对象不会生成缓存键,性能影响

SQLAlchemy 从版本 1.4 开始包含一个 SQL 编译缓存设施,它允许 Core 和 ORM SQL 构造缓存它们的字符串形式,以及用于从语句中获取结果的其他结构信息,当下次使用另一个结构上等效的构造时,可以跳过相对昂贵的字符串编译过程。该系统依赖于为所有 SQL 构造实现的功能,包括对象如Columnselect()TypeEngine对象,以生成完全代表它们状态的缓存键,以影响 SQL 编译过程。

如果问题中的警告涉及到广泛使用的对象,如Column对象,并且显示影响到发出的大多数 SQL 构造(使用估算缓存性能使用日志记录描述的估算技术),以至于缓存通常不会为应用程序启用,这将对性能产生负面影响,并且在某些情况下,与之前的 SQLAlchemy 版本相比,实际上会产生性能下降。在为什么升级到 1.4 和/或 2.x 后我的应用程序变慢?的常见问题解答中详细介绍了这一点。

如果有任何疑问,缓存会自行禁用。

缓存依赖于能够以一致的方式生成准确代表语句完整结构的缓存键。如果特定的 SQL 构造(或类型)没有适当的指令,允许它生成正确的缓存键,那么不能安全地启用缓存:

  • 缓存键必须代表完整结构:如果使用两个不同实例的构造可能导致渲染不同 SQL,那么针对第一个元素使用不捕捉第一和第二元素之间不同之处的缓存键缓存 SQL,将导致第二个实例渲染出错误的 SQL。

  • 缓存键必须是一致的:如果一个构造表示每次都会改变的状态,比如字面值,为每个实例生成唯一的 SQL,那么这个构造也不安全可缓存,因为重复使用构造将快速填满语句缓存,其中包含可能不会再次使用的唯一 SQL 字符串,从而破坏了缓存的目的。

由于上述两个原因,SQLAlchemy 的缓存系统对于决定是否缓存与对象对应的 SQL 是非常保守的。

缓存断言属性

根据以下标准发出警告。有关每个标准的详细信息,请参见 为什么升级到 1.4 和/或 2.x 后我的应用程序变慢了?。

  • Dialect 本身(即由我们传递给 create_engine() 的 URL 的第一部分指定的模块,如 postgresql+psycopg2://),必须指示已经审查和测试以正确支持缓存,这由 Dialect.supports_statement_cache 属性设置为 True 来表示。在使用第三方方言时,请咨询方言的维护人员,以便他们遵循确保可以启用缓存的步骤并发布新版本。

  • 第三方或用户定义的类型,它们继承自TypeDecoratorUserDefinedType,必须在其定义中包含 ExternalType.cache_ok 属性,包括所有派生子类,遵循ExternalType.cache_ok的文档字符串中描述的准则。与以前一样,如果这些数据类型是从第三方库导入的,请咨询该库的维护人员,以便他们提供必要的更改并发布新版本。

  • 第三方或用户定义的 SQL 构造,从类似 ClauseElementColumnInsert 等类继承,包括简单的子类以及设计用于与自定义 SQL 构造和编译扩展一起工作的那些,通常应包括 HasCacheKey.inherit_cache 属性设置为 TrueFalse,根据构造的设计,遵循在为自定义构造启用缓存支持中描述的准则。

另请参阅

使用日志估算缓存性能 - 关于观察缓存行为和效率的背景知识

为什么升级到 1.4 和/或 2.x 后我的应用程序变慢了? - 在常见问题解答部分 ### Compiler StrSQLCompiler 无法渲染类型为 的元素

这个错误通常发生在尝试将包含不属于默认编译的元素的 SQL 表达式构造转换为字符串时;在这种情况下,错误将针对 StrSQLCompiler 类。在较少见的情况下,当使用错误类型的 SQL 表达式与特定类型的数据库后端一起使用时,也会发生这种情况;在这些情况下,将命名其他类型的 SQL 编译器类,如 SQLCompilersqlalchemy.dialects.postgresql.PGCompiler。以下指导更具体地针对“字符串化”用例,但也描述了一般背景。

通常,核心 SQL 构造或 ORM Query 对象可以直接转换为字符串,例如当我们使用 print() 时:

>>> from sqlalchemy import column
>>> print(column("x") == 5)
x  =  :x_1 

当上述 SQL 表达式被转换为字符串时,将使用 StrSQLCompiler 编译器类,这是一个特殊的语句编译器,当一个构造被转换为字符串时,没有任何特定于方言的信息时会被调用。

然而,有许多构造是特定于某种数据库方言的,对于这些构造,StrSQLCompiler 不知道如何转换为字符串,例如 PostgreSQL 的 “insert on conflict” 构造:

>>> from sqlalchemy.dialects.postgresql import insert
>>> from sqlalchemy import table, column
>>> my_table = table("my_table", column("x"), column("y"))
>>> insert_stmt = insert(my_table).values(x="foo")
>>> insert_stmt = insert_stmt.on_conflict_do_nothing(index_elements=["y"])
>>> print(insert_stmt)
Traceback (most recent call last):

...

sqlalchemy.exc.UnsupportedCompilationError:
Compiler <sqlalchemy.sql.compiler.StrSQLCompiler object at 0x7f04fc17e320>
can't render element of type
<class 'sqlalchemy.dialects.postgresql.dml.OnConflictDoNothing'>

为了将特定于特定后端的结构字符串化,必须使用ClauseElement.compile()方法,传递一个Engine或一个Dialect对象,该对象将调用正确的编译器。下面我们使用了一个 PostgreSQL 方言:

>>> from sqlalchemy.dialects import postgresql
>>> print(insert_stmt.compile(dialect=postgresql.dialect()))
INSERT  INTO  my_table  (x)  VALUES  (%(x)s)  ON  CONFLICT  (y)  DO  NOTHING 

对于 ORM Query 对象,可以使用 Query.statement 访问器访问语句:

statement = query.statement
print(statement.compile(dialect=postgresql.dialect()))

请查看下方的常见问题解答链接,了解关于直接字符串化/编译 SQL 元素的额外细节。

请参阅

如何将 SQL 表达式呈现为字符串,可能会内联绑定参数?

TypeError: <operator>不支持‘ColumnProperty’和<something>实例之间的操作

当尝试在 SQL 表达式的上下文中使用 column_property()deferred() 对象时,通常会发生这种情况,通常是在声明性的上下文中,如下所示:

class Bar(Base):
    __tablename__ = "bar"

    id = Column(Integer, primary_key=True)
    cprop = deferred(Column(Integer))

    __table_args__ = (CheckConstraint(cprop > 5),)

上面,cprop 属性在映射之前内联使用,但是这个 cprop 属性不是一个Column,它是一个ColumnProperty,这是一个临时对象,因此它没有 Column 对象或 InstrumentedAttribute 对象的全部功能,一旦声明过程完成,它将被映射到 Bar 类上。

虽然 ColumnProperty 有一个 __clause_element__() 方法,它允许它在某些面向列的上下文中工作,但它不能在上面示例中所示的开放式比较上下文中工作,因为它没有 Python __eq__() 方法,该方法允许它将与数字“5”的比较解释为 SQL 表达式而不是常规 Python 比较。

解决方案是直接使用Column访问ColumnProperty.expression属性:

class Bar(Base):
    __tablename__ = "bar"

    id = Column(Integer, primary_key=True)
    cprop = deferred(Column(Integer))

    __table_args__ = (CheckConstraint(cprop.expression > 5),)

绑定参数(在参数组中)需要一个值

当语句隐式或显式地使用bindparam(),并且在执行语句时没有提供值时,会发生此错误:

stmt = select(table.c.column).where(table.c.id == bindparam("my_param"))

result = conn.execute(stmt)

在上述示例中,没有为参数“my_param”提供值。正确的方法是提供一个值:

result = conn.execute(stmt, {"my_param": 12})

当消息采用“在参数组< y >中需要为绑定参数< x >提供值”的形式时,消息是指执行的“executemany”风格。在这种情况下,语句通常是 INSERT、UPDATE 或 DELETE,并且正在传递参数列表。在这种格式中,语句可以动态生成,以包含参数列表中提供的每个参数的参数位置,它将使用第一组参数来确定这些参数应该是什么。

例如,下面的语句是基于第一个参数集计算的,需要参数“a”、“b”和“c” - 这些名称确定了语句的最终字符串格式,该格式将用于列表中的每个参数集。由于第二个条目不包含“b”,因此会生成此错误:

m = MetaData()
t = Table("t", m, Column("a", Integer), Column("b", Integer), Column("c", Integer))

e.execute(
    t.insert(),
    [
        {"a": 1, "b": 2, "c": 3},
        {"a": 2, "c": 4},
        {"a": 3, "b": 4, "c": 5},
    ],
)
sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError)
A value is required for bind parameter 'b', in parameter group 1
[SQL: u'INSERT INTO t (a, b, c) VALUES (?, ?, ?)']
[parameters: [{'a': 1, 'c': 3, 'b': 2}, {'a': 2, 'c': 4}, {'a': 3, 'c': 5, 'b': 4}]]

由于“b”是必需的,因此将其传递为None,以便进行 INSERT 操作:

e.execute(
    t.insert(),
    [
        {"a": 1, "b": 2, "c": 3},
        {"a": 2, "b": None, "c": 4},
        {"a": 3, "b": 4, "c": 5},
    ],
)

请参阅

发送参数 ### 期望的 FROM 子句,得到 Select。要创建 FROM 子句,请使用 .subquery() 方法

这是 SQLAlchemy 1.4 所做的更改,其中通过诸如select()之类的函数生成的 SELECT 语句,但也包括联合和文本 SELECT 表达式等内容不再被视为FromClause对象,并且不能直接放置在另一个 SELECT 语句的 FROM 子句中,而不是首先将它们包装在Subquery中。这是核心中的一个重大概念性变化,完整的原理在 SELECT 语句不再隐式视为 FROM 子句中讨论。

给出一个示例:

m = MetaData()
t = Table("t", m, Column("a", Integer), Column("b", Integer), Column("c", Integer))
stmt = select(t)

在上述示例中,stmt表示一个 SELECT 语句。当我们想要直接将stmt作为另一个 SELECT 语句的 FROM 子句时,比如如果我们试图从中选择:

new_stmt_1 = select(stmt)

或者如果我们想在 FROM 子句中使用它,比如在 JOIN 中:

new_stmt_2 = select(some_table).select_from(some_table.join(stmt))

在 SQLAlchemy 的早期版本中,使用一个 SELECT 语句在另一个 SELECT 语句内会产生一个有括号的无名称子查询。在大多数情况下,这种形式的 SQL 不是很有用,因为像 MySQL 和 PostgreSQL 这样的数据库要求 FROM 子句中的子查询具有命名别名,这意味着需要使用SelectBase.alias()方法或者从 1.4 版本开始使用SelectBase.subquery()方法来产生这个。在其他数据库中,子查询有一个名称来解析子查询内部列名的任何歧义仍然更清晰。

除了上述实际原因外,还有很多其他与 SQLAlchemy 相关的原因导致进行此更改。因此,上述两个语句的正确形式要求使用SelectBase.subquery()

subq = stmt.subquery()

new_stmt_1 = select(subq)

new_stmt_2 = select(some_table).select_from(some_table.join(subq))

另请参阅

SELECT 语句不再隐式地被视为 FROM 子句 ### 为原始 clauseelement 自动生成别名

版本 1.4.26 中新增。

这个弃用警告指的是一个非常古老且可能不太为人所知的模式,适用于传统的Query.join()方法以及 2.0 风格的Select.join()方法,其中联接可以根据relationship()来表示,但目标是将类映射到的Table或其他核心可选择项,而不是 ORM 实体,比如映射类或aliased()构造:

a1 = Address.__table__

q = (
    s.query(User)
    .join(a1, User.addresses)
    .filter(Address.email_address == "ed@foo.com")
    .all()
)

上述模式还允许任意可选择项,比如核心JoinAlias对象,但是没有对此元素的自动适应,这意味着需要直接引用核心元素:

a1 = Address.__table__.alias()

q = (
    s.query(User)
    .join(a1, User.addresses)
    .filter(a1.c.email_address == "ed@foo.com")
    .all()
)

指定联接目标的正确方法始终是使用映射类本身或一个aliased对象,后者使用PropComparator.of_type()修饰符来设置一个别名:

# normal join to relationship entity
q = s.query(User).join(User.addresses).filter(Address.email_address == "ed@foo.com")

# name Address target explicitly, not necessary but legal
q = (
    s.query(User)
    .join(Address, User.addresses)
    .filter(Address.email_address == "ed@foo.com")
)

加入到别名:

from sqlalchemy.orm import aliased

a1 = aliased(Address)

# of_type() form; recommended
q = (
    s.query(User)
    .join(User.addresses.of_type(a1))
    .filter(a1.email_address == "ed@foo.com")
)

# target, onclause form
q = s.query(User).join(a1, User.addresses).filter(a1.email_address == "ed@foo.com")
```  ### 由于重叠表而自动生成别名

新版本为 1.4.26。

此警告通常是在使用`Select.join()`方法或传统的`Query.join()`方法进行查询时生成的,其中涉及到涉及连接表继承的映射。问题在于,在两个共享共同基表的连接继承模型之间进行连接时,如果不对其中一个或另一个应用别名,就无法形成两个实体之间的适当 SQL JOIN;SQLAlchemy 将别名应用于连接的右侧。例如,考虑到连接继承映射:

```py
class Employee(Base):
    __tablename__ = "employee"
    id = Column(Integer, primary_key=True)
    manager_id = Column(ForeignKey("manager.id"))
    name = Column(String(50))
    type = Column(String(50))

    reports_to = relationship("Manager", foreign_keys=manager_id)

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": type,
    }

class Manager(Employee):
    __tablename__ = "manager"
    id = Column(Integer, ForeignKey("employee.id"), primary_key=True)

    __mapper_args__ = {
        "polymorphic_identity": "manager",
        "inherit_condition": id == Employee.id,
    }

上述映射包括EmployeeManager类之间的关系。由于两个类都使用“employee”数据库表,从 SQL 的角度来看,这是一种自引用关系。如果我们想要使用连接从EmployeeManager模型中查询,SQL 级别上“employee”表需要在查询中包含两次,这意味着它必须被别名化。当我们使用 SQLAlchemy ORM 创建这样的连接时,我们得到的 SQL 看起来像下面这样:

>>> stmt = select(Employee, Manager).join(Employee.reports_to)
>>> print(stmt)
SELECT  employee.id,  employee.manager_id,  employee.name,
employee.type,  manager_1.id  AS  id_1,  employee_1.id  AS  id_2,
employee_1.manager_id  AS  manager_id_1,  employee_1.name  AS  name_1,
employee_1.type  AS  type_1
FROM  employee  JOIN
(employee  AS  employee_1  JOIN  manager  AS  manager_1  ON  manager_1.id  =  employee_1.id)
ON  manager_1.id  =  employee.manager_id 

上面的 SQL 从employee表中选择,表示查询中的Employee实体。然后加入到employee AS employee_1 JOIN manager AS manager_1的右嵌套连接,其中再次声明了employee表,但作为匿名别名employee_1。这就是警告消息所指的“自动生成别名”。

当 SQLAlchemy 加载包含EmployeeManager对象的 ORM 行时,ORM 必须将来自上述employee_1manager_1表别名的行适应为未别名化的Manager类的行。这个过程在内部是复杂的,并且不支持所有 API 功能,特别是当尝试在比这里展示的更深度嵌套查询中使用急加载功能时,如contains_eager()。由于该模式对于更复杂的情况不可靠,并涉及难以预测和遵循的隐式决策,因此会发出警告,并且该模式可能被视为传统功能。编写此查询的更好方法是使用适用于任何其他自引用关系的相同模式,即显式使用aliased()构造。对于联接继承和其他基于联接的映射,通常希望添加使用aliased.flat参数,这将允许通过将别名应用于联接中的各个表来对两个或更多表进行联接别名化,而不是将联接嵌入到新的子查询中:

>>> from sqlalchemy.orm import aliased
>>> manager_alias = aliased(Manager, flat=True)
>>> stmt = select(Employee, manager_alias).join(Employee.reports_to.of_type(manager_alias))
>>> print(stmt)
SELECT  employee.id,  employee.manager_id,  employee.name,
employee.type,  manager_1.id  AS  id_1,  employee_1.id  AS  id_2,
employee_1.manager_id  AS  manager_id_1,  employee_1.name  AS  name_1,
employee_1.type  AS  type_1
FROM  employee  JOIN
(employee  AS  employee_1  JOIN  manager  AS  manager_1  ON  manager_1.id  =  employee_1.id)
ON  manager_1.id  =  employee.manager_id 

如果我们想要使用contains_eager()来填充reports_to属性,我们将引用别名:

>>> stmt = (
...     select(Employee)
...     .join(Employee.reports_to.of_type(manager_alias))
...     .options(contains_eager(Employee.reports_to.of_type(manager_alias)))
... )

在某些更嵌套的情况下,如果没有使用显式的aliased()对象,在 ORM 在非常嵌套的上下文中“自动别名化”的情况下,contains_eager()选项可能没有足够的上下文来知道从哪里获取其数据。因此,最好不要依赖此功能,而是尽可能保持 SQL 构造尽可能明确。###对象不会生成缓存键,性能影响

截至版本 1.4,SQLAlchemy 包括一个 SQL 编译缓存设施,它允许 Core 和 ORM SQL 构造缓存它们的字符串形式,以及用于从语句中获取结果的其他结构信息,这样当下次使用另一个结构等效的构造时,就可以跳过相对昂贵的字符串编译过程。该系统依赖于为所有 SQL 构造实现的功能,包括诸如Columnselect()TypeEngine对象等对象,以生成完全代表它们状态的缓存键,以至于影响 SQL 编译过程。

如果警告涉及到广泛使用的对象,比如Column对象,并且显示为影响到大部分发出的 SQL 构造(使用通过日志估算缓存性能描述的估算技术)以至于缓存通常不会为应用程序启用,这将对性能产生负面影响,并且在某些情况下,与之前的 SQLAlchemy 版本相比实际上会产生性能下降。在为什么升级到 1.4 和/或 2.x 后我的应用程序变慢了?的常见问题解答中详细介绍了这一点。

如果存在任何疑问,缓存会自行禁用

缓存依赖于能够生成一个缓存键,以一种一致的方式准确地表示语句的完整结构。如果某个特定的 SQL 构造(或类型)没有适当的指令来生成正确的缓存键,那么就不能安全地启用缓存:

  • 缓存键必须表示完整的结构:如果两个单独实例的使用可能导致呈现不同 SQL,则针对第一个元素使用一个不捕获第一和第二个元素之间不同之处的缓存键缓存 SQL,将导致为第二个实例缓存并呈现不正确的 SQL。

  • 缓存键必须是一致的:如果一个构造代表每次都会更改的状态,比如文字值,为每个实例产生唯一的 SQL,那么这个构造也不安全可以缓存,因为重复使用这个构造将很快填满语句缓存,里面包含的唯一 SQL 字符串可能不会再次使用,从而使缓存失去了意义。

由于上述两个原因,SQLAlchemy 的缓存系统在决定是否缓存与对象对应的 SQL 时极其保守

缓存的断言属性

根据以下标准发出警告。有关每个标准的更多详细信息,请参阅升级到 1.4 和/或 2.x 后为什么我的应用程序变慢?部分。

  • Dialect本身(即由我们传递给create_engine()的 URL 的第一部分指定的模块,如postgresql+psycopg2://),必须指示已经审查并测试以正确支持缓存,这由Dialect.supports_statement_cache属性设置为True来指示。在使用第三方方言时,请与方言的维护者协商,以便他们遵循确保可以启用缓存的步骤并发布新版本。

  • 第三方或用户定义的类型,继承自TypeDecoratorUserDefinedType,必须在其定义中包含ExternalType.cache_ok属性,包括所有派生子类,在ExternalType.cache_ok的文档字符串中描述的指南中遵循。同样,如果这些数据类型是从第三方库导入的,请与该库的维护者协商,以便他们提供必要的更改并发布新版本。

  • 第三方或用户定义的 SQL 构造,从诸如ClauseElementColumnInsert等类继承,包括简单的子类以及设计用于与自定义 SQL 构造和编译扩展一起工作的子类,通常应包含HasCacheKey.inherit_cache属性设置为TrueFalse,根据构造的设计,在为自定义构造启用缓存支持中描述的指南。

另请参阅

使用日志估算缓存性能 - 观察缓存行为和效率的背景

为什么升级到 1.4 和/或 2.x 后我的应用程序变慢了? - 在常见问题部分

如果有任何疑问,缓存会自行禁用

缓存依赖于能够生成准确代表语句的完整结构的缓存键,以一致的方式。如果特定的 SQL 结构(或类型)没有适当的指令来允许其生成正确的缓存键,则不能安全地启用缓存:

  • 缓存键必须代表完整结构:如果使用两个不同实例的结构可能导致渲染不同的 SQL,则使用不捕获第一个和第二个元素之间不同之处的缓存键对第一个元素的 SQL 进行缓存将导致第二个实例的 SQL 被错误地缓存和渲染。

  • 缓存键必须是一致的:如果一个结构代表每次都会改变的状态,比如字面值,为每个实例生成唯一的 SQL,那么这个结构也不能安全地缓存,因为对该结构的重复使用将迅速用唯一的 SQL 字符串填满语句缓存,这些字符串可能不会再次使用,从而打破缓存的目的。

由于上述两个原因,SQLAlchemy 的缓存系统对于决定是否缓存与对象对应的 SQL 是极端保守的

缓存的断言属性

根据以下标准发出警告。有关每个标准的更多详细信息,请参见 为什么升级到 1.4 和/或 2.x 后我的应用程序变慢了?一节。

  • Dialect 本身(即我们传递给 create_engine() 的 URL 的第一部分指定的模块,如 postgresql+psycopg2://),必须指示它已经经过审查和测试,以正确支持缓存,这由 Dialect.supports_statement_cache 属性设置为 True 来表示。使用第三方方言时,请咨询方言的维护者,以便他们可以按照确保可以启用缓存的步骤进行操作,并发布一个新的版本。

  • TypeDecoratorUserDefinedType 继承的第三方或用户定义的类型必须在其定义中包含 ExternalType.cache_ok 属性,包括所有派生子类,在外部类型缓存支持 的文档字符串中描述的准则。与以前一样,如果这些数据类型是从第三方库导入的,请咨询该库的维护者,以便他们提供必要的更改并发布新版本。

  • 第三方或用户定义的 SQL 构造,它们从类中子类化,如ClauseElementColumnInsert 等,包括简单的子类以及那些设计用于与 自定义 SQL 构造和编译扩展一起工作的子类,通常应该将HasCacheKey.inherit_cache 属性设置为 TrueFalse,根据构造的设计,遵循在 启用自定义构造的缓存支持 中描述的准则。

参见

使用日志估算缓存性能 - 观察缓存行为和效率的背景知识

为什么我的应用程序在升级到 1.4 和/或 2.x 后变慢? - 在常见问题解答部分

编译器 StrSQLCompiler 无法渲染类型为 的元素

当尝试将包含不属于默认编译的元素的 SQL 表达式构造进行字符串化时,通常会发生此错误;在这种情况下,错误将针对StrSQLCompiler 类。在较少见的情况下,当使用错误类型的 SQL 表达式与特定类型的数据库后端时,也可能发生这种情况;在这些情况下,将命名其他类型的 SQL 编译器类,例如 SQLCompilersqlalchemy.dialects.postgresql.PGCompiler。下面的指导更具体地针对“字符串化”用例,但也描述了一般背景。

通常,Core SQL 构造或 ORM Query对象可以直接转换为字符串,比如当我们使用print()时:

>>> from sqlalchemy import column
>>> print(column("x") == 5)
x  =  :x_1 

当上述 SQL 表达式被字符串化时,将使用StrSQLCompiler 编译器类,这是一个特殊的语句编译器,当一个构造在没有任何特定于方言的信息的情况下被字符串化时会被调用。

然而,有许多构造是特定于某种数据库方言的,对于这些构造,StrSQLCompiler 不知道如何转换为字符串,比如 PostgreSQL 的“insert on conflict”构造:

>>> from sqlalchemy.dialects.postgresql import insert
>>> from sqlalchemy import table, column
>>> my_table = table("my_table", column("x"), column("y"))
>>> insert_stmt = insert(my_table).values(x="foo")
>>> insert_stmt = insert_stmt.on_conflict_do_nothing(index_elements=["y"])
>>> print(insert_stmt)
Traceback (most recent call last):

...

sqlalchemy.exc.UnsupportedCompilationError:
Compiler <sqlalchemy.sql.compiler.StrSQLCompiler object at 0x7f04fc17e320>
can't render element of type
<class 'sqlalchemy.dialects.postgresql.dml.OnConflictDoNothing'>

为了将特定于特定后端的构造转换为字符串,必须使用ClauseElement.compile() 方法,传递一个 Engine 或一个 Dialect 对象,这将调用正确的编译器。下面我们使用一个 PostgreSQL 方言:

>>> from sqlalchemy.dialects import postgresql
>>> print(insert_stmt.compile(dialect=postgresql.dialect()))
INSERT  INTO  my_table  (x)  VALUES  (%(x)s)  ON  CONFLICT  (y)  DO  NOTHING 

对于 ORM Query 对象,可以通过Query.statement访问器访问语句:

statement = query.statement
print(statement.compile(dialect=postgresql.dialect()))

请查看下面的 FAQ 链接,了解有关 SQL 元素的直接字符串化/编译的更多详细信息。

另请参阅

如何将 SQL 表达式呈现为字符串,可能包含内联的绑定参数?

TypeError: not supported between instances of ‘ColumnProperty’ and

当尝试在 SQL 表达式的上下文中使用column_property()deferred()对象时,通常在声明中会出现这种情况:

class Bar(Base):
    __tablename__ = "bar"

    id = Column(Integer, primary_key=True)
    cprop = deferred(Column(Integer))

    __table_args__ = (CheckConstraint(cprop > 5),)

在上面的例子中,在映射之前内联使用了cprop属性,但是这个cprop属性不是一个Column,它是一个ColumnProperty,这是一个临时对象,因此不具备Column对象或InstrumentedAttribute对象的全部功能,一旦声明过程完成,它将映射到Bar类上。

虽然ColumnProperty确实具有__clause_element__()方法,允许它在某些面向列的上下文中工作,但是它无法在开放式比较上下文中工作,如上所示,因为它没有 Python __eq__() 方法,该方法将允许它将对数字“5”的比较解释为 SQL 表达式而不是常规的 Python 比较。

解决方案是直接访问Column,使用ColumnProperty.expression 属性:

class Bar(Base):
    __tablename__ = "bar"

    id = Column(Integer, primary_key=True)
    cprop = deferred(Column(Integer))

    __table_args__ = (CheckConstraint(cprop.expression > 5),)

绑定参数(在参数组中)需要值

当语句在执行时使用bindparam() 时,如果未显式或隐式地提供值,则会出现此错误:

stmt = select(table.c.column).where(table.c.id == bindparam("my_param"))

result = conn.execute(stmt)

上述情况下,未为参数 “my_param” 提供任何值。正确的方法是提供一个值:

result = conn.execute(stmt, {"my_param": 12})

当消息采用“在参数组中需要绑定参数的值”的形式时,消息是指“executemany”执行风格。在这种情况下,语句通常是 INSERT、UPDATE 或 DELETE,并且正在传递参数列表。在此格式中,语句可以动态生成,以包括参数列表中提供的每个参数的参数位置,其中它将使用第一组参数来确定这些参数应该是什么。

例如,以下语句是基于第一个参数集计算的,要求参数 “a”、“b” 和 “c” - 这些名称确定语句的最终字符串格式,该格式将用于列表中每个参数集的参数。由于第二个条目不包含 “b”,因此会生成此错误:

m = MetaData()
t = Table("t", m, Column("a", Integer), Column("b", Integer), Column("c", Integer))

e.execute(
    t.insert(),
    [
        {"a": 1, "b": 2, "c": 3},
        {"a": 2, "c": 4},
        {"a": 3, "b": 4, "c": 5},
    ],
)
sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError)
A value is required for bind parameter 'b', in parameter group 1
[SQL: u'INSERT INTO t (a, b, c) VALUES (?, ?, ?)']
[parameters: [{'a': 1, 'c': 3, 'b': 2}, {'a': 2, 'c': 4}, {'a': 3, 'c': 5, 'b': 4}]]

由于“b”是必需的,因此将其传递为 None,以便 INSERT 可以继续进行:

e.execute(
    t.insert(),
    [
        {"a": 1, "b": 2, "c": 3},
        {"a": 2, "b": None, "c": 4},
        {"a": 3, "b": 4, "c": 5},
    ],
)

另请参阅

发送参数

期望 FROM 子句,但是得到了 Select。要创建 FROM 子句,请使用 .subquery() 方法

这指的是 SQLAlchemy 1.4 中的一项更改,根据此更改,由 select() 等函数生成的 SELECT 语句,但也包括联合和文本型 SELECT 表达式,不再被视为 FromClause 对象,而且不能直接放在另一个 SELECT 语句的 FROM 子句中,而必须首先将它们包装在 Subquery 中。这是 Core 中的一个重大概念变更,完整的解释在 不再将 SELECT 语句隐式视为 FROM 子句 中讨论。

举个例子:

m = MetaData()
t = Table("t", m, Column("a", Integer), Column("b", Integer), Column("c", Integer))
stmt = select(t)

在上述中,stmt 表示一个 SELECT 语句。当我们想要直接将 stmt 用作另一个 SELECT 的 FROM 子句时,比如我们试图从中选择时,会产生错误:

new_stmt_1 = select(stmt)

或者,如果我们想在 FROM 子句中使用它,比如在 JOIN 中:

new_stmt_2 = select(some_table).select_from(some_table.join(stmt))

在之前的 SQLAlchemy 版本中,使用一个 SELECT 嵌套在另一个 SELECT 中会产生一个带括号的、未命名的子查询。在大多数情况下,这种 SQL 形式并不是很有用,因为像 MySQL 和 PostgreSQL 这样的数据库要求 FROM 子句中的子查询具有命名别名,这意味着需要使用 SelectBase.alias() 方法,或者从 1.4 开始使用 SelectBase.subquery() 方法来实现这一点。在其他数据库中,为子查询命名仍然更清晰,以解决在子查询内部对列名的未来引用可能产生的任何歧义。

除了上述实际原因外,还有许多其他基于 SQLAlchemy 的原因导致了这一更改的进行。因此,上述两个语句的正确形式要求使用 SelectBase.subquery()

subq = stmt.subquery()

new_stmt_1 = select(subq)

new_stmt_2 = select(some_table).select_from(some_table.join(subq))

另请参阅

不再将 SELECT 语句隐式视为 FROM 子句

自动为原始 clauseelement 生成别名

自 1.4.26 版开始新加入的功能。

此弃用警告是针对非常古老且可能不为人知的模式的,该模式适用于遗留的Query.join()方法以及 2.0 样式 Select.join()方法,其中可以根据relationship()指定连接,但目标是Table或其他映射到类的 Core 可选择对象,而不是 ORM 实体,如映射类或aliased()构造:

a1 = Address.__table__

q = (
    s.query(User)
    .join(a1, User.addresses)
    .filter(Address.email_address == "ed@foo.com")
    .all()
)

以上模式还允许任意可选择对象,例如 Core JoinAlias对象,但是没有此元素的自动适应,这意味着必须直接引用 Core 元素:

a1 = Address.__table__.alias()

q = (
    s.query(User)
    .join(a1, User.addresses)
    .filter(a1.c.email_address == "ed@foo.com")
    .all()
)

指定连接目标的正确方式始终是使用映射类本身或一个aliased对象,后者使用PropComparator.of_type()修饰符来设置别名:

# normal join to relationship entity
q = s.query(User).join(User.addresses).filter(Address.email_address == "ed@foo.com")

# name Address target explicitly, not necessary but legal
q = (
    s.query(User)
    .join(Address, User.addresses)
    .filter(Address.email_address == "ed@foo.com")
)

连接到别名:

from sqlalchemy.orm import aliased

a1 = aliased(Address)

# of_type() form; recommended
q = (
    s.query(User)
    .join(User.addresses.of_type(a1))
    .filter(a1.email_address == "ed@foo.com")
)

# target, onclause form
q = s.query(User).join(a1, User.addresses).filter(a1.email_address == "ed@foo.com")

由于表重叠而自动生成别名

自 1.4.26 版新增。

当使用Select.join()方法或遗留的Query.join()方法查询涉及联合表继承的映射时,通常会生成此警告。问题在于,当在两个共享公共基表的联合继承模型之间进行连接时,如果不对其中一侧应用别名,则无法形成两个实体之间的适当 SQL JOIN;SQLAlchemy 对连接的右侧应用了别名。例如,给定一个联合继承映射:

class Employee(Base):
    __tablename__ = "employee"
    id = Column(Integer, primary_key=True)
    manager_id = Column(ForeignKey("manager.id"))
    name = Column(String(50))
    type = Column(String(50))

    reports_to = relationship("Manager", foreign_keys=manager_id)

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": type,
    }

class Manager(Employee):
    __tablename__ = "manager"
    id = Column(Integer, ForeignKey("employee.id"), primary_key=True)

    __mapper_args__ = {
        "polymorphic_identity": "manager",
        "inherit_condition": id == Employee.id,
    }

以上映射包括EmployeeManager类之间的关系。由于这两个类都使用“employee”数据库表,从 SQL 角度来看,这是一个自引用关系。如果我们想要使用连接从EmployeeManager模型查询,那么在 SQL 级别上,“employee”表需要在查询中出现两次,这意味着必须给它起个别名。当我们使用 SQLAlchemy ORM 创建这样的连接时,得到的 SQL 如下所示:

>>> stmt = select(Employee, Manager).join(Employee.reports_to)
>>> print(stmt)
SELECT  employee.id,  employee.manager_id,  employee.name,
employee.type,  manager_1.id  AS  id_1,  employee_1.id  AS  id_2,
employee_1.manager_id  AS  manager_id_1,  employee_1.name  AS  name_1,
employee_1.type  AS  type_1
FROM  employee  JOIN
(employee  AS  employee_1  JOIN  manager  AS  manager_1  ON  manager_1.id  =  employee_1.id)
ON  manager_1.id  =  employee.manager_id 

在上面,SQL 从 employee 表中选择,表示查询中的 Employee 实体。然后加入到一个右嵌套连接 employee AS employee_1 JOIN manager AS manager_1,其中 employee 表再次出现,但是作为一个匿名别名 employee_1。这就是警告消息所指的‘自动生成别名’。

当 SQLAlchemy 加载包含一个 Employee 和一个 Manager 对象的 ORM 行时,ORM 必须将来自上面的 employee_1manager_1 表别名的行适配到未别名化的 Manager 类中。这个过程内部复杂,并且不能适应所有 API 特性,尤其是当尝试使用比这里显示的更深度嵌套的查询时,如 contains_eager() 等急切加载特性。由于该模式对于更复杂的场景不可靠,并涉及难以预测和遵循的隐式决策,因此会发出警告,并且该模式可能被视为一种传统特性。编写此查询的更好方法是使用适用于任何其他自引用关系的相同模式,即显式使用 aliased() 构造。对于连接继承和其他基于连接的映射,通常希望添加使用 aliased.flat 参数的使用,这将允许通过将别名应用于连接中的各个表来对两个或多个表进行 JOIN,而不是将连接嵌入到新的子查询中:

>>> from sqlalchemy.orm import aliased
>>> manager_alias = aliased(Manager, flat=True)
>>> stmt = select(Employee, manager_alias).join(Employee.reports_to.of_type(manager_alias))
>>> print(stmt)
SELECT  employee.id,  employee.manager_id,  employee.name,
employee.type,  manager_1.id  AS  id_1,  employee_1.id  AS  id_2,
employee_1.manager_id  AS  manager_id_1,  employee_1.name  AS  name_1,
employee_1.type  AS  type_1
FROM  employee  JOIN
(employee  AS  employee_1  JOIN  manager  AS  manager_1  ON  manager_1.id  =  employee_1.id)
ON  manager_1.id  =  employee.manager_id 

如果我们想要使用 contains_eager() 来填充 reports_to 属性,我们引用别名:

>>> stmt = (
...     select(Employee)
...     .join(Employee.reports_to.of_type(manager_alias))
...     .options(contains_eager(Employee.reports_to.of_type(manager_alias)))
... )

在某些更嵌套的情况下,如果 ORM 在非常嵌套的上下文中“自动别名”,则不使用显式 aliased() 对象,contains_eager() 选项没有足够的上下文来知道从哪里获取其数据。因此,最好不要依赖此功能,而是尽可能保持 SQL 构造的显式性。

对象关系映射

IllegalStateChangeError 和并发异常

SQLAlchemy 2.0 引入了一个新系统,描述在会话主动引发非法并发或重入访问时,该系统主动检测在Session对象的单个实例上调用并发方法以及通过扩展AsyncSession代理对象。这些并发访问调用通常,尽管不是专门,会在单个Session实例在多个并发线程之间共享时发生,而没有进行同步访问,或者类似地,当单个AsyncSession实例在多个并发任务之间共享时(例如在使用asyncio.gather()等函数时)。这些使用模式不是这些对象的适当使用方式,如果没有 SQLAlchemy 实现的主动警告系统,仍然会在对象内产生无效状态,导致难以调试的错误,包括数据库连接本身的驱动程序级错误。

SessionAsyncSession的实例是可变的、有状态的对象,没有内置的方法调用同步,并代表一次性数据库事务,一次只能连接一个特定的EngineAsyncEngine(请注意,这些对象都支持同时绑定到多个引擎,但在这种情况下,在事务范围内仍然只会有一个连接与引擎相关)。单个数据库事务不是并发 SQL 命令的适当目标;相反,运行并发数据库操作的应用程序应该使用并发事务。因此,对于这些对象,适当的模式是每个线程一个Session,或每个任务一个AsyncSession

有关并发性的更多背景信息,请参阅会话是否线程安全?AsyncSession 在并发任务中是否安全共享?部分。### 父实例未绑定到会话;(延迟加载/延迟加载/刷新等)操作无法继续

这可能是处理 ORM 时最常见的错误消息,它是由于 ORM 广泛使用的一种称为延迟加载的技术的性质造成的。延迟加载是一种常见的对象关系模式,其中由 ORM 持久化的对象维护与数据库本身的代理,以便当访问对象的各种属性时,可以从数据库中惰性检索它们的值。这种方法的优点是可以从数据库中检索对象而无需一次性加载所有属性或相关数据,而只需在那个时间点传递请求的数据即可。主要缺点基本上是优点的镜像,即如果加载了许多对象,这些对象在所有情况下都需要某组数据,则逐步加载该附加数据是浪费的。

延迟加载的另一个警告是,为了使延迟加载继续进行,对象必须保持与 Session 关联,以便能够检索其状态。此错误消息意味着对象已从其Session中解除关联,并且正在被要求从数据库中惰性加载数据。

对象与其Session分离的最常见原因是会话本身被关闭,通常是通过Session.close()方法。这些对象将继续存在,很常见地在 web 应用程序中被访问,它们被传递到服务器端模板引擎,并被要求加载更多它们无法加载的属性。

减轻此错误的方法是通过以下技术:

  • 尽量不要有分离的对象;不要过早关闭会话 - 通常,应用程序会在将相关对象传递给其他系统之前关闭事务,然后由于此错误而失败。有时事务不需要那么快关闭;一个例子是 web 应用程序在渲染视图之前关闭事务。这通常是以“正确性”的名义来做的,但可能被视为“封装”的错误应用,因为该术语指的是代码组织,而不是实际操作。使用 ORM 对象的模板正在使用代理模式,该模式将数据库逻辑封装在调用者之外。如果Session可以保持打开,直到对象的寿命结束,这是最佳方法。

  • 否则,加载所有所需内容 - 很多时候不可能保持事务开启,特别是在需要将对象传递给无法在相同上下文中运行的其他系统的更复杂的应用程序中。在这种情况下,应用程序应准备处理分离对象,并应尽量适当地使用急切加载来确保对象在一开始就拥有所需内容。

  • 而且,重要的是,将 expire_on_commit 设置为 False - 在使用分离对象时,对象需要重新加载数据的最常见原因是因为它们在上一次调用Session.commit()时被标记为过期。在处理分离对象时不应该使用这种过期机制;因此,Session.expire_on_commit参数应设置为False。通过防止对象在事务外部过期,加载的数据将保持存在,并且在访问数据时不会产生额外的延迟加载。

    还要注意,Session.rollback()方法会无条件地使Session中的所有内容过期,并且在非错误情况下也应该避免使用。

    另请参阅

    关系加载技术 - 关于急切加载和其他基于关系的加载技术的详细文档

    提交 - 有关会话提交的背景信息

    刷新/过期 - 属性过期的背景信息 ### 由于在刷新过程中发生了先前的异常,此会话的事务已被回滚

Session的刷新过程,描述在刷新中,如果遇到错误,将回滚数据库事务,以保持内部一致性。然而,一旦发生这种情况,会话的事务现在是“不活动的”,必须由调用应用程序显式回滚,就像如果没有发生故障,否则需要显式提交一样。

在使用 ORM 时,这是一个常见的错误,通常适用于尚未正确围绕其Session操作进行“框架化”的应用程序。更多详细信息请参阅常见问题解答中的“由于刷新期间的先前异常,此会话的事务已被回滚。”或类似问题。###对于关系,delete-orphan 级联通常仅配置在一对多关系的“一”侧,而不是多对一或多对多关系的“多”侧。

当“delete-orphan”级联设置在多对一或多对多关系上时,会引发此错误,例如:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

    bs = relationship("B", back_populates="a")

class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

    # this will emit the error message when the mapper
    # configuration step occurs
    a = relationship("A", back_populates="bs", cascade="all, delete-orphan")

configure_mappers()

上面的“delete-orphan”设置在B.a上表示的意图是,当每个引用特定AB对象被删除时,那么A也应该被删除。也就是说,它表达了正在被删除的“孤立”将是一个A对象,当每个引用它的B都被删除时,它变成了“孤立”。

“delete-orphan”级联模型不支持此功能。只有在单个对象被删除的情况下才考虑“孤立”问题,这个对象随后会引用零个或多个现在由此单个删除而“孤立”的对象,这将导致这些对象也被删除。换句话说,它只设计用于跟踪基于“父”对象的单个删除而创建“孤立”对象的情况,这是一个自然的情况,即一对多关系中的一个对象的删除会导致“多”侧上的相关项目的后续删除。

支持此功能的上述映射将级联设置放置在一对多的一侧,如下所示:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

    bs = relationship("B", back_populates="a", cascade="all, delete-orphan")

class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

    a = relationship("A", back_populates="bs")

当表达意图时,即当删除一个A时,所有它所指向的B对象也被删除。

错误消息然后继续建议使用relationship.single_parent标志。该标志可用于强制将能够有多个对象引用特定对象的关系实际上只有一个对象在某一时间引用它。它用于遗留或其他不太理想的数据库模式,在这些模式中,外键关系表明存在“多”集合,但实际上在任何时间只有一个对象会引用给定目标对象。可以通过上述示例来演示这种不常见的情况如下:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

    bs = relationship("B", back_populates="a")

class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

    a = relationship(
        "A",
        back_populates="bs",
        single_parent=True,
        cascade="all, delete-orphan",
    )

上述配置将安装一个验证器,该验证器将强制执行在B.a关系的范围内只能将一个B与一个A关联起来的规则:

>>> b1 = B()
>>> b2 = B()
>>> a1 = A()
>>> b1.a = a1
>>> b2.a = a1
sqlalchemy.exc.InvalidRequestError: Instance <A at 0x7eff44359350> is
already associated with an instance of <class '__main__.B'> via its
B.a attribute, and is only allowed a single parent.

请注意,此验证器的范围有限,并且无法阻止通过其他方向创建多个“父”对象。例如,它不会检测到与A.bs相同的设置:

>>> a1.bs = [b1, b2]
>>> session.add_all([a1, b1, b2])
>>> session.commit()
INSERT  INTO  a  DEFAULT  VALUES
()
INSERT  INTO  b  (a_id)  VALUES  (?)
(1,)
INSERT  INTO  b  (a_id)  VALUES  (?)
(1,) 

然而,后续事情将不会如预期那样进行,因为“delete-orphan”级联将继续按照单个主要对象的术语工作,这意味着如果我们删除任一B对象,A将被删除。另一个B仍然存在,虽然 ORM 通常足够聪明以将外键属性设置为 NULL,但这通常不是所期望的:

>>> session.delete(b1)
>>> session.commit()
UPDATE  b  SET  a_id=?  WHERE  b.id  =  ?
(None,  2)
DELETE  FROM  b  WHERE  b.id  =  ?
(1,)
DELETE  FROM  a  WHERE  a.id  =  ?
(1,)
COMMIT 

对于上述所有示例,类似的逻辑适用于多对多关系的计算;如果多对多关系在一侧设置了 single_parent=True,则该侧可以使用“delete-orphan”级联,但这几乎不可能是某人实际想要的,因为多对多关系的目的是可以有许多对象引用任一方向的对象。

总的来说,“delete-orphan”级联通常应用于一对多关系的“一”侧,以便删除“多”侧的对象,而不是相反。

从版本 1.3.18 开始更改:当在一对多或多对多关系上使用“delete-orphan”时,错误消息的文本已更新为更具描述性。

另请参阅

级联

delete-orphan

实例 已通过其 属性与实例 关联,且仅允许有一个父实例。 ### 实例 已通过其 属性与实例 关联,且仅允许有一个父实例。

当使用relationship.single_parent标志,并且一次分配多个对象作为对象的“父”时,会发出此错误。

给定以下映射:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

    a = relationship(
        "A",
        single_parent=True,
        cascade="all, delete-orphan",
    )

意图指示一次最多只能有一个B对象引用特定的A对象:

>>> b1 = B()
>>> b2 = B()
>>> a1 = A()
>>> b1.a = a1
>>> b2.a = a1
sqlalchemy.exc.InvalidRequestError: Instance <A at 0x7eff44359350> is
already associated with an instance of <class '__main__.B'> via its
B.a attribute, and is only allowed a single parent.

当此错误意外发生时,通常是因为在响应于对于关系 ,delete-orphan 级联通常仅在一对多关系的“一”侧上配置,而不在多对一或多对多关系的“多”侧上配置。描述的错误消息时,应用了relationship.single_parent标志,而实际问题是对“delete-orphan”级联设置的误解。有关详细信息,请参阅该消息。

另请参阅

对于关系,删除孤立节点级联通常仅在一对多关系的“一”侧上配置,并不在多对一或多对多关系的“多”侧上配置。 ### 关系 X 将列 Q 复制到列 P,与关系‘Y’存在冲突。

此警告是指当两个或更多关系在 flush 时将数据写入相同列,但 ORM 没有任何协调这些关系的方式时发生的情况。根据具体情况,解决方案可能是两个关系需要使用relationship.back_populates相互引用,或者一个或多个关系应该配置为relationship.viewonly以防止冲突写入,或者有时配置是完全有意为之的,并应该配置relationship.overlaps来抑制每个警告。

对于典型示例,缺少relationship.back_populates的情况,给定以下映射:

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)
    children = relationship("Child")

class Child(Base):
    __tablename__ = "child"
    id = Column(Integer, primary_key=True)
    parent_id = Column(ForeignKey("parent.id"))
    parent = relationship("Parent")

上述映射将生成警告:

SAWarning: relationship 'Child.parent' will copy column parent.id to column child.parent_id,
which conflicts with relationship(s): 'Parent.children' (copies parent.id to child.parent_id).

关系Child.parentParent.children似乎存在冲突。解决方案是应用relationship.back_populates

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)
    children = relationship("Child", back_populates="parent")

class Child(Base):
    __tablename__ = "child"
    id = Column(Integer, primary_key=True)
    parent_id = Column(ForeignKey("parent.id"))
    parent = relationship("Parent", back_populates="children")

对于更加定制化的关系,在“重叠”情况可能是有意为之且无法解决时,relationship.overlaps参数可以指定不应该触发警告的关系名称。这通常发生在两个或更多关系指向相同基础表的情况下,这些关系包括自定义的relationship.primaryjoin条件,限制了每种情况下的相关项:

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)
    c1 = relationship(
        "Child",
        primaryjoin="and_(Parent.id == Child.parent_id, Child.flag == 0)",
        backref="parent",
        overlaps="c2, parent",
    )
    c2 = relationship(
        "Child",
        primaryjoin="and_(Parent.id == Child.parent_id, Child.flag == 1)",
        overlaps="c1, parent",
    )

class Child(Base):
    __tablename__ = "child"
    id = Column(Integer, primary_key=True)
    parent_id = Column(ForeignKey("parent.id"))

    flag = Column(Integer)

在上述情况下,ORM 将知道Parent.c1Parent.c2Child.parent之间的重叠是有意为之的。### 对象无法转换为“持久”状态,因为此标识映射不再有效。

新版本 1.4.26 中新增。

此消息添加是为了适应以下情况:在原始Session关闭后或者已调用其Session.expunge_all()方法后,迭代将产生 ORM 对象的Result对象。当一个Session一次性清除所有对象时,该Session使用的内部身份映射将被替换为一个新的,并且原始的将被丢弃。一个未消耗且未缓冲的Result对象将在内部保持对该现在已丢弃的身份映射的引用。因此,当消耗Result时,将要产生的对象无法与该Session相关联。这种安排是有意设计的,因为通常不建议在创建它的事务上下文之外迭代未缓冲的Result对象:

# context manager creates new Session
with Session(engine) as session_obj:
    result = sess.execute(select(User).where(User.id == 7))

# context manager is closed, so session_obj above is closed, identity
# map is replaced

# iterating the result object can't associate the object with the
# Session, raises this error.
user = result.first()

使用asyncio ORM 扩展时,通常不会发生上述情况,因为当AsyncSession返回一个同步风格的Result时,结果在语句执行时已经被预先缓冲。这样做是为了允许次级的急切加载器在不需要额外的await调用的情况下调用。

在上述情况下使用常规Session来预缓冲结果,可以像asyncio扩展一样使用prebuffer_rows执行选项,如下所示:

# context manager creates new Session
with Session(engine) as session_obj:
    # result internally pre-fetches all objects
    result = sess.execute(
        select(User).where(User.id == 7), execution_options={"prebuffer_rows": True}
    )

# context manager is closed, so session_obj above is closed, identity
# map is replaced

# pre-buffered objects are returned
user = result.first()

# however they are detached from the session, which has been closed
assert inspect(user).detached
assert inspect(user).session is None

在上面,所选的 ORM 对象完全在session_obj块中生成,与session_obj关联并在Result对象中缓冲以进行迭代。在块外,session_obj被关闭并且清除这些 ORM 对象。迭代Result对象将产生这些 ORM 对象,但是由于它们的来源Session已将它们清除,它们将以分离状态交付。

注意

上面提到的 “预缓冲” vs. “非缓冲” Result 对象是指 ORM 将来自 DBAPI 的传入原始数据库行转换为 ORM 对象的过程。它不意味着底层的 cursor 对象本身,它表示来自 DBAPI 的待处理结果,是缓冲的还是非缓冲的,因为这实际上是一个更低层的缓冲。有关缓冲 cursor 结果本身的背景,请参阅 使用服务器端游标(也称为流式结果) 部分。 ### 无法解释注解式声明表形式的类型注解

SQLAlchemy 2.0 引入了一种新的注解式声明表声明系统,它从类定义中的 PEP 484 注解在运行时派生 ORM 映射属性信息。这种形式的要求是,所有的 ORM 注解都必须使用一个称为 Mapped 的通用容器才能正确注解。包括显式 PEP 484 类型注解的传统 SQLAlchemy 映射,例如使用 旧版 Mypy 扩展 进行类型支持的映射,可能包含诸如 relationship() 之类的指令,这些指令不包括这个通用容器。

要解决此问题,可以在类中添加 __allow_unmapped__ 布尔属性,直到它们可以完全迁移到 2.0 语法。参见 迁移到 2.0 步骤六 - 为明确定义的 ORM 模型添加 allow_unmapped 的迁移说明中的示例。

另请参阅

迁移到 2.0 步骤六 - 为明确定义的 ORM 模型添加 allow_unmapped - 在 SQLAlchemy 2.0 - 主要迁移指南 文档中 ### 当将 转换为数据类时,属性(s) 来自不是数据类的超类

当使用在 声明式数据类映射 中描述的 SQLAlchemy ORM 映射数据类功能与任何未本身声明为数据类的 mixin 类或抽象基类一起使用时(例如下面的示例)会出现此警告:

from __future__ import annotations

import inspect
from typing import Optional
from uuid import uuid4

from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass

class Mixin:
    create_user: Mapped[int] = mapped_column()
    update_user: Mapped[Optional[int]] = mapped_column(default=None, init=False)

class Base(DeclarativeBase, MappedAsDataclass):
    pass

class User(Base, Mixin):
    __tablename__ = "sys_user"

    uid: Mapped[str] = mapped_column(
        String(50), init=False, default_factory=uuid4, primary_key=True
    )
    username: Mapped[str] = mapped_column()
    email: Mapped[str] = mapped_column()

由于 Mixin 本身不扩展自 MappedAsDataclass,因此会生成以下警告:

SADeprecationWarning: When transforming <class '__main__.User'> to a
dataclass, attribute(s) "create_user", "update_user" originates from
superclass <class
'__main__.Mixin'>, which is not a dataclass. This usage is deprecated and
will raise an error in SQLAlchemy 2.1\. When declaring SQLAlchemy
Declarative Dataclasses, ensure that all mixin classes and other
superclasses which include attributes are also a subclass of
MappedAsDataclass.

解决方法是在 Mixin 的签名中也添加 MappedAsDataclass

class Mixin(MappedAsDataclass):
    create_user: Mapped[int] = mapped_column()
    update_user: Mapped[Optional[int]] = mapped_column(default=None, init=False)

Python 的 PEP 681 规范不适用于声明在不是 dataclasses 的 dataclasses 超类上的属性;根据 Python dataclasses 的行为,这样的字段将被忽略,如以下示例所示:

from dataclasses import dataclass
from dataclasses import field
import inspect
from typing import Optional
from uuid import uuid4

class Mixin:
    create_user: int
    update_user: Optional[int] = field(default=None)

@dataclass
class User(Mixin):
    uid: str = field(init=False, default_factory=lambda: str(uuid4()))
    username: str
    password: str
    email: str

上述 User 类将不会在其构造函数中包含 create_user,也不会尝试将 update_user 解释为 dataclass 属性。这是因为 Mixin 不是一个 dataclass。

SQLAlchemy 2.0 系列中的 dataclasses 功能未正确遵守此行为;相反,非 dataclass 混合类和超类上的属性被视为最终 dataclass 配置的一部分。然而,像 Pyright 和 Mypy 这样的类型检查器不会将这些字段视为 dataclass 构造函数的一部分,因为根据 PEP 681,它们应该被忽略。由于否则存在歧义,因此 SQLAlchemy 2.1 将要求在 dataclass 层次结构中具有 SQLAlchemy 映射属性的混合类本身必须是 dataclasses。 ### 创建 的 dataclass 时遇到的 Python dataclasses 错误

当使用 MappedAsDataclass 混合类或 registry.mapped_as_dataclass() 装饰器时,SQLAlchemy 使用实际的 Python dataclasses 模块,该模块位于 Python 标准库中,以将 dataclass 行为应用于目标类。此 API 具有自己的错误场景,其中大部分涉及在用户定义的类上构建 __init__() 方法;在类上声明的属性的顺序,以及在超类上的顺序决定了 __init__() 方法将如何构建,并且有特定规则规定了属性的组织方式以及它们应该如何使用参数,如 init=Falsekw_only=True 等。SQLAlchemy 不控制或实现这些规则。因此,对于这种类型的错误,请参考 Python dataclasses 文档,特别注意应用于继承的规则。

另请参阅

声明性 Dataclass 映射 - SQLAlchemy dataclasses 文档

Python dataclasses - 在 python.org 网站上

继承 - 在 python.org 网站上

在使用 ORM 通过主键进行批量更新功能时,如果在给定的记录中没有提供主键值,则会出现此错误,例如:

>>> session.execute(
...     update(User).where(User.name == bindparam("u_name")),
...     [
...         {"u_name": "spongebob", "fullname": "Spongebob Squarepants"},
...         {"u_name": "patrick", "fullname": "Patrick Star"},
...     ],
... )

上述情况下,参数字典列表的存在结合使用Session执行 ORM 启用的 UPDATE 语句将自动使用 ORM 通过主键进行批量更新,该批量更新期望参数字典包括主键值,例如:

>>> session.execute(
...     update(User),
...     [
...         {"id": 1, "fullname": "Spongebob Squarepants"},
...         {"id": 3, "fullname": "Patrick Star"},
...         {"id": 5, "fullname": "Eugene H. Krabs"},
...     ],
... )

要在不提供每个记录的主键值的情况下调用 UPDATE 语句,请使用Session.connection()来获取当前的Connection,然后使用它调用:

>>> session.connection().execute(
...     update(User).where(User.name == bindparam("u_name")),
...     [
...         {"u_name": "spongebob", "fullname": "Spongebob Squarepants"},
...         {"u_name": "patrick", "fullname": "Patrick Star"},
...     ],
... )

另请参阅

ORM 通过主键进行批量更新

禁用通过主键进行批量 ORM 更新以使用多个参数集的 UPDATE 语句 ### 非法状态更改错误和并发异常

SQLAlchemy 2.0 引入了一个新系统,描述在检测到非法并发或重新进入访问时,会主动引发会话,该系统主动检测在Session对象的个别实例上以及通过扩展AsyncSession代理对象调用并发方法时的情况。这些并发访问调用通常,但不仅仅,会发生在单个Session实例在多个并发线程之间共享时,而没有同步这样的访问,或者类似地,当单个AsyncSession实例在多个并发任务之间共享时(例如使用asyncio.gather()函数)。这些使用模式不是这些对象的适当使用方式,如果没有 SQLAlchemy 实现的主动警告系统,否则仍然会在对象内部产生无效状态,从而产生难以调试的错误,包括在数据库连接本身上的驱动程序级错误。

SessionAsyncSession的实例是可变的、有状态的对象,没有内置的方法调用同步,并且代表一次单一的持续数据库事务,一次只能在一个特定的EngineAsyncEngine上绑定的数据库连接(请注意,这些对象都支持同时绑定到多个引擎,但在这种情况下,在事务范围内仍然只有一个连接在运行)。单个数据库事务不是并发 SQL 命令的适当目标;相反,运行并发数据库操作的应用程序应该使用并发事务。因此,对于这些对象,适当的模式是每个线程一个Session,或每个任务一个AsyncSession

有关并发性的更多背景信息,请参阅会话是否线程安全?AsyncSession 是否安全可在并发任务中共享?部分。

父实例 未绑定到会话;(延迟加载/延迟加载/刷新等)操作无法继续

这很可能是处理 ORM 时最常见的错误消息,它是由 ORM 广泛使用的一种技术的性质导致的,这种技术被称为延迟加载。延迟加载是一种常见的对象关系模式,其中由 ORM 持久化的对象维护一个代理到数据库本身,因此当访问对象上的各种属性时,它们的值可能会被惰性地从数据库中检索出来。这种方法的优势在于可以从数据库中检索对象,而无需一次加载所有属性或相关数据,而只需在请求时传递所需的数据。主要的缺点基本上是优势的镜像,即如果正在加载许多需要在所有情况下都需要一组数据的对象,逐步加载额外数据是浪费的。

延迟加载的另一个警告是,为了使延迟加载继续进行,对象必须保持与会话关联,以便能够检索其状态。此错误消息意味着一个对象已经与其Session解除关联,并且正在被要求从数据库中延迟加载数据。

对象从其 Session 分离的最常见原因是会话本身被关闭,通常是通过 Session.close() 方法。然后,这些对象将继续存在,被进一步访问,往往是在 Web 应用程序中,在那里它们被传递给服务器端模板引擎,并要求获取它们无法加载的进一步属性。

对这个错误的缓解是通过这些技术:

  • 尽量避免分离对象;不要过早关闭会话 - 通常,应用程序会在将相关对象传递给其他系统之前关闭事务,但由于这个错误而失败。有时,事务不需要那么快关闭;一个例子是 Web 应用在视图呈现之前关闭事务。这通常是以“正确性”的名义而完成的,但可能被视为对“封装”的错误应用,因为此术语指的是代码组织,而不是实际操作。使用 ORM 对象的模板正在使用代理模式来保持数据库逻辑与调用者的封装。如果Session可以保持打开状态,直到对象的生命周期结束,这是最佳方法。

  • 否则,加载所有需要的内容 - 很多时候是不可能保持事务处于打开状态的,特别是在需要将对象传递给其他系统的更复杂的应用程序中,即使它们在同一个进程中也无法运行在相同的上下文中。在这种情况下,应用程序应准备处理分离的对象,并应尽量适当地使用急切加载以确保对象从一开始就拥有所需内容。

  • 而且,重要的是,将 expire_on_commit 设置为 False - 当使用分离对象时,对象需要重新加载数据的最常见原因是因为它们从上一次调用 Session.commit() 被标记为过期。在处理分离对象时不应使用此过期;因此应将 Session.expire_on_commit 参数设置为False。通过防止对象在事务外部过期,已加载的数据将保持存在,并且在访问该数据时不会产生额外的延迟加载。

    Session.rollback() 方法会无条件地使 Session 中的所有内容过期,因此在非错误情况下也应避免使用。

    另请参阅

    关系加载技术 - 关于急加载和其他面向关系的加载技术的详细文档

    提交 - 会话提交的背景介绍

    刷新/过期 - 属性过期的背景介绍

由于刷新期间的先前异常,此会话的事务已回滚

Session 的刷新过程在遇到错误时会回滚数据库事务,以保持内部一致性。然而,一旦发生这种情况,会话的事务现在处于 “不活动” 状态,并且必须由调用应用程序显式地回滚,就像如果没有发生故障时需要显式提交一样。

当使用 ORM 时,这是一个常见的错误,通常适用于尚未在其 Session 操作周围正确设置 “框架”的应用程序。更多详细信息请参阅“由于刷新期间的先前异常,此会话的事务已回滚。”(或类似内容)的常见问题。

对于关系 ,只有在一对多关系的“一”端才通常配置了 delete-orphan 级联,而不是在多对一或多对多关系的“多”端。

当在多对一或多对多关系上设置了 “delete-orphan” 级联 时会出现此错误,例如:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

    bs = relationship("B", back_populates="a")

class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

    # this will emit the error message when the mapper
    # configuration step occurs
    a = relationship("A", back_populates="bs", cascade="all, delete-orphan")

configure_mappers()

上面的 B.a 上的 “delete-orphan” 设置表明了这样一个意图,即当指向特定 A 的每个 B 对象都被删除时,该 A 也应该被删除。也就是说,它表达了被删除的 “孤立” 对象将是一个 A 对象,并且当指向它的每个 B 都被删除时,它就成为了一个 “孤立” 对象。

“delete-orphan”级联模型不支持此功能。 “孤儿”考虑仅在单个对象的删除方面进行,然后引用零个或多个由此单个删除“孤儿”对象的对象,这将导致这些对象也被删除。换句话说,它仅设计为基于删除每个孤儿的一个且仅一个“父”对象的创建,“父”对象在一对多关系中的自然情况下导致“多”侧的相关项目随后被删除。

为支持此功能的上述映射将在一对多关系的一侧放置级联设置,如下所示:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

    bs = relationship("B", back_populates="a", cascade="all, delete-orphan")

class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

    a = relationship("A", back_populates="bs")

当表达出当删除一个A时,所有它所引用的B对象也将被删除的意图时。

错误消息随后建议使用relationship.single_parent标志。此标志可用于强制执行一个关系,该关系可以让多个对象引用特定对象,但实际上一次只能有一个对象引用它。它用于传统或其他不太理想的数据库模式,其中外键关系暗示“多”集合,但实际上只有一个对象会引用给定目标对象。这种不常见的情况可以如上例所示进行演示:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

    bs = relationship("B", back_populates="a")

class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

    a = relationship(
        "A",
        back_populates="bs",
        single_parent=True,
        cascade="all, delete-orphan",
    )

上述配置将安装一个验证器,该验证器将强制执行在B.a关系的范围内一次只能关联一个B与一个A

>>> b1 = B()
>>> b2 = B()
>>> a1 = A()
>>> b1.a = a1
>>> b2.a = a1
sqlalchemy.exc.InvalidRequestError: Instance <A at 0x7eff44359350> is
already associated with an instance of <class '__main__.B'> via its
B.a attribute, and is only allowed a single parent.

请注意,此验证器的范围有限,并不会阻止通过其他方向创建多个“父级”。例如,它不会检测到关于A.bs的相同设置:

>>> a1.bs = [b1, b2]
>>> session.add_all([a1, b1, b2])
>>> session.commit()
INSERT  INTO  a  DEFAULT  VALUES
()
INSERT  INTO  b  (a_id)  VALUES  (?)
(1,)
INSERT  INTO  b  (a_id)  VALUES  (?)
(1,) 

然而,事情不会按预期进行,因为“delete-orphan”级联将继续按照单个主要对象的方式工作,这意味着如果我们删除其中一个B对象,A将被删除。另一个B仍然存在,ORM 通常会足够聪明地将外键属性设置为 NULL,但这通常不是期望的结果:

>>> session.delete(b1)
>>> session.commit()
UPDATE  b  SET  a_id=?  WHERE  b.id  =  ?
(None,  2)
DELETE  FROM  b  WHERE  b.id  =  ?
(1,)
DELETE  FROM  a  WHERE  a.id  =  ?
(1,)
COMMIT 

对于上述所有示例,类似的逻辑也适用于多对多关系的计算;如果多对多关系在一侧设置了 single_parent=True,则该侧可以使用“delete-orphan”级联,但这很不可能是某人实际想要的,因为多对多关系的目的是让可以有许多对象相互引用。

通常,“delete-orphan”级联通常应用于一对多关系的“一”侧,以便删除“多”侧的对象,而不是相反。

1.3.18 版本中的更改:当在多对一或多对多关系上使用“delete-orphan”时,错误消息的文本已更新为更详细的描述。

另请参阅

级联

delete-orphan

实例已通过其属性与的实例关联,并且只允许一个父级。

实例已通过其属性与的实例关联,并且只允许一个父级。

当使用relationship.single_parent标志,并且同时为一个对象分配了多个“父级”对象时,会发出此错误。

给定以下映射:

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

    a = relationship(
        "A",
        single_parent=True,
        cascade="all, delete-orphan",
    )

意图指示不超过一个B对象可以同时引用特定的A对象:

>>> b1 = B()
>>> b2 = B()
>>> a1 = A()
>>> b1.a = a1
>>> b2.a = a1
sqlalchemy.exc.InvalidRequestError: Instance <A at 0x7eff44359350> is
already associated with an instance of <class '__main__.B'> via its
B.a attribute, and is only allowed a single parent.

当这种错误出现时,通常是因为在错误消息中描述的错误消息响应中应用了relationship.single_parent标志,实际上问题是对“delete-orphan”级联设置的误解。请参阅该消息以了解详情。

另请参阅

对于关系,delete-orphan 级联通常仅在一对多关系的“one”端上配置,并且不在多对一或多对多关系的“many”端上配置。

关系 X 将列 Q 复制到列 P,与关系‘Y’冲突

此警告是指当两个或更多关系将数据写入相同的列时,但 ORM 没有任何协调这些关系的方式时。根据具体情况,解决方案可能是两个关系需要彼此引用,使用relationship.back_populates,或者一个或多个关系应该配置为relationship.viewonly以防止冲突的写入,有时配置是完全有意的,应该配置relationship.overlaps以使每个警告静音。

对于典型的缺少relationship.back_populates的示例,给定以下映射:

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)
    children = relationship("Child")

class Child(Base):
    __tablename__ = "child"
    id = Column(Integer, primary_key=True)
    parent_id = Column(ForeignKey("parent.id"))
    parent = relationship("Parent")

上述映射将生成警告:

SAWarning: relationship 'Child.parent' will copy column parent.id to column child.parent_id,
which conflicts with relationship(s): 'Parent.children' (copies parent.id to child.parent_id).

关系Child.parentParent.children似乎存在冲突。解决方案是应用relationship.back_populates

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)
    children = relationship("Child", back_populates="parent")

class Child(Base):
    __tablename__ = "child"
    id = Column(Integer, primary_key=True)
    parent_id = Column(ForeignKey("parent.id"))
    parent = relationship("Parent", back_populates="children")

对于更自定义的关系,其中“重叠”情况可能是有意的并且无法解决的情况,relationship.overlaps参数可以指定不应触发警告的关系名称。这通常发生在对同一基础表的两个或多个关系中,这些关系包括限制每种情况中相关项的自定义relationship.primaryjoin条件:

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)
    c1 = relationship(
        "Child",
        primaryjoin="and_(Parent.id == Child.parent_id, Child.flag == 0)",
        backref="parent",
        overlaps="c2, parent",
    )
    c2 = relationship(
        "Child",
        primaryjoin="and_(Parent.id == Child.parent_id, Child.flag == 1)",
        overlaps="c1, parent",
    )

class Child(Base):
    __tablename__ = "child"
    id = Column(Integer, primary_key=True)
    parent_id = Column(ForeignKey("parent.id"))

    flag = Column(Integer)

在上述情况下,ORM 将知道Parent.c1Parent.c2Child.parent之间的重叠是有意的。

对象无法转换为‘持久’状态,因为此标识映射不再有效。

自版本 1.4.26 新增。

添加此消息是为了适应以下情况:当迭代一个在原始Session关闭后或在其上调用Session.expunge_all()方法后仍会产生 ORM 对象的Result对象时。当一个Session一次性删除所有对象时,该Session使用的内部标识映射将被替换为新的,并且原始映射将被丢弃。一个未使用且未缓冲的Result对象将在内部维护对该现在被丢弃的标识映射的引用。因此,当消耗了Result时,将要产生的对象无法与该Session关联。这种安排是有意设计的,因为通常不建议在创建它的事务上下文之外迭代未缓冲的Result对象:

# context manager creates new Session
with Session(engine) as session_obj:
    result = sess.execute(select(User).where(User.id == 7))

# context manager is closed, so session_obj above is closed, identity
# map is replaced

# iterating the result object can't associate the object with the
# Session, raises this error.
user = result.first()

使用 asyncio ORM 扩展时,通常不会出现上述情况,因为当 AsyncSession 返回一个同步风格的 Result 时,结果在语句执行时已经被预先缓冲。这样做是为了允许次级急切加载器在不需要额外的 await 调用的情况下调用。

若要在上述情况下像 asyncio 扩展一样预先缓冲结果,可以使用 prebuffer_rows 执行选项如下所示:

# context manager creates new Session
with Session(engine) as session_obj:
    # result internally pre-fetches all objects
    result = sess.execute(
        select(User).where(User.id == 7), execution_options={"prebuffer_rows": True}
    )

# context manager is closed, so session_obj above is closed, identity
# map is replaced

# pre-buffered objects are returned
user = result.first()

# however they are detached from the session, which has been closed
assert inspect(user).detached
assert inspect(user).session is None

在上面的例子中,所选的 ORM 对象完全在 session_obj 块内生成,与 session_obj 关联并在 Result 对象内缓冲以供迭代。在块外,session_obj 被关闭并清除这些 ORM 对象。迭代 Result 对象将产生这些 ORM 对象,但是由于它们的来源 Session 已将它们清除,它们将以 分离 状态传递。

注意

上文提到的“预缓冲”与“未缓冲”的 Result 对象指的是 ORM 将传入的原始数据库行从 DBAPI 转换为 ORM 对象的过程。这并不意味着底层的 cursor 对象本身,它代表了来自 DBAPI 的待处理结果,是缓冲的还是非缓冲的,因为这本质上是一个更低层次的缓冲。有关 cursor 结果本身的缓冲背景,请参阅 使用服务器端游标 (即流式结果) 部分。

无法解释注释的声明式表格形式的类型注释

SQLAlchemy 2.0 引入了一个新的 注释式声明表 声明系统,它会在运行时从类定义中的 PEP 484 注释中派生 ORM 映射属性信息。这种形式的要求是,所有 ORM 注释都必须使用一个名为Mapped的通用容器才能正确注释。包含显式 PEP 484 类型注释的传统 SQLAlchemy 映射,例如那些使用 传统 Mypy 扩展 进行类型支持的映射,可能包含不包括此通用容器的诸如relationship()之类的指令。

要解决此问题,可以将类标记为__allow_unmapped__布尔属性,直到它们可以完全迁移到 2.0 语法。请参阅迁移到 2.0 第六步 - 向显式类型化的 ORM 模型添加 allow_unmapped 的迁移说明以获取示例。

另请参阅

迁移到 2.0 第六步 - 向显式类型化的 ORM 模型添加 allow_unmapped - 在 SQLAlchemy 2.0 - 主要迁移指南 文档中

当将转换为数据类时,属性源自于不是数据类的超类

当与任何不是自身声明为数据类的混入类或抽象基类一起使用 SQLAlchemy ORM 映射数据类功能时,会出现此警告,例如下面的示例:

from __future__ import annotations

import inspect
from typing import Optional
from uuid import uuid4

from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass

class Mixin:
    create_user: Mapped[int] = mapped_column()
    update_user: Mapped[Optional[int]] = mapped_column(default=None, init=False)

class Base(DeclarativeBase, MappedAsDataclass):
    pass

class User(Base, Mixin):
    __tablename__ = "sys_user"

    uid: Mapped[str] = mapped_column(
        String(50), init=False, default_factory=uuid4, primary_key=True
    )
    username: Mapped[str] = mapped_column()
    email: Mapped[str] = mapped_column()

在上述情况下,由于Mixin本身不是扩展自MappedAsDataclass,因此会生成以下警告:

SADeprecationWarning: When transforming <class '__main__.User'> to a
dataclass, attribute(s) "create_user", "update_user" originates from
superclass <class
'__main__.Mixin'>, which is not a dataclass. This usage is deprecated and
will raise an error in SQLAlchemy 2.1\. When declaring SQLAlchemy
Declarative Dataclasses, ensure that all mixin classes and other
superclasses which include attributes are also a subclass of
MappedAsDataclass.

修复方法是在Mixin的签名中也添加MappedAsDataclass:

class Mixin(MappedAsDataclass):
    create_user: Mapped[int] = mapped_column()
    update_user: Mapped[Optional[int]] = mapped_column(default=None, init=False)

Python 的 PEP 681 规范不包含不是数据类本身的数据类超类上声明的属性; 根据 Python 数据类的行为,这些字段会被忽略,如下例所示:

from dataclasses import dataclass
from dataclasses import field
import inspect
from typing import Optional
from uuid import uuid4

class Mixin:
    create_user: int
    update_user: Optional[int] = field(default=None)

@dataclass
class User(Mixin):
    uid: str = field(init=False, default_factory=lambda: str(uuid4()))
    username: str
    password: str
    email: str

在上述情况下,User类将不会在其构造函数中包含create_user,也不会尝试将update_user解释为数据类属性。这是因为Mixin不是数据类。

SQLAlchemy 2.0 系列中的数据类功能未正确遵守此行为;相反,非数据类混合类和超类上的属性被视为最终数据类配置的一部分。但是像 Pyright 和 Mypy 这样的类型检查器不会将这些字段视为数据类构造函数的一部分,因为根据PEP 681,它们应该被忽略。由于否则它们的存在是模棱两可的,因此 SQLAlchemy 2.1 将要求在数据类层次结构中具有 SQLAlchemy 映射属性的混合类本身必须是数据类。

创建类时遇到的 Python 数据类错误

当使用MappedAsDataclass混合类或registry.mapped_as_dataclass()装饰器时,SQLAlchemy 利用 Python 标准库中实际的Python 数据类模块,以将数据类行为应用于目标类。此 API 具有自己的错误场景,其中大多数涉及在用户定义的类上构建__init__()方法;在类上声明的属性的顺序,以及在超类上声明的属性,决定了__init__()方法将如何构建,并且有特定规则规定了属性的组织方式以及它们应如何使用参数,如init=Falsekw_only=True等。SQLAlchemy 不控制或实现这些规则。因此,对于这种类型的错误,请参阅Python 数据类文档,特别注意应用于继承的规则。

另请参阅

声明式数据类映射 - SQLAlchemy 数据类文档

Python 数据类 - 在 python.org 网站上

继承 - 在 python.org 网站上

按主键进行每行 ORM 批量更新要求记录包含主键值

当在给定记录中使用 ORM 按主键批量更新功能而未提供主键值时,将出现此错误,例如:

>>> session.execute(
...     update(User).where(User.name == bindparam("u_name")),
...     [
...         {"u_name": "spongebob", "fullname": "Spongebob Squarepants"},
...         {"u_name": "patrick", "fullname": "Patrick Star"},
...     ],
... )

上述,将参数字典列表与使用Session执行 ORM 启用的 UPDATE 语句结合使用将自动使用按主键进行 ORM 批量更新,该功能期望参数字典包含主键值,例如:

>>> session.execute(
...     update(User),
...     [
...         {"id": 1, "fullname": "Spongebob Squarepants"},
...         {"id": 3, "fullname": "Patrick Star"},
...         {"id": 5, "fullname": "Eugene H. Krabs"},
...     ],
... )

若要在不提供每条记录主键值的情况下调用 UPDATE 语句,请使用 Session.connection() 获取当前的 Connection,然后使用它进行调用:

>>> session.connection().execute(
...     update(User).where(User.name == bindparam("u_name")),
...     [
...         {"u_name": "spongebob", "fullname": "Spongebob Squarepants"},
...         {"u_name": "patrick", "fullname": "Patrick Star"},
...     ],
... )

另请参阅

按主键进行 ORM 批量更新

禁用批量 ORM 按主键更新,以及包含多个参数集的 UPDATE 语句

AsyncIO 异常

等待所需

SQLAlchemy 异步模式要求使用异步驱动程序连接到数据库。尝试使用不兼容的 DBAPI 的异步版本与 SQLAlchemy 的异步版本一起使用时通常会引发此错误。

另请参阅

异步 I/O (asyncio) ### 缺少 Greenlet

在没有创建 SQLAlchemy AsyncIO 代理类设置的协程生成上下文之外启动异步 DBAPI 调用时,通常会引发此错误。通常情况下,当在意外位置尝试进行 IO 操作时,使用了不直接提供 await 关键字的调用模式时会发生此错误。在使用 ORM 时,几乎总是由于使用了延迟加载,这在 asyncio 中不直接支持,需要采取额外步骤和/或替代加载器模式才能成功使用。

另请参阅

使用 AsyncSession 时防止隐式 IO - 涵盖了大多数可能发生此问题的 ORM 方案以及如何缓解这个问题,包括在懒加载情况下使用的特定模式。 ### 无可用检查

当前不支持直接在 AsyncConnectionAsyncEngine 对象上直接使用 inspect() 函数,因为尚未提供 Inspector 对象的可等待形式。相反,该对象是通过使用 inspect() 函数获取的,以一种方式,使其引用 AsyncConnection 对象的底层 AsyncConnection.sync_connection 属性;然后,通过使用 AsyncConnection.run_sync() 方法以及执行所需操作的自定义函数,以“同步”调用样式使用 Inspector

async def async_main():
    async with engine.connect() as conn:
        tables = await conn.run_sync(
            lambda sync_conn: inspect(sync_conn).get_table_names()
        )

另请参阅

使用 Inspector 检查模式对象 - 使用 asyncio 扩展的 inspect() 的附加示例。### 必须等待

SQLAlchemy 的异步模式需要使用异步驱动程序连接到数据库。当尝试使用不兼容的 DBAPI 时,通常会引发此错误。

另请参阅

异步 I/O (asyncio)

缺少 Greenlet

尝试在由 SQLAlchemy AsyncIO 代理类设置的 greenlet spawn 上下文之外启动异步 DBAPI 调用时会引发此错误。通常,当在意外位置尝试进行 IO 操作时,使用不直接提供 await 关键字的调用模式会发生此错误。在使用 ORM 时,这几乎总是由于使用 懒加载,在 asyncio 中,需要通过额外的步骤和/或替代加载程序模式才能成功使用。

另请参阅

在使用 AsyncSession 时预防隐式 IO - 涵盖了大多数可能出现此问题的 ORM 方案以及如何缓解,包括在懒加载场景中使用的特定模式。

无可用检查

直接在 AsyncConnectionAsyncEngine 对象上使用 inspect() 函数目前不受支持,因为尚未提供 Inspector 对象的可等待形式。相反,通过以获取 AsyncConnection 对象的基础 AsyncConnection.sync_connection 属性的方式获取该对象;然后使用 Inspector 通过使用 AsyncConnection.run_sync() 方法以及执行所需操作的自定义函数来进行 “同步” 调用:

async def async_main():
    async with engine.connect() as conn:
        tables = await conn.run_sync(
            lambda sync_conn: inspect(sync_conn).get_table_names()
        )

另请参阅

使用 Inspector 检查模式对象 - 使用 asyncio 扩展与 inspect() 的其他示例。

核心异常类

查看 核心异常 以获取核心异常类。

ORM 异常类

查看 ORM 异常 以获取 ORM 异常类。

旧版本异常

本节中的异常不是由当前的 SQLAlchemy 版本生成的,但提供了这些异常以适应异常消息的超链接。

在 SQLAlchemy 2.0 中,<某个函数> 将不再 <某事>

SQLAlchemy 2.0 对于核心和 ORM 组件中的许多关键 SQLAlchemy 使用模式都表示了一个重大转变。2.0 发布的目标是在 SQLAlchemy 自早期开始以来的一些最基本的假设中进行轻微调整,并提供一个新的简化使用模型,希望它在核心和 ORM 组件之间更加简约一致,并更加强大。

在 SQLAlchemy 2.0 - 主要迁移指南中介绍的 SQLAlchemy 2.0 项目包含了一个全面的未来兼容系统,该系统已集成到 SQLAlchemy 1.4 系列中,因此应用程序将具有明确、无歧义和逐步的升级路径,以将应用程序迁移到完全兼容 2.0 的状态。RemovedIn20Warning废弃警告是该系统的基础,提供了关于现有代码库中需要修改的行为的指导。如何启用此警告的概述在 SQLAlchemy 2.0 Deprecations Mode 中。

另请参阅

SQLAlchemy 2.0 - 主要迁移指南 - 从 1.x 系列升级过程的概述,以及 SQLAlchemy 2.0 的当前目标和进展。

SQLAlchemy 2.0 Deprecations Mode - 关于如何在 SQLAlchemy 1.4 中使用“2.0 废弃模式”的具体指南。### 对象正在被合并到会话中,沿着反向引用级联。

此消息指的是 SQLAlchemy 的“backref cascade”行为,在版本 2.0 中已删除。这指的是将对象添加到Session中,因为该会话中已经存在的另一个对象与之关联。由于这种行为被证明比有用更令人困惑,因此添加了relationship.cascade_backrefsbackref.cascade_backrefs参数,可以将其设置为False以禁用它,在 SQLAlchemy 2.0 中完全删除了“cascade backrefs”行为。

对于较旧的 SQLAlchemy 版本,要在当前使用relationship.backref字符串参数配置的反向引用上设置relationship.cascade_backrefsFalse,必须首先使用backref()函数声明反向引用,以便可以传递backref.cascade_backrefs参数。

或者,可以通过在“未来”模式下使用Session,通过为Session.future参数传递True来全面关闭“cascade backrefs”行为。

另请参阅

cascade_backrefs 行为在 2.0 中已弃用 - SQLAlchemy 2.0 的变更背景。 ### 创建在“传统”模式下的 select() 构造;关键字参数等。

从 SQLAlchemy 1.4 开始,select() 构造已经更新为支持 SQLAlchemy 2.0 中标准的新调用风格。为了在 1.4 系列内保持向后兼容性,该构造在“传统”风格以及“新”风格下都接受参数。

“新”风格的特点是,列和表达式只传递给 select() 构造;对象的任何其他修饰符必须通过后续的方法链传递:

# this is the way to do it going forward
stmt = select(table1.c.myid).where(table1.c.myid == table2.c.otherid)

作为对比,在 SQLAlchemy 的传统形式中,像 Select.where() 这样的方法甚至还未添加之前,select() 会是这样的:

# this is how it was documented in original SQLAlchemy versions
# many years ago
stmt = select([table1.c.myid], whereclause=table1.c.myid == table2.c.otherid)

或者甚至,“whereclause”会被按位置传递:

# this is also how it was documented in original SQLAlchemy versions
# many years ago
stmt = select([table1.c.myid], table1.c.myid == table2.c.otherid)

多年来,大多数叙述性文档中已经删除了接受的额外“whereclause”和其他参数,导致了一种最为熟悉的调用风格,即将列参数作为列表传递,但没有进一步的参数:

# this is how it's been documented since around version 1.0 or so
stmt = select([table1.c.myid]).where(table1.c.myid == table2.c.otherid)

在 select() 不再接受多样化的构造函数参数,列是按位置传递的 文档中以 2.0 迁移 的术语描述了这一变更。

另请参阅

select() 不再接受多样化的构造函数参数,列是按位置传递的

SQLAlchemy 2.0 - 重大迁移指南 ### 通过传递 future=True 到 Session 上,将会忽略通过传统绑定的元数据所定位的绑定。

“绑定元数据”的概念一直存在直到 SQLAlchemy 1.4;截至 SQLAlchemy 2.0,它已被移除。

此错误指的是MetaData.bind参数,它在 ORM Session中允许将特定映射类与Engine相关联的MetaData对象上。在 SQLAlchemy 2.0 中,Session必须直接链接到每个Engine上。也就是说,不能再不带任何参数实例化Sessionsessionmaker,并将EngineMetaData相关联:

engine = create_engine("sqlite://")
Session = sessionmaker()
metadata_obj = MetaData(bind=engine)
Base = declarative_base(metadata=metadata_obj)

class MyClass(Base): ...

session = Session()
session.add(MyClass())
session.commit()

Engine必须直接与sessionmakerSession相关联。MetaData对象不应再与任何引擎相关联:

engine = create_engine("sqlite://")
Session = sessionmaker(engine)
Base = declarative_base()

class MyClass(Base): ...

session = Session()
session.add(MyClass())
session.commit()

在 SQLAlchemy 1.4 中,当在sessionmakerSession上设置Session.future标志时,启用此 2.0 样式行为。###此编译对象未绑定到任何引擎或连接

此错误涉及到“绑定元数据”的概念,这是仅存在于 1.x 版本中的传统 SQLAlchemy 模式。当直接从未与任何Engine相关联的 Core 表达式对象上调用Executable.execute()方法时会发生此问题:

metadata_obj = MetaData()
table = Table("t", metadata_obj, Column("q", Integer))

stmt = select(table)
result = stmt.execute()  # <--- raises

逻辑预期的是MetaData对象已经绑定Engine

engine = create_engine("mysql+pymysql://user:pass@host/db")
metadata_obj = MetaData(bind=engine)

在上述情况下,从Table派生的任何语句将隐式使用给定的Engine来调用该语句。

请注意,绑定元数据的概念在 SQLAlchemy 2.0 中不存在。调用语句的正确方式是通过Connection.execute()方法的Connection

with engine.connect() as conn:
    result = conn.execute(stmt)

在使用 ORM 时,通过Session也可以使用类似的功能:

result = session.execute(stmt)

另请参阅

语句执行基础知识### 此连接处于非活动事务状态。请在继续之前完全回滚()

此错误条件已添加到 SQLAlchemy 自版本 1.4 起,不适用于 SQLAlchemy 2.0。该错误指的是将Connection放入事务中,使用类似Connection.begin()的方法创建一个进一步的“标记”事务;然后使用Transaction.rollback()回滚或使用Transaction.close()关闭“标记”事务,但外部事务仍处于“非活动”状态,必须回滚。

该模式如下:

engine = create_engine(...)

connection = engine.connect()
transaction1 = connection.begin()

# this is a "sub" or "marker" transaction, a logical nesting
# structure based on "real" transaction transaction1
transaction2 = connection.begin()
transaction2.rollback()

# transaction1 is still present and needs explicit rollback,
# so this will raise
connection.execute(text("select 1"))

上面,transaction2是一个“标记”事务,表示在外部事务内部的事务逻辑嵌套;虽然内部事务可以通过其 rollback()方法回滚整个事务,但其 commit()方法除了关闭“标记”事务本身的范围外,没有任何效果。调用transaction2.rollback()的效果是停用transaction1,这意味着它在数据库级别上基本上被回滚,但仍然存在以适应一致的事务嵌套模式。

正确的解决方法是确保外部事务也被回滚:

transaction1.rollback()

此模式在 Core 中并不常用。在 ORM 中,可能会出现类似的问题,这是 ORM 的“逻辑”事务结构的产物;这在 FAQ 条目中有描述“由于刷新期间的先前异常,此会话的事务已回滚。”(或类似)。

在 SQLAlchemy 2.0 中,已删除“子事务”模式,因此这种特定的编程模式不再可用,从而避免了这个错误消息。### 在 SQLAlchemy 2.0 中,<某个函数>将不再<某事>

SQLAlchemy 2.0 对于核心和 ORM 组件中的许多关键 SQLAlchemy 使用模式都表示了一个重大转变。2.0 版本的目标是在 SQLAlchemy 从一开始的基本假设中进行一些轻微调整,并提供一个新的简化的使用模型,希望在核心和 ORM 组件之间更加一致和简约,并且更具有能力。

在 SQLAlchemy 2.0 - 主要迁移指南 中介绍的 SQLAlchemy 2.0 项目包括一个综合的未来兼容性系统,该系统集成到 SQLAlchemy 1.4 系列中,以便应用程序能够清晰、明确地、逐步地升级到完全兼容 2.0 版本。RemovedIn20Warning 弃用警告是这个系统的基础,它提供了对现有代码库中需要修改的行为的指导。关于如何启用此警告的概述在 SQLAlchemy 2.0 弃用模式 中。

另请参阅

SQLAlchemy 2.0 - 主要迁移指南 - 从 1.x 系列的升级过程的概述,以及 SQLAlchemy 2.0 的当前目标和进展。

SQLAlchemy 2.0 弃用模式 - 如何在 SQLAlchemy 1.4 中使用“2.0 弃用模式”的具体指南。

对象正被合并到会话中,沿着反向引用级联

此消息指的是 SQLAlchemy 的“backref cascade”行为,在 2.0 版本中已删除。这是指对象作为已经存在于该会话中的另一个对象的关联而被添加到 Session 中的操作。由于这种行为显示出比有帮助更加令人困惑,添加了 relationship.cascade_backrefsbackref.cascade_backrefs 参数,可以将其设置为 False 以禁用它,并且在 SQLAlchemy 2.0 中已完全删除“级联反向引用”行为。

对于较旧的 SQLAlchemy 版本,要在当前使用 relationship.backref 字符串参数配置的反向引用上将 relationship.cascade_backrefs 设置为 False,必须首先使用 backref() 函数声明反向引用,以便传递 backref.cascade_backrefs 参数。

或者,可以通过在“未来”模式下使用 Session,将整个“级联反向引用”行为全部关闭,通过为 Session.future 参数传递 True

另请参阅

级联反向引用行为在 2.0 中已弃用 - SQLAlchemy 2.0 变更的背景。

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

select() 构造已在 SQLAlchemy 1.4 中更新,以支持在 SQLAlchemy 2.0 中标准的新调用风格。为了向后兼容 1.4 系列,该构造接受“传统”风格和“新”风格的参数。

“新”风格的特点是列和表达式仅以位置方式传递给 select() 构造;对象的任何其他修饰符必须使用后续方法链接传递:

# this is the way to do it going forward
stmt = select(table1.c.myid).where(table1.c.myid == table2.c.otherid)

作为对比,在 SQLAlchemy 的传统形式中,即使在添加像 Select.where() 这样的方法之前,select() 会像这样:

# this is how it was documented in original SQLAlchemy versions
# many years ago
stmt = select([table1.c.myid], whereclause=table1.c.myid == table2.c.otherid)

或者甚至“whereclause”将以位置方式传���:

# this is also how it was documented in original SQLAlchemy versions
# many years ago
stmt = select([table1.c.myid], table1.c.myid == table2.c.otherid)

多年来,大多数叙述性文档中接受的额外“whereclause”和其他参数已被移除,导致调用风格最为熟悉的是作为列参数传递的列表,但没有其他参数:

# this is how it's been documented since around version 1.0 or so
stmt = select([table1.c.myid]).where(table1.c.myid == table2.c.otherid)

select() no longer accepts varied constructor arguments, columns are passed positionally 中的文档描述了这一变化,涉及 2.0 迁移。

另请参阅

select() no longer accepts varied constructor arguments, columns are passed positionally

SQLAlchemy 2.0 - 重大迁移指南

通过传统的绑定元数据找到了一个绑定,但由于此会话设置了 future=True,因此会忽略此绑定。

“绑定元数据”的概念一直存在于 SQLAlchemy 1.4 之前;从 SQLAlchemy 2.0 开始已将其删除。

此错误指的是MetaData.bind参数,该参数位于MetaData对象上,该对象允许像 ORM Session这样的对象将特定的映射类与Engine关联起来。在 SQLAlchemy 2.0 中,Session必须直接与每个Engine关联。也就是说,不要实例化Sessionsessionmaker而不带任何参数,并将EngineMetaData关联:

engine = create_engine("sqlite://")
Session = sessionmaker()
metadata_obj = MetaData(bind=engine)
Base = declarative_base(metadata=metadata_obj)

class MyClass(Base): ...

session = Session()
session.add(MyClass())
session.commit()

相反,Engine必须直接与sessionmakerSession关联。MetaData对象不应再与任何引擎相关联:

engine = create_engine("sqlite://")
Session = sessionmaker(engine)
Base = declarative_base()

class MyClass(Base): ...

session = Session()
session.add(MyClass())
session.commit()

在 SQLAlchemy 1.4 中,当在sessionmakerSession上设置了Session.future标志时,将启用此 2.0 样式行为。

此 Compiled 对象未绑定到任何 Engine 或 Connection

此错误指的是“绑定元数据”的概念,这是一个仅在 1.x 版本中存在的传统 SQLAlchemy 模式。当直接从未与任何Engine相关联的 Core 表达式对象上调用Executable.execute()方法时,就会出现此问题:

metadata_obj = MetaData()
table = Table("t", metadata_obj, Column("q", Integer))

stmt = select(table)
result = stmt.execute()  # <--- raises

逻辑期望的是MetaData对象已经与Engine绑定

engine = create_engine("mysql+pymysql://user:pass@host/db")
metadata_obj = MetaData(bind=engine)

在上述情况中,任何从Table派生的语句,其又派生自MetaData的语句,将隐式使用给定的Engine来调用该语句。

请注意,在 SQLAlchemy 2.0 中不存在绑定元数据的概念。调用语句的正确方式是通过Connection.execute()方法的一个Connection

with engine.connect() as conn:
    result = conn.execute(stmt)

当使用 ORM 时,可以通过Session提供类似的功能:

result = session.execute(stmt)

请参阅

语句执行基础

此连接处于非活动事务状态。请在继续之前完全 rollback()。

此错误条件已添加到 SQLAlchemy 自 1.4 版本以来,并且不适用于 SQLAlchemy 2.0。该错误是指将Connection放入事务中,使用类似Connection.begin()的方法,然后在该范围内创建一个进一步的“标记”事务;然后使用Transaction.rollback()回滚“标记”事务,或使用Transaction.close()关闭它,但是外部事务仍然以“非活动”状态存在,必须回滚。

模式如下:

engine = create_engine(...)

connection = engine.connect()
transaction1 = connection.begin()

# this is a "sub" or "marker" transaction, a logical nesting
# structure based on "real" transaction transaction1
transaction2 = connection.begin()
transaction2.rollback()

# transaction1 is still present and needs explicit rollback,
# so this will raise
connection.execute(text("select 1"))

在上述代码中,transaction2 是一个“标记”事务,它表示外部事务内部的逻辑嵌套;而内部事务可以通过其 rollback()方法回滚整个事务,但是其 commit()方法除了关闭“标记”事务本身的范围外,并不产生任何效果。调用transaction2.rollback()的效果是停用transaction1,这意味着它在数据库级别上基本上已被回滚,但仍然存在以适应一致的事务嵌套模式。

正确的解决方法是确保外部事务也被回滚:

transaction1.rollback()

此模式在核心中不常用。在 ORM 中,可能会出现类似的问题,这是 ORM 的“逻辑”事务结构的产物;这在常见问题解答条目中有描述:“此会话的事务由于刷新期间的先前异常而已被回滚。”(或类似)。

“子事务”模式在 SQLAlchemy 2.0 中被移除,因此这种特定的编程模式不再可用,从而防止了这个错误消息的出现。

posted @ 2024-06-22 11:33  绝不原创的飞龙  阅读(26)  评论(0编辑  收藏  举报