SqlAlchemy 2.0 中文文档(十一)(1)https://developer.aliyun.com/article/1562986
使用延迟评估形式来处理“次要”参数
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)
这里有几种可能性:
- 如果从
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 指令。假设数据库支持此功能,数据库本身可以被设置为在删除“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
,它映射到了名为 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 提供了 Association Proxy 扩展。这个扩展允许配置属性,这些属性将通过单一访问访问两个“跳”,一个“跳”到关联对象,第二个“跳”到目标属性。
另见
关联代理 - 允许父级和子级之间进行直接“多对多”样式访问,用于三类关联对象映射。
警告
避免直接混合使用关联对象模式和多对多模式,因为这会产生数据可能以不一致的方式读取和写入的情况,而无需特殊步骤;关联代理通常用于提供更简洁的访问。有关此组合引入的注意事项的更详细背景,请参阅下一节结合关联对象与多对多访问模式。
结合关联对象与多对多访问模式
如前一节所述,关联对象模式不会自动与同时针对相同表/列使用多对多模式的情况集成。由此可见,读操作可能返回冲突的数据,写操作也可能尝试刷新冲突的更改,导致完整性错误或意外的插入或删除。
为了说明,下面的示例配置了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.children
或 Child.parents
的读取不一定会匹配从 Parent.child_associations
或 Child.parent_associations
中读取的数据。如果很少使用关联对象关系,并且对访问多对多集合的代码进行了精心组织以避免过时的读取(在极端情况下,直接使用 Session.expire()
来使集合在当前事务内刷新),则该模式可能是可行的。
上述模式的一个流行替代方案是,直接的多对多 Parent.children
和 Child.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.Child
和myapp.model2.Child
之间的歧义,我们可以指定model1.Child
或model2.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()
函数进行解释,即使它通常是表的名称。不要传递不可信的输入给这个字符串。
SqlAlchemy 2.0 中文文档(十一)(3)https://developer.aliyun.com/article/1562989