Overview
このガイドでは、MongoDB Extension for Hibernate ORMを使用して楽観的ロックを実行する方法を学ぶことができます。楽観的ロックを使用すると、複数のセッションが同じエンティティを読み取ることができます。その後、複数のセッションがそのエンティティに書き込み (write)を試みたときに競合が検出されます。
楽観的ロックを有効にすると、Hibernate ORM拡張機能は、各更新操作または削除する操作に対してMongoDB クエリ言語 (MQL)ステートメントを生成します。このステートメントは、プライマリキーに加えて、セッションが読み取ったバージョン値でフィルターをかけます。別のセッションが先にドキュメントを変更した場合、フィルターはどのドキュメントにも一致せず、Hibernate ORMはOptimisticLockExceptionをスローします。
Hibernate ORM拡張機能は、悲観的ロックをサポートしていません。悲観的ロックは、セッションがエンティティを読み取るときにそのエンティティをロックします。これにより、セッションがロックを解放するまで、他のセッションがそのエンティティを変更できなくなります。
バージョンフィールドを追加する
エンティティのオプティミスティック ロックを有効にするには、@Version で注釈が付けられたフィールドをエンティティクラスに追加します。
アプリケーションがバージョン値を読み取る場合は、そのフィールドのゲッターを定義しますが、セッターは定義しないでください。Hibernate ORMは値を自身で割り当てます。アプリケーションコードでフィールドを設定すると、Hibernate ORM拡張機能が保存されたドキュメントと一致しないフィルターを生成する可能性があります。
このガイドの例では、次の Product エンティティを使用します。これは products コレクションにマップされます。version フィールドは初期化しないでください。Hibernate ORM がエンティティを最初に永続化するときに初期値を割り当てるためです。
@Entity@Table(name = "products")public class Product { @Id @ObjectIdGenerator private ObjectId id; private String name; private int quantity;
@Version 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 から始まり、書き込み (write) ごとに 1 ずつ増加します。Instant バージョンフィールドには、書き込み (write) の時刻が保存 (する) されます。
バージョン管理されたエンティティの更新
新しいエンティティを永続化すると、 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 modifytry (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: Hibernate ORM拡張機能は、セッションが変更したフィールドの前の値のみをフィルターします。OptimisticLockType.ALL: Hibernetes ORM 拡張機能は、すべてのエンティティのフィールドの前の値をフィルタリングします。
どちらのタイプにも @DynamicUpdate アノテーションが必要です。
次の例では、DIRTY型を使用します。
@Entity@Table(name = "products")@OptimisticLocking(type = OptimisticLockType.DIRTY)@DynamicUpdatepublic class Product { @Id @ObjectIdGenerator private ObjectId id; private String name; private int quantity;
// Constructors, getters, and setters}バージョン管理されたエンティティでフィールドをオプティミスティック ロックから除外するには、フィールドに @OptimisticLock(excluded = true) で注釈を付けます。除外されたフィールドを変更しても、バージョンは増加しません。
@Entity@Table(name = "products")public class Product { @Id @ObjectIdGenerator private ObjectId id;
@OptimisticLock(excluded = true) private String name;
private int quantity;
@Version private Long version;
// Constructors, getters, and setters}制限
楽観的ロックには、次の制限があります。
@Versionフィールドを持つエンティティをStatelessSession.upsert()メソッドに渡すことはできません。 Hibernetes ORM 拡張機能はこの操作に対してUnsupportedFeatureExceptionをスローします。
詳細情報
詳細を学ぶには、 Hibernate ORM ドキュメントの 楽観的ロック を参照してください。
トランザクションでの書き込み (write)操作の実行について学ぶには、「トランザクションとセッション」ガイドをご覧ください。