特定のバージョンのエンティティインスタンスをリクエスト間で維持できないシステムでオプティミスティックロック(楽観的同時実行制御)を実装する最良の方法は何でしょうか。これは実際にはかなり一般的なシナリオですが、ほとんどすべての例は、リクエスト間で読み込まれたエンティティを(httpセッションで)保持するアプリケーションに基づいています。
API汚染をできるだけ少なくして楽観的ロックを実装するにはどうすればよいですか?
スタックは、関連性がある場合、JPA(Hibernate)を備えたSpringです。
@Version
_のみの使用に関する問題多くのドキュメントでは、フィールドを_@Version
_で装飾するだけでよく、JPA/Hibernateは自動的にバージョンをチェックします。しかし、これは、更新が同じインスタンスを変更するまで、ロードされたオブジェクトと現在のバージョンがメモリに保持されている場合にのみ機能します。
ステートレスアプリケーションで_@Version
_を使用するとどうなりますか。
id = 1
_でアイテムをロードし、Item(id = 1, version = 1, name = "a")
を取得しますid = 1
_でアイテムをロードし、Item(id = 1, version = 1, name = "a")
を取得しますItem(id = 1, version = 1, name = "b")
EntityManager
でアイテムをロードし、Item(id = 1, version = 1, name = "a")
を返します。name
を変更し、Item(id = 1, version = 1, name = "b")
を永続化します。 Hibernateはバージョンを_2
_にインクリメントします。Item(id = 1, version = 1, name = "c")
。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とバージョンでロードできるため、ロード時に競合が表示されます。
_@Transactional
fun changeName(dto: ItemDto) {
val item = itemRepository.findByIdAndVersion(dto.id, dto.version)
item.changeName(dto.name)
}
_
findByIdAndVersion
が、指定されたIDを持つが異なるバージョンのインスタンスを見つけると、OptimisticLockException
がスローされます。
長所
version
はドメインオブジェクトのすべてのメソッドを汚染しません(リポジトリもドメインオブジェクトです)短所
findById
バージョンなしの場合は、初期読み込み(編集の開始時)に必要であり、このメソッドは誤って簡単に使用される可能性があります_@Transactional
fun changeName(dto: itemDto) {
val item = itemRepository.findById(dto.id)
item.changeName(dto.name)
itemRepository.update(item, dto.version)
}
_
長所
短所
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の新しいオブジェクトをアプリケーションで作成できます。このオブジェクトは、コンストラクターのバージョンプロパティを取得します。新しく作成されたオブジェクトは、永続化コンテキストにマージされます。
_@Transactional
fun update(dto: ItemDto) {
val item = Item(dto.id, dto.version, dto.name) // and other properties ...
repository.save(item)
}
_
長所
短所
changeName
メソッドがあるかもしれません。このようなメソッドは、このシナリオでは呼び出されません。たぶん、この欠点は特定のファクトリーメソッドで軽減できるでしょう。どのように解決しますか、またその理由は何ですか。もっと良いアイデアはありますか?
同時変更を防ぐために、どこで変更されているアイテムのバージョンを追跡する必要があります。
アプリケーションがステートフルである場合、この情報をサーバーサイドに保持するオプションがあります。セッション中の可能性がありますが、これは最善の選択ではないかもしれません。
ステートレスアプリケーションでは、この情報はクライアントに到達し、すべての変更リクエストで返される必要があります。
そのため、IMOは、同時変更の防止が機能要件である場合、変化するAPI呼び出しでアイテムバージョン情報を使用してもAPIを汚染せず、APIを完全なものにします。