web-dev-qa-db-ja.com

JPA / Hibernateを使用したステートレスアプリケーションでの楽観的ロック

特定のバージョンのエンティティインスタンスをリクエスト間で維持できないシステムでオプティミスティックロック(楽観的同時実行制御)を実装する最良の方法は何でしょうか。これは実際にはかなり一般的なシナリオですが、ほとんどすべての例は、リクエスト間で読み込まれたエンティティを(httpセッションで)保持するアプリケーションに基づいています。

API汚染をできるだけ少なくして楽観的ロックを実装するにはどうすればよいですか?

制約

  • このシステムは、ドメイン駆動設計の原則に基づいて開発されています。
  • クライアント/サーバーシステム
  • エンティティインスタンスはリクエスト間で保持できません(可用性とスケーラビリティの理由から)。
  • 技術的な詳細により、ドメインのAPIをできるだけ汚染しないようにする必要があります。

スタックは、関連性がある場合、JPA(Hibernate)を備えたSpringです。

_@Version_のみの使用に関する問題

多くのドキュメントでは、フィールドを_@Version_で装飾するだけでよく、JPA/Hibernateは自動的にバージョンをチェックします。しかし、これは、更新が同じインスタンスを変更するまで、ロードされたオブジェクトと現在のバージョンがメモリに保持されている場合にのみ機能します。

ステートレスアプリケーションで_@Version_を使用するとどうなりますか。

  1. クライアントAは_id = 1_でアイテムをロードし、Item(id = 1, version = 1, name = "a")を取得します
  2. クライアントBは_id = 1_でアイテムをロードし、Item(id = 1, version = 1, name = "a")を取得します
  3. クライアントAはアイテムを変更してサーバーに送り返します:Item(id = 1, version = 1, name = "b")
  4. サーバーはEntityManagerでアイテムをロードし、Item(id = 1, version = 1, name = "a")を返します。nameを変更し、Item(id = 1, version = 1, name = "b")を永続化します。 Hibernateはバージョンを_2_にインクリメントします。
  5. クライアントBはアイテムを変更してサーバーに送り返します:Item(id = 1, version = 1, name = "c")
  6. サーバーはEntityManagerでアイテムをロードし、Item(id = 1, version = 2, name = "b")を返します。nameを変更し、Item(id = 1, version = 2, name = "c")を永続化します。 Hibernateはバージョンを_3_にインクリメントします。 一見矛盾はありません!

手順6でわかるように、問題はEntityManagerが更新の直前にその時点の現在のバージョン(_version = 2_)のアイテムをリロードすることです。クライアントBが_version = 1_で編集を開始したという情報は失われ、Hibernateは競合を検出できません。クライアントBによって実行される更新要求は、代わりにItem(id = 1, version = 1, name = "b")を永続化する必要があります(_version = 2_ではありません)。

JPA/Hibernateが提供する自動バージョンチェックは、最初のGETリクエストでロードされたインスタンスがサーバー上のある種のクライアントセッションで存続し、後でそれぞれのクライアントによって更新される場合にのみ機能します。ただし、statelessサーバーでは、クライアントからのバージョンを何らかの方法で考慮する必要があります。

可能な解決策

明示的なバージョンチェック

アプリケーションサービスのメソッドで明示的なバージョンチェックを実行できます。

_@Transactional
fun changeName(dto: ItemDto) {
    val item = itemRepository.findById(dto.id)
    if (dto.version > item.version) {
        throw OptimisticLockException()
    }
    item.changeName(dto.name)
}
_

長所

  • ドメインクラス(Item)は、外部からバージョンを操作する方法を必要としません。
  • バージョンチェックはドメインの一部ではありません(バージョンプロパティ自体を除く)

短所

  • 忘れやすい
  • バージョンフィールドはパブリックである必要があります
  • フレームワークによる自動バージョンチェック(可能な限り最新の時点)は使用されません。

追加のラッパー(以下の例ではConcurrencyGuard)を使用して、チェックを忘れないようにすることができます。リポジトリはアイテムを直接返すのではなく、チェックを強制するコンテナを返します。

_@Transactional
fun changeName(dto: ItemDto) {
    val guardedItem: ConcurrencyGuard<Item> = itemRepository.findById(dto.id)
    val item = guardedItem.checkVersionAndReturnEntity(dto.version)
    item.changeName(dto.name)
}
_

