SqlAlchemy-2-0-中文文档-六-

SqlAlchemy 2.0 中文文档(六)

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

SQL 表达式映射

原文:docs.sqlalchemy.org/en/20/orm/scalar_mapping.html

本页面已合并至 ORM 映射类配置索引。

映射表列

链接:docs.sqlalchemy.org/en/20/orm/mapping_columns.html

本节内容已整合到使用声明性配置表一节中。

关系配置

原文:docs.sqlalchemy.org/en/20/orm/relationships.html

本节描述了relationship()函数及其用法的深入讨论。关于关系的介绍,请从使用 ORM 相关对象开始,参阅 SQLAlchemy 统一教程。

  • 基本关系模式

    • 声明式 vs. 命令式形式

    • 一对多

      • 使用集合、列表或其他集合类型进行一对多

      • 为一对多配置删除行为

    • 多对一

      • 可空多对一
    • 一对一

      • 为非注释配置设置 uselist=False
    • 多对多

      • 设置双向多对多关系

      • 使用延迟评估形式的“次要”参数

      • 使用集合、列表或其他集合类型进行多对多

      • 从多对多表中删除行

    • 关联对象

      • 将关联对象与多对多访问模式相结合
    • 延迟评估关系参数

      • 在声明后为映射类添加关系

      • 使用多对多的“次要”参数进行延迟评估

  • 邻接列表关系

    • 复合邻接列表

    • 自引用查询策略

    • 配置自引用急加载

  • 配置关系连接方式

    • 处理多个连接路径

    • 指定备用连接条件

    • 创建自定义外键条件

    • 在连接条件中使用自定义运算符

    • 基于 SQL 函数的自定义运算符

    • 重叠的外键

    • 非关系比较 / 材料化路径

    • 自引用多对多关系

    • 复合“次要”连接

    • 与别名类的关系

      • 将别名类映射与类型化集成并避免早期映射器配置

      • 在查询中使用别名类目标

    • 使用窗口函数进行行限制关系

    • 构建支持查询的属性

    • 关于使用 viewonly 关系参数的注意事项

      • 在 Python 中进行突变,包括具有 viewonly=True 的反向引用不适用

      • viewonly=True 集合 / 属性直到过期才重新查询

  • 处理大型集合

    • 只写关系

      • 创建和持久化新的只写集合

      • 向现有集合添加新项目

      • 查询项目

      • 删除项目

      • 批量插入新项目

      • 项目的批量更新和删除

      • 只写集合 - API 文档

    • 动态关系加载器

      • 动态关系加载器 - API
    • 设置 RaiseLoad

    • 使用被动删除

  • 集合自定义和 API 详情

    • 自定义集合访问

      • 字典集合
    • 自定义集合实现

      • 通过装饰器注释自定义集合

      • 自定义基于字典的集合

      • 仪器化和自定义类型

    • 集合 API

      • attribute_keyed_dict()

      • column_keyed_dict()

      • keyfunc_mapping()

      • attribute_mapped_collection

      • column_mapped_collection

      • mapped_collection

      • KeyFuncDict

      • MappedCollection

    • 集合内部

      • bulk_replace()

      • collection

      • collection_adapter

      • CollectionAdapter

      • InstrumentedDict

      • InstrumentedList

      • InstrumentedSet

      • prepare_instrumentation()

  • 特殊关系持久化模式

    • 指向自身的行/相互依赖的行

    • 可变主键/更新级联

      • 模拟无外键支持的有限 ON UPDATE CASCADE
  • 使用传统的 'backref' 关系参数

    • Backref 默认参数

    • 指定 Backref 参数

  • 关系 API

    • relationship()

    • backref()

    • dynamic_loader()

    • foreign()

    • remote()

基本关系模式

原文:docs.sqlalchemy.org/en/20/orm/basic_relationships.html

本节通过基本关系模式的快速概述,使用基于Mapped注释类型的声明性样式映射来进行说明。

每个以下章节的设置如下:

from __future__ import annotations
from typing import List

from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

声明式与命令式形式的对比

随着 SQLAlchemy 的发展,不同的 ORM 配置样式已经出现。在本节和其他使用带有注释的声明性映射的示例中,相应的非注释形式应该使用所需的类或字符串类名作为传递给relationship()的第一个参数。下面的示例说明了本文档中使用的形式,这是一个完全使用 PEP 484 注释的声明性示例,其中 relationship() 构造还从 Mapped 注释中派生出目标类和集合类型,这是 SQLAlchemy 声明式映射的最现代形式:

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship(back_populates="parent")

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
    parent: Mapped["Parent"] = relationship(back_populates="children")

相比之下,使用不带注释的声明式映射是更加“经典”的映射形式,其中relationship()要求直接传递所有参数,就像下面的示例中所示:

class Parent(Base):
    __tablename__ = "parent_table"

    id = mapped_column(Integer, primary_key=True)
    children = relationship("Child", back_populates="parent")

class Child(Base):
    __tablename__ = "child_table"

    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(ForeignKey("parent_table.id"))
    parent = relationship("Parent", back_populates="children")

最后,使用命令式映射,这是 SQLAlchemy 在声明式之前的原始映射形式(尽管仍然是一小部分用户偏爱的形式),以上配置看起来如下:

registry.map_imperatively(
    Parent,
    parent_table,
    properties={"children": relationship("Child", back_populates="parent")},
)

registry.map_imperatively(
    Child,
    child_table,
    properties={"parent": relationship("Parent", back_populates="children")},
)

此外,非注释映射的默认集合样式是list。要在没有注释的情况下使用set或其他集合,请使用relationship.collection_class参数进行指定:

class Parent(Base):
    __tablename__ = "parent_table"

    id = mapped_column(Integer, primary_key=True)
    children = relationship("Child", collection_class=set, ...)

关于relationship()的集合配置的详细信息,请参阅自定义集合访问。

根据需要将带有注释和不带注释 / 命令式样式之间的其他差异进行说明。

一对多

一对多关系在子表上放置一个引用父表的外键。然后在父表上指定relationship(),表示引用子项的集合:

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship()

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))

要在一对多关系中建立双向关系,其中“反向”方是多对一,请指定一个额外的relationship()并使用relationship.back_populates参数将两者连接起来,使用每个relationship()的属性名称作为另一个relationship.back_populates上的值:

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship(back_populates="parent")

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
    parent: Mapped["Parent"] = relationship(back_populates="children")

Child将获得一个具有多对一语义的parent属性。

使用集合、列表或其他集合类型进行一对多关系

使用带注释的声明性映射,relationship()所使用的集合类型是从传递给Mapped容器类型的集合类型派生出来的。前一节中的示例可以编写为使用set而不是list作为Parent.children集合,使用Mapped[Set["Child"]]

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[Set["Child"]] = relationship(back_populates="parent")

在使用非带注释形式的映射时,可以通过relationship.collection_class参数传递要用作集合的 Python 类。

另请参阅

自定义集合访问 - 包含了对集合配置的进一步细节,包括一些将relationship()映射到字典的技巧。

配置一对多的删除行为

往往情况下,当它们所属的Parent被删除时,所有的Child对象都应该被删除。为了配置这种行为,使用在 delete 中描述的delete级联选项。另一个选项是,当Child对象与其父对象解除关联时,可以将Child对象本身删除。该行为在 delete-orphan 中描述。

另请参阅

delete

使用 ORM 关联的外键 ON DELETE cascade

delete-orphan ## 多对一

多对一(Many to one)在父表中放置一个外键,指向子表。relationship()在父表上声明,在此将创建一个新的标量持有属性:

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    child_id: Mapped[int] = mapped_column(ForeignKey("child_table.id"))
    child: Mapped["Child"] = relationship()

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)

上面的例子展示了假定非空行为的多对一关系;下一节,可空的多对一(Nullable Many-to-One),说明了可空版本。

双向行为通过添加第二个 relationship() 并在两个方向上应用 relationship.back_populates 参数来实现,在另一个 relationship() 的属性名称作为 relationship.back_populates 的值:

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    child_id: Mapped[int] = mapped_column(ForeignKey("child_table.id"))
    child: Mapped["Child"] = relationship(back_populates="parents")

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parents: Mapped[List["Parent"]] = relationship(back_populates="child")

可空的多对一(Nullable Many-to-One)

在上述例子中,Parent.child 关系未被类型化为允许 None;这源于 Parent.child_id 列本身不可为空,因为它使用 Mapped[int] 类型。如果我们希望 Parent.child可空的多对一关系,我们可以将 Parent.child_idParent.child 都设置为 Optional[],在这种情况下,配置将如下所示:

from typing import Optional

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    child_id: Mapped[Optional[int]] = mapped_column(ForeignKey("child_table.id"))
    child: Mapped[Optional["Child"]] = relationship(back_populates="parents")

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parents: Mapped[List["Parent"]] = relationship(back_populates="child")

上面,DDL 中将创建 Parent.child_id 列以允许 NULL 值。当使用 mapped_column() 与显式类型声明时,指定 child_id: Mapped[Optional[int]] 等效于在 Column 上设置 Column.nullableTrue,而 child_id: Mapped[int] 等效于将其设置为 False。有关此行为的背景,请参见 mapped_column() 从 Mapped 注释中派生数据类型和可为空性。

提示

如果使用 Python 3.10 或更高版本,PEP 604 语法更方便,可以使用 | None 指示可选类型,与PEP 563延迟注释评估结合使用,这样就不需要使用带字符串引号的类型,如下所示:

from __future__ import annotations

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    child_id: Mapped[int | None] = mapped_column(ForeignKey("child_table.id"))
    child: Mapped[Child | None] = relationship(back_populates="parents")

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parents: Mapped[List[Parent]] = relationship(back_populates="child")
```  ## 一对一(One To One)

一对一(One To One)在外键视角上本质上是一对多(One To Many)关系,但表示任何时候只会有一行引用特定父行。

当使用带有`Mapped`注释的映射时,通过在关系的两端都应用非集合类型的`Mapped`注释来实现“一对一”约定,这将使 ORM 意识到不应在任一侧使用集合,就像下面的示例一样:

```py
class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    child: Mapped["Child"] = relationship(back_populates="parent")

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
    parent: Mapped["Parent"] = relationship(back_populates="child")

在上述情况中,当我们加载一个Parent对象时,Parent.child属性将引用单个Child对象而不是集合。如果我们用一个新的Child对象替换Parent.child的值,ORM 的工作单元过程将用新的对象替换以前的对象,将以前的child.parent_id列默认设置为 NULL,除非设置了特定的级联行为。

提示

正如之前提到的,ORM 将“一对一”模式视为一种约定,其中它假设当它加载Parent.child属性时,将只返回一行。如果返回多行,ORM 将发出警告。

但是,上述关系的Child.parent一侧仍然保持为“多对一”关系。单独使用它,它将无法检测到分配超过一个Child的情况,除非设置了relationship.single_parent参数,这可能很有用:

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
    parent: Mapped["Parent"] = relationship(back_populates="child", single_parent=True)

在设置此参数之外,“一对多”侧(在这里按照惯例是一对一)也无法可靠地检测到一个Parent关联多个Child的情况,例如,多个Child对象处于挂起状态且不在数据库中持久存在的情况。

是否使用relationship.single_parent,建议数据库模式包含一个唯一约束,以指示Child.parent_id列应该是唯一的,以确保在数据库级别上,只有一个Child行可以同时引用特定的Parent行(有关__table_args__元组语法的背景,请参阅声明性表配置):

from sqlalchemy import UniqueConstraint

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
    parent: Mapped["Parent"] = relationship(back_populates="child")

    __table_args__ = (UniqueConstraint("parent_id"),)

新版本 2.0 中:relationship()构造可以从给定的Mapped注释中派生出relationship.uselist参数的有效值。

将非注释配置的 uselist 参数设置为 False

当使用没有 Mapped 注解的 relationship() 时,可以通过在通常是“多”的一侧将 relationship.uselist 参数设置为 False 来启用一对一模式,如下所示的非注解式声明配置:

class Parent(Base):
    __tablename__ = "parent_table"

    id = mapped_column(Integer, primary_key=True)
    child = relationship("Child", uselist=False, back_populates="parent")

class Child(Base):
    __tablename__ = "child_table"

    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(ForeignKey("parent_table.id"))
    parent = relationship("Parent", back_populates="child")
```  ## 多对多

Many to Many 在两个类之间添加了一个关联表。关联表几乎总是作为一个核心 `Table` 对象或其他核心可选择的对象,比如一个 `Join` 对象来给出,并且通过 `relationship()` 函数的 `relationship.secondary` 参数来指定。通常,`Table` 使用与声明基类关联的 `MetaData` 对象,这样 `ForeignKey` 指令就可以定位远程表以进行关联:

```py
from __future__ import annotations

from sqlalchemy import Column
from sqlalchemy import Table
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

# note for a Core table, we use the sqlalchemy.Column construct,
# not sqlalchemy.orm.mapped_column
association_table = Table(
    "association_table",
    Base.metadata,
    Column("left_id", ForeignKey("left_table.id")),
    Column("right_id", ForeignKey("right_table.id")),
)

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List[Child]] = relationship(secondary=association_table)

class Child(Base):
    __tablename__ = "right_table"

    id: Mapped[int] = mapped_column(primary_key=True)

提示

上面的“关联表”中已经建立了指向关系两侧实体表的外键约束。association.left_idassociation.right_id 的每个数据类型通常从引用表中推断出,并且可以省略。虽然 SQLAlchemy 并不强制要求,但也建议将引用两个实体表的列建立在唯一约束或更常见的主键约束中;这样可以确保无论应用程序端出现什么问题,都不会在表中持久化重复的行:

association_table = Table(
    "association_table",
    Base.metadata,
    Column("left_id", ForeignKey("left_table.id"), primary_key=True),
    Column("right_id", ForeignKey("right_table.id"), primary_key=True),
)

设置双向 Many-to-many

对于双向关系,关系的两侧都包含一个集合。使用 relationship.back_populates 进行指定,并且对于每个 relationship() 指定共同的关联表:

from __future__ import annotations

from sqlalchemy import Column
from sqlalchemy import Table
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

association_table = Table(
    "association_table",
    Base.metadata,
    Column("left_id", ForeignKey("left_table.id"), primary_key=True),
    Column("right_id", ForeignKey("right_table.id"), primary_key=True),
)

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List[Child]] = relationship(
        secondary=association_table, back_populates="parents"
    )

class Child(Base):
    __tablename__ = "right_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parents: Mapped[List[Parent]] = relationship(
        secondary=association_table, back_populates="children"
    )

使用延迟评估形式来处理“次要”参数

relationship.secondary参数还接受两种不同的“延迟评估”形式,包括字符串表名以及 lambda 可调用。请参阅使用“secondary”参数的延迟评估形式进行多对多关系部分了解背景和示例。

使用集合(Sets)、列表(Lists)或其他集合类型进行多对多关系

为多对多关系配置集合的方式与一对多完全相同,如在使用集合(Sets)、列表(Lists)或其他集合类型进行一对多关系中描述的那样。对于使用Mapped进行注释的映射,可以通过Mapped泛型类内部使用的集合类型来指示集合,例如set

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[Set["Child"]] = relationship(secondary=association_table)

当使用非注释形式包括命令式映射时,如一对多的情况,可以通过relationship.collection_class参数传递要用作集合的 Python 类。

另请参阅

自定义集合访问 - 包含有关集合配置的进一步详细信息,包括一些将relationship()映射到字典的技术。

从多对多表中删除行

relationship.secondary参数的一个独特行为是,此处指定的Table会自动受到 INSERT 和 DELETE 语句的影响,因为对象被添加或从集合中删除。无需手动从此表中删除。从集合中删除记录的操作将在刷新时将行删除:

# row will be deleted from the "secondary" table
# automatically
myparent.children.remove(somechild)

经常出现的一个问题是,当直接将子对象传递给Session.delete()时,如何删除“secondary”表中的行:

session.delete(somechild)

这里有几种可能性:

  • 如果从ParentChild有一个relationship(),但没有反向关系将特定的Child链接到每个Parent,SQLAlchemy 将不会意识到在删除此特定的Child对象时,需要维护将其链接到Parent的“secondary”表。不会删除“secondary”表。

  • 如果存在将特定的Child链接到每个Parent的关系,假设它称为Child.parents,SQLAlchemy 默认将加载Child.parents集合以定位所有Parent对象,并从建立此链接的“secondary”表中删除每行。请注意,此关系不需要是双向的;SQLAlchemy 严格查看与要删除的Child对象关联的每个relationship()

  • 这里的一个性能更高的选项是使用数据库使用的外键 ON DELETE CASCADE 指令。假设数据库支持此功能,数据库本身可以被设置为在删除“child”中的引用行时自动删除“secondary”表中的行。在这种情况下,SQLAlchemy 可以通过在relationship()上使用relationship.passive_deletes指令来指示放弃主动加载Child.parents集合;有关此操作的更多详细信息,请参阅使用外键 ON DELETE cascade 处理 ORM 关系。

再次注意,这些行为仅与与relationship()一起使用的relationship.secondary选项相关。如果处理的是显式映射的关联表,并且不存在于相关relationship()relationship.secondary选项中,那么可以使用级联规则来自动删除实体以响应相关实体的删除 - 有关此功能的信息,请参阅级联。

另请参阅

使用级联删除处理多对多关系

使用外键 ON DELETE 处理多对多关系 ## 关联对象

关联对象模式是一种与多对多模式相异的变体:当一个关联表包含除了与父表和子表(或左表和右表)是外键关系的列之外的其他列时,最理想的情况是将这些列映射到它们自己的 ORM 映射类中。这个映射类被映射到了 Table ,否则会在使用多对多模式时被标记为 relationship.secondary

在关联对象模式中,不使用 relationship.secondary 参数;相反,一个类直接映射到关联表。然后,两个独立的 relationship() 构造将首先父侧通过一对多连接到映射的关联类,然后通过多对一将映射的关联类连接到子侧,以形成从父对象到关联对象到子对象的单向关联对象关系。对于双向关系,使用四个 relationship() 构造将映射的关联类链接到父对象和子对象,以在两个方向上建立联系。

下面的示例说明了一个新的类 Association ,它映射到了名为 associationTable ,这个表现在包含了一个名为 extra_data 的额外列,这是一个字符串值,与 ParentChild 之间的每个关联一起存储。通过将表映射到一个显式类,从 ParentChild 的原始访问明确地使用了 Association

from typing import Optional

from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

class Association(Base):
    __tablename__ = "association_table"
    left_id: Mapped[int] = mapped_column(ForeignKey("left_table.id"), primary_key=True)
    right_id: Mapped[int] = mapped_column(
        ForeignKey("right_table.id"), primary_key=True
    )
    extra_data: Mapped[Optional[str]]
    child: Mapped["Child"] = relationship()

class Parent(Base):
    __tablename__ = "left_table"
    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Association"]] = relationship()

class Child(Base):
    __tablename__ = "right_table"
    id: Mapped[int] = mapped_column(primary_key=True)

为了说明双向版本,我们增加了两个 relationship() 构造,通过 relationship.back_populates 与现有的构造相连:

from typing import Optional

from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

class Association(Base):
    __tablename__ = "association_table"
    left_id: Mapped[int] = mapped_column(ForeignKey("left_table.id"), primary_key=True)
    right_id: Mapped[int] = mapped_column(
        ForeignKey("right_table.id"), primary_key=True
    )
    extra_data: Mapped[Optional[str]]
    child: Mapped["Child"] = relationship(back_populates="parents")
    parent: Mapped["Parent"] = relationship(back_populates="children")

class Parent(Base):
    __tablename__ = "left_table"
    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Association"]] = relationship(back_populates="parent")

class Child(Base):
    __tablename__ = "right_table"
    id: Mapped[int] = mapped_column(primary_key=True)
    parents: Mapped[List["Association"]] = relationship(back_populates="child")

在其直接形式中使用关联模式需要在将子对象附加到父对象之前将其与关联实例关联起来;同样,从父对象到子对象的访问通过关联对象进行:

# create parent, append a child via association
p = Parent()
a = Association(extra_data="some data")
a.child = Child()
p.children.append(a)

# iterate through child objects via association, including association
# attributes
for assoc in p.children:
    print(assoc.extra_data)
    print(assoc.child)

为了增强关联对象模式,使直接访问 Association 对象是可选的,SQLAlchemy 提供了 Association Proxy 扩展。这个扩展允许配置属性,这些属性将通过单一访问访问两个“跳”,一个“跳”到关联对象,第二个“跳”到目标属性。

另见

关联代理 - 允许父级和子级之间进行直接“多对多”样式访问,用于三类关联对象映射。

警告

避免直接混合使用关联对象模式和多对多模式,因为这会产生数据可能以不一致的方式读取和写入的情况,而无需特殊步骤;关联代理通常用于提供更简洁的访问。有关此组合引入的注意事项的更详细背景,请参阅下一节结合关联对象与多对多访问模式。

结合关联对象与多对多访问模式

如前一节所述,关联对象模式不会自动与同时针对相同表/列使用多对多模式的情况集成。由此可见,读操作可能返回冲突的数据,写操作也可能尝试刷新冲突的更改,导致完整性错误或意外的插入或删除。

为了说明,下面的示例配置了ParentChild之间的双向多对多关系,通过Parent.childrenChild.parents。同时,还配置了一个关联对象关系,即Parent.child_associations -> Association.childChild.parent_associations -> Association.parent之间的关系:

from typing import Optional

from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

class Association(Base):
    __tablename__ = "association_table"

    left_id: Mapped[int] = mapped_column(ForeignKey("left_table.id"), primary_key=True)
    right_id: Mapped[int] = mapped_column(
        ForeignKey("right_table.id"), primary_key=True
    )
    extra_data: Mapped[Optional[str]]

    # association between Assocation -> Child
    child: Mapped["Child"] = relationship(back_populates="parent_associations")

    # association between Assocation -> Parent
    parent: Mapped["Parent"] = relationship(back_populates="child_associations")

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)

    # many-to-many relationship to Child, bypassing the `Association` class
    children: Mapped[List["Child"]] = relationship(
        secondary="association_table", back_populates="parents"
    )

    # association between Parent -> Association -> Child
    child_associations: Mapped[List["Association"]] = relationship(
        back_populates="parent"
    )

class Child(Base):
    __tablename__ = "right_table"

    id: Mapped[int] = mapped_column(primary_key=True)

    # many-to-many relationship to Parent, bypassing the `Association` class
    parents: Mapped[List["Parent"]] = relationship(
        secondary="association_table", back_populates="children"
    )

    # association between Child -> Association -> Parent
    parent_associations: Mapped[List["Association"]] = relationship(
        back_populates="child"
    )

当使用此 ORM 模型进行更改时,在 Python 中对Parent.children进行的更改将不会与对Parent.child_associationsChild.parent_associations进行的更改协调;虽然所有这些关系本身都将继续正常运行,但在一个关系上的更改不会显示在另一个关系中,直到Session过期,通常在Session.commit()之后会自动发生。

另外,如果进行了冲突的更改,例如同时添加了一个新的Association对象,同时将相同的相关Child附加到Parent.children,则在工作单元刷新过程进行时将引发完整性错误,如下例所示:

p1 = Parent()
c1 = Child()
p1.children.append(c1)

# redundant, will cause a duplicate INSERT on Association
p1.child_associations.append(Association(child=c1))

直接将Child附加到Parent.children中也意味着在association表中创建行,而不指示association.extra_data列的任何值,该值将接收NULL作为其值。

如果你知道你在做什么,像上面这样使用映射是可以的;在很少使用“关联对象”模式的情况下使用多对多关系可能是有充分理由的,因为通过单个多对多关系加载关系更容易,这也可以优化“次要”表在 SQL 语句中的使用效果,与两个对显式关联类的分开关系的使用相比略有优势。至少将 relationship.viewonly 参数应用于“次要”关系是一个好主意,以避免发生冲突的更改,同时防止将 NULL 写入额外的关联列,如下所示:

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)

    # many-to-many relationship to Child, bypassing the `Association` class
    children: Mapped[List["Child"]] = relationship(
        secondary="association_table", back_populates="parents", viewonly=True
    )

    # association between Parent -> Association -> Child
    child_associations: Mapped[List["Association"]] = relationship(
        back_populates="parent"
    )

class Child(Base):
    __tablename__ = "right_table"

    id: Mapped[int] = mapped_column(primary_key=True)

    # many-to-many relationship to Parent, bypassing the `Association` class
    parents: Mapped[List["Parent"]] = relationship(
        secondary="association_table", back_populates="children", viewonly=True
    )

    # association between Child -> Association -> Parent
    parent_associations: Mapped[List["Association"]] = relationship(
        back_populates="child"
    )

上述映射不会将任何更改写入到数据库的 Parent.childrenChild.parents 中,从而防止冲突写入。然而,如果在同一个事务或 Session 中对视图集合进行读取的同时对 Parent.childrenChild.parents 进行读取,则对 Parent.childrenChild.parents 的读取不一定会匹配从 Parent.child_associationsChild.parent_associations 中读取的数据。如果很少使用关联对象关系,并且对访问多对多集合的代码进行了精心组织以避免过时的读取(在极端情况下,直接使用 Session.expire() 来使集合在当前事务内刷新),则该模式可能是可行的。

上述模式的一个流行替代方案是,直接的多对多 Parent.childrenChild.parents 关系被替换为一个扩展,该扩展将通过 Association 类透明代理,同时从 ORM 的角度保持一切一致。这个扩展被称为关联代理。

另请参阅

关联代理 - 允许父对象和子对象之间直接“多对多”样式的访问,用于三类关联对象映射。## 关系参数的延迟评估

在前面的部分中,大多数示例都说明了各种relationship() 构造是如何使用字符串名称而不是类本身来引用它们的目标类的,比如当使用Mapped时,会生成一个仅在运行时存在的字符串引用:

class Parent(Base):
    # ...

    children: Mapped[List["Child"]] = relationship(back_populates="parent")

class Child(Base):
    # ...

    parent: Mapped["Parent"] = relationship(back_populates="children")

类似地,当使用非注释形式,如非注释的声明式或命令式映射时,relationship()构造也直接支持字符串名称:

registry.map_imperatively(
    Parent,
    parent_table,
    properties={"children": relationship("Child", back_populates="parent")},
)

registry.map_imperatively(
    Child,
    child_table,
    properties={"parent": relationship("Parent", back_populates="children")},
)

这些字符串名称在映射解析阶段解析为类,这是一个内部过程,通常在所有映射都被定义后发生,并且通常由映射本身的第一次使用触发。registry对象是存储这些名称并将其解析为它们所引用的映射类的容器。

除了relationship()的主类参数之外,还可以指定依赖于尚未定义类中存在的列的其他参数,这些参数可以是 Python 函数,或更常见的是字符串。对于这些参数中的大多数(除了主参数之外),字符串输入 使用 Python 内置的 eval()函数求值为 Python 表达式,因为它们旨在接收完整的 SQL 表达式。

警告

由于 Python 的eval()函数用于解释传递给relationship()映射配置构造函数的后期评估的字符串参数,因此这些参数 不应该 被重新用于接收不受信任的用户输入;eval()对不受信任的用户输入 不安全

此评估中可用的完整命名空间包括为此声明基类映射的所有类,以及sqlalchemy包的内容,包括表达式函数如desc()sqlalchemy.sql.functions.func

class Parent(Base):
    # ...

    children: Mapped[List["Child"]] = relationship(
        order_by="desc(Child.email_address)",
        primaryjoin="Parent.id == Child.parent_id",
    )

对于包含多个模块都包含相同名称类的情况,字符串类名也可以在任何这些字符串表达式中指定为模块限定路径:

class Parent(Base):
    # ...

    children: Mapped[List["myapp.mymodel.Child"]] = relationship(
        order_by="desc(myapp.mymodel.Child.email_address)",
        primaryjoin="myapp.mymodel.Parent.id == myapp.mymodel.Child.parent_id",
    )

在类似上面的示例中,传递给Mapped的字符串也可以通过直接将类位置字符串传递给relationship.argument来消除特定类参数的歧义。下面演示了对Child进行仅类型导入,并与将在registry中搜索正确名称的目标类的运行时说明符相结合的示例:

import typing

if typing.TYPE_CHECKING:
    from myapp.mymodel import Child

class Parent(Base):
    # ...

    children: Mapped[List["Child"]] = relationship(
        "myapp.mymodel.Child",
        order_by="desc(myapp.mymodel.Child.email_address)",
        primaryjoin="myapp.mymodel.Parent.id == myapp.mymodel.Child.parent_id",
    )

合格的路径可以是任何消除名称歧义的部分路径。例如,为了消除myapp.model1.Childmyapp.model2.Child之间的歧义,我们可以指定model1.Childmodel2.Child

class Parent(Base):
    # ...

    children: Mapped[List["Child"]] = relationship(
        "model1.Child",
        order_by="desc(mymodel1.Child.email_address)",
        primaryjoin="Parent.id == model1.Child.parent_id",
    )

relationship() 构造还接受 Python 函数或 lambda 作为这些参数的输入。Python 函数式方法可能如下所示:

import typing

from sqlalchemy import desc

if typing.TYPE_CHECKING:
    from myapplication import Child

def _resolve_child_model():
    from myapplication import Child

    return Child

class Parent(Base):
    # ...

    children: Mapped[List["Child"]] = relationship(
        _resolve_child_model,
        order_by=lambda: desc(_resolve_child_model().email_address),
        primaryjoin=lambda: Parent.id == _resolve_child_model().parent_id,
    )

接受 Python 函数/lambda 或将传递给 eval() 的字符串的完整参数列表为:

  • relationship.order_by

  • relationship.primaryjoin

  • relationship.secondaryjoin

  • relationship.secondary

  • relationship.remote_side

  • relationship.foreign_keys

  • relationship._user_defined_foreign_keys

警告

如前所述,上述参数relationship()作为 Python 代码表达式使用 eval() 进行评估。不要向这些参数传递不受信任的输入。

在声明后向映射类添加关系

还应注意,与向现有的 Declarative 映射类添加附加列中描述的类似,任何MapperProperty 构造都可以随时添加到声明基本映射中(请注意,在此上下文中不支持注释形式)。如果我们希望在 Address 类可用后实现此 relationship(),我们也可以稍后应用它:

# first, module A, where Child has not been created yet,
# we create a Parent class which knows nothing about Child

class Parent(Base): ...

# ... later, in Module B, which is imported after module A:

class Child(Base): ...

from module_a import Parent

# assign the User.addresses relationship as a class variable.  The
# declarative base class will intercept this and map the relationship.
Parent.children = relationship(Child, primaryjoin=Child.parent_id == Parent.id)

对于 ORM 映射列一样,Mapped 注释类型没有参与此操作的能力;因此,相关类必须直接在 relationship() 构造中指定,可以作为类本身、类的字符串名称或返回目标类引用的可调用函数。

注意

对于 ORM 映射列一样,只有当“声明基类”类被使用时,即用户定义的 DeclarativeBase 的子类或由 declarative_base()registry.generate_base() 返回的动态生成的类时,将映射属性分配给已经映射的类才会正常工作。这个“基”类包含一个实现特殊 __setattr__() 方法的 Python 元类,该方法拦截这些操作。

如果类使用装饰器如registry.mapped()或者使用命令式函数如registry.map_imperatively()进行映射,那么将类映射属性运行时分配给映射类 不会 起作用。### 对于多对多关系使用后期评估的形式

多对多关系使用 relationship.secondary 参数,通常指示一个参考到通常非映射的 Table 对象或其他 Core 可选择对象。通常使用 lambda 可调用对象进行延迟评估。

对于给定的示例在多对多关系中,如果我们假设association_table Table对象会在模块中的后面定义,我们可以使用 lambda 来编写relationship()如下:

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship(
        "Child", secondary=lambda: association_table
    )

对于也是有效的 Python 标识符的表名的快捷方式,relationship.secondary 参数也可以作为字符串传递,其中解析工作通过将字符串作为 Python 表达式进行评估,简单标识符名称与当前 registry 引用的相同名称 Table 对象链接到相同的 MetaData 集合中。

在下面的示例中,表达式 "association_table" 被视为一个名为"association_table"的变量,该变量相对于 MetaData 集合中的表名进行解析:

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship(secondary="association_table")

注意

当作为字符串传递时,传递给relationship.secondary的名称必须是有效的 Python 标识符,以字母开头,并且只包含字母数字字符或下划线。其他字符如短划线等将被解释为 Python 运算符,不会解析为给定的名称。请考虑使用 lambda 表达式而不是字符串以提高清晰度。

警告

当作为字符串传递时,relationship.secondary参数将使用 Python 的eval()函数进行解释,即使它通常是表的名称。不要传递不可信的输入给这个字符串

声明式 vs. 命令式形式

随着 SQLAlchemy 的发展,不同的 ORM 配置风格已经出现。对于本节及其他使用带注解的声明式映射的示例,相应的非带注解形式应使用所需的类或字符串类名作为传递给relationship()的第一个参数。下面的示例说明了本文档中使用的形式,这是一个完全声明式的示例,使用PEP 484注解,其中relationship()构造还从Mapped注解中推断目标类和集合类型,这是 SQLAlchemy 声明式映射的最现代形式:

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship(back_populates="parent")

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
    parent: Mapped["Parent"] = relationship(back_populates="children")

相比之下,使用不带注解的声明式映射更像是更“经典”的映射形式,其中relationship()需要直接传递所有参数,如下例所示:

class Parent(Base):
    __tablename__ = "parent_table"

    id = mapped_column(Integer, primary_key=True)
    children = relationship("Child", back_populates="parent")

class Child(Base):
    __tablename__ = "child_table"

    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(ForeignKey("parent_table.id"))
    parent = relationship("Parent", back_populates="children")

最后,使用命令式映射,这是在声明式之前 SQLAlchemy 的原始映射形式(尽管仍然被少数用户偏爱),以上配置如下所示:

registry.map_imperatively(
    Parent,
    parent_table,
    properties={"children": relationship("Child", back_populates="parent")},
)

registry.map_imperatively(
    Child,
    child_table,
    properties={"parent": relationship("Parent", back_populates="children")},
)

此外,对于非带注解的映射,默认的集合样式是list。要使用set或其他集合而不带注解,可以使用relationship.collection_class参数进行指示:

class Parent(Base):
    __tablename__ = "parent_table"

    id = mapped_column(Integer, primary_key=True)
    children = relationship("Child", collection_class=set, ...)

有关relationship()的集合配置详细信息,请参阅自定义集合访问。

根据需要,将注意到注释和非注释/命令式样式之间的其他差异。

一对多

一对多关系在子表上放置一个外键,引用父表。然后在父表上指定relationship(),作为对由子表表示的项目集合的引用:

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship()

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))

在一对多关系中建立双向关系时,其中“反向”端是多对一,需要指定一个额外的relationship()并使用relationship.back_populates参数将两者连接起来,其中使用每个relationship()的属性名称作为另一个relationship.back_populates的值:

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship(back_populates="parent")

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
    parent: Mapped["Parent"] = relationship(back_populates="children")

Child将获得一个具有多对一语义的parent属性。

使用集合、列表或其他集合类型进行一对多关系

使用带注释的声明性映射时,用于relationship()的集合类型是从传递给Mapped容器类型的集合类型派生的。上一节的示例可以编写为使用set而不是list来表示Parent.children集合,使用Mapped[Set["Child"]]

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[Set["Child"]] = relationship(back_populates="parent")

当使用非注释形式,包括命令式映射时,可以使用relationship.collection_class参数传递要用作集合的 Python 类。

另请参见

自定义集合访问 - 包含了更多关于集合配置的细节,包括一些将relationship()映射到字典的技术。

配置一对多关系的删除行为

通常情况下,当其所属的Parent被删除时,所有Child对象都应该被删除。为了配置这种行为,使用 delete 中描述的delete级联选项。另一个选项是,当Child对象与其父对象解除关联时,可以删除Child对象本身。这种行为在 delete-orphan 中描述。

另请参见

delete

使用 ORM 关系的外键 ON DELETE 级联

delete-orphan

使用集合、列表或其他集合类型进行一对多关系

使用带注释的声明性映射,从传递给Mapped容器类型的集合类型派生出用于relationship()的集合类型。可以写一个例子,以使用set而不是list来表示Parent.children集合,使用Mapped[Set["Child"]]

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[Set["Child"]] = relationship(back_populates="parent")

在使用非注释形式,包括命令式映射时,可以使用relationship.collection_class参数传递要用作集合的 Python 类。

另请参阅

自定义集合访问 - 包含有关集合配置的更多详细信息,包括一些将relationship()映射到字典的技术。

配置一对多的删除行为

通常情况下,当其所属的Parent被删除时,所有的Child对象都应该被删除。要配置此行为,可以使用在删除中描述的delete级联选项。另一个选项是当Child对象与其父对象解除关联时,它本身也可以被删除。这种行为在删除孤儿中描述。

另请参阅

删除

使用 ORM 关系的外键 ON DELETE 级联

删除孤儿

多对一

多对一在父表中放置了一个引用子表的外键。在父级上声明relationship(),将创建一个新的标量持有属性:

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    child_id: Mapped[int] = mapped_column(ForeignKey("child_table.id"))
    child: Mapped["Child"] = relationship()

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)

上面的示例显示了一个假定为非空的多对一关系;下一节,可空多对一,介绍了一个可空版本。

通过在两个方向添加第二个relationship()并在两个方向上应用relationship.back_populates参数,使用每个relationship()的属性名称作为另一个relationship.back_populates的值,实现了双向行为:

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    child_id: Mapped[int] = mapped_column(ForeignKey("child_table.id"))
    child: Mapped["Child"] = relationship(back_populates="parents")

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parents: Mapped[List["Parent"]] = relationship(back_populates="child")

可空多对一

在上述示例中,Parent.child 的关系未被类型化为允许 None;这是因为 Parent.child_id 列本身不可为空,因为它被类型化为 Mapped[int]。如果我们想要 Parent.child 成为一个可为空的多对一关系,我们可以将 Parent.child_idParent.child 都设置为 Optional[],在这种情况下,配置将如下所示:

from typing import Optional

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    child_id: Mapped[Optional[int]] = mapped_column(ForeignKey("child_table.id"))
    child: Mapped[Optional["Child"]] = relationship(back_populates="parents")

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parents: Mapped[List["Parent"]] = relationship(back_populates="child")

在上述代码中,用于 Parent.child_id 的列将在 DDL 中被创建以允许 NULL 值。在使用 mapped_column() 进行显式类型声明时,对 child_id: Mapped[Optional[int]] 的规定等效于将 Column.nullable 设置为 TrueColumn 上,而 child_id: Mapped[int] 等效于将其设置为 False。有关此行为的背景信息,请参阅 mapped_column() 从 Mapped 注释中推导出数据类型和可为空性。

提示

如果使用 Python 3.10 或更高版本,PEP 604 语法更方便,可使用 | None 来指示可选类型,结合 PEP 563 推迟注释评估,以便不需要字符串引号类型,这将如下所示:

from __future__ import annotations

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    child_id: Mapped[int | None] = mapped_column(ForeignKey("child_table.id"))
    child: Mapped[Child | None] = relationship(back_populates="parents")

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parents: Mapped[List[Parent]] = relationship(back_populates="child")
``` ### 可为空的多对一关系

在上述示例中,`Parent.child` 的关系未被类型化为允许 `None`;这是因为 `Parent.child_id` 列本身不可为空,因为它被类型化为 `Mapped[int]`。如果我们想要 `Parent.child` 成为一个**可为空的**多对一关系,我们可以将 `Parent.child_id` 和 `Parent.child` 都设置为 `Optional[]`,在这种情况下,配置将如下所示:

```py
from typing import Optional

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    child_id: Mapped[Optional[int]] = mapped_column(ForeignKey("child_table.id"))
    child: Mapped[Optional["Child"]] = relationship(back_populates="parents")

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parents: Mapped[List["Parent"]] = relationship(back_populates="child")

在上述代码中,用于 Parent.child_id 的列将在 DDL 中被创建以允许 NULL 值。在使用 mapped_column() 进行显式类型声明时,对 child_id: Mapped[Optional[int]] 的规定等效于将 Column.nullable 设置为 TrueColumn 上,而 child_id: Mapped[int] 等效于将其设置为 False。有关此行为的背景信息,请参阅 mapped_column() 从 Mapped 注释中推导出数据类型和可为空性。

提示

如果使用 Python 3.10 或更高版本,PEP 604 语法更方便地使用| None来指示可选类型,与PEP 563推迟的注释评估结合使用,以便不需要字符串引号的类型,会是这样的:

from __future__ import annotations

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    child_id: Mapped[int | None] = mapped_column(ForeignKey("child_table.id"))
    child: Mapped[Child | None] = relationship(back_populates="parents")

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parents: Mapped[List[Parent]] = relationship(back_populates="child")

一对一

One To One 本质上是一个从外键角度来看的一对多关系,但表示在任何时候只会有一行指向特定父行的行。

当使用带注释的映射和Mapped时,“一对一”约定通过在关系的两侧应用非集合类型到Mapped注释来实现,这将暗示 ORM 不应在任一侧使用集合,如下面的示例所示:

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    child: Mapped["Child"] = relationship(back_populates="parent")

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
    parent: Mapped["Parent"] = relationship(back_populates="child")

上面的例子中,当我们加载一个Parent对象时,Parent.child属性将引用一个单个的Child对象而不是一个集合。如果我们用一个新的Child对象替换Parent.child的值,ORM 的工作单元过程将用新的对象替换之前的对象,将之前的child.parent_id列默认设置为 NULL,除非设置了特定的级联行为。

提示

正如前面提到的,ORM 将“一对一”模式视为一种约定,其中它假设当它加载Parent.child属性时,它只会得到一行返回。如果返回多行,ORM 将发出警告。

然而,上述关系中的Child.parent方面仍然是一个“多对一”的关系。独自使用时,它不会检测到多个Child的赋值,除非设置了relationship.single_parent参数,这可能是有用的:

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
    parent: Mapped["Parent"] = relationship(back_populates="child", single_parent=True)

在设置此参数之外,“一对多”方面(这里按照惯例是一对一)也不会可靠地检测到多个Child关联到单个Parent的情况,比如多个Child对象是挂起的并且不是数据库持久的情况。

是否使用了relationship.single_parent,建议数据库模式包括一个唯一约束来指示Child.parent_id列应该是唯一的,以确保在数据库级别只有一行Child可能指向特定的Parent行(有关__table_args__元组语法的背景,请参阅声明式表配置):

from sqlalchemy import UniqueConstraint

class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
    parent: Mapped["Parent"] = relationship(back_populates="child")

    __table_args__ = (UniqueConstraint("parent_id"),)

新版本 2.0 中:relationship() 构造可以从给定的Mapped注解推导出relationship.uselist参数的有效值。

对于非注解配置设置 uselist=False

当在没有使用Mapped注解的情况下使用relationship()时,可以通过将通常是“多”端的relationship.uselist参数设置为False来启用一对一模式,如下面的非注解式声明配置所示:

class Parent(Base):
    __tablename__ = "parent_table"

    id = mapped_column(Integer, primary_key=True)
    child = relationship("Child", uselist=False, back_populates="parent")

class Child(Base):
    __tablename__ = "child_table"

    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(ForeignKey("parent_table.id"))
    parent = relationship("Parent", back_populates="child")

对于非注解配置设置 uselist=False

当在没有使用Mapped注解的情况下使用relationship()时,可以通过将通常是“多”端的relationship.uselist参数设置为False来启用一对一模式,如下面的非注解式声明配置所示:

class Parent(Base):
    __tablename__ = "parent_table"

    id = mapped_column(Integer, primary_key=True)
    child = relationship("Child", uselist=False, back_populates="parent")

class Child(Base):
    __tablename__ = "child_table"

    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(ForeignKey("parent_table.id"))
    parent = relationship("Parent", back_populates="child")

多对多

多对多在两个类之间添加了一个关联表。这个关联表几乎总是以一个核心Table对象或其他核心可选项(如Join对象)的形式给出,并通过relationship()函数的relationship.secondary参数指示。通常,Table使用与声明基类关联的MetaData对象,以便ForeignKey指令可以定位要链接的远程表:

from __future__ import annotations

from sqlalchemy import Column
from sqlalchemy import Table
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

# note for a Core table, we use the sqlalchemy.Column construct,
# not sqlalchemy.orm.mapped_column
association_table = Table(
    "association_table",
    Base.metadata,
    Column("left_id", ForeignKey("left_table.id")),
    Column("right_id", ForeignKey("right_table.id")),
)

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List[Child]] = relationship(secondary=association_table)

class Child(Base):
    __tablename__ = "right_table"

    id: Mapped[int] = mapped_column(primary_key=True)

提示

上面的“关联表”已建立了引用关系的外键约束,这些约束指向关系两侧的两个实体表。association.left_idassociation.right_id的数据类型通常是从引用表的数据类型推断出来的,可以省略。虽然 SQLAlchemy 没有要求,但建议将指向两个实体表的列建立在唯一约束或更常见的主键约束中;这样可以确保无论应用程序端是否存在问题,表中都不会持续存在重复行:

association_table = Table(
    "association_table",
    Base.metadata,
    Column("left_id", ForeignKey("left_table.id"), primary_key=True),
    Column("right_id", ForeignKey("right_table.id"), primary_key=True),
)

设置双向多对多

对于双向关系,关系的两侧都包含一个集合。使用relationship.back_populates进行指定,并且对于每个relationship()指定共同的关联表:

from __future__ import annotations

from sqlalchemy import Column
from sqlalchemy import Table
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

association_table = Table(
    "association_table",
    Base.metadata,
    Column("left_id", ForeignKey("left_table.id"), primary_key=True),
    Column("right_id", ForeignKey("right_table.id"), primary_key=True),
)

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List[Child]] = relationship(
        secondary=association_table, back_populates="parents"
    )

class Child(Base):
    __tablename__ = "right_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parents: Mapped[List[Parent]] = relationship(
        secondary=association_table, back_populates="children"
    )

使用“secondary”参数的后期评估形式

relationship()relationship.secondary参数还接受两种不同的“后期评估”形式,包括字符串表名称以及 lambda 可调用。有关背景和示例,请参见使用“secondary”参数的后期评估形式进行多对多关系部分。

使用集合、列表或其他集合类型进行多对多

配置多对多关系的集合与一对多的配置相同,如在使用集合、列表或其他集合类型进行一对多关系中所述。对于使用Mapped进行注释的映射,集合可以由Mapped泛型类内部使用的集合类型指示,例如set

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[Set["Child"]] = relationship(secondary=association_table)

当使用命令式映射(即一对多情况)的非注释形式时,可以通过relationship.collection_class参数传递用作集合的 Python 类。

另请参阅

自定义集合访问 - 包含有关集合配置的进一步详细信息,包括一些将relationship()映射到字典的技术。

从多对多表中删除行

对于relationship()relationship.secondary参数是唯一的行为,这里指定的Table将自动受到 INSERT 和 DELETE 语句的影响,当对象被添加或从集合中删除时。没有必要手动从此表中删除。从集合中删除记录的行为将导致刷新时删除该行的效果:

# row will be deleted from the "secondary" table
# automatically
myparent.children.remove(somechild)

当子对象直接传递给Session.delete()时,“次要”表中的行如何删除经常会引起一个问题:

session.delete(somechild)

这里有几种可能性:

  • 如果从ParentChild有一个relationship(),但是没有将特定的Child链接到每个Parent的反向关系,SQLAlchemy 不会意识到删除此特定Child对象时需要维护链接到Parent的“次要”表。不会删除“次要”表的删除。

  • 如果存在将特定的Child链接到每个Parent的关系,假设它被称为Child.parents,SQLAlchemy 默认会加载Child.parents集合以定位所有Parent对象,并从建立此链接的“次要”表中删除每行。请注意,此关系不需要是双向的;SQLAlchemy 严格地查看与被删除的Child对象相关联的每个relationship()

  • 在这里的一个性能较高的选项是使用数据库中使用的外键的 ON DELETE CASCADE 指令。假设数据库支持这个特性,数据库本身可以被设置为在“子”中的引用行被删除时自动删除“次要”表中的行。在这种情况下,SQLAlchemy 可以被指示放弃主动加载Child.parents集合,使用relationship()上的relationship.passive_deletes指令;参见使用 ORM 关系的外键 ON DELETE 级联以获取更多关于此的详细信息。

再次注意,这些行为与与relationship()一起使用的relationship.secondary选项相关。如果处理的是显式映射的关联表,并且这些表出现在相关relationship()relationship.secondary选项中,则可以改用级联规则来自动删除实体,以响应相关实体的删除 - 有关此功能的信息,请参阅级联。

另请参阅

在多对多关系中使用级联删除

在多对多关系中使用外键 ON DELETE

设置双向多对多

对于双向关系,关系的两端都包含一个集合。使用relationship.back_populates来指定,并且对于每个relationship()都要指定共同的关联表:

from __future__ import annotations

from sqlalchemy import Column
from sqlalchemy import Table
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

association_table = Table(
    "association_table",
    Base.metadata,
    Column("left_id", ForeignKey("left_table.id"), primary_key=True),
    Column("right_id", ForeignKey("right_table.id"), primary_key=True),
)

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List[Child]] = relationship(
        secondary=association_table, back_populates="parents"
    )

class Child(Base):
    __tablename__ = "right_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parents: Mapped[List[Parent]] = relationship(
        secondary=association_table, back_populates="children"
    )

使用延迟评估形式的“secondary”参数

relationship()relationship.secondary参数还接受两种不同的“延迟评估”形式,包括字符串表名以及 lambda 可调用。有关背景和示例,请参阅使用“secondary”参数的延迟评估形式进行多对多关系部分。

使用集合、列表或其他集合类型进行多对多关系

对于多对多关系的集合配置与一对多完全相同,如使用集合、列表或其他集合类型进行一对多关系中所述。对于使用Mapped进行注释的映射,可以通过Mapped泛型类中使用的集合类型来指示集合,例如set

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[Set["Child"]] = relationship(secondary=association_table)

当使用非注释形式,包括命令式映射时,就像一对多一样,可以使用relationship.collection_class参数传递要用作集合的 Python 类。

另请参阅

自定义集合访问 - 包含有关集合配置的进一步详细信息,包括一些将relationship()映射到字典的技术。

从多对多表中删除行

relationship()参数中唯一的行为是,指定的Table在对象被添加或从集合中删除时会自动受到 INSERT 和 DELETE 语句的影响。无需手动从此表中删除。从集合中删除记录的行为将导致在 flush 时删除该行:

# row will be deleted from the "secondary" table
# automatically
myparent.children.remove(somechild)

经常出现的一个问题是当直接将子对象传递给Session.delete()时如何删除“secondary”表中的行:

session.delete(somechild)

这里有几种可能性:

  • 如果从ParentChild有一个relationship(),但没有一个反向关系将特定的Child与每个Parent关联起来,SQLAlchemy 将不会意识到当删除这个特定的Child对象时,它需要维护将其与Parent链接起来的“secondary”表。不会删除“secondary”表。

  • 如果有一个将特定的Child与每个Parent关联起来的关系,假设它被称为Child.parents,SQLAlchemy 默认会加载Child.parents集合以定位所有Parent对象,并从建立此链接的“secondary”表中删除每一行。请注意,此关系不需要是双向的;SQLAlchemy 严格查看与正在删除的Child对象相关联的每一个relationship()

  • 这里的一个性能更高的选项是与数据库一起使用 ON DELETE CASCADE 指令。假设数据库支持这个功能,数据库本身可以被设置为在“子”中的引用行被删除时自动删除“辅助”表中的行。在这种情况下,SQLAlchemy 可以被指示不要主动加载 Child.parents 集合,使用 relationship.passive_deletes 指令在 relationship() 上;有关此更多详细信息,请参阅 使用外键 ON DELETE cascade 处理 ORM 关系。

再次注意,这些行为relationship()relationship.secondary 选项相关。如果处理显式映射的关联表,而不是存在于相关 relationship()relationship.secondary 选项中的关联表,那么级联规则可以被用来在相关实体被删除时自动删除实体 - 有关此功能的信息,请参阅 级联。

另请参阅

使用多对多关系的级联删除

使用外键 ON DELETE 处理多对多关系

协会对象

协会对象模式是多对多关系的一种变体:当一个关联表包含除了那些与父表和子表(或左表和右表)的外键不同的额外列时,通常最理想的是将这些列映射到自己的 ORM 映射类。这个映射类被映射到了Table,在使用多对多模式时,它本来会被指定为 relationship.secondary

在关联对象模式中,不使用relationship.secondary参数;相反,将类直接映射到关联表。然后,两个独立的relationship()构造首先通过一对多将父侧链接到映射的关联类,然后通过多对一将映射的关联类链接到子侧,以形成从父对象到关联对象到子对象的单向关联对象关系。对于双向关系,使用四个relationship()构造将映射的关联类与父对象和子对象在两个方向上进行链接。

下面的示例说明了一个新的类Association,它映射到名为associationTable;此表现在包括一个额外的列称为extra_data,它是一个字符串值,与ParentChild之间的每个关联一起存储。通过将表映射到显式类,从ParentChild的基本访问明确使用了Association

from typing import Optional

from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

class Association(Base):
    __tablename__ = "association_table"
    left_id: Mapped[int] = mapped_column(ForeignKey("left_table.id"), primary_key=True)
    right_id: Mapped[int] = mapped_column(
        ForeignKey("right_table.id"), primary_key=True
    )
    extra_data: Mapped[Optional[str]]
    child: Mapped["Child"] = relationship()

class Parent(Base):
    __tablename__ = "left_table"
    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Association"]] = relationship()

class Child(Base):
    __tablename__ = "right_table"
    id: Mapped[int] = mapped_column(primary_key=True)

为了说明双向版本,我们添加了两个更多的relationship()构造,使用relationship.back_populates连接到现有的构造:

from typing import Optional

from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

class Association(Base):
    __tablename__ = "association_table"
    left_id: Mapped[int] = mapped_column(ForeignKey("left_table.id"), primary_key=True)
    right_id: Mapped[int] = mapped_column(
        ForeignKey("right_table.id"), primary_key=True
    )
    extra_data: Mapped[Optional[str]]
    child: Mapped["Child"] = relationship(back_populates="parents")
    parent: Mapped["Parent"] = relationship(back_populates="children")

class Parent(Base):
    __tablename__ = "left_table"
    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Association"]] = relationship(back_populates="parent")

class Child(Base):
    __tablename__ = "right_table"
    id: Mapped[int] = mapped_column(primary_key=True)
    parents: Mapped[List["Association"]] = relationship(back_populates="child")

使用关联对象模式的直接形式需要在将子对象附加到父对象之前将其与关联实例关联;同样,从父对象到子对象的访问需要通过关联对象进行:

# create parent, append a child via association
p = Parent()
a = Association(extra_data="some data")
a.child = Child()
p.children.append(a)

# iterate through child objects via association, including association
# attributes
for assoc in p.children:
    print(assoc.extra_data)
    print(assoc.child)

为了增强关联对象模式,使得对Association对象的直接访问是可选的,SQLAlchemy 提供了关联代理扩展。该扩展允许配置属性,这些属性将通过单个访问实现两次“跳跃”,一次是到关联对象,另一次是到目标属性。

另请参阅

关联代理 - 允许在三类关联对象映射中在父对象和子对象之间直接进行“多对多”样式的访问。

警告

避免直接混合使用关联对象模式和多对多模式,因为这会导致数据可能以不一致的方式读取和写入,除非采取特殊步骤;关联代理通常用于提供更简洁的访问。有关此组合引入的注意事项的更详细背景,请参阅下一节将关联对象与多对多访问模式组合使用。

将关联对象与多对多访问模式结合使用

如前一节所述,关联对象模式不会自动与相同表/列的多对多模式集成。由此可知,读操作可能返回冲突数据,写操作也可能尝试刷新冲突更改,导致完整性错误或意外插入或删除。

为了说明,下面的示例配置了ParentChild之间的双向多对多关系,通过Parent.childrenChild.parents。同时,还配置了一个关联对象关系,即Parent.child_associations -> Association.childChild.parent_associations -> Association.parent之间的关系:

from typing import Optional

from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

class Association(Base):
    __tablename__ = "association_table"

    left_id: Mapped[int] = mapped_column(ForeignKey("left_table.id"), primary_key=True)
    right_id: Mapped[int] = mapped_column(
        ForeignKey("right_table.id"), primary_key=True
    )
    extra_data: Mapped[Optional[str]]

    # association between Assocation -> Child
    child: Mapped["Child"] = relationship(back_populates="parent_associations")

    # association between Assocation -> Parent
    parent: Mapped["Parent"] = relationship(back_populates="child_associations")

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)

    # many-to-many relationship to Child, bypassing the `Association` class
    children: Mapped[List["Child"]] = relationship(
        secondary="association_table", back_populates="parents"
    )

    # association between Parent -> Association -> Child
    child_associations: Mapped[List["Association"]] = relationship(
        back_populates="parent"
    )

class Child(Base):
    __tablename__ = "right_table"

    id: Mapped[int] = mapped_column(primary_key=True)

    # many-to-many relationship to Parent, bypassing the `Association` class
    parents: Mapped[List["Parent"]] = relationship(
        secondary="association_table", back_populates="children"
    )

    # association between Child -> Association -> Parent
    parent_associations: Mapped[List["Association"]] = relationship(
        back_populates="child"
    )

使用此 ORM 模型进行更改时,对Parent.children进行的更改不会与在 Python 中对Parent.child_associationsChild.parent_associations进行的更改协调;虽然所有这些关系将继续正常运作,但一个上的更改不会显示在另一个上,直到Session过期,通常在Session.commit()之后会自动发生。

此外,如果发生冲突更改,例如同时添加新的Association对象并将相同相关的Child附加到Parent.children,则在工作单元刷新过程中会引发完整性错误,如下例所示:

p1 = Parent()
c1 = Child()
p1.children.append(c1)

# redundant, will cause a duplicate INSERT on Association
p1.child_associations.append(Association(child=c1))

直接将Parent.children附加Child也意味着在association表中创建行,而不指定association.extra_data列的任何值,该列将接收NULL作为其值。

如果知道自己在做什么,使用上述映射是可以的;在很少使用“关联对象”模式的情况下使用多对多关系可能有充分的理由,因为在单个多对多关系中加载关系更容易,这也可以稍微优化“secondary”表在 SQL 语句中的使用方式,与两个分开的关系到显式关联类的使用方式相比。至少最好将relationship.viewonly参数应用于“secondary”关系,以避免发生冲突更改的问题,并防止将NULL写入附加的关联列,如下所示:

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)

    # many-to-many relationship to Child, bypassing the `Association` class
    children: Mapped[List["Child"]] = relationship(
        secondary="association_table", back_populates="parents", viewonly=True
    )

    # association between Parent -> Association -> Child
    child_associations: Mapped[List["Association"]] = relationship(
        back_populates="parent"
    )

class Child(Base):
    __tablename__ = "right_table"

    id: Mapped[int] = mapped_column(primary_key=True)

    # many-to-many relationship to Parent, bypassing the `Association` class
    parents: Mapped[List["Parent"]] = relationship(
        secondary="association_table", back_populates="children", viewonly=True
    )

    # association between Child -> Association -> Parent
    parent_associations: Mapped[List["Association"]] = relationship(
        back_populates="child"
    )

上述映射不会将任何更改写入到数据库的Parent.childrenChild.parents,从而防止冲突的写入。然而,如果在相同事务或Session中对这些集合进行更改,那么对Parent.childrenChild.parents的读取将不一定匹配从Parent.child_associationsChild.parent_associations读取的数据。如果关联对象关系的使用不频繁,并且针对访问多对多集合的代码进行了精心组织以避免过时的读取(在极端情况下,直接使用Session.expire()来使集合在当前事务中刷新),那么这种模式可能是可行的。

上述模式的一种流行替代方案是,直接的多对多Parent.childrenChild.parents关系被一个扩展所取代,该扩展将通过Association类透明地代理,同时从 ORM 的角度保持一切一致。这个扩展被称为关联代理。

另请参阅

关联代理 - 允许在三类关联对象映射之间直接进行“多对多”样式的父子访问。### 将关联对象与多对多访问模式结合使用

如前所述,在上一节中,关联对象模式不会自动与同时针对相同表/列使用的多对多模式集成。由此可见,读取操作可能会返回冲突的数据,并且写入操作也可能尝试刷新冲突的更改,导致完整性错误或意外的插入或删除。

为了说明,下面的示例配置了ParentChild之间的双向多对多关系,通过Parent.childrenChild.parents。同时,还配置了一个关联对象关系,Parent.child_associations -> Association.childChild.parent_associations -> Association.parent

from typing import Optional

from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

class Association(Base):
    __tablename__ = "association_table"

    left_id: Mapped[int] = mapped_column(ForeignKey("left_table.id"), primary_key=True)
    right_id: Mapped[int] = mapped_column(
        ForeignKey("right_table.id"), primary_key=True
    )
    extra_data: Mapped[Optional[str]]

    # association between Assocation -> Child
    child: Mapped["Child"] = relationship(back_populates="parent_associations")

    # association between Assocation -> Parent
    parent: Mapped["Parent"] = relationship(back_populates="child_associations")

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)

    # many-to-many relationship to Child, bypassing the `Association` class
    children: Mapped[List["Child"]] = relationship(
        secondary="association_table", back_populates="parents"
    )

    # association between Parent -> Association -> Child
    child_associations: Mapped[List["Association"]] = relationship(
        back_populates="parent"
    )

class Child(Base):
    __tablename__ = "right_table"

    id: Mapped[int] = mapped_column(primary_key=True)

    # many-to-many relationship to Parent, bypassing the `Association` class
    parents: Mapped[List["Parent"]] = relationship(
        secondary="association_table", back_populates="children"
    )

    # association between Child -> Association -> Parent
    parent_associations: Mapped[List["Association"]] = relationship(
        back_populates="child"
    )

当使用此 ORM 模型进行更改时,在 Python 中对Parent.children进行的更改不会与对Parent.child_associationsChild.parent_associations进行的更改协调;虽然所有这些关系都将继续正常运行,但在Session过期之前,一个的更改不会显示在另一个上,Session.commit()通常会在自动发生后使之过期。

另外,如果发生冲突的更改,例如同时添加一个新的Association对象,同时将相同的相关Child附加到Parent.children,则在工作单元刷新过程进行时,会引发完整性错误,如下例所示:

p1 = Parent()
c1 = Child()
p1.children.append(c1)

# redundant, will cause a duplicate INSERT on Association
p1.child_associations.append(Association(child=c1))

直接将Child附加到Parent.children也意味着在association表中创建行,而不指定association.extra_data列的任何值,该列的值将为NULL

如果你知道自己在做什么,像上面的映射那样使用映射是可以的;在很少使用“关联对象”模式的情况下使用多对多关系可能是有充分理由的,这是因为沿着单一的多对多关系加载关系是更容易的,这也可以略微优化“辅助”表在 SQL 语句中的使用方式,与如何使用两个到显式关联类的分离关系相比。至少应该将relationship.viewonly参数应用于“辅助”关系,以避免出现冲突更改的问题,并防止将NULL写入附加的关联列,如下所示:

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)

    # many-to-many relationship to Child, bypassing the `Association` class
    children: Mapped[List["Child"]] = relationship(
        secondary="association_table", back_populates="parents", viewonly=True
    )

    # association between Parent -> Association -> Child
    child_associations: Mapped[List["Association"]] = relationship(
        back_populates="parent"
    )

class Child(Base):
    __tablename__ = "right_table"

    id: Mapped[int] = mapped_column(primary_key=True)

    # many-to-many relationship to Parent, bypassing the `Association` class
    parents: Mapped[List["Parent"]] = relationship(
        secondary="association_table", back_populates="children", viewonly=True
    )

    # association between Child -> Association -> Parent
    parent_associations: Mapped[List["Association"]] = relationship(
        back_populates="child"
    )

上面的映射不会将对Parent.childrenChild.parents的任何更改写入数据库,从而防止冲突写入。但是,如果在相同的事务或Session中对这些集合进行更改的地方读取Parent.childrenChild.parents将不一定与从Parent.child_associationsChild.parent_associations中读取的数据匹配。如果对关联对象关系的使用不频繁,并且针对访问多对多集合的代码进行了精心组织以避免过时读取(在极端情况下,直接使用Session.expire()来导致集合在当前事务中被刷新),那么这种模式可能是可行的。

一个流行的替代模式是,直接的多对多Parent.childrenChild.parents关系被一个扩展所取代,该扩展将通过Association类透明地代理,同时从 ORM 的角度保持一切一致。这个扩展被称为关联代理。

另请参阅

关联代理 - 允许在三类关联对象映射中直接实现“多对多”样式的父子访问。

关系参数的延迟评估

大多数前面部分的示例展示了映射,其中各种relationship()构造使用字符串名称而不是类本身引用其目标类,例如在使用Mapped时,会生成一个仅作为字符串存在的前向引用:

