【问题标题】:Optimistic locking not throwing exception when manually setting version field手动设置版本字段时乐观锁定不抛出异常
【发布时间】:2015-09-02 01:37:50
【问题描述】:

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

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

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

我希望这会在版本不匹配时引发异常。但事实并非如此。谷歌搜索,我发现一些帖子说我们无法设置附加实体的 @Vesion 属性,就像我在上面的第三条语句中所做的那样。

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

【问题讨论】:

  • Hibernate does 允许您手动修改 @version 字段(与 OpenJPA 不同)但这不符合 JPA 规范(请参阅第 11.1.54 节 @987654321 @) 。如果您直接绑定到实体,您的方法应该有效。您是否将 DTO 传递给您的服务并在此处填充实体?
  • 是的,我上面代码中的 updatedUser 是 DTO。

标签: hibernate jpa spring-data-jpa


【解决方案1】:

不幸的是,(至少对于 Hibernate)手动更改 @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 到模型”数据填充,并且我已经将版本检查逻辑放在那里。另一种方法是将逻辑放在setVersion() 中,而不是真正设置版本,而是进行版本检查:

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

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

    //.....
}

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

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

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

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

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

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

      编辑。 不知道您在 Spring Data JPA 中使用哪个 JPA 持久性提供程序,但有关使用 JPA+Hibernate 进行乐观锁定的更多详细信息,我建议您阅读本书的第 10 章,控制并发访问部分em>Java Persistence with Hibernate(Hibernate in Action)

      【讨论】:

      • 您错过了我们正在讨论的内容。我们都清楚乐观并发的工作原理。我们关注的是,对于 Hibernate 创建的更新查询,它没有使用实体中的@Version 字段。相反,Hibernate 将实体版本存储在其他地方,并将其用于乐观并发检查。
      【解决方案4】:

      除了@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  
      

      【讨论】:

      • 如何查找预先存在的实体,但没有将其放入缓存中以使您的第一个代码示例工作?
      • @pgreen2 在这种情况下,您查找现有实体,但为了在数据库中持久化,您使用用户发送的实体来组装它。分离实体的版本字段是可修改的。相反,获取的实体的版本字段是不可修改的。
      猜你喜欢
      • 2016-07-18
      • 2014-07-06
      • 2014-02-18
      • 2015-05-14
      • 1970-01-01
      • 1970-01-01
      • 2015-04-28
      • 2011-08-18
      • 2015-04-16
      相关资源
      最近更新 更多