第三方集成问题
- 我遇到了与“
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 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.True
或numpy.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.int32
和numpy.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 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.True
或 numpy.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.int32
和 numpy.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_”, 等。
错误消息
本节列出了 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_size
和create_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
之外,NullPool
和StaticPool
池类不使用锁,并且也适用于与异步引擎一起使用。
在极少数情况下,如果使用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 构造实现的功能,包括对象,如 Column
、select()
和 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
来表示。在使用第三方方言时,请与方言的维护者协商,以便他们可以遵循 确保可以启用缓存的步骤 并发布新版本。- 第三方或用户定义的类型,其继承自
TypeDecorator
或UserDefinedType
必须在其定义中包含ExternalType.cache_ok
属性,包括所有派生的子类,遵循ExternalType.cache_ok
的文档字符串中描述的指南。如前所述,如果这些数据类型是从第三方库导入的,请与该库的维护者联系,以便他们提供必要的更改并发布新版本。 - 第三方或用户定义的 SQL 构造,它们从诸如
ClauseElement
、Column
、Insert
等类继承,包括简单的子类以及设计用于与自定义 SQL 构造和编译扩展一起使用的构造,通常应包括HasCacheKey.inherit_cache
属性设置为True
或False
,根据构造的设计而定,遵循启用自定义构造的缓存支持中描述的指南。
参见
使用日志估算缓存性能 - 关于观察缓存行为和效率的背景信息
升级到 1.4 和/或 2.x 后,为什么我的应用变慢了? - 在常见问题解答部分 ### Compiler StrSQLCompiler 无法呈现 类型的元素
当尝试对包含不是默认编译的元素的 SQL 表达式构造进行字符串化时,通常会发生此错误;在这种情况下,错误将针对StrSQLCompiler
类。在较少见的情况下,当使用错误类型的 SQL 表达式与特定类型的数据库后端时,也可能发生这种情况;在这些情况下,将命名其他类型的 SQL 编译器类,例如 SQLCompiler
或 sqlalchemy.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 Join
或 Alias
对象,但是这个元素没有自动适应,这意味着必须直接引用 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, }
上述映射包括Employee
和Manager
类之间的关系。由于这两个类都使用了“employee”数据库表,从 SQL 的角度来看,这是一种自引用关系。如果我们想要使用连接从Employee
和Manager
模型中查询,那么在 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_1
和manager_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 构造明确化。
SqlAlchemy 2.0 中文文档(五十五)(2)https://developer.aliyun.com/article/1563187