Hibernate 如何使用Spring数据正确处理JPA实体中的版本字段

Hibernate 如何使用Spring数据正确处理JPA实体中的版本字段,hibernate,jpa,spring-data-jpa,spring-data,Hibernate,Jpa,Spring Data Jpa,Spring Data,我有一个非常简单的域模型,一个实体有一个版本字段,以便使用JPA(api v2.2)提供的乐观锁定功能。我使用的实现是Hibernate v5.3.10.Final @Entity @Data public class Activity { @Id @SequenceGenerator(name = "gen", sequenceName = "gen_seq", allocationSize = 1) @GeneratedValue(strategy = Genera

我有一个非常简单的域模型,一个实体有一个版本字段,以便使用JPA(api v2.2)提供的乐观锁定功能。我使用的实现是Hibernate v5.3.10.Final

@Entity
@Data
public class Activity {

    @Id
    @SequenceGenerator(name = "gen", sequenceName = "gen_seq", allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "gen")
    private Long id;

    @Column
    private String state;

    @Version
    private int version;
}
然后对该实体进行简单操作,例如临时更改其阶段:

@Transactional
public Activity startProgress(Long id) {
    var entity = activityRepository.findById(id).orElseThrow(RuntimeException::new);

    if (entity.getState() == "this") { // or that, or else, etc
        // throw some exceptions in some cases
    }

    entity.setState("IN_PROGRESS");

    return activityRepository.saveAndFlush(entity);
}
这样做的结果,我想实现的是,有一种方法来更新实体,并且更新还应该增加版本。如果存在不匹配,我希望抛出
ObjectOptimisticLockingFailureException
OptimisticLockingException
。当我将
version
字段返回给客户机时,我还希望得到对象中
version
字段的更新值

我尝试过的几个选项:

  • 只需调用
    save
    -版本字段会得到更新,但不会返回新值,客户端会得到旧值,这会使下一个请求命中锁定异常
  • 调用
    saveAndFlush
    ——在本例中,我在客户机上执行了两次update语句,奇怪的是,它返回的是版本X,在数据库中,版本是X+1。然后,下一个客户端请求再次命中锁定异常
  • 创建一个
    @Modifying
    查询,自动将其标记为clear(以自动刷新更改),并使用hql
    创建版本化的
    语法。然后执行以下查询:
    updateactivitiessetversion=version+1,state=?其中id=?
    ,但这似乎没有进行乐观锁检查(
    其中version=:version\u from_entity
    )。我也不认为它会引发适当的例外

因此,最终我想要实现的是非常简单的,我不认为我必须自己编写它-有一种方法来更新一个版本化实体上的一个或多个字段,依靠JPA进行乐观锁定,并获得最新版本,以便客户端可以对该实体执行进一步的操作。我读过很多类似的问题,但大多数都直接使用实体管理器,这不是我想要的。

您的代码看起来非常正确,所以很难说问题出在哪里

我创建了一个使用Spring数据JPA和乐观锁定的简单示例。我希望这将有助于你解决这个问题

创建实体:

@Transactional
公共模型创建(模型){
返回moderepo.save(model);
}
更新实体:

@Transactional
公共可选更新(int-id,模型源){
回归模型回购
.findById(id)
.map(model->modelMapper.apply(model,source));
}
获取实体:

@Transactional(readOnly=true)
公共列表getAll(){
返回modelRepo.findAll();
}
只需克隆项目,运行它(例如,使用
mvn spring boot:run
),然后检查日志:

15:44:35.905  INFO 2800 --- [ main] i.g.c.d.Application : [i] Creating...
15:44:35.925  INFO 2800 --- [ main] jdbc.sqltiming      : batching 1 statements:
1:  insert into model (name, version, id) values ('model', 0, 1); {executed in 1 msec}

(如果你不介意的话,给你一些建议)

对于
version
属性(例如
Integer
Long
等),使用对象而不是简单类型。例如,当您自己创建实体标识符时,它非常有用。在这种情况下,Spring Data/Hibernate检查
version
是否为
null
,并且不会对数据库执行额外的select查询


当您更新实体时,您不必显式调用回购的
save
方法-因为您在事务中,Hibernate会更新更改的实体本身。

版本应该自动更新。您是否使用了
javax.persistence
中正确的
@Version
注释?是的,
javax.persistence
而不是spring data commons中的注释。我在本地尝试了
saveAndFlush()
,无法重现您提到的问题。它按预期工作,不再发生双重更新……那么在您的情况下,这两个更新SQL是什么样子的?它们是一样的吗?好的,但在您的示例中,您更新了实体,然后在单独的事务中读取它。在
公共可选更新(int-id,Model-source)
方法的末尾,您是否正确地将版本设置为1?@MilanMilanov再次检查日志-我已经更新了一点答案…好的,我正式没有想法了。我将方法更改为不调用
saveAndFlush
,并让Hibernate负责更新。测试仍然是绿色的,但令人惊讶的是,它在真正的Oracle DB设置中不起作用。日志是这样写的:
SQL-select。。。来自活动act0,其中act0.id=?BasicBinder-将[1]绑定为[BIGINT]-[117]SQL-更新活动集状态=,版本=?其中id=?和版本=?BasicBinder-binding[1]作为[BIGINT]-[117]BasicBinder-binding[2]作为[VARCHAR]-[“正在进行中”]BasicBinder-binding[3]作为[INTEGER]-[113]BasicBinder-binding[4]作为[BIGINT]-[117]BasicBinder-binding[5]作为[INTEGER]-[112]
显然,版本从112更新到113,但在方法末尾仍然返回112,然后到客户端,异常再次发生。唯一使用的Oracle特定的事情是使用创建数据源。除此之外,设置与测试中的设置相同。任何关于行为可能不同的想法都是受欢迎的。
15:44:35.930  INFO 2800 --- [ main] i.g.c.d.Application : [i] Updating...
15:44:35.934  INFO 2800 --- [ main] jdbc.sqltiming      : select model0_.id as id1_0_0_, model0_.name as name2_0_0_, model0_.version as version3_0_0_ from model model0_ where model0_.id=1; {executed in 0 msec}
15:44:35.939  INFO 2800 --- [ main] jdbc.resultsettable : 
|---------|------|--------|
|id       |name  |version |
|---------|------|--------|
|[unread] |model |0       |
|---------|------|--------|
15:44:35.944  INFO 2800 --- [ main] jdbc.sqltiming      : batching 1 statements:
1:  update model set name='model_updated', version=1 where id=1 and version=0; {executed in 1 msec}
15:44:36.010  INFO 2800 --- [ main] i.g.c.d.Application : [i] Getting...
15:44:36.015  INFO 2800 --- [ main] jdbc.sqltiming      : select model0_.id as id1_0_, model0_.name as name2_0_, model0_.version as version3_0_ from model model0_; {executed in 0 msec}
15:44:36.017  INFO 2800 --- [ main] jdbc.resultsettable : 
|---|--------------|--------|
|id |name          |version |
|---|--------------|--------|
|1  |model_updated |1       |
|---|--------------|--------|