基本关系模式


基本关系模式的快速演练,在本节中,这些模式使用基于 Mapped 注释类型的 Declarative 样式映射进行说明。


以下每个部分的设置如下:

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


声明式 vs. 命令式表单¶


随着 SQLAlchemy 的发展,出现了不同的 ORM 配置样式。 有关本节中示例以及其他使用注释 声明式映射 Maped,相应的未注释表单应使用 desired class 或 string class name 作为传递给 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")


相比之下,使用不带注解的 Declarative 映射是更“经典”的映射形式,其中 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")},
)


此外,未注释的映射的默认集合样式为 列表。要使用不带注释的集合或其他集合,请使用 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 属性。


使用 Set、Lists 或其他 Collection 类型进行 One To Many


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

class Parent(Base):
    __tablename__ = "parent_table"

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


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


另请参阅


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


为 One to Many 配置 Delete 行为


通常情况下,当删除其拥有的 Parent 时,应删除所有 Child 对象。要配置此行为,请使用 delete 中描述的 delete cascade 选项。另一个选项是,在以下情况下可以删除 Child 对象本身 它与父级取消关联。 此行为在 delete-orphan


多对一


多对一在父表中放置一个引用子级的外键。 relationship() 在父级上声明,其中将创建一个新的 scalar-holding 属性:

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)


上面的示例显示了一个假定不可为 null 行为的多对一关系;下一部分 可为 Nullable Many-to-One 说明了可为 null 的版本。


双向行为是通过添加第二个 relationship() 来实现的 并使用每个 relationship() 的属性名称在两个方向上应用 relationship.back_populates 参数 作为另一个 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 关系未类型为 allowing None;这是因为 Parent.child_id 列本身不可为空,因为它是使用 Mapped[int] 键入的。 如果我们想要 Parent.child 设置为可为 null 的多对一,我们可以将两者 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,而 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 Comments,这将意味着 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 行,默认情况下将前一个 child.parent_id 列设置为 NULL,除非设置了特定的级联行为。


提示


如前所述,ORM 将 “一对一” 模式视为 约定,其中它假设在加载 Parent.child 属性,则它只会返回一行。如果返回多行,ORM 将发出警告。


但是,上述关系的 Child.parent 端仍为 “多对一” 关系。它本身不会检测到多个 Child 的分配,除非 relationship.single_parent parameter 设置,这可能很有用:

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() 结构可以派生 relationship.uselist 的有效值 参数。


为非注释配置设置 uselist=False


当使用 relationship() 而没有 Mapped 的好处时 annotations,可以使用 relationship.uselist 参数设置为 False,在通常是 “many” 端,在下面的非注释声明式配置中说明:

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 table” 建立了外键约束,这些约束引用关系两侧的两个实体表。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),
)


设置双向多对多


对于双向关系,关系的两端都包含一个集合。指定 using 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() 也接受两种不同的 “late evaluated” 形式, 包括 String Table Name 以及 Lambda Callable。 请参阅该部分 对多对多的 “secondary” 参数使用后期计算形式作为背景和示例。


使用 Set、List 或其他集合类型进行 Many To Many


多对多关系的集合配置与一对关系的集合配置相同,如 对 One To Many 使用 Sets、Lists 或其他集合类型。对于使用 Mapped 的带注释的 Map,集合可以由 Mapped 泛型类中使用的集合类型来指示,例如 set

class Parent(Base):
    __tablename__ = "left_table"

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


当使用非注释形式(包括命令式映射)时,按原样 一对多的情况是 Python 类作为集合,可以使用 relationship.collection_class 参数。


另请参阅


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


从 Many to Many 表中删除行


relationship.secondary 独有的行为 relationship() 的参数是,当对象在集合中添加或删除时,此处指定的 Table 会自动受到 INSERT 和 DELETE 语句的约束。无需删除 从此表中手动。从集合中删除记录的作将具有在 flush 时删除该行的效果:

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


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

session.delete(somechild)


这里有几种可能性:


  • 如果存在从 ParentChildrelationship(),但存在 而不是将特定 Child 与每个 Parent 联系起来的反向关系, SQLAlchemy 不会意识到,在删除此特定 Child 对象,它需要维护将其链接到 Parent 的 “secondary” 表。不会删除 “secondary” 表。


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


  • 此处性能更高的选项是将 ON DELETE CASCADE 指令与数据库使用的外键一起使用。假设数据库支持此功能,则可以使数据库本身在删除“child”中的引用行时自动删除“secondary”表中的行。可以指示 SQLAlchemy 放弃在 Child.parents 中主动加载 集合(在本例中使用 relationship.passive_deletes directive on relationship() 的命令;有关此内容的更多详细信息,请参阅对 ORM 关系使用外键 ON DELETE 级联


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


关联对象


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


在关联对象模式中,relationship.secondary parameter 未使用;相反,类直接映射到关联 桌子。然后,两个单独的 relationship() 构造首先通过一对多将父端链接到映射的关联类,然后通过多对一将映射的关联类链接到子端,以形成从父端到关联再到子端的单向关联对象关系。对于双向关系,四个 relationship() 构造用于将映射的关联类链接到 parent 和 child 在两个方向上。


下面的示例说明了一个新类 Association,它映射到名为 associationTable;此表现在包含一个名为 extra_data 的附加列,它是一个字符串值,与 Parent子项。通过将表映射到显式类,从 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")


使用直接形式的关联模式需要先将子对象与关联实例关联,然后再将其附加到父对象;同样,从 Parent 到 Child 的访问也通过 Association 对象进行:

# 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 扩展。此扩展允许配置属性,这些属性将访问两个“跃点”,一次访问关联对象,另一个访问目标属性。


另请参阅


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


警告


避免将关联对象模式与多对多混合 pattern 的 intent 的 Array 的 Tim API API 的 Quin Bean 的 Alpha S S Package,因为这会产生可以 并且以不一致的方式编写,没有特殊的步骤; 关联代理通常为 用于提供更简洁的访问。 有关更详细的背景 关于这种组合引入的注意事项,请参阅下一节 将 Association Object 与 Many-to-Many Access Patterns 相结合


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


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


为了说明这一点,下面的示例通过 Parent.childrenChild.parentsParentChild 之间配置了双向多对多关系。同时,还配置了关联对象关系,在 Parent.child_associations -> Association.child 以及 Child.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.commit() 的 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.extra_data 列的任何值,该列将收到 NULL 作为其值。


如果您知道自己在做什么,则可以使用上述映射;那里 可能是使用多对多关系的充分理由,其中使用 “关联对象”模式并不常见,也就是说,它更容易 沿单个多对多关系加载关系,这也可以 稍微优化了 SQL 语句中 “secondary” 表的使用方式, 与显式关联类的两个单独关系相比 使用。 至少将 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 读取的数据匹配作为读取 ViewOnly 集合的位置。如果关联对象关系的使用不频繁,并且针对访问多对多集合的代码进行仔细组织,以避免过时的读取(在极端情况下,直接使用 Session.expire() 导致集合在当前事务中刷新),该模式可能是可行的。


上述模式的流行替代方案是将直接多对多 Parent.childrenChild.parents 关系将替换为一个扩展,该扩展将透明地通过 Association 进行代理 类,同时从 ORM 的 视图。 此扩展称为 Association Proxy


另请参阅


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


关系参数的后期评估


前面各节中的大多数示例都说明了映射,其中各种 relationship() 结构使用字符串名称而不是类本身来引用其目标类,例如,当使用 Map 时,会生成一个在运行时仅以字符串形式存在的前向引用:

class Parent(Base):
    # ...

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


class Child(Base):
    # ...

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


同样,当使用非注释的形式(例如非注释的 Declarative 或 Imperative 映射)时,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")},
)


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


除了 relationship() 的主类参数之外, 其他参数,这些参数取决于 asyet 上存在的列 undefined 类也可以指定为 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 的字符串 可以通过传递类 location 字符串直接添加到 relationship.argument 中。下面说明了 Child 的仅键入导入,并结合了目标类的运行时说明符,该说明符将在注册表中搜索正确的名称:

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。子项模型 2.儿童

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,
    )


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


警告


如前所述,relationship() 的上述参数 使用 eval() 作为 Python 代码表达式进行计算。 不通过 不受信任的输入。


声明后向 Map 类添加关系


还应注意,以与 将其他列追加到现有的 Declarative 映射类、任何 MapperProperty construct 可以随时添加到声明性基映射中 (请注意,在此上下文中不支持带注释的形式)。 如果 我们想在 Address 之后实现这个 relationship() class 可用,我们也可以在之后应用它:

# 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() 的 API 中。这个 “base” 类包括一个 Python 元类,它实现了一个特殊的 __setattr__() 方法来拦截这些作。


如果使用 registry.mapped() 等装饰器映射类,则将类映射属性的运行时分配给映射类将不起作用 或命令式函数(如 registry.map_imperatively())。


对多对多的 “secondary” 参数使用延迟计算的形式


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


对于多对多中给出的示例,如果我们假设 association_tableTable 对象将在模块中定义,而不是在映射类本身之后的某个时间点定义,我们可以编写 relationship() 使用 Lambda 作为:

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 参数也可以作为 string,其中 resolution 的工作原理是将字符串作为 Python 进行评估 表达式,其中简单的标识符名称链接到同名 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() 函数,即使它通常是表的名称。 不要将不受信任的输入传递给此字符串