邻接列表关系


邻接列表模式是一种常见的关系模式,其中表 包含对自身的外键引用,换句话说,它是一个 自引用关系。这是在平面表中表示分层数据的最常用方法。其他方法包括嵌套集(有时称为“修改的 preorder”)以及具体化路径。尽管修改后的 preorder 在评估其在 SQL 查询中的流畅性时具有吸引力,但邻接列表模型可能是最适合大多数分层存储需求的模式,因为并发性、降低复杂性,并且修改后的 preorder 与可以将子树完全加载到应用程序空间的应用程序相比几乎没有优势。


另请参阅


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


在此示例中,我们将使用一个名为 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() 配置的工作方式与 “正常” 一对多关系相同,不同之处在于 “direction” ,即关系是一对多还是多对一,默认为一对多。为了建立多对一的关系,添加了一个名为 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_siderelationship() 中,从而建立 parent_id作为 “本地” 端,然后关系表现为多对一。


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

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 进行了更新


复合邻接列表


邻接列表关系的子类别是特定列同时存在于联接条件的 “local” 和 “remote” 端的罕见情况。一个例子是 Folder 类;使用复合主键,account_id column 引用自身,以指示位于 与父帐户相同的帐户;而 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 列 位于两侧,并将 “remote” 列与 folder_id列,它将其识别为唯一存在于 “remote” 端。


自引用查询策略


查询自引用结构的工作方式与任何其他查询类似:

# 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']


配置 self-referential Eager loading


在正常查询作期间,使用从父表到子表的 join 或 outerjoins 预先加载关系,以便可以从单个 SQL 语句或所有直接子集合的第二个语句填充父集合及其直接子集合或引用。SQLAlchemy 的 joined 和 subquery eager loading 在连接到相关项时在所有情况下都使用别名表,因此与自引用连接兼容。但是,要通过自引用关系使用预先加载,需要告诉 SQLAlchemy 它应该连接和/或查询多少层;否则,EAGER 加载将根本不会发生。此深度设置可通过 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 []