Basic Relationship Patterns¶
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 Forms¶
SQLAlchemyが進化するにつれて、さまざまなORM設定スタイルが登場してきました。このセクションの例や、 Mapped
で注釈付きの Declarative マッピングを使用する他の例では、対応する非注釈付き形式は、 relationship()
に渡される最初の引数として、目的のクラスまたは文字列クラス名を使用する必要があります。次の例は、このドキュメントで使用されている形式を示しています。これは、 PEP 484 注釈を使用した完全な宣言型の例です。ここで、 relationship()
構文は、SQLAlchemy宣言型マッピングの最も現代的な形式である Mapped
注釈からターゲットクラスとコレクション型も派生させています:
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")
最後に、Declarativeが作成される前のSQLAlchemyのオリジナルのマッピング形式である Imperative Mapping を使用すると、上記の設定は次のようになります(それでも少数のユーザに好まれています)。:
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 にあります。
必要に応じて、注釈付きスタイルと注釈なし/命令型スタイルのその他の違いを注記します。
One To Many¶
一対多リレーションシップは、親を参照する子テーブルに外部キーを配置します。 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対多で双方向の関係を確立し、”逆”側が多対1である場合は、追加の relationship()
を指定し、 relationship.back_populates
パラメータを使用して2つを接続します。各 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")
子
は、多対1のセマンティクスを持つ 親
属性を取得します。
Using Sets, Lists, or other Collection Types for One To Many¶
注釈付き宣言型マッピングを使用すると、 relationship()
に使用されるコレクションの型は、 Mapped
コンテナ型に渡されたコレクション型から派生します。前のセクションの例では、 Mapped[Set["Child"]]
を使用して、 Parent.children
コレクションに list
ではなく set
を使用するように記述できます:
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
パラメータを使用して渡すことができます。
See also
Customizing Collection Access - relationship()
を辞書にマップするテクニックを含む、コレクション設定の詳細を含んでいます。
Configuring Delete Behavior for One to Many¶
多くの場合、所有している Parent
が削除されると、すべての Child
オブジェクトが削除されます。この動作を設定するには、 delete で説明されている delete
カスケードオプションを使用します。追加のオプションとして、 Child
オブジェクトが親から関連付けられていない場合に、それ自体を削除することができます。この動作は delete-orphan で説明されています。
Many To One¶
多対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)
上の例は、null許容でない振る舞いを前提とした多対1の関係を示しています。次のセクション Nullable Many-to-One は、null許容のバージョンを示しています。
双方向の動作は、2番目の relationship()
を追加し、 relationship.back_populates
パラメータを双方向に適用し、それぞれの 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")
Nullable Many-to-One¶
前の例では、 Parent.child
関係は None
を許可するように型指定されていません。これは、 Parent.child_id
列自体が Mapped[int]
で型指定されているため、null許容ではないことに由来します。 Parent.child
を**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")
上記では、 NULL
値を許可するために Parent.child_id
の列がDDLに作成されます。明示的な型宣言で mapped_column()
を使用する場合、 child_id:Mapped[Optional[int]]
の指定は、 Column
で Column.nullable
を True
に設定するのと同じです。一方、 child_id:Mapped[int]
は False
に設定するのと同じです。この動作の背景については、 mapped_column() derives the datatype and nullability from the Mapped annotation を参照してください。
Tip
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")
One To One¶
One To Oneは本質的には外部キーの観点からの One To Many 関係ですが、特定の親行を参照する行が常に1つだけ存在することを示します。
Mapped
で注釈付きマッピングを使用する場合、関係の両側の 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
オブジェクトを参照します。 Parent.child
の値を新しい Child
オブジェクトに置き換えると、ORMの作業単位プロセスは前の Child
行を新しい行に置き換え、特定の cascade 動作が設定されていない限り、前の child.parent_id
列をデフォルトでNULLに設定します。
Tip
前述したように、ORMは”1対1”パターンを規約と見なします。ここでは、ORMが Parent.child
属性を Parent
オブジェクトにロードすると、1つの行しか返されないことを前提としています。複数の行が返された場合、ORMは警告を発します。
ただし、上記の関係の Child.parent
側は”多対1”の関係のままです。それだけでは、 relationship.single_parent
パラメータが設定されていない限り、複数の Child
の割り当ては検出されません。これは便利な場合があります:
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です)は、複数の Child
オブジェクトが保留中でデータベースに保持されていない場合など、複数の Child
が単一の Parent
に関連付けられているかどうかも確実には検出しません。
relationship.single_parent
が使用されるかどうかに関わらず、データベーススキーマには unique constraint を含めることをお勧めします。これは、一度に1つの Child
行だけが特定の`Parent`行を参照できることをデータベースレベルで保証するために、 Child.parent_id
列が一意でなければならないことを示します( __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"),)
New in version 2.0: relationship()
構文は与えられた Mapped
アノテーションから relationship.uselist
パラメータの有効な値を得ることができます。
Setting uselist=False for non-annotated configurations¶
Mapped
アノテーションを使用せずに relationship()
を使用する場合、1対1のパターンを有効にするには、 relationship.uselist
パラメータを False
に設定します。これは、以下のアノテーションなしの宣言的な設定で示されています。:
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")
Many To Many¶
Many to Manyは、2つのクラスの間に関連付けテーブルを追加します。関連付けテーブルは、ほとんどの場合、Core Table
オブジェクト、または Join
オブジェクトなどの他のCore選択可能オブジェクトとして与えられ、 relationship()
への relationship.secondary
引数によって示されます。通常、 Table
は宣言的な基本クラスに関連付けられた MetaData
オブジェクトを使用するので、 ForeignKey
ディレクティブはリンク先のリモートテーブルを見つけることができます:
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
# note for a Core table, we use the sqlalchemy.Column construct,
# not 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)
Tip
上記の 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),
)
Setting Bi-Directional Many-to-many¶
双方向リレーションシップの場合、リレーションシップの両側にコレクションが含まれます。 relationship.back_populates
を使用して指定し、 relationship()
ごとに共通の関連付けテーブルを指定します:
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"
)
Using a late-evaluated form for the “secondary” argument¶
relationship()
の relationship.secondary
パラメータも、文字列テーブル名とlambda callableを含む2つの異なる”late evaluated”形式を受け付けます。背景と例については Using a late-evaluated form for the “secondary” argument of many-to-many を参照してください。
Using Sets, Lists, or other Collection Types for Many To Many¶
Many to Many関係のコレクションの設定は、 Using Sets, Lists, or other Collection Types for One To Many で説明されているように、 One To Many の設定と同じです。 Mapped
を使用した注釈付きマッピングでは、コレクションは 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
パラメータを使用して渡すことができます。
See also
Customizing Collection Access - relationship()
を辞書にマップするテクニックを含む、コレクション設定の詳細を含んでいます。
Deleting Rows from the Many to Many Table¶
relationship()
の relationship.secondary
引数に固有の振る舞いとして、ここで指定された Table
は、コレクションに対してオブジェクトが追加されたり削除されたりするときに、自動的にINSERT文やDELETE文の対象になります。このテーブルから手動で削除する必要はありません。コレクションからレコードを削除すると、フラッシュ時に行が削除されます:
# row will be deleted from the "secondary" table
# automatically
myparent.children.remove(somechild)
よく出てくる疑問は、子オブジェクトが Session.delete()
に直接渡された場合に、どのようにして”二次”テーブルの行を削除できるかということです:
session.delete(somechild)
ここにはいくつかの可能性があります。
Parent
からChild
へのrelationship()
があっても、特定のChild
をそれぞれのParent
にリンクする逆の関係が ない 場合、SQLAlchemyは、この特定のChild
オブジェクトを削除するときに、それをParent
にリンクする”二次”テーブルを維持する必要があることを認識しません。”二次”テーブルの削除は行われません。
もし特定の
Child
をそれぞれのParent
にリンクする関係があって、それがChild.parents
と呼ばれていると仮定すると、SQLAlchemyはデフォルトでChild.parents
コレクションにロードしてすべてのParent
オブジェクトを見つけ、このリンクを確立する”二次”テーブルから各行を削除します。この関係は双方向である必要はないことに注意してください。SQLAlchemyは削除されるChild
オブジェクトに関連するすべてのrelationship()
を厳密に見ています。
ここでのより高いパフォーマンスのオプションは、データベースで使用される外部キーと共にON DELETE CASCADEディレクティブを使用することです。データベースがこの機能をサポートしていると仮定すると、”child”内の参照行が削除されるときに、データベース自体が”二次”テーブル内の行を自動的に削除するようにすることができます。この場合、SQLAlchemyは、
relationship()
のrelationship.passive_deletes
ディレクティブを使用して、Child.parents
コレクションへのアクティブなロードを放棄するように指示できます。詳細については Using foreign key ON DELETE cascade with ORM relationships を参照してください。
繰り返しますが、これらの動作は relationship()
で使用される relationship.secondary
オプションに*のみ*関連します。明示的にマップされ、関連する relationship()
の relationship.secondary
オプションに 存在しない 関連テーブルを扱う場合、カスケードルールを代わりに使用して、関連するエンティティが削除されたときに自動的にエンティティを削除することができます。この機能についての情報は Cascades を参照してください。
Association Object¶
関連付けオブジェクトパターンは多対多のバリエーションです。関連付けテーブルに、親テーブルと子テーブル(または左と右)の外部キーである列以外の追加の列が含まれている場合に使用されます。これらの列は、独自のORMマップクラスにマップされるのが最も理想的です。このマップされたクラスは、多対多パターンを使用する場合に relationship.secondary
と表記される Table
に対してマップされます。
関連付けオブジェクトパターンでは、 relationship.secondary
パラメータは使用されません。代わりに、クラスは関連付けテーブルに直接マップされます。2つの個別の relationship()
構成体は、まず親側を1対多を介してマップされた関連付けクラスにリンクし、次にマップされた関連付けクラスを多対1を介して子側にリンクして、親から関連付け、子への一方向の関連付けオブジェクト関係を形成します。双方向の関係では、4つの relationship()
構成体を使用して、マップされた関連付けクラスを双方向で親と子の両方にリンクします。
以下の例は、新しいクラス Association
を示しています。このクラスは、 association
という名前の Table
にマップされます。このテーブルには、 extra_data
という追加の列が含まれています。この列は、 Parent
と Child
の間の各関連付けと共に保存される文字列値です。テーブルを明示的なクラスにマップすることで、 Parent
から Child
への基本的なアクセスは、明示的に Association
を使用します:
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")
直接形式で関連付けパターンを操作するには、子オブジェクトが親に追加される前に関連付けインスタンスに関連付けられている必要があります。同様に、親から子へのアクセスは関連付けオブジェクトを経由します。:
# create parent, append a child via association
p = Parent()
a = Association(extra_data="some data")
a.child = Child()
p.children.append(a)
# iterate through child objects via association, including association
# attributes
for assoc in p.children:
print(assoc.extra_data)
print(assoc.child)
Association
オブジェクトへの直接アクセスがオプションになるように関連付けオブジェクトのパターンを拡張するために、SQLAlchemyは Association Proxy 拡張を提供しています。この拡張では、1回のアクセスで2つの”ホップ”、1つは関連付けられたオブジェクトへの「ホップ」、もう1つはターゲット属性への”ホップ”にアクセスする属性を設定できます。
See also
Association Proxy - 3クラスの関連付けオブジェクトマッピングのために、親と子の間で直接”多対多”スタイルのアクセスを可能にします。
Warning
関連付けオブジェクトパターンと many-to-many パターンを直接混合することは避けてください。これは、特別な手順なしに一貫性のない方法でデータが読み書きされる可能性がある状態を作り出すためです。 association proxy は、より簡潔なアクセスを提供するために一般的に使用されます。この組み合わせによって導入された警告の詳細な背景については、次のセクション Combining Association Object with Many-to-Many Access Patterns を参照してください。
Combining Association Object with Many-to-Many Access Patterns¶
前のセクションで説明したように、関連付けオブジェクトパターンは、同じテーブル/列に対する多対多パターンの使用と同時に自動的に統合されません。このことから、読み取り操作は競合するデータを返す可能性があり、書き込み操作も競合する変更をフラッシュしようとする可能性があり、整合性エラーまたは予期しない挿入または削除を引き起こすことになります。
以下の例では、 Parent.children
と Child.parents
を経由して Parent
と Child
の双方向の多対多の関係を設定しています。同時に、 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]]
# association between Assocation -> Child
child: Mapped["Child"] = relationship(back_populates="parent_associations")
# association between Assocation -> Parent
parent: Mapped["Parent"] = relationship(back_populates="child_associations")
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
# many-to-many relationship to Child, bypassing the `Association` class
children: Mapped[List["Child"]] = relationship(
secondary="association_table", back_populates="parents"
)
# association between 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)
# many-to-many relationship to Parent, bypassing the `Association` class
parents: Mapped[List["Parent"]] = relationship(
secondary="association_table", back_populates="children"
)
# association between Child -> Association -> Parent
parent_associations: Mapped[List["Association"]] = relationship(
back_populates="child"
)
このORMモデルを使用して変更を行う場合、Pythonでは、 Parent.children
に加えられた変更は、 Parent.child_associations
や Child.parent_associations
に加えられた変更と調整されません。これらの関係はすべて、それ自体では正常に機能し続けますが、一方で加えられた変更は、通常 Session.commit()
の後に自動的に発生する Session
が期限切れになるまで、もう一方には現れません。
さらに、新しい Association
オブジェクトを追加する一方で、関連する同じ Child
を Parent.children
に追加するなど、矛盾する変更が行われた場合、次の例のように、作業単位フラッシュプロセスが進行するときに整合性エラーが発生します。:
p1 = Parent()
c1 = Child()
p1.children.append(c1)
# redundant, will cause a duplicate INSERT on Association
p1.child_associations.append(Association(child=c1))
あなたが何をしているか知っているなら、上記のようなマッピングを使うのは良いことです。”関連オブジェクト”パターンの使用が頻繁でない場合に多対多の関係を使うのには十分な理由があるかもしれません。それは、単一の多対多の関係に沿って関係をロードする方が簡単であり、明示的な関連クラスへの2つの別々の関係が使われる方法と比較して、SQL文で”二次”テーブルが使われる方法を少し良く最適化することができるからです。少なくとも、 relationship.viewonly
パラメータを”二次”関係に適用して、矛盾する変更が発生する問題を回避し、次のように追加の関連列に NULL
が書き込まれないようにすることをお勧めします。:
class Parent(Base):
__tablename__ = "left_table"
id: Mapped[int] = mapped_column(primary_key=True)
# many-to-many relationship to Child, bypassing the `Association` class
children: Mapped[List["Child"]] = relationship(
secondary="association_table", back_populates="parents", viewonly=True
)
# association between 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)
# many-to-many relationship to Parent, bypassing the `Association` class
parents: Mapped[List["Parent"]] = relationship(
secondary="association_table", back_populates="children", viewonly=True
)
# association between 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
から読み取られたデータと必ずしも一致しません。これは、表示のみのコレクションが読み取られているのと同じトランザクションまたは Session
内でこれらのコレクションに変更が加えられている場合です。関連オブジェクトの関係の使用頻度が低く、古い読み取りを避けるために多対多のコレクションにアクセスするコードに対して慎重に編成されている場合(極端な場合には、 Session.expire()
を直接使用して、現在のトランザクション内でコレクションを更新します)、このパターンは実現可能かもしれません。
上記のパターンに代わる一般的な方法は、直接の多対多の Parent.children
と Child.parents
の関係を、ORMの観点からすべての一貫性を保ちながら、 Association
クラスを通じて透過的にプロキシする拡張に置き換えることです。この拡張は Association Proxy として知られています。
See also
Association Proxy - 3クラスの関連付けオブジェクトマッピングのために、親と子の間で直接”多対多”スタイルのアクセスを可能にします。
Late-Evaluation of Relationship Arguments¶
これまでの節で紹介した例のほとんどは、さまざまな 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式として評価されます。
この評価内で利用可能な完全な名前空間には、この宣言ベース用にマップされたすべてのクラスと、 desc()
や sqlalchemy.sql.functions.func
のような式関数を含む sqlalchemy
パッケージの内容が含まれます:
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
に渡すことで、特定のクラス引数から明確にすることもできます。以下は、 registry
内で正しい名前を検索するターゲットクラスの実行時指定子と組み合わせた、 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,
)
Pythonの関数やラムダ、あるいは文字列を受け入れて``eval()``に渡されるパラメータの完全なリストは以下の通りです:
Warning
前述したように、 relationship()
の上記のパラメータは eval()を使ってPythonコード式として評価されます。これらの引数に信頼できない入力を渡してはいけません 。
Adding Relationships to Mapped Classes After Declaration¶
また、 Appending additional columns to an existing Declarative mapped class で説明されているのと同様の方法で、任意の MapperProperty
構文をいつでも宣言ベースマッピングに追加できることにも注意してください(このコンテキストでは注釈付きフォームはサポートされていないことに注意してください)。この relationship()
を Address
クラスが利用可能になった後に実装したい場合は、後で適用することもできます:
# first, module A, where Child has not been created yet,
# we create a Parent class which knows nothing about Child
class Parent(Base): ...
# ... later, in Module B, which is imported after module A:
class Child(Base): ...
from module_a import Parent
# assign the User.addresses relationship as a class variable. The
# declarative base class will intercept this and map the relationship.
Parent.children = relationship(Child, primaryjoin=Child.parent_id == Parent.id)
ORMマップされた列の場合と同様に、 Mapped
アノテーション型がこの操作に関与する機能はありません。したがって、関連するクラスは relationship()
構造体内で直接指定する必要があります。クラス自体、クラスの文字列名、またはターゲットクラスへの参照を返す呼び出し可能な関数のいずれかです。
Note
ORMマップされた列の場合と同様に、既にマップされたクラスへのマップされたプロパティの割り当て
“宣言ベース”クラス、つまり DeclarativeBase
のユーザ定義のサブクラスや、 declarative_base()
や registry.generate_base()
が返す動的に生成されるクラスを使用すれば、正しく機能します。この”ベース”クラスには、これらの操作をインターセプトする特別な __setattr__()
メソッドを実装するPythonメタクラスが含まれています。
クラスが registry.mapped()
のようなデコレータや registry.map_mandatory()
のような命令関数を使ってマップされている場合、クラスにマップされた属性のマップされたクラスへの実行時の割り当ては 機能しません 。
Using a late-evaluated form for the “secondary” argument of many-to-many¶
多対多の関係は relationship.secondary
パラメータを利用します。これは通常、通常はマップされていない Table
オブジェクトやその他のCoreの選択可能なオブジェクトへの参照を示します。lambda callableを使用した後期評価が一般的です。
Many To Many で与えられた例で、もし 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
)
有効なPython識別子 でもあるテーブル名のショートカットとして、 relationship.secondary
パラメータも文字列として渡すことができます。この場合の解決は、現在の registry
によって参照される同じ MetaData
コレクションに存在する同じ名前の Table
オブジェクトにリンクされた単純な識別子名を使用して、文字列をPython式として評価することによって行われます。
以下の例では、式「association_table」は、 MetaData
コレクション内のテーブル名に対して解決される association_table
という名前の変数として評価されます。
- class Parent(Base):
__tablename__ = “left_table”
id: Mapped[int] = mapped_column(primary_key=True) children: Mapped[List[“Child”]] = relationship(secondary=”association_table”)
Note
文字列として渡される場合、 relationship.secondary
に渡される名前 は有効なPython識別子でなければなりません
文字で始まり、英数字またはアンダースコアのみを含みます。ダッシュなどのその他の文字は、指定された名前に解決されないPython演算子として解釈されます。わかりやすくするために、文字列ではなくラムダ式を使用することを検討してください。
..warning:: 文字列として渡された場合、 relationship.secondary
引数は、通常はテーブルの名前ですが、Pythonの eval()
関数を使って解釈されます。 この文字列に信頼できない入力を渡してはいけません 。