JPAにおける悲観的ロック

JPAの悲観的ロック

1. 概要

データベースからデータを取得したい状況はたくさんあります。 他の人がアクションを中断できないように、さらに処理するために自分用にロックしたい場合があります。

これを可能にする2つの同時実行制御メカニズムを考えることができます。適切なトランザクション分離レベルの設定と、現時点で必要なデータのロックの設定です。

トランザクション分離は、データベース接続に対して定義されます。 さまざまな程度のロックデータを保持するように構成できます。

ただし、the isolation level is set once the connection is createdは、その接続内のすべてのステートメントに影響します。 幸いなことに、データベースメカニズムを使用してデータへのより詳細な排他的アクセスを予約する悲観的ロックを使用できます。

悲観的ロックを使用して、他のトランザクションが予約済みデータを変更または削除できないようにすることができます。

保持できるロックには、排他的ロックと共有ロックの2種類があります。 他の誰かが共有ロックを保持している場合、データを読み取ることはできますが、書き込むことはできません。 予約データを変更または削除するには、排他ロックが必要です。

SELECT … FOR UPDATE‘ステートメントを使用して排他ロックを取得できます。

2. ロックモード

JPA仕様では、これから説明する3つの悲観的なロックモードが定義されています。

  • PESSIMISTIC_READ –共有ロックを取得し、データの更新や削除を防ぐことができます

  • PESSIMISTIC_WRITE –排他ロックを取得し、データの読み取り、更新、または削除を防止できます

  • PESSIMISTIC_FORCE_INCREMENTPESSIMISTIC_WRITEのように機能し、バージョン管理されたエンティティのバージョン属性をさらにインクリメントします

それらはすべてLockModeTypeクラスの静的メンバーであり、トランザクションがデータベースロックを取得できるようにします。 これらはすべて、トランザクションがコミットまたはロールバックされるまで保持されます。

一度に取得できるロックは1つだけであることに注意してください。 不可能な場合は、PersistenceExceptionがスローされます。

2.1. PESSIMISTIC_READ

データを読み取るだけで、ダーティリードが発生しない場合は、PESSIMISTIC_READ(共有ロック)を使用できます。 We won’t be able to make any updates or deletes though.

使用するデータベースがPESSIMISTIC_READロックをサポートしていない場合があるため、代わりにPESSIMISTIC_WRITEロックを取得する可能性があります。

2.2. PESSIMISTIC_WRITE

データのロックを取得して変更する必要があるトランザクションは、PESSIMISTIC_WRITEロックを取得する必要があります。 JPAの仕様によると、PESSIMISTIC_WRITEロックを保持すると、他のトランザクションがデータを読み取ったり、更新したり、削除したりできなくなります。

一部のデータベースシステムはmulti-version concurrency controlを実装しているため、リーダーはすでにブロックされているデータを取得できます。

2.3. PESSIMISTIC_FORCE_INCREMENT

このロックはPESSIMISTIC_WRITEと同様に機能しますが、バージョン管理されたエンティティ(@Versionで注釈が付けられた属性を持つエンティティ)と連携するために導入されました。

バージョン管理されたエンティティの更新の前に、PESSIMISTIC_FORCE_INCREMENTロックを取得することができます。 Acquiring that lock results in updating the version column.

バージョン管理されていないエンティティに対してPESSIMISTIC_FORCE_INCREMENTをサポートするかどうかを決定するのは、永続性プロバイダー次第です。 そうでない場合は、PersistanceException.をスローします

2.4. 例外

ペシミスティックロックの操作中に発生する可能性のある例外を知っておくとよいでしょう。 JPA仕様は、さまざまなタイプの例外を提供します。

  • PessimisticLockException –ロックの取得または共有ロックから排他ロックへの変換が失敗し、トランザクションレベルのロールバックが発生することを示します

  • LockTimeoutException – は、ロックを取得するか、共有ロックを排他的タイムアウトに変換すると、ステートメントレベルのロールバックが発生することを示します。

  • PersistanceException –は、永続性の問題が発生したことを示します。 PersistanceExceptionとそのサブタイプ(NoResultExceptionNonUniqueResultException,LockTimeoutExceptionQueryTimeoutException, marks the active transaction to be rolled back.を除く)

3. 悲観的ロックの使用

There are a few possible ways to configure a pessimistic lock on a single record or group of records.JPAでそれを行う方法を見てみましょう。

3.1. Find

おそらく最も簡単な方法です。 LockModeTypeオブジェクトをパラメータとしてfindメソッドに渡すだけで十分です。

entityManager.find(Student.class, studentId, LockModeType.PESSIMISTIC_READ);

3.2. 問い合わせ

さらに、Queryオブジェクトを使用して、パラメーターとしてロックモードを指定してsetLockModeセッターを呼び出すこともできます。

Query query = entityManager.createQuery("from Student where studentId = :studentId");
query.setParameter("studentId", studentId);
query.setLockMode(LockModeType.PESSIMISTIC_WRITE);
query.getResultList()

3.3. 明示的なロック

findメソッドによって取得された結果を手動でロックすることもできます。

Student resultStudent = entityManager.find(Student.class, studentId);
entityManager.lock(resultStudent, LockModeType.PESSIMISTIC_WRITE);

