【问题标题】:Test on @Transactional method fails:Concurrent update in table "WORK": another transaction has updated or deleted the same row@Transactional 方法测试失败:表“WORK”中的并发更新:另一个事务已更新或删除同一行
【发布时间】:2025-11-26 10:05:01
【问题描述】:

我有两个实体和一个服务。没有 @Transactional 一切正常(回滚除外)。现在我在服务方法中添加了一个@Transactional,使其成为事务并在错误时自动回滚。但是现在使用这种方法的所有测试都失败了javax.persistence.EntityNotFoundException: Unable to find org.kitodo.mediaserver.core.db.entities.Work with id xyz(xyz 是我的工作项的 ID)。

然后我尝试将cascade = {CascadeType.PERSIST, CascadeType.MERGE} 添加到 ActionData 实体的工作字段中。比我在与以前相同的位置上得到另一个例外:org.h2.jdbc.JdbcSQLException: Concurrent update in table "WORK": another transaction has updated or deleted the same row [90131-196]

我假设出于某种原因它会尝试同时使用两个转换。

这是什么原因,我该如何做?

实体

@Entity
public class Work {

    private String id;
    private String title;
    private String path;
    private String hostId;
    private Instant indexTime;
    private Set<Collection> collections;
    private String allowedNetwork = "global";

    protected Work() {}

    public Work(String id, String title) {
        this.id = id;
        this.title = title;
    }

    @Id
    public String getId() {
        return id;
    }

    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "work_collection",
            joinColumns = @JoinColumn(name = "work_id", referencedColumnName = "id"),
            inverseJoinColumns = @JoinColumn(name = "collection_name", referencedColumnName = "name"))
    public Set<Collection> getCollections() {
        return collections;
    }

    // getters/setters

}


@Entity
public class ActionData {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @ElementCollection
    @CollectionTable(name = "action_parameter")
    private Map<String, String> parameter = new HashMap<>();

    @ManyToOne
    @JoinColumn(name = "work_id", referencedColumnName = "id", nullable = false)
    private Work work;

    private String actionName;

    private Instant requestTime;
    private Instant startTime;
    private Instant endTime;

    private ActionData() {}

    public ActionData(Work work, String actionName, Map<String, String> parameter) {
        this.work = work;
        this.parameter = parameter;
        this.actionName = actionName;
    }

    // getters/setters

}

服务方法

@Service
public class ActionService {

    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public Object performRequested(ActionData actionData) throws Exception {

        // some checks

        actionData.setStartTime(Instant.now());

        // !!! javax.persistence.EntityNotFoundException: Unable to find org.kitodo.mediaserver.core.db.entities.Work with id xyz
        actionRepository.save(actionData);

        IAction actionInstance = getActionInstance(actionData.getActionName());

        Object result;

        result = actionInstance.perform(actionData.getWork(), actionData.getParameter());

        actionData.setEndTime(Instant.now());
        actionRepository.save(actionData);

        return result;
    }
}

测试

@Test
public void performRequestedAction() throws Exception {

    // given
    init();
    work1 = entityManager.persist(work1);
    actionData1 = new ActionData(work1, "mockAction", parameter1);
    actionData1.setRequestTime(Instant.now());
    actionData1 = entityManager.persist(actionData1);
    entityManager.flush();

    // when
    Object action = actionService.performRequested(actionData1);

    // then
    assertThat(action).isNotNull();
    assertThat(action).isInstanceOf(String.class);
    assertThat(action).isEqualTo("performed");
    assertThat(actionData1.getStartTime()).isBetween(Instant.now().minusSeconds(2), Instant.now());
    assertThat(actionData1.getEndTime()).isBetween(Instant.now().minusSeconds(2), Instant.now());
}

【问题讨论】:

    标签: java spring hibernate spring-boot transactions


    【解决方案1】:

    我怀疑,您的单元测试没有在自动提交模式下工作。

    问题可能是,您没有在测试函数中提交插入事务。

    因此,调用的方法 actionService.performRequested() 无法看到保存的数据,该方法启动了一个全新的事务。该事务将不允许看到任何脏数据。 因此,要么通过设置 autocommit-mode 或提交将 actionData1 保存在 performRequestedAction 中的事务来确保数据被保存。

    【讨论】:

    • 是的,我认为问题在于,测试本身使用了转换。 DEBUG 输出在每次测试时都会启动一个事务并在测试后回滚它。保持干净的测试数据库是有意义的。这就是为什么我自己的交易不起作用的原因。如果我在测试中使用自动提交或没有事务,我将破坏这个干净的数据库。所以我认为这会导致其他问题。
    • 我同意,我们总是使用 Transactions 来准备测试数据。在我们的例子中,我们可以在测试代码中使用 UserTransaction。或者,您可以检查您是否真的需要 REQUIRES_NEW。在需要的默认传播类型中,测试应该可以工作。您必须意识到,如果您对方法进行本地服务调用,您在将测试事务与方法耦合时也会遇到问题。通常事务应该封装完整的业务事务,而不仅仅是它的一部分。
    • 是的,这是一种解决方法。类上有一个 @Transactional 以确保 Hibernate 会话保持打开状态。我不需要在那里进行交易,但这是使其工作的唯一方法。此类调用需要运行事务的 performRequested()。因此 REQUIRES_NEW 仅从 performRequested() 回滚当前更改。我认为我需要一个更好的 Hibernate 会话解决方案才能按预期工作。
    • 休眠会话意味着数据库连接。 DB-Connection 意味着单独的事务。在 J2EE-Container-talk 中,我个人更愿意用事务术语而不是开放的休眠会话。我个人更愿意完全(或尽可能地)忽略hibernate,只使用JPA和J2EE,即使底层的OR-Mapping是由hibernate实现的。
    • 好的,我了解问题和原因。我删除了解决方法@Transactional 并执行 JOIN FETCH 以解决会话问题。现在真正的@Transactional 不需要 REQUIRES_NEW 并且测试正常 - 一切正常!
    最近更新 更多