You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JPA事务提交失败引发ConcurrentModificationException及@PreRemove触发问题求助

解决JPA事务提交失败与@PreRemove误触发问题

嘿,我来帮你梳理下这个问题的核心和解决思路——你这明显是踩了JPA双向关联里orphanRemoval的坑,再加上集合遍历修改导致的并发异常,一步步来解决:

1. 先搞定@PreRemove误触发的根源

你看你的Project实体里的@OneToMany注解加了orphanRemoval=true,这个属性的作用是当子实体从父集合中移除时,JPA会自动删除该子实体。这就解释了为什么你调用project.removeJob(job)时会触发Job的@PreRemove——JPA以为你要删掉这个Job,而不是仅仅解除它和旧Project的关联!

如果你的业务逻辑是Job可以切换Project而不是被删除,那这个orphanRemoval=true绝对要去掉,改成这样:

@OneToMany(mappedBy = "project", cascade = CascadeType.ALL)
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
private Set<Job> jobs = new HashSet<>();

另外,CascadeType.ALL包含了REMOVE级联,如果不需要删除Project时连带删掉所有Job,也可以把cascade调整成CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH,更贴合你的业务场景。

2. 解决ConcurrentModificationException异常

这个异常通常是遍历集合的同时修改集合结构导致的。你看@PreRemove里的循环:

for (AppUser ap : this.appUsers) { ap.removeJob(this); }

如果this.appUsers是普通HashSet,遍历的时候调用ap.removeJob(this)可能会修改当前Job的appUsers集合,直接触发并发修改异常。

解决这个有两种简单办法:

  • 用迭代器遍历并修改:
if (!this.appUsers.isEmpty()) {
    Iterator<AppUser> iterator = this.appUsers.iterator();
    while (iterator.hasNext()) {
        AppUser ap = iterator.next();
        ap.removeJob(this);
        iterator.remove(); // 从当前Job的集合里移除,避免重复处理
    }
}
  • 把集合转成ArrayList再遍历(相当于复制一份快照):
for (AppUser ap : new ArrayList<>(this.appUsers)) {
    ap.removeJob(this);
}

同样,performances的循环也可以用这个方式修改,彻底解决并发修改的问题。

3. 优化服务层的事务逻辑,减少冗余操作

你的服务层里多次调用save(),但在@Transactional注解下,事务内的实体都是托管状态,JPA会自动跟踪修改,提交事务时会同步到数据库,很多save都是多余的。优化后的代码可以简化成这样:

public JobDTO save(JobDTO jobDTO) {
    log.debug("Request to save Job : {}", jobDTO);
    Job job = jobMapper.toEntity(jobDTO);
    
    // 处理旧Project的关联解除(仅当Job已存在时)
    if (job.getId() != null) {
        Project oldProject = projectRepository.findProjectByJob(job.getId()).orElse(null);
        if (oldProject != null && !oldProject.getId().equals(jobDTO.getProjectId())) {
            oldProject.getJobs().remove(job);
            job.setProject(null);
        }
    }
    
    // 关联新的Project
    if (jobDTO.getProjectId() != null) {
        Project newProject = projectRepository.findById(jobDTO.getProjectId())
                .orElseThrow(() -> new RuntimeException("Project not found with id: " + jobDTO.getProjectId()));
        job.setProject(newProject);
        newProject.getJobs().add(job);
    }
    
    // 只需要保存一次Job,关联的Project会被JPA自动同步
    job = jobRepository.save(job);
    return jobMapper.toDto(job);
}

这样不仅减少了冗余的数据库操作,还能避免多次save导致的状态不一致问题。

4. 验证@PreRemove的触发逻辑

现在去掉orphanRemoval=true后,只有当你真正删除Job(比如调用jobRepository.delete(job))时,@PreRemove才会被触发,这正好符合你的预期——只有删除Job时才清理关联的AppUser和Performance,切换Project时不会触发。

如果需要在解除Project关联时做一些额外操作,别放在@PreRemove里,单独写个方法比如onProjectDisassociate(),在解除关联的时候手动调用就好。


内容的提问来源于stack exchange,提问作者Girbouillle

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 08:33:31