Working with ORM Related Objects¶
このセクションでは、ORMが他のオブジェクトを参照するマップされたクラスとどのように相互作用するかという、もう1つの重要なORMの概念について説明します。 Declaring Mapped Classes のセクションで、マップされたクラスの例は relationship()
という構成体を使用しています。この構成体は、2つの異なるマップされたクラス間、またはマップされたクラスからそれ自身へのリンクを定義します。後者は**自己参照**関係と呼ばれます。
relationship()
の基本的な考え方を説明するために、まず、 mapped_column()
のマッピングやその他の指示を省略して、マッピングを短い形式で復習します。:
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import relationship
class User(Base):
__tablename__ = "user_account"
# ... mapped_column() mappings
addresses: Mapped[List["Address"]] = relationship(back_populates="user")
class Address(Base):
__tablename__ = "address"
# ... mapped_column() mappings
user: Mapped["User"] = relationship(back_populates="addresses")
上の例では、 User
クラスに User.addresses
属性が追加され、 Address
クラスに Address.user
属性が追加されています。 relationship()
構文は、型定義の振る舞いを示す Mapped
構文とともに、 User
クラスと Address
クラスにマップされた Table
オブジェクト間のテーブルの関係を調べるために使用されます。 address
テーブルを表す Table
オブジェクトは、 user_account
テーブルを参照する ForeignKeyConstraint
を持っているので、 relationship()
は、 User.addresses
関係に沿って、 User
クラスから Address
クラスへの:term:1対多`の関係があることを明確に判断できます。 ``user_account` テーブルの1つの特定の行は、 address
テーブルの多くの行から参照される可能性があります。
すべての1対多の関係は、逆方向の many to one 関係に自然に対応します。この場合は、 Address.user
によって示される関係です。上で見たように、他の名前を参照する両方の relationship()
オブジェクトに設定された relationship.back_populates
パラメータは、これら2つの relationship()
構成体のそれぞれが互いに相補的であると考えるべきであることを確立します。これがどのように行われるかは次のセクションで説明します。
Persisting and Loading Relationships¶
relationship()
がオブジェクトのインスタンスに対して何をするかを説明することから始めましょう。新しい User
オブジェクトを作成すると、 .addresses
要素にアクセスしたときにPythonのリストがあることがわかります。
>>> u1 = User(name="pkrabs", fullname="Pearl Krabs")
>>> u1.addresses
[]
このオブジェクトはSQLAlchemyに特化したバージョンのPythonの list
で、変更を追跡して応答することができます。このコレクションは、属性にアクセスしたときにも自動的に表示されますが、属性をオブジェクトに割り当てたことはありません。これは Inserting Rows using the ORM Unit of Work pattern に記載されている動作と似ています。ここでは、明示的に値を割り当てていない列ベースの属性も、Pythonの通常の動作である AttributeError
を発生させるのではなく、自動的に None
と表示されることが確認されています。
このコレクションは、その中に永続化できるPythonオブジェクトの唯一の型である Address
クラスに固有のものです。 list.append()
メソッドを使って、 Address
オブジェクトを追加することができます。:
>>> a1 = Address(email_address="pearl.krabs@gmail.com")
>>> u1.addresses.append(a1)
この時点で、予想通り u1.addresses
コレクションには新しい Address
オブジェクトが含まれています。:
>>> u1.addresses
[Address(id=None, email_address='pearl.krabs@gmail.com')]
この同期は、2つの relationship()
オブジェクトの間で relationship.back_populates
パラメータを使用した結果として発生しました。このパラメータは、補完的な属性割り当て/リスト変換が発生する別の relationship()
を指定します。別の方向でも同様にうまく機能します。つまり、別の Address
オブジェクトを作成し、その Address.user
属性に割り当てると、その Address
はその User
オブジェクトの User.addresses
コレクションの一部になります。:
>>> a2 = Address(email_address="pearl@aol.com", user=u1)
>>> u1.addresses
[Address(id=None, email_address='pearl.krabs@gmail.com'), Address(id=None, email_address='pearl@aol.com')]
実際には、 user
パラメータを Address
コンストラクタのキーワード引数として使用しました。これは、 Address
クラスで宣言された他のマップされた属性と同じように受け入れられます。これは、 Address.user
属性を次の事実の後に割り当てるのと同じです。:
# equivalent effect as a2 = Address(user=u1)
>>> a2.user = u1
Cascading Objects into the Session¶
これで、メモリ内の双方向構造体に関連付けられた User
オブジェクトと2つの Address
オブジェクトができましたが、 Inserting Rows using the ORM Unit of Work pattern で前述したように、これらのオブジェクトは Session
オブジェクトに関連付けられるまでは transient 状態にあると言われます。
まだ実行中の Session
を利用していますが、 Session.add()
メソッドを先頭の User
オブジェクトに適用すると、関連する Address
オブジェクトも同じ Session
に追加されることに注意してください。:
>>> session.add(u1)
>>> u1 in session
True
>>> a1 in session
True
>>> a2 in session
True
Session
が`User`オブジェクトを受け取り、関連する`Address`オブジェクトを見つけるために`User.addresses`関係に従った上記の振る舞いは、 save-update カスケード として知られており、 Cascades のORMリファレンス文書で詳細に論じられています。
3つのオブジェクトは pending 状態になっています。これは、これらのオブジェクトがINSERT操作の対象になる準備ができているが、まだ処理されていないことを意味します。3つのオブジェクトはすべて、まだ主キーが割り当てられていません。さらに、 a1
と a2
オブジェクトは、 user_id
という属性を持っています。この属性は、 user_account.id
列を参照する ForeignKeyConstraint
を持つ Column
を参照します。これらのオブジェクトはまだ実際のデータベース行に関連付けられていないので、 None
にもなります。:
>>> print(u1.id)
None
>>> print(a1.user_id)
None
作業単位プロセスが提供する非常に大きな有用性を見ることができるのは、この段階です。 INSERT usually generates the “values” clause automatically の節を思い出してください。この節では、 address.user_id
列と user_account
行を自動的に関連付けるために、いくつかの精巧な構文を使って user_account
テーブルと address
テーブルに行が挿入されました。さらに、 address
行の前に user_account
行に対してINSERTを発行する必要がありました。なぜなら、 address
行の行は user_id
列の値に関して user_account
の親行に 依存 しているからです。
Session
を使用すると、これらの退屈な作業はすべて私たちのために処理され、最も頑固なSQL純粋主義者でさえ、INSERT、UPDATE、DELETE文の自動化から恩恵を受けることができます。 Session.commit()
を使用すると、すべてのステップが正しい順序でトランザクションを呼び出し、さらに新しく生成された user_account
行の主キーが address.user_id
列に適切に適用されます。:
>>> session.commit()
INSERT INTO user_account (name, fullname) VALUES (?, ?)
[...] ('pkrabs', 'Pearl Krabs')
INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id
[... (insertmanyvalues) 1/2 (ordered; batch not supported)] ('pearl.krabs@gmail.com', 6)
INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id
[insertmanyvalues 2/2 (ordered; batch not supported)] ('pearl@aol.com', 6)
COMMIT
Loading Relationships¶
最後のステップでは、 Session.commit()
を呼び出してトランザクションのCOMMITを発行し、次に Session.commit.expire_on_commit
ごとにすべてのオブジェクトを期限切れにして、次のトランザクションのために更新します。
次にこれらのオブジェクトの属性にアクセスすると、例えば u1
オブジェクトに対して新しく生成されたプライマリ・キーを見るときのように、その行のプライマリ属性に対してSELECTが発行されていることがわかります。
>>> u1.id
BEGIN (implicit)
SELECT user_account.id AS user_account_id, user_account.name AS user_account_name,
user_account.fullname AS user_account_fullname
FROM user_account
WHERE user_account.id = ?
[...] (6,)
6
>>> u1.addresses
SELECT address.id AS address_id, address.email_address AS address_email_address,
address.user_id AS address_user_id
FROM address
WHERE ? = address.user_id
[...] (6,)
[Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')]
SQLAlchemy ORM内のコレクションと関連する属性はメモリ内に保持されます。コレクションや属性が生成されると、そのコレクションや属性が expired になるまでSQLは生成されなくなります。 u1.addresses
に再度アクセスしたり、項目を追加または削除したりすることができますが、これによって新しいSQL呼び出しが発生することはありません:
>>> u1.addresses
[Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')]
遅延読み込みによって発生する読み込みは、それを最適化するための明示的な手順を実行しなければ、すぐに高価になる可能性がありますが、少なくとも遅延読み込みのネットワークは、冗長な作業を実行しないようにかなり最適化されています。 u1.addresses
コレクションが更新されたので、 identity map ごとに、これらは実際には、私たちがすでに扱っている a1
および a2
オブジェクトと同じ Address
インスタンスであるため、この特定のオブジェクトグラフのすべての属性の読み込みは完了です:
>>> a1
Address(id=4, email_address='pearl.krabs@gmail.com')
>>> a2
Address(id=5, email_address='pearl@aol.com')
関係がどのようにロードされるかという問題は、それ自体の主題です。これらの概念についての追加の紹介は、このセクションの後半の Loader Strategies にあります。
Using Relationships in Queries¶
前のセクションでは、マップされたクラスの**インスタンス**を扱う場合の relationship()
構文の動作を紹介しました。上では、 User
クラスと Address
クラスの u1
、 a1
、 a2
インスタンスを紹介しました。このセクションでは、マップされたクラスの クラスレベルの動作 に適用される relationship()
の動作を紹介します。このクラスでは、いくつかの方法でSQLクエリの構築を自動化するのに役立ちます。
Using Relationships to Join¶
Explicit FROM clauses and JOINs 節と Setting the ON Clause 節では、SQL JOIN句を作成するための Select.join()
メソッドと Select.join_from()
メソッドの使用法を紹介しました。テーブル間の結合方法を記述するために、これらのメソッドは、2つのテーブルをリンクするテーブルメタデータ構造内の単一の明白な ForeignKeyConstraint
オブジェクトの存在に基づいてON句を**推論**するか、または特定のON句を示す明示的なSQL式構文を提供することができます。
ORMエンティティを使用する場合、結合のON句を設定するのに役立つ追加のメカニズムが利用できます。これは、 Declaring Mapped Classes で示されているように、ユーザマッピングで設定した relationship()
オブジェクトを利用することです。 relationship()
に対応するclass-bound属性は、 単一の引数 として Select.join()
に渡すことができます。この場合、結合の右側とON句の両方を同時に示す役割を果たします:
>>> print(select(Address.email_address).select_from(User).join(User.addresses))
SELECT address.email_address
FROM user_account JOIN address ON user_account.id = address.user_id
マッピング上のORM relationship()
の存在は、 Select.join()
や Select.join_from()
では、指定しない場合にON句を推測するために使用されません。つまり、ON句なしで User
から Address
に結合すると、 User
と Address
クラスの relationship()
オブジェクトではなく、2つのマップされた Table
オブジェクト間の ForeignKeyConstraint
のために機能します。:
>>> print(select(Address.email_address).join_from(User, Address))
SELECT address.email_address
FROM user_account JOIN address ON user_account.id = address.user_id
Select.join()
と Select.join_from()
を relationship()
構文で使用する方法の例については、 ORM Querying Guide の Joins 節を参照してください。
See also
Joins in the ORM Querying Guide
Relationship WHERE Operators¶
relationship()
に付属しているSQL生成ヘルパーには、他にもいくつかの種類があります。これらは通常、文のWHERE句を作成するときに役立ちます。 ORM Querying Guide の Relationship WHERE Operators 節を参照してください。
See also
Loader Strategies¶
Loading Relationships 節では、マップされたオブジェクトのインスタンスを操作する際に、デフォルトの場合で relationship()
を使ってマップされた属性にアクセスすると、このコレクションに存在するはずのオブジェクトをロードするためにコレクションが作成されていない場合に lazy load が発生するという概念を紹介しました。
遅延読み込みは、最も有名なORMパターンの1つであり、最も物議を醸すパターンでもあります。メモリ内の数十個のORMオブジェクトがそれぞれアンロードされた属性を参照している場合、これらのオブジェクトをルーチンに操作すると、追加される可能性のある多くのクエリがスピンオフされ( N plus one problem としても知られています)、さらに悪いことに、それらは暗黙的に発行されます。これらの暗黙的なクエリは気づかれない可能性があり、利用可能なデータベーストランザクションがなくなった後に試行された場合にエラーを引き起こす可能性があります。また、 asyncio のような別の同時実行パターンを使用している場合には、実際にはまったく動作しません。
同時に、遅延ロードは、使用されている並行性アプローチと互換性があり、他に問題を引き起こさない場合には、非常に一般的で有用なパターンである。これらの理由から、SQLAlchemyのORMは、このロード動作を制御し最適化できることに重点を置いている。
何よりも、ORM遅延読み込みを効果的に使用するための最初のステップは、 アプリケーションをテストし、SQLエコーをオンにし、生成されるSQLを監視する ことです。より効率的に1つにロールできるように見える冗長なSELECT文がたくさんあるように見える場合、 Session
から detached されたオブジェクトに対して不適切に発生するロードがある場合、 ローダ戦略 を使用することを検討する必要があります。
ローダー戦略は、 Select.options()
メソッドを使用してSELECT文に関連付けられるオブジェクトとして表されます。次に例を示します。:
for user_obj in session.execute(
select(User).options(selectinload(User.addresses))
).scalars():
user_obj.addresses # access addresses collection already loaded
これらは relationship.lazy
オプションを使って relationship()
のデフォルトとして設定することもできます。:
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import relationship
class User(Base):
__tablename__ = "user_account"
addresses: Mapped[List["Address"]] = relationship(
back_populates="user", lazy="selectin"
)
それぞれのローダ戦略オブジェクトは、ある種の情報を文に追加します。この情報は、後で Session
が様々な属性のロード方法やアクセス時の振る舞いを決定する際に使用されます。
以下のセクションでは、最も顕著に使用されるローダー戦略のいくつかを紹介します。
See also
Relationship Loading Techniques の2つのセクション:
Configuring Loader Strategies at Mapping Time -
relationship()
での戦略の設定に関する詳細Relationship Loading with Loader Options - クエリ時のローダー戦略の詳細
Selectin Load¶
最近のSQLAlchemyで最も便利なローダーは、 selectinload()
ローダーオプションです。このオプションは、関連するコレクションを参照するオブジェクトの集合の問題である N+1
問題の最も一般的な形式を解決します。 selectinload()
は、完全な一連のオブジェクトの特定のコレクションが単一のクエリを使用して事前にロードされることを保証します。これは、ほとんどの場合、JOINやサブクエリを導入せずに関連するテーブルのみに対して発行でき、コレクションがまだロードされていない親オブジェクトに対するクエリのみに対して発行できるSELECT形式を使用して行われます。以下では、 selectinload()
がすべての User
オブジェクトとそれに関連するすべての Address
オブジェクトをロードすることを説明します。 Session.execute()
を一度だけ呼び出しますが、 select()
構文が与えられると、データベースがアクセスされると、実際には2つのSELECT文が発行され、2番目は関連する Address
オブジェクトを取得します。
>>> from sqlalchemy.orm import selectinload
>>> stmt = select(User).options(selectinload(User.addresses)).order_by(User.id)
>>> for row in session.execute(stmt):
... print(
... f"{row.User.name} ({', '.join(a.email_address for a in row.User.addresses)})"
... )
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account ORDER BY user_account.id
[...] ()
SELECT address.user_id AS address_user_id, address.id AS address_id,
address.email_address AS address_email_address
FROM address
WHERE address.user_id IN (?, ?, ?, ?, ?, ?)
[...] (1, 2, 3, 4, 5, 6)
spongebob (spongebob@sqlalchemy.org)
sandy (sandy@sqlalchemy.org, sandy@squirrelpower.org)
patrick ()
squidward ()
ehkrabs ()
pkrabs (pearl.krabs@gmail.com, pearl@aol.com)
See also
Joined Load¶
joinedload()
のeager load戦略は、SQLAlchemyの中で最も古いeager loaderで、データベースに渡されるSELECT文をJOIN(オプションに応じて外部結合または内部結合)で補強し、関連するオブジェクトにロードすることができます。
joinedload()
戦略は、関連する多対1のオブジェクトをロードするのに最も適しています。なぜなら、これは追加の列が、いかなる場合にもフェッチされるプライマリエンティティ行に追加されることのみを必要とするからです。より効率的にするために、 joinedload.innerjoin
オプションも受け入れています。これにより、以下のようにすべての Address
オブジェクトに User
オブジェクトが関連付けられていることがわかっている場合に、外部結合の代わりに内部結合を使用できます。
>>> from sqlalchemy.orm import joinedload
>>> stmt = (
... select(Address)
... .options(joinedload(Address.user, innerjoin=True))
... .order_by(Address.id)
... )
>>> for row in session.execute(stmt):
... print(f"{row.Address.email_address} {row.Address.user.name}")
SELECT address.id, address.email_address, address.user_id, user_account_1.id AS id_1,
user_account_1.name, user_account_1.fullname
FROM address
JOIN user_account AS user_account_1 ON user_account_1.id = address.user_id
ORDER BY address.id
[...] ()
spongebob@sqlalchemy.org spongebob
sandy@sqlalchemy.org sandy
sandy@squirrelpower.org sandy
pearl.krabs@gmail.com pkrabs
pearl@aol.com pkrabs
joinedload()
はコレクションに対しても機能します。これは一対多の関係を意味しますが、再帰的な方法で関連する項目ごとにプライマリ行を乗算する効果があり、ネストされたコレクションやより大きなコレクションに対して結果セットに送信されるデータの量を桁違いに増加させます。そのため、 selectinload()
のような他のオプションと比較して、その使用はケースごとに評価されるべきです。
囲んでいる Select
文 のWHEREおよびORDER BY条件は、joinedload() によって描画されたテーブルを対象にしていないことに注意してください。上記のSQLでは、 匿名のalias がクエリ内で直接アドレス指定できないように user_account
テーブルに適用されていることがわかります。この概念については、 The Zen of Joined Eager Loading の節で詳しく説明します。
Tip
“N plus one”問題は一般的なケースではあまり一般的ではないため、多対1のEager Loadは必要ないことが多いことに注意してください。多くのオブジェクトがすべて同じ関連オブジェクトを参照している場合、例えば、それぞれが同じ User
を参照している多くの Address
オブジェクトの場合、通常の遅延読み込みを使用して、その User
オブジェクトに対してSQLが一度だけ生成されます。遅延読み込みルーチンは、可能な限りSQLを生成せずに、現在の Session
内の主キーによって関連オブジェクトを検索します。
See also
Explicit Join + Eager load¶
Select.join()
などのメソッドを使用してJOINをレンダリングしながら、 user_account
テーブルへの結合中に Address
行をロードする場合、返された各 Address
オブジェクトの Address.user
属性の内容を積極的にロードするために、そのJOINを利用することもできます。これは基本的に、 結合された積極的なロード
を使用していますが、JOIN自体をレンダリングしているということです。この一般的なユースケースは、 contains_eager()
オプションを使用することで実現されます。このオプションは joinedload()
と非常によく似ていますが、JOIN自体が設定されていることを前提としており、代わりにCOLUMNS句の追加列が返された各オブジェクトの関連する属性にロードされることを示しているだけです。次に例を示します。
>>> from sqlalchemy.orm import contains_eager
>>> stmt = (
... select(Address)
... .join(Address.user)
... .where(User.name == "pkrabs")
... .options(contains_eager(Address.user))
... .order_by(Address.id)
... )
>>> for row in session.execute(stmt):
... print(f"{row.Address.email_address} {row.Address.user.name}")
SELECT user_account.id, user_account.name, user_account.fullname,
address.id AS id_1, address.email_address, address.user_id
FROM address JOIN user_account ON user_account.id = address.user_id
WHERE user_account.name = ? ORDER BY address.id
[...] ('pkrabs',)
pearl.krabs@gmail.com pkrabs
pearl@aol.com pkrabs
上記では、 user_account.name
の行をフィルタし、 user_account
の行を返された行の Address.user
属性にロードしました。 joinedload()
を別々に適用すると、不必要に2回結合するSQLクエリが得られます:
>>> stmt = (
... select(Address)
... .join(Address.user)
... .where(User.name == "pkrabs")
... .options(joinedload(Address.user))
... .order_by(Address.id)
... )
>>> print(stmt) # SELECT has a JOIN and LEFT OUTER JOIN unnecessarily
SELECT address.id, address.email_address, address.user_id,
user_account_1.id AS id_1, user_account_1.name, user_account_1.fullname
FROM address JOIN user_account ON user_account.id = address.user_id
LEFT OUTER JOIN user_account AS user_account_1 ON user_account_1.id = address.user_id
WHERE user_account.name = :name_1 ORDER BY address.id
See also
Two sections in Relationship Loading Techniques:
The Zen of Joined Eager Loading - describes the above problem in detail
Routing Explicit Joins/Statements into Eagerly Loaded Collections - using
contains_eager()
Raiseload¶
特筆すべきもう1つのローダー戦略は raiseload()
です。このオプションは、通常は遅延ロードであるものが代わりにエラーを発生させることによって、アプリケーションが N plus one 問題を起こすのを完全にブロックするために使用されます。 raiseload.sql_only
オプションで制御される2つのバリエーションがあり、SQLを必要とする遅延ロードと、現在の Session
を参照するだけでよいものを含むすべての「ロード」操作のどちらかをブロックします。
raiseload()
を使用する1つの方法は、 relationship.lazy
を値 raise_on_sql
に設定して、 relationship()
自体に設定することです。これにより、特定のマッピングに対して、特定の関係がSQLを出力しようとしなくなります。
:<数値>:
>>> from sqlalchemy.orm import Mapped
>>> from sqlalchemy.orm import relationship
>>> class User(Base):
... __tablename__ = "user_account"
... id: Mapped[int] = mapped_column(primary_key=True)
... addresses: Mapped[List["Address"]] = relationship(
... back_populates="user", lazy="raise_on_sql"
... )
>>> class Address(Base):
... __tablename__ = "address"
... id: Mapped[int] = mapped_column(primary_key=True)
... user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
... user: Mapped["User"] = relationship(back_populates="addresses", lazy="raise_on_sql")
このようなマッピングを使用すると、アプリケーションは遅延読み込みからブロックされ、特定のクエリがローダー戦略を指定する必要があることを示します:
>>> u1 = session.execute(select(User)).scalars().first()
SELECT user_account.id FROM user_account
[...] ()
>>> u1.addresses
Traceback (most recent call last):
...
sqlalchemy.exc.InvalidRequestError: 'User.addresses' is not available due to lazy='raise_on_sql'
この例外は、このコレクションを最初にロードする必要があることを示します。:
>>> u1 = (
... session.execute(select(User).options(selectinload(User.addresses)))
... .scalars()
... .first()
... )
SELECT user_account.id
FROM user_account
[...] ()
SELECT address.user_id AS address_user_id, address.id AS address_id
FROM address
WHERE address.user_id IN (?, ?, ?, ?, ?, ?)
[...] (1, 2, 3, 4, 5, 6)
lazy="raise_on_sql"``オプションは、多対1の関係についても賢く扱おうとします。上記では、 ``Address
オブジェクトの Address.user
属性がロードされていないが、その User
オブジェクトが同じ Session
にローカルに存在する場合、”raiseload”戦略はエラーを発生させません。