使用JPA/Hibernate在无状态应用程序中进行乐观锁定 [英] Optimistic locking in a stateless application with JPA / Hibernate

查看:84
本文介绍了使用JPA/Hibernate在无状态应用程序中进行乐观锁定的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想知道在无法在请求之间保留具有特定版本的实体实例的系统中,实现乐观锁定(乐观并发控制)的最佳方法是什么.这实际上是一个非常普遍的场景,但是几乎所有示例都是基于在请求之间(在http会话中)保存已加载实体的应用程序.

I'm wondering what would be the best way to implement optimistic locking (optimistic concurrency control) in a system where entity instances with a certain version can not be kept between requests. This is actually a pretty common scenario but almost all examples are based on applications that would hold the loaded entity between requests (in a http session).

如何在尽可能少的API污染的情况下实现乐观锁定?

How could optimistic locking be implemented with as little API pollution as possible?

  • 该系统是根据域驱动设计原则开发的.
  • 客户端/服务器系统
  • 不能在请求之间保留实体实例(出于可用性和可伸缩性的原因).
  • 技术细节应尽可能少地污染域的API.

如果有任何相关性,则堆栈为带有JPA(休眠)的Spring.

The stack is Spring with JPA (Hibernate), if this should be of any relevance.

在许多文档中,您似乎所需要做的就是用 @Version 装饰字段,而JPA/Hibernate会自动检查版本.但这只有在将加载的对象及其当前版本保存在内存中,直到更新更改了相同实例的情况下才有效.

In many documents it looks like all you need to do would be to decorate a field with @Version and JPA/Hibernate would automatically check versions. But that only works if the loaded objects with their then current version are kept in memory until the update changes the same instance.

在无状态应用程序中使用 @Version 时会发生什么:

What would happen when using @Version in a stateless application:

  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"),它更改名称,并保留 Item(id = 1,version = 1,名称="b").Hibernate将版本增加到 2 .
  5. 客户端B修改项目并将其发送回服务器: Item(id = 1,version = 1,name ="c").
  6. 服务器使用 EntityManager 加载项目,该项目返回 Item(id = 1,version = 2,name ="b"),它更改名称,并保留 Item(id = 1,version = 2,名称="c").Hibernate将版本增加到 3 .看似没有冲突!
  1. Client A loads item with id = 1 and gets Item(id = 1, version = 1, name = "a")
  2. Client B loads item with id = 1 and gets Item(id = 1, version = 1, name = "a")
  3. Client A modifies the item and sends it back to the server: Item(id = 1, version = 1, name = "b")
  4. The server loads the item with the EntityManager which returns Item(id = 1, version = 1, name = "a"), it changes the name and persist Item(id = 1, version = 1, name = "b"). Hibernate increments the version to 2.
  5. Client B modifies the item and sends it back to the server: Item(id = 1, version = 1, name = "c").
  6. The server loads the item with the EntityManager which returns Item(id = 1, version = 2, name = "b"), it changes the name and persist Item(id = 1, version = 2, name = "c"). Hibernate increments the version to 3. Seemingly no conflict!

正如您在步骤6中所看到的那样,问题在于EntityManager会在更新之前立即重新加载Item的当时最新版本( version = 2 ).客户端B以 version = 1 开始编辑的信息将丢失,并且Hibernate无法检测到冲突.客户端B执行的更新请求必须保留 Item(id = 1,version = 1,name ="b")(而不是 version = 2 )

As you can see in step 6, the problem is that the EntityManager reloads the then current version (version = 2) of the Item immediately before the update. The information that Client B started editing with version = 1 is lost and the conflict can not be detected by Hibernate. The update request performed by Client B would have to persist Item(id = 1, version = 1, name = "b") instead (and not version = 2).

JPA/Hibernate提供的自动版本检查仅在初始GET请求中加载的实例在服务器上的某种客户端会话中保持活动并稍后由相应客户端更新时才起作用.但是在无状态服务器中,必须以某种方式考虑来自客户端的版本.

The automatic version check provided by JPA/Hibernate would only work if the instances loaded on the the initial GET request would be kept alive in some kind of client session on the server, and would be updated later by the respective client. But in a stateless server the version coming from the client must be taken into consideration somehow.

可以在应用程序服务的方法中执行显式版本检查:

An explicit version check could be performed in a method of an application service:

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

专业人士

  • 域类( Item )不需要从外部操纵版本的方法.
  • 版本检查不属于域(版本属性本身除外)
  • The domain class (Item) doesn't need a way to manipulate the version from the outside.
  • Version checking is not part of the domain (except the version property itself)

缺点

  • 容易忘记
  • 版本字段必须为公开
  • 未使用框架的自动版本检查(在最新的时间点)

可以通过附加包装程序(在下面的示例中为 ConcurrencyGuard )防止忘记支票.存储库不会直接返回商品,而是会执行检查的容器.​​

Forgetting the check could be prevented through an additional wrapper (ConcurrencyGuard in my example below). The repository would not directly return the item, but a container that would enforce the check.

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

不利的一面是,在某些情况下(只读访问)该检查是不必要的.但是可能还有另一种方法 returnEntityForReadOnlyAccess .另一个缺点是, ConcurrencyGuard 类会将技术方面带到存储库的域概念.

A downside would be that the check is unnecessary in some cases (read-only access). But there could be another method returnEntityForReadOnlyAccess. Another downside would be that the ConcurrencyGuard class would bring a technical aspect to the domain concept of a repository.

可以通过ID和版本来加载实体,以便在加载时显示冲突.

Entities could be loaded by ID and version, so that the conflict would show at load time.

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

如果 findByIdAndVersion 将找到具有给定ID但版本不同的实例,则会抛出 OptimisticLockException .

If findByIdAndVersion would find an instance with the given ID but with a different version, an OptimisticLockException would be thrown.

专业人士

  • 不可能忘记处理版本
  • 版本不会污染域对象的所有方法(尽管存储库也是域对象)
  • impossible to forget handle the version
  • version doesn't pollute all methods of the domain object (though repositories are domain objects, too)

缺点

  • 存储库API的污染
  • findById 无需版本即可进行初始加载(开始编辑时),并且这种方法很容易被偶然使用
  • Pollution of the repository API
  • findById without version would be needed anyway for initial loading (when editing starts) and this method could be easily used accidentally
@Transactional
fun changeName(dto: itemDto) {
    val item = itemRepository.findById(dto.id)
    item.changeName(dto.name)
    itemRepository.update(item, dto.version)
}

专业人士

  • 并非每个实体的变异方法都必须使用版本参数来污染

缺点

  • 存储库API被技术参数 version
  • 污染
  • 显式的 update 方法将与工作单元"冲突.模式
  • Repository API is polluted with the technical parameter version
  • Explicit update methods would contradict the "unit of work" pattern

可以将version参数传递给可以在内部更新version字段的变异方法.

The version parameter could be passed to mutating methods which could internally update the version field.

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

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

专业人士

  • 不可能忘记

缺点

  • 所有变异域方法中的技术细节泄漏
  • 容易忘记
  • 不允许更改以下版本的属性直接管理实体.
  • technical details leaks in all mutating domain methods
  • easy to forget
  • It is not allowed to change the version attribute of managed entities directly.

此模式的一种变体是直接在加载的对象上设置版本.

A variant of this pattern would be to set the version directly on the loaded object.

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

但是这将直接暴露该版本以供读取和写入,这将增加出错的可能性,因为此调用很容易被遗忘.但是,并非每种方法都会被 version 参数污染.

But that would expose the version directly expose for reading and writing and it would increase the possibility for errors, since this call could be easily forgotten. However, not every method would be polluted with a version parameter.

可以在应用程序中创建一个与要更新的对象具有相同ID的新对象.该对象将在构造函数中获取version属性.然后,新创建的对象将合并到持久性上下文中.

A new object with the same ID as the object to be update could created in the application. This object would get the version property in the constructor. The newly created object would then be merged into the persistence context.

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

专业人士

  • 对各种修改都一致
  • 不可能忘记版本属性
  • 不可变对象易于创建
  • 在很多情况下无需先加载现有对象

缺点

  • 作为技术属性的ID和版本是域类接口的一部分
  • 创建新对象将阻止使用具有域含义的突变方法.也许有一个 changeName 方法仅应对更改而不是对名称的初始设置执行某些操作.在这种情况下不会调用这种方法.也许可以通过特定的工厂方法来缓解这种不利影响.
  • 与工作单元"冲突模式.
  • ID and version as technical attributes are part of the interface of domain classes
  • Creating new objects would prevent the usage of mutation methods with a meaning in the domain. Maybe there is a changeName method that should perform a certain action only on changes but not on the initial setting of the name. Such a method wouldn't be called in this scenario. Maybe this downside could be mitigated with specific factory methods.
  • Conflicts with the "unit of work" pattern.

您将如何解决它,为什么?有更好的主意吗?

How would you solve it and why? Is there a better idea?

  • Optimistic locking in a RESTful application
  • Managing concurrency in a distributed RESTful environment with Spring Boot and Angular 2 (this is basically the "explicit version check" from above implemented with HTTP headers)

推荐答案

服务器使用EntityManager加载项目,该项目返回Item(id = 1,version = 1,名称="a"),它更改名称并保留Item(id = 1,version = 1,name ="b"").Hibernate将版本增加到2.

The server loads the item with the EntityManager which returns Item(id = 1, version = 1, name = "a"), it changes the name and persist Item(id = 1, version = 1, name = "b"). Hibernate increments the version to 2.

这是对JPA API的误用,也是导致您的错误的根本原因.

That's a misuse of the JPA API, and the root cause of your bug.

如果您改用 entityManager.merge(itemFromClient),则将自动检查开放式锁定版本,并拒绝过去更新".

If you use entityManager.merge(itemFromClient) instead, the optimistic locking version would be checked automatically, and "updates from the past" rejected.

一个警告是 entityManager.merge 将合并实体的 entire 状态.如果您只想更新某些字段,则使用普通的JPA会有些混乱.具体来说,由于您可能无法分配版本属性,您必须自己检查版本.但是,该代码易于重用:

One caveat is that entityManager.merge will merge the entire state of the entity. If you only want to update certain fields, things are a bit messy with plain JPA. Specifically, because you may not assign the version property, you must check the version yourself. However, that code is easy to reuse:

<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;
}

然后您可以简单地执行以下操作:

and then you can simply do:

public Item updateItem(Item itemFromClient) {
    Item item = find(itemFromClient);
    item.setName(itemFromClient.getName());
    return item;
}

根据不可修改字段的性质,您也可以执行以下操作:

depending on the nature of the unmodifiable fields, you may also be able to do:

public Item updateItem(Item itemFromClient) {
    Item item = entityManager.merge(itemFromClient);
    item.setLastUpdated(now());
}

对于以DDD方式执行此操作,版本检查是持久性技术的实现细节,因此应在存储库实现中进行.

As for doing this in a DDD way, the version checking is an implementation detail of the persistence technology, and should therefore occur in the repository implementation.

要通过应用程序的各个层传递版本,我发现将版本作为域实体或值对象的一部分很方便.这样,其他层不必显式地与version字段进行交互.

To pass the version through the various layers of the app, I find it convenient to make the version part of the domain entity or value object. That way, other layers do not have to explicitly interact with the version field.

这篇关于使用JPA/Hibernate在无状态应用程序中进行乐观锁定的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