Adjacency List Relationships

adjacency list パターンは、共通のリレーショナル・パターンであり、テーブルにはそれ自体への外部キー参照が含まれます。つまり、 self referential relationship です。これは、フラット・テーブルで階層データを表す最も一般的な方法です。その他の方法には、 ネストされたセット 、”変更された事前順序” と呼ばれることもあります、や materialized path などがあります。変更された事前順序は、SQLクエリー内での流暢さを評価した場合に魅力的であるにもかかわらず、隣接リスト・モデルは、同時実行性、複雑さの軽減、および変更された事前順序がアプリケーション空間にサブツリーを完全にロードできるアプリケーションに対してほとんど利点がないという理由から、階層ストレージのニーズの大部分に対しておそらく最も適切なパターンです。

See also

この節では、単一テーブルバージョンの自己参照関係について詳しく説明します。2番目のテーブルを関連付けテーブルとして使用する自己参照関係については、 Self-Referential Many-to-Many Relationship 節を参照してください。

この例では、ツリー構造を表す 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() の設定は、”通常の”1対多の関係と同じように動作します。ただし、”方向”、つまり関係が1対多か多対1かは、デフォルトで1対多と見なされます。多対1の関係を確立するために、 relationship.remote_side として知られる追加のディレクティブが追加されます。これは Column または Column オブジェクトのコレクションで、”リモート”であると見なされるオブジェクトを示します:

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 として適用され、 local 側として parent_id を確立し、多対1の関係として動作します。

いつものように、 relationship.back_populates によってリンクされた2つの 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])

See also

Adjacency List - working example, updated for SQLAlchemy 2.0

Composite Adjacency Lists

隣接リスト関係のサブカテゴリは、結合条件の ローカル 側と リモート 側の両方に特定のカラムが存在するまれなケースです。たとえば、以下の 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")

上記では、 relationship.remote_side リストに account_id を渡しています。 relationship() は、ここの account_id 列が両側にあることを認識し、”remote”列を、”remote”側に一意に存在すると認識される folder_id 列と並べます。

Self-Referential Query Strategies

自己参照構造体の問い合わせは、他の問い合わせと同じように動作します。:

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

ただし、ツリーのあるレベルから次のレベルに外部キーに沿って結合しようとする場合は、特別な注意が必要です。SQLでは、テーブルからそれ自体への結合には、明確に参照できるように、式の少なくとも片側が「エイリアス」されている必要があります。

ORMチュートリアルの Selecting ORM Aliases で、 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']

Configuring Self-Referential Eager Loading

通常の問い合わせ操作では、親テーブルから子テーブルへの結合または外部結合を使用して、関係のEager Loadingが行われます。これにより、親とその直下の子コレクションまたは参照は、単一のSQL文から、または直下のすべての子コレクションに対して2番目の文から入力することができます。SQLAlchemyの結合および副問い合わせのEager Loadingは、関連する項目に結合するときにエイリアステーブルを使用するため、自己参照結合と互換性があります。ただし、自己参照関係でEager Loadingを使用するには、SQLAlchemyが結合または問い合わせを行うレベルの深さを指定する必要があります。そうしないと、Eager Loadingはまったく行われません。この深さの設定は 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 []