SQLAlchemy基本的なRelationshipパターン
以下の文章はこちらのページを翻訳したものです。
基本的なリレーションシップパターン
このセクションでは、Mapped
アノテーションの使用に基づいて宣言型(Declarative)マッピングを使用し、基本的なリレーションシップパターンを紹介します。
以下のセクションの設定は次の通りです:
from __future__ import annotations
from typing import List
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship
class Base(DeclarativeBase):
pass
宣言型(Declarative) vs. 命令型(Imperative)
SQLAlchemyが進化するにつれて、さまざまなORMの設定スタイルが出現してきました。このセクションの例や他のアノテーション付き宣言型マッピングでMapped
を使う場合、アノテーションがない形式では、relationship()
の最初の引数としてクラス名またはその文字列を指定する必要があります。以下の例は、この文書で使用されている形式、すなわち**PEP 484** アノテーションを使用した完全な宣言型の例を示しています。ここでの構造は、アノテーションからターゲットクラスとコレクションタイプを導出しています。これはSQLAlchemyの宣言型マッピングの最新の形式です:
class Parent(Base):
__tablename__ = "parent_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List["Child"]] = relationship(back_populates="parent")
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
parent: Mapped["Parent"] = relationship(back_populates="children")
対照的に、アノテーションを使用しない宣言型マッピングは、より「クラシックな」マッピング形式です。この場合、relationship()
は、以下の例のように、直接渡されるすべてのパラメータを必要とします。
class Parent(Base):
__tablename__ = "parent_table"
id = mapped_column(Integer, primary_key=True)
children = relationship("Child", back_populates="parent")
class Child(Base):
__tablename__ = "child_table"
id = mapped_column(Integer, primary_key=True)
parent_id = mapped_column(ForeignKey("parent_table.id"))
parent = relationship("Parent", back_populates="children")
最後に、命令型(Imperative)マッピングについて触れます。これは、宣言型が導入される前のSQLAlchemyのオリジナルのマッピング手法です。一部のユーザーには今でも好まれていますが、上記の設定は以下のように見えます:
registry.map_imperatively(
Parent,
parent_table,
properties={"children": relationship("Child", back_populates="parent")},
)
registry.map_imperatively(
Child,
child_table,
properties={"parent": relationship("Parent", back_populates="children")},
)
さらに、アノテーションを使用しないマッピングのデフォルトのコレクションスタイルはlist
です。アノテーションなしでset
や他のコレクションを使用するには、relationship.collection_class
パラメーターでそれを指定します。
class Parent(Base):
__tablename__ = "parent_table"
id = mapped_column(Integer, primary_key=True)
children = relationship("Child", collection_class=set, ...)
relationship()
のコレクション設定の詳細は、Customizing Collection Accessで確認できます。
アノテーションの有無や、命令型のスタイルとの違いについては、必要に応じて後ほど触れます。
1対多
1対多の関係は、子テーブルに親を参照する外部キーを配置します。その後、親の方でrelationship()
が指定されます。これは子が表現するアイテムのコレクションを参照するものとしてです。
class Parent(Base):
__tablename__ = "parent_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List["Child"]] = relationship()
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
1対多の関係で双方向リレーションを確立するとき、つまり、逆側となるChildから見たときに多対1であるような関係を作りたいとき、追加のrelationship()
を指定し、relationship.back_populates
パラメーターを使って二つを繋げます。それぞれの属性名を他方の値として使用します。
class Parent(Base):
__tablename__ = "parent_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List["Child"]] = relationship(back_populates="parent")
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
parent: Mapped["Parent"] = relationship(back_populates="children")
Child
には多対1のセマンティクスを持つparent
属性が追加されます。
1対多リレーションにおけるSetやList、その他のコレクションタイプの利用
アノテーション付きの宣言型マッピングでは、relationship()
でのコレクションの種類は、Mapped
に渡されるタイプから決まります。例えば、前のセクションのParent.children
をlist
ではなくset
として扱いたい場合、Mapped[Set["Child"]]
のように指定します。
class Parent(Base):
__tablename__ = "parent_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[Set["Child"]] = relationship(back_populates="parent")
アノテーションを使用しない形式や命令型マッピングを使用する場合、コレクションとして使用するPythonクラスは、relationship.collection_class
パラメーターを使用して渡すことができます。
関連情報:
Customizing Collection Access - コレクションの設定に関する詳細や、relationship()
をdictionaryにマッピングするためのテクニックなどが含まれています。
1対多リレーションでの削除動作の設定
Parent
が削除される際に、それに紐づくすべてのChild
オブジェクトも削除されるケースはよくあります。この動作を設定するには、delete で説明されているdelete
カスケードオプションを使用します。さらに、オブジェクトがその親から分離されたときに自身を削除するというオプションもあります。この動作については、delete-orphan で解説されています。
関連情報:
Using foreign key ON DELETE cascade with ORM relationships
多対1
多対1リレーションでは、親テーブルに外部キーが置かれ、子を参照します。relationship()は親上で宣言され、新しいスカラーを保持する属性が作成されます。
class Parent(Base):
__tablename__ = "parent_table"
id: Mapped[int] = mapped_column(primary_key=True)
child_id: Mapped[int] = mapped_column(ForeignKey("child_table.id"))
child: Mapped["Child"] = relationship()
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
この例では、多対1リレーションが示されており、外部キーがNULLを持たないという前提で記述されています。NULLを許容する多対1リレーションについては、次のセクションで詳しく解説いたします。
双方向の動作を実現する場合は、以下のように2つ目の relationship()
を追加し、双方向に relationship.back_populates
パラメーターを適用し、お互いの属性名を使用して設定します。
class Parent(Base):
__tablename__ = "parent_table"
id: Mapped[int] = mapped_column(primary_key=True)
child_id: Mapped[int] = mapped_column(ForeignKey("child_table.id"))
child: Mapped["Child"] = relationship(back_populates="parents")
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
parents: Mapped[List["Parent"]] = relationship(back_populates="child")
NULL許容の多対1
前述の例では、Parent.child
の関係はNone
を許容する型として指定されていません。これは、Parent.child_id
列自体がnull許容でないためであり、それはMapped[int]
として型指定されているからです。もしNull許容の多対1にしたい場合、Parent.child_id
とParent.child
の両方をOptional[]
に設定する方法があります。その場合の設定は以下のようになります。
from typing import Optional
class Parent(Base):
__tablename__ = "parent_table"
id: Mapped[int] = mapped_column(primary_key=True)
child_id: Mapped[Optional[int]] = mapped_column(ForeignKey("child_table.id"))
child: Mapped[Optional["Child"]] = relationship(back_populates="parents")
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
parents: Mapped[List["Parent"]] = relationship(back_populates="child")
この設定により、Parent.child
の列は、データベース上でNULL値を許容するようになります。SQLAlchemyのmapped_column()
を使用して明示的なタイピングを行う場合、child_id: Mapped[Optional[int]]
という設定は、Column.nullable
属性をTrue
に設定することと同等です。逆に、child_id: Mapped[int]
と設定することは、False
に設定することと同等です。この動作の詳細については、こちらで確認することができます。
ヒント:
Python 3.10以降を使用している場合、PEP 604 の構文を使用すると、オプションの型を示すのが簡単です。これにより | None
を組み合わせ、PEP 563 による遅延アノテーション評価を使用できます。その結果、文字列で型を囲む必要がありません。具体的な例を示します。
from __future__ import annotations
class Parent(Base):
__tablename__ = "parent_table"
id: Mapped[int] = mapped_column(primary_key=True)
child_id: Mapped[int | None] = mapped_column(ForeignKey("child_table.id"))
child: Mapped[Child | None] = relationship(back_populates="parents")
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
parents: Mapped[List[Parent]] = relationship(back_populates="child")
1対1
1対1は、外部キーの観点からは基本的に1対多リレーションですが、特定の親行を参照する行が常に1つだけ存在することを示します。
アノテーションを使用してMapped
と組み合わせてマッピングを行う場合、この "1対1" の規則は、リレーションの両側にコレクションでない型を適用することで実現されます。これにより、ORMに対してコレクションを使用しないように指示されます。以下の例のようになります。
class Parent(Base):
__tablename__ = "parent_table"
id: Mapped[int] = mapped_column(primary_key=True)
child: Mapped["Child"] = relationship(back_populates="parent")
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
parent: Mapped["Parent"] = relationship(back_populates="child")
この設定では、Parent
オブジェクトを取得するとき、Parent.child
属性はコレクションではなく、単一のChild
オブジェクトを参照します。新しいChild
オブジェクトにその値を置き換えると、以前のChild
行は新しい行に置き換えられます。特定のcascadeの動作が設定されていない限り、前のchild.parent_id
カラムはデフォルトでNULLになります。
ヒント:
前述の通り、ORMは1対1パターンを慣習として扱い、Parent
オブジェクトのParent.child
属性をロードする際に、1つの行だけが返されると仮定します。1つ以上の行が返された場合、ORMは警告を発します。
しかし、上記のリレーションのChild.parent
側は多対1リレーションとして残ります。それ自体では、1つ以上のChild
の割り当てを検出しませんが、relationship.single_parent
パラメーターが設定されている場合に便利です。
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
parent: Mapped["Parent"] = relationship(back_populates="child", single_parent=True)
このパラメーターを設定しない場合、"1対多"側(ここでは慣習的に"1対1")も、1つのParent
に複数のChild
が関連付けられている場合、特に複数のオブジェクトが保留中でデータベースには永続化されていない場合など、確実には検出しません。
relationship.single_parent
を使用するかどうかに関係なく、データベーススキーマにChild.parent_id
カラムが一意であることを示すunique constraintを含めることが推奨されます。これにより、データベースレベルで特定のParent
行について同時に1つのChild
行のみが参照できるようになります(__table_args__
タプル構文の背景については、Declarative Table Configurationを参照)。
from sqlalchemy import UniqueConstraint
class Child(Base):
__tablename__ = "child_table"
id: Mapped[int] = mapped_column(primary_key=True)
parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
parent: Mapped["Parent"] = relationship(back_populates="child")
__table_args__ = (UniqueConstraint("parent_id"),)
バージョン2.0では、relationship()
は指定されたMapped
アノテーションから relationship.uselist
パラメータの実際の値を導出することができます。
アノテーションを使用しないconfigurationでのuselist=Falseの設定
Mapped
アノテーションを使用せずにrelationship()
を使う場合、通常は「多」側となる部分にrelationship.uselist
パラメーターをFalse
に設定することで、1対1パターンを有効にできます。以下は、非アノテーションの宣言型(Declarative)設定の例です。
class Parent(Base):
__tablename__ = "parent_table"
id = mapped_column(Integer, primary_key=True)
child = relationship("Child", uselist=False, back_populates="parent")
class Child(Base):
__tablename__ = "child_table"
id = mapped_column(Integer, primary_key=True)
parent_id = mapped_column(ForeignKey("parent_table.id"))
parent = relationship("Parent", back_populates="child")
多対多
多対多は、2つのクラス間にassociationテーブルを追加します。associationテーブルはほとんどの場合、CoreのTable
オブジェクトや他のCoreの選択可能なオブジェクト、例えばJoin
オブジェクトとして指定され、relationship()
のrelationship.secondary
引数で示されます。通常、これにより、ForeignKey
指令がリモートテーブルを検索してリンクできるように、宣言(declarative)ベースクラスに関連付けられたMetaData
オブジェクトが使用されます。
from __future__ import annotations
from sqlalchemy import Column
from sqlalchemy import Table
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship
class Base(DeclarativeBase):
pass
# 注意: Coreテーブルの場合、sqlalchemy.Columnを使用し、
# sqlalchemy.orm.mapped_columnは使用しない。
association_table = Table(
"association_table",
Base.metadata,
Column("left_id", ForeignKey("left_table.id")),
Column("right_id", ForeignKey("right_table.id")),
)
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List[Child]] = relationship(secondary=association_table)
class Child(Base):
__tablename__ = "right_table"
id: Mapped[int] = mapped_column(primary_key=True)
ヒント
上記の「association table」には、リレーションの両側の2つのエンティティテーブルを参照する外部キー制約が確立されています。通常、association.left_id
と association.right_id
の各データタイプは、参照されるテーブルのものから推測され、省略することができます。また、SQLAlchemyが要求するわけではありませんが、2つのエンティティテーブルを参照する列は、一意制約 またはより一般的には 主キー制約 のいずれかで確立されることが推奨されています。これにより、アプリケーション側の問題に関係なく、テーブル内に重複した行が永続化されることはありません。
`association_table = Table(
"association_table",
Base.metadata,
Column("left_id", ForeignKey("left_table.id"), primary_key=True),
Column("right_id", ForeignKey("right_table.id"), primary_key=True),
)`
双方向での多対多の設定
双方向リレーションでは両側にコレクションが含まれます。これを指定するには、relationship.back_populates
を使用し、各relationship()
に共通のassociationテーブルを指定します。
from __future__ import annotations
from sqlalchemy import Column
from sqlalchemy import Table
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship
class Base(DeclarativeBase):
pass
association_table = Table(
"association_table",
Base.metadata,
Column("left_id", ForeignKey("left_table.id"), primary_key=True),
Column("right_id", ForeignKey("right_table.id"), primary_key=True),
)
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List[Child]] = relationship(
secondary=association_table, back_populates="parents"
)
class Child(Base):
__tablename__ = "right_table"
id: Mapped[int] = mapped_column(primary_key=True)
parents: Mapped[List[Parent]] = relationship(
secondary=association_table, back_populates="children"
)
"secondary"引数の遅延評価形式の利用
relationship()
のrelationship.secondary
パラメーターは、テーブル名の文字列やラムダ呼び出し可能な関数を含む2つの異なる「遅延評価」形式を受け入れます。詳細については後ほど解説いたします。
多対多リレーションにおけるコレクションタイプの選択
多対多リレーションにおけるコレクションの設定は、「1対多」の解説で記載されている方法と同じです。アノテーション付きのマッピングを使用してMapped
を行う場合、コレクションの型はジェネリッククラス内で使用されるコレクションの型(例:set
)によって指定できます。
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[Set["Child"]] = relationship(secondary=association_table)
アノテーションのない形式、命令型のマッピングを含む場合、1対多の場合のように、コレクションとして使用するPythonクラスは、relationship.collection_class
パラメーターを使用して渡すことができます。
参考:
Customizing Collection Access - コレクションの設定に関する詳細情報が含まれており、relationship()
をdictionaryにマッピングするためのいくつかのテクニックも説明されています。
多対多テーブルからの行の削除
これは relationship()
の relationship.secondary
引数に固有の動作として、指定された Table
は、コレクションからオブジェクトが追加または削除されると、自動的にINSERTおよびDELETEステートメントの対象となります。このテーブルを手動で削除する必要はありません。コレクションからレコードを削除すると、その行がフラッシュ時に削除されます。
# "secondary"テーブルからの行は自動的に削除されます
myparent.children.remove(somechild)
しかし、Session.delete()
で子オブジェクトを直接削除した場合、"secondary"テーブルの行の削除動作は以下の通りです:
- 逆の関係(例えば
Child.parents
など)が存在しない場合、SQLAlchemyはその子オブジェクトの削除と"secondary"テーブルの行の削除との関連を認識しません。この結果、"secondary"テーブルの削除は発生しません。 - 逆の関係が存在する場合(例:
Child.parents
)、SQLAlchemyは関連する"secondary"テーブルの行を削除します。この動作はリレーションが双方向であるかどうかに依存しません。 - 効率的な方法の一つとして、外部キーに
ON DELETE CASCADE
指令を使用することができます。データベースがこの機能をサポートしている場合、子オブジェクトの参照行が削除されると、データベースは関連する"secondary"テーブルの行も自動で削除します。
再度注意してください。これらの動作は、relationship()
で使用されるrelationship.secondary
オプションにのみ関連しています。明示的にマップされたassociationテーブルを扱っていて、関連するrelationship()
のsecondary
オプションに存在しない場合は、代わりにカスケードルールを使用して、関連するエンティティが削除された際に自動的にエンティティを削除することができます。この機能についての情報は、Cascadesを参照してください。
関連リンク:
- Using delete cascade with many-to-many relationships
- Using foreign key ON DELETE with many-to-many relationships
Associationオブジェクト
Associationオブジェクトパターンは、多対多の変種です。親と子(または左と右)のテーブルに外部キー以外の追加の列が含まれている場合に使用されます。これらの列は理想として、それぞれのORMマップされたクラスにマップされます。マップされたクラスは、通常は多対多パターンを使用する場合にrelationship.secondary
として指定されるであろうTable
にマップされます。
Associationオブジェクトパターンでは、relationship.secondary
パラメータは使用されず、代わりにクラスが直接Associationテーブルにマップされます。その後、2つの個別のrelationship()
構築物が使用され、まず親側をマップされたAssociationクラスに1対多のリンクで接続し、次にマップされたAssociationクラスを子側の多対1のリンクで接続して、親からAssociation、そして子への単方向のAssociationオブジェクト関係を形成します。双方向リレーションの場合、マップされたAssociationクラスを親と子の両方の方向にリンクするために4つの構築物が使用されます。
以下の例は、新しいAssociation
クラスを示しており、これはassociation
という名前のTable
にマップされています。このテーブルにはextra_data
という追加の列が含まれており、これはParent
とChild
間の各関連付けと一緒に格納される文字列値です。テーブルをexplicitクラスにマップすることにより、親から子への基本的なアクセスが明示的に行われます。
from typing import Optional
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship
class Base(DeclarativeBase):
pass
class Association(Base):
__tablename__ = "association_table"
left_id: Mapped[int] = mapped_column(ForeignKey("left_table.id"), primary_key=True)
right_id: Mapped[int] = mapped_column(
ForeignKey("right_table.id"), primary_key=True
)
extra_data: Mapped[Optional[str]]
child: Mapped["Child"] = relationship()
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List["Association"]] = relationship()
class Child(Base):
__tablename__ = "right_table"
id: Mapped[int] = mapped_column(primary_key=True)
双方向の例を示すために、既存のリレーションを補完するために2つのrelationship()
構築を追加します。これらの新しい関係は、relationship.back_populates
を使用して既存のリレーションと連携しています。
from typing import Optional
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship
class Base(DeclarativeBase):
pass
class Association(Base):
__tablename__ = "association_table"
left_id: Mapped[int] = mapped_column(ForeignKey("left_table.id"), primary_key=True)
right_id: Mapped[int] = mapped_column(
ForeignKey("right_table.id"), primary_key=True
)
extra_data: Mapped[Optional[str]]
child: Mapped["Child"] = relationship(back_populates="parents")
parent: Mapped["Parent"] = relationship(back_populates="children")
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List["Association"]] = relationship(back_populates="parent")
class Child(Base):
__tablename__ = "right_table"
id: Mapped[int] = mapped_column(primary_key=True)
parents: Mapped[List["Association"]] = relationship(back_populates="child")
直接的にAssociationパターンを使用する際、子オブジェクトは親に追加される前にAssociationインスタンスと関連づけられる必要があります。同様に、親から子へのアクセスはAssociationオブジェクトを通じて行われます:
# 親オブジェクトを作成し、Associationオブジェクトを介して子オブジェクトを追加
p = Parent()
a = Association(extra_data="some data")
a.child = Child()
p.children.append(a)
# Associationオブジェクトを介して子オブジェクトを処理、Associationの属性も含む
for assoc in p.children:
print(assoc.extra_data)
print(assoc.child)
Association
オブジェクトへの直接アクセスを必須ではなくするために、SQLAlchemyはAssociation Proxy拡張を提供しています。この拡張により、関連するオブジェクトへの1つの“hop”と、ターゲット属性への2つ目の“hop”を、一回のアクセスで行う属性の設定が可能となります。
参照:
Association Proxy - 3クラスのアソシエーションオブジェクトマッピングにおいて、親と子の間での直接的な多対多スタイルのアクセスを可能にします。
注意:
Associationオブジェクトパターンと多対多パターンを直接混ぜることは避けてください。この組み合わせにより、特別な手順がない限り、データが一貫性のない方法で読み書きされる状況が生じる可能性があります。association proxyは、通常、より簡潔なアクセスを提供するために使用されるものです。この組み合わせによって導入される注意点の詳細については、次のセクションを参照してください。
Associationオブジェクトと多対多アクセスパターンの組み合わせ
前のセクションで触れたように、Associationオブジェクトパターンは、同じテーブル/カラムに対して多対多パターンの使用と自動的に統合されません。これにより、読み取り操作が矛盾したデータを返す可能性があり、書き込み操作も矛盾した変更をフラッシュしようとすることがあるため、整合性エラーや予期しない挿入削除が発生することがあります。
例を挙げると、以下の例ではParent
とChild
の間に双方向の多対多リレーションをParent.childrenChild.parents
を通して設定しています。同時に、Associationオブジェクトのリレーションも設定されており、Parent.child_associations -> Association.child
と Child.parent_associations -> Association.parent
の間です。
from typing import Optional
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship
class Base(DeclarativeBase):
pass
class Association(Base):
__tablename__ = "association_table"
left_id: Mapped[int] = mapped_column(ForeignKey("left_table.id"), primary_key=True)
right_id: Mapped[int] = mapped_column(
ForeignKey("right_table.id"), primary_key=True
)
extra_data: Mapped[Optional[str]]
# Assocation -> Child の関係
child: Mapped["Child"] = relationship(back_populates="parent_associations")
# Assocation -> Parent の関係
parent: Mapped["Parent"] = relationship(back_populates="child_associations")
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
# Childとの多対多の関連付け。Associationクラスを経由しない
children: Mapped[List["Child"]] = relationship(
secondary="association_table", back_populates="parents"
)
# Parent -> Association -> Childのリレーション
child_associations: Mapped[List["Association"]] = relationship(
back_populates="parent"
)
class Child(Base):
__tablename__ = "right_table"
id: Mapped[int] = mapped_column(primary_key=True)
# ParentとのMany To Manyの関連付け。Associationクラスを経由しない
parents: Mapped[List["Parent"]] = relationship(
secondary="association_table", back_populates="children"
)
# Child -> Association -> Parentのリレーション
parent_associations: Mapped[List["Association"]] = relationship(
back_populates="child"
)
このORMモデルを使用して変更を加えると、PythonでParent.children
に加えられた変更はParent.child_associations
やChild.parent_associations
の変更と連携しません。これらの関係はそれぞれ正常に機能し続けるものの、一方での変更はもう一方に表示されるまで、セッションが期限切れになるまで反映されません。これは通常、Session.commit()
の後に自動的に発生します。
矛盾する変更が行われると、新しいAssociation
オブジェクトの追加と同時に、関連するChild
をParent.children
に追加するといった場合、ワークフローのフラッシュ処理が進行する際に整合性エラーが発生します。以下の例を参照してください:
p1 = Parent()
c1 = Child()
p1.children.append(c1)
# これは冗長であり、Associationへの重複INSERTを引き起こします
p1.child_associations.append(Association(child=c1))
Parent.children
に直接Child
を追加することは、association.extra_data
カラムの値を示さずにassociation
テーブルに行を作成することを意味します。この結果、そのカラムの値としてNULL
が設定されます。
上記のようなマッピングを使用することは問題ありませんが、何をしているかを正確に理解している場合に限ります。"Associationオブジェクト"パターンの使用が稀である場合、多対多の関係を使用する理由があるかもしれません。それは、単一の多対多の関係を通じて関係をロードする方が簡単であり、また"secondary"テーブルがSQL文でどのように使用されるかを、明示的なassociationクラスへの2つの別々の関係と比較してわずかに最適化することができるからです。少なくとも、"secondary"関係にrelationship.viewonly
パラメータを適用することで、矛盾する変更が生じる問題を避けるとともに、追加のassociationカラムにNULL
が書き込まれるのを防ぐことができるのは良いアイディアです。以下のようになります:
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
# `Association`クラス経由しない、Childへの多対多リレーション
children: Mapped[List["Child"]] = relationship(
secondary="association_table", back_populates="parents", viewonly=True
)
# Parent -> Association -> Child の間の関連付け
child_associations: Mapped[List["Association"]] = relationship(
back_populates="parent"
)
class Child(Base):
__tablename__ = "right_table"
id: Mapped[int] = mapped_column(primary_key=True)
# `Association`クラスを経由しない、Parentへの多対多リレーション
parents: Mapped[List["Parent"]] = relationship(
secondary="association_table", back_populates="children", viewonly=True
)
# Child -> Association -> Parent 間の関連付け
parent_associations: Mapped[List["Association"]] = relationship(
back_populates="child"
上記のマッピングでは、Parent.children
やChild.parents
への変更はデータベースに書き込まれません。これにより、書き込みの競合が防止されます。ただし、Parent.children
やChild.parents
の読み取りは、Parent.child_associations
やChild.parent_associations
から読み取られるデータと必ずしも一致しない可能性があります。これは、viewonlyのコレクションが読み取られているのと同じトランザクションやSession
の中でこれらのコレクションに変更が加えられている場合に発生します。Associationオブジェクトの関係の使用がまれで、多対多のコレクションへのアクセスとの間で古い読み取りを避けるように注意深く整理されている場合、このパターンは実現可能です。
上記のパターンの代替手段として人気なのは、直接的な多対多のParent.children
とChild.parents
のリレーションを、Associationオブジェクトを透明にプロキシする拡張で置き換えるものです。この拡張は、ORMの観点からすべてを一貫して保持します。Association Proxyとして知られています。
参照
Association Proxy - 三つのクラスのAssociationオブジェクトマッピングのための親と子の間の直接の「多対多」スタイルのアクセスを許可します。
リレーション引数の遅延評価
これまでのセクションの例のほとんどは、マッピングを示しています。このマッピングでは、さまざまなrelationship()
構造は、そのターゲットクラスをクラス自体ではなく文字列名で参照しています。Mapped
を使用する場合など、実行時には文字列としてのみ存在する前方参照が生成されます。
class Parent(Base):
# ...
children: Mapped[List["Child"]] = relationship(back_populates="parent")
class Child(Base):
# ...
parent: Mapped["Parent"] = relationship(back_populates="children")
同様に、アノテーションを使用しない形式、例えばアノテーションなしの宣言型や命令型マッピングを使用する場合、relationship()
構造体にも文字列の名前が直接サポートされています。
registry.map_imperatively(
Parent,
parent_table,
properties={"children": relationship("Child", back_populates="parent")},
)
registry.map_imperatively(
Child,
child_table,
properties={"parent": relationship("Parent", back_populates="children")},
)
これらの文字列の名前は、マッパーの解決段階でクラスに解決されます。これは通常、すべてのマッピングが定義された後に発生し、マッピングの最初の使用によってトリガーされる内部プロセスです。registry
オブジェクトは、これらの名前が保存され、それらが参照するマップされたクラスに解決されるコンテナです。
relationship()
の主要なクラス引数に加えて、まだ未定義のクラスに存在するカラムに依存する他の引数も、Python関数として、また、一般的な文字列として指定することができます。これらの引数のうち、主要な引数を除くほとんどの引数は、完全なSQL式を受け取ることを意図しているため、Pythonの組み込みeval()関数を使用してPython式として評価される文字列入力です。
注意:
eval()
関数は、relationship()
マッパー設定に渡される遅延評価の文字列引数を解析する際にPythonで使用されます。しかし、これらの引数は信頼できないユーザー入力を受け取るように使用するべきではありません。なぜなら、信頼できない入力に対して安全ではないからです。
この評価時に利用できる名前空間は、該当の宣言ベースでマッピングされたすべてのクラスや、sqlalchemy
パッケージの中身が含まれています。その中には、desc()
や sqlalchemy.sql.functions.func
のような式関数もあります。
class Parent(Base):
# ...
children: Mapped[List["Child"]] = relationship(
order_by="desc(Child.email_address)",
primaryjoin="Parent.id == Child.parent_id",
)
複数のモジュールに同じ名前のクラスが存在する場合、文字列のクラス名は、これらの文字列式のいずれにもモジュール修飾パスとして指定することができます。
class Parent(Base):
# ...
children: Mapped[List["myapp.mymodel.Child"]] = relationship(
order_by="desc(myapp.mymodel.Child.email_address)",
primaryjoin="myapp.mymodel.Parent.id == myapp.mymodel.Child.parent_id",
)
上記のような例では、Mapped
への文字列は、クラス位置の文字列をrelationship.argument
に直接渡すことで、特定のクラス引数から区別できます。
以下は、Child
の型のみのインポートと、レジストリ内で正しい名前を検索するランタイムのターゲットクラス指定を組み合わせた例です:
import typing
if typing.TYPE_CHECKING:
from myapp.mymodel import Child
class Parent(Base):
# ...
children: Mapped[List["Child"]] = relationship(
"myapp.mymodel.Child",
order_by="desc(myapp.mymodel.Child.email_address)",
primaryjoin="myapp.mymodel.Parent.id == myapp.mymodel.Child.parent_id",
)
修飾パスは、名前間のあいまいさを解消するための任意の部分的なパスとなることができます。たとえば、myapp.model1.Child
と myapp.model2.Child
を区別するために、model1.Child
または model2.Child
を指定できます:
class Parent(Base):
# ...
children: Mapped[List["Child"]] = relationship(
"model1.Child",
order_by="desc(mymodel1.Child.email_address)",
primaryjoin="Parent.id == model1.Child.parent_id",
)
relationship()
構築体は、これらの引数の入力としてPython関数またはラムダも受け入れます。Python関数アプローチは次のようになるでしょう:
import typing
from sqlalchemy import desc
if typing.TYPE_CHECKING:
from myapplication import Child
def _resolve_child_model():
from myapplication import Child
return Child
class Parent(Base):
# ...
children: Mapped[List["Child"]] = relationship(
_resolve_child_model,
order_by=lambda: desc(_resolve_child_model().email_address),
primaryjoin=lambda: Parent.id == _resolve_child_model().parent_id,
)
eval()
に渡されるPython関数/ラムダまたは文字列を受け入れるパラメータの完全なリストは次のとおりです:
relationship.order_by
relationship.primaryjoin
relationship.secondaryjoin
relationship.secondary
relationship.remote_side
relationship.foreign_keys
relationship._user_defined_foreign_keys
注意:
前述のように、上記のrelationship()
へのパラメータはeval()を使用してPythonコード式として評価されます。これらの引数に信頼できない入力を渡さないでください。
宣言後にマッピングクラスへリレーションを追加
MapperProperty
構造はいつでも宣言型ベースのマッピングに追加することが可能です(この場面でのアノテーション形式はサポートされていません)。Address
クラスをすでに定義した後で、このrelationship()
を実装したい場合、後から追加することも可能です。
# まず、モジュールAで、Childがまだ作成されていない状態で、
# Childについて何も知らないParentクラスを作成します。
class Parent(Base):
...
# その後、モジュールAの後にインポートされるモジュールBで...:
class Child(Base):
...
from module_a import Parent
# Parentクラスのクラス変数としてUser.addresses関連を割り当てます。
# Declarative Baseクラスがこれをインターセプトして関連をマップします。
Parent.children = relationship(Child, primaryjoin=Child.parent_id == Parent.id)
ORMマップ列の場合と同じく、[Mapped]
アノテーションタイプがこの操作に参加する能力はありません。そのため、関連するクラスは、[relationship()]
構築体内で直接指定する必要があります。これは、クラス自体、クラスの文字列名、またはターゲットクラスへの参照を返すcallable関数のいずれかとして行うことができます。
注意
ORMマップされたカラムの場合と同様に、すでにマップされたクラスにマップされたプロパティを割り当てることは、"宣言型ベース"のクラスが使用された場合のみ正しく機能します。これは、DeclarativeBase
のユーザー定義のサブクラスやdeclarative_base()
、registry.generate_base()
によって動的に生成されたクラスを指します。この"base"クラスには、これらの操作をインターセプトする特別な__setattr__()
メソッドを実装するPythonのメタクラスが含まれています。
マップされたクラスにクラスマップ属性をランタイムで割り当てることは、クラスがregistry.mapped()
のようなデコレーターやregistry.map_imperatively()
のような命令型の関数を使用してマップされている場合には動作しません。
多対多の“secondary”引数に遅延評価形式を使用
多対多の関係は、通常、マップされていない Table
オブジェクトや他のCore selectableオブジェクトを指すrelationship.secondary
パラメータを使用します。lambda callableまたは文字列名を使用した遅延評価がサポートされており、文字列の解決は、現在のregistry
によって参照される同じMetaData
コレクションに存在する同名のオブジェクトへのリンクを示すPython式の評価によって行われます。
多対多のセクションで示された例で、association_table
Table
オブジェクトが、マッピングされたクラス自体よりも後の時点でモジュールに定義されると仮定するならば、ラムダを使用してrelationship()
を書くことができます:
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List["Child"]] = relationship(
"Child", secondary=lambda: association_table
)
あるいは、同じTable
オブジェクトを名前で探すための例として、Table
の名前が引数として使用されます。Pythonの観点からすると、これは「association_table」という名前の変数として評価されるPython式であり、MetaData
コレクション内のテーブル名と照らし合わせて解決されます。
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
children: Mapped[List["Child"]] = relationship(secondary="association_table")
注意:
文字列として渡される場合、relationship.secondary
引数は、通常はテーブルの名前であるにもかかわらず、Pythonのeval()
関数を使用して解釈されます。
この文字列に信頼できない入力を渡さないでください。