邻接表关系

邻接表模式是一种常见的关联模式,其中表包含对自身的外部键引用,换句话说,它是一种自引用关系。这是在平面表中表示层次数据的最常见方法。其他方法包括嵌套集,有时称为“修改后的预序”,以及物化路径。尽管在评估 SQL 查询的流畅性时,修改后的预序具有吸引力,但邻接表模型可能最适合绝大多数层次存储需求,原因是并发性、降低复杂性,以及修改后的预序在应用程序能够完全将子树加载到应用程序空间的情况下,相对于应用程序而言优势不大。

另请参阅

本节详细介绍了自引用关系的单表版本。对于使用第二个表作为关联表的自引用关系,请参阅 自引用多对多关系 部分。

在本例中,我们将使用一个名为 Node 的映射类,它表示树结构

class Node(Base):
    __tablename__ = "node"
    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(Integer, ForeignKey("node.id"))
    data = mapped_column(String(50))
    children = relationship("Node")

使用这种结构,如下所示的图形

root --+---> child1
       +---> child2 --+--> subchild1
       |              +--> subchild2
       +---> child3

将使用如下数据表示

id       parent_id     data
---      -------       ----
1        NULL          root
2        1             child1
3        1             child2
4        3             subchild1
5        3             subchild2
6        1             child3

relationship() 配置在这里的工作方式与“普通”一对多关系相同,区别在于“方向”,即关系是一对多还是多对一,默认情况下被假定为一对多。为了将关系设置为多对一,需要添加一个额外的指令,称为 relationship.remote_side,它是一个 ColumnColumn 对象集合,表示应被视为“远程”的那些对象

class Node(Base):
    __tablename__ = "node"
    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(Integer, ForeignKey("node.id"))
    data = mapped_column(String(50))
    parent = relationship("Node", remote_side=[id])

在上面的例子中,id 列被用作 relationship.remote_sideparent relationship(),从而将 parent_id 建立为“本地”端,然后关系的行为就如同多对一关系。

与往常一样,两个方向可以使用两个由 relationship.back_populates 链接的 relationship() 结构组合成双向关系

class Node(Base):
    __tablename__ = "node"
    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(Integer, ForeignKey("node.id"))
    data = mapped_column(String(50))
    children = relationship("Node", back_populates="parent")
    parent = relationship("Node", back_populates="children", remote_side=[id])

另请参阅

邻接表 - 工作示例,已更新至 SQLAlchemy 2.0

复合邻接表

邻接表关系的一个子类别是罕见的情况,其中特定列同时出现在连接条件的“本地”和“远程”端。例如,下面的 Folder 类;使用复合主键,account_id 列引用自身,以指示与父级位于同一帐户内的子文件夹;而 folder_id 则引用该帐户内的特定文件夹

class Folder(Base):
    __tablename__ = "folder"
    __table_args__ = (
        ForeignKeyConstraint(
            ["account_id", "parent_id"], ["folder.account_id", "folder.folder_id"]
        ),
    )

    account_id = mapped_column(Integer, primary_key=True)
    folder_id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(Integer)
    name = mapped_column(String)

    parent_folder = relationship(
        "Folder", back_populates="child_folders", remote_side=[account_id, folder_id]
    )

    child_folders = relationship("Folder", back_populates="parent_folder")

在上面,我们将 account_id 传递到 relationship.remote_side 列表中。 relationship() 识别出这里的 account_id 列位于两侧,并将“远程”列与 folder_id 列对齐,它识别出该列在“远程”端是唯一的。

自引用查询策略

对自引用结构的查询与任何其他查询类似

# get all nodes named 'child2'
session.scalars(select(Node).where(Node.data == "child2"))

但是,在尝试从树的一级连接到下一级时,需要格外小心。在 SQL 中,从表到自身的连接要求至少有一侧的表达式被“别名”,以便能够明确地引用它。

回想一下 ORM 教程中 选择 ORM 别名 中的 aliased() 结构通常用于提供 ORM 实体的“别名”。使用此技术从 Node 到自身的连接如下所示

from sqlalchemy.orm import aliased

nodealias = aliased(Node)
session.scalars(
    select(Node)
    .where(Node.data == "subchild1")
    .join(Node.parent.of_type(nodealias))
    .where(nodealias.data == "child2")
).all()
SELECT node.id AS node_id, node.parent_id AS node_parent_id, node.data AS node_data FROM node JOIN node AS node_1 ON node.parent_id = node_1.id WHERE node.data = ? AND node_1.data = ? ['subchild1', 'child2']

配置自引用急切加载

关系的急切加载在正常查询操作期间使用从父表到子表的连接或外连接,以便从单个 SQL 语句中填充父级及其直接子级集合或引用,或者对所有直接子级集合使用第二个语句。SQLAlchemy 的连接和子查询急切加载在连接到相关项时始终使用别名表,因此与自引用连接兼容。但是,要将急切加载与自引用关系一起使用,SQLAlchemy 需要知道应该连接和/或查询多少层;否则,急切加载根本不会发生。此深度设置通过 relationships.join_depth 配置

class Node(Base):
    __tablename__ = "node"
    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(Integer, ForeignKey("node.id"))
    data = mapped_column(String(50))
    children = relationship("Node", lazy="joined", join_depth=2)


session.scalars(select(Node)).all()
SELECT node_1.id AS node_1_id, node_1.parent_id AS node_1_parent_id, node_1.data AS node_1_data, node_2.id AS node_2_id, node_2.parent_id AS node_2_parent_id, node_2.data AS node_2_data, node.id AS node_id, node.parent_id AS node_parent_id, node.data AS node_data FROM node LEFT OUTER JOIN node AS node_2 ON node.id = node_2.parent_id LEFT OUTER JOIN node AS node_1 ON node_2.id = node_1.parent_id []