dea*_*mon 20 java hibernate jpa optimistic-locking optimistic-concurrency
我想知道在无法在请求之间保留具有特定版本的实体实例的系统中,实现乐观锁定(乐观并发控制)的最佳方法是什么。这实际上是一个非常常见的场景,但是几乎所有示例都基于在请求之间(在http会话中)保存已加载实体的应用程序。
如何在尽可能少的API污染的情况下实现乐观锁定?
栈是带有JPA(休眠)的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在更新之前立即重新加载了Item 的当时最新版本()。客户端B开始使用的信息version = 1丢失,并且Hibernate无法检测到冲突。客户端B执行的更新请求将不得不Item(id = 1, version = 1, name = "b")(而不是version = 2)保留。
JPA / Hibernate提供的自动版本检查仅在初始GET请求中加载的实例在服务器上的某种客户端会话中保持活动并稍后由相应客户端更新时才起作用。但是在无状态服务器中,必须以某种方式考虑来自客户端的版本。
可以使用应用程序服务的方法执行显式版本检查:
@Transactional
fun changeName(dto: ItemDto) {
val item = itemRepository.findById(dto.id)
if (dto.version > item.version) {
throw OptimisticLockException()
}
item.changeName(dto.name)
}
Run Code Online (Sandbox Code Playgroud)
优点
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)
}
Run Code Online (Sandbox Code Playgroud)
不利的一面是,在某些情况下,该检查是不必要的(只读访问)。但是可能还有另一种方法returnEntityForReadOnlyAccess。另一个缺点是,ConcurrencyGuard该类会将技术方面带入存储库的域概念。
可以通过ID和版本来加载实体,以便在加载时显示冲突。
@Transactional
fun changeName(dto: ItemDto) {
val item = itemRepository.findByIdAndVersion(dto.id, dto.version)
item.changeName(dto.name)
}
Run Code Online (Sandbox Code Playgroud)
如果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)
}
Run Code Online (Sandbox Code Playgroud)
优点
缺点
versionupdate方法将与“工作单元”模式相矛盾可以将version参数传递给可变方法,而这些方法可以在内部更新version字段。
@Entity
class Item(var name: String) {
@Version
private version: Int
fun changeName(name: String, version: Int) {
this.version = version
this.name = name
}
}
Run Code Online (Sandbox Code Playgroud)
优点
缺点
这种模式的一种变体是直接在加载的对象上设置版本。
@Transactional
fun changeName(dto: ItemDto) {
val item = itemRepository.findById(dto.id)
it.version = dto.version
item.changeName(dto.name)
}
Run Code Online (Sandbox Code Playgroud)
但这会使暴露的版本直接暴露给读写,并且会增加出错的可能性,因为此调用很容易被遗忘。但是,并非每种方法都会被version参数污染。
可以在应用程序中创建一个与要更新的对象具有相同ID的新对象。该对象将在构造函数中获取version属性。然后,新创建的对象将合并到持久性上下文中。
@Transactional
fun update(dto: ItemDto) {
val item = Item(dto.id, dto.version, dto.name) // and other properties ...
repository.save(item)
}
Run Code Online (Sandbox Code Playgroud)
优点
缺点
changeName方法应该只对更改而不对名称的初始设置执行某些操作。在这种情况下不会调用这种方法。也许可以通过特定的工厂方法来减轻这种不利影响。您将如何解决?为什么?有更好的主意吗?
服务器使用EntityManager加载项目,该项目返回Item(id = 1,version = 1,名称=“ a”),它更改名称并保留Item(id = 1,version = 1,名称=“ b”)。Hibernate将版本增加到2。
这是对JPA API的滥用,也是导致您的错误的根本原因。
如果entityManager.merge(itemFromClient)改为使用,则将自动检查乐观锁定版本,并且拒绝“过去更新”。
一个警告是entityManager.merge合并实体的整个状态。如果只想更新某些字段,那么使用普通的JPA会有些混乱。具体来说,由于您可能未分配version属性,因此您必须自己检查版本。但是,该代码易于重用:
<E extends BaseEntity> E find(E clientEntity) {
E entity = entityManager.find(clientEntity.getClass(), clientEntity.getId());
if (entity.getVersion() != clientEntity.getVersion()) {
throw new ObjectOptimisticLockingFailureException(...);
}
return entity;
}
Run Code Online (Sandbox Code Playgroud)
然后您可以简单地执行以下操作:
public Item updateItem(Item itemFromClient) {
Item item = find(itemFromClient);
item.setName(itemFromClient.getName());
return item;
}
Run Code Online (Sandbox Code Playgroud)
根据不可修改字段的性质,您还可以执行以下操作:
public Item updateItem(Item itemFromClient) {
Item item = entityManager.merge(itemFromClient);
item.setLastUpdated(now());
}
Run Code Online (Sandbox Code Playgroud)
对于以DDD方式执行此操作,版本检查是持久性技术的实现细节,因此应在存储库实现中进行。
要通过应用程序的各个层传递版本,我发现使版本成为域实体或值对象的一部分很方便。这样,其他层不必显式与version字段进行交互。
| 归档时间: |
|
| 查看次数: |
524 次 |
| 最近记录: |