邻接列表关系

邻接列表模式是一种常见的关系模式,其中一个表包含对其自身的外部键引用,换句话说,是一种自引用关系。 这是在平面表中表示分层数据的最常见方法。 其他方法包括嵌套集,有时称为“修改后的前序遍历”,以及物化路径。 尽管修改后的前序遍历在 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 列被应用为 parent relationship()relationship.remote_side,从而将 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 []