SqlAlchemy 2.0 中文文档(五十五)(1)https://developer.aliyun.com/article/1563186
对象关系映射
IllegalStateChangeError 和并发异常
SQLAlchemy 2.0 引入了一个新系统,详见会话在检测到非法并发或重入访问时主动引发,该系统主动检测在单个 Session
对象的实例以及其扩展的 AsyncSession
代理对象上调用并发方法。这些并发访问调用通常会发生在单个 Session
实例在多个并发线程之间共享而没有进行同步访问时,或者类似地,当单个 AsyncSession
实例在多个并发任务之间共享时(例如使用 asyncio.gather()
这样的函数)。这些使用模式不是这些对象的适当用法,在没有 SQLAlchemy 实现的主动警告系统的情况下,仍然会在对象内部产生无效状态,从而产生难以调试的错误,包括数据库连接本身的驱动程序级错误。
Session
和 AsyncSession
的实例都是可变、有状态的对象,没有内置的方法调用同步,并且代表着一次单一的数据库事务,该事务在一次特定的 Engine
或 AsyncEngine
绑定的数据库连接上进行(请注意,这些对象都支持同时绑定到多个引擎,但在这种情况下,在事务范围内仍然只会有一个连接与引擎相关)。单个数据库事务不是并发 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.parent
和Parent.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.c1
、Parent.c2
和Child.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=False
、kw_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 批量更新
SqlAlchemy 2.0 中文文档(五十五)(3)https://developer.aliyun.com/article/1563189