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
[]