Overview
このガイドでは、 Hibernetes ORM 用のMongoDB拡張機能 を使用してオプティミスティックロックを実行する方法を学習します。楽観的なロックにより、同時セッションで同じエンティティを読み取ることができ、複数のセッションがそのエンティティに書込みを試みたときに競合を検出します。
オプティミスティックロックをオンにすると、 Hibernetes ORM 拡張機能はMongoDBクエリ言語 (MQL) ステートメントをアップデートまたは削除操作ごとに生成します。ステートメントは、プライマリキーに加えて、セッションが読み取ったバージョン値でフィルタリングします。別のセッションが最初にドキュメントを変更した場合、フィルターはどのドキュメントとも一致せず、 Hibernetes ORM は OptimisticLockException をスローします。
Hibernetes ORM 拡張機能は悲観的なロックをサポートしていません。悲観的なロックでは、セッションが読み取り時にエンティティをロックし、セッションがロックを解放するまで、他のセッションがエンティティを変更することをブロックします。
バージョン フィールドの追加
エンティティのオプティミスティック ロックを有効にするには、@Version で注釈が付けられたフィールドをエンティティクラスに追加します。
アプリケーションがバージョン値を読み取る場合は、フィールドの getter を定義しますが、setter を定義しません。非表示 ORM は値自体を割り当て、アプリケーションコードでフィールドを設定すると、非表示 ORM 拡張機能が保存されたドキュメントと一致しないフィルターを生成する可能性があります。
このガイドの例では、productsコレクションにマップされる次の Product エンティティを使用します。非表示の ORM は最初にエンティティを永続化するときに初期値を割り当てるため、 versionフィールドを初期化しないでください。
public class Product { private ObjectId id; private String name; private int quantity; private Long version; public Product() { } public Product(String name, int quantity) { this.name = name; this.quantity = quantity; } public ObjectId getId() { return id; } public Long getVersion() { return version; } public void setQuantity(int quantity) { this.quantity = quantity; } }
Hibernetes ORM 拡張機能は、@Versionフィールドに対して次のタイプをサポートします。
intおよびIntegerlongおよびLongInstant
数値バージョンフィールドは0 から始まり、書き込みごとに 1 ずつ増加します。 Instant バージョンフィールドには、書込み時間が保存されます。
バージョン管理されたエンティティの更新
新しいエンティティを永続化すると、 Hibernetes ORM はバージョンフィールドを 0 に設定します。エンティティを変更してトランザクションをコミットすると、 Hibernetes ORM 拡張機能は 、セッションが読み取ったプライマリキーとバージョン値の両方でフィルタリングするアップデート ステートメントを生成します。このステートメントでは新しいバージョンの値も設定されます。
次の例では、新しい Product エンティティを挿入し、その quantityフィールドを更新してバージョンを増やします。
var sf = HibernateUtil.getSessionFactory(); try (Session session = sf.openSession()) { // Insert a new product, which sets the version to 0 session.beginTransaction(); var product = new Product("notebook", 100); session.persist(product); session.getTransaction().commit(); System.out.println("Version after insert: " + product.getVersion()); // Update the same product, which increments the version to 1 session.beginTransaction(); product.setQuantity(75); session.getTransaction().commit(); System.out.println("Version after update: " + product.getVersion()); } sf.close();
前の例の更新では、次のMQLのようなステートメントが生成されます。
{ "update": "products", "updates": [ { "q": { "$and": [ { "_id": { "$eq": "..." } }, { "version": { "$eq": 0 } } ] }, "u": { "$set": { "quantity": 75, "version": 1 } } } ] }
削除 操作では同じフィルターが使用されます。バージョン値は フィルターの一部であるため、セッションが読み取ったバージョンをドキュメントがまだ保持している場合にのみ書込み (write) は成功します。
バージョン競合の処理
セッションがドキュメントを読み込んだ後に別のドキュメントが変更された場合、フィルターのバージョン値は保存された値と一致しなくなり、非表示 ORM は OptimisticLockException をスローします。回復するには、例外をキャッチしてエンティティを再ロードして書き込みを再試行するか、アプリケーションのユーザーに競合を通知するエラーを返します。
次の例では、2 つのセッションを使用して同じドキュメントを読み取ります。 2 番目のセッションが最初にコミットされるため、最初のセッションのコミットでは OptimisticLockException がスローされます。
var sf = HibernateUtil.getSessionFactory(); ObjectId productId; // Insert a new product for two sessions to modify try (Session session = sf.openSession()) { session.beginTransaction(); var product = new Product("notebook", 100); session.persist(product); session.getTransaction().commit(); productId = product.getId(); } try (Session sessionA = sf.openSession(); Session sessionB = sf.openSession()) { // Both sessions load the product at version 0 var productA = sessionA.find(Product.class, productId); var productB = sessionB.find(Product.class, productId); // Session B commits first and increments the version to 1 sessionB.beginTransaction(); productB.setQuantity(75); sessionB.getTransaction().commit(); // Session A commits second, but still holds version 0 try { sessionA.beginTransaction(); productA.setQuantity(50); sessionA.getTransaction().commit(); } catch (OptimisticLockException e) { System.out.println("Update failed: Another session modified this document"); // Reload the entity and retry, or report the conflict to the user } } sf.close();
注意
アプリケーションがエンティティをロードする方法によっては、 Hibernetes ORM が代わりに StaleObjectStateException をスローする場合があります。この例外は、OptimisticLockException の Hibernetes ORM ネイティブ 同等です。
データを変更せずにバージョンを増やす
セッションで変更されなかったエンティティのバージョンを増やすには、エンティティと LockMode.OPTIMISTIC_FORCE_INCREMENTロックモードをSession の lock() メソッドに渡します。関連データへの変更によってエンティティの他のセッションのコピーを無効にする必要がある場合は、このロックモードを使用します。
次の例では、Productドキュメントのバージョンを増加させます。
var sf = HibernateUtil.getSessionFactory(); try (Session session = sf.openSession()) { session.beginTransaction(); var product = session.createQuery("from Product where name = :n", Product.class) .setParameter("n", "notebook") .setMaxResults(1) .getSingleResult(); session.lock(product, LockMode.OPTIMISTIC_FORCE_INCREMENT); session.getTransaction().commit(); } sf.close();
別のセッションが最初にバージョンを増加させた場合、コミットは OptimisticLockException をスローします。
バージョン フィールドなしのロック
バージョンフィールドを追加せずに競合を検出することもできます。そのためには、エンティティを @OptimisticLocking で注釈を付け、次のいずれかのタイプを指定します。
OptimisticLockType.DIRTY: Hibernetes ORM 拡張機能は、セッションが変更したフィールドの以前の値のみをフィルタリングします。OptimisticLockType.ALL: Hibernetes ORM 拡張機能は、すべてのエンティティのフィールドの前の値をフィルタリングします。
どちらのタイプにも @DynamicUpdate アノテーションが必要です。
次の例ではDIRTY 型を使用します。
public class Product { private ObjectId id; private String name; private int quantity; // Constructors, getters, and setters }
バージョン管理されたエンティティでフィールドをオプティミスティック ロックから除外するには、フィールドに @OptimisticLock(excluded = true) で注釈を付けます。除外されたフィールドを変更しても、バージョンは増加しません。
public class Product { private ObjectId id; private String name; private int quantity; private Long version; // Constructors, getters, and setters }
制限
楽観的なロックには、次の制限があります。
@Versionフィールドを持つエンティティをStatelessSession.upsert()メソッドに渡すことはできません。 Hibernetes ORM 拡張機能はこの操作に対してUnsupportedFeatureExceptionをスローします。
詳細情報
オプティミスティック ロックの詳細については、 Hibernet ORM ドキュメントの「 オプティミスティック ロック 」を参照してください。
トランザクションで書込み (write) 操作を実行中方法の詳細については、 「トランザクションとセッションに関するガイド」を参照してください。