手动设置版本字段时乐观锁定不会抛出异常

Optimistic locking not throwing exception when manually setting version field

我有一个使用 Spring Data JPA 的 Spring Boot 1.3.M1 Web 应用程序。对于乐观锁定,我正在执行以下操作:

  1. 注释实体中的版本列:@Version private long version;。我通过查看数据库 table 确认该字段正在正确递增。
  2. 当用户请求编辑实体时,也会发送 version 字段。
  3. 当用户在编辑后按提交时,收到 version 字段作为隐藏字段或其他内容。
  4. 服务器端,获取实体的新副本,然后更新所需字段以及 version 字段。像这样:

    User user = userRepository.findOne(id);
    user.setName(updatedUser.getName());
    user.setVersion(updatedUser.getVersion());
    userRepository.save(user);
    

我原以为这会在版本不匹配时抛出异常。但事实并非如此。谷歌搜索,我发现一些帖子说我们不能设置附加实体的 @Vesion 属性,就像我在上面的第三个声明中所做的那样。

所以,我猜我必须手动检查版本不匹配并自己抛出异常。这是正确的方法吗,还是我遗漏了什么?

不幸的是,(至少对于 Hibernate)手动更改 @Version 字段不会使其成为另一个 "version"。即乐观并发检查是针对读取实体时检索到的版本值进行的,而不是实体更新时的版本字段。

例如

这会起作用

Foo foo = fooRepo.findOne(id);  // assume version is 2 here
foo.setSomeField(....);

// Assume at this point of time someone else change the record in DB, 
// and incrementing version in DB to 3

fooRepo.flush();  // forcing an update, then Optimistic Concurrency exception will be thrown

但是这行不通

Foo foo = fooRepo.findOne(id);  // assume version is 2 here
foo.setSomeField(....);
foo.setVersion(1);
fooRepo.flush();  // forcing an update, no optimistic concurrency exception
                  // Coz Hibernate is "smart" enough to use the original 2 for comparison

有一些方法可以解决这个问题。最直接的方法可能是自己实现乐观并发检查。我曾经有一个实用程序来执行 "DTO to Model" 数据填充,并且我已将版本检查逻辑放在那里。另一种方法是将逻辑放在 setVersion() 中,而不是真正设置版本,它会进行版本检查:

class User {
    private int version = 0;
    //.....

    public void setVersion(int version) {
        if (this.version != version) {
            throw new YourOwnOptimisticConcurrencyException();
        }
    }

    //.....
}

@AdrianShum 的部分回答是正确的。

版本比较行为基本上遵循以下步骤:

  1. 检索版本化实体及其版本号,我们称之为 V1。
  2. 假设您修改了某个实体的 属性,然后 Hibernate 将版本号增加到 V2 "in memory"。它不接触数据库。
  3. 您提交更改或它们由环境自动提交,然后 Hibernate 将尝试使用 V2 值更新实体,包括其版本号。 Hibernate 生成的更新查询只有在匹配 ID 和以前的版本号 (V1) 时才会修改实体的注册表。
  4. 实体注册表修改成功后,实体以V2为实际版本值。

现在假设在第 1 步和第 3 步之间实体被另一个事务修改,因此它在第 3 步的版本号不是 V1。然后由于版本号不同,更新查询不会修改任何注册表,hibernate 意识到并抛出异常。

您可以简单地测试此行为并检查是否抛出异常,在步骤 1 和步骤 3 之间直接在数据库上更改版本号。

编辑。 不知道您将哪个 JPA 持久性提供程序与 Spring Data JPA 一起使用,但有关使用 JPA+Hibernate 进行乐观锁定的更多详细信息,我建议您阅读第 10 章,第 节控制并发访问,来自 Java Hibernate 的持久性(Hibernate 实战)

除了@Adrian Shum 的回答,我还想展示一下我是如何解决这个问题的。如果您想手动更改实体的版本并执行更新以导致 OptimisticConcurrencyException,您可以简单地复制实体及其所有字段,从而导致实体离开其上下文(与 EntityManager.detach() 相同)。这样,它的行为就正常了。

Entity entityCopy = new Entity();
entityCopy.setId(id);
... //copy fields
entityCopy.setVersion(0L); //set invalid version
repository.saveAndFlush(entityCopy); //boom! OptimisticConcurrencyException

编辑: 仅当休眠缓存不包含具有相同 ID 的实体时,组装版本才有效。这行不通:

Entity entityCopy = new Entity();
entityCopy.setId(repository.findOne(id).getId()); //instance loaded and cached 
... //copy fields
entityCopy.setVersion(0L); //will be ignored due to cache
repository.saveAndFlush(entityCopy); //no exception thrown  

您也可以在从数据库中读取实体后将其分离,这也会导致版本检查。

User user = userRepository.findOne(id);
userRepository.detach(user);
user.setName(updatedUser.getName());
user.setVersion(updatedUser.getVersion());
userRepository.save(user);

Spring 存储库没有 detach 方法,您必须实现它。一个例子:

public class BaseRepositoryImpl<T, PK extends Serializable> extends QuerydslJpaRepository<T, PK> {

   private final EntityManager entityManager;

   public BaseRepositoryImpl(JpaEntityInformation entityInformation, EntityManager entityManager) {
       super(entityInformation, entityManager);
       this.entityManager = entityManager;
   }

   public void detach(T entity) {
       entityManager.detach(entity);
   }
...
}