State Management

Quickie Intro to Object States

セッション内でインスタンスが持つことのできる状態を知っておくと便利です。

  • Transient - an instance that’s not in a session, and is not saved to the database; i.e. it has no database identity. The only relationship such an object has to the ORM is that its class has a Mapper associated with it.

  • 一時的-セッション内になく、データベースに保存されないインスタンス。つまり、データベースIDを持たないインスタンス。このようなオブジェクトがORMに対して持つ唯一の関係は、そのクラスに関連付けられた Mapper があることです。

  • Pending-一時インスタンスを Session.add() すると、それはpendingになります。実際にはまだデータベースにフラッシュされていませんが、次のフラッシュが発生したときにフラッシュされます。

  • 永続的-セッションに存在し、データベースにレコードを持つインスタンス。永続的なインスタンスを取得するには、保留中のインスタンスが永続的になるようにフラッシュするか、既存のインスタンスをデータベースに照会します(または、永続的なインスタンスを他のセッションからローカル・セッションに移動します)。

  • Deleted-フラッシュ内で削除されたが、トランザクションがまだ完了していないインスタンス。この状態のオブジェクトは、基本的に”保留中”状態の反対です。セッションのトランザクションがコミットされると、オブジェクトはデタッチ状態に移動します。または、セッションのトランザクションがロールバックされると、削除されたオブジェクトは永続状態に*戻り*ます。

  • Detached-データベース内のレコードに対応する、または以前に対応していたが、現在どのセッションにも存在しないインスタンス。デタッチされたオブジェクトにはデータベースIDマーカーが含まれますが、セッションに関連付けられていないため、このデータベースIDがターゲットデータベース内に実際に存在するかどうかは不明です。デタッチされたオブジェクトは、アンロードされた属性や以前に「期限切れ」とマークされた属性をロードできないことを除いて、通常どおり使用しても安全です。

可能なすべての状態遷移の詳細については、 Object Lifecycle Events のセクションを参照してください。このセクションでは、各遷移と、各遷移をプログラムで追跡する方法について説明しています。

Getting the Current State of an Object

マップされたオブジェクトの実際の状態は、マップされたインスタンスで inspect() 関数を使っていつでも見ることができます;この関数は、オブジェクトの内部ORM状態を管理する対応する InstanceState オブジェクトを返します。 InstanceState は、他のアクセサの中でも、オブジェクトの持続状態を示す以下のようなブール属性を提供します:

例:

>>> from sqlalchemy import inspect
>>> insp = inspect(my_object)
>>> insp.persistent
True

See also

Inspection of Mapped Instances - further examples of InstanceState

Session Attributes

Session 自体は、セットのようなコレクションのように動作します。存在するすべての項目は、イテレータインタフェースを使ってアクセスできます:

for obj in session:
    print(obj)

また、通常の”contains”セマンティクスを使用してプレゼンスをテストすることもできます。:

if obj in session:
    print("Object is present")

セッションはまた、新しく作成された(つまり、保留中の)すべてのオブジェクト、最後にロードまたは保存されてから変更された(つまり、”ダーティ”)すべてのオブジェクト、および削除されたとマークされたすべてのものを追跡します:

# pending objects recently added to the Session
session.new

# persistent objects which currently have changes detected
# (this collection is now created on the fly each time the property is called)
session.dirty

# persistent objects that have been marked as deleted via session.delete(obj)
session.deleted

# dictionary of all persistent objects, keyed on their
# identity key
session.identity_map

(Documentation: Session.new , Session.dirty , Session.deleted , Session.identity_map ).

Session Referencing Behavior

セッション内のオブジェクトは*弱く参照*されます。これは、外部アプリケーションで参照解除されると、 Session 内からもスコープ外になり、Pythonインタプリタによるガベージコレクションの対象になることを意味します。例外として、保留中のオブジェクト、削除済みとマークされたオブジェクト、保留中の変更がある永続的なオブジェクトがあります。フルフラッシュの後、これらのコレクションはすべて空になり、すべてのオブジェクトは再び弱く参照されます。

