Spring Data JPA:删除乐观锁定语义

8

有一个实体Foo,带有@Version列。如果我想删除它,我希望Spring Data JPA和/或Hibernate检查@Version列的当前值是否与数据库中的值匹配。如果不匹配,则应该拒绝删除。这对于脱离实体来说是可以正常工作的:

@Transactional
public void delete(Foo foo) {
    fooRepository.delete(foo); // throws ObjectOptimisticLockingFailureException
}

但是如果我首先从仓库中加载实体,然后在同一事务中使用不同版本删除它,则无论 @Version 列的值如何,删除都会通过:

@Transactional
public void delete(int fooId, long version) {
    Foo foo = fooRepository.findOne(fooId);
    foo.setVersion(version);
    fooRepository.delete(foo); // passes regardless of value of version
}

当我查看Hibernate的调试输出时,版本比较是通过执行(delete from foo where id=? and version=?)来完成的,但结果并不符合我的预期。
我错过了什么?
2个回答

11
JPA规范3.4.2节中可以得知:

实体可以访问其版本字段或属性的状态,或导出用于应用程序访问版本的方法,但不得修改版本值。除第4.10节中注明的情况外,只有持久性提供者被允许在对象中设置或更新版本属性的值。

版本属性的目的是保护我们免受在当前持久化上下文中加载对象后可能发生的并发更新,Hibernate通过忽略您手动设置的任何值来实现它,而是在加载对象时使用从数据库获取的值。为了验证这一点,您可以启用打印绑定变量值,并注意到使用了来自数据库的值。

例如,在使用DTO处理实体状态更新时,实践中使用的标准解决方案是手动执行检查:

if (entity.getVersion() != dto.getVersion()) {
    throw new OptimisticLockException("...");
}

当然,您可以通过从一个提供所有可版本化实体检查的基类扩展,或者使用一些实用方法来使其更通用。例如,有些作者直接在版本设置器中执行此操作:

public void setVersion(long version) {
    if (this.version != version) {
      throw new OptimisticLockException("...");
    }
} 

Hibernate会自动为脱离状态的实体执行此检查,可以在DefaultMergeEventListener的实现中看到:

else if (isVersionChanged(entity, source, persister, target)) {
    if (source.getFactory().getStatistics().isStatisticsEnabled()) {
        source.getFactory().getStatisticsImplementor()
            .optimisticFailure(entityName);
    }
    throw new StaleObjectStateException(entityName, id);
}

7
根据JPA规范(第11.1.54节,重点是我的):

Version注解指定了实体类的版本字段或属性,用作乐观锁值。 在执行合并操作时,版本用于确保完整性,并用于乐观并发控制。

在未受管理的实例上执行存储库delete操作会首先执行merge,因此会如预期地抛出ObjectOptimisticLockingFailureException异常。
但是,在受管理的实例上执行存储库delete操作直接调用基础EntityManager上的delete,因此不会抛出异常。
总之,规范要求@Version字段由merge使用,而对于受管理的实例不会调用merge,因此第二种情况没有错误。

1
确切地说,您只能删除托管实体,因此需要执行合并操作,这是执行版本检查的地方。我查看了Hibernate源代码,如果您查看DefaultDeleteEventListener.onDelete()内部,可以看到在调用内部删除方法时它不会查看版本。 - Klaus Groenbaek
@KlausGroenbaek,我不理解你的评论。发帖者问为什么版本不匹配在某些情况下会导致错误而在另一些情况下不会,我已经指出规范中只有一个情况需要进行版本检查(在这种情况下它起作用),而另一个情况则不需要,因此观察到的行为符合规范。当你说Hibernate的onDelete不会查看版本时,这是符合规范的,因为规范不要求delete检查版本。我没有看到任何偏离规范的地方。 - manish
我点赞你的回答,因为它非常到位。我只是指出如果他读了代码,就会发现在删除时没有检查版本。许多开发人员提出问题并等待其他人回答,通常阅读/调试代码更快。在这种情况下,我花了不到15分钟就找到了答案。当然,当涉及到JPA时,了解规范也很重要,但有时您需要查看代码/文档,因为供应商实际上没有实现规范。例如,在EclipseLink中从未遇到过“LazyInitializationException”的等效项。 - Klaus Groenbaek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接