3.4. 更新する

エンティティの状態をrefresh メソッドで上書きする場合は、次のようにロックを設定することもできます。

Student resultStudent = entityManager.find(Student.class, studentId);
entityManager.refresh(resultStudent, LockModeType.PESSIMISTIC_FORCE_INCREMENT);

3.5. NamedQuery

@NamedQuery注釈を使用すると、ロックモードも設定できます。

@NamedQuery(name="lockStudent",
  query="SELECT s FROM Student s WHERE s.id LIKE :studentId",
  lockMode = PESSIMISTIC_READ)

4. ロック範囲

Lock scope parameter defines how to deal with locking relationships of the locked entity.クエリで定義された単一のエンティティに対してのみロックを取得したり、その関係をさらにブロックしたりすることができます。

スコープを構成するには、PessimisticLockScope列挙型を使用できます。 これには、NORMALEXTENDEDの2つの値が含まれます。

EntityManagerQueryTypedQuery、または%の適切なメソッドへの引数としてPessimisticLockScope値を持つパラメータ 'javax.persistance.lock.scope'を渡すことで、スコープを設定できます。 (t5)s:

Map properties = new HashMap<>();
map.put("javax.persistence.lock.scope", PessimisticLockScope.EXTENDED);

entityManager.find(
  Student.class, 1L, LockModeType.PESSIMISTIC_WRITE, properties);

4.1. PessimisticLockScope.NORMAL

We should know that the PessimisticLockScope.NORMAL is the default scope.このロックスコープを使用して、エンティティ自体をロックします。 結合継承とともに使用すると、先祖もロックされます。

2つのエンティティを持つサンプルコードを見てみましょう。

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Person {

    @Id
    private Long id;
    private String name;
    private String lastName;

    // getters and setters
}

@Entity
public class Employee extends Person {

    private BigDecimal salary;

    // getters and setters
}

Employeeのロックを取得する場合、これら2つのエンティティにまたがるSQLクエリを確認できます。

SELECT t0.ID, t0.DTYPE, t0.LASTNAME, t0.NAME, t1.ID, t1.SALARY
FROM PERSON t0, EMPLOYEE t1
WHERE ((t0.ID = ?) AND ((t1.ID = t0.ID) AND (t0.DTYPE = ?))) FOR UPDATE

4.2. PessimisticLockScope.EXTENDED

EXTENDEDスコープは、NORMAL.と同じ機能をカバーします。さらに、it’s able to block related entities in a join table

簡単に言えば、@ElementCollectionまたは@OneToOne@OneToManyなどで注釈が付けられたエンティティで機能します。 @JoinTableで。

@ElementCollectionアノテーションが付いたサンプルコードを見てみましょう。

@Entity
public class Customer {

    @Id
    private Long customerId;
    private String name;
    private String lastName;
    @ElementCollection
    @CollectionTable(name = "customer_address")
    private List
addressList; // getters and setters } @Embeddable public class Address { private String country; private String city; // getters and setters }

Customerエンティティを検索するときにいくつかのクエリを分析してみましょう。

SELECT CUSTOMERID, LASTNAME, NAME
FROM CUSTOMER WHERE (CUSTOMERID = ?) FOR UPDATE

SELECT CITY, COUNTRY, Customer_CUSTOMERID
FROM customer_address
WHERE (Customer_CUSTOMERID = ?) FOR UPDATE

顧客テーブルの行と結合テーブルの行をロックする2つの ‘FOR UPDATE’クエリがあることがわかります。

注意すべきもう1つの興味深い事実は、すべての永続性プロバイダーがロックスコープをサポートしているわけではないということです。

5. ロックタイムアウトの設定

ロックスコープの設定に加えて、別のロックパラメーター(タイムアウト)を調整できます。 The timeout value is the number of milliseconds that we want to wait for obtaining a lock until the LockTimeoutException occurs.

プロパティ ‘javax.persistence.lock.timeout'を適切なミリ秒数で使用することにより、ロックスコープと同様にタイムアウトの値を変更できます。

タイムアウト値をゼロに変更することで、「待機なし」のロックを指定することもできます。 ただし、don’t support setting a timeout value this way.のデータベースドライバがあることに注意してください。

Map properties = new HashMap<>();
map.put("javax.persistence.lock.timeout", 1000L);

entityManager.find(
  Student.class, 1L, LockModeType.PESSIMISTIC_READ, properties);

6. 結論

適切な分離レベルを設定するだけでは同時トランザクションに対処できない場合、JPAは悲観的なロックを提供します。 これにより、異なるトランザクションを分離して調整できるため、同じリソースに同時にアクセスすることはありません。

これを実現するために、議論したロックの種類から選択し、その結果、スコープやタイムアウトなどのパラメーターを変更できます。

一方、データベースロックを理解することは、基礎となるデータベースシステムのメカニズムを理解することと同じくらい重要であることを覚えておく必要があります。 また、ペシミスティックロックの動作は、使用する永続性プロバイダーに依存することを覚えておくことが重要です。

最後に、このチュートリアルのソースコードは、GitHubでhibernateEclipseLinkで入手できます。