Session 内のオブジェクトが強く参照され続けるようにするには、通常は単純な方法だけで十分です。外部で管理される強い参照の振る舞いの例としては、オブジェクトをその主キーをキーとするローカル辞書にロードしたり、参照され続ける必要がある期間のリストやセットにロードしたりすることがあります。これらのコレクションは、必要に応じて Session.info 辞書に配置することで、 Session に関連付けることができます。

イベントベースのアプローチも実現可能である。 persistent 状態にあるすべてのオブジェクトに対して”強い参照”動作を提供する簡単なレシピは次のとおりです:

from sqlalchemy import event

def strong_reference_session(session):
    @event.listens_for(session, "pending_to_persistent")
    @event.listens_for(session, "deleted_to_persistent")
    @event.listens_for(session, "detached_to_persistent")
    @event.listens_for(session, "loaded_as_persistent")
    def strong_ref_object(sess, instance):
        if "refs" not in sess.info:
            sess.info["refs"] = refs = set()
        else:
            refs = sess.info["refs"]

        refs.add(instance)

    @event.listens_for(session, "persistent_to_detached")
    @event.listens_for(session, "persistent_to_deleted")
    @event.listens_for(session, "persistent_to_transient")
    def deref_object(sess, instance):
        sess.info["refs"].discard(instance)

上記では、 SessionEvents.pending_to_persistent()SessionEvents.detached_to_persistent()SessionEvents.deleted_to_persistent() および SessionEvents.loaded_as_persistent() イベントフックをインターセプトして、オブジェクトが persistent 遷移に入るときにオブジェクトをインターセプトします。また、 SessionEvents.persistent_to_detached() および SessionEvents.persistent_to_deleted() フックをインターセプトして、オブジェクトが永続状態を離れるときにオブジェクトをインターセプトします。

上記の関数は、 Session ごとに強い参照動作を提供するために、任意の Session に対して呼び出すことができます:

from sqlalchemy.orm import Session

my_session = Session()
strong_reference_session(my_session)

任意の sessionmaker に対して呼び出すこともできます:

from sqlalchemy.orm import sessionmaker

maker = sessionmaker()
strong_reference_session(maker)

Merging

Session.merge() は、外部のオブジェクトからセッション内の新しいまたは既存のインスタンスに状態を転送します。また、受信データをデータベースの状態と照合して、次のフラッシュに適用される履歴ストリームを生成します。あるいは、変更履歴を生成したり、データベースにアクセスしたりせずに、状態の単純な「転送」を生成するようにすることもできます。使用方法は次のとおりです。:

merged_object = session.merge(existing_object)

インスタンスを指定する場合は、次の手順に従います。:

  • インスタンスの主キーを検査します。主キーが存在する場合は、ローカルIDマップ内でそのインスタンスを見つけようとします。 load=True フラグがデフォルトのままになっている場合は、この主キーがローカルに存在しない場合にもデータベースをチェックします。

  • 指定されたインスタンスに主キーがない場合、または指定された主キーでインスタンスが見つからない場合は、新しいインスタンスが作成されます。

  • 指定されたインスタンスの状態は、指定された/新しく作成されたインスタンスにコピーされます。ソースインスタンスに存在する属性値の場合、値はターゲットインスタンスに転送されます。ソースインスタンスに存在しない属性値の場合、ターゲットインスタンスの対応する属性はメモリから expired になります。これは、その属性のターゲットインスタンスからローカルに存在する値を破棄しますが、その属性のデータベースに保持されている値には直接変更は加えられません。

  • 操作は、merge カスケード( Cascades を参照)によって示されるように、関連するオブジェクトとコレクションにカスケードされます。

  • 新しいインスタンスが返されます。

