SqlAlchemy 2.0 中文文档(五十五)(4)https://developer.aliyun.com/article/1563191
对象关系映射
IllegalStateChangeError 和并发异常
SQLAlchemy 2.0 引入了一个新系统,描述在会话主动引发非法并发或重入访问时,该系统主动检测在Session
对象的单个实例上调用并发方法以及通过扩展AsyncSession
代理对象。这些并发访问调用通常,尽管不是专门,会在单个Session
实例在多个并发线程之间共享时发生,而没有进行同步访问,或者类似地,当单个AsyncSession
实例在多个并发任务之间共享时(例如在使用asyncio.gather()
等函数时)。这些使用模式不是这些对象的适当使用方式,如果没有 SQLAlchemy 实现的主动警告系统,仍然会在对象内产生无效状态,导致难以调试的错误,包括数据库连接本身的驱动程序级错误。
Session
和AsyncSession
的实例是可变的、有状态的对象,没有内置的方法调用同步,并代表一次性数据库事务,一次只能连接一个特定的Engine
或AsyncEngine
(请注意,这些对象都支持同时绑定到多个引擎,但在这种情况下,在事务范围内仍然只会有一个连接与引擎相关)。单个数据库事务不是并发 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
上表示的意图是,当每个引用特定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’存在冲突。
此警告是指当两个或更多关系在 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.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
之间的重叠是有意为之的。### 对象无法转换为“持久”状态,因为此标识映射不再有效。
新版本 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=False
,kw_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 实现的主动警告系统,否则仍然会在对象内部产生无效状态,从而产生难以调试的错误,包括在数据库连接本身上的驱动程序级错误。
Session
和AsyncSession
的实例是可变的、有状态的对象,没有内置的方法调用同步,并且代表一次单一的持续数据库事务,一次只能在一个特定的Engine
或AsyncEngine
上绑定的数据库连接(请注意,这些对象都支持同时绑定到多个引擎,但在这种情况下,在事务范围内仍然只有一个连接在运行)。单个数据库事务不是并发 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.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
之间的重叠是有意的。
SqlAlchemy 2.0 中文文档(五十五)(6)https://developer.aliyun.com/article/1563192