基本关系模式

优质
小牛编辑
141浏览
2023-12-01

快速浏览基本关系模式。

以下各部分使用的导入如下:

from sqlalchemy import Table, Column, Integer, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

一对多

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

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)
    children = relationship("Child")

class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey('parent.id'))

要在一对多中建立双向关系,“反向”端是多对一,请指定一个附加的 relationship() 然后用 relationship.back_populates 参数::

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)
    children = relationship("Child", back_populates="parent")

class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey('parent.id'))
    parent = relationship("Parent", back_populates="children")

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

或者, relationship.backref 选项可用于单个 relationship() 而不是使用 relationship.back_populates ::

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)
    children = relationship("Child", backref="parent")

为一对多配置删除行为

通常情况下 Child 当对象拥有 Parent 已删除。若要配置此行为,则 delete 级联选项在 删除 被使用。另一个选择是 Child 对象与其父对象解除关联时,可以将其自身删除。此行为在中描述 删除孤儿 .

参见

删除

在具有ORM关系的DELETE cascade中使用外键

删除孤儿

多对一

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

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)
    child_id = Column(Integer, ForeignKey('child.id'))
    child = relationship("Child")

class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)

双向行为是通过增加一秒钟来实现的。 relationship() 并应用 relationship.back_populates 双向参数:

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)
    child_id = Column(Integer, ForeignKey('child.id'))
    child = relationship("Child", back_populates="parents")

class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)
    parents = relationship("Parent", back_populates="child")

或者, relationship.backref 参数可以应用于单个 relationship() ,如 Parent.child ::

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)
    child_id = Column(Integer, ForeignKey('child.id'))
    child = relationship("Child", backref="parents")

一对一

一对一本质上是具有两端标量属性的双向关系。在ORM中,“一对一”被认为是一种约定,ORM期望任何父行都只有一个相关行。

“一对一”约定是通过应用 False 发送到 relationship.uselist 属性的参数 relationship() 构造,或者在某些情况下构造 backref() 构造,并将其应用于关系的“一对多”或“集合”端。

In the example below we present a bidirectional relationship that includes both one-to-many (Parent.children) and a many-to-one (Child.parent) relationships:

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)

    # one-to-many collection
    children = relationship("Child", back_populates="parent")

class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey('parent.id'))

    # many-to-one scalar
    parent = relationship("Parent", back_populates="children")

上图, Parent.children 是引用集合的“一对多”端,并且 Child.parent 是指单个对象的“多对一”方面。若要将其转换为“一对一”,“一对多”或“集合”端将使用 uselist=False 标志,重命名 Parent.childrenParent.child 为清楚起见:

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)

    # previously one-to-many Parent.children is now
    # one-to-one Parent.child
    child = relationship("Child", back_populates="parent", uselist=False)

class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey('parent.id'))

    # many-to-one side remains, see tip below
    parent = relationship("Parent", back_populates="child")

上面,当我们加载一个 Parent 对象,则 Parent.child 属性将引用单个 Child 对象而不是集合。如果我们将 Parent.child 使用新的 Child 对象时,ORM的工作进程单元将替换以前的 Child 与新的行一起,设置上一个 child.parent_id 列默认情况下设置为NULL,除非有特定的 cascade 行为设置。

小技巧

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

然而, Child.parent 上述关系的一方仍然是“多对一”关系,并且没有变化,ORM本身中没有内部系统可以防止多对一关系 Child 要针对同一对象创建的 Parent 在持之以恒的过程中。取而代之的是,诸如 unique constraints 可以在实际数据库架构中使用,以强制执行此安排,其中对 Child.parent_id 列将确保只有一个 Child 行可以引用特定的 Parent 一次排一排。

在这种情况下, relationship.backref 参数用于定义“一对多”一方,则可以将其转换为“一对一”约定。 backref() 函数,该函数允许 relationship.backref 参数来接收自定义参数,在本例中为 uselist 参数::

from sqlalchemy.orm import backref

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)

class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey('parent.id'))
    parent = relationship("Parent", backref=backref("child", uselist=False))

多对多

多对多在两个类之间添加一个关联表。关联表由 relationship.secondary 参数 relationship() . 通常, Table 使用 MetaData 与声明性基类关联的对象,以便 ForeignKey 指令可以定位要链接的远程表:

association_table = Table('association', Base.metadata,
    Column('left_id', ForeignKey('left.id')),
    Column('right_id', ForeignKey('right.id'))
)

class Parent(Base):
    __tablename__ = 'left'
    id = Column(Integer, primary_key=True)
    children = relationship("Child",
                    secondary=association_table)

class Child(Base):
    __tablename__ = 'right'
    id = Column(Integer, primary_key=True)

小技巧

上面的“关联表”建立了引用关系两端的两个实体表的外键约束。每个对象的数据类型 association.left_idassociation.right_id 通常是从被引用的表中推断出来的,可以省略。它也是 推荐 ,尽管不是SQLAlChemy所要求的任何方式,但是引用这两个实体表的列是在 唯一约束 或者更常见的是 主键约束 ;这可确保无论应用程序端出现什么问题,表中都不会保留重复的行::