Session.merge() では、与えられた”source”インスタンスは変更されず、ターゲットの Session に関連付けられることもなく、他の Session オブジェクトと任意の数だけマージすることができます。 Session.merge() は、その起源や現在のセッションの関連付けに関係なく、あらゆる種類のオブジェクト構造の状態を取得し、その状態を新しいセッションにコピーするのに便利です。以下に例を示します。

  • ファイルからオブジェクト構造を読み込んでデータベースに保存しようとするアプリケーションは、ファイルを解析して構造を構築し、 Session.merge() を使用してデータベースに保存します。これにより、ファイル内のデータが構造の各要素の主キーを形成するために使用されます。後でファイルが変更されたときに、同じプロセスを再実行してわずかに異なるオブジェクト構造を生成し、それを再び merged することができます。 Session は、これらの変更を反映するようにデータベースを自動的に更新し、主キーによってデータベースから各オブジェクトを読み込み、与えられた新しい状態でその状態を更新します

  • アプリケーションは、多くの Session オブジェクトによって同時に共有されるインメモリキャッシュにオブジェクトを保存しています。 Session.merge() は、オブジェクトがキャッシュから取得されるたびに使用され、それを要求する各 Session 内にそのローカルコピーを作成します。キャッシュされたオブジェクトはデタッチされたままになり、その状態のみが個々の Session オブジェクトに対してローカルな自身のコピーに移動されます。

    キャッシュのユースケースでは、オブジェクトの状態をデータベースと一致させるオーバーヘッドを取り除くために、 load=False フラグを使用するのが一般的です。また、キャッシュ拡張された Query オブジェクトで動作するように設計された Session.merge() の”バルク”バージョンである Query.merge_result() もあります。 Dogpile Caching の節を参照してください。

  • アプリケーションが、一連のオブジェクトの状態を、ワーカスレッドまたは他の並行システムによって維持される Session に転送しようとしています。 Session.merge() は、この新しい Session に配置される各オブジェクトのコピーを作成します。操作の最後に、親スレッド/プロセスは開始したオブジェクトを維持し、スレッド/ワーカはそれらのオブジェクトのローカルコピーを処理できます。

    “スレッド/プロセス間の転送”のユースケースでは、データの転送時にオーバーヘッドや冗長なSQLクエリが発生しないように、アプリケーションで load=False フラグを使用することもできます。

Merge Tips

Session.merge() は多くの目的で非常に便利なメソッドです。しかし、一時的/切り離されたオブジェクトと永続的なオブジェクトの間の複雑な境界や、状態の自動転送を処理します。ここで提示されるさまざまなシナリオでは、オブジェクトの状態に対してより慎重なアプローチが必要になることがよくあります。マージに関する一般的な問題には、通常、 Session.merge() に渡されるオブジェクトに関する予期しない状態が含まれます。

UserオブジェクトとAddressオブジェクトの標準的な例を使用しましょう。:

class User(Base):
    __tablename__ = "user"

    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50), nullable=False)
    addresses = relationship("Address", backref="user")

class Address(Base):
    __tablename__ = "address"

    id = mapped_column(Integer, primary_key=True)
    email_address = mapped_column(String(50), nullable=False)
    user_id = mapped_column(Integer, ForeignKey("user.id"), nullable=False)

すでに永続化されている1つの Address を持つ User オブジェクトを想定します。:

>>> u1 = User(name="ed", addresses=[Address(email_address="ed@ed.com")])
>>> session.add(u1)
>>> session.commit()

ここで、セッション外のオブジェクトである a1 を作成し、既存の Address の上にマージします。:

>>> existing_a1 = u1.addresses[0]
>>> a1 = Address(id=existing_a1.id)

こんなことを言ったらびっくりします:

>>> a1.user = u1
>>> a1 = session.merge(a1)
>>> session.commit()
sqlalchemy.orm.exc.FlushError: New instance <Address at 0x1298f50>
with identity key (<class '__main__.Address'>, (1,)) conflicts with
persistent instance <Address at 0x12a25d0>

