Data Manipulation with the ORM

前のセクション Working with Data では、主要なSQLステートメント構造全体にわたって連続性を提供するために、コアの観点からSQL式言語に焦点を当てました。次に、このセクションでは Session のライフサイクルと、これらの構造との相互作用について説明します。

前提となるセクション - このチュートリアルのORMに焦点を当てた部分は、このドキュメントの前の2つのORM中心のセクションに基づいています。

Inserting Rows using the ORM Unit of Work pattern

ORMを使用する場合、 Session オブジェクトは、 Insert 構文を構築し、それを実行中のトランザクション内にINSERT文として出力します。 Session にこれを行うように指示する方法は、オブジェクトエントリを 追加 することです。 Session は、これらの新しいエントリが必要なときに、 フラッシュ と呼ばれるプロセスを使用してデータベースに出力されることを確認します。 Session がオブジェクトを保持するために使用する全体的なプロセスは、unit of work パターンと呼ばれます。

Instances of Classes represent Rows

前の例では、追加したいデータを示すためにPython辞書を使用してINSERTを発行しましたが、ORMでは、 Using ORM Declarative Forms to Define Table Metadata で定義したカスタムPythonクラスを直接使用します。クラスレベルでは、 User クラスと Address クラスが、対応するデータベーステーブルがどのように見えるべきかを定義する場所として機能しました。これらのクラスは、トランザクション内で行を作成し操作するために使用する拡張可能なデータオブジェクトとしても機能します。以下では、それぞれが挿入される可能性のあるデータベース行を表す2つの User オブジェクトを作成します。:

>>> squidward = User(name="squidward", fullname="Squidward Tentacles")
>>> krabs = User(name="ehkrabs", fullname="Eugene H. Krabs")

コンストラクタ内のキーワード引数として、マップされた列の名前を使用して、これらのオブジェクトを構築することができます。これは、 User クラスには、ORMマッピングによって提供された自動的に生成された __init__() コンストラクタが含まれているため、コンストラクタ内のキーとして列名を使用して各オブジェクトを作成できるためです。

Insert のコアの例と同じように、主キー(つまり、 id 列のエントリ)を含めませんでした。これは、ORMにも統合されているデータベース(この場合はSQLite)の自動インクリメント主キー機能を利用したいからです。上記のオブジェクトの「id」属性の値を表示すると、 None と表示されます。:

>>> squidward
User(id=None, name='squidward', fullname='Squidward Tentacles')

SQLAlchemyによって提供される None という値は、その属性がまだ値を持っていないことを示します。SQLAlchemyでマップされた属性は、Pythonでは常に値を返し、値が割り当てられていない新しいオブジェクトを処理するときに、それらが欠落していても AttributeError を発生させません。

現時点では、上記の2つのオブジェクトは transient と呼ばれる状態にあると言われています。これらはどのデータベース状態にも関連付けられておらず、INSERT文を生成できる Session オブジェクトにはまだ関連付けられていません。

Adding objects to a Session

追加プロセスを順を追って説明するために、コンテキストマネージャを使わずに Session を作成します(したがって、後で必ず閉じる必要があります!):

>>> session = Session(engine)

その後、オブジェクトは Session.add() メソッドを使って Session に追加されます。このメソッドが呼ばれると、オブジェクトは pending という状態になり、まだ挿入されていません:

>>> session.add(squidward)
>>> session.add(krabs)

保留中のオブジェクトがある場合、 Session.new という Session のコレクションを見ることで、この状態を見ることができます:

>>> session.new
IdentitySet([User(id=None, name='squidward', fullname='Squidward Tentacles'), User(id=None, name='ehkrabs', fullname='Eugene H. Krabs')])

上記のビューは IdentitySet と呼ばれるコレクションを使用しています。これは基本的にPythonのセットで、あらゆる場合にオブジェクトのIDをハッシュします(つまり、Pythonの hash() 関数ではなく、Pythonの組み込みの id() 関数を使用します)。

Flushing

Sessionunit of work として知られるパターンを利用します。これは一般的に、一度に1つずつ変更を蓄積しますが、実際には必要になるまでそれらをデータベースに伝達しないことを意味します。これにより、指定された一連の保留中の変更に基づいて、トランザクションでSQL DMLをどのように発行するかについて、より適切な決定を下すことができます。現在の一連の変更をプッシュするためにデータベースにSQLを発行する場合、このプロセスは フラッシュ と呼ばれます。

Session.flush() メソッドを呼び出すことで、フラッシュプロセスを手動で示すことができます。:

>>> session.flush()
BEGIN (implicit) INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id [... (insertmanyvalues) 1/2 (ordered; batch not supported)] ('squidward', 'Squidward Tentacles') INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id [insertmanyvalues 2/2 (ordered; batch not supported)] ('ehkrabs', 'Eugene H. Krabs')

上の例では、 Session が最初にSQLを発行するために呼び出されたので、新しいトランザクションを作成し、2つのオブジェクトに対して適切なINSERT文を発行しました。このトランザクションは、 SessionSession.commit()Session.rollback()Session.close() のいずれかのメソッドを呼び出すまで、 開いたまま なります。

Session.flush() は現在のトランザクションに対して保留中の変更を手動でプッシュするために使われることがありますが、 Sessionautoflush として知られている振る舞いを特徴としており、これについては後で説明します。また、 Session.commit() が呼ばれるたびに変更をフラッシュします。

Autogenerated primary key attributes

行が挿入されると、作成した2つのPythonオブジェクトは persistent と呼ばれる状態になり、追加またはロードされた Session オブジェクトに関連付けられます。その他にも多くの動作がありますが、これについては後で説明します。

実行されたINSERTのもう1つの効果は、ORMが新しいオブジェクトごとに新しい主キー識別子を取得したことです。内部的には、通常は前に紹介したのと同じ CursorResult.inserted_primary_key アクセッサを使用します。 squidward および krabs オブジェクトには、これらの新しい主キー識別子が関連付けられ、 id 属性にアクセスすることで表示できます。:

>>> squidward.id
4
>>> krabs.id
5

Tip

ref:executemany <tutorial_multiple_parameters> を使用できたのに、なぜORMは2つの別々のINSERT文を発行したのですか?次のセクションで見るように、オブジェクトをフラッシュするときの Session は、新しく挿入されたオブジェクトのプライマリキーを常に知る必要があります。SQLiteのオートインクリメントのような機能が使用されている場合(他の例としては、PostgreSQLのIDENTITYやSERIAL、シーケンスの使用などがあります)、 CursorResult.inserted_primary_key 機能は、通常、各INSERTが一度に1行ずつ発行されることを要求します。事前にプライマリキーの値を提供していれば、ORMは操作をより良く最適化することができたでしょう。 psycopg2 のようないくつかのデータベースバックエンドも、プライマリキーの値を取得しながら、一度に多くの行をINSERTできます。

Getting Objects by Primary Key from the Identity Map

オブジェクトのプライマリキーIDは Session にとって重要です。なぜなら、オブジェクトは:term:IDマップ`として知られる機能を使用して、メモリ内でこのIDにリンクされているからです。IDマップは、現在メモリにロードされているすべてのオブジェクトをそのプライマリキーIDにリンクするメモリ内のストアです。 :meth:`_orm.Session.get メソッドを使用して上記のオブジェクトの1つを取得することで、これを確認できます。このメソッドは、ローカルに存在する場合はIDマップからエントリを返し、存在しない場合はSELECT:

>>> some_squidward = session.get(User, 4)
>>> some_squidward
User(id=4, name='squidward', fullname='Squidward Tentacles')

IDマップに関して注意すべき重要なことは、特定の Session オブジェクトのスコープ内で、特定のデータベースIDごとに特定のPythonオブジェクトの ユニークなインスタンス を保持するということです。 some_squidward が、以前の squidward同じオブジェクト を参照していることがわかります。:

>>> some_squidward is squidward
True

IDマップは、トランザクション内でオブジェクトの複雑なセットを同期させずに操作できるようにする重要な機能です。

Committing

Session がどのように動作するかについては、さらに詳しく説明します。ここでは、ORMの動作や機能を調べる前に、行をSELECTする方法に関する知識を蓄積できるように、トランザクションをコミットします。:

>>> session.commit()
COMMIT

上記の操作は、進行中のトランザクションをコミットします。これまで扱ってきたオブジェクトは、 Sessionattached のままです。これは、 Session が閉じられるまでの状態です( Closing a Session で紹介されています)。

Tip

注意すべき重要な点は、今作業したオブジェクトの属性が expired であることです。これは、次にそれらの属性にアクセスすると、 Session が新しいトランザクションを開始し、その状態を再ロードすることを意味します。このオプションは、パフォーマンス上の理由から、または Session ( detached 状態として知られています)を閉じた後にオブジェクトを使用したい場合に、問題になることがあります。なぜなら、オブジェクトには状態がなく、その状態をロードするための Session もないため、”detached instance”エラーが発生するからです。この動作は Session.expire_on_commit というパラメータを使用して制御できます。これについての詳細は Closing a Session を参照してください。

Updating ORM Objects using the Unit of Work pattern

前のセクション Using UPDATE and DELETE Statements で、SQL UPDATE文を表す Update 構文を紹介しました。ORMを使用する場合、この構文を使用する方法は2つあります。主な方法は、 Session で使用される unit of work プロセスの一部として自動的に発行されることです。ここで、UPDATE文は、変更された個々のオブジェクトに対応する主キーごとに発行されます。

ユーザ名 sandyUser オブジェクトをトランザクションにロードしたとします( Select.filter_by() メソッドと Result.scalar_one() メソッドも表示しています)。

>>> sandy = session.execute(select(User).filter_by(name="sandy")).scalar_one()
BEGIN (implicit) SELECT user_account.id, user_account.name, user_account.fullname FROM user_account WHERE user_account.name = ? [...] ('sandy',)

前述のPythonオブジェクト sandy は、データベース内の行の プロキシ として動作します。具体的には、 現在のトランザクション から見て、主キーのIDが 2 であるデータベースの行のプロキシとして動作します。:

>>> sandy
User(id=2, name='sandy', fullname='Sandy Cheeks')

このオブジェクトの属性を変更すると、 Session がこの変更を追跡します:

>>> sandy.fullname = "Sandy Squirrel"

オブジェクトは Session.dirty という名前のコレクションに表示され、オブジェクトが”dirty”であることを示します:

>>> sandy in session.dirty
True

Session が次にフラッシュを発行すると、データベース内のこの値を更新するUPDATEが発行されます。前述したように、フラッシュはSELECTを発行する前に、 autoflush という動作を使って自動的に行われます。この行から直接 User.fullname 列を検索すると、更新された値が返されます。

>>> sandy_fullname = session.execute(select(User.fullname).where(User.id == 2)).scalar_one()
UPDATE user_account SET fullname=? WHERE user_account.id = ? [...] ('Sandy Squirrel', 2) SELECT user_account.fullname FROM user_account WHERE user_account.id = ? [...] (2,)
>>> print(sandy_fullname) Sandy Squirrel

上記では、 Session が単一の select() 文を実行するように要求したことがわかります。しかし、出力されたSQLは、保留中の変更をプッシュするフラッシュプロセスであるUPDATEも出力されたことを示しています。 sandy Pythonオブジェクトはもはやダーティとは見なされなくなりました:

>>> sandy in session.dirty
False

ただし、 まだトランザクション内にあり 、変更がデータベースの永続ストレージにプッシュされていないことに注意してください。Sandyの姓は実際には「Squirrel」ではなく「Chewers」であるため、後でトランザクションをロールバックするときにこの間違いを修正します。ただし、最初にデータをさらに変更します。

See also

Flushing - フラッシュプロセスの詳細と Session.autoflush 設定に関する情報を提供します。

Deleting ORM Objects using the Unit of Work pattern

基本的なパーシステンス操作を完成させるために、 Session.delete() メソッドを使って、個々のORMオブジェクトを:term:unit of work`プロセス内で削除するようにマークすることができます。データベースから ``patrick` をロードしましょう。

>>> patrick = session.get(User, 3)
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,)

他の操作と同じように patrick に削除のマークを付けても、実際にはフラッシュが行われるまで何も起こりません:

>>> session.delete(patrick)

現在のORMの振る舞いでは、フラッシュが行われるまで patrickSession にとどまります。フラッシュは前述のように問い合わせを発行した場合に行われます。

>>> session.execute(select(User).where(User.name == "patrick")).first()
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,) DELETE FROM user_account WHERE user_account.id = ? [...] (3,) SELECT user_account.id, user_account.name, user_account.fullname FROM user_account WHERE user_account.name = ? [...] ('patrick',)

上の例では、出力するように要求したSELECTの前にDELETEがありました。これは、 patrick の保留中の削除が進行したことを示しています。また、 address テーブルに対する SELECT もありました。これは、ORMがこのテーブル内でターゲット行に関連する可能性のある行を探すことによって促されました。この動作は cascade として知られる動作の一部であり、データベースが address 内の関連する行を自動的に処理できるようにすることで、より効率的に動作するように調整できます。 delete セクションには、この詳細がすべて記載されています。

See also

delete - Session.delete() の動作を、他のテーブルの関連する行の処理方法に関して調整する方法を説明します。

それ以上に、削除された patrick オブジェクトインスタンスは、包含チェックで示されるように、 Session 内で永続的であるとは見なされなくなります:

>>> patrick in session
False

しかし、 sandy オブジェクトに対して行った更新と同様に、ここで行ったすべての変更は進行中のトランザクションに対してローカルなものであり、コミットしなければ永続的なものにはなりません。トランザクションをロールバックする方が現時点では興味深いので、次のセクションでそれを行います。

Bulk / Multi Row INSERT, upsert, UPDATE and DELETE

このセクションで説明する unit of work のテクニックは、 dml やINSERT/UPDATE/DELETE文をPythonのオブジェクト機構と統合することを意図しており、相互に関連するオブジェクトの複雑なグラフを含むことがよくあります。 Session.add() を使ってオブジェクトが Session に追加されると、オブジェクトの属性が作成され変更されると、作業単位プロセスが透過的にINSERT/UPDATE/DELETEを発行します。

しかし、ORM Session には、ORMで永続化されたオブジェクトを渡さずに、INSERT、UPDATE、DELETE文を直接発行できるようにするコマンドを処理する機能もあります。その代わりに、INSERT、UPDATE、UPDATEされる値のリスト、またはWHERE基準を渡して、一度に多くの行に一致するUPDATEまたはDELETE文を呼び出すことができます。この使用モードは、マップされたオブジェクトを構築および操作する必要なしに、多数の行に影響を与える必要がある場合に特に重要です。マップされたオブジェクトは、大規模なバルク挿入などの単純でパフォーマンスを重視するタスクでは煩雑で不要な場合があります。

ORM Session のBulk/Multi row機能は insert(),:func:_dml.update,:func:_dml.delete 構文を直接利用しており、その使い方はSQLAlchemy Core(このチュートリアルでは Using INSERT StatementsUsing UPDATE and DELETE Statements で初めて紹介されました)での使い方と似ています。これらの構文を普通の Connection ではなくORM Session で使う場合、その構築、実行、結果の処理はORMと完全に統合されます。

これらの機能の背景と使用例については、 ORM Querying Guide のセクション ORM-Enabled INSERT, UPDATE, and DELETE statements を参照してください。

See also

ORM-Enabled INSERT, UPDATE, and DELETE statements - ORM Querying Guide を参照してください。

Rolling Back

Session には Session.rollback() メソッドがあり、これは予想通り、進行中のSQL接続に対してROLLBACKを発行します。しかし、これは現在 Session に関連付けられているオブジェクトにも影響を与えます。前の例では、Pythonオブジェクトの sandy です。 sandy オブジェクトの .fullnameSandy Squirrel に変更しましたが、この変更をロールバックしたいと思います。 Session.rollback() を呼び出すと、トランザクションがロールバックされるだけでなく、現在この Session に関連付けられているすべてのオブジェクトが**期限切れ**になります。これにより、次に lazy loading と呼ばれるプロセスを使ってアクセスされたときに更新されます。

>>> session.rollback()
ROLLBACK

“expiration”プロセスをもっと詳しく見ると、Pythonオブジェクト sandy には、特殊なSQLAlchemy内部状態オブジェクトを除いて、Python __dict__ 内に状態が残っていないことがわかります:

>>> sandy.__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>}

これは “expired” 状態です。属性に再度アクセスすると、新しいトランザクションが自動的に開始され、現在のデータベースの行で sandy が更新されます。

>>> sandy.fullname
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 Cheeks'

これで、データベースの行全体が sandy オブジェクトの __dict__ にも追加されたことがわかります。:

>>> sandy.__dict__  
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>,
 'id': 2, 'name': 'sandy', 'fullname': 'Sandy Cheeks'}

削除されたオブジェクトについては、以前に patrick がセッションに存在しないことを示したときに、そのオブジェクトのIDも復元されます:

>>> patrick in session
True

もちろんデータベースのデータも存在します。:

>>> session.execute(select(User).where(User.name == "patrick")).scalar_one() is patrick
SELECT user_account.id, user_account.name, user_account.fullname FROM user_account WHERE user_account.name = ? [...] ('patrick',)
True

Closing a Session

上記のセクションでは、Pythonコンテキストマネージャの外で Session オブジェクトを使用しました。つまり、 with 文は使用しませんでした。これは問題ありませんが、この方法で行う場合は、 Session を終了したときに明示的に閉じるのが最善です。

>>> session.close()
ROLLBACK

Session を閉じることは、コンテキストマネージャでも同様に行われますが、以下のことを実現します。:

  • releases はすべての接続リソースを接続プールに解放`し、進行中のトランザクションをキャンセル(例えばロールバック)します。

    つまり、あるセッションを使っていくつかの読み取り専用のタスクを実行し、それを閉じた場合、トランザクションがロールバックされたことを確認するために Session.rollback() を明示的に呼び出す必要はありません。

  • Session からすべてのオブジェクトを 削除 します。

    つまり、この Session のためにロードしたPythonオブジェクトは、 sandypatricksquidward のように、すべて detached として知られる状態になります。特に、例えば Session.commit() の呼び出しによってまだ expired 状態にあったオブジェクトは、現在の行の状態を含んでおらず、更新されるデータベーストランザクションに関連付けられていないので、機能しなくなります:

    # note that 'squidward.name' was just expired previously, so its value is unloaded
    >>> squidward.name
    Traceback (most recent call last):
      ...
    sqlalchemy.orm.exc.DetachedInstanceError: Instance <User at 0x...> is not bound to a Session; attribute refresh operation cannot proceed

    デタッチされたオブジェクトは、同じ、または Session.add() メソッドを使って新しい Session に再度関連付けることができます。これにより、特定のデータベース行との関係が再確立されます。:

    >>> session.add(squidward)
    >>> squidward.name
    
    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 = ? [...] (4,)
    'squidward'

    Tip

    可能であれば、デタッチされた状態でオブジェクトを使用しないようにしてください。 Session が閉じられたら、以前にアタッチされたすべてのオブジェクトへの参照もクリーンアップします。デタッチされたオブジェクトが必要な場合、通常は、ビューがレンダリングされる前に Session が閉じられているWebアプリケーションのコミットされたばかりのオブジェクトをすぐに表示する場合は、 Session.expire_on_commit フラグを False に設定します。