欠点は、チェックが不要な場合があることです(読み取り専用アクセス)。しかし、別のメソッドreturnEntityForReadOnlyAccessが存在する可能性があります。もう1つの欠点は、ConcurrencyGuardクラスがリポジトリのドメイン概念に技術的側面をもたらすことです。

IDとバージョンによるロード

エンティティはIDとバージョンでロードできるため、ロード時に競合が表示されます。

_@Transactional
fun changeName(dto: ItemDto) {
    val item = itemRepository.findByIdAndVersion(dto.id, dto.version)
    item.changeName(dto.name)
}
_

findByIdAndVersionが、指定されたIDを持つが異なるバージョンのインスタンスを見つけると、OptimisticLockExceptionがスローされます。

長所

  • バージョンの処理を忘れることは不可能
  • versionはドメインオブジェクトのすべてのメソッドを汚染しません(リポジトリもドメインオブジェクトです)

短所

  • リポジトリAPIの汚染
  • findByIdバージョンなしの場合は、初期読み込み(編集の開始時)に必要であり、このメソッドは誤って簡単に使用される可能性があります

明示的なバージョンで更新する

_@Transactional
fun changeName(dto: itemDto) {
    val item = itemRepository.findById(dto.id)
    item.changeName(dto.name)
    itemRepository.update(item, dto.version)
}
_

長所

  • エンティティのすべての変更メソッドがバージョンパラメータで汚染されている必要はありません

短所

  • リポジトリAPIは技術パラメータversionで汚染されています
  • 明示的なupdateメソッドは「作業単位」パターンと矛盾します

ミューテーションでバージョンプロパティを明示的に更新する

バージョンパラメータは、バージョンフィールドを内部的に更新できる変更メソッドに渡すことができます。

_@Entity
class Item(var name: String) {
    @Version
    private version: Int

    fun changeName(name: String, version: Int) {
        this.version = version
        this.name = name
    }
}
_

長所

  • 忘れられない

短所

  • すべての変化するドメインメソッドでの技術的な詳細リーク
  • 忘れやすい
  • 管理対象エンティティのバージョン属性を直接変更するのは 許可されていません です。

このパターンの変形は、ロードされたオブジェクトに直接バージョンを設定することです。

_@Transactional
fun changeName(dto: ItemDto) {
    val item = itemRepository.findById(dto.id)
    it.version = dto.version
    item.changeName(dto.name)
}
_

しかし、この呼び出しは読み取りと書き込みのために直接公開されるバージョンを公開し、この呼び出しは簡単に忘れられる可能性があるため、エラーの可能性が高くなります。ただし、すべてのメソッドがversionパラメータで汚染されるわけではありません。

同じIDで新しいオブジェクトを作成する

更新するオブジェクトと同じIDの新しいオブジェクトをアプリケーションで作成できます。このオブジェクトは、コンストラクターのバージョンプロパティを取得します。新しく作成されたオブジェクトは、永続化コンテキストにマージされます。

_@Transactional
fun update(dto: ItemDto) {
    val item = Item(dto.id, dto.version, dto.name) // and other properties ...
    repository.save(item)
}
_

長所

  • あらゆる種類の変更に一貫
  • バージョン属性を忘れることは不可能
  • 不変オブジェクトは簡単に作成できます
  • 多くの場合、最初に既存のオブジェクトをロードする必要はありません

短所

  • 技術属性としてのIDとバージョンは、ドメインクラスのインターフェースの一部です。
  • 新しいオブジェクトを作成すると、ドメイン内で意味を持つミューテーションメソッドを使用できなくなります。名前の初期設定ではなく、変更に対してのみ特定のアクションを実行するchangeNameメソッドがあるかもしれません。このようなメソッドは、このシナリオでは呼び出されません。たぶん、この欠点は特定のファクトリーメソッドで軽減できるでしょう。
  • 「作業単位」パターンと競合しています。

質問

どのように解決しますか、またその理由は何ですか。もっと良いアイデアはありますか?

関連した

21
deamon

同時変更を防ぐために、どこで変更されているアイテムのバージョンを追跡する必要があります。

アプリケーションがステートフルである場合、この情報をサーバーサイドに保持するオプションがあります。セッション中の可能性がありますが、これは最善の選択ではないかもしれません。

ステートレスアプリケーションでは、この情報はクライアントに到達し、すべての変更リクエストで返される必要があります。

そのため、IMOは、同時変更の防止が機能要件である場合、変化するAPI呼び出しでアイテムバージョン情報を使用してもAPIを汚染せず、APIを完全なものにします。

0
ckedar