それはなぜですか?私たちはカスケードに注意を払っていませんでした。永続的なオブジェクトへの a1.user の割り当ては、 User.addresses のbackrefにカスケードされ、 a1 オブジェクトを追加したかのように保留にしました。これで、セッションに Address オブジェクトが*2*個できました:

>>> a1 = Address()
>>> a1.user = u1
>>> a1 in session
True
>>> existing_a1 in session
True
>>> a1 is existing_a1
False

上の例では、 a1 はすでにセッションで保留されています。その後の Session.merge() 操作は基本的に何もしません。カスケードは relationship()relationship.cascade オプションで設定できますが、この場合は User.addresses 関係から save-update カスケードを削除することになります。通常、この動作は非常に便利です。ここでの解決策は、通常、ターゲットセッションですでに永続的なオブジェクトに a1.user を割り当てないことです。

また、 relationship()cascade_backrefs=False オプションは、 a1.user=u1 の割り当てによって Address がセッションに追加されるのを防ぎます。

カスケード操作の詳細については Cascades を参照してください。

予期しない状態の別の例:

>>> a1 = Address(id=existing_a1.id, user_id=u1.id)
>>> a1.user = None
>>> a1 = session.merge(a1)
>>> session.commit()
sqlalchemy.exc.IntegrityError: (IntegrityError) address.user_id
may not be NULL

上記では、 user の割り当てが user_id の外部キー割り当てよりも優先され、その結果、 user_idNone が適用されて失敗します。

ほとんどの Session.merge() の問題は、最初にチェックすることで調べられます-オブジェクトがセッション内に存在していますか?

>>> a1 = Address(id=existing_a1, user_id=user.id)
>>> assert a1 not in session
>>> a1 = session.merge(a1)

それとも、オブジェクトに必要のない状態があるのでしょうか? __dict__ を調べると簡単にチェックできます:

>>> a1 = Address(id=existing_a1, user_id=user.id)
>>> a1.user
>>> a1.__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x1298d10>,
    'user_id': 1,
    'id': 1,
    'user': None}
>>> # we don't want user=None merged, remove it
>>> del a1.user
>>> a1 = session.merge(a1)
>>> # success
>>> session.commit()

Expunging

expungeはセッションからオブジェクトを削除し、永続インスタンスをデタッチ状態に、保留中のインスタンスを一時状態に送信します。

session.expunge(obj1)

すべての項目を削除するには、 Session.expunge_all() を呼び出します(このメソッドは以前は clear() と呼ばれていました)。

Refreshing / Expiring

Expiring は、一連のオブジェクト属性内に保持されているデータベース永続化データが消去されることを意味します。これは、これらの属性が次にアクセスされたときに、データベースからそのデータを更新するSQLクエリが発行されるような方法です。

データの有効期限について話すとき、通常は persistent 状態にあるオブジェクトについて話します。例えば、次のようにオブジェクトをロードしたとします。:

user = session.scalars(select(User).filter_by(name="user1").limit(1)).first()

上記の User オブジェクトは永続的で、一連の属性が存在します。その __dict__ の内部を見ると、その状態がロードされていることがわかります。:

>>> user.__dict__
{
  'id': 1, 'name': u'user1',
  '_sa_instance_state': <...>,
}

ここで、 idname はデータベース内のこれらの列を参照します。 _sa_instance_state はデータベースに保持されない値で、SQLAlchemyによって内部的に使用されます(インスタンスの InstanceState を参照します。このセクションに直接関係はありませんが、アクセスしたい場合は inspect() 関数を使用してください)。

この時点で、User オブジェクトの状態は、ロードされたデータベース行の状態と一致します。しかし、 Session.expire() などのメソッドを使ってオブジェクトを期限切れにすると、状態が削除されていることがわかります。

>>> session.expire(user)
>>> user.__dict__
{'_sa_instance_state': <...>}

