Data Manipulation with the ORM¶
前のセクション Working with Data では、主要なSQLステートメント構造全体にわたって連続性を提供するために、コアの観点からSQL式言語に焦点を当てました。次に、このセクションでは Session
のライフサイクルと、これらの構造との相互作用について説明します。
前提となるセクション - このチュートリアルのORMに焦点を当てた部分は、このドキュメントの前の2つのORM中心のセクションに基づいています。
Executing with an ORM Session- ORM:class:_orm.Session オブジェクトの作成方法を紹介します
Using ORM Declarative Forms to Define Table Metadata - ここでは、
User
エンティティとAddress
エンティティのORMマッピングを設定します。
Selecting ORM Entities and Columns -
User
のようなエンティティに対してSELECT文を実行する方法の例
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¶
Session
は unit 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文を発行しました。このトランザクションは、 Session
の Session.commit()
、 Session.rollback()
、 Session.close()
のいずれかのメソッドを呼び出すまで、 開いたまま なります。
Session.flush()
は現在のトランザクションに対して保留中の変更を手動でプッシュするために使われることがありますが、 Session
は autoflush として知られている振る舞いを特徴としており、これについては後で説明します。また、 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
上記の操作は、進行中のトランザクションをコミットします。これまで扱ってきたオブジェクトは、 Session
に attached のままです。これは、 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文は、変更された個々のオブジェクトに対応する主キーごとに発行されます。
ユーザ名 sandy
の User
オブジェクトをトランザクションにロードしたとします( 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の振る舞いでは、フラッシュが行われるまで patrick
は Session
にとどまります。フラッシュは前述のように問い合わせを発行した場合に行われます。
>>> 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 Statements と Using 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
オブジェクトの .fullname
を Sandy 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オブジェクトは、sandy
、patrick
、squidward
のように、すべて 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
に設定します。