Configuring a Version Counter¶
Mapper
は version id column の管理をサポートしています。これは、マップされたテーブルに対して UPDATE
が発生するたびに値をインクリメントしたり更新したりする単一のテーブル列です。この値は、メモリに保持されている値がデータベースの値と一致することを保証するために、ORMが行に対して UPDATE
または DELETE
を発行するたびにチェックされます。
Warning
バージョン管理機能はオブジェクトの in memory レコードの比較に依存しているため、この機能は Session.flush()
プロセスにのみ適用されます。このプロセスでは、ORMが個々のメモリ内の行をデータベースにフラッシュします。 Query.update()
または Query.delete()
メソッドを使用して複数行のUPDATEまたはDELETEを実行する場合は 有効になりません 。これらのメソッドはUPDATEまたはDELETE文を発行するだけで、影響を受ける行の内容には直接アクセスできないからです。
この機能の目的は、2つの同時実行中のトランザクションがほぼ同時に同じ行を変更していることを検出すること、あるいは、更新せずに前のトランザクションからのデータを再利用している可能性のあるシステム内の「古い」行の使用に対するガードを提供することです(例えば、 Session
で expire_on_commit=False
を設定すると、前のトランザクションからのデータを再利用できます)。
Simple Version Counting¶
バージョンを追跡する最も簡単な方法は、マップされたテーブルに整数の列を追加し、それをマッパーオプション内で version_id_col
として設定することです。:
class User(Base):
__tablename__ = "user"
id = mapped_column(Integer, primary_key=True)
version_id = mapped_column(Integer, nullable=False)
name = mapped_column(String(50), nullable=False)
__mapper_args__ = {"version_id_col": version_id}
Note
version_id
列をNOT NULLにすることを 強く推奨します 。バージョン管理機能は、 バージョン管理列にNULL値をサポートしていません 。
上の例では、 User
のマッピングは version_id
列を使って整数のバージョンを追跡しています。 User
型のオブジェクトが最初にフラッシュされると、 version_id
列には”1”という値が与えられます。その後、テーブルのUPDATEは常に次のような方法で行われます。
UPDATE user SET version_id=:version_id, name=:name
WHERE user.id = :user_id AND user.version_id = :user_version_id
-- {"name": "new name", "version_id": 2, "user_id": 1, "user_version_id": 1}
上記のUPDATE文は、 user.id = 1
に一致する行を更新するだけでなく、 user.version_id = 1
を要求しています。ここで「1」は、このオブジェクトで使用されていることがわかっている最後のバージョン識別子です。他のトランザクションが独立して行を変更した場合、このバージョンIDは一致しなくなり、UPDATE文は一致する行がないことを報告します。これは、SQLAlchemyがテストする条件であり、正確に1行がUPDATE(またはDELETE)文に一致したことを示します。0行が一致する場合は、データのバージョンが古いことを示し、 StaleDataError
が発生します。
Custom Version Counters / Types¶
バージョニングには、他の種類の値やカウンタを使用することもできます。一般的な型には、日付やGUIDなどがあります。別の型やカウンタのスキームを使用する場合、SQLAlchemyは、バージョン生成の呼び出し可能なものを受け入れる version_id_generator
引数を使用して、このスキームのフックを提供します。この呼び出し可能なものには、現在の既知のバージョンの値が渡され、後続のバージョンを返すことが期待されます。
例えば、ランダムに生成されたGUIDを使って User
クラスのバージョンを追跡したい場合、以下のようにすることができます(一部のバックエンドはネイティブなGUID型をサポートしていますが、ここでは単純な文字列を使って説明します):
import uuid
class User(Base):
__tablename__ = "user"
id = mapped_column(Integer, primary_key=True)
version_uuid = mapped_column(String(32), nullable=False)
name = mapped_column(String(50), nullable=False)
__mapper_args__ = {
"version_id_col": version_uuid,
"version_id_generator": lambda version: uuid.uuid4().hex,
}
パーシステンスエンジンは、 User
オブジェクトがINSERTまたはUPDATEの対象となるたびに、 uuid.uuid4()
を呼び出します。この場合、 uuid4()
関数は前提条件となる値を持たない識別子を生成するので、バージョン生成関数は version
の入力値を無視できます。数値や特殊文字システムなどの連続的なバージョン指定スキームを使用している場合は、後続の値を決定するために指定された version
を使用できます。
See also
Server Side Version Counters¶
上記のマッピングでは、ORMはバージョンIDカウンタの新しい値を自動的に提供するために xmin
列に依存します。
ORMは通常、INSERTやUPDATEを発行しても、データベースが生成した値を積極的に取得しません。代わりに、これらの列を”expired”のままにしておき、次にアクセスされたときに取得します。ただし、 eager_defaults
Mapper
フラグが設定されている場合を除きます。ただし、サーバ側のバージョン列が使用されている場合、ORMは新たに生成された値を積極的に取得する必要があります。これは、同時実行中のトランザクションがバージョンカウンタを再度更新する*前*にバージョンカウンタを設定するためです。この取得は、 RETURNING を使用してINSERT文またはUPDATE文の中で同時に行うのが最善です。そうでなければ、後でSELECT文を発行した場合、バージョンカウンタが取得前に変更される可能性がある競合状態がまだ存在します。
ターゲットデータベースがRETURNINGをサポートしている場合、 User
クラスのINSERT文は次のようになります。:
INSERT INTO "user" (name) VALUES (%(name)s) RETURNING "user".id, "user".xmin
-- {'name': 'ed'}
上記の場合、ORMは、サーバが生成したバージョン識別子とともに、新たに生成された任意のプライマリキー値を1つの文で取得できます。バックエンドがRETURNINGをサポートしていない場合、追加のSELECTを すべての INSERTとUPDATEに対して発行する必要があります。これは非常に効率が悪く、バージョンカウンタが欠落する可能性もあります。
INSERT INTO "user" (name) VALUES (%(name)s)
-- {'name': 'ed'}
SELECT "user".version_id AS user_version_id FROM "user" where
"user".id = :param_1
-- {"param_1": 1}
サーバ側のバージョンカウンタは、絶対に必要な場合にのみ、また RETURNING をサポートするバックエンド(現在はPostgreSQL、Oracle、MariaDB 10.5、SQLite 3.35、SQL Server)でのみ使用することを*強く推奨*します。
Programmatic or Conditional Version Counters¶
バージョンカウンタをインクリメントせずに User
オブジェクトを更新することもできます。カウンタの値は変更されず、UPDATE文は以前の値に対してチェックを続けます。これは、UPDATEの特定のクラスだけが同時実行性の問題に敏感であるようなスキームで有用です。:
# will leave version_uuid unchanged
u1.name = "u3"
session.commit()