内部の「状態」はまだ残っていますが、 idname 列に対応する値はなくなっています。これらの列の1つにアクセスしてSQLを監視していると、次のように表示されます。:

>>> print(user.name)
SELECT user.id AS user_id, user.name AS user_name FROM user WHERE user.id = ? (1,)
user1

上記では、期限切れの属性 user.name``にアクセスすると、ORMは :term:`lazy load` を開始して、このユーザが参照するユーザ行に対してSELECTを発行することで、データベースから最新の状態を取得します。その後、 ``__dict__ に再び値が設定されます。:

>>> user.__dict__
{
  'id': 1, 'name': u'user1',
  '_sa_instance_state': <...>,
}

Note

SQLAlchemyがオブジェクト属性に対して何をしているかを少し見るために、私たちが __dict__ の内部を覗いている間は、少なくともSQLAlchemyのORMが保持している属性に関しては、 __dict__ の内容を直接変更**すべきではありません(SQLAのレルム外の他の属性は問題ありません)。これは、SQLAlchemyがオブジェクトに対して行った変更を追跡するために:term:descriptors`を使用しており、 ``__dict__` を直接変更した場合、ORMは私たちが何かを変更したことを追跡できないからです。

Session.expire()Session.refresh() のもう1つの重要な動作は、オブジェクトに対するフラッシュされていない変更がすべて破棄されることです。つまり、 User の属性を変更する場合です。:

>>> user.name = "user2"

しかし、最初に Session.flush() を呼び出さずに Session.expire() を呼び出すと、保留中の値である 'user2' は破棄されます。:

>>> session.expire(user)
>>> user.name
'user1'

Session.expire() メソッドを使用して、インスタンスのORMマップされたすべての属性を”期限切れ”としてマークできます:

# expire all ORM-mapped attributes on obj1
session.expire(obj1)

期限切れとマークされる特定の属性を参照する文字列属性名のリストを渡すこともできます:

# expire only attributes obj1.attr1, obj1.attr2
session.expire(obj1, ["attr1", "attr2"])

Session.expire_all() メソッドを使うと、 Session に含まれるすべてのオブジェクトに対して Session.expire() を一度に呼び出すことができます:

session.expire_all()

Session.refresh() メソッドも似たようなインタフェースを持っていますが、期限切れにする代わりに、オブジェクトの行に対して即座にSELECTを発行します:

# reload all attributes on obj1
session.refresh(obj1)

Session.refresh() も文字列属性名のリストを受け付けますが、 Session.expire() とは異なり、少なくとも1つの名前が列にマップされた属性の名前であることを期待します:

# reload obj1.attr1, obj1.attr2
session.refresh(obj1, ["attr1", "attr2"])

Tip

より柔軟なリフレッシュの別の方法は、ORMの Populate Existing 機能を使用することです。これは select() を使用した 2.0 style クエリや、 1.x style クエリ内の QueryQuery.populate_existing() メソッドから利用できます。この実行オプションを使用すると、文の結果セットに返されるすべてのORMオブジェクトがデータベースからのデータでリフレッシュされます:

stmt = (
    select(User)
    .execution_options(populate_existing=True)
    .where((User.name.in_(["a", "b", "c"])))
)
for user in session.execute(stmt).scalars():
    print(user)  # will be refreshed for those columns that came back from the query

See Populate Existing for further detail.

What Actually Loads