association_table = Table('association', Base.metadata,
    Column('left_id', ForeignKey('left.id'), primary_key=True),
    Column('right_id', ForeignKey('right.id'), primary_key=True)
)

对于双向关系,关系的两边都包含一个集合。指定使用 relationship.back_populates 为每个 relationship() 指定公共关联表::

association_table = Table('association', Base.metadata,
    Column('left_id', ForeignKey('left.id'), primary_key=True),
    Column('right_id', ForeignKey('right.id'), primary_key=True)
)

class Parent(Base):
    __tablename__ = 'left'
    id = Column(Integer, primary_key=True)
    children = relationship(
        "Child",
        secondary=association_table,
        back_populates="parents")

class Child(Base):
    __tablename__ = 'right'
    id = Column(Integer, primary_key=True)
    parents = relationship(
        "Parent",
        secondary=association_table,
        back_populates="children")

当使用 relationship.backref 参数而不是 relationship.back_populates ,backref将自动使用相同的 relationship.secondary 反向关系的参数:

association_table = Table('association', Base.metadata,
    Column('left_id', ForeignKey('left.id'), primary_key=True),
    Column('right_id', ForeignKey('right.id'), primary_key=True)
)

class Parent(Base):
    __tablename__ = 'left'
    id = Column(Integer, primary_key=True)
    children = relationship("Child",
                    secondary=association_table,
                    backref="parents")

class Child(Base):
    __tablename__ = 'right'
    id = Column(Integer, primary_key=True)

这个 relationship.secondary 的参数 relationship() 还接受一个返回最终参数的可调用文件,该参数仅在首次使用映射器时计算。使用这个,我们可以定义 association_table 稍后,只要在所有模块初始化完成后可调用文件可用,则:

class Parent(Base):
    __tablename__ = 'left'
    id = Column(Integer, primary_key=True)
    children = relationship("Child",
                    secondary=lambda: association_table,
                    backref="parents")

使用声明性扩展时,也接受传统的“表的字符串名称”,与存储在 Base.metadata.tables ::

class Parent(Base):
    __tablename__ = 'left'
    id = Column(Integer, primary_key=True)
    children = relationship("Child",
                    secondary="association",
                    backref="parents")

警告

当作为Python可求值字符串传递时 relationship.secondary 参数使用Python的 eval() 功能。 不要将不受信任的输入传递到此字符串 . 见 关系论据的评估 有关声明性评估的详细信息 relationship() 争论。

从多对多表中删除行

一种独特的行为 relationship.secondary 参数 relationship() 那是 Table 当对象从集合中添加或删除时,此处指定的对象将自动服从INSERT和DELETE语句。有 无需手动从此表中删除 . 从集合中删除记录的操作将具有在刷新时删除的行的效果:

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

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

session.delete(somechild)

这里有几种可能性:

  • 如果有一个 relationship() 从… ParentChild ,但是有 not 链接特定对象的反向关系 Child 给每个人 Parent ,SQLAlChemy将不会意识到在删除此特定的 Child 对象时,它需要维护将其链接到 Parent 。不会删除“辅助”表。

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

  • 这里的一个更高性能的选项是使用数据库使用的外键来删除CASCADE指令。假设数据库支持此功能,则可以使数据库本身自动删除“辅助”表中的行,因为“子”中的引用行将被删除。可以指示SQLAlchemy放弃在 Child.parents 在本例中,使用 relationship.passive_deletes 指令 relationship()在具有ORM关系的DELETE cascade中使用外键 有关此的详细信息。

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

参见

对多对多关系使用delete cascade

对多对多关系在DELETE上使用外键

关联对象

关联对象模式是多对多的变体:当关联表包含的列超出了左表和右表的外键之外的其他列时,就会使用它。而不是使用 relationship.secondary 参数,将新类直接映射到关联表。关系的左侧通过一对多引用关联对象,关联类通过多对一引用右侧。下面我们演示了一个映射到 Association 类,其中包含一个名为 extra_data ,它是一个字符串值,与 ParentChild ::

class Association(Base):
    __tablename__ = 'association'
    left_id = Column(ForeignKey('left.id'), primary_key=True)
    right_id = Column(ForeignKey('right.id'), primary_key=True)
    extra_data = Column(String(50))
    child = relationship("Child")

class Parent(Base):
    __tablename__ = 'left'
    id = Column(Integer, primary_key=True)
    children = relationship("Association")

class Child(Base):
    __tablename__ = 'right'
    id = Column(Integer, primary_key=True)

和往常一样,双向版本使用 relationship.back_populatesrelationship.backref ::

class Association(Base):
    __tablename__ = 'association'
    left_id = Column(ForeignKey('left.id'), primary_key=True)
    right_id = Column(ForeignKey('right.id'), primary_key=True)
    extra_data = Column(String(50))
    child = relationship("Child", back_populates="parents")
    parent = relationship("Parent", back_populates="children")