class Parent(Base):
    # ...

    children: Mapped[List["Child"]] = relationship(back_populates="parent")

class Child(Base):
    # ...

    parent: Mapped["Parent"] = relationship(back_populates="children")

同样,在使用非注释形式,如非注释性的声明式或命令式映射时,relationship()构造也直接支持字符串名称:

registry.map_imperatively(
    Parent,
    parent_table,
    properties={"children": relationship("Child", back_populates="parent")},
)

registry.map_imperatively(
    Child,
    child_table,
    properties={"parent": relationship("Parent", back_populates="children")},
)

这些字符串名称在映射器解析阶段被解析为类,这是一个内部过程,通常在定义所有映射之后发生,并且通常由映射本身的第一次使用触发。registry对象是这些名称存储和解析为它们引用的映射类的容器。

除了relationship()的主要类参数之外,还可以指定依赖于尚未定义类上存在的列的其他参数,这些参数可以是 Python 函数,或更常见的是字符串。对于这些参数中的大多数,除了主要参数之外,字符串输入都会使用 Python 内置的 eval()函数评估为 Python 表达式,因为它们旨在接收完整的 SQL 表达式。

警告

由于 Python 的eval()函数用于解释传递给relationship()映射配置构造的延迟评估的字符串参数,这些参数不应该被重新用于接收不受信任的用户输入;eval()对不受信任的用户输入不安全

在这个评估中可用的完整命名空间包括为这个声明基类映射的所有类,以及sqlalchemy包的内容,包括表达式函数如desc()sqlalchemy.sql.functions.func

class Parent(Base):
    # ...

    children: Mapped[List["Child"]] = relationship(
        order_by="desc(Child.email_address)",
        primaryjoin="Parent.id == Child.parent_id",
    )

对于一个模块包含多个同名类的情况,字符串类名也可以在这些字符串表达式中作为模块限定路径指定:

class Parent(Base):
    # ...

    children: Mapped[List["myapp.mymodel.Child"]] = relationship(
        order_by="desc(myapp.mymodel.Child.email_address)",
        primaryjoin="myapp.mymodel.Parent.id == myapp.mymodel.Child.parent_id",
    )

在上述示例中,传递给Mapped的字符串也可以通过直接将类位置字符串传递给relationship.argument来消除特定类参数。下面说明了仅类型导入Child的示例,结合了将运行时说明符与将在registry中搜索正确名称的目标类相结合:

import typing

if typing.TYPE_CHECKING:
    from myapp.mymodel import Child

class Parent(Base):
    # ...

    children: Mapped[List["Child"]] = relationship(
        "myapp.mymodel.Child",
        order_by="desc(myapp.mymodel.Child.email_address)",
        primaryjoin="myapp.mymodel.Parent.id == myapp.mymodel.Child.parent_id",
    )

合格路径可以是任何消除名称之间歧义的部分路径。例如,要消除myapp.model1.Childmyapp.model2.Child之间的歧义,我们可以指定model1.Childmodel2.Child

class Parent(Base):
    # ...

    children: Mapped[List["Child"]] = relationship(
        "model1.Child",
        order_by="desc(mymodel1.Child.email_address)",
        primaryjoin="Parent.id == model1.Child.parent_id",
    )

relationship()构造还接受 Python 函数或 lambda 作为这些参数的输入。Python 函数式方法可能如下所示:

import typing

from sqlalchemy import desc

if typing.TYPE_CHECKING:
    from myapplication import Child

def _resolve_child_model():
    from myapplication import Child

    return Child

class Parent(Base):
    # ...

    children: Mapped[List["Child"]] = relationship(
        _resolve_child_model,
        order_by=lambda: desc(_resolve_child_model().email_address),
        primaryjoin=lambda: Parent.id == _resolve_child_model().parent_id,
    )

完整的参数列表接受 Python 函数/lambda 或将传递给eval()的字符串的参数包括:

  • relationship.order_by

  • relationship.primaryjoin

  • relationship.secondaryjoin

  • relationship.secondary

  • relationship.remote_side

  • relationship.foreign_keys

  • relationship._user_defined_foreign_keys

警告

如前所述,relationship()中的上述参数会作为 Python 代码表达式使用 eval()进行评估。不要将不受信任的输入传递给这些参数。

在声明后将关系添加到映射类

还应注意,与向现有的声明映射类添加附加列中描述的类似方式,任何MapperProperty构造都可以随时添加到声明基础映射中(注意在此上下文中不支持注释形式)。如果我们想要在Address类可用之后实现这个relationship(),我们也可以随后应用它:

# first, module A, where Child has not been created yet,
# we create a Parent class which knows nothing about Child

class Parent(Base): ...

# ... later, in Module B, which is imported after module A:

class Child(Base): ...

from module_a import Parent

# assign the User.addresses relationship as a class variable.  The
# declarative base class will intercept this and map the relationship.
Parent.children = relationship(Child, primaryjoin=Child.parent_id == Parent.id)

与 ORM 映射列一样,Mapped注解类型无法参与此操作;因此,相关类必须直接在relationship()构造中指定,可以是类本身、类的字符串名称或返回目标类引用的可调用函数。

注意

与 ORM 映射列一样,对已映射类的映射属性的赋值仅在使用“声明基类”类时才能正确执行,这意味着用户定义的DeclarativeBase子类或declarative_base()返回的动态生成类或registry.generate_base()。这个“基”类包括一个 Python 元类,实现了一个特殊的__setattr__()方法来拦截这些操作。

如果类使用像registry.mapped()这样的装饰器或像registry.map_imperatively()这样的命令式函数进行映射,则无法在运行时将类映射属性分配给映射类。 ### 使用多对多关系的“secondary”参数的延迟评估形式

多对多关系使用relationship.secondary参数,通常表示对通常不映射的Table对象或其他 Core 可选择对象的引用。通常使用 lambda 可调用进行延迟评估。

对于 Many To Many 中给出的示例,如果我们假设association_table Table对象将在模块中稍后定义,则我们可以使用 lambda 编写relationship()

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship(
        "Child", secondary=lambda: association_table
    )

作为也是有效 Python 标识符的表名的快捷方式,relationship.secondary参数也可以作为字符串传递,其中解析通过将字符串作为 Python 表达式进行评估来完成,简单标识符名称链接到当前registry引用的相同命名的Table对象。

在下面的示例中,表达式"association_table"将作为名为"association_table"的变量进行评估,该变量将根据MetaData集合中的表名进行解析:

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship(secondary="association_table")

注意

当作为字符串传递时,传递给relationship.secondary的名称必须是有效的 Python 标识符,以字母开头,只包含字母数字字符或下划线。其他字符,如破折号等,将被解释为 Python 运算符,而不会解析为给定的名称。请考虑使用 lambda 表达式而不是字符串以提高清晰度。

警告

当作为字符串传递时,relationship.secondary参数使用 Python 的eval()函数进行解释,即使它通常是一个表的名称。不要将不受信任的输入传递给此字符串。###在声明后向映射类添加关系

还应注意,在类似于 Appending additional columns to an existing Declarative mapped class 描述的方式中,任何MapperProperty构造都可以随时添加到声明基本映射中(注意,此上下文中不支持注释形式)。如果我们希望在Address类可用后实现此relationship(),我们也可以随后应用它:

# first, module A, where Child has not been created yet,
# we create a Parent class which knows nothing about Child

class Parent(Base): ...

# ... later, in Module B, which is imported after module A:

class Child(Base): ...

from module_a import Parent

# assign the User.addresses relationship as a class variable.  The
# declarative base class will intercept this and map the relationship.
Parent.children = relationship(Child, primaryjoin=Child.parent_id == Parent.id)

与 ORM 映射列一样,Mapped注解类型没有参与此操作的能力;因此,相关类必须直接在relationship()构造中指定,可以是类本身、类的字符串名称,或者返回目标类引用的可调用函数。

注意

与 ORM 映射列一样,将映射属性分配给已经映射的类只有在使用“声明式基类”时才能正确运行,这意味着必须使用用户定义的DeclarativeBase子类或者declarative_base()返回的动态生成的类或者registry.generate_base()返回的动态生成的类。这个“基类”包含一个实现了特殊__setattr__()方法的 Python 元类,它拦截这些操作。

如果使用类似于registry.mapped()这样的装饰器或像registry.map_imperatively()这样的命令式函数来映射类,则无法在运行时将映射属性分配给映射类。

使用“secondary”参数的延迟评估形式来处理多对多关系

多对多关系使用relationship.secondary参数,通常表示对通常非映射的Table对象或其他核心可选择对象的引用。典型的延迟评估使用 lambda 可调用。

对于 Many To Many 中给出的例子,如果我们假设association_table Table对象将在模块中的某个后续点被定义,那么我们可以使用 lambda 来编写relationship(),如下所示:

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship(
        "Child", secondary=lambda: association_table
    )

作为表名的快捷方式,也可以将relationship.secondary参数传递为字符串,其中解析工作通过将字符串作为 Python 表达式进行评估,简单标识符名称链接到与当前registry引用的相同MetaData集合中存在的同名Table对象。

在下面的示例中,表达式"association_table"被解析为一个名为"association_table"的变量,该变量根据MetaData集合中的表名解析:

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship(secondary="association_table")

注意

当作为字符串传递时,传递给relationship.secondary的名称必须是有效的 Python 标识符,以字母开头,仅包含字母数字字符或下划线。其他字符,如破折号等,将被解释为 Python 操作符,而不会解析为给定的名称。请考虑使用 lambda 表达式而不是字符串,以提高清晰度。

警告

当作为字符串传递时,relationship.secondary参数将使用 Python 的eval()函数进行解释,即使它通常是一个表的名称。不要将不受信任的输入传递给该字符串

邻接列表关系

原文:docs.sqlalchemy.org/en/20/orm/self_referential.html

邻接列表模式是一种常见的关系模式,其中表包含对自身的外键引用,换句话说是自引用关系。这是在平面表中表示层次数据的最常见方法。其他方法包括嵌套集,有时称为“修改的先序”,以及材料路径。尽管在 SQL 查询中评估其流畅性时修改的先序具有吸引力,但邻接列表模型可能是满足大多数层次存储需求的最合适模式,原因是并发性、减少的复杂性,以及修改的先序对于能够完全加载子树到应用程序空间的应用程序几乎没有优势。

另请参阅

此部分详细说明了自引用关系的单表版本。有关使用第二个表作为关联表的自引用关系,请参阅自引用多对多关系部分。

在本示例中,我们将使用一个名为Node的单个映射类,表示树结构:

class Node(Base):
    __tablename__ = "node"
    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(Integer, ForeignKey("node.id"))
    data = mapped_column(String(50))
    children = relationship("Node")

使用此结构,可以构建如下的图形:

root --+---> child1
       +---> child2 --+--> subchild1
       |              +--> subchild2
       +---> child3

可以用数据表示为:

id       parent_id     data
---      -------       ----
1        NULL          root
2        1             child1
3        1             child2
4        3             subchild1
5        3             subchild2
6        1             child3

这里的relationship()配置与“正常”的一对多关系的工作方式相同,唯一的例外是,“方向”,即关系是一对多还是多对一,默认假定为一对多。要将关系建立为多对一,需要添加一个额外的指令,称为relationship.remote_side,它是一个Column或一组Column对象,指示应该被视为“远程”的对象:

class Node(Base):
    __tablename__ = "node"
    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(Integer, ForeignKey("node.id"))
    data = mapped_column(String(50))
    parent = relationship("Node", remote_side=[id])

在上述情况下,id列被应用为parent relationship()relationship.remote_side,从而将parent_id建立为“本地”端,并且关系随后表现为多对一。

一如既往,两个方向可以结合成一个双向关系,使用两个由relationship.back_populates链接的relationship()构造。

class Node(Base):
    __tablename__ = "node"
    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(Integer, ForeignKey("node.id"))
    data = mapped_column(String(50))
    children = relationship("Node", back_populates="parent")
    parent = relationship("Node", back_populates="children", remote_side=[id])

另请参阅

邻接列表 - 更新为 SQLAlchemy 2.0 的工作示例

复合邻接列表

邻接列表关系的一个子类别是在连接条件的“本地”和“远程”两侧都存在特定列的罕见情况。下面是Folder类的一个示例;使用复合主键,account_id列指向自身,以指示位于与父文件夹相同帐户内的子文件夹;而folder_id则指向该帐户内的特定文件夹:

class Folder(Base):
    __tablename__ = "folder"
    __table_args__ = (
        ForeignKeyConstraint(
            ["account_id", "parent_id"], ["folder.account_id", "folder.folder_id"]
        ),
    )

    account_id = mapped_column(Integer, primary_key=True)
    folder_id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(Integer)
    name = mapped_column(String)

    parent_folder = relationship(
        "Folder", back_populates="child_folders", remote_side=[account_id, folder_id]
    )

    child_folders = relationship("Folder", back_populates="parent_folder")

在上述示例中,我们将account_id传递到relationship.remote_side列表中。relationship()识别到这里的account_id列在两侧都存在,并将“远程”列与它识别为唯一存在于“远程”侧的folder_id列对齐。

自引用查询策略

查询自引用结构的方式与任何其他查询相同:

# get all nodes named 'child2'
session.scalars(select(Node).where(Node.data == "child2"))

但是,当尝试沿着树的一个级别从一个外键连接到下一个级别时,需要额外小心。在 SQL 中,从表连接到自身的连接需要至少对表达式的一侧进行“别名”,以便可以明确引用它。

请回想一下 ORM 教程中的选择 ORM 别名,aliased()结构通常用于提供 ORM 实体的“别名”。使用此技术从Node连接到自身的连接如下所示:

from sqlalchemy.orm import aliased

nodealias = aliased(Node)
session.scalars(
    select(Node)
    .where(Node.data == "subchild1")
    .join(Node.parent.of_type(nodealias))
    .where(nodealias.data == "child2")
).all()
SELECT  node.id  AS  node_id,
  node.parent_id  AS  node_parent_id,
  node.data  AS  node_data
FROM  node  JOIN  node  AS  node_1
  ON  node.parent_id  =  node_1.id
WHERE  node.data  =  ?
  AND  node_1.data  =  ?
['subchild1',  'child2'] 
```  ## 配置自引用的急切加载

在正常查询操作期间,通过从父表到子表的连接或外连接来发生关系的急切加载,以便可以从单个 SQL 语句或所有子集合的第二个语句中填充父对象及其直接子集合或引用。SQLAlchemy 的连接和子查询急切加载在连接到相关项时在所有情况下使用别名表,因此与自引用连接兼容。然而,要使用自引用关系进行急切加载,SQLAlchemy 需要告知应该连接和/或查询多少级深度;否则,急切加载将根本不会发生。此深度设置通过`relationships.join_depth`进行配置:

```py
class Node(Base):
    __tablename__ = "node"
    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(Integer, ForeignKey("node.id"))
    data = mapped_column(String(50))
    children = relationship("Node", lazy="joined", join_depth=2)

session.scalars(select(Node)).all()
SELECT  node_1.id  AS  node_1_id,
  node_1.parent_id  AS  node_1_parent_id,
  node_1.data  AS  node_1_data,
  node_2.id  AS  node_2_id,
  node_2.parent_id  AS  node_2_parent_id,
  node_2.data  AS  node_2_data,
  node.id  AS  node_id,
  node.parent_id  AS  node_parent_id,
  node.data  AS  node_data
FROM  node
  LEFT  OUTER  JOIN  node  AS  node_2
  ON  node.id  =  node_2.parent_id
  LEFT  OUTER  JOIN  node  AS  node_1
  ON  node_2.id  =  node_1.parent_id
[] 

复合邻接列表

邻接列表关系的一个子类别是在连接条件的“本地”和“远程”两侧都存在特定列的罕见情况。下面是Folder类的一个示例;使用复合主键,account_id列指向自身,以指示位于与父文件夹相同帐户内的子文件夹;而folder_id则指向该帐户内的特定文件夹:

class Folder(Base):
    __tablename__ = "folder"
    __table_args__ = (
        ForeignKeyConstraint(
            ["account_id", "parent_id"], ["folder.account_id", "folder.folder_id"]
        ),
    )

    account_id = mapped_column(Integer, primary_key=True)
    folder_id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(Integer)
    name = mapped_column(String)

    parent_folder = relationship(
        "Folder", back_populates="child_folders", remote_side=[account_id, folder_id]
    )

    child_folders = relationship("Folder", back_populates="parent_folder")

在上面的例子中,我们将account_id传递到relationship.remote_side列表中。relationship()识别到这里的account_id列在两侧均存在,并且将“远程”列与它识别为唯一存在于“远程”一侧的folder_id列对齐。

自引用查询策略

自引用结构的查询与任何其他查询相同:

# get all nodes named 'child2'
session.scalars(select(Node).where(Node.data == "child2"))

但是,在尝试从树的一级到下一级进行连接时需要特别注意。在 SQL 中,从表连接到自身需要至少一个表达式的一侧被“别名”,以便可以明确地引用它。

请回想一下在 ORM 教程中选择 ORM 别名,aliased()构造通常用于提供 ORM 实体的“别名”。使用这种技术从Node到自身的连接看起来像:

from sqlalchemy.orm import aliased

nodealias = aliased(Node)
session.scalars(
    select(Node)
    .where(Node.data == "subchild1")
    .join(Node.parent.of_type(nodealias))
    .where(nodealias.data == "child2")
).all()
SELECT  node.id  AS  node_id,
  node.parent_id  AS  node_parent_id,
  node.data  AS  node_data
FROM  node  JOIN  node  AS  node_1
  ON  node.parent_id  =  node_1.id
WHERE  node.data  =  ?
  AND  node_1.data  =  ?
['subchild1',  'child2'] 

配置自引用关系的急切加载

通过在正常查询操作期间从父表到子表使用连接或外连接来进行关系的急切加载,以便可以从单个 SQL 语句或所有直接子集合的第二个语句中填充父表及其直接子集合或引用。SQLAlchemy 的连接和子查询急切加载在加入相关项时始终使用别名表,因此与自引用连接兼容。然而,要想使用自引用关系的急切加载,需要告诉 SQLAlchemy 应该加入和/或查询多少级深度;否则,急切加载将根本不会发生。此深度设置通过relationships.join_depth进行配置:

class Node(Base):
    __tablename__ = "node"
    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(Integer, ForeignKey("node.id"))
    data = mapped_column(String(50))
    children = relationship("Node", lazy="joined", join_depth=2)

session.scalars(select(Node)).all()
SELECT  node_1.id  AS  node_1_id,
  node_1.parent_id  AS  node_1_parent_id,
  node_1.data  AS  node_1_data,
  node_2.id  AS  node_2_id,
  node_2.parent_id  AS  node_2_parent_id,
  node_2.data  AS  node_2_data,
  node.id  AS  node_id,
  node.parent_id  AS  node_parent_id,
  node.data  AS  node_data
FROM  node
  LEFT  OUTER  JOIN  node  AS  node_2
  ON  node.id  =  node_2.parent_id
  LEFT  OUTER  JOIN  node  AS  node_1
  ON  node_2.id  =  node_1.parent_id
[] 

配置关系连接

原文:docs.sqlalchemy.org/en/20/orm/join_conditions.html

relationship()通常会通过检查两个表之间的外键关系来创建两个表之间的连接,以确定应该比较哪些列。有各种情况需要定制此行为。

处理多个连接路径

处理的最常见情况之一是两个表之间存在多个外键路径时。

考虑一个包含两个外键到Address类的Customer类:

from sqlalchemy import Integer, ForeignKey, String, Column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

class Customer(Base):
    __tablename__ = "customer"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String)

    billing_address_id = mapped_column(Integer, ForeignKey("address.id"))
    shipping_address_id = mapped_column(Integer, ForeignKey("address.id"))

    billing_address = relationship("Address")
    shipping_address = relationship("Address")

class Address(Base):
    __tablename__ = "address"
    id = mapped_column(Integer, primary_key=True)
    street = mapped_column(String)
    city = mapped_column(String)
    state = mapped_column(String)
    zip = mapped_column(String)

当我们尝试使用上述映射时,将产生错误:

sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join
condition between parent/child tables on relationship
Customer.billing_address - there are multiple foreign key
paths linking the tables.  Specify the 'foreign_keys' argument,
providing a list of those columns which should be
counted as containing a foreign key reference to the parent table.

上述消息相当长。relationship()可以返回许多潜在消息,这些消息已经被精心设计用于检测各种常见的配置问题;大多数都会建议需要哪些额外配置来解决模糊或其他缺失的信息。

在这种情况下,消息希望我们通过为每个指定的relationship()指定应该考虑哪个外键列来修饰每一个,并且适当的形式如下:

class Customer(Base):
    __tablename__ = "customer"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String)

    billing_address_id = mapped_column(Integer, ForeignKey("address.id"))
    shipping_address_id = mapped_column(Integer, ForeignKey("address.id"))

    billing_address = relationship("Address", foreign_keys=[billing_address_id])
    shipping_address = relationship("Address", foreign_keys=[shipping_address_id])

在上面,我们指定了foreign_keys参数,它是一个ColumnColumn对象的列表,指示要考虑的“外键”列,或者换句话说,包含引用父表的值的列。从Customer对象加载Customer.billing_address关系将使用billing_address_id中的值来标识要加载的Address行;类似地,shipping_address_id用于shipping_address关系。这两列的关联在持久性期间也起到了作用;刚刚插入的Address对象的新生成的主键将在刷新期间复制到关联的Customer对象的适当外键列中。

在使用 Declarative 指定foreign_keys时,我们还可以使用字符串名称进行指定,但是如果使用列表,列表应该是字符串的一部分是很重要的:

billing_address = relationship("Address", foreign_keys="[Customer.billing_address_id]")

在这个具体的例子中,在任何情况下列表都是不必要的,因为我们只需要一个Column

billing_address = relationship("Address", foreign_keys="Customer.billing_address_id")

警告

当作为 Python 可执行字符串传递时,relationship.foreign_keys 参数将使用 Python 的 eval() 函数进行解释。请勿将不受信任的输入传递给此字符串。详细信息请参见关系参数的评估。

relationship() 在构建连接时的默认行为是将一侧的主键列的值等同于另一侧的外键引用列的值。我们可以使用 relationship.primaryjoin 参数来更改此条件,以及在使用“次要”表时,还可以使用 relationship.secondaryjoin 参数。

在下面的示例中,我们使用 User 类以及存储街道地址的 Address 类来创建一个关系 boston_addresses,它将仅加载指定城市为“波士顿”的 Address 对象:

from sqlalchemy import Integer, ForeignKey, String, Column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String)
    boston_addresses = relationship(
        "Address",
        primaryjoin="and_(User.id==Address.user_id, Address.city=='Boston')",
    )

class Address(Base):
    __tablename__ = "address"
    id = mapped_column(Integer, primary_key=True)
    user_id = mapped_column(Integer, ForeignKey("user.id"))

    street = mapped_column(String)
    city = mapped_column(String)
    state = mapped_column(String)
    zip = mapped_column(String)

在此字符串 SQL 表达式中,我们使用了 and_() 连接构造来为连接条件建立两个不同的谓词 - 将 User.idAddress.user_id 列相互连接,同时将 Address 中的行限制为只有 city='Boston'。在使用声明式时,诸如 and_() 这样的基本 SQL 函数会自动在字符串 relationship() 参数的评估命名空间中可用。

警告

当作为 Python 可执行字符串传递时,relationship.primaryjoin 参数将使用 Python 的 eval() 函数进行解释。请勿将不受信任的输入传递给此字符串。详细信息请参见关系参数的评估。

我们在relationship.primaryjoin中使用的自定义标准通常只在 SQLAlchemy 渲染 SQL 以加载或表示此关系时才重要。也就是说,它用于在执行每个属性的延迟加载时发出的 SQL 语句中,或者在查询时构造联接时,例如通过Select.join()或通过渴望的“joined”或“subquery”加载样式。当操作内存中的对象时,我们可以将任何我们想要的Address对象放入boston_addresses集合中,而不管.city属性的值是什么。这些对象将一直保留在集合中,直到属性过期并重新从应用准则的数据库中加载为止。当发生刷新时,boston_addresses内的对象将被无条件地刷新,将主键user.id列的值分配给每一行的持有外键的address.user_id列。在这里,city标准没有影响,因为刷新过程只关心将主键值同步到引用外键值中。## 创建自定义外键条件

主要连接条件的另一个元素是如何确定那些被认为是“外部”的列的。通常,一些Column对象的子集将指定ForeignKey,或者否则将是与连接条件相关的ForeignKeyConstraint的一部分。relationship()查找此外键状态,因为它决定了它应该如何加载和持久化此关系的数据。然而,relationship.primaryjoin参数可以用来创建不涉及任何“架构”级外键的连接条件。我们可以结合relationship.primaryjoin以及relationship.foreign_keysrelationship.remote_side显式地建立这样一个连接。

下面,一个名为HostEntry的类与自身连接,将字符串content列与ip_address列相等,后者是一种名为INET的 PostgreSQL 类型。我们需要使用cast()来将连接的一侧转换为另一侧的类型:

from sqlalchemy import cast, String, Column, Integer
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import INET

from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

class HostEntry(Base):
    __tablename__ = "host_entry"

    id = mapped_column(Integer, primary_key=True)
    ip_address = mapped_column(INET)
    content = mapped_column(String(50))

    # relationship() using explicit foreign_keys, remote_side
    parent_host = relationship(
        "HostEntry",
        primaryjoin=ip_address == cast(content, INET),
        foreign_keys=content,
        remote_side=ip_address,
    )

上述关系将产生如下连接:

SELECT  host_entry.id,  host_entry.ip_address,  host_entry.content
FROM  host_entry  JOIN  host_entry  AS  host_entry_1
ON  host_entry_1.ip_address  =  CAST(host_entry.content  AS  INET)

上述的另一种替代语法是在relationship.primaryjoin表达式内联使用foreign()remote()注释。此语法表示了relationship()通常自动应用于连接条件的注释,给定了relationship.foreign_keysrelationship.remote_side参数。当存在显式连接条件时,这些函数可能更加简洁,并且还标记了“外键”或“远程”列的确切位置,无论该列是否多次声明或在复杂的 SQL 表达式中声明:

from sqlalchemy.orm import foreign, remote

class HostEntry(Base):
    __tablename__ = "host_entry"

    id = mapped_column(Integer, primary_key=True)
    ip_address = mapped_column(INET)
    content = mapped_column(String(50))

    # relationship() using explicit foreign() and remote() annotations
    # in lieu of separate arguments
    parent_host = relationship(
        "HostEntry",
        primaryjoin=remote(ip_address) == cast(foreign(content), INET),
    )
```  ## 在连接条件中使用自定义运算符

关于关系的另一个用例是使用自定义运算符,例如在与`INET`和`CIDR`等类型结合时,使用 PostgreSQL 的“包含于”`<<`运算符。对于自定义布尔运算符,我们使用`Operators.bool_op()`函数:

```py
inet_column.bool_op("<<")(cidr_column)

类似上述的比较可以直接在构建relationship()时,与relationship.primaryjoin一起使用:

class IPA(Base):
    __tablename__ = "ip_address"

    id = mapped_column(Integer, primary_key=True)
    v4address = mapped_column(INET)

    network = relationship(
        "Network",
        primaryjoin="IPA.v4address.bool_op('<<')(foreign(Network.v4representation))",
        viewonly=True,
    )

class Network(Base):
    __tablename__ = "network"

    id = mapped_column(Integer, primary_key=True)
    v4representation = mapped_column(CIDR)

上面的查询如下:

select(IPA).join(IPA.network)

将呈现为:

SELECT  ip_address.id  AS  ip_address_id,  ip_address.v4address  AS  ip_address_v4address
FROM  ip_address  JOIN  network  ON  ip_address.v4address  <<  network.v4representation
```  ## 基于 SQL 函数的自定义运算符

`Operators.op.is_comparison`的用例的一种变体是当我们不使用运算符,而是使用 SQL 函数时。这种用例的典型示例是 PostgreSQL PostGIS 函数,但任何数据库中解析为二进制条件的 SQL 函数都可以应用。为了适应这种用例,`FunctionElement.as_comparison()`方法可以修改任何 SQL 函数,例如从`func`命名空间调用的函数,以指示 ORM 函数生成两个表达式的比较。下面的示例用[Geoalchemy2](https://geoalchemy-2.readthedocs.io/)库说明了这一点:

```py
from geoalchemy2 import Geometry
from sqlalchemy import Column, Integer, func
from sqlalchemy.orm import relationship, foreign

class Polygon(Base):
    __tablename__ = "polygon"
    id = mapped_column(Integer, primary_key=True)
    geom = mapped_column(Geometry("POLYGON", srid=4326))
    points = relationship(
        "Point",
        primaryjoin="func.ST_Contains(foreign(Polygon.geom), Point.geom).as_comparison(1, 2)",
        viewonly=True,
    )

class Point(Base):
    __tablename__ = "point"
    id = mapped_column(Integer, primary_key=True)
    geom = mapped_column(Geometry("POINT", srid=4326))

在上面,FunctionElement.as_comparison()表明func.ST_Contains() SQL 函数正在比较Polygon.geomPoint.geom表达式。foreign()注释另外指出了在此特定关系中哪个列承担“外键”角色。

1.3 版本中的新增功能:添加了FunctionElement.as_comparison()。## 重叠的外键

当使用复合外键时,可能会出现罕见的情况,使得单个列可能是通过外键约束引用的多个列的主题。

考虑一个(诚然复杂的)映射,如Magazine对象,由Writer对象和Article对象使用包含magazine_id的复合主键方案引用;然后为了使Article也引用WriterArticle.magazine_id涉及到两个单独的关系;Article.magazineArticle.writer

class Magazine(Base):
    __tablename__ = "magazine"

    id = mapped_column(Integer, primary_key=True)

class Article(Base):
    __tablename__ = "article"

    article_id = mapped_column(Integer)
    magazine_id = mapped_column(ForeignKey("magazine.id"))
    writer_id = mapped_column()

    magazine = relationship("Magazine")
    writer = relationship("Writer")

    __table_args__ = (
        PrimaryKeyConstraint("article_id", "magazine_id"),
        ForeignKeyConstraint(
            ["writer_id", "magazine_id"], ["writer.id", "writer.magazine_id"]
        ),
    )

class Writer(Base):
    __tablename__ = "writer"

    id = mapped_column(Integer, primary_key=True)
    magazine_id = mapped_column(ForeignKey("magazine.id"), primary_key=True)
    magazine = relationship("Magazine")

配置上述映射后,我们将看到发出此警告:

SAWarning: relationship 'Article.writer' will copy column
writer.magazine_id to column article.magazine_id,
which conflicts with relationship(s): 'Article.magazine'
(copies magazine.id to article.magazine_id). Consider applying
viewonly=True to read-only relationships, or provide a primaryjoin
condition marking writable columns with the foreign() annotation.

这指的是Article.magazine_id是两个不同外键约束的主题的事实;它直接引用Magazine.id作为源列,但也在Writer的复合键的上下文中引用Writer.magazine_id作为源列。如果我们将Article与特定的Magazine关联起来,然后将Article与与不同Magazine关联的Writer关联起来,ORM 将非确定性地覆盖Article.magazine_id,在不通知的情况下更改我们引用的杂志;如果我们将WriterArticle中解除关联,它还可能尝试将 NULL 放入此列。警告让我们知道这是这种情况。

要解决这个问题,我们需要将Article的行为分解,包括以下三个特性:

  1. 首先,Article根据仅在Article.magazine关系中持久化的数据写入Article.magazine_id,即从Magazine.id复制的值。

  2. Article可以代表在Article.writer关系中持久化的数据写入Article.writer_id,但仅限于Writer.id列;Writer.magazine_id列不应写入Article.magazine_id,因为它最终源自Magazine.id

  3. 当加载Article.writer时,Article会考虑Article.magazine_id,尽管它在此关系中不会向其写入。

要获得仅#1 和#2,我们可以仅指定Article.writer_id作为Article.writer的“外键”:

class Article(Base):
    # ...

    writer = relationship("Writer", foreign_keys="Article.writer_id")

然而,这会导致Article.writer在与Writer查询时不考虑Article.magazine_id

SELECT  article.article_id  AS  article_article_id,
  article.magazine_id  AS  article_magazine_id,
  article.writer_id  AS  article_writer_id
FROM  article
JOIN  writer  ON  writer.id  =  article.writer_id

因此,为了获得所有#1、#2 和#3,我们通过完全组合relationship.primaryjoinrelationship.foreign_keys参数,或者更简洁地通过用foreign()注释来表达连接条件以及要写入的列:

class Article(Base):
    # ...

    writer = relationship(
        "Writer",
        primaryjoin="and_(Writer.id == foreign(Article.writer_id), "
        "Writer.magazine_id == Article.magazine_id)",
    )

非关系对比/实现路径

警告

此部分详细介绍了一个实验性功能。

使用自定义表达式意味着我们可以生成不遵循通常的主键/外键模型的非正统连接条件。其中一个例子是实现路径模式,其中我们比较字符串以产生重叠路径标记,以便生成树结构。

通过精心使用foreign()remote(),我们可以构建一个有效地产生基本的物化路径系统的关系。基本上,当foreign()remote()相同的比较表达式的一侧时,关系被视为“一对多”;当它们在不同的一侧时,关系被视为“多对一”。对于我们将在此处使用的比较,我们将处理集合,因此保持配置为“一对多”:

class Element(Base):
    __tablename__ = "element"

    path = mapped_column(String, primary_key=True)

    descendants = relationship(
        "Element",
        primaryjoin=remote(foreign(path)).like(path.concat("/%")),
        viewonly=True,
        order_by=path,
    )

上述情况下,如果给定一个具有路径属性为"/foo/bar2"Element对象,我们寻求加载Element.descendants以如下形式:

SELECT  element.path  AS  element_path
FROM  element
WHERE  element.path  LIKE  ('/foo/bar2'  ||  '/%')  ORDER  BY  element.path

自引用多对多关系

另见

本节记录了“邻接列表”模式的两表变体,该模式在邻接列表关系有所描述。务必查看子节自引用查询策略和配置自引用急切加载,这两者同样适用于此处讨论的映射模式。

多对多关系可以通过relationship.primaryjoinrelationship.secondaryjoin中的一个或两个进行自定义 - 后者对于使用relationship.secondary参数指定多对多引用的关系非常重要。一个常见的情况涉及使用relationship.primaryjoinrelationship.secondaryjoin来建立从一个类到自身的多对多关系,如下所示:

from typing import List

from sqlalchemy import Integer, ForeignKey, Column, Table
from sqlalchemy.orm import DeclarativeBase, Mapped
from sqlalchemy.orm import mapped_column, relationship

class Base(DeclarativeBase):
    pass

node_to_node = Table(
    "node_to_node",
    Base.metadata,
    Column("left_node_id", Integer, ForeignKey("node.id"), primary_key=True),
    Column("right_node_id", Integer, ForeignKey("node.id"), primary_key=True),
)

class Node(Base):
    __tablename__ = "node"
    id: Mapped[int] = mapped_column(primary_key=True)
    label: Mapped[str]
    right_nodes: Mapped[List["Node"]] = relationship(
        "Node",
        secondary=node_to_node,
        primaryjoin=id == node_to_node.c.left_node_id,
        secondaryjoin=id == node_to_node.c.right_node_id,
        back_populates="left_nodes",
    )
    left_nodes: Mapped[List["Node"]] = relationship(
        "Node",
        secondary=node_to_node,
        primaryjoin=id == node_to_node.c.right_node_id,
        secondaryjoin=id == node_to_node.c.left_node_id,
        back_populates="right_nodes",
    )

在上述情况下,SQLAlchemy 无法自动知道哪些列应该连接到right_nodesleft_nodes关系。relationship.primaryjoinrelationship.secondaryjoin参数确定我们希望如何连接到关联表。在上面的声明形式中,由于我们正在声明这些条件,因此id变量直接可用作我们希望与之连接的Column对象。

或者,我们可以使用字符串来定义relationship.primaryjoinrelationship.secondaryjoin参数,这在我们的配置中可能没有Node.id列对象可用,或者node_to_node表可能还没有可用时很合适。当在声明字符串中引用普通的Table对象时,我们使用表的字符串名称,就像它在MetaData中一样:

class Node(Base):
    __tablename__ = "node"
    id = mapped_column(Integer, primary_key=True)
    label = mapped_column(String)
    right_nodes = relationship(
        "Node",
        secondary="node_to_node",
        primaryjoin="Node.id==node_to_node.c.left_node_id",
        secondaryjoin="Node.id==node_to_node.c.right_node_id",
        backref="left_nodes",
    )

警告

当作为 Python 可评估字符串传递时,relationship.primaryjoinrelationship.secondaryjoin参数使用 Python 的eval()函数进行解释。不要将不受信任的输入传递给这些字符串。有关声明式评估relationship()参数的详细信息,请参阅关系参数的评估。

在此处的经典映射情况类似,其中node_to_node可以连接到node.c.id

from sqlalchemy import Integer, ForeignKey, String, Column, Table, MetaData
from sqlalchemy.orm import relationship, registry

metadata_obj = MetaData()
mapper_registry = registry()

node_to_node = Table(
    "node_to_node",
    metadata_obj,
    Column("left_node_id", Integer, ForeignKey("node.id"), primary_key=True),
    Column("right_node_id", Integer, ForeignKey("node.id"), primary_key=True),
)

node = Table(
    "node",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("label", String),
)

class Node:
    pass

mapper_registry.map_imperatively(
    Node,
    node,
    properties={
        "right_nodes": relationship(
            Node,
            secondary=node_to_node,
            primaryjoin=node.c.id == node_to_node.c.left_node_id,
            secondaryjoin=node.c.id == node_to_node.c.right_node_id,
            backref="left_nodes",
        )
    },
)

请注意,在两个示例中,relationship.backref关键字指定了一个left_nodes的 backref - 当relationship()在相反方向创建第二个关系时,它足够智能以反转relationship.primaryjoinrelationship.secondaryjoin参数。

另请参阅

  • 邻接列表关系 - 单表版本

  • 自引用查询策略 - 关于使用自引用映射进行查询的提示

  • 配置自引用急切加载 - 使用自引用映射进行急切加载的提示 ## 复合“次要”连接

注意

本节介绍了 SQLAlchemy 支持的一些边缘案例,但建议尽可能以更简单的方式解决这类问题,例如使用合理的关系布局和/或 Python 属性内部。

有时,当需要在两个表之间建立relationship()时,需要涉及更多的表才能将它们连接起来。这是一种relationship()的领域,人们试图推动可能性的边界,而这类奇特用例的最终解决方案通常需要在 SQLAlchemy 邮件列表上讨论出来。

在较新的 SQLAlchemy 版本中,relationship.secondary参数可以在某些情况下使用,以提供由多个表组成的复合目标。下面是这种连接条件的示例(至少需要版本 0.9.2 才能正常运行):

class A(Base):
    __tablename__ = "a"

    id = mapped_column(Integer, primary_key=True)
    b_id = mapped_column(ForeignKey("b.id"))

    d = relationship(
        "D",
        secondary="join(B, D, B.d_id == D.id).join(C, C.d_id == D.id)",
        primaryjoin="and_(A.b_id == B.id, A.id == C.a_id)",
        secondaryjoin="D.id == B.d_id",
        uselist=False,
        viewonly=True,
    )

class B(Base):
    __tablename__ = "b"

    id = mapped_column(Integer, primary_key=True)
    d_id = mapped_column(ForeignKey("d.id"))

class C(Base):
    __tablename__ = "c"

    id = mapped_column(Integer, primary_key=True)
    a_id = mapped_column(ForeignKey("a.id"))
    d_id = mapped_column(ForeignKey("d.id"))

class D(Base):
    __tablename__ = "d"

    id = mapped_column(Integer, primary_key=True)

在上面的示例中,我们直接引用了命名为abcd的表,提供了relationship.secondaryrelationship.primaryjoinrelationship.secondaryjoin这三个声明式样式的参数。从AD的查询如下所示:

sess.scalars(select(A).join(A.d)).all()

SELECT  a.id  AS  a_id,  a.b_id  AS  a_b_id
FROM  a  JOIN  (
  b  AS  b_1  JOIN  d  AS  d_1  ON  b_1.d_id  =  d_1.id
  JOIN  c  AS  c_1  ON  c_1.d_id  =  d_1.id)
  ON  a.b_id  =  b_1.id  AND  a.id  =  c_1.a_id  JOIN  d  ON  d.id  =  b_1.d_id 

在上面的示例中,我们利用能够将多个表放入“secondary”容器的优势,以便我们可以跨多个表进行连接,同时保持对relationship()的“简化”,在这种情况下,“左”和“右”两侧都只有“一个”表;复杂性保持在中间。

警告

像上面的关系通常标记为viewonly=True,使用relationship.viewonly,应视为只读。虽然有时可以使类似上面的关系可写,但这通常很复杂且容易出错。

另请参阅

使用 viewonly 关系参数的注意事项 ## 别名类的关系

在前一节中,我们介绍了一种技术,其中我们使用relationship.secondary来在连接条件中放置额外的表。有一种复杂的连接情况,即使使用这种技术也不够;当我们试图从A连接到B,并在其中使用任意数量的CD等,但AB之间也直接有连接条件时。在这种情况下,仅仅使用一个复杂的relationship.primaryjoin条件可能难以表达从AB的连接,因为中间表可能需要特殊处理,并且也不能用relationship.secondary对象来表达,因为A->secondary->B模式不支持AB之间的任何引用。当出现这种极其复杂的情况时,我们可以采用创建第二个映射作为关系目标的方法。这就是我们使用AliasedClass来制作一个包含我们所需的所有额外表的类的映射。为了将此映射作为我们类的“替代”映射生成,我们使用aliased()函数生成新的构造,然后针对该对象使用relationship(),就像它是一个普通的映射类一样。

下面说明了一个从ABrelationship(),其中主要连接条件增加了两个额外的实体CD,这两个实体必须同时与AB中的行对齐:

class A(Base):
    __tablename__ = "a"

    id = mapped_column(Integer, primary_key=True)
    b_id = mapped_column(ForeignKey("b.id"))

class B(Base):
    __tablename__ = "b"

    id = mapped_column(Integer, primary_key=True)

class C(Base):
    __tablename__ = "c"

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

    some_c_value = mapped_column(String)

class D(Base):
    __tablename__ = "d"

    id = mapped_column(Integer, primary_key=True)
    c_id = mapped_column(ForeignKey("c.id"))
    b_id = mapped_column(ForeignKey("b.id"))

    some_d_value = mapped_column(String)

# 1\. set up the join() as a variable, so we can refer
# to it in the mapping multiple times.
j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)

# 2\. Create an AliasedClass to B
B_viacd = aliased(B, j, flat=True)

A.b = relationship(B_viacd, primaryjoin=A.b_id == j.c.b_id)

使用上述映射,简单的连接如下所示:

sess.scalars(select(A).join(A.b)).all()

SELECT  a.id  AS  a_id,  a.b_id  AS  a_b_id
FROM  a  JOIN  (b  JOIN  d  ON  d.b_id  =  b.id  JOIN  c  ON  c.id  =  d.c_id)  ON  a.b_id  =  b.id 

将别名类映射与类型和避免早期映射器配置集成

对映射类使用aliased()构造会强制执行configure_mappers()步骤,该步骤将解析所有当前类及其关系。如果当前映射需要但尚未声明的不相关映射类,或者如果关系本身的配置需要访问尚未声明的类,则可能会出现问题。此外,当关系提前声明时,SQLAlchemy 的声明模式与 Python 类型最有效地配合使用。

为了组织关系的构建以解决这些问题,可以使用像MapperEvents.before_mapper_configured()这样的配置级事件钩子,该钩子仅在所有映射准备好进行配置时才会调用配置代码:

from sqlalchemy import event

class A(Base):
    __tablename__ = "a"

    id = mapped_column(Integer, primary_key=True)
    b_id = mapped_column(ForeignKey("b.id"))

@event.listens_for(A, "before_mapper_configured")
def _configure_ab_relationship(mapper, cls):
    # do the above configuration in a configuration hook

    j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)
    B_viacd = aliased(B, j, flat=True)
    A.b = relationship(B_viacd, primaryjoin=A.b_id == j.c.b_id)

上面,函数_configure_ab_relationship()仅在请求完全配置的A版本时调用,此时类BDC将可用。

对于与内联类型配合使用的方法,可以使用类似的技术有效地生成用于别名类的“单例”创建模式,其中它作为全局变量进行了延迟初始化,然后可以在关系内联中使用:

from typing import Any

B_viacd: Any = None
b_viacd_join: Any = None

class A(Base):
    __tablename__ = "a"

    id: Mapped[int] = mapped_column(primary_key=True)
    b_id: Mapped[int] = mapped_column(ForeignKey("b.id"))

    # 1\. the relationship can be declared using lambdas, allowing it to resolve
    #    to targets that are late-configured
    b: Mapped[B] = relationship(
        lambda: B_viacd, primaryjoin=lambda: A.b_id == b_viacd_join.c.b_id
    )

# 2\. configure the targets of the relationship using a before_mapper_configured
#    hook.
@event.listens_for(A, "before_mapper_configured")
def _configure_ab_relationship(mapper, cls):
    # 3\. set up the join() and AliasedClass as globals from within
    #    the configuration hook.

    global B_viacd, b_viacd_join

    b_viacd_join = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)
    B_viacd = aliased(B, b_viacd_join, flat=True)

在查询中使用 AliasedClass 目标

在前面的示例中,A.b关系指的是B_viacd实体作为目标,而不是直接的B类。要添加涉及A.b关系的额外条件,通常需要直接引用B_viacd,而不是使用B,特别是在A.b的目标实体要转换为别名或子查询的情况下。下面用子查询而不是连接说明了相同的关系:

subq = select(B).join(D, D.b_id == B.id).join(C, C.id == D.c_id).subquery()

B_viacd_subquery = aliased(B, subq)

A.b = relationship(B_viacd_subquery, primaryjoin=A.b_id == subq.c.id)

使用上述A.b关系的查询将呈现为一个子查询:

sess.scalars(select(A).join(A.b)).all()

SELECT  a.id  AS  a_id,  a.b_id  AS  a_b_id
FROM  a  JOIN  (SELECT  b.id  AS  id,  b.some_b_column  AS  some_b_column
FROM  b  JOIN  d  ON  d.b_id  =  b.id  JOIN  c  ON  c.id  =  d.c_id)  AS  anon_1  ON  a.b_id  =  anon_1.id 

如果我们想要基于A.b连接添加额外的条件,则必须根据B_viacd_subquery而不是直接根据B来做:

sess.scalars(
    select(A)
    .join(A.b)
    .where(B_viacd_subquery.some_b_column == "some b")
    .order_by(B_viacd_subquery.id)
).all()

SELECT  a.id  AS  a_id,  a.b_id  AS  a_b_id
FROM  a  JOIN  (SELECT  b.id  AS  id,  b.some_b_column  AS  some_b_column
FROM  b  JOIN  d  ON  d.b_id  =  b.id  JOIN  c  ON  c.id  =  d.c_id)  AS  anon_1  ON  a.b_id  =  anon_1.id
WHERE  anon_1.some_b_column  =  ?  ORDER  BY  anon_1.id 
```  ## 使用窗口函数限制行关系

另一个与`AliasedClass`对象关系的有趣用例是在关系需要连接到任意形式的专门 SELECT 的情况下。一种情况是当需要使用窗口函数时,例如限制应返回多少行以供关系使用。下面的示例说明了一个非主映射器关系,它将为每个集合加载前十个项目:

```py
class A(Base):
    __tablename__ = "a"

    id = mapped_column(Integer, primary_key=True)

class B(Base):
    __tablename__ = "b"
    id = mapped_column(Integer, primary_key=True)
    a_id = mapped_column(ForeignKey("a.id"))

partition = select(
    B, func.row_number().over(order_by=B.id, partition_by=B.a_id).label("index")
).alias()

partitioned_b = aliased(B, partition)

A.partitioned_bs = relationship(
    partitioned_b, primaryjoin=and_(partitioned_b.a_id == A.id, partition.c.index < 10)
)

我们可以将上述partitioned_bs关系与大多数加载器策略一起使用,例如selectinload()

for a1 in session.scalars(select(A).options(selectinload(A.partitioned_bs))):
    print(a1.partitioned_bs)  # <-- will be no more than ten objects

上面,"selectinload" 查询如下所示:

SELECT
  a_1.id  AS  a_1_id,  anon_1.id  AS  anon_1_id,  anon_1.a_id  AS  anon_1_a_id,
  anon_1.data  AS  anon_1_data,  anon_1.index  AS  anon_1_index
FROM  a  AS  a_1
JOIN  (
  SELECT  b.id  AS  id,  b.a_id  AS  a_id,  b.data  AS  data,
  row_number()  OVER  (PARTITION  BY  b.a_id  ORDER  BY  b.id)  AS  index
  FROM  b)  AS  anon_1
ON  anon_1.a_id  =  a_1.id  AND  anon_1.index  <  %(index_1)s
WHERE  a_1.id  IN  (  ...  primary  key  collection  ...)
ORDER  BY  a_1.id

上述情况下,对于“a”中的每个匹配主键,我们将按“b.id”排序获取前十个“bs”。通过在“a_id”上进行分区,我们确保每个“行号”都是相对于父“a_id”的局部的。

这样的映射通常也会包括从“A”到“B”的“普通”关系,用于持久性操作以及当需要每个“A”的完整“B”对象集合时。## 构建查询可用的属性

非常雄心勃勃的自定义连接条件可能无法直接持久化,并且在某些情况下甚至可能无法正确加载。要删除等式的持久性部分,请在relationship()上使用标志relationship.viewonly,将其建立为只读属性(写入到集合的数据将在刷新时被忽略)。然而,在极端情况下,考虑与Query一起使用普通的 Python 属性,如下所示:

class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)

    @property
    def addresses(self):
        return object_session(self).query(Address).with_parent(self).filter(...).all()

在其他情况下,描述符可以构建以利用现有的 Python 数据。有关使用描述符和混合体的更一般讨论,请参见使用描述符和混合体部分。

另见

使用描述符和混合体 ## 关于使用 viewonly 关系参数的注意事项

当应用于relationship()构造时,relationship.viewonly参数指示这个relationship()不会参与任何 ORM 工作单元操作,并且该属性不希望在其表示的集合的 Python 变异中参与。这意味着虽然只读关系可能引用一个可变的 Python 集合,如列表或集合,但对该列表或集合进行更改,如在映射实例上存在的那样,对 ORM 刷新过程没有影响

要探索这种情况,请考虑这种映射:

from __future__ import annotations

import datetime

from sqlalchemy import and_
from sqlalchemy import ForeignKey
from sqlalchemy import func
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str | None]

    all_tasks: Mapped[list[Task]] = relationship()

    current_week_tasks: Mapped[list[Task]] = relationship(
        primaryjoin=lambda: and_(
            User.id == Task.user_account_id,
            # this expression works on PostgreSQL but may not be supported
            # by other database engines
            Task.task_date >= func.now() - datetime.timedelta(days=7),
        ),
        viewonly=True,
    )

class Task(Base):
    __tablename__ = "task"

    id: Mapped[int] = mapped_column(primary_key=True)
    user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
    description: Mapped[str | None]
    task_date: Mapped[datetime.datetime] = mapped_column(server_default=func.now())

    user: Mapped[User] = relationship(back_populates="current_week_tasks")

以下各节将注意这种配置的不同方面。

包括反向引用的 Python 中的变异与 viewonly=True 不适用

上述映射将User.current_week_tasks只读关系作为Task.user属性的反向引用目标。这目前并未被 SQLAlchemy 的 ORM 配置过程标记,但这是一个配置错误。改变Task上的.user属性不会影响.current_week_tasks属性:

>>> u1 = User()
>>> t1 = Task(task_date=datetime.datetime.now())
>>> t1.user = u1
>>> u1.current_week_tasks
[]

这里还有另一个参数叫做relationship.sync_backrefs,可以在这里打开,以允许在这种情况下对.current_week_tasks进行变异,然而这并不被认为是最佳实践,对于一个只读关系,不应该依赖于 Python 中的变异。

在这种映射中,可以在User.all_tasksTask.user之间配置反向引用,因为这两者都不是只读的,将正常同步。

除了禁用只读关系的反向引用变异问题外,Python 中对User.all_tasks集合的普通更改也不会反映在User.current_week_tasks集合中,直到更改已刷新到数据库。

总的来说,对于一个需要立即响应 Python 中的变异的自定义集合的用例,只读关系通常不合适。更好的方法是使用 SQLAlchemy 的混合属性功能,或者对于仅实例情况,使用 Python 的@property,其中可以实现一个根据当前 Python 实例生成的用户定义集合。要将我们的示例更改为这种方式工作,我们修复Task.user上的relationship.back_populates参数,引用User.all_tasks,然后演示一个简单的@property,将以立即User.all_tasks集合的形式提供结果:

class User(Base):
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str | None]

    all_tasks: Mapped[list[Task]] = relationship(back_populates="user")

    @property
    def current_week_tasks(self) -> list[Task]:
        past_seven_days = datetime.datetime.now() - datetime.timedelta(days=7)
        return [t for t in self.all_tasks if t.task_date >= past_seven_days]

class Task(Base):
    __tablename__ = "task"

    id: Mapped[int] = mapped_column(primary_key=True)
    user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
    description: Mapped[str | None]
    task_date: Mapped[datetime.datetime] = mapped_column(server_default=func.now())

    user: Mapped[User] = relationship(back_populates="all_tasks")

使用在 Python 中每次动态计算的集合,我们保证始终有正确的答案,而无需使用数据库:

>>> u1 = User()
>>> t1 = Task(task_date=datetime.datetime.now())
>>> t1.user = u1
>>> u1.current_week_tasks
[<__main__.Task object at 0x7f3d699523c0>]

viewonly=True 的集合/属性直到过期才会重新查询

继续使用原始的 viewonly 属性,如果我们确实对 persistent 对象上的 User.all_tasks 集合进行更改,则在 两个 事情发生之后,viewonly 集合只能显示此更改的净结果。第一个是将更改刷新到 User.all_tasks 中,以便新数据在数据库中可用,至少在本地事务的范围内是如此。第二个是 User.current_week_tasks 属性被 expired 并通过对数据库进行新的 SQL 查询重新加载。

为了支持这一需求,最简单的流程是仅在主要是只读操作中使用 viewonly 关系。例如,如果我们从数据库中检索到一个新的 User,那么集合将是当前的:

>>> with Session(e) as sess:
...     u1 = sess.scalar(select(User).where(User.id == 1))
...     print(u1.current_week_tasks)
[<__main__.Task object at 0x7f8711b906b0>]

当我们对 u1.all_tasks 进行修改时,如果想要在 u1.current_week_tasks 视图关系中看到这些更改,这些更改需要被刷新,并且 u1.current_week_tasks 属性需要过期,以便在下一次访问时进行 惰性加载。最简单的方法是使用 Session.commit(),保持 Session.expire_on_commit 参数设置为其默认值 True

>>> with Session(e) as sess:
...     u1 = sess.scalar(select(User).where(User.id == 1))
...     u1.all_tasks.append(Task(task_date=datetime.datetime.now()))
...     sess.commit()
...     print(u1.current_week_tasks)
[<__main__.Task object at 0x7f8711b90ec0>, <__main__.Task object at 0x7f8711b90a10>]

上面,对 Session.commit() 的调用将更改刷新到了数据库中的 u1.all_tasks,然后使所有对象过期,因此当我们访问 u1.current_week_tasks 时,会发生 :term:惰性加载,从数据库中新鲜获取此属性的内容。

要拦截操作而不实际提交事务,需要先显式 expired 该属性。一种简单的方法是直接调用它。在下面的示例中,Session.flush() 将挂起的更改发送到数据库,然后使用 Session.expire() 来过期 u1.current_week_tasks 集合,以便在下一次访问时重新获取:

>>> with Session(e) as sess:
...     u1 = sess.scalar(select(User).where(User.id == 1))
...     u1.all_tasks.append(Task(task_date=datetime.datetime.now()))
...     sess.flush()
...     sess.expire(u1, ["current_week_tasks"])
...     print(u1.current_week_tasks)
[<__main__.Task object at 0x7fd95a4c8c50>, <__main__.Task object at 0x7fd95a4c8c80>]

事实上,我们可以跳过对Session.flush()的调用,假设一个保持Session.autoflush为其默认值TrueSession,因为过期的current_week_tasks属性在过期后访问时将触发自动刷新:

>>> with Session(e) as sess:
...     u1 = sess.scalar(select(User).where(User.id == 1))
...     u1.all_tasks.append(Task(task_date=datetime.datetime.now()))
...     sess.expire(u1, ["current_week_tasks"])
...     print(u1.current_week_tasks)  # triggers autoflush before querying
[<__main__.Task object at 0x7fd95a4c8c50>, <__main__.Task object at 0x7fd95a4c8c80>]

继续使用上述方法进行更详细的处理,我们可以在相关的User.all_tasks集合发生变化时通过 event hooks 进行程序化过期。这是一种高级技术,应该首先检查更简单的架构,比如@property或坚持只读用例。在我们简单的示例中,这将被配置为:

from sqlalchemy import event, inspect

@event.listens_for(User.all_tasks, "append")
@event.listens_for(User.all_tasks, "remove")
@event.listens_for(User.all_tasks, "bulk_replace")
def _expire_User_current_week_tasks(target, value, initiator):
    inspect(target).session.expire(target, ["current_week_tasks"])

有了上述钩子,突变操作被拦截并导致User.current_week_tasks集合自动过期:

>>> with Session(e) as sess:
...     u1 = sess.scalar(select(User).where(User.id == 1))
...     u1.all_tasks.append(Task(task_date=datetime.datetime.now()))
...     print(u1.current_week_tasks)
[<__main__.Task object at 0x7f66d093ccb0>, <__main__.Task object at 0x7f66d093cce0>]

上述使用的AttributeEvents事件钩子也会被 backref 突变触发,因此,使用上述钩子会拦截对Task.user的更改:

>>> with Session(e) as sess:
...     u1 = sess.scalar(select(User).where(User.id == 1))
...     t1 = Task(task_date=datetime.datetime.now())
...     t1.user = u1
...     sess.add(t1)
...     print(u1.current_week_tasks)
[<__main__.Task object at 0x7f3b0c070d10>, <__main__.Task object at 0x7f3b0c057d10>]
```  ## 处理多个连接路径

处理的最常见情况之一是两个表之间存在多个外键路径。

考虑一个包含对`Address`类的两个外键的`Customer`类:

```py
from sqlalchemy import Integer, ForeignKey, String, Column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

class Customer(Base):
    __tablename__ = "customer"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String)

    billing_address_id = mapped_column(Integer, ForeignKey("address.id"))
    shipping_address_id = mapped_column(Integer, ForeignKey("address.id"))

    billing_address = relationship("Address")
    shipping_address = relationship("Address")

class Address(Base):
    __tablename__ = "address"
    id = mapped_column(Integer, primary_key=True)
    street = mapped_column(String)
    city = mapped_column(String)
    state = mapped_column(String)
    zip = mapped_column(String)

上述映射,在我们尝试使用它时,会产生错误:

sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join
condition between parent/child tables on relationship
Customer.billing_address - there are multiple foreign key
paths linking the tables.  Specify the 'foreign_keys' argument,
providing a list of those columns which should be
counted as containing a foreign key reference to the parent table.

上述消息相当长。relationship()可能返回许多潜在消息,这些消息经过精心设计,以检测各种常见的配置问题;大多数消息都会建议需要解决模糊性或其他缺失信息的附加配置。

在这种情况下,消息希望我们为每个relationship()进行限定,指示每个外键列应该被考虑,并且适当的形式如下:

class Customer(Base):
    __tablename__ = "customer"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String)

    billing_address_id = mapped_column(Integer, ForeignKey("address.id"))
    shipping_address_id = mapped_column(Integer, ForeignKey("address.id"))

    billing_address = relationship("Address", foreign_keys=[billing_address_id])
    shipping_address = relationship("Address", foreign_keys=[shipping_address_id])

在上面的例子中,我们指定了foreign_keys参数,它是一个ColumnColumn对象列表,指示要考虑的“外键”列,或者换句话说,包含指向父表的值的列。从Customer对象加载Customer.billing_address关系将使用billing_address_id中存在的值来标识要加载的Address行;类似地,shipping_address_id用于shipping_address关系。这两列的关联在持久化过程中也起着作用;刚插入的Address对象的新生成的主键将在刷新期间被复制到关联的Customer对象的适当外键列中。

在使用 Declarative 指定foreign_keys时,我们还可以使用字符串名称进行指定,但是重要的是,如果使用列表,列表是字符串的一部分

billing_address = relationship("Address", foreign_keys="[Customer.billing_address_id]")

在这个具体的例子中,在任何情况下列表都是不必要的,因为我们只需要一个Column

billing_address = relationship("Address", foreign_keys="Customer.billing_address_id")

警告

当作为 Python 可评估字符串传递时,relationship.foreign_keys 参数将使用 Python 的 eval() 函数进行解释。请勿将不受信任的输入传递给此字符串。详情请参阅关系参数的评估以了解有关relationship() 参数的声明性评估的详细信息。

指定备用连接条件

在构建连接时,relationship()的默认行为是将一侧的主键列的值等同于另一侧的外键引用列的值。我们可以使用relationship.primaryjoin参数来更改此标准为任何我们喜欢的内容,以及在使用“次要”表时,在使用relationship.secondaryjoin参数。

在下面的例子中,我们使用User类以及一个存储街道地址的Address类,我们创建了一个关系boston_addresses,它只会加载那些指定城市为“Boston”的Address对象:

from sqlalchemy import Integer, ForeignKey, String, Column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String)
    boston_addresses = relationship(
        "Address",
        primaryjoin="and_(User.id==Address.user_id, Address.city=='Boston')",
    )

class Address(Base):
    __tablename__ = "address"
    id = mapped_column(Integer, primary_key=True)
    user_id = mapped_column(Integer, ForeignKey("user.id"))

    street = mapped_column(String)
    city = mapped_column(String)
    state = mapped_column(String)
    zip = mapped_column(String)

在这个字符串的 SQL 表达式中,我们使用了 and_() 连接构造来建立两个不同的谓词,用于连接 User.idAddress.user_id 列,以及将 Address 中的行限制为只有 city='Boston'。当使用声明式时,类似 and_() 这样的基本 SQL 函数会自动在字符串 relationship() 参数的计算命名空间中可用。

警告

当作为 Python 可评估字符串传递时,relationship.primaryjoin 参数是使用 Python 的 eval() 函数解释的。不要将不受信任的输入传递给此字符串。有关声明式评估 relationship() 参数的详细信息,请参阅 关系参数的评估。

我们在 relationship.primaryjoin 中使用的自定义条件通常只在 SQLAlchemy 渲染 SQL 以加载或表示此关系时才重要。也就是说,在执行每个属性的惰性加载的 SQL 语句中使用它,或者在查询时构造连接,例如通过 Select.join() 或通过急切的“连接”或“子查询”加载样式。当操作内存中的对象时,我们可以将任何我们想要的 Address 对象放入 boston_addresses 集合中,而不管 .city 属性的值是什么。这些对象将保留在集合中,直到属性过期并重新从应用条件的数据库中加载为止。当执行刷新时,boston_addresses 中的对象将被无条件地刷新,将主键 user.id 列的值分配到每行的持有外键 address.user_id 列。这里的 city 条件没有效果,因为刷新过程只关心将主键值同步到引用外键值。

创建自定义外键条件

主要连接条件的另一个元素是如何确定那些被认为是“外部”的列的。通常,一些 Column 对象的子集将指定 ForeignKey,或者是 ForeignKeyConstraint 的一部分,这与连接条件相关。relationship() 查看这个外键状态,以确定它应该如何为这个关系加载和持久化数据。然而,relationship.primaryjoin 参数可以用来创建一个不涉及任何“模式”级外键的连接条件。我们可以显式地结合 relationship.primaryjoin 以及 relationship.foreign_keysrelationship.remote_side 来建立这样一个连接。

下面,一个 HostEntry 类与自身连接,将字符串 content 列等同于 ip_address 列,这是一个名为 INET 的 PostgreSQL 类型。我们需要使用 cast() 来将连接的一侧转换为另一侧的类型:

from sqlalchemy import cast, String, Column, Integer
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import INET

from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

class HostEntry(Base):
    __tablename__ = "host_entry"

    id = mapped_column(Integer, primary_key=True)
    ip_address = mapped_column(INET)
    content = mapped_column(String(50))

    # relationship() using explicit foreign_keys, remote_side
    parent_host = relationship(
        "HostEntry",
        primaryjoin=ip_address == cast(content, INET),
        foreign_keys=content,
        remote_side=ip_address,
    )

上述关系将产生类似以下的连接:

SELECT  host_entry.id,  host_entry.ip_address,  host_entry.content
FROM  host_entry  JOIN  host_entry  AS  host_entry_1
ON  host_entry_1.ip_address  =  CAST(host_entry.content  AS  INET)

以上的另一种语法是在 relationship.primaryjoin 表达式内联使用 foreign()remote() annotations。这种语法表示了 relationship() 通常自己应用于连接条件的注释,考虑到 relationship.foreign_keysrelationship.remote_side 参数。当存在明确的连接条件时,这些函数可能更简洁,并且还可以标记出“外部”或“远程”的确切列,而不管该列是否在多次声明或在复杂的 SQL 表达式中:

from sqlalchemy.orm import foreign, remote

class HostEntry(Base):
    __tablename__ = "host_entry"

    id = mapped_column(Integer, primary_key=True)
    ip_address = mapped_column(INET)
    content = mapped_column(String(50))

    # relationship() using explicit foreign() and remote() annotations
    # in lieu of separate arguments
    parent_host = relationship(
        "HostEntry",
        primaryjoin=remote(ip_address) == cast(foreign(content), INET),
    )

在连接条件中使用自定义运算符

另一个关系的用例是使用自定义运算符,比如 PostgreSQL 的“包含在内” << 运算符,当与诸如 INETCIDR 这样的类型连接时。对于自定义布尔运算符,我们使用 Operators.bool_op() 函数:

inet_column.bool_op("<<")(cidr_column)

像上面的比较可以直接用于构造 relationship() 中的 relationship.primaryjoin

class IPA(Base):
    __tablename__ = "ip_address"

    id = mapped_column(Integer, primary_key=True)
    v4address = mapped_column(INET)

    network = relationship(
        "Network",
        primaryjoin="IPA.v4address.bool_op('<<')(foreign(Network.v4representation))",
        viewonly=True,
    )

class Network(Base):
    __tablename__ = "network"

    id = mapped_column(Integer, primary_key=True)
    v4representation = mapped_column(CIDR)

上述,像这样的查询:

select(IPA).join(IPA.network)

将显示为:

SELECT  ip_address.id  AS  ip_address_id,  ip_address.v4address  AS  ip_address_v4address
FROM  ip_address  JOIN  network  ON  ip_address.v4address  <<  network.v4representation

基于 SQL 函数的自定义运算符

Operators.op.is_comparison 用例的变体是当我们不是使用运算符,而是使用 SQL 函数。这种用例的典型例子是 PostgreSQL PostGIS 函数,但任何解析为二进制条件的任何数据库上的 SQL 函数都可能适用。为适应这种用例,FunctionElement.as_comparison() 方法可以修改任何 SQL 函数,例如从 func 命名空间调用的函数,以指示 ORM 该函数生成了两个表达式的比较。下面的例子使用了 Geoalchemy2 库说明了这一点:

from geoalchemy2 import Geometry
from sqlalchemy import Column, Integer, func
from sqlalchemy.orm import relationship, foreign

class Polygon(Base):
    __tablename__ = "polygon"
    id = mapped_column(Integer, primary_key=True)
    geom = mapped_column(Geometry("POLYGON", srid=4326))
    points = relationship(
        "Point",
        primaryjoin="func.ST_Contains(foreign(Polygon.geom), Point.geom).as_comparison(1, 2)",
        viewonly=True,
    )

class Point(Base):
    __tablename__ = "point"
    id = mapped_column(Integer, primary_key=True)
    geom = mapped_column(Geometry("POINT", srid=4326))

上述,FunctionElement.as_comparison() 表示 func.ST_Contains() SQL 函数正在比较 Polygon.geomPoint.geom 表达式。foreign() 注释另外指出了在这种特定关系中扮演“外键”角色的列。

新版本 1.3 中新增了 FunctionElement.as_comparison()

重叠的外键

很少见的情况可能会出现,即使用复合外键,以便单个列可能是通过外键约束引用的多个列的主题。

考虑一个(诚然复杂的)映射,例如Magazine对象,使用包括magazine_id的复合主键方案,分别由Writer对象和Article对象引用;然后,为了使Article也引用WriterArticle.magazine_id涉及到两个不同的关系;Article.magazineArticle.writer

class Magazine(Base):
    __tablename__ = "magazine"

    id = mapped_column(Integer, primary_key=True)

class Article(Base):
    __tablename__ = "article"

    article_id = mapped_column(Integer)
    magazine_id = mapped_column(ForeignKey("magazine.id"))
    writer_id = mapped_column()

    magazine = relationship("Magazine")
    writer = relationship("Writer")

    __table_args__ = (
        PrimaryKeyConstraint("article_id", "magazine_id"),
        ForeignKeyConstraint(
            ["writer_id", "magazine_id"], ["writer.id", "writer.magazine_id"]
        ),
    )

class Writer(Base):
    __tablename__ = "writer"

    id = mapped_column(Integer, primary_key=True)
    magazine_id = mapped_column(ForeignKey("magazine.id"), primary_key=True)
    magazine = relationship("Magazine")

当上述映射被配置时,我们将看到此警告被发出:

SAWarning: relationship 'Article.writer' will copy column
writer.magazine_id to column article.magazine_id,
which conflicts with relationship(s): 'Article.magazine'
(copies magazine.id to article.magazine_id). Consider applying
viewonly=True to read-only relationships, or provide a primaryjoin
condition marking writable columns with the foreign() annotation.

这指的是Article.magazine_id是两个不同外键约束的主体;它直接引用Magazine.id作为源列,但在与Writer的复合键上下文中,也引用Writer.magazine_id作为源列。如果我们将Article与特定的Magazine关联起来,但然后将Article与另一个与不同Magazine关联的Writer关联起来,ORM 会非确定性地覆盖Article.magazine_id,悄悄地改变我们所引用的杂志;如果我们将WriterArticle中取消关联,它还可能尝试将 NULL 放入此列。警告让我们知道这种情况。

要解决这个问题,我们需要打破Article的行为,包括以下三个功能:

  1. 首先,Article根据仅在Article.magazine关系中持久化的数据来写入Article.magazine_id,即从Magazine.id复制的值。

  2. Article可以代表在Article.writer关系中持久化的数据写入Article.writer_id,但只能写入Writer.id列;Writer.magazine_id列不应写入Article.magazine_id,因为它最终来自Magazine.id

  3. 当加载Article.writer时,Article考虑了Article.magazine_id,即使在此关系中并不代表它。

要获取只有#1 和#2,我们可以将Article.writer_id指定为Article.writer的“外键”:

class Article(Base):
    # ...

    writer = relationship("Writer", foreign_keys="Article.writer_id")

然而,这会导致Article.writer在与Writer进行查询时不考虑Article.magazine_id

SELECT  article.article_id  AS  article_article_id,
  article.magazine_id  AS  article_magazine_id,
  article.writer_id  AS  article_writer_id
FROM  article
JOIN  writer  ON  writer.id  =  article.writer_id

因此,要获取#1、#2 和#3 的所有内容,我们需要通过完全组合relationship.primaryjoin来表达连接条件,以及要写入的列,同时使用relationship.foreign_keys参数,或者更简洁地使用foreign()进行注释:

class Article(Base):
    # ...

    writer = relationship(
        "Writer",
        primaryjoin="and_(Writer.id == foreign(Article.writer_id), "
        "Writer.magazine_id == Article.magazine_id)",
    )

非关系比较 / 材料化路径

警告

本节详细介绍了一个实验性功能。

使用自定义表达式意味着我们可以生成不遵循通常的主键/外键模型的非正统连接条件。其中一个例子是材料化路径模式,我们在比较字符串以产生重叠路径标记时,以便生成树结构。

通过谨慎使用foreign()remote(),我们可以构建一个有效地生成基本材料化路径系统的关系。基本上,当foreign()remote()相同的比较表达式一侧时,关系被认为是“一对多”;当它们在不同的一侧时,关系被认为是“多对一”。对于我们将在此处使用的比较,我们将处理集合,所以我们保持事物配置为“一对多”:

class Element(Base):
    __tablename__ = "element"

    path = mapped_column(String, primary_key=True)

    descendants = relationship(
        "Element",
        primaryjoin=remote(foreign(path)).like(path.concat("/%")),
        viewonly=True,
        order_by=path,
    )

上文中,如果给定具有"/foo/bar2"路径属性的Element对象,则我们寻找对Element.descendants的加载应如下所示:

SELECT  element.path  AS  element_path
FROM  element
WHERE  element.path  LIKE  ('/foo/bar2'  ||  '/%')  ORDER  BY  element.path

自我引用多对多关系

另请参阅

本节记录了“邻接列表”模式的两个表变体,该模式在邻接列表关系中有所记录。务必查看自我引用查询模式的子部分自我引用查询策略和配置自我引用急加载,它们同样适用于此处讨论的映射模式。

多对多关系可以通过relationship.primaryjoin和/或relationship.secondaryjoin中的一个或两个进行自定义 - 后者对于使用relationship.secondary参数指定多对多引用的关系非常重要。涉及使用relationship.primaryjoinrelationship.secondaryjoin的常见情况是在从一个类到自身建立多对多关系时,如下所示:

from typing import List

from sqlalchemy import Integer, ForeignKey, Column, Table
from sqlalchemy.orm import DeclarativeBase, Mapped
from sqlalchemy.orm import mapped_column, relationship

class Base(DeclarativeBase):
    pass

node_to_node = Table(
    "node_to_node",
    Base.metadata,
    Column("left_node_id", Integer, ForeignKey("node.id"), primary_key=True),
    Column("right_node_id", Integer, ForeignKey("node.id"), primary_key=True),
)

class Node(Base):
    __tablename__ = "node"
    id: Mapped[int] = mapped_column(primary_key=True)
    label: Mapped[str]
    right_nodes: Mapped[List["Node"]] = relationship(
        "Node",
        secondary=node_to_node,
        primaryjoin=id == node_to_node.c.left_node_id,
        secondaryjoin=id == node_to_node.c.right_node_id,
        back_populates="left_nodes",
    )
    left_nodes: Mapped[List["Node"]] = relationship(
        "Node",
        secondary=node_to_node,
        primaryjoin=id == node_to_node.c.right_node_id,
        secondaryjoin=id == node_to_node.c.left_node_id,
        back_populates="right_nodes",
    )

在上述情况下,SQLAlchemy 无法自动知道哪些列应该连接到right_nodesleft_nodes关系的哪些列上。relationship.primaryjoinrelationship.secondaryjoin参数建立了我们希望如何加入关联表的方式。在上面的声明形式中,由于我们在对应于Node类的 Python 块中声明了这些条件,因此id变量直接作为我们希望与之连接的Column对象是可用的。

或者,我们可以使用字符串定义relationship.primaryjoinrelationship.secondaryjoin参数,在我们的配置中可能还没有Node.id列对象可用,或者node_to_node表可能还不可用的情况下非常适用。当在声明字符串中引用普通的Table对象时,我们使用表的字符串名称,就像在MetaData中一样:

class Node(Base):
    __tablename__ = "node"
    id = mapped_column(Integer, primary_key=True)
    label = mapped_column(String)
    right_nodes = relationship(
        "Node",
        secondary="node_to_node",
        primaryjoin="Node.id==node_to_node.c.left_node_id",
        secondaryjoin="Node.id==node_to_node.c.right_node_id",
        backref="left_nodes",
    )

警告

当作为 Python 可评估字符串传递时,relationship.primaryjoinrelationship.secondaryjoin参数使用 Python 的eval()函数解释。不要将不受信任的输入传递给这些字符串。有关声明性relationship()参数的评估详细信息,请参阅关系参数的评估。

在这里的经典映射情况中,node_to_node可以连接到node.c.id

from sqlalchemy import Integer, ForeignKey, String, Column, Table, MetaData
from sqlalchemy.orm import relationship, registry

metadata_obj = MetaData()
mapper_registry = registry()

node_to_node = Table(
    "node_to_node",
    metadata_obj,
    Column("left_node_id", Integer, ForeignKey("node.id"), primary_key=True),
    Column("right_node_id", Integer, ForeignKey("node.id"), primary_key=True),
)

node = Table(
    "node",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("label", String),
)

class Node:
    pass

mapper_registry.map_imperatively(
    Node,
    node,
    properties={
        "right_nodes": relationship(
            Node,
            secondary=node_to_node,
            primaryjoin=node.c.id == node_to_node.c.left_node_id,
            secondaryjoin=node.c.id == node_to_node.c.right_node_id,
            backref="left_nodes",
        )
    },
)

请注意,在这两个示例中,relationship.backref 关键字都指定了一个 left_nodes 回引 - 当relationship()在反向创建第二个关系时,它足够聪明地反转了relationship.primaryjoinrelationship.secondaryjoin 参数。

另请参阅

  • 邻接列表关系 - 单表版本

  • 自引用查询策略 - 使用自引用映射查询的技巧

  • 配置自引用预加载 - 使用自引用映射预加载的技巧

复合“次要”连接

注意

本节涵盖了一些在某种程度上受 SQLAlchemy 支持的边缘案例,但建议尽可能在可能的情况下通过使用合理的关系布局和/或 Python 内的属性来解决类似问题。

有时,当一个人试图在两个表之间建立一个relationship()时,需要涉及更多的表,而不仅仅是两个或三个表。这是一个relationship()的领域,在这个领域,人们试图推动可能性的边界,并且通常对许多这些奇特用例的最终解决方案需要在 SQLAlchemy 邮件列表上讨论。

在较新版本的 SQLAlchemy 中,relationship.secondary 参数可用于某些情况,以提供由多个表组成的复合目标。以下是这种连接条件的示例(至少需要版本 0.9.2 才能正常运行):

class A(Base):
    __tablename__ = "a"

    id = mapped_column(Integer, primary_key=True)
    b_id = mapped_column(ForeignKey("b.id"))

    d = relationship(
        "D",
        secondary="join(B, D, B.d_id == D.id).join(C, C.d_id == D.id)",
        primaryjoin="and_(A.b_id == B.id, A.id == C.a_id)",
        secondaryjoin="D.id == B.d_id",
        uselist=False,
        viewonly=True,
    )

class B(Base):
    __tablename__ = "b"

    id = mapped_column(Integer, primary_key=True)
    d_id = mapped_column(ForeignKey("d.id"))

class C(Base):
    __tablename__ = "c"

    id = mapped_column(Integer, primary_key=True)
    a_id = mapped_column(ForeignKey("a.id"))
    d_id = mapped_column(ForeignKey("d.id"))

class D(Base):
    __tablename__ = "d"

    id = mapped_column(Integer, primary_key=True)

在上面的示例中,我们提供了relationship.secondaryrelationship.primaryjoinrelationship.secondaryjoin 这三个参数,在声明样式中直接引用了命名表 abcd。从 AD 的查询如下:

sess.scalars(select(A).join(A.d)).all()

SELECT  a.id  AS  a_id,  a.b_id  AS  a_b_id
FROM  a  JOIN  (
  b  AS  b_1  JOIN  d  AS  d_1  ON  b_1.d_id  =  d_1.id
  JOIN  c  AS  c_1  ON  c_1.d_id  =  d_1.id)
  ON  a.b_id  =  b_1.id  AND  a.id  =  c_1.a_id  JOIN  d  ON  d.id  =  b_1.d_id 

在上面的示例中,我们利用能够将多个表填入“次要”容器的优势,这样我们就可以跨多个表进行连接,同时保持对relationship()的“简单”使用,因为“左”和“右”两侧只有“一个”表;复杂性被保留在中间。

警告

类似上述的关系通常标记为viewonly=True,使用relationship.viewonly,应当被视为只读。虽然有时可以使类似上述的关系可写,但这通常是复杂且容易出错的。

另请参阅

关于使用只读关系参数的注意事项

别名类的关系

在前一节中,我们说明了一种技术,在这种技术中,我们使用了relationship.secondary来将额外的表放置在连接条件中。有一种复杂的连接情况,即使使用这种技术也不足够;当我们试图从A连接到B时,中间可能会使用任意数量的CD等,但是在AB之间也有直接的连接条件。在这种情况下,仅使用复杂的relationship.primaryjoin条件可能难以表达,因为中间表可能需要特殊处理,而且也不能使用relationship.secondary对象来表达,因为A->secondary->B模式不支持AB之间的任何引用。当出现这种极其高级的情况时,我们可以采用创建第二个映射作为关系的目标。这就是我们使用AliasedClass来创建一个包含我们所需的所有额外表的类的映射。为了将这个映射作为我们类的“替代”映射生成,我们使用aliased()函数来生成新的结构,然后针对该对象使用relationship(),就像它是一个普通的映射类一样。

下面演示了从 AB 的简单连接的 relationship(),但是主连接条件增加了另外两个实体 CD,这两个实体必须同时与 AB 中的行对应起来:

class A(Base):
    __tablename__ = "a"

    id = mapped_column(Integer, primary_key=True)
    b_id = mapped_column(ForeignKey("b.id"))

class B(Base):
    __tablename__ = "b"

    id = mapped_column(Integer, primary_key=True)

class C(Base):
    __tablename__ = "c"

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

    some_c_value = mapped_column(String)

class D(Base):
    __tablename__ = "d"

    id = mapped_column(Integer, primary_key=True)
    c_id = mapped_column(ForeignKey("c.id"))
    b_id = mapped_column(ForeignKey("b.id"))

    some_d_value = mapped_column(String)

# 1\. set up the join() as a variable, so we can refer
# to it in the mapping multiple times.
j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)

# 2\. Create an AliasedClass to B
B_viacd = aliased(B, j, flat=True)

A.b = relationship(B_viacd, primaryjoin=A.b_id == j.c.b_id)

使用上述映射,简单连接如下所示:

sess.scalars(select(A).join(A.b)).all()

SELECT  a.id  AS  a_id,  a.b_id  AS  a_b_id
FROM  a  JOIN  (b  JOIN  d  ON  d.b_id  =  b.id  JOIN  c  ON  c.id  =  d.c_id)  ON  a.b_id  =  b.id 

将别名类映射与类型化结合,并避免早期映射器配置

aliased() 构造对映射类的创建强制执行 configure_mappers() 步骤,该步骤将解析所有当前类及其关系。如果当前映射所需的不相关映射类尚未声明,或者如果关系本身的配置需要访问尚未声明的类,则可能会出现问题。此外,当关系在前面声明时,SQLAlchemy 的声明模式与 Python 类型化的协作效果最佳。

为了组织关系的构建以解决这些问题,可以使用配置级别的事件钩子,如 MapperEvents.before_mapper_configured(),该钩子将仅在所有映射准备好配置时调用配置代码:

from sqlalchemy import event

class A(Base):
    __tablename__ = "a"

    id = mapped_column(Integer, primary_key=True)
    b_id = mapped_column(ForeignKey("b.id"))

@event.listens_for(A, "before_mapper_configured")
def _configure_ab_relationship(mapper, cls):
    # do the above configuration in a configuration hook

    j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)
    B_viacd = aliased(B, j, flat=True)
    A.b = relationship(B_viacd, primaryjoin=A.b_id == j.c.b_id)

在上面的示例中,当请求完全配置的 A 版本时,函数 _configure_ab_relationship() 将被调用,此时类 BDC 将可用。

对于与内联类型化集成的方法,可以使用类似的技术来有效地生成别名类的“单例”创建模式,其中它作为全局变量进行延迟初始化,然后可以在关系内联中使用它:

from typing import Any

B_viacd: Any = None
b_viacd_join: Any = None

class A(Base):
    __tablename__ = "a"

    id: Mapped[int] = mapped_column(primary_key=True)
    b_id: Mapped[int] = mapped_column(ForeignKey("b.id"))

    # 1\. the relationship can be declared using lambdas, allowing it to resolve
    #    to targets that are late-configured
    b: Mapped[B] = relationship(
        lambda: B_viacd, primaryjoin=lambda: A.b_id == b_viacd_join.c.b_id
    )

# 2\. configure the targets of the relationship using a before_mapper_configured
#    hook.
@event.listens_for(A, "before_mapper_configured")
def _configure_ab_relationship(mapper, cls):
    # 3\. set up the join() and AliasedClass as globals from within
    #    the configuration hook.

    global B_viacd, b_viacd_join

    b_viacd_join = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)
    B_viacd = aliased(B, b_viacd_join, flat=True)

在查询中使用别名类目标

在前面的示例中,A.b 关系将 B_viacd 实体作为目标,而 不是 直接的 B 类。要添加涉及 A.b 关系的附加条件,通常需要直接引用 B_viacd 而不是使用 B,特别是在将 A.b 的目标实体转换为别名或子查询的情况下。下面演示了使用子查询而不是连接的相同关系:

subq = select(B).join(D, D.b_id == B.id).join(C, C.id == D.c_id).subquery()

B_viacd_subquery = aliased(B, subq)

A.b = relationship(B_viacd_subquery, primaryjoin=A.b_id == subq.c.id)

使用上述 A.b 关系的查询将呈现一个子查询:

sess.scalars(select(A).join(A.b)).all()

SELECT  a.id  AS  a_id,  a.b_id  AS  a_b_id
FROM  a  JOIN  (SELECT  b.id  AS  id,  b.some_b_column  AS  some_b_column
FROM  b  JOIN  d  ON  d.b_id  =  b.id  JOIN  c  ON  c.id  =  d.c_id)  AS  anon_1  ON  a.b_id  =  anon_1.id 

如果我们想要根据 A.b 连接添加额外的条件,必须以 B_viacd_subquery 而不是直接以 B 的形式添加:

sess.scalars(
    select(A)
    .join(A.b)
    .where(B_viacd_subquery.some_b_column == "some b")
    .order_by(B_viacd_subquery.id)
).all()

SELECT  a.id  AS  a_id,  a.b_id  AS  a_b_id
FROM  a  JOIN  (SELECT  b.id  AS  id,  b.some_b_column  AS  some_b_column
FROM  b  JOIN  d  ON  d.b_id  =  b.id  JOIN  c  ON  c.id  =  d.c_id)  AS  anon_1  ON  a.b_id  =  anon_1.id
WHERE  anon_1.some_b_column  =  ?  ORDER  BY  anon_1.id 

将别名类映射与类型化结合,并避免早期映射器配置

对映射类创建 aliased() 构造会强制进行 configure_mappers() 步骤,这将解析所有当前的类及其关系。如果当前映射需要未声明的不相关的映射类,或者如果关系的配置本身需要访问尚未声明的类,则可能会出现问题。另外,当关系提前声明时,SQLAlchemy 的声明式模式与 Python 类型的工作方式最有效。

要组织关系构建以处理这些问题,可以使用一个配置级别的事件钩子,比如 MapperEvents.before_mapper_configured(),它只有在所有映射都准备好配置时才会调用配置代码:

from sqlalchemy import event

class A(Base):
    __tablename__ = "a"

    id = mapped_column(Integer, primary_key=True)
    b_id = mapped_column(ForeignKey("b.id"))

@event.listens_for(A, "before_mapper_configured")
def _configure_ab_relationship(mapper, cls):
    # do the above configuration in a configuration hook

    j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)
    B_viacd = aliased(B, j, flat=True)
    A.b = relationship(B_viacd, primaryjoin=A.b_id == j.c.b_id)

上述函数 _configure_ab_relationship() 只有在请求一个完全配置好的 A 版本时才会被调用,在这时 BDC 类会被加载。

对于与内联类型集成的方法,可以使用类似的技术有效地为别名类生成“单例”创建模式,在其中作为全局变量进行延迟初始化,然后可以在关系中使用:

from typing import Any

B_viacd: Any = None
b_viacd_join: Any = None

class A(Base):
    __tablename__ = "a"

    id: Mapped[int] = mapped_column(primary_key=True)
    b_id: Mapped[int] = mapped_column(ForeignKey("b.id"))

    # 1\. the relationship can be declared using lambdas, allowing it to resolve
    #    to targets that are late-configured
    b: Mapped[B] = relationship(
        lambda: B_viacd, primaryjoin=lambda: A.b_id == b_viacd_join.c.b_id
    )

# 2\. configure the targets of the relationship using a before_mapper_configured
#    hook.
@event.listens_for(A, "before_mapper_configured")
def _configure_ab_relationship(mapper, cls):
    # 3\. set up the join() and AliasedClass as globals from within
    #    the configuration hook.

    global B_viacd, b_viacd_join

    b_viacd_join = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)
    B_viacd = aliased(B, b_viacd_join, flat=True)

在查询中使用 AliasedClass 目标

在前面的示例中,A.b 关系将 B_viacd 实体作为目标,而不是直接使用 B 类。要添加涉及 A.b 关系的额外条件,通常需要直接引用 B_viacd 而不是使用 B,特别是在目标实体 A.b 需要转换为别名或子查询的情况下。以下是使用子查询而不是连接的相同关系的示例:

subq = select(B).join(D, D.b_id == B.id).join(C, C.id == D.c_id).subquery()

B_viacd_subquery = aliased(B, subq)

A.b = relationship(B_viacd_subquery, primaryjoin=A.b_id == subq.c.id)

使用上述 A.b 关系的查询将呈现一个子查询:

sess.scalars(select(A).join(A.b)).all()

SELECT  a.id  AS  a_id,  a.b_id  AS  a_b_id
FROM  a  JOIN  (SELECT  b.id  AS  id,  b.some_b_column  AS  some_b_column
FROM  b  JOIN  d  ON  d.b_id  =  b.id  JOIN  c  ON  c.id  =  d.c_id)  AS  anon_1  ON  a.b_id  =  anon_1.id 

如果我们想要根据 A.b 连接添加额外的条件,必须以 B_viacd_subquery 而不是直接使用 B

sess.scalars(
    select(A)
    .join(A.b)
    .where(B_viacd_subquery.some_b_column == "some b")
    .order_by(B_viacd_subquery.id)
).all()

SELECT  a.id  AS  a_id,  a.b_id  AS  a_b_id
FROM  a  JOIN  (SELECT  b.id  AS  id,  b.some_b_column  AS  some_b_column
FROM  b  JOIN  d  ON  d.b_id  =  b.id  JOIN  c  ON  c.id  =  d.c_id)  AS  anon_1  ON  a.b_id  =  anon_1.id
WHERE  anon_1.some_b_column  =  ?  ORDER  BY  anon_1.id 

带窗口函数的行限制关系

另一个关系到 AliasedClass 对象的有趣用例是关系需要连接到任何形式的专门 SELECT 时。一种情况是当需要使用窗口函数时,例如限制返回的行数。下面的示例说明了一个非主映射器关系,该关系将为每个集合加载前十个项目:

class A(Base):
    __tablename__ = "a"

    id = mapped_column(Integer, primary_key=True)

class B(Base):
    __tablename__ = "b"
    id = mapped_column(Integer, primary_key=True)
    a_id = mapped_column(ForeignKey("a.id"))

partition = select(
    B, func.row_number().over(order_by=B.id, partition_by=B.a_id).label("index")
).alias()

partitioned_b = aliased(B, partition)

A.partitioned_bs = relationship(
    partitioned_b, primaryjoin=and_(partitioned_b.a_id == A.id, partition.c.index < 10)
)

我们可以使用上述 partitioned_bs 关系与大多数加载器策略,例如 selectinload():

for a1 in session.scalars(select(A).options(selectinload(A.partitioned_bs))):
    print(a1.partitioned_bs)  # <-- will be no more than ten objects

在上面的例子中,“selectinload”查询如下所示:

SELECT
  a_1.id  AS  a_1_id,  anon_1.id  AS  anon_1_id,  anon_1.a_id  AS  anon_1_a_id,
  anon_1.data  AS  anon_1_data,  anon_1.index  AS  anon_1_index
FROM  a  AS  a_1
JOIN  (
  SELECT  b.id  AS  id,  b.a_id  AS  a_id,  b.data  AS  data,
  row_number()  OVER  (PARTITION  BY  b.a_id  ORDER  BY  b.id)  AS  index
  FROM  b)  AS  anon_1
ON  anon_1.a_id  =  a_1.id  AND  anon_1.index  <  %(index_1)s
WHERE  a_1.id  IN  (  ...  primary  key  collection  ...)
ORDER  BY  a_1.id

在上面的例子中,对于“a”中的每个匹配的主键,我们将按照“b.id”的顺序获取前十个“bs”。通过在“a_id”上分区,我们确保每个“行号”都局限于父“a_id”。

这样的映射通常还会包括从“A”到“B”的“普通”关系,用于持久性操作以及当需要“A”每个对象的完整集合时。

构建查询可用的属性

非常雄心勃勃的自定义连接条件可能无法直接持久化,有些情况下甚至可能无法正确加载。要消除持久性方程式的部分,使用标志relationship.viewonlyrelationship()上,将其建立为只读属性(写入到集合的数据将在 flush()时被忽略)。但是,在极端情况下,请考虑与Query一起使用常规的 Python 属性,如下所示:

class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)

    @property
    def addresses(self):
        return object_session(self).query(Address).with_parent(self).filter(...).all()

在其他情况下,可以构建描述符来利用现有的 Python 数据。有关更一般的 Python 属性的特殊讨论,请参阅使用描述符和混合部分。

另请参阅

使用描述符和混合

使用 viewonly 关系参数的注意事项

当应用于relationship()构造时,relationship.viewonly参数指示此relationship()不会参与任何 ORM 工作单元操作,此外,该属性也不会参与其表示的集合的 Python 变异。这意味着虽然 viewonly 关系可能引用可变的 Python 集合,如列表或集合,但对在映射实例上存在的该列表或集合进行更改对 ORM flush 过程没有影响。

要探索这种情景,请考虑以下映射:

from __future__ import annotations

import datetime

from sqlalchemy import and_
from sqlalchemy import ForeignKey
from sqlalchemy import func
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str | None]

    all_tasks: Mapped[list[Task]] = relationship()

    current_week_tasks: Mapped[list[Task]] = relationship(
        primaryjoin=lambda: and_(
            User.id == Task.user_account_id,
            # this expression works on PostgreSQL but may not be supported
            # by other database engines
            Task.task_date >= func.now() - datetime.timedelta(days=7),
        ),
        viewonly=True,
    )

class Task(Base):
    __tablename__ = "task"

    id: Mapped[int] = mapped_column(primary_key=True)
    user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
    description: Mapped[str | None]
    task_date: Mapped[datetime.datetime] = mapped_column(server_default=func.now())

    user: Mapped[User] = relationship(back_populates="current_week_tasks")

以下各节将说明此配置的不同方面。

在 Python 中,包括 backrefs 在内的变异操作不适用于 viewonly=True

上述映射针对User.current_week_tasks视图关系,作为Task.user属性的 backref 目标。目前,SQLAlchemy 的 ORM 配置过程尚未标记此项,但这是配置错误。更改Task上的.user属性不会影响.current_week_tasks属性:

>>> u1 = User()
>>> t1 = Task(task_date=datetime.datetime.now())
>>> t1.user = u1
>>> u1.current_week_tasks
[]

还有另一个称为relationship.sync_backrefs的参数,可以在这里打开,以允许在这种情况下对.current_week_tasks进行突变,但是这并不被认为是viewonly关系的最佳实践,其不应该依赖于 Python 中的突变。

在此映射中,可以在User.all_tasksTask.user之间配置反向引用,因为它们都不是viewonly并且将正常同步。

除了禁用viewonly关系的反向引用突变之外,Python 中对User.all_tasks集合的普通更改也不会反映在User.current_week_tasks集合中,直到更改已刷新到数据库中。

总的来说,对于一个自定义集合应该立即响应 Python 中突变的用例,viewonly关系通常不合适。更好的方法是使用 SQLAlchemy 的 Hybrid Attributes 功能,或者仅对于实例化的情况使用 Python 的@property,在这种情况下,可以实现一个用户定义的集合,该集合是以当前 Python 实例为基础生成的。要将我们的示例更改为这种工作方式,我们修复Task.user上的relationship.back_populates参数,以引用User.all_tasks,然后说明一个简单的@property,该@property将以User.all_tasks集合的立即结果的形式提供结果:

class User(Base):
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str | None]

    all_tasks: Mapped[list[Task]] = relationship(back_populates="user")

    @property
    def current_week_tasks(self) -> list[Task]:
        past_seven_days = datetime.datetime.now() - datetime.timedelta(days=7)
        return [t for t in self.all_tasks if t.task_date >= past_seven_days]

class Task(Base):
    __tablename__ = "task"

    id: Mapped[int] = mapped_column(primary_key=True)
    user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
    description: Mapped[str | None]
    task_date: Mapped[datetime.datetime] = mapped_column(server_default=func.now())

    user: Mapped[User] = relationship(back_populates="all_tasks")

每次在 Python 中计算的集合,我们都能保证始终得到正确的答案,而无需使用数据库:

>>> u1 = User()
>>> t1 = Task(task_date=datetime.datetime.now())
>>> t1.user = u1
>>> u1.current_week_tasks
[<__main__.Task object at 0x7f3d699523c0>]

viewonly=True的集合/属性在过期之前不会被重新查询

对于原始的viewonly属性,如果我们确实对User.all_tasks集合进行了更改,那么在个事件发生后,viewonly集合才能显示这些更改的最终结果。第一个是将User.all_tasks的更改 flushed,以便新数据在数据库中可用,至少在本地事务范围内可用。第二个是User.current_week_tasks属性被 expired,并通过新的 SQL 查询重新加载到数据库。

为了支持这一要求,使用的最简单的流程是只在主要是只读操作中使用viewonly关系。比如下面,如果我们从数据库中检索到一个新的User,集合将是当前的:

>>> with Session(e) as sess:
...     u1 = sess.scalar(select(User).where(User.id == 1))
...     print(u1.current_week_tasks)
[<__main__.Task object at 0x7f8711b906b0>]

当我们对u1.all_tasks进行修改时,如果我们希望在u1.current_week_tasks的 viewonly 关系中看到这些更改反映出来,这些更改需要被刷新,并且u1.current_week_tasks属性需要过期,以便在下次访问时懒加载。这样做的最简单方法是使用Session.commit(),保持Session.expire_on_commit参数设置为其默认值True

>>> with Session(e) as sess:
...     u1 = sess.scalar(select(User).where(User.id == 1))
...     u1.all_tasks.append(Task(task_date=datetime.datetime.now()))
...     sess.commit()
...     print(u1.current_week_tasks)
[<__main__.Task object at 0x7f8711b90ec0>, <__main__.Task object at 0x7f8711b90a10>]

上述,对Session.commit()的调用将更改刷新到数据库的u1.all_tasks,然后过期所有对象,因此当我们访问u1.current_week_tasks时,将从数据库中新鲜地获取此属性的内容,触发了一个:term:懒加载

要拦截操作而不实际提交事务,必须首先显式地过期属性。这样做的简单方法就是直接调用它。在下面的例子中,Session.flush()发送挂起的更改到数据库,然后使用Session.expire()来过期u1.current_week_tasks集合,以便在下次访问时重新获取:

>>> with Session(e) as sess:
...     u1 = sess.scalar(select(User).where(User.id == 1))
...     u1.all_tasks.append(Task(task_date=datetime.datetime.now()))
...     sess.flush()
...     sess.expire(u1, ["current_week_tasks"])
...     print(u1.current_week_tasks)
[<__main__.Task object at 0x7fd95a4c8c50>, <__main__.Task object at 0x7fd95a4c8c80>]

实际上,我们可以跳过对Session.flush()的调用,假设Session保持其默认值为TrueSession.autoflush,因为过期的current_week_tasks属性将在过期后在访问时触发自动刷新:

>>> with Session(e) as sess:
...     u1 = sess.scalar(select(User).where(User.id == 1))
...     u1.all_tasks.append(Task(task_date=datetime.datetime.now()))
...     sess.expire(u1, ["current_week_tasks"])
...     print(u1.current_week_tasks)  # triggers autoflush before querying
[<__main__.Task object at 0x7fd95a4c8c50>, <__main__.Task object at 0x7fd95a4c8c80>]

进一步发展上述方法,我们可以在相关的User.all_tasks集合发生变化时,通过事件钩子来以编程方式应用过期。这是一种高级技术,应该首先检查像@property这样的更简单的架构或者坚持只读用例。在我们的简单示例中,配置如下:

from sqlalchemy import event, inspect

@event.listens_for(User.all_tasks, "append")
@event.listens_for(User.all_tasks, "remove")
@event.listens_for(User.all_tasks, "bulk_replace")
def _expire_User_current_week_tasks(target, value, initiator):
    inspect(target).session.expire(target, ["current_week_tasks"])

使用上述钩子,突变操作被拦截,并导致User.current_week_tasks集合自动过期:

>>> with Session(e) as sess:
...     u1 = sess.scalar(select(User).where(User.id == 1))
...     u1.all_tasks.append(Task(task_date=datetime.datetime.now()))
...     print(u1.current_week_tasks)
[<__main__.Task object at 0x7f66d093ccb0>, <__main__.Task object at 0x7f66d093cce0>]

上面使用的AttributeEvents事件钩子也会被后向引用突变触发,因此通过上面的钩子也会拦截对Task.user的更改:

>>> with Session(e) as sess:
...     u1 = sess.scalar(select(User).where(User.id == 1))
...     t1 = Task(task_date=datetime.datetime.now())
...     t1.user = u1
...     sess.add(t1)
...     print(u1.current_week_tasks)
[<__main__.Task object at 0x7f3b0c070d10>, <__main__.Task object at 0x7f3b0c057d10>]

使用 viewonly=True 的 In-Python 突变不合适

上述映射将User.current_week_tasks视图关系作为Task.user属性的 backref 目标。这目前并未被 SQLAlchemy 的 ORM 配置过程标记,但这是一个配置错误。更改Task上的.user属性不会影响.current_week_tasks属性:

>>> u1 = User()
>>> t1 = Task(task_date=datetime.datetime.now())
>>> t1.user = u1
>>> u1.current_week_tasks
[]

这里还有另一个参数叫做relationship.sync_backrefs,可以在这里打开,允许在这种情况下对.current_week_tasks进行变异,然而这并不被认为是最佳实践,对于只读关系,不应依赖于 Python 中的变异。

在这种映射中,可以在User.all_tasksTask.user之间配置反向引用,因为这两者都不是只读的,将正常同步。

除了对于只读关系禁用反向引用变异的问题外,Python 中对User.all_tasks集合的普通更改也不会反映在User.current_week_tasks集合中,直到更改已刷新到数据库。

总的来说,对于一个自定义集合应立即响应 Python 中的变异的用例,只读关系通常不合适。更好的方法是使用 SQLAlchemy 的 Hybrid Attributes 功能,或者对于仅实例情况,使用 Python 的@property,其中可以实现以当前 Python 实例为基础生成的用户定义集合。要将我们的示例更改为这种方式工作,我们修复Task.user上的relationship.back_populates参数,引用User.all_tasks,然后演示一个简单的@property,将以即时User.all_tasks集合的结果为基础提供结果。

class User(Base):
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str | None]

    all_tasks: Mapped[list[Task]] = relationship(back_populates="user")

    @property
    def current_week_tasks(self) -> list[Task]:
        past_seven_days = datetime.datetime.now() - datetime.timedelta(days=7)
        return [t for t in self.all_tasks if t.task_date >= past_seven_days]

class Task(Base):
    __tablename__ = "task"

    id: Mapped[int] = mapped_column(primary_key=True)
    user_account_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
    description: Mapped[str | None]
    task_date: Mapped[datetime.datetime] = mapped_column(server_default=func.now())

    user: Mapped[User] = relationship(back_populates="all_tasks")

使用每次都在 Python 中计算的即时集合,我们保证始终具有正确答案,而无需使用数据库:

>>> u1 = User()
>>> t1 = Task(task_date=datetime.datetime.now())
>>> t1.user = u1
>>> u1.current_week_tasks
[<__main__.Task object at 0x7f3d699523c0>]

viewonly=True 集合/属性在过期之前不会重新查询

继续使用原始的只读属性,如果实际上对持久对象上的User.all_tasks集合进行更改,那么只有在发生两个事情之后,只读集合才能显示这种更改的净结果。第一是刷新对User.all_tasks的更改,以便新数据在数据库中可用,至少在本地事务范围内。第二是User.current_week_tasks属性被过期并通过对数据库的新 SQL 查询重新加载。

为了支持这个要求,使用最简单的流程是仅在主要是只读操作中使用仅视图关系。比如,如果我们从数据库中获取一个新的User,那么集合将是当前的:

>>> with Session(e) as sess:
...     u1 = sess.scalar(select(User).where(User.id == 1))
...     print(u1.current_week_tasks)
[<__main__.Task object at 0x7f8711b906b0>]

当我们对u1.all_tasks进行修改时,如果我们希望在u1.current_week_tasks视图关系中看到这些更改的反映,这些更改需要被刷新,并且u1.current_week_tasks属性需要过期,这样它将在下一次访问时进行延迟加载。最简单的方法是使用Session.commit(),保持Session.expire_on_commit参数设置为其默认值True

>>> with Session(e) as sess:
...     u1 = sess.scalar(select(User).where(User.id == 1))
...     u1.all_tasks.append(Task(task_date=datetime.datetime.now()))
...     sess.commit()
...     print(u1.current_week_tasks)
[<__main__.Task object at 0x7f8711b90ec0>, <__main__.Task object at 0x7f8711b90a10>]

上面,对Session.commit()的调用将更改刷新到数据库中,然后使所有对象过期,这样当我们访问u1.current_week_tasks时,一个:term:延迟加载会发生,从数据库中重新获取该属性的内容。

要拦截操作而不实际提交事务,需要首先显式地将属性过期。一个简单的方法是直接调用它。在下面的示例中,Session.flush()将挂起的更改发送到数据库,然后使用Session.expire()使u1.current_week_tasks集合过期,以便在下一次访问时重新获取:

>>> with Session(e) as sess:
...     u1 = sess.scalar(select(User).where(User.id == 1))
...     u1.all_tasks.append(Task(task_date=datetime.datetime.now()))
...     sess.flush()
...     sess.expire(u1, ["current_week_tasks"])
...     print(u1.current_week_tasks)
[<__main__.Task object at 0x7fd95a4c8c50>, <__main__.Task object at 0x7fd95a4c8c80>]

我们实际上可以跳过对Session.flush()的调用,假设一个保持Session.autoflush值为默认值TrueSession,因为过期的current_week_tasks属性在过期后被访问时会触发自动刷新:

>>> with Session(e) as sess:
...     u1 = sess.scalar(select(User).where(User.id == 1))
...     u1.all_tasks.append(Task(task_date=datetime.datetime.now()))
...     sess.expire(u1, ["current_week_tasks"])
...     print(u1.current_week_tasks)  # triggers autoflush before querying
[<__main__.Task object at 0x7fd95a4c8c50>, <__main__.Task object at 0x7fd95a4c8c80>]

继续上述方法到更复杂的内容,当相关的User.all_tasks集合发生变化时,我们可以在程序上应用过期,使用 event hooks。这是一种高级技术,应该首先检查简单的体系结构,比如@property或者坚持只读用例。在我们的简单示例中,这将配置为:

from sqlalchemy import event, inspect

@event.listens_for(User.all_tasks, "append")
@event.listens_for(User.all_tasks, "remove")
@event.listens_for(User.all_tasks, "bulk_replace")
def _expire_User_current_week_tasks(target, value, initiator):
    inspect(target).session.expire(target, ["current_week_tasks"])

有了上述钩子,变更操作将被拦截,并导致User.current_week_tasks集合自动过期:

>>> with Session(e) as sess:
...     u1 = sess.scalar(select(User).where(User.id == 1))
...     u1.all_tasks.append(Task(task_date=datetime.datetime.now()))
...     print(u1.current_week_tasks)
[<__main__.Task object at 0x7f66d093ccb0>, <__main__.Task object at 0x7f66d093cce0>]

上面使用的AttributeEvents事件钩子也会被反向引用的变化触发,因此通过上述钩子,对Task.user的更改也会被拦截:

>>> with Session(e) as sess:
...     u1 = sess.scalar(select(User).where(User.id == 1))
...     t1 = Task(task_date=datetime.datetime.now())
...     t1.user = u1
...     sess.add(t1)
...     print(u1.current_week_tasks)
[<__main__.Task object at 0x7f3b0c070d10>, <__main__.Task object at 0x7f3b0c057d10>]

处理大型集合

原文链接:docs.sqlalchemy.org/en/20/orm/large_collections.html

relationship()的默认行为是根据配置的 加载策略 完全将集合内容加载到内存中,该加载策略控制何时以及如何从数据库加载这些内容。 相关集合可能不仅在访问时加载到内存中,或者急切地加载,而且在集合本身发生变化时以及在由工作单元系统删除所有者对象时也需要进行填充。

当相关集合可能非常大时,无论在任何情况下将这样的集合加载到内存中都可能不可行,因为这样的操作可能会过度消耗时间、网络和内存资源。

本节包括旨在允许relationship()与大型集合一起使用并保持足够性能的 API 特性。

仅写关系

仅写加载器策略是配置relationship()的主要方法,该方法将保持可写性,但不会加载其内容到内存中。 下面是使用现代类型注释的声明式形式的仅写 ORM 配置的示例:

>>> from decimal import Decimal
>>> from datetime import datetime

>>> from sqlalchemy import ForeignKey
>>> from sqlalchemy import func
>>> from sqlalchemy.orm import DeclarativeBase
>>> from sqlalchemy.orm import Mapped
>>> from sqlalchemy.orm import mapped_column
>>> from sqlalchemy.orm import relationship
>>> from sqlalchemy.orm import Session
>>> from sqlalchemy.orm import WriteOnlyMapped

>>> class Base(DeclarativeBase):
...     pass

>>> class Account(Base):
...     __tablename__ = "account"
...     id: Mapped[int] = mapped_column(primary_key=True)
...     identifier: Mapped[str]
...
...     account_transactions: WriteOnlyMapped["AccountTransaction"] = relationship(
...         cascade="all, delete-orphan",
...         passive_deletes=True,
...         order_by="AccountTransaction.timestamp",
...     )
...
...     def __repr__(self):
...         return f"Account(identifier={self.identifier!r})"

>>> class AccountTransaction(Base):
...     __tablename__ = "account_transaction"
...     id: Mapped[int] = mapped_column(primary_key=True)
...     account_id: Mapped[int] = mapped_column(
...         ForeignKey("account.id", ondelete="cascade")
...     )
...     description: Mapped[str]
...     amount: Mapped[Decimal]
...     timestamp: Mapped[datetime] = mapped_column(default=func.now())
...
...     def __repr__(self):
...         return (
...             f"AccountTransaction(amount={self.amount:.2f}, "
...             f"timestamp={self.timestamp.isoformat()!r})"
...         )
...
...     __mapper_args__ = {"eager_defaults": True}

上述示例中,account_transactions 关系不是使用普通的Mapped注释配置的,而是使用WriteOnlyMapped类型注释配置的,在运行时会将 lazy="write_only" 的 加载策略 分配给目标 relationship()WriteOnlyMapped 注释是 Mapped 注释的替代形式,指示对象实例上使用 WriteOnlyCollection 集合类型。

上述relationship()配置还包括几个元素,这些元素是特定于删除 Account 对象时要采取的操作以及从 account_transactions 集合中移除 AccountTransaction 对象时要采取的操作。 这些元素包括:

  • passive_deletes=True - 允许工作单元在删除Account时无需加载集合;参见使用 ORM 关系进行外键级联删除。

  • ForeignKey约束上配置ondelete="cascade"。这也在使用 ORM 关系进行外键级联删除中详细说明。

  • cascade="all, delete-orphan" - 指示工作单元在从集合中删除时删除AccountTransaction对象。请参见 delete-orphan 中的 Cascades 文档。

2.0 版本新增:“仅写入”关系加载器。

创建和持久化新的仅写入集合

写入-仅集合仅允许对瞬态或挂起对象直接分配集合。根据我们上面的映射,这表示我们可以创建一个新的Account对象,其中包含一系列要添加到Session中的AccountTransaction对象。任何 Python 可迭代对象都可以用作要开始的对象的来源,下面我们使用 Python list

>>> new_account = Account(
...     identifier="account_01",
...     account_transactions=[
...         AccountTransaction(description="initial deposit", amount=Decimal("500.00")),
...         AccountTransaction(description="transfer", amount=Decimal("1000.00")),
...         AccountTransaction(description="withdrawal", amount=Decimal("-29.50")),
...     ],
... )

>>> with Session(engine) as session:
...     session.add(new_account)
...     session.commit()
BEGIN  (implicit)
INSERT  INTO  account  (identifier)  VALUES  (?)
[...]  ('account_01',)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)
VALUES  (?,  ?,  ?,  CURRENT_TIMESTAMP)  RETURNING  id,  timestamp
[...  (insertmanyvalues)  1/3  (ordered;  batch  not  supported)]  (1,  'initial deposit',  500.0)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)
VALUES  (?,  ?,  ?,  CURRENT_TIMESTAMP)  RETURNING  id,  timestamp
[insertmanyvalues  2/3  (ordered;  batch  not  supported)]  (1,  'transfer',  1000.0)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)
VALUES  (?,  ?,  ?,  CURRENT_TIMESTAMP)  RETURNING  id,  timestamp
[insertmanyvalues  3/3  (ordered;  batch  not  supported)]  (1,  'withdrawal',  -29.5)
COMMIT 

一旦对象被持久化到数据库(即处于持久化或分离状态),该集合就具有扩展新项目的能力,以及删除单个项目的能力。但是,该集合可能不再重新分配一个完整的替换集合,因为这样的操作需要将先前的集合完全加载到内存中,以便将旧条目与新条目进行协调:

>>> new_account.account_transactions = [
...     AccountTransaction(description="some transaction", amount=Decimal("10.00"))
... ]
Traceback (most recent call last):
...
sqlalchemy.exc.InvalidRequestError: Collection "Account.account_transactions" does not
support implicit iteration; collection replacement operations can't be used

向现有集合添加新项目

对于持久对象的写入-仅集合,使用工作单元过程对集合进行修改只能通过使用WriteOnlyCollection.add()WriteOnlyCollection.add_all()WriteOnlyCollection.remove()方法进行:

>>> from sqlalchemy import select
>>> session = Session(engine, expire_on_commit=False)
>>> existing_account = session.scalar(select(Account).filter_by(identifier="account_01"))
BEGIN  (implicit)
SELECT  account.id,  account.identifier
FROM  account
WHERE  account.identifier  =  ?
[...]  ('account_01',)
>>> existing_account.account_transactions.add_all(
...     [
...         AccountTransaction(description="paycheck", amount=Decimal("2000.00")),
...         AccountTransaction(description="rent", amount=Decimal("-800.00")),
...     ]
... )
>>> session.commit()
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)
VALUES  (?,  ?,  ?,  CURRENT_TIMESTAMP)  RETURNING  id,  timestamp
[...  (insertmanyvalues)  1/2  (ordered;  batch  not  supported)]  (1,  'paycheck',  2000.0)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)
VALUES  (?,  ?,  ?,  CURRENT_TIMESTAMP)  RETURNING  id,  timestamp
[insertmanyvalues  2/2  (ordered;  batch  not  supported)]  (1,  'rent',  -800.0)
COMMIT 

上述添加的项目将在Session中的挂起队列中保留,直到下一次刷新,在此刻它们将被插入到数据库中,假设添加的对象之前是瞬态的。

查询项目

WriteOnlyCollection 在任何时候都不会存储对集合当前内容的引用,也不具有直接发出 SELECT 到数据库以加载它们的行为;其覆盖的假设是集合可能包含数千或数百万行,并且不应作为任何其他操作的副作用而完全加载到内存中。

相反,WriteOnlyCollection 包括诸如WriteOnlyCollection.select()之类的生成 SQL 的助手,该方法将生成一个预先配置了当前父行的正确 WHERE / FROM 条件的Select构造,然后可以进一步修改以选择所需的任何行范围,以及使用像服务器端游标之类的特性来调用以便以内存高效的方式迭代完整集合的进程。

下面是生成的语句的示例。请注意,它还包括在示例映射中由relationship.order_by参数指示的 ORDER BY 条件;如果未配置该参数,则将省略此条件:

>>> print(existing_account.account_transactions.select())
SELECT  account_transaction.id,  account_transaction.account_id,  account_transaction.description,
account_transaction.amount,  account_transaction.timestamp
FROM  account_transaction
WHERE  :param_1  =  account_transaction.account_id  ORDER  BY  account_transaction.timestamp 

我们可以使用这个Select构造与Session一起来查询AccountTransaction对象,最容易的是使用Session.scalars()方法,该方法将返回直接生成 ORM 对象的Result。通常,但不是必须的,Select可能会进一步修改以限制返回的记录;在下面的示例中,还添加了额外的 WHERE 条件,以仅加载“debit”账户交易,以及“LIMIT 10”以仅检索前十行:

>>> account_transactions = session.scalars(
...     existing_account.account_transactions.select()
...     .where(AccountTransaction.amount < 0)
...     .limit(10)
... ).all()
BEGIN  (implicit)
SELECT  account_transaction.id,  account_transaction.account_id,  account_transaction.description,
account_transaction.amount,  account_transaction.timestamp
FROM  account_transaction
WHERE  ?  =  account_transaction.account_id  AND  account_transaction.amount  <  ?
ORDER  BY  account_transaction.timestamp  LIMIT  ?  OFFSET  ?
[...]  (1,  0,  10,  0)
>>> print(account_transactions)
[AccountTransaction(amount=-29.50, timestamp='...'), AccountTransaction(amount=-800.00, timestamp='...')]

删除项目

在当前Session中加载的个体项可能会被标记为要从集合中删除,使用WriteOnlyCollection.remove()方法。当操作继续时,刷新过程将隐式地将对象视为已经是集合的一部分。下面的示例说明了如何删除单个AccountTransaction项,根据级联设置,将导致删除该行:

>>> existing_transaction = account_transactions[0]
>>> existing_account.account_transactions.remove(existing_transaction)
>>> session.commit()
DELETE  FROM  account_transaction  WHERE  account_transaction.id  =  ?
[...]  (3,)
COMMIT 

与任何 ORM 映射的集合一样,对象的删除可以按照解除与集合的关联并将对象保留在数据库中的方式进行,也可以根据relationship()的 delete-orphan 配置发出其行的 DELETE。

在不删除的情况下删除集合涉及将外键列设置为 NULL 以进行一对多关系,或者删除相应的关联行以进行多对多关系。

新项目的批量插入

WriteOnlyCollection可以生成 DML 构造,例如Insert对象,可在 ORM 上下文中使用以产生批量插入行为。请参阅 ORM 批量 INSERT 语句部分,了解 ORM 批量插入的概述。

一对多集合

仅针对常规的一对多集合WriteOnlyCollection.insert()方法将生成一个预先建立了与父对象相对应的 VALUES 条件的Insert构造。由于这个 VALUES 条件完全针对相关表,因此该语句可用于插入新的行,这些新行同时将成为相关集合中的新记录:

>>> session.execute(
...     existing_account.account_transactions.insert(),
...     [
...         {"description": "transaction 1", "amount": Decimal("47.50")},
...         {"description": "transaction 2", "amount": Decimal("-501.25")},
...         {"description": "transaction 3", "amount": Decimal("1800.00")},
...         {"description": "transaction 4", "amount": Decimal("-300.00")},
...     ],
... )
BEGIN  (implicit)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)  VALUES  (?,  ?,  ?,  CURRENT_TIMESTAMP)
[...]  [(1,  'transaction 1',  47.5),  (1,  'transaction 2',  -501.25),  (1,  'transaction 3',  1800.0),  (1,  'transaction 4',  -300.0)]
<...>
>>> session.commit()
COMMIT

另请参阅

ORM 批量 INSERT 语句 - 在 ORM 查询指南中

一对多 - 在基本关系模式中

多对多集合

对于一个多对多集合,两个类之间的关系涉及一个使用relationship.secondary参数配置的第三个表的情况,通过WriteOnlyCollection.add_all()方法,可以先分别批量插入新记录,然后检索它们,并将这些记录传递给WriteOnlyCollection.add_all()方法,单位操作过程将继续将它们作为集合的一部分进行持久化。

假设一个类BankAudit使用一个多对多表引用了许多AccountTransaction记录:

>>> from sqlalchemy import Table, Column
>>> audit_to_transaction = Table(
...     "audit_transaction",
...     Base.metadata,
...     Column("audit_id", ForeignKey("audit.id", ondelete="CASCADE"), primary_key=True),
...     Column(
...         "transaction_id",
...         ForeignKey("account_transaction.id", ondelete="CASCADE"),
...         primary_key=True,
...     ),
... )
>>> class BankAudit(Base):
...     __tablename__ = "audit"
...     id: Mapped[int] = mapped_column(primary_key=True)
...     account_transactions: WriteOnlyMapped["AccountTransaction"] = relationship(
...         secondary=audit_to_transaction, passive_deletes=True
...     )

为了说明这两个操作,我们使用批量插入添加更多的AccountTransaction对象,通过在批量插入语句中添加returning(AccountTransaction)来使用 RETURNING 检索它们(请注意,我们也可以同样轻松地使用现有的AccountTransaction对象):

>>> new_transactions = session.scalars(
...     existing_account.account_transactions.insert().returning(AccountTransaction),
...     [
...         {"description": "odd trans 1", "amount": Decimal("50000.00")},
...         {"description": "odd trans 2", "amount": Decimal("25000.00")},
...         {"description": "odd trans 3", "amount": Decimal("45.00")},
...     ],
... ).all()
BEGIN  (implicit)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)  VALUES
(?,  ?,  ?,  CURRENT_TIMESTAMP),  (?,  ?,  ?,  CURRENT_TIMESTAMP),  (?,  ?,  ?,  CURRENT_TIMESTAMP)
RETURNING  id,  account_id,  description,  amount,  timestamp
[...]  (1,  'odd trans 1',  50000.0,  1,  'odd trans 2',  25000.0,  1,  'odd trans 3',  45.0) 

准备好一个AccountTransaction对象列表后,可以使用WriteOnlyCollection.add_all()方法一次性将许多行与一个新的BankAudit对象关联起来:

>>> bank_audit = BankAudit()
>>> session.add(bank_audit)
>>> bank_audit.account_transactions.add_all(new_transactions)
>>> session.commit()
INSERT  INTO  audit  DEFAULT  VALUES
[...]  ()
INSERT  INTO  audit_transaction  (audit_id,  transaction_id)  VALUES  (?,  ?)
[...]  [(1,  10),  (1,  11),  (1,  12)]
COMMIT 

另请参见

ORM 批量插入语句 - 在 ORM 查询指南中

多对多 - 在基本关系模式中

项目的批量更新和删除

类似于WriteOnlyCollection可以预先建立 WHERE 条件生成Select构造的方式,它也可以生成具有相同 WHERE 条件的UpdateDelete构造,以允许针对大集合中的元素进行基于条件的 UPDATE 和 DELETE 语句。

一对多集合

就像插入(INSERT)一样,这个特性在一对多集合中最直接。

在下面的示例中,使用WriteOnlyCollection.update()方法生成一个 UPDATE 语句,针对集合中的元素,定位“amount”等于-800的行,并将200的数量添加到它们中:

>>> session.execute(
...     existing_account.account_transactions.update()
...     .values(amount=AccountTransaction.amount + 200)
...     .where(AccountTransaction.amount == -800),
... )
BEGIN  (implicit)
UPDATE  account_transaction  SET  amount=(account_transaction.amount  +  ?)
WHERE  ?  =  account_transaction.account_id  AND  account_transaction.amount  =  ?
[...]  (200,  1,  -800)
<...>

类似地,WriteOnlyCollection.delete()将生成一个 DELETE 语句,以相同的方式调用:

>>> session.execute(
...     existing_account.account_transactions.delete().where(
...         AccountTransaction.amount.between(0, 30)
...     ),
... )
DELETE  FROM  account_transaction  WHERE  ?  =  account_transaction.account_id
AND  account_transaction.amount  BETWEEN  ?  AND  ?  RETURNING  id
[...]  (1,  0,  30)
<...> 

多对多集合

提示

这里的技术涉及到稍微高级的多表更新表达式。

对于多对多集合的批量更新和删除,为了使 UPDATE 或 DELETE 语句与父对象的主键相关联,关联表必须明确地成为 UPDATE/DELETE 语句的一部分,这要求后端包括对非标准 SQL 语法的支持,或者在构造 UPDATE 或 DELETE 语句时需要额外的显式步骤。

对于支持多表版本的 UPDATE 的后端,WriteOnlyCollection.update()方法应该可以在多对多集合上工作,就像下面的示例中对AccountTransaction对象进行的 UPDATE 一样,涉及多对多的BankAudit.account_transactions集合:

>>> session.execute(
...     bank_audit.account_transactions.update().values(
...         description=AccountTransaction.description + " (audited)"
...     )
... )
UPDATE  account_transaction  SET  description=(account_transaction.description  ||  ?)
FROM  audit_transaction  WHERE  ?  =  audit_transaction.audit_id
AND  account_transaction.id  =  audit_transaction.transaction_id  RETURNING  id
[...]  (' (audited)',  1)
<...>

上述语句自动使用“UPDATE..FROM”语法,由 SQLite 和其他后端支持,在 WHERE 子句中命名附加的audit_transaction表。

要更新或删除多对多集合,其中不支持多表语法的情况下,多对多条件可以移动到 SELECT 中,例如可以与 IN 组合以匹配行。WriteOnlyCollection在这里仍然对我们有所帮助,因为我们使用WriteOnlyCollection.select()方法为我们生成此 SELECT,利用Select.with_only_columns()方法生成标量子查询:

>>> from sqlalchemy import update
>>> subq = bank_audit.account_transactions.select().with_only_columns(AccountTransaction.id)
>>> session.execute(
...     update(AccountTransaction)
...     .values(description=AccountTransaction.description + " (audited)")
...     .where(AccountTransaction.id.in_(subq))
... )
UPDATE  account_transaction  SET  description=(account_transaction.description  ||  ?)
WHERE  account_transaction.id  IN  (SELECT  account_transaction.id
FROM  audit_transaction
WHERE  ?  =  audit_transaction.audit_id  AND  account_transaction.id  =  audit_transaction.transaction_id)
RETURNING  id
[...]  (' (audited)',  1)
<...> 

只写集合 - API 文档

对象名称 描述
WriteOnlyCollection 只写集合可以将更改同步到属性事件系统中。
WriteOnlyMapped 代表“只写”关系的 ORM 映射属性类型。
class sqlalchemy.orm.WriteOnlyCollection

只写集合可以将更改同步到属性事件系统中。

使用WriteOnlyCollection在映射中使用"write_only"延迟加载策略与relationship()一起。有关此配置的背景,请参阅只写关系。

2.0 版本中的新功能。

另请参阅

只写关系

成员

add(), add_all(), delete(), insert(), remove(), select(), update()

类签名

sqlalchemy.orm.WriteOnlyCollection (sqlalchemy.orm.writeonly.AbstractCollectionWriter)

method add(item: _T) → None

将一个项添加到此WriteOnlyCollection中。

给定项将在下一个刷新时以父实例的集合的形式持久化到数据库中。

method add_all(iterator: Iterable[_T]) → None

将一个可迭代的项添加到此WriteOnlyCollection中。

给定的项将在下一个刷新时以父实例的集合的形式持久化到数据库中。

method delete() → Delete

生成一个Delete,该语句将以此实例本地的WriteOnlyCollection的形式引用行。

method insert() → Insert

对于一对多的集合,生成一个Insert,该语句将以此实例本地的WriteOnlyCollection的形式插入新的行。

此构造仅支持不包括relationship.secondary参数的Relationship。对于指向多对多表的关系,请使用普通的批量插入技术来生成新对象,然后使用AbstractCollectionWriter.add_all()将它们与集合关联起来。

method remove(item: _T) → None

从此WriteOnlyCollection中移除一个项。

下一个刷新时,给定项将从父实例的集合中移除。

method select() → Select[Tuple[_T]]

生成一个Select构造,表示此实例本地的WriteOnlyCollection中的行。

method update() → Update

生成一个Update,该语句将以此实例本地的WriteOnlyCollection的形式引用行。

class sqlalchemy.orm.WriteOnlyMapped

表示“只写”关系的 ORM 映射属性类型。

WriteOnlyMapped 类型注释可以在带注释的声明性表映射中使用,以指示对于特定的relationship()应使用lazy="write_only"加载策略。

例如:

class User(Base):
 __tablename__ = "user"
 id: Mapped[int] = mapped_column(primary_key=True)
 addresses: WriteOnlyMapped[Address] = relationship(
 cascade="all,delete-orphan"
 )

有关背景,请参阅仅写关系部分。

2.0 版中的新功能。

请参阅还有

仅写关系 - 完整背景

DynamicMapped - 包含遗留的Query支持

类签名

sqlalchemy.orm.WriteOnlyMapped (sqlalchemy.orm.base._MappedAnnotationBase) ## 动态关系加载器

遗留特性

“动态”延迟加载策略是现在在仅写关系部分中描述的“write_only”策略的遗留形式。

“动态”策略从相关集合中生成一个遗留的Query对象。然而,“动态”关系的一个主要缺点是,有几种情况下集合会完全迭代,其中一些是不明显的,只能通过细心的编程和逐案的测试来预防。因此,对于真正大型集合管理,应优先考虑WriteOnlyCollection

动态加载器也与异步 I/O(asyncio)扩展不兼容。可以在一些限制下使用,如 Asyncio 动态指南中所示,但再次建议优先考虑与 asyncio 完全兼容的WriteOnlyCollection

动态关系策略允许配置一个 relationship(),当在实例上访问时,将返回一个旧版的 Query 对象,而不是集合。然后可以进一步修改返回的 Query 对象,以便基于过滤条件迭代数据库集合。返回的 Query 对象是 AppenderQuery 的实例,它结合了 Query 的加载和迭代行为,以及 rudimentary 集合变异方法,如 AppenderQuery.append()AppenderQuery.remove()

可以使用带有类型注释的 Declarative 形式配置“动态”加载器策略,使用 DynamicMapped 注解类:

from sqlalchemy.orm import DynamicMapped

class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    posts: DynamicMapped[Post] = relationship()

在上述情况下,单个 User 对象上的 User.posts 集合将返回 AppenderQuery 对象,它是 Query 的子类,还支持基本的集合变异操作:

jack = session.get(User, id)

# filter Jack's blog posts
posts = jack.posts.filter(Post.headline == "this is a post")

# apply array slices
posts = jack.posts[5:20]

动态关系支持有限的写入操作,通过 AppenderQuery.append()AppenderQuery.remove() 方法:

oldpost = jack.posts.filter(Post.headline == "old post").one()
jack.posts.remove(oldpost)

jack.posts.append(Post("new post"))

由于动态关系的读取端总是查询数据库,对基础集合的更改直到数据刷新后才可见。然而,只要所使用的 Session 启用了“自动刷新”,这将在每次集合即将发出查询时自动发生。

动态关系加载器 - API

对象名称 描述
AppenderQuery 支持基本集合存储操作的动态查询。
DynamicMapped 代表“动态”关系的 ORM 映射属性类型。
class sqlalchemy.orm.AppenderQuery

支持基本集合存储操作的动态查询。

AppenderQuery 上的方法包括 Query 的所有方法,以及用于集合持久化的附加方法。

成员

add(), add_all(), append(), count(), extend(), remove()

类签名

sqlalchemy.orm.AppenderQuery (sqlalchemy.orm.dynamic.AppenderMixin, sqlalchemy.orm.Query)

method add(item: _T) → None

继承自 AppenderMixin.add() 方法的 AppenderMixin

将项目添加到此 AppenderQuery

给定的项目将在下一次 flush 时以父实例集合的形式持久化到数据库中。

此方法旨在帮助与 WriteOnlyCollection 集合类实现向前兼容。

版本 2.0 中的新功能。

method add_all(iterator: Iterable[_T]) → None

继承自 AppenderMixin.add_all() 方法的 AppenderMixin

将可迭代项目添加到此 AppenderQuery

给定的项目将在下一次 flush 时以父实例集合的形式持久化到数据库中。

此方法旨在帮助与 WriteOnlyCollection 集合类实现向前兼容。

版本 2.0 中的新功能。

method append(item: _T) → None

继承自 AppenderMixin.append() 方法的 AppenderMixin

将项目追加到此 AppenderQuery

给定的项目将在下一次 flush 时以父实例集合的形式持久化到数据库中。

method count() → int

继承自 AppenderMixin.count() 方法的 AppenderMixin

返回此 Query 形成的 SQL 返回的行数。

这将生成此查询的 SQL 如下:

SELECT count(1) AS count_1 FROM (
    SELECT <rest of query follows...>
) AS anon_1

上述 SQL 返回一行,即计数函数的聚合值;然后 Query.count() 方法返回该单个整数值。

警告

需要注意的是,count() 返回的值与此 Query 从诸如 .all() 方法返回的 ORM 对象数量不同。当 Query 对象被要求返回完整实体时,将根据主键对条目进行去重,这意味着如果相同的主键值在结果中出现多次,则仅存在一个该主键的对象。这不适用于针对个别列的查询。

另请参阅

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

若要对特定列进行精细化计数控制,跳过子查询的使用或以其他方式控制 FROM 子句,或使用其他聚合函数,请将 expression.func 表达式与 Session.query() 结合使用,例如:

from sqlalchemy import func

# count User records, without
# using a subquery.
session.query(func.count(User.id))

# return count of user "id" grouped
# by "name"
session.query(func.count(User.id)).\
        group_by(User.name)

from sqlalchemy import distinct

# count distinct "name" values
session.query(func.count(distinct(User.name)))

另请参阅

2.0 迁移 - ORM 使用

method extend(iterator: Iterable[_T]) → None

继承自 AppenderMixin.extend() 方法的 AppenderMixin

将项目的可迭代对象添加到此AppenderQuery中。

给定的项目将在下次 flush 时以父实例的集合的形式持久化到数据库中。

method remove(item: _T) → None

继承自 AppenderMixin.remove() 方法的 AppenderMixin

从此 AppenderQuery 中移除一个项目。

下次 flush 时,给定的项目将从父实例的集合中移除。

class sqlalchemy.orm.DynamicMapped

表示“动态”关系的 ORM 映射属性类型。

DynamicMapped 类型注释可以在 注释的声明性表 映射中使用,以指示应该为特定的 relationship() 使用 lazy="dynamic" 加载策略。

传统功能

“dynamic” 懒加载策略是现在称为“write_only”策略的传统形式,详情请参见 写入关系 部分。

例如:

class User(Base):
    __tablename__ = "user"
    id: Mapped[int] = mapped_column(primary_key=True)
    addresses: DynamicMapped[Address] = relationship(
        cascade="all,delete-orphan"
    )

请参阅 动态关系加载器 部分以了解背景知识。

2.0 版中新增。

另请参阅

动态关系加载器 - 完整背景

WriteOnlyMapped - 完全 2.0 版本的风格

类签名

sqlalchemy.orm.DynamicMapped (sqlalchemy.orm.base._MappedAnnotationBase) ## 设置 RaiseLoad

当属性通常会发出懒加载时,“raise”-loaded 关系将引发一个InvalidRequestError

class MyClass(Base):
    __tablename__ = "some_table"

    # ...

    children: Mapped[List[MyRelatedClass]] = relationship(lazy="raise")

在上面的示例中,如果 children 集合之前未填充,则对该集合进行属性访问将引发异常。这包括读取访问,但对于集合还将影响写入访问,因为集合不能在未加载的情况下进行突变。这样做的原因是确保应用程序在某个特定上下文中不会发出任何意外的惰性加载。与必须通过 SQL 日志来确定所有必要属性是否已急切加载相比,"raise" 策略将在访问时立即引发未加载的属性。raise 策略也可基于查询选项使用 raiseload() 加载器选项。

另请参阅

使用 raiseload 防止不必要的惰性加载

使用被动删除

SQLAlchemy 中集合管理的一个重要方面是,当删除引用集合的对象时,SQLAlchemy 需要考虑到位于此集合内部的对象。这些对象将需要与父对象解除关联,对于一对多集合,这意味着外键列将被设置为 NULL,或者根据 级联 设置,可能希望为这些行发出 DELETE。

工作单元 过程只考虑逐行处理对象,这意味着 DELETE 操作意味着集合内的所有行必须在刷新过程中完全加载到内存中。对于大型集合来说,这是不可行的,因此我们转而依靠数据库自身的能力,使用外键 ON DELETE 规则自动更新或删除行,指示工作单元无需实际加载这些行即可处理它们。可以通过配置 relationship.passive_deletesrelationship() 构造上来指示工作单元以此方式工作;正在使用的外键约束也必须正确配置。

有关完整的“被动删除”配置的进一步细节,请参阅章节 使用 ORM 关系与外键 ON DELETE 级联。

只写关系

只写 加载器策略是配置 relationship() 的主要手段,它将保持可写,但不会加载其内容到内存中。现代类型注释的 Declarative 形式中的只写 ORM 配置示例如下:

>>> from decimal import Decimal
>>> from datetime import datetime

>>> from sqlalchemy import ForeignKey
>>> from sqlalchemy import func
>>> from sqlalchemy.orm import DeclarativeBase
>>> from sqlalchemy.orm import Mapped
>>> from sqlalchemy.orm import mapped_column
>>> from sqlalchemy.orm import relationship
>>> from sqlalchemy.orm import Session
>>> from sqlalchemy.orm import WriteOnlyMapped

>>> class Base(DeclarativeBase):
...     pass

>>> class Account(Base):
...     __tablename__ = "account"
...     id: Mapped[int] = mapped_column(primary_key=True)
...     identifier: Mapped[str]
...
...     account_transactions: WriteOnlyMapped["AccountTransaction"] = relationship(
...         cascade="all, delete-orphan",
...         passive_deletes=True,
...         order_by="AccountTransaction.timestamp",
...     )
...
...     def __repr__(self):
...         return f"Account(identifier={self.identifier!r})"

>>> class AccountTransaction(Base):
...     __tablename__ = "account_transaction"
...     id: Mapped[int] = mapped_column(primary_key=True)
...     account_id: Mapped[int] = mapped_column(
...         ForeignKey("account.id", ondelete="cascade")
...     )
...     description: Mapped[str]
...     amount: Mapped[Decimal]
...     timestamp: Mapped[datetime] = mapped_column(default=func.now())
...
...     def __repr__(self):
...         return (
...             f"AccountTransaction(amount={self.amount:.2f}, "
...             f"timestamp={self.timestamp.isoformat()!r})"
...         )
...
...     __mapper_args__ = {"eager_defaults": True}

上述的 account_transactions 关系不是使用普通的 Mapped 注解配置的,而是使用 WriteOnlyMapped 类型注解,在运行时将 lazy="write_only" 的加载策略分配给目标 relationship()WriteOnlyMapped 注解是 Mapped 注解的替代形式,表示在对象实例上使用 WriteOnlyCollection 集合类型。

上述的 relationship() 配置还包括几个元素,用于指定在删除 Account 对象以及从 account_transactions 集合中移除 AccountTransaction 对象时要执行的操作。这些元素是:

  • passive_deletes=True - 允许工作单元在删除 Account 时不必加载集合;请参阅使用 ORM 关系进行级联删除。

  • ondelete="cascade" 配置在 ForeignKey 约束上。这也在 使用 ORM 关系进行级联删除 中详细说明。

  • cascade="all, delete-orphan" - 指示工作单元在从集合中删除 AccountTransaction 对象时删除它们。请参阅 delete-orphan 中的 Cascades 文档。

2.0 版本中的新功能:添加了“只写”关系加载器。

创建和持久化新的只写集合

只写集合允许对瞬态或待处理对象进行集合的直接赋值。通过我们上述的映射,这表示我们可以创建一个新的 Account 对象,并将一系列 AccountTransaction 对象添加到 Session 中。任何 Python 可迭代对象都可以作为对象的来源,下面我们使用了 Python 的 list

>>> new_account = Account(
...     identifier="account_01",
...     account_transactions=[
...         AccountTransaction(description="initial deposit", amount=Decimal("500.00")),
...         AccountTransaction(description="transfer", amount=Decimal("1000.00")),
...         AccountTransaction(description="withdrawal", amount=Decimal("-29.50")),
...     ],
... )

>>> with Session(engine) as session:
...     session.add(new_account)
...     session.commit()
BEGIN  (implicit)
INSERT  INTO  account  (identifier)  VALUES  (?)
[...]  ('account_01',)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)
VALUES  (?,  ?,  ?,  CURRENT_TIMESTAMP)  RETURNING  id,  timestamp
[...  (insertmanyvalues)  1/3  (ordered;  batch  not  supported)]  (1,  'initial deposit',  500.0)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)
VALUES  (?,  ?,  ?,  CURRENT_TIMESTAMP)  RETURNING  id,  timestamp
[insertmanyvalues  2/3  (ordered;  batch  not  supported)]  (1,  'transfer',  1000.0)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)
VALUES  (?,  ?,  ?,  CURRENT_TIMESTAMP)  RETURNING  id,  timestamp
[insertmanyvalues  3/3  (ordered;  batch  not  supported)]  (1,  'withdrawal',  -29.5)
COMMIT 

一旦对象已经持久化到数据库(即处于持久化或分离状态),集合就具有了扩展新项目以及删除单个项目的能力。但是,集合可能不能再重新分配为完整的替换集合,因为这样的操作需要将先前的集合完全加载到内存中,以便将旧条目与新条目进行协调:

>>> new_account.account_transactions = [
...     AccountTransaction(description="some transaction", amount=Decimal("10.00"))
... ]
Traceback (most recent call last):
...
sqlalchemy.exc.InvalidRequestError: Collection "Account.account_transactions" does not
support implicit iteration; collection replacement operations can't be used

向现有集合添加新项目

对于持久化对象的只写集合,使用工作单元过程修改集合只能通过使用WriteOnlyCollection.add()WriteOnlyCollection.add_all()WriteOnlyCollection.remove() 方法进行:

>>> from sqlalchemy import select
>>> session = Session(engine, expire_on_commit=False)
>>> existing_account = session.scalar(select(Account).filter_by(identifier="account_01"))
BEGIN  (implicit)
SELECT  account.id,  account.identifier
FROM  account
WHERE  account.identifier  =  ?
[...]  ('account_01',)
>>> existing_account.account_transactions.add_all(
...     [
...         AccountTransaction(description="paycheck", amount=Decimal("2000.00")),
...         AccountTransaction(description="rent", amount=Decimal("-800.00")),
...     ]
... )
>>> session.commit()
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)
VALUES  (?,  ?,  ?,  CURRENT_TIMESTAMP)  RETURNING  id,  timestamp
[...  (insertmanyvalues)  1/2  (ordered;  batch  not  supported)]  (1,  'paycheck',  2000.0)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)
VALUES  (?,  ?,  ?,  CURRENT_TIMESTAMP)  RETURNING  id,  timestamp
[insertmanyvalues  2/2  (ordered;  batch  not  supported)]  (1,  'rent',  -800.0)
COMMIT 

上面添加的项目在 Session 内部保持在挂起队列中,直到下一次刷新,在此时它们被插入到数据库中,假设添加的对象之前是瞬态的。

查询项目

WriteOnlyCollection 在任何时候都不会存储对集合当前内容的引用,也不会具有直接发出 SELECT 到数据库以加载它们的行为;覆盖的假设是集合可能包含许多千万个行,并且绝不应作为任何其他操作的副作用完全加载到内存中。

相反,WriteOnlyCollection 包括 SQL 生成助手,如 WriteOnlyCollection.select(),它将生成一个预先配置了当前父行的正确 WHERE / FROM 条件的 Select 构造,然后可以进一步修改以选择所需的任何行范围,以及使用诸如服务器端游标等功能调用,以便以内存有效的方式迭代通过整个集合。

生成的语句如下所示。请注意,示例映射中包括 ORDER BY 标准,由 relationship()relationship.order_by 参数指示;如果未配置该参数,则会省略此标准:

>>> print(existing_account.account_transactions.select())
SELECT  account_transaction.id,  account_transaction.account_id,  account_transaction.description,
account_transaction.amount,  account_transaction.timestamp
FROM  account_transaction
WHERE  :param_1  =  account_transaction.account_id  ORDER  BY  account_transaction.timestamp 

我们可以使用这个 Select 结构以及 Session 来查询 AccountTransaction 对象,最简单的方法是使用 Session.scalars() 方法,该方法将直接返回一个 Result,其中包含 ORM 对象。通常情况下,虽然不是必需的,但Select 可能会进一步修改以限制返回的记录;在下面的示例中,添加了额外的 WHERE 条件,仅加载“借方”账户交易,并且使用“LIMIT 10”只检索前十行:

>>> account_transactions = session.scalars(
...     existing_account.account_transactions.select()
...     .where(AccountTransaction.amount < 0)
...     .limit(10)
... ).all()
BEGIN  (implicit)
SELECT  account_transaction.id,  account_transaction.account_id,  account_transaction.description,
account_transaction.amount,  account_transaction.timestamp
FROM  account_transaction
WHERE  ?  =  account_transaction.account_id  AND  account_transaction.amount  <  ?
ORDER  BY  account_transaction.timestamp  LIMIT  ?  OFFSET  ?
[...]  (1,  0,  10,  0)
>>> print(account_transactions)
[AccountTransaction(amount=-29.50, timestamp='...'), AccountTransaction(amount=-800.00, timestamp='...')]

移除项目

在当前 Session 中加载到持久状态的个别项目可以使用WriteOnlyCollection.remove() 方法标记为从集合中移除。在操作继续时,刷新过程将隐式考虑对象已经是集合的一部分。下面的示例说明了删除单个 AccountTransaction 项目,根据 cascade 设置,会导致删除该行:

>>> existing_transaction = account_transactions[0]
>>> existing_account.account_transactions.remove(existing_transaction)
>>> session.commit()
DELETE  FROM  account_transaction  WHERE  account_transaction.id  =  ?
[...]  (3,)
COMMIT 

与任何 ORM 映射的集合一样,对象移除可以根据relationship() 的 delete-orphan 配置,要么取消与集合的关联,同时保留对象在数据库中的存在,要么根据配置发出对其行的 DELETE 请求。

不删除的集合移除涉及将外键列设置为 NULL(对于一对多关系)或删除相应的关联行(对于多对多关系)。

新项目的批量插入

WriteOnlyCollection可以生成诸如Insert对象之类的 DML 构造,这些构造可以在 ORM 上下文中用于生成批量插入行为。有关 ORM 批量插入的概述,请参阅 ORM 批量插入语句部分。

一对多集合

对于普通的一对多集合WriteOnlyCollection.insert()方法将产生一个预先建立了与父对象对应的 VALUES 条件的Insert构造。由于这个 VALUES 条件完全针对相关表,所以该语句可以用于插入新行,这些新行将同时成为相关集合中的新记录:

>>> session.execute(
...     existing_account.account_transactions.insert(),
...     [
...         {"description": "transaction 1", "amount": Decimal("47.50")},
...         {"description": "transaction 2", "amount": Decimal("-501.25")},
...         {"description": "transaction 3", "amount": Decimal("1800.00")},
...         {"description": "transaction 4", "amount": Decimal("-300.00")},
...     ],
... )
BEGIN  (implicit)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)  VALUES  (?,  ?,  ?,  CURRENT_TIMESTAMP)
[...]  [(1,  'transaction 1',  47.5),  (1,  'transaction 2',  -501.25),  (1,  'transaction 3',  1800.0),  (1,  'transaction 4',  -300.0)]
<...>
>>> session.commit()
COMMIT

另请参阅

ORM 批量插入语句 - 在 ORM 查询指南中

一对多 - 在基本关系模式中

多对多集合

对于多对多集合,两个类之间的关系涉及第三个表,该表使用relationship.secondary参数配置relationship。要使用WriteOnlyCollection批量插入此类型的集合中的行,可以先单独批量插入新记录,然后使用 RETURNING 检索,然后将这些记录传递给WriteOnlyCollection.add_all()方法,工作单元过程将继续将其作为集合的一部分持久化。

假设一个类BankAudit通过多对多表引用了许多AccountTransaction记录:

>>> from sqlalchemy import Table, Column
>>> audit_to_transaction = Table(
...     "audit_transaction",
...     Base.metadata,
...     Column("audit_id", ForeignKey("audit.id", ondelete="CASCADE"), primary_key=True),
...     Column(
...         "transaction_id",
...         ForeignKey("account_transaction.id", ondelete="CASCADE"),
...         primary_key=True,
...     ),
... )
>>> class BankAudit(Base):
...     __tablename__ = "audit"
...     id: Mapped[int] = mapped_column(primary_key=True)
...     account_transactions: WriteOnlyMapped["AccountTransaction"] = relationship(
...         secondary=audit_to_transaction, passive_deletes=True
...     )

为了说明这两个操作,我们使用批量插入添加了更多的AccountTransaction对象,我们通过在批量 INSERT 语句中添加returning(AccountTransaction)来检索它们 (注意我们也可以使用现有的AccountTransaction对象):

>>> new_transactions = session.scalars(
...     existing_account.account_transactions.insert().returning(AccountTransaction),
...     [
...         {"description": "odd trans 1", "amount": Decimal("50000.00")},
...         {"description": "odd trans 2", "amount": Decimal("25000.00")},
...         {"description": "odd trans 3", "amount": Decimal("45.00")},
...     ],
... ).all()
BEGIN  (implicit)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)  VALUES
(?,  ?,  ?,  CURRENT_TIMESTAMP),  (?,  ?,  ?,  CURRENT_TIMESTAMP),  (?,  ?,  ?,  CURRENT_TIMESTAMP)
RETURNING  id,  account_id,  description,  amount,  timestamp
[...]  (1,  'odd trans 1',  50000.0,  1,  'odd trans 2',  25000.0,  1,  'odd trans 3',  45.0) 

有了一系列准备好的AccountTransaction对象,可以使用WriteOnlyCollection.add_all()方法一次性将许多行与新的BankAudit对象关联起来:

>>> bank_audit = BankAudit()
>>> session.add(bank_audit)
>>> bank_audit.account_transactions.add_all(new_transactions)
>>> session.commit()
INSERT  INTO  audit  DEFAULT  VALUES
[...]  ()
INSERT  INTO  audit_transaction  (audit_id,  transaction_id)  VALUES  (?,  ?)
[...]  [(1,  10),  (1,  11),  (1,  12)]
COMMIT 

另请参阅

ORM 批量插入语句 - 在 ORM 查询指南中

多对多 - 位于基本关系模式

Items 的批量 UPDATE 和 DELETE

WriteOnlyCollection类似,它可以生成带有预先建立的 WHERE 条件的Select结构,也可以生成带有相同 WHERE 条件的UpdateDelete结构,以允许针对大型集合中的元素进行基于条件的 UPDATE 和 DELETE 语句。

一对多集合

与 INSERT 一样,这个特性对于一对多集合来说最为直接。

在下面的示例中,WriteOnlyCollection.update()方法用于生成一条 UPDATE 语句,针对集合中的元素发出,定位“amount”等于-800的行,并将200的数量添加到它们:

>>> session.execute(
...     existing_account.account_transactions.update()
...     .values(amount=AccountTransaction.amount + 200)
...     .where(AccountTransaction.amount == -800),
... )
BEGIN  (implicit)
UPDATE  account_transaction  SET  amount=(account_transaction.amount  +  ?)
WHERE  ?  =  account_transaction.account_id  AND  account_transaction.amount  =  ?
[...]  (200,  1,  -800)
<...>

类似地,WriteOnlyCollection.delete()将生成一个 DELETE 语句,以相同的方式调用:

>>> session.execute(
...     existing_account.account_transactions.delete().where(
...         AccountTransaction.amount.between(0, 30)
...     ),
... )
DELETE  FROM  account_transaction  WHERE  ?  =  account_transaction.account_id
AND  account_transaction.amount  BETWEEN  ?  AND  ?  RETURNING  id
[...]  (1,  0,  30)
<...> 

多对多集合

提示

这里涉及到稍微高级的多表 UPDATE 表达式技巧。

对于多对多集合的批量 UPDATE 和 DELETE,为了使 UPDATE 或 DELETE 语句与父对象的主键相关联,必须显式地将关联表包含在 UPDATE/DELETE 语句中,这要求后端要么包括对非标准 SQL 语法的支持,要么在构建 UPDATE 或 DELETE 语句时需要额外的显式步骤。

对于支持多表版本 UPDATE 的后端,WriteOnlyCollection.update()方法应该可以直接用于多对多集合,就像下面的示例中针对多对多BankAudit.account_transactions集合中的AccountTransaction对象发出 UPDATE 一样:

>>> session.execute(
...     bank_audit.account_transactions.update().values(
...         description=AccountTransaction.description + " (audited)"
...     )
... )
UPDATE  account_transaction  SET  description=(account_transaction.description  ||  ?)
FROM  audit_transaction  WHERE  ?  =  audit_transaction.audit_id
AND  account_transaction.id  =  audit_transaction.transaction_id  RETURNING  id
[...]  (' (audited)',  1)
<...>

上述语句自动使用了“UPDATE..FROM”语法,该语法由 SQLite 和其他数据库支持,以在 WHERE 子句中命名额外的audit_transaction表。

要更新或删除多对多集合,其中多表语法不可用,多对多条件可以移动到 SELECT 语句中,例如可以与 IN 组合以匹配行。 在这里,WriteOnlyCollection仍然对我们有帮助,因为我们使用WriteOnlyCollection.select()方法为我们生成此 SELECT,利用Select.with_only_columns()方法生成标量子查询:

>>> from sqlalchemy import update
>>> subq = bank_audit.account_transactions.select().with_only_columns(AccountTransaction.id)
>>> session.execute(
...     update(AccountTransaction)
...     .values(description=AccountTransaction.description + " (audited)")
...     .where(AccountTransaction.id.in_(subq))
... )
UPDATE  account_transaction  SET  description=(account_transaction.description  ||  ?)
WHERE  account_transaction.id  IN  (SELECT  account_transaction.id
FROM  audit_transaction
WHERE  ?  =  audit_transaction.audit_id  AND  account_transaction.id  =  audit_transaction.transaction_id)
RETURNING  id
[...]  (' (audited)',  1)
<...> 

写入仅集合 - API 文档

对象名称 描述
WriteOnlyCollection 写入仅集合,可将更改同步到属性事件系统中。
WriteOnlyMapped 代表“仅写”关系的 ORM 映射属性类型。
class sqlalchemy.orm.WriteOnlyCollection

写入仅集合,可将更改同步到属性事件系统中。

WriteOnlyCollection在映射中使用"write_only"延迟加载策略与relationship()一起使用。有关此配置的背景,请参阅仅写关系。

版本 2.0 中的新功能。

请参阅

仅写关系

成员

add(), add_all(), delete(), insert(), remove(), select(), update()

类签名

class sqlalchemy.orm.WriteOnlyCollection (sqlalchemy.orm.writeonly.AbstractCollectionWriter)

method add(item: _T) → None

向此WriteOnlyCollection添加一个项目。

下一次刷新时,给定的项目将以父实例的集合的形式持久化到数据库中。

method add_all(iterator: Iterable[_T]) → None

向此WriteOnlyCollection添加一组项目。

下一次刷新时,给定的项目将以父实例的集合的形式持久化到数据库中。

method delete() → Delete

生成一个 Delete,将引用以这个实例本地的WriteOnlyCollection

method insert() → Insert

对于一对多集合,产生一个 Insert,该插入将以此实例本地 WriteOnlyCollection 为条件插入新的行。

此构造仅支持不包含 relationship.secondary 参数的 Relationship。对于引用到多对多表的关系,请使用普通的批量插入技术来产生新对象,然后使用 AbstractCollectionWriter.add_all() 将其与集合关联起来。

method remove(item: _T) → None

从此 WriteOnlyCollection 中移除一个项目。

给定的项目将在下次刷新时从父实例的集合中移除。

method select() → Select[Tuple[_T]]

产生一个 Select 构造,表示此实例本地 WriteOnlyCollection 中的行。

method update() → Update

产生一个 Update,该更新将参考以此实例本地为条件的行的 WriteOnlyCollection

class sqlalchemy.orm.WriteOnlyMapped

表示“只写”关系的 ORM 映射属性类型。

WriteOnlyMapped 类型注释可以在 带注释的声明性表 映射中使用,以指示特定的 relationship() 应使用 lazy="write_only" 加载策略。

例如:

class User(Base):
 __tablename__ = "user"
 id: Mapped[int] = mapped_column(primary_key=True)
 addresses: WriteOnlyMapped[Address] = relationship(
 cascade="all,delete-orphan"
 )

参见章节 只写关系 了解背景。

新版本 2.0 中新增。

另请参阅

只写关系 - 完整的背景

DynamicMapped - 包含旧的 Query 支持

类签名

sqlalchemy.orm.WriteOnlyMapped (sqlalchemy.orm.base._MappedAnnotationBase)

创建和持久化新的只写集合

仅写集合允许直接将集合整体分配为用于瞬态或待处理对象。根据我们上面的映射,这表示我们可以创建一个新的 Account 对象,其中包含要添加到Session中的一系列 AccountTransaction 对象。任何 Python 可迭代对象都可以用作要开始的对象的源,下面我们使用 Python list

>>> new_account = Account(
...     identifier="account_01",
...     account_transactions=[
...         AccountTransaction(description="initial deposit", amount=Decimal("500.00")),
...         AccountTransaction(description="transfer", amount=Decimal("1000.00")),
...         AccountTransaction(description="withdrawal", amount=Decimal("-29.50")),
...     ],
... )

>>> with Session(engine) as session:
...     session.add(new_account)
...     session.commit()
BEGIN  (implicit)
INSERT  INTO  account  (identifier)  VALUES  (?)
[...]  ('account_01',)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)
VALUES  (?,  ?,  ?,  CURRENT_TIMESTAMP)  RETURNING  id,  timestamp
[...  (insertmanyvalues)  1/3  (ordered;  batch  not  supported)]  (1,  'initial deposit',  500.0)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)
VALUES  (?,  ?,  ?,  CURRENT_TIMESTAMP)  RETURNING  id,  timestamp
[insertmanyvalues  2/3  (ordered;  batch  not  supported)]  (1,  'transfer',  1000.0)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)
VALUES  (?,  ?,  ?,  CURRENT_TIMESTAMP)  RETURNING  id,  timestamp
[insertmanyvalues  3/3  (ordered;  batch  not  supported)]  (1,  'withdrawal',  -29.5)
COMMIT 

一旦对象被数据库持久化(即处于持久化或分离状态),集合就有能力扩展新项目以及个别项目的能力被移除。但是,集合可能不再被重新分配为完整替换集合,因为这样的操作要求以前的集合完全加载到内存中,以便将旧条目与新条目进行对比:

>>> new_account.account_transactions = [
...     AccountTransaction(description="some transaction", amount=Decimal("10.00"))
... ]
Traceback (most recent call last):
...
sqlalchemy.exc.InvalidRequestError: Collection "Account.account_transactions" does not
support implicit iteration; collection replacement operations can't be used

向现有集合添加新项目

对于持久对象的仅写集合,使用 unit of work 过程对集合进行修改只能使用WriteOnlyCollection.add()WriteOnlyCollection.add_all()WriteOnlyCollection.remove()方法:

>>> from sqlalchemy import select
>>> session = Session(engine, expire_on_commit=False)
>>> existing_account = session.scalar(select(Account).filter_by(identifier="account_01"))
BEGIN  (implicit)
SELECT  account.id,  account.identifier
FROM  account
WHERE  account.identifier  =  ?
[...]  ('account_01',)
>>> existing_account.account_transactions.add_all(
...     [
...         AccountTransaction(description="paycheck", amount=Decimal("2000.00")),
...         AccountTransaction(description="rent", amount=Decimal("-800.00")),
...     ]
... )
>>> session.commit()
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)
VALUES  (?,  ?,  ?,  CURRENT_TIMESTAMP)  RETURNING  id,  timestamp
[...  (insertmanyvalues)  1/2  (ordered;  batch  not  supported)]  (1,  'paycheck',  2000.0)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)
VALUES  (?,  ?,  ?,  CURRENT_TIMESTAMP)  RETURNING  id,  timestamp
[insertmanyvalues  2/2  (ordered;  batch  not  supported)]  (1,  'rent',  -800.0)
COMMIT 

上述添加的项目在Session中被保留在待处理队列中,直到下一个 flush,此时它们被插入到数据库中,假设添加的对象之前是瞬态的。

查询项目

WriteOnlyCollection不会在任何时候存储对集合当前内容的引用,也不会有任何直接发出 SELECT 到数据库以加载它们的行为;其覆盖的假设是集合可能包含许多千万个或数百万个行,并且不应作为任何其他操作的副作用完全加载到内存中。

相反,WriteOnlyCollection 包括生成 SQL 的辅助工具,如 WriteOnlyCollection.select(),它将生成一个预先配置了正确的 WHERE / FROM 条件的 Select 构造,然后可以进一步修改以选择所需的任何行范围,还可以使用 服务器端游标 进行调用,以便以内存有效的方式遍历整个集合。

下面说明了生成的语句。请注意,示例映射中的 relationship.order_by 参数指示了 ORDER BY 条件;如果未配置该参数,则该条件将被省略:

>>> print(existing_account.account_transactions.select())
SELECT  account_transaction.id,  account_transaction.account_id,  account_transaction.description,
account_transaction.amount,  account_transaction.timestamp
FROM  account_transaction
WHERE  :param_1  =  account_transaction.account_id  ORDER  BY  account_transaction.timestamp 

我们可以使用此 Select 构造以及 Session 来查询 AccountTransaction 对象,最简单的方法是使用 Session.scalars() 方法,该方法将返回一个直接产生 ORM 对象的 Result。通常情况下,但不是必需的,会进一步修改 Select 以限制返回的记录;在下面的示例中,添加了额外的 WHERE 条件以仅加载 “借方” 账户交易,并添加了 “LIMIT 10” 以仅检索前十行:

>>> account_transactions = session.scalars(
...     existing_account.account_transactions.select()
...     .where(AccountTransaction.amount < 0)
...     .limit(10)
... ).all()
BEGIN  (implicit)
SELECT  account_transaction.id,  account_transaction.account_id,  account_transaction.description,
account_transaction.amount,  account_transaction.timestamp
FROM  account_transaction
WHERE  ?  =  account_transaction.account_id  AND  account_transaction.amount  <  ?
ORDER  BY  account_transaction.timestamp  LIMIT  ?  OFFSET  ?
[...]  (1,  0,  10,  0)
>>> print(account_transactions)
[AccountTransaction(amount=-29.50, timestamp='...'), AccountTransaction(amount=-800.00, timestamp='...')]

删除项目

在当前 Session 中针对持久状态的单个加载项目可以使用 WriteOnlyCollection.remove() 方法标记为从集合中删除。当操作进行时,刷新过程将隐式考虑对象已经是集合的一部分。下面的示例说明了如何删除单个 AccountTransaction 项目,根据 级联 设置,这将导致删除该行:

>>> existing_transaction = account_transactions[0]
>>> existing_account.account_transactions.remove(existing_transaction)
>>> session.commit()
DELETE  FROM  account_transaction  WHERE  account_transaction.id  =  ?
[...]  (3,)
COMMIT 

与任何 ORM 映射的集合一样,对象的移除可以选择将对象与集合解除关联,同时保留对象在数据库中,或者可以基于 relationship() 的 delete-orphan 配置发出其行的 DELETE。

在不删除的情况下移除集合涉及将外键列设置为 NULL(对于 一对多 关系)或删除相应的关联行(对于 多对多 关系)。

批量插入新项目

WriteOnlyCollection 可以生成诸如 Insert 对象之类的 DML 构造,这些构造可以在 ORM 上下文中用于产生批量插入行为。参见 ORM 批量插入语句 章节了解 ORM 批量插入的概述。

一对多集合

仅适用于常规的一对多集合WriteOnlyCollection.insert() 方法将产生一个预先设定了与父对象相对应的 VALUES 条件的 Insert 构造。由于这个 VALUES 条件完全针对相关表,该语句可用于插入新行,这些新行将同时成为相关集合中的新记录:

>>> session.execute(
...     existing_account.account_transactions.insert(),
...     [
...         {"description": "transaction 1", "amount": Decimal("47.50")},
...         {"description": "transaction 2", "amount": Decimal("-501.25")},
...         {"description": "transaction 3", "amount": Decimal("1800.00")},
...         {"description": "transaction 4", "amount": Decimal("-300.00")},
...     ],
... )
BEGIN  (implicit)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)  VALUES  (?,  ?,  ?,  CURRENT_TIMESTAMP)
[...]  [(1,  'transaction 1',  47.5),  (1,  'transaction 2',  -501.25),  (1,  'transaction 3',  1800.0),  (1,  'transaction 4',  -300.0)]
<...>
>>> session.commit()
COMMIT

另请参阅

ORM 批量插入语句 - 在 ORM 查询指南 中

一对多 - 在 基本关系模式 中

多对多集合

对于多对多集合,两个类之间的关系涉及使用 relationship.secondary 参数配置的第三个表的情况。要使用 WriteOnlyCollection 批量插入此类型的集合中的行,新记录可能首先单独进行批量插入,然后使用 RETURNING 检索,然后将这些记录传递给 WriteOnlyCollection.add_all() 方法,在这个过程中,工作单元将会将它们作为集合的一部分持久化。

假设一个类BankAudit使用多对多表引用了许多AccountTransaction记录:

>>> from sqlalchemy import Table, Column
>>> audit_to_transaction = Table(
...     "audit_transaction",
...     Base.metadata,
...     Column("audit_id", ForeignKey("audit.id", ondelete="CASCADE"), primary_key=True),
...     Column(
...         "transaction_id",
...         ForeignKey("account_transaction.id", ondelete="CASCADE"),
...         primary_key=True,
...     ),
... )
>>> class BankAudit(Base):
...     __tablename__ = "audit"
...     id: Mapped[int] = mapped_column(primary_key=True)
...     account_transactions: WriteOnlyMapped["AccountTransaction"] = relationship(
...         secondary=audit_to_transaction, passive_deletes=True
...     )

为了说明这两个操作,我们使用批量插入添加更多AccountTransaction对象,我们通过将returning(AccountTransaction)添加到批量 INSERT 语句中使用 RETURNING 检索(注意我们也可以使用现有的AccountTransaction对象):

>>> new_transactions = session.scalars(
...     existing_account.account_transactions.insert().returning(AccountTransaction),
...     [
...         {"description": "odd trans 1", "amount": Decimal("50000.00")},
...         {"description": "odd trans 2", "amount": Decimal("25000.00")},
...         {"description": "odd trans 3", "amount": Decimal("45.00")},
...     ],
... ).all()
BEGIN  (implicit)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)  VALUES
(?,  ?,  ?,  CURRENT_TIMESTAMP),  (?,  ?,  ?,  CURRENT_TIMESTAMP),  (?,  ?,  ?,  CURRENT_TIMESTAMP)
RETURNING  id,  account_id,  description,  amount,  timestamp
[...]  (1,  'odd trans 1',  50000.0,  1,  'odd trans 2',  25000.0,  1,  'odd trans 3',  45.0) 

有一组准备好的AccountTransaction对象列表,使用WriteOnlyCollection.add_all()方法可以一次性将许多行与新的BankAudit对象关联起来:

>>> bank_audit = BankAudit()
>>> session.add(bank_audit)
>>> bank_audit.account_transactions.add_all(new_transactions)
>>> session.commit()
INSERT  INTO  audit  DEFAULT  VALUES
[...]  ()
INSERT  INTO  audit_transaction  (audit_id,  transaction_id)  VALUES  (?,  ?)
[...]  [(1,  10),  (1,  11),  (1,  12)]
COMMIT 

另请参阅

ORM 批量 INSERT 语句 - 在 ORM 查询指南中

多对多 - 在基本关系模式中

一对多集合

对于普通的一对多集合WriteOnlyCollection.insert()方法将生成一个与父对象对应的 VALUES 条件预设的Insert构造。由于这个 VALUES 条件完全针对相关表,该语句可用于插入新行,同时这些新行也将成为相关集合中的新记录:

>>> session.execute(
...     existing_account.account_transactions.insert(),
...     [
...         {"description": "transaction 1", "amount": Decimal("47.50")},
...         {"description": "transaction 2", "amount": Decimal("-501.25")},
...         {"description": "transaction 3", "amount": Decimal("1800.00")},
...         {"description": "transaction 4", "amount": Decimal("-300.00")},
...     ],
... )
BEGIN  (implicit)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)  VALUES  (?,  ?,  ?,  CURRENT_TIMESTAMP)
[...]  [(1,  'transaction 1',  47.5),  (1,  'transaction 2',  -501.25),  (1,  'transaction 3',  1800.0),  (1,  'transaction 4',  -300.0)]
<...>
>>> session.commit()
COMMIT

另请参阅

ORM 批量 INSERT 语句 - 在 ORM 查询指南中

一对多 - 在基本关系模式中

多对多集合

对于多对多集合,两个类之间的关系涉及使用relationshiprelationship.secondary参数配置的第三个表。要使用WriteOnlyCollection批量插入此类型的集合中的行,新记录可能首先被单独批量插入,然后使用 RETURNING 检索,并将这些记录传递给WriteOnlyCollection.add_all()方法,其中工作单元过程将继续将它们持久化为集合的一部分。

假设一个类BankAudit使用多对多表引用了许多AccountTransaction记录:

>>> from sqlalchemy import Table, Column
>>> audit_to_transaction = Table(
...     "audit_transaction",
...     Base.metadata,
...     Column("audit_id", ForeignKey("audit.id", ondelete="CASCADE"), primary_key=True),
...     Column(
...         "transaction_id",
...         ForeignKey("account_transaction.id", ondelete="CASCADE"),
...         primary_key=True,
...     ),
... )
>>> class BankAudit(Base):
...     __tablename__ = "audit"
...     id: Mapped[int] = mapped_column(primary_key=True)
...     account_transactions: WriteOnlyMapped["AccountTransaction"] = relationship(
...         secondary=audit_to_transaction, passive_deletes=True
...     )

为了说明这两个操作,我们使用批量插入添加更多的AccountTransaction对象,我们通过在批量插入语句中添加returning(AccountTransaction)来检索这些对象(请注意,我们也可以轻松地使用现有的AccountTransaction对象):

>>> new_transactions = session.scalars(
...     existing_account.account_transactions.insert().returning(AccountTransaction),
...     [
...         {"description": "odd trans 1", "amount": Decimal("50000.00")},
...         {"description": "odd trans 2", "amount": Decimal("25000.00")},
...         {"description": "odd trans 3", "amount": Decimal("45.00")},
...     ],
... ).all()
BEGIN  (implicit)
INSERT  INTO  account_transaction  (account_id,  description,  amount,  timestamp)  VALUES
(?,  ?,  ?,  CURRENT_TIMESTAMP),  (?,  ?,  ?,  CURRENT_TIMESTAMP),  (?,  ?,  ?,  CURRENT_TIMESTAMP)
RETURNING  id,  account_id,  description,  amount,  timestamp
[...]  (1,  'odd trans 1',  50000.0,  1,  'odd trans 2',  25000.0,  1,  'odd trans 3',  45.0) 

准备好一个AccountTransaction对象列表后,使用WriteOnlyCollection.add_all()方法一次将许多行与新的BankAudit对象关联起来:

>>> bank_audit = BankAudit()
>>> session.add(bank_audit)
>>> bank_audit.account_transactions.add_all(new_transactions)
>>> session.commit()
INSERT  INTO  audit  DEFAULT  VALUES
[...]  ()
INSERT  INTO  audit_transaction  (audit_id,  transaction_id)  VALUES  (?,  ?)
[...]  [(1,  10),  (1,  11),  (1,  12)]
COMMIT 

请参见

ORM 批量插入语句 - 在 ORM 查询指南

多对多 - 在基本关系模式

批量更新和删除项目

类似于WriteOnlyCollection可以生成带有预先建立 WHERE 条件的Select构造,它也可以生成具有相同 WHERE 条件的UpdateDelete构造,以允许针对大型集合中的元素的基于条件的 UPDATE 和 DELETE 语句。

一对多集合

和插入一样,这个特性在一对多集合中最为直接。

在下面的例子中,使用WriteOnlyCollection.update()方法生成一个 UPDATE 语句,该语句针对集合中的元素发出,定位“amount”等于-800的行,并向它们添加200的金额:

>>> session.execute(
...     existing_account.account_transactions.update()
...     .values(amount=AccountTransaction.amount + 200)
...     .where(AccountTransaction.amount == -800),
... )
BEGIN  (implicit)
UPDATE  account_transaction  SET  amount=(account_transaction.amount  +  ?)
WHERE  ?  =  account_transaction.account_id  AND  account_transaction.amount  =  ?
[...]  (200,  1,  -800)
<...>

类似地,WriteOnlyCollection.delete()将生成一个 DELETE 语句,以相同的方式调用:

>>> session.execute(
...     existing_account.account_transactions.delete().where(
...         AccountTransaction.amount.between(0, 30)
...     ),
... )
DELETE  FROM  account_transaction  WHERE  ?  =  account_transaction.account_id
AND  account_transaction.amount  BETWEEN  ?  AND  ?  RETURNING  id
[...]  (1,  0,  30)
<...> 

多对多集合

提示

这里涉及到多表 UPDATE 表达式,这略微更加复杂。

对于批量更新和删除多对多集合,为了使 UPDATE 或 DELETE 语句与父对象的主键相关联,关联表必须明确地包含在 UPDATE/DELETE 语句中,这要求后端包含对非标准 SQL 语法的支持,或者在构建 UPDATE 或 DELETE 语句时进行额外的明确步骤。

对于支持 UPDATE 的多表版本的后端,WriteOnlyCollection.update() 方法应该在多对多集合中工作而无需额外步骤,就像下面的例子中,在BankAudit.account_transactions集合的多对多对象AccountTransaction对象上发出 UPDATE 一样:

>>> session.execute(
...     bank_audit.account_transactions.update().values(
...         description=AccountTransaction.description + " (audited)"
...     )
... )
UPDATE  account_transaction  SET  description=(account_transaction.description  ||  ?)
FROM  audit_transaction  WHERE  ?  =  audit_transaction.audit_id
AND  account_transaction.id  =  audit_transaction.transaction_id  RETURNING  id
[...]  (' (audited)',  1)
<...>

上面的语句自动使用了“UPDATE..FROM”语法,由 SQLite 和其他后端支持,在 WHERE 子句中命名附加的audit_transaction表。

要更新或删除多对多集合,其中多表语法不可用,多对多条件可能会移到 SELECT 中,例如可以与 IN 组合以匹配行。WriteOnlyCollection 仍然在这里帮助我们,因为我们使用WriteOnlyCollection.select() 方法为我们生成这个 SELECT,利用Select.with_only_columns() 方法产生一个标量子查询:

>>> from sqlalchemy import update
>>> subq = bank_audit.account_transactions.select().with_only_columns(AccountTransaction.id)
>>> session.execute(
...     update(AccountTransaction)
...     .values(description=AccountTransaction.description + " (audited)")
...     .where(AccountTransaction.id.in_(subq))
... )
UPDATE  account_transaction  SET  description=(account_transaction.description  ||  ?)
WHERE  account_transaction.id  IN  (SELECT  account_transaction.id
FROM  audit_transaction
WHERE  ?  =  audit_transaction.audit_id  AND  account_transaction.id  =  audit_transaction.transaction_id)
RETURNING  id
[...]  (' (audited)',  1)
<...> 

一对多集合

就像在 INSERT 中一样,这个特性在一对多集合中最直接。

在下面的例子中,WriteOnlyCollection.update() 方法用于生成一个 UPDATE 语句,针对集合中的元素发出,定位“amount”等于-800的行,并将200的数量添加到它们中:

>>> session.execute(
...     existing_account.account_transactions.update()
...     .values(amount=AccountTransaction.amount + 200)
...     .where(AccountTransaction.amount == -800),
... )
BEGIN  (implicit)
UPDATE  account_transaction  SET  amount=(account_transaction.amount  +  ?)
WHERE  ?  =  account_transaction.account_id  AND  account_transaction.amount  =  ?
[...]  (200,  1,  -800)
<...>

类似地,WriteOnlyCollection.delete() 将产生一个 DELETE 语句,以相同的方式调用:

>>> session.execute(
...     existing_account.account_transactions.delete().where(
...         AccountTransaction.amount.between(0, 30)
...     ),
... )
DELETE  FROM  account_transaction  WHERE  ?  =  account_transaction.account_id
AND  account_transaction.amount  BETWEEN  ?  AND  ?  RETURNING  id
[...]  (1,  0,  30)
<...> 

多对多集合

小贴士

这里的技术涉及多表 UPDATE 表达式,稍微更高级一些。

对于多对多集合的批量 UPDATE 和 DELETE,为了使 UPDATE 或 DELETE 语句与父对象的主键相关联,关联表必须明确地成为 UPDATE/DELETE 语句的一部分,这要求后端包含支持非标准 SQL 语法的支持,或者在构造 UPDATE 或 DELETE 语句时需要额外的显式步骤。

对于支持 UPDATE 的多表版本的后端,WriteOnlyCollection.update() 方法应该在多对多集合中工作而无需额外步骤,就像下面的例子中,在BankAudit.account_transactions集合的多对多对象AccountTransaction对象上发出 UPDATE 一样:

>>> session.execute(
...     bank_audit.account_transactions.update().values(
...         description=AccountTransaction.description + " (audited)"
...     )
... )
UPDATE  account_transaction  SET  description=(account_transaction.description  ||  ?)
FROM  audit_transaction  WHERE  ?  =  audit_transaction.audit_id
AND  account_transaction.id  =  audit_transaction.transaction_id  RETURNING  id
[...]  (' (audited)',  1)
<...>

上述语句自动使用“UPDATE..FROM”语法,在 SQLite 和其他支持的数据库中,在 WHERE 子句中命名附加的audit_transaction表。

要更新或删除多对多集合,其中多表语法不可用,多对多条件可以移动到 SELECT 中,例如可以与 IN 结合使用来匹配行。在这里,WriteOnlyCollection 仍然对我们有帮助,因为我们使用WriteOnlyCollection.select()方法为我们生成此 SELECT,利用Select.with_only_columns()方法生成标量子查询:

>>> from sqlalchemy import update
>>> subq = bank_audit.account_transactions.select().with_only_columns(AccountTransaction.id)
>>> session.execute(
...     update(AccountTransaction)
...     .values(description=AccountTransaction.description + " (audited)")
...     .where(AccountTransaction.id.in_(subq))
... )
UPDATE  account_transaction  SET  description=(account_transaction.description  ||  ?)
WHERE  account_transaction.id  IN  (SELECT  account_transaction.id
FROM  audit_transaction
WHERE  ?  =  audit_transaction.audit_id  AND  account_transaction.id  =  audit_transaction.transaction_id)
RETURNING  id
[...]  (' (audited)',  1)
<...> 

只写集合 - API 文档

对象名称 描述
WriteOnlyCollection 可以将更改同步到属性事件系统的只写集合。
WriteOnlyMapped 表示“只写”关系的 ORM 映射属性类型。
class sqlalchemy.orm.WriteOnlyCollection

只写集合,可以将更改同步到属性事件系统。

使用relationship()"write_only"延迟加载策略在映射中使用WriteOnlyCollection。有关此配置的背景,请参阅只写关系。

新版本 2.0 中新增。

参见

只写关系

成员

add(), add_all(), delete(), insert(), remove(), select(), update()

类签名

sqlalchemy.orm.WriteOnlyCollection (sqlalchemy.orm.writeonly.AbstractCollectionWriter)

method add(item: _T) → None

向此WriteOnlyCollection添加项目。

下一个刷新时,给定的项目将以父实例集合的形式持久化到数据库中。

method add_all(iterator: Iterable[_T]) → None

向此WriteOnlyCollection添加项目的可迭代项。

给定的项目将以父实例集合的形式在下一个刷新时持久化到数据库中。

method delete() → Delete

生成一个Delete,该删除将以此实例本地的WriteOnlyCollection来引用行。

method insert() → Insert

对于一对多集合,生成一个Insert,该插入将以此实例本地的WriteOnlyCollection来插入新行。

该构造仅支持包括relationship.secondary参数的Relationship。对于引用多对多表的关系,请使用普通的批量插入技术来生成新对象,然后使用AbstractCollectionWriter.add_all()将它们与集合关联起来。

method remove(item: _T) → None

从此WriteOnlyCollection中移除一个项目。

下一个刷新时,给定的项目将从父实例的集合中移除。

method select() → Select[Tuple[_T]]

生成一个表示此实例本地WriteOnlyCollection内行的Select构造。

method update() → Update

生成一个Update,该更新将以此实例本地的WriteOnlyCollection来引用行。

class sqlalchemy.orm.WriteOnlyMapped

表示“仅写”关系的 ORM 映射属性类型。

WriteOnlyMapped类型注解可用于注释式声明表映射中,以指示特定的relationship()应使用lazy="write_only"加载策略。

例如:

class User(Base):
 __tablename__ = "user"
 id: Mapped[int] = mapped_column(primary_key=True)
 addresses: WriteOnlyMapped[Address] = relationship(
 cascade="all,delete-orphan"
 )

请参阅仅写关系部分了解背景信息。

从版本 2.0 开始新增。

另请参阅

仅写关系 - 完整背景

DynamicMapped - 包含传统的Query支持

类签名

sqlalchemy.orm.WriteOnlyMapped (sqlalchemy.orm.base._MappedAnnotationBase)

动态关系加载器

传统功能

“动态”惰性加载策略是现在“write_only”策略的传统形式,详细信息请参见仅写关系一节。

“动态”策略从相关集合生成传统的Query对象。然而,“动态”关系的一个主要缺点是,有几种情况下集合会完全迭代,其中一些情况并不明显,只有通过仔细的编程和逐个测试才能预防,因此对于真正大型的集合管理,应优先选择WriteOnlyCollection

动态加载器也与异步 I/O(asyncio)扩展不兼容。它可以在一定程度上使用,如 Asyncio 动态指南中所示的,但是应优先选择与 asyncio 完全兼容的WriteOnlyCollection,因为有一些限制。

动态关系策略允许配置一个relationship(),当在实例上访问时,将返回一个传统的Query对象,而不是集合。然后可以进一步修改Query以便基于过滤条件迭代数据库集合。返回的Query对象是AppenderQuery的一个实例,它结合了Query的加载和迭代行为以及基本的集合变异方法,如AppenderQuery.append()AppenderQuery.remove()

可以使用类型注释的声明形式配置“动态”加载策略,使用DynamicMapped注解类:

from sqlalchemy.orm import DynamicMapped

class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    posts: DynamicMapped[Post] = relationship()

上面,个体User对象上的User.posts集合将返回AppenderQuery对象,它是Query的子类,也支持基本的集合变异操作:

jack = session.get(User, id)

# filter Jack's blog posts
posts = jack.posts.filter(Post.headline == "this is a post")

# apply array slices
posts = jack.posts[5:20]

动态关系支持有限的写操作,通过 AppenderQuery.append()AppenderQuery.remove() 方法:

oldpost = jack.posts.filter(Post.headline == "old post").one()
jack.posts.remove(oldpost)

jack.posts.append(Post("new post"))

由于动态关系的读取端总是查询数据库,对底层集合的更改在数据刷新之前将不可见。但是,只要使用的 Session 上启用了“自动刷新”,这将在每次集合准备发出查询时自动发生。

动态关系加载器 - API

对象名称 描述
AppenderQuery 支持基本集合存储操作的动态查询。
DynamicMapped 代表“动态”关系的 ORM 映射属性类型。
class sqlalchemy.orm.AppenderQuery

支持基本集合存储操作的动态查询。

AppenderQuery 上的方法包括 Query 的所有方法,以及用于集合持久化的其他方法。

成员

add(), add_all(), append(), count(), extend(), remove()

类签名

sqlalchemy.orm.AppenderQuery (sqlalchemy.orm.dynamic.AppenderMixin, sqlalchemy.orm.Query)

method add(item: _T) → None

继承自 AppenderMixin.add() 方法的 AppenderMixin

向此 AppenderQuery 添加一个项目。

给定的项目将在下一次提交时以父实例集合的形式持久化到数据库中。

提供此方法是为了帮助实现与 WriteOnlyCollection 集合类的向前兼容。

版本 2.0 中的新功能。

method add_all(iterator: Iterable[_T]) → None

继承自 AppenderMixin.add_all() 方法的 AppenderMixin

向此 AppenderQuery 添加一个项目的可迭代对象。

给定的项目将在下一次提交时以父实例集合的形式持久化到数据库中。

提供此方法是为了帮助实现与 WriteOnlyCollection 集合类的向前兼容。

版本 2.0 中的新功能。

method append(item: _T) → None

继承自 AppenderMixin.append() 方法的 AppenderMixin

将一个项目追加到此 AppenderQuery 中。

给定的项目将在下一个 flush 时以父实例集合的形式持久化到数据库中。

method count() → int

继承自 AppenderMixin.count() 方法的 AppenderMixin

返回此 Query 生成的 SQL 所返回的行数。

这将生成此查询的 SQL 如下:

SELECT count(1) AS count_1 FROM (
    SELECT <rest of query follows...>
) AS anon_1

上述 SQL 返回单行,该行是 count 函数的聚合值;然后 Query.count() 方法返回该单个整数值。

警告

需要注意的是,count() 返回的值与此查询从 .all() 方法等返回的 ORM 对象数量不同。当 Query 对象被要求返回完整实体时,将基于主键去重,这意味着如果相同的主键值会在结果中出现多次,那么只会有一个该主键的对象存在。这不适用于针对单个列的查询。

另请参阅

我的查询结果与 query.count() 告诉我的对象数量不同 - 为什么?

要对特定列进行精细化控制以进行计数,跳过子查询的使用或以其他方式控制 FROM 子句,或者使用 Session.query()expression.func 表达式结合使用,例如:

from sqlalchemy import func

# count User records, without
# using a subquery.
session.query(func.count(User.id))

# return count of user "id" grouped
# by "name"
session.query(func.count(User.id)).\
        group_by(User.name)

from sqlalchemy import distinct

# count distinct "name" values
session.query(func.count(distinct(User.name)))

另请参阅

2.0 迁移 - ORM 用法

method extend(iterator: Iterable[_T]) → None

继承自 AppenderMixin.extend() 方法的 AppenderMixin

将一个项目可迭代的添加到此 AppenderQuery 中。

给定的项目将在下一个 flush 时以父实例集合的形式持久化到数据库中。

method remove(item: _T) → None

继承自 AppenderMixin.remove() 方法的 AppenderMixin

从此 AppenderQuery 中删除一个项目。

给定的项目将在下一个 flush 时从父实例的集合中移除。

class sqlalchemy.orm.DynamicMapped

代表“动态”关系的 ORM 映射属性类型。

DynamicMapped 类型注释可用于 Annotated Declarative Table 映射中,指示对特定 relationship() 使用 lazy="dynamic" 加载策略。

传统特性

“动态”延迟加载策略是现在在 Write Only Relationships 部分中描述的 “write_only” 策略的传统形式。

例如:

class User(Base):
    __tablename__ = "user"
    id: Mapped[int] = mapped_column(primary_key=True)
    addresses: DynamicMapped[Address] = relationship(
        cascade="all,delete-orphan"
    )

参见 Dynamic Relationship Loaders 部分的背景信息。

2.0 版本中新增。

另请参见

Dynamic Relationship Loaders - 完整背景信息

WriteOnlyMapped - 完全符合 2.0 版本风格

类签名

sqlalchemy.orm.DynamicMapped (sqlalchemy.orm.base._MappedAnnotationBase)

动态关系加载器 - API

对象名称 描述
AppenderQuery 支持基本集合存储操作的动态查询。
DynamicMapped 代表 “动态” 关系的 ORM 映射属性类型。
class sqlalchemy.orm.AppenderQuery

支持基本集合存储操作的动态查询。

AppenderQuery 上的方法包括 Query 的所有方法,以及用于集合持久性的额外方法。

成员

add(), add_all(), append(), count(), extend(), remove()

类签名

sqlalchemy.orm.AppenderQuery (sqlalchemy.orm.dynamic.AppenderMixin, sqlalchemy.orm.Query) 的签名

method add(item: _T) → None

继承自 AppenderMixin.add() 方法的 AppenderMixin

将项添加到此 AppenderQuery 中。

给定的项将以父实例集合的形式在下一个 flush 中持久化到数据库中。

提供此方法是为了帮助与 WriteOnlyCollection 集合类保持向前兼容。

2.0 版本中新增。

method add_all(iterator: Iterable[_T]) → None

继承自 AppenderMixin.add_all() 方法的 AppenderMixin

将项的可迭代对象添加到此 AppenderQuery 中。

下一个 flush 时,给定的项将以父实例集合的形式持久化到数据库中。

提供此方法是为了帮助与 WriteOnlyCollection 集合类保持向前兼容。

2.0 版本中新增。

method append(item: _T) → None

继承自 AppenderMixin.append() 方法的 AppenderMixin

将项目附加到此 AppenderQuery

给定的项目将在下一次刷新时以父实例集合的形式持久化到数据库中。

method count() → int

继承自 AppenderMixin.count() 方法的 AppenderMixin

返回此 Query 形成的 SQL 将返回的行数计数。

这将为此查询生成以下 SQL:

SELECT count(1) AS count_1 FROM (
    SELECT <rest of query follows...>
) AS anon_1

上述 SQL 返回单行,该行是计数函数的聚合值;然后 Query.count() 方法返回该单个整数值。

警告

重要的是要注意,count() 返回的值 不同于此查询从 .all() 方法等返回的 ORM 对象数。当 Query 对象被要求返回完整实体时,将 基于主键去重 条目,这意味着如果相同的主键值会出现在结果中超过一次,则该主键的对象只会出现一次。这不适用于针对单个列的查询。

另请参阅

我的查询的对象数与 query.count() 告诉我的不一样 - 为什么?

若要对特定列进行精细控制以计数,跳过子查询的使用或以其他方式控制 FROM 子句,或者使用 expression.func 表达式结合 Session.query() 使用,即:

from sqlalchemy import func

# count User records, without
# using a subquery.
session.query(func.count(User.id))

# return count of user "id" grouped
# by "name"
session.query(func.count(User.id)).\
        group_by(User.name)

from sqlalchemy import distinct

# count distinct "name" values
session.query(func.count(distinct(User.name)))

另请参阅

2.0 迁移 - ORM 用法

method extend(iterator: Iterable[_T]) → None

继承自 AppenderMixin.extend() 方法的 AppenderMixin

将项目的可迭代项添加到此 AppenderQuery 中。

给定的项目将在下一次刷新时以父实例集合的形式持久化到数据库中。

method remove(item: _T) → None

继承自 AppenderMixin.remove() 方法的 AppenderMixin

从此 AppenderQuery 中删除项目。

给定的项目将在下一次刷新时从父实例的集合中移除。

class sqlalchemy.orm.DynamicMapped

代表“动态”关系的 ORM 映射属性类型。

DynamicMapped 类型注释可在注释的声明性表映射中使用,以指示应该为特定 relationship() 使用 lazy="dynamic" 加载器策略。

传统特性

“dynamic”延迟加载策略是当前称为“write_only”策略的旧形式,在 只写关系部分中描述。

例如:

class User(Base):
    __tablename__ = "user"
    id: Mapped[int] = mapped_column(primary_key=True)
    addresses: DynamicMapped[Address] = relationship(
        cascade="all,delete-orphan"
    )

查看动态关系加载器部分以了解背景。

版本 2.0 中的新功能。

另请参阅

动态关系加载器 - 完整背景

WriteOnlyMapped - 完全符合 2.0 风格的版本

类签名

sqlalchemy.orm.DynamicMapped (sqlalchemy.orm.base._MappedAnnotationBase)

设置 RaiseLoad

“raise”加载的关系将在属性通常会发出延迟加载时引发 InvalidRequestError

class MyClass(Base):
    __tablename__ = "some_table"

    # ...

    children: Mapped[List[MyRelatedClass]] = relationship(lazy="raise")

在上面,对children集合的属性访问将在之前未填充时引发异常。这包括读访问,但对于集合,也会影响写访问,因为集合在未加载之前无法进行变异。这样做的原因是确保应用程序在某一上下文中不会发出任何意外的延迟加载。与其必须阅读 SQL 日志以确定所有必要的属性是否已经被急加载,不如使用“raise”策略,如果访问了未加载的属性,将立即引发未加载的属性。也可以在查询选项基础上使用 raiseload() 加载器选项。

另请参阅

使用 raiseload 防止不需要的延迟加载

使用被动删除

SQLAlchemy 中集合管理的一个重要方面是,当引用集合的对象被删除时,SQLAlchemy 需要考虑到位于该集合内的对象。这些对象将需要从父对象中取消关联,对于一对多集合,这意味着外键列将被设置为 NULL,或者根据 级联 设置,可能希望对这些行发出 DELETE。

工作单元过程仅仅考虑逐行对象,这意味着 DELETE 操作意味着集合中的所有行必须在刷新过程中完全加载到内存中。对于大型集合来说,这是不可行的,因此我们转而依赖数据库自身的能力来使用外键 ON DELETE 规则自动更新或删除行,指示工作单元放弃实际需要加载这些行以处理它们。可以通过在relationship()构造上配置relationship.passive_deletes来指示工作单元以这种方式工作;使用的外键约束也必须正确配置。

有关完整“被动删除”配置的更多详细信息,请参阅使用 ORM 关系的外键 ON DELETE 级联部分。

posted @ 2024-06-22 11:33  绝不原创的飞龙  阅读(19)  评论(0编辑  收藏  举报