基本关系模式

快速浏览基本关系模式,在本节中,使用 声明式 样式映射基于使用 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 配置样式。在本节和其他使用带注释的 声明式 映射和 Mapped 的示例中,相应的非注释形式应使用所需的类或字符串类名作为传递给 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 而不是 listParent.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 对象与其父对象分离时,它本身可以被删除。此行为在 delete-orphan 中描述。

多对一

多对一将一个外键放在父表中,引用子表。在父表上声明 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 设置为 True,而 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")

一对一关系

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

在使用带 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 对象,而不是一个集合。如果我们将 Parent.child 的值替换为一个新的 Child 对象,ORM 的工作单元过程将用新的行替换之前的 Child 行,默认情况下将之前的 child.parent_id 列设置为 NULL,除非设置了特定的 级联 行为。

提示

如前所述,ORM 将“一对一”模式视为一个约定,其中假设当它在 Parent 对象上加载 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: The relationship() 构造函数可以从给定的 Mapped 注释中推导出 relationship.uselist 参数的有效值。

为非注释配置设置 uselist=False

在使用 relationship() 而不使用 Mapped 注释的情况下,可以使用将 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")

多对多关系

多对多关系在两个类之间添加了一个关联表。关联表几乎总是作为 Core Table 对象或其他 Core 可选对象(如 Join 对象)给出,并由 relationship.secondary 参数指示,该参数传递给 relationship()。通常,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.secondary 参数的 relationship() 也接受两种不同的“延迟计算”形式,包括字符串表名和 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.secondary 参数对 relationship() 的独特行为是,此处指定的 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.passive_deletes 指令在 relationship() 上放弃主动加载 Child.parents 集合;有关此的更多详细信息,请参见 使用外部键 ON DELETE cascade 与 ORM 关系

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

关联对象

关联对象模式是多对多的变体:当关联表包含除父表和子表(或左表和右表)的外部键以外的其他列时,它会被使用,这些列最理想地映射到它们自己的 ORM 映射类。这个映射类被映射到 Table,否则会被记为 relationship.secondary,当使用多对多模式时。

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

下面的示例说明了一个新的类 Association,它映射到名为 Table 的表 association;此表现在包含一个名为 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 做出的更改不会与对 Parent.child_associationsChild.parent_associations 在 Python 中做出的更改协调;虽然所有这些关系都将继续正常运行,但对一个关系的更改在另一个关系中不会显示,直到 Session 过期,这通常在 Session.commit() 后自动发生。

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

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,从而防止发生冲突的写入。但是,如果在与读取 viewonly 集合相同的交易或 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",
    )

The 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() 的上述参数将 **使用 eval() 作为 Python 代码表达式进行评估。不要将不受信任的输入传递给这些参数。**

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

还应注意,与在 向现有的声明式映射类追加额外的列 中描述的方式类似,任何 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 对象或其他核心可选择对象的引用。使用 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 引用的相同 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() 函数进行解释,即使它通常是表的名称。 **不要将不受信任的输入传递给此字符串。**