class Parent(Base):
    __tablename__ = 'left'
    id = Column(Integer, primary_key=True)
    children = relationship("Association", back_populates="parent")

class Child(Base):
    __tablename__ = 'right'
    id = Column(Integer, primary_key=True)
    parents = relationship("Association", 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.children 不会与对 Parent.child_associationsChild.parent_associations 在python中;虽然所有这些关系都将继续正常运行,但是在 Session 过期,通常在 Session.commit() ::

class Association(Base):
    __tablename__ = 'association'

    left_id = Column(ForeignKey('left.id'), primary_key=True)
    right_id = Column(ForeignKey('right.id'), primary_key=True)
    extra_data = Column(String(50))

    child = relationship("Child", backref="parent_associations")
    parent = relationship("Parent", backref="child_associations")

class Parent(Base):
    __tablename__ = 'left'
    id = Column(Integer, primary_key=True)

    children = relationship("Child", secondary="association")

class Child(Base):
    __tablename__ = 'right'
    id = Column(Integer, primary_key=True)

此外,正如对一个关系的更改不会自动反映在另一个关系中一样,向两个关系中写入相同的数据也会导致插入或删除语句冲突,例如下面我们在 ParentChild 对象两次:

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

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

如果你知道自己在做什么,可以使用上面这样的映射,尽管应用 viewonly=True 参数设置为“次要”关系,以避免记录冗余更改的问题。然而,为了得到一个简单的两个对象的万无一失的模式 Parent->Child 关系在仍然使用关联对象模式时,使用关联代理扩展,如中所述 关联代理 .

关系论据的后期评估

前面几节中的许多示例说明了 relationship() 构造使用字符串名称而不是类本身引用目标类:

class Parent(Base):
    # ...

    children = relationship("Child", back_populates="parent")

class Child(Base):
    # ...

    parent = relationship("Parent", back_populates="children")

这些字符串名在mapper resolution stage中被解析为类,mapper resolution stage是一个内部进程,通常在定义完所有映射之后发生,通常由首次使用映射本身触发。这个 registry 对象是存储这些名称并将其解析为它们所指向的映射类的容器。

除了 relationship() ,也可以将依赖于出现在尚未定义的类上的列的其他参数指定为Python函数,或者更常见的情况是指定为字符串。对于这些参数中的大多数(主参数除外),字符串输入为 evaluated as Python expressions using Python's built-in eval() function ,因为它们旨在接收完整的SQL表达式。

警告

就像 Python eval() 函数用于解释传递给 relationship() 映射器配置构造,这些参数应该 not 重新调整用途,使其能够接收不可信的用户输入; eval()不安全 针对不可信的用户输入。

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

class Parent(Base):
    # ...

    children = relationship(
        "Child",
        order_by="desc(Child.email_address)",
        primaryjoin="Parent.id == Child.parent_id"
    )

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

class Parent(Base):
    # ...

    children = 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 = relationship(
        "model1.Child",
        order_by="desc(mymodel1.Child.email_address)",
        primaryjoin="Parent.id == model1.Child.parent_id"
    )

这个 relationship() construct还接受Python函数或lambdas作为这些参数的输入。这样做的好处是提供了更多的编译时安全性和对ide和 PEP 484 情节。

Python函数方法可能如下所示:

from sqlalchemy import desc

def _resolve_child_model():
     from myapplication import Child
     return Child

class Parent(Base):
    # ...

    children = 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() 是:

在 1.3.16 版更改: 在SQLAlChemy 1.3.16之前,主要 relationship.argumentrelationship() 也是通过 eval() 从1.3.16开始,字符串名称直接从类解析器解析,不支持自定义Python表达式。

警告

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

还应注意的是,以类似于 添加新列 任何 MapperProperty 构造可以随时添加到声明性基映射中。如果我们想实现这个目标 relationship()Address 有课,我们以后也可以申请:

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

注解

只有在使用“声明性基”类时,将映射属性分配给声明性映射类才会正常工作,这也提供了元类驱动 __setattr__() 方法来截获这些操作。会的 not 如果 registry.mapped() 它也不适用于由映射的强制映射类 registry.map_imperatively() .

多对多关系的延迟评估

多对多关系包括对附加的(通常是非映射的)的引用 Table 对象的 MetaDataregistry . 后期求值系统还支持将此属性指定为字符串参数,该参数将从中解析 MetaData 收藏。下面我们指定一个关联表 keyword_author ,共享 MetaData 与我们的声明基及其 registry . 我们可以参考这个 Tablerelationship.secondary 参数::

keyword_author = Table(
    'keyword_author', Base.metadata,
    Column('author_id', Integer, ForeignKey('authors.id')),
    Column('keyword_id', Integer, ForeignKey('keywords.id'))
    )

class Author(Base):
    __tablename__ = 'authors'
    id = Column(Integer, primary_key=True)
    keywords = relationship("Keyword", secondary="keyword_author")

有关多对多关系的更多详细信息,请参阅部分 多对多 .