JPA事务提交失败引发ConcurrentModificationException及@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

