【问题标题】:How to properly handle the version field in a JPA entity with Spring Data如何使用 Spring Data 正确处理 JPA 实体中的版本字段
【发布时间】:2019-12-31 16:57:17
【问题描述】:

我有一个非常简单的域模型,一个具有版本字段的实体,以便使用 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);
}

我想要实现的结果是有一种方法来更新实体,并且该更新还应该增加版本。如果存在不匹配,我希望会抛出 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用于乐观锁定并获取最新版本,以便客户端可以对实体进行进一步的操作。我阅读了很多类似的问题,但大多数都直接使用实体管理器,这不是我想要的。

【问题讨论】:

  • 版本应该会自动更新。您是否使用来自javax.persistence 的正确@Versionannotation?
  • 是的,javax.persistence 不是来自 spring data commons 的那个。
  • 我在本地尝试saveAndFlush(),但无法重现您提到的问题。它按预期工作,不再发生双重更新......那么在你的情况下,这两个 UPDATE SQL 是什么样的?它们是一样的吗?

标签: hibernate jpa spring-data-jpa spring-data


【解决方案1】:

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

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

创建实体:

@Transactional
public Model create(Model model) {
    return modelRepo.save(model);
}

更新实体:

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

获取实体:

@Transactional(readOnly = true)
public List<Model> getAll() {
    return 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}
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       |
|---|--------------|--------|

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

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

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

【讨论】:

  • 好的,但在您的示例中,您更新实体,然后在单独的事务中读回它。您是否在 public Optional&lt;Model&gt; update(int id, Model source) 方法的末尾正确地将版本设置为 1?
  • @MilanMilanov 再次检查日志 - 我已经稍微更新了答案...
  • 好吧,我真的没有想法了。我将方法更改为不调用 saveAndFlush 并离开 Hibernate 来处理更新。测试仍然是绿色的,但令人惊讶的是,它在真正的设置(Oracle DB)中不起作用。日志是这样写的:
  • SQL - select ... from activities act0_ where act0_.id=? BasicBinder - binding [1] as [BIGINT] - [117] SQL - update activities set state=?, version=? where id=? and version=? BasicBinder - binding [1] as [BIGINT] - [117] BasicBinder - binding [2] as [VARCHAR] - ["IN_PROGRESS"] BasicBinder - binding [3] as [INTEGER] - [113] BasicBinder - binding [4] as [BIGINT] - [117] BasicBinder - binding [5] as [INTEGER] - [112] 显然版本从112更新到113,但是在方法结束时仍然返回112,然后返回到客户端并再次发生异常。
  • 唯一使用 Oracle 特定的东西是创建数据源,使用 PoolDataSourceFactory#getPoolDataSource 完成。除此之外,设置与测试中的设置相同。欢迎任何关于为什么行为可能不同的想法。
猜你喜欢
  • 2023-02-10
  • 2020-05-11
  • 2020-11-16
  • 2019-09-14
  • 2018-03-21
  • 2019-09-21
  • 2017-08-17
  • 2018-09-18
  • 2012-08-06
相关资源
最近更新 更多