如何使用Spring Data正确处理JPA实体中的version字段

Mil*_*nov 5 hibernate jpa spring-data spring-data-jpa

我有一个非常简单的域模型,一个实体具有一个version字段,以便使用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;
}
Run Code Online (Sandbox Code Playgroud)

然后对该实体进行简单的操作,例如临时更改其阶段:

@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);
}
Run Code Online (Sandbox Code Playgroud)

我想要实现的结果是,有一种更新实体的方法,并且该更新还应该增加版本。如果存在不匹配,我希望会抛出a ObjectOptimisticLockingFailureExceptionOptimisticLockingException。我还希望version在对象中具有该字段的更新值,因为我要将其返回给客户端。

我尝试过的几种选择:

  • 只需调用save-版本字段将被更新,但不会返回新值,并且客户端将获取旧值,这会使下一个请求到达锁定异常
  • 调用saveAndFlush-在这种情况下,我在客户端执行了两次更新语句,这奇怪地返回了版本X,在数据库中版本为X + 1。然后,下一个客户端请求再次遇到锁定异常。
  • 创建一个@Modifying查询,将其标记为自动清除(以自动刷新更改)并使用hql create versioned语法。然后,我执行以下查询:update activities set version=version+1, state=? where id=?,但这似乎并没有进行乐观锁检查(where version = :version_from_entity)。我也不认为这会引发适当的例外。

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

Cep*_*pr0 3

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

我使用 Spring Data JPA 和乐观锁定创建了一个简单的工作示例。希望对您解决问题有所帮助。

创建一个实体:

@Transactional
public Model create(Model model) {
    return modelRepo.save(model);
}
Run Code Online (Sandbox Code Playgroud)

更新实体:

@Transactional
public Optional<Model> update(int id, Model source) {
    return modelRepo
            .findById(id)
            .map(model -> modelMapper.apply(model, source));
}
Run Code Online (Sandbox Code Playgroud)

获取实体:

@Transactional(readOnly = true)
public List<Model> getAll() {
    return modelRepo.findAll();
}
Run Code Online (Sandbox Code Playgroud)

只需克隆项目,运行它(例如,使用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}
Run Code Online (Sandbox Code Playgroud)
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}
Run Code Online (Sandbox Code Playgroud)
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       |
|---|--------------|--------|
Run Code Online (Sandbox Code Playgroud)

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

使用对象而不是version属性的简单类型(例如IntegerLong等)。例如,当您自己创建实体标识符时,它很有用。在这种情况下,Spring Data/Hibernate 检查是否versionnull数据库执行额外的选择查询。

当您更新实体时,您不必显式调用save存储库的方法 - 因为您处于事务中,Hibernate 会更新已更改的实体本身。