ORM Quick Start

ORMの基本的な使い方を簡単に知りたい新規ユーザのために、 SQLAlchemy Unified Tutorial で使用されているマッピングと例の省略形を以下に示します。ここでのコードは、クリーンなコマンドラインから完全に実行できます。

このセクションの説明は意図的に 非常に短い ため、ここで説明されている各概念のより詳細な説明については、完全な SQLAlchemy Unified Tutorial に進んでください。

Changed in version 2.0: ORM Quickstartは 、mapped_column() を含む新しい構成体を使用して、最新の PEP 484 対応機能のために更新されました。マイグレーション情報については ORM Declarative Models 節を参照してください。

Declare Models

ここでは、データベースからクエリする構造を形成するモジュールレベルの構成体を定義します。この構造は Declarative Mapping と呼ばれ、Pythonオブジェクトモデルと、特定のデータベースに存在する、または存在する予定の実際のSQLテーブルを記述する database metadata の両方を同時に定義します:

>>> from typing import List
>>> from typing import Optional
>>> from sqlalchemy import ForeignKey
>>> from sqlalchemy import String
>>> from sqlalchemy.orm import DeclarativeBase
>>> from sqlalchemy.orm import Mapped
>>> from sqlalchemy.orm import mapped_column
>>> from sqlalchemy.orm import relationship

>>> class Base(DeclarativeBase):
...     pass

>>> class User(Base):
...     __tablename__ = "user_account"
...
...     id: Mapped[int] = mapped_column(primary_key=True)
...     name: Mapped[str] = mapped_column(String(30))
...     fullname: Mapped[Optional[str]]
...
...     addresses: Mapped[List["Address"]] = relationship(
...         back_populates="user", cascade="all, delete-orphan"
...     )
...
...     def __repr__(self) -> str:
...         return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"

>>> class Address(Base):
...     __tablename__ = "address"
...
...     id: Mapped[int] = mapped_column(primary_key=True)
...     email_address: Mapped[str]
...     user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
...
...     user: Mapped["User"] = relationship(back_populates="addresses")
...
...     def __repr__(self) -> str:
...         return f"Address(id={self.id!r}, email_address={self.email_address!r})"

このマッピングは、上では Base と呼ばれている基底クラスから始まり、 DeclarativeBase クラスに対して単純なサブクラスを作ることで作成されます。

個々のマップされたクラスは、 Base のサブクラスを作成することで作成されます。マップされたクラスは通常、単一の特定のデータベーステーブルを参照し、その名前はクラスレベルの属性 __tablename__ を使って示されます。

次に、 Mapped と呼ばれる特別な型付け注釈を含む属性を追加することによって、テーブルの一部である列が宣言されます。各属性の名前は、データベーステーブルの一部となる列に対応しています。各列のデータ型は、最初に各 Mapped 注釈に関連付けられたPythonのデータ型から取得されます。つまり、 INTEGER の場合は int 、 VARCHAR の場合は str などです。Nullabilityは、 Optional[] 型修飾子が使用されているかどうかによって決まります。より具体的な型付け情報は、右側の mapped_column() ディレクティブ内のSQLAlchemy型オブジェクトを使用して示すことができます。たとえば、上記の User.name 列で使用されている String データ型などです。Python型とSQL型の関連付けは、 type annotation map を使用してカスタマイズできます。

mapped_column() ディレクティブは、より具体的なカスタマイズを必要とするすべての列ベースの属性に使用されます。このディレクティブは、型情報の他に、サーバのデフォルトや制約情報(主キーや外部キー内のメンバシップなど)を含む、データベース列に関する特定の詳細を示すさまざまな引数を受け入れます。 mapped_column() ディレクティブは、SQLAlchemy Column クラスで受け入れられる引数のスーパーセットを受け入れます。このクラスは、SQLAlchemyコアでデータベース列を表現するために使用されます。

すべてのORMマップクラスでは、少なくとも1つの列が主キーの一部として宣言されている必要があります。通常は、キーの一部であるべき mapped_column() オブジェクトに対して Column.primary_key パラメータを使用します。上の例では、 User.id と Address.id 列が主キーとしてマークされています。

SQLAlchemyでは、文字列テーブル名と列宣言のリストの組み合わせは、 table metadata として知られています。CoreとORMの両方のアプローチを使用したテーブルメタデータの設定は、 Working with Database Metadata の SQLAlchemy Unified Tutorial で紹介されています。上記のマッピングは、 Annotated Declarative Table 構成として知られているものの例です。

Mapped の他のバリエーションも利用できます。最も一般的なのは、上で示した relationship() 構文です。列ベースの属性とは対照的に、 relationship() は2つのORMクラス間のリンクを表します。上の例では、 User.addresses は User を Address にリンクし、 Address.user は Address を User にリンクします。 relationship() 構文は Working with ORM Related Objects の SQLAlchemy Unified Tutorial で導入されています。

最後に、上記のクラスの例には、必須ではありませんが、デバッグには便利な __repr__() メソッドが含まれています。マップされたクラスは、データクラスを使って自動的に生成される __repr__() などのメソッドで作成できます。データクラスマッピングの詳細については、 Declarative Dataclass Mapping を参照してください。

Create an Engine

Engine は、新しいデータベース接続を作成できる**ファクトリ**で、高速な再利用のために Connection Pool 内の接続も保持します。学習のために、通常は便宜上 SQLite メモリのみのデータベースを使用します:

>>> from sqlalchemy import create_engine
>>> engine = create_engine("sqlite://", echo=True)

Tip

echo=True パラメータは、接続によって発行されたSQLが標準出力に記録されることを示します。

Emit CREATE TABLE DDL

テーブルのメタデータとエンジンを使うと、 MetaData.create_all() というメソッドを使って、ターゲットのSQLiteデータベースにスキーマを一度に生成することができます。:

>>> Base.metadata.create_all(engine)
BEGIN (implicit) PRAGMA main.table_...info("user_account") ... PRAGMA main.table_...info("address") ... CREATE TABLE user_account ( id INTEGER NOT NULL, name VARCHAR(30) NOT NULL, fullname VARCHAR, PRIMARY KEY (id) ) ... CREATE TABLE address ( id INTEGER NOT NULL, email_address VARCHAR NOT NULL, user_id INTEGER NOT NULL, PRIMARY KEY (id), FOREIGN KEY(user_id) REFERENCES user_account (id) ) ... COMMIT

私たちが書いたちょっとしたPythonコードから多くのことが起こりました。テーブルメタデータで何が行われているかの完全な概要については、 Working with Database Metadata のチュートリアルを参照してください。

Create Objects and Persist

これで、データベースにデータを挿入する準備ができました。これを行うには、 User クラスと Address クラスのインスタンスを作成します。これらのクラスには、宣言的なマッピングプロセスによって自動的に確立された __init__() メソッドがあります。次に、 Session というオブジェクトを使ってデータベースに渡します。このオブジェクトは、 Engine を利用してデータベースと対話します。ここでは、複数のオブジェクトを一度に追加するために Session.add_all() メソッドを使用しています。また、 Session.commit() メソッドを使用して、データベースへの保留中の変更を flush してから、現在のデータベーストランザクションを commit します。これは、 Session が使用されるたびに常に進行中です。

>>> from sqlalchemy.orm import Session

>>> with Session(engine) as session:
...     spongebob = User(
...         name="spongebob",
...         fullname="Spongebob Squarepants",
...         addresses=[Address(email_address="spongebob@sqlalchemy.org")],
...     )
...     sandy = User(
...         name="sandy",
...         fullname="Sandy Cheeks",
...         addresses=[
...             Address(email_address="sandy@sqlalchemy.org"),
...             Address(email_address="sandy@squirrelpower.org"),
...         ],
...     )
...     patrick = User(name="patrick", fullname="Patrick Star")
...
...     session.add_all([spongebob, sandy, patrick])
...
...     session.commit()
BEGIN (implicit) INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id [...] ('spongebob', 'Spongebob Squarepants') INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id [...] ('sandy', 'Sandy Cheeks') INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id [...] ('patrick', 'Patrick Star') INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id [...] ('spongebob@sqlalchemy.org', 1) INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id [...] ('sandy@sqlalchemy.org', 2) INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id [...] ('sandy@squirrelpower.org', 2) COMMIT

Tip

Session は上記のようにコンテキストマネージャスタイルで使うこと、つまりPythonの with: 文を使うことをお勧めします。 Session オブジェクトはアクティブなデータベースリソースを表すので、一連の操作が完了したときには必ず閉じておくとよいでしょう。次のセクションでは、説明のためだけに Session を開いたままにしておきます。

Session を作成するための基礎は Executing with an ORM Session に、その他は Basics of Using a Session にあります。

次に、 Inserting Rows using the ORM Unit of Work pattern でいくつかの種類の基本的なパーシスタンス操作が紹介されています。

Simple SELECT

データベース内のいくつかの行を使用して、いくつかのオブジェクトをロードするためにSELECT文を発行する最も簡単な形式を次に示します。SELECT文を作成するには、 select() 関数を使用して新しい Select オブジェクトを作成し、 Session を使用して呼び出します。ORMオブジェクトを照会するときによく役立つメソッドは Session.scalars() メソッドです。このメソッドは、選択したORMオブジェクトを反復する ScalarResult オブジェクトを返します。

>>> from sqlalchemy import select

>>> session = Session(engine)

>>> stmt = select(User).where(User.name.in_(["spongebob", "sandy"]))

>>> for user in session.scalars(stmt):
...     print(user)
BEGIN (implicit) SELECT user_account.id, user_account.name, user_account.fullname FROM user_account WHERE user_account.name IN (?, ?) [...] ('spongebob', 'sandy')
User(id=1, name='spongebob', fullname='Spongebob Squarepants') User(id=2, name='sandy', fullname='Sandy Cheeks')

上記の問い合わせは、WHERE条件を追加するために Select.where() メソッドも使用しています。また、SQLAlchemyの列のような構文の一部である ColumnOperators.in_() メソッドを使用してSQL IN演算子を使用しています。

オブジェクトと個々の列を選択する方法の詳細については、 Selecting ORM Entities and Columns を参照してください。

SELECT with JOIN

一度に複数のテーブル間でクエリを行うことは非常に一般的であり、SQLではJOINキーワードがこれを行う主要な方法です。 Select 構文は、 Select.join() メソッドを使用して結合を作成します。

>>> stmt = (
...     select(Address)
...     .join(Address.user)
...     .where(User.name == "sandy")
...     .where(Address.email_address == "sandy@sqlalchemy.org")
... )
>>> sandy_address = session.scalars(stmt).one()
SELECT address.id, address.email_address, address.user_id FROM address JOIN user_account ON user_account.id = address.user_id WHERE user_account.name = ? AND address.email_address = ? [...] ('sandy', 'sandy@sqlalchemy.org')
>>> sandy_address Address(id=2, email_address='sandy@sqlalchemy.org')

上記の問い合わせは、ANDを使って自動的に連結される複数のWHERE条件と、オーバーライドされたPythonメソッド ColumnOperators.__eq__() を使ってSQL条件オブジェクトを生成するSQLAlchemyの列のようなオブジェクトを使って「等価性」の比較を行う方法を示しています。

上記の概念に関する背景は The WHERE clause と Explicit FROM clauses and JOINs にあります。

Make Changes

Session オブジェクトは、ORMにマップされたクラスの User と Address と共に、オブジェクトに加えられた変更を自動的に追跡します。その結果、次に Session がフラッシュされたときにSQL文が出力されます。以下では、”patrick”の行を取得するためにSELECTを発行した後に、”sandy”に関連付けられた1つの電子メールアドレスを変更し、”patrick”に新しい電子メールアドレスを追加します。

>>> stmt = select(User).where(User.name == "patrick")
>>> patrick = session.scalars(stmt).one()
SELECT user_account.id, user_account.name, user_account.fullname FROM user_account WHERE user_account.name = ? [...] ('patrick',)
>>> patrick.addresses.append(Address(email_address="patrickstar@sqlalchemy.org"))
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 [...] (3,)
>>> sandy_address.email_address = "sandy_cheeks@sqlalchemy.org" >>> session.commit()
UPDATE address SET email_address=? WHERE address.id = ? [...] ('sandy_cheeks@sqlalchemy.org', 2) INSERT INTO address (email_address, user_id) VALUES (?, ?) [...] ('patrickstar@sqlalchemy.org', 3) COMMIT

patrick.addresses にアクセスした時に、SELECTが発行されたことに注意してください。これは:term:遅延ロード`と呼ばれます。多少のSQLを使って関連する項目にアクセスする様々な方法の背景は :ref:`tutorial_orm_loader_strategies で紹介されています。

ORMデータ操作の詳細なウォークスルーは Data Manipulation with the ORM から始まります。

Some Deletes

ここでは、特定のユース・ケースに基づいて重要な2つの異なる形式の削除について簡単に説明します。

まず、”sandy” ユーザーから Address オブジェクトの1つを削除します。次に Session がフラッシュすると、行が削除されます。この動作は、 delete cascade と呼ばれるマッピングで設定したものです。 Session.get() を使って主キーで sandy オブジェクトのハンドルを取得し、そのオブジェクトを操作します。

>>> sandy = session.get(User, 2)
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 = ? [...] (2,)
>>> sandy.addresses.remove(sandy_address)
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 [...] (2,)

上記の最後のSELECTは、 lazy load 操作です。これにより、 sandy.addresses コレクションをロードして、 sandy.addresses メンバーを削除することができます。この一連の操作を実行する方法は他にもありますが、それほど多くのSQLは生成されません。

Session.flush() メソッドを使用して、トランザクションをコミットせずに、これまでに変更するように設定されたものに対してDELETE SQLを出力することを選択できます。:

>>> session.flush()
DELETE FROM address WHERE address.id = ? [...] (2,)

次に、 “patrick” ユーザーを完全に削除します。オブジェクト自体をトップレベルで削除するには、 Session.delete() メソッドを使用します。このメソッドは実際に削除を実行するのではなく、次のフラッシュ時に削除されるようにオブジェクトを設定します。この操作は、設定したカスケードオプションに基づいて、関連するオブジェクトに:term:カスケード`します。この場合は、関連する ``Address` オブジェクトに cascade します。

>>> session.delete(patrick)
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 = ? [...] (3,) 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 [...] (3,)

この場合の Session.delete() メソッドは、DELETEを発行しなかったにもかかわらず、2つのSELECT文を発行しました。これは驚くべきことかもしれません。これは、メソッドがオブジェクトを検査したときに、最後に Session.commit() を呼び出したときに発生した patrick オブジェクトが expired であることが判明し、発行されたSQLが新しいトランザクションから行を再ロードすることになったためです。この有効期限はオプションであり、通常の使用では、有効期限が適切に適用されない状況では無効にすることがよくあります。

削除されるローを説明するために、コミットを次に示します。:

.. sourcecode:: pycon+sql
>>> session.commit()
{execsql}DELETE FROM address WHERE address.id = ?
[...] (4,)
DELETE FROM user_account WHERE user_account.id = ?
[...] (3,)
COMMIT
{stop}

このチュートリアルでは、 Deleting ORM Objects using the Unit of Work pattern でORMの削除について説明しています。オブジェクトの有効期限に関する背景は Expiring / Refreshing にあります。カスケードについては Cascades で詳しく説明されています。

Learn the above concepts in depth

新規ユーザーにとって、上記のセクションはおそらく慌ただしいツアーでした。上記の各ステップには、カバーされていない多くの重要な概念があります。物事がどのように見えるかを簡単に概観し、 SQLAlchemy Unified Tutorial を通じて、上記で実際に何が起こっているのかについての確かな実務知識を得ることをお勧めします。幸運を祈ります!