SqlAlchemy 2.0 中文文档(十一)(3)https://developer.aliyun.com/article/1562989
多对多
多对多在两个类之间添加了一个关联表。这个关联表几乎总是以一个核心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_id
和association.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)
这里有几种可能性:
- 如果从
Parent
到Child
有一个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)
这里有几种可能性:
- 如果从
Parent
到Child
有一个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
,它映射到名为association
的Table
;此表现在包括一个额外的列称为extra_data
,它是一个字符串值,与Parent
和Child
之间的每个关联一起存储。通过将表映射到显式类,从Parent
到Child
的基本访问明确使用了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 提供了关联代理扩展。该扩展允许配置属性,这些属性将通过单个访问实现两次“跳跃”,一次是到关联对象,另一次是到目标属性。
另请参阅
关联代理 - 允许在三类关联对象映射中在父对象和子对象之间直接进行“多对多”样式的访问。
警告
避免直接混合使用关联对象模式和多对多模式,因为这会导致数据可能以不一致的方式读取和写入,除非采取特殊步骤;关联代理通常用于提供更简洁的访问。有关此组合引入的注意事项的更详细背景,请参阅下一节将关联对象与多对多访问模式组合使用。
将关联对象与多对多访问模式结合使用
如前一节所述,关联对象模式不会自动与相同表/列的多对多模式集成。由此可知,读操作可能返回冲突数据,写操作也可能尝试刷新冲突更改,导致完整性错误或意外插入或删除。
为了说明,下面的示例配置了Parent
和Child
之间的双向多对多关系,通过Parent.children
和Child.parents
。同时,还配置了一个关联对象关系,即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_associations
或Child.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.children
或Child.parents
,从而防止冲突的写入。然而,如果在相同事务或Session
中对这些集合进行更改,那么对Parent.children
或Child.parents
的读取将不一定匹配从Parent.child_associations
或Child.parent_associations
读取的数据。如果关联对象关系的使用不频繁,并且针对访问多对多集合的代码进行了精心组织以避免过时的读取(在极端情况下,直接使用Session.expire()
来使集合在当前事务中刷新),那么这种模式可能是可行的。
上述模式的一种流行替代方案是,直接的多对多Parent.children
和Child.parents
关系被一个扩展所取代,该扩展将通过Association
类透明地代理,同时从 ORM 的角度保持一切一致。这个扩展被称为关联代理。
另请参阅
关联代理 - 允许在三类关联对象映射之间直接进行“多对多”样式的父子访问。### 将关联对象与多对多访问模式结合使用
如前所述,在上一节中,关联对象模式不会自动与同时针对相同表/列使用的多对多模式集成。由此可见,读取操作可能会返回冲突的数据,并且写入操作也可能尝试刷新冲突的更改,导致完整性错误或意外的插入或删除。
为了说明,下面的示例配置了Parent
和Child
之间的双向多对多关系,通过Parent.children
和Child.parents
。同时,还配置了一个关联对象关系,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 模型进行更改时,在 Python 中对Parent.children
进行的更改不会与对Parent.child_associations
或Child.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.children
或Child.parents
的任何更改写入数据库,从而防止冲突写入。但是,如果在相同的事务或Session
中对这些集合进行更改的地方读取Parent.children
或Child.parents
将不一定与从Parent.child_associations
或Child.parent_associations
中读取的数据匹配。如果对关联对象关系的使用不频繁,并且针对访问多对多集合的代码进行了精心组织以避免过时读取(在极端情况下,直接使用Session.expire()
来导致集合在当前事务中被刷新),那么这种模式可能是可行的。
一个流行的替代模式是,直接的多对多Parent.children
和Child.parents
关系被一个扩展所取代,该扩展将通过Association
类透明地代理,同时从 ORM 的角度保持一切一致。这个扩展被称为关联代理。
另请参阅
关联代理 - 允许在三类关联对象映射中直接实现“多对多”样式的父子访问。
SqlAlchemy 2.0 中文文档(十一)(5)https://developer.aliyun.com/article/1562991