Session.expire() でマークされたオブジェクトや Session.refresh() でロードされたオブジェクトに対して発行されるSELECT文は、以下のようないくつかの要因によって変化します。

  • 期限切れ属性のロードは、 列にマップされた属性のみ からトリガされます。 relationship() にマップされた属性を含め、あらゆる種類の属性を期限切れとしてマークすることができますが、期限切れの relationship() 属性にアクセスすると、標準的な関係指向の遅延ロードを使用して、その属性に対してのみロードが発生します。列指向の属性は、期限切れであっても、この操作の一部としてはロードされず、代わりに列指向の属性がアクセスされたときにロードされます。

  • relationship() - マップされた属性は、期限切れの列ベースの属性がアクセスされてもロードされません。

  • 関係に関しては、 Session.refresh()Session.expire() よりも列にマップされていない属性に関して制限があります。 Session.refresh() を呼び出し、関係にマップされた属性のみを含む名前のリストを渡すと、実際にはエラーが発生します。いずれにしても、非eager-loading relationship() 属性はどの更新操作にも含まれません。

  • relationship.lazy パラメータを介して”eager loading”として設定された relationship() 属性は、 Session.refresh() の場合、属性名が指定されていないか、その名前が更新される属性のリストに含まれている場合にロードされます。

  • deferred() として設定された属性は、expired-attributeのロード中も更新中も、通常はロードされません。代わりに、 deferred() でアンロードされた属性は、直接アクセスされたとき、またはそのグループ内のアンロードされた属性がアクセスされたときに、遅延属性の「グループ」の一部である場合に、単独でロードされます。

  • アクセス時にロードされる期限切れの属性の場合、結合継承テーブル・マッピングは、通常、アンロードされた属性が存在するテーブルのみを含むSELECTを発行します。ここでのアクションは、親テーブルまたは子テーブルのみをロードするのに十分洗練されています。たとえば、最初に期限切れになった列のサブセットが、これらのテーブルのいずれか1つしか含まない場合などです。

  • Session.refresh() が結合継承テーブルマッピングで使用された場合、出力されるSELECTは Session.query() がターゲットオブジェクトのクラスで使用された場合と似たものになります。通常、これはマッピングの一部として設定されたすべてのテーブルです。

When to Expire or Refresh

Session は、セッションが参照するトランザクションが終了すると、自動的にexpiration機能を使用します。つまり、 Session.commit() または Session.rollback() が呼び出されると、 Session 内のすべてのオブジェクトが、 Session.expire_all() メソッドと同等の機能を使用して期限切れになります。その理論的根拠は、トランザクションの終了は、データベースの現在の状態が何であるかを知るために利用可能なコンテキストがもはや存在しない境界点であり、他のいくつかのトランザクションがそれに影響を与えている可能性があるということです。新しいトランザクションが開始されたときにのみ、データベースの現在の状態に再びアクセスすることができ、その時点でいくつかの変更が発生している可能性があります。

Session.expire() メソッドと Session.refresh() メソッドは、オブジェクトのデータをデータベースから強制的に再ロードしたい場合や、データの現在の状態が古い可能性があることがわかっている場合に使用されます。これには次のような理由が考えられます。

  • Table.update() 構文が Session.execute() メソッドを使用して発行された場合など、一部のSQLがORMのオブジェクト処理の範囲外のトランザクション内で発行されました。

  • アプリケーションが、同時実行中のトランザクションで変更されたことがわかっているデータを取得しようとしているが、有効な分離ルールによってこのデータを表示できることもわかっている場合。

2番目の箇条書きには、「有効な分離規則によってこのデータを表示できることも知られている」という重要な警告があります。これは、別のデータベース接続で発生したUPDATEが、ここでローカルに表示されるとは想定できないことを意味します。多くの場合、表示されません。これが、進行中のトランザクション間のデータを表示するために Session.expire() または Session.refresh() を使用したい場合、有効な分離動作を理解することが不可欠である理由です。

See also

Session.expire()

Session.expire_all()

Session.refresh()

Populate Existing - 任意のORMクエリが通常ロードされるようにオブジェクトを更新し、SELECT文の結果に対してIDマップ内の一致するすべてのオブジェクトを更新できるようにします。

isolation - Wikipediaへのリンクを含む、独立性に関する用語集の説明です。

The SQLAlchemy Session In-Depth - データ有効期限の役割を含むオブジェクトライフサイクルの詳細な議論を含むビデオ+スライド。