SpringBoot CRUD方法返回早于关联Service执行完成问题排查
问题场景
定义了Technology和Project两个实体,通过@ManyToMany做双向关联,关联配置如下:
Technology实体(关联被维护端/inverse端)配置:
@ManyToMany(mappedBy = "internalTechnologies") private List<Project> internalProjects = new ArrayList<>();
Project实体(关联维护端/owning端,持有中间表配置)配置:
@ManyToMany(cascade = {CascadeType.MERGE}) @JoinTable( name = "project_technology", joinColumns = @JoinColumn(name = "project_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "technology_id", referencedColumnName = "id") ) private List<Technology> internalTechnologies = new ArrayList<>();
关联增删逻辑封装在@Service标注的辅助类中,注入Controller使用:
- 移除关联逻辑:
public void removeTechnologyFromProjects(Technology technologyToRemove, List<Project> projects){ List<Project> projectsToEdit = new ArrayList<>(projects); for ( Project project: projectsToEdit) { List<Technology> internalTechnologies = project.getInternalTechnologies(); internalTechnologies.remove(technologyToRemove); project.setInternalTechnologies(internalTechnologies); projectRepository.save(project); } }
注:此处新建ArrayList是为了避免遍历集合时触发
ConcurrentModificationException。
- 新增关联逻辑:
public void linkTechnologyToProjects(Technology technologyToAdd, List<Long> ids) throws ResponseStatusException { for (Long id : ids) { Optional<Project> project = projectRepository.findById(id); if (project.isPresent()) { Project projectToEdit = project.get(); List<Technology> technologyList = projectToEdit.getInternalTechnologies(); technologyList.add(technologyToAdd); projectToEdit.setInternalTechnologies(technologyList); projectRepository.save(projectToEdit); } else { throw new ResponseStatusException(HttpStatus.NOT_FOUND, String.format(PROJECT_NOT_FOUND, id)); } } }
Technology更新接口代码如下:
@PutMapping("/tech/{id}") public TechReturnDTO updateTech(@PathVariable("id") long id, @Valid @RequestBody TechCreationDTO requestBody) { Technology techToUpdate = techRep.findById(id); // 其他字段编辑逻辑 if (requestBody.getInternalProjects()!=null){ linkRemover.removeTechnologyFromProjects(techToUpdate, techToUpdate.getInternalProjects()); itemLinker.linkTechnologyToProjects(techToUpdate, requestBody.getInternalProjects()); }else{ linkRemover.removeTechnologyFromProjects(techToUpdate, techToUpdate.getInternalProjects()); } // 其他关联列表编辑逻辑 Technology savedTech = techRep.save(techToUpdate); return new TechReturnDTO(savedTech); } throw new ResponseStatusException(HttpStatus.NOT_FOUND, String.format("Technology with Id %s not found!", id)); }
异常表现
接口返回时TechReturnDTO中没有关联项的变更内容,但接口返回后发送GET请求查询,能正常获取到变更后的数据,误以为是Service异步执行未等待完成导致。
补充信息:TechnologyRepository直接继承CrudRepository,定义如下:
public interface TechnologyRepository extends CrudRepository<Technology, Long> { Technology findById(long id); }
编辑Project实体时关联变更可以立即在返回值中展示,仅编辑Technology更新多对多关联时出现上述问题。
根本原因
不存在异步执行未等待的问题,Spring中所有Service方法默认都是同步执行的。
问题核心是JPA双向关联的内存状态不同步:
所有关联修改都在关联维护端(Project侧)执行,数据库中间表的数据已经正确更新,但内存中持有的techToUpdate对象的internalProjects集合从来没有同步过修改,最后save返回的savedTech用的还是旧的内存集合,所以返回的DTO看不到变更。后续GET请求是重新从数据库加载全量数据,所以能拿到正确结果。
编辑Project时无此问题,是因为修改的就是维护端自身的集合,内存状态和数据库更新一致。
解决方案
两种方案选其一即可:
方案1:关联操作时同步维护双向集合(推荐)
JPA双向关联要求两侧集合状态始终保持一致,修改关联维护端集合的同时,同步修改被维护端的集合:
- 调整移除关联方法:
public void removeTechnologyFromProjects(Technology technologyToRemove, List<Project> projects){ List<Project> projectsToEdit = new ArrayList<>(projects); for (Project project: projectsToEdit) { List<Technology> internalTechnologies = project.getInternalTechnologies(); internalTechnologies.remove(technologyToRemove); project.setInternalTechnologies(internalTechnologies); // 同步维护Technology侧的集合 technologyToRemove.getInternalProjects().remove(project); projectRepository.save(project); } }
- 调整新增关联方法:
public void linkTechnologyToProjects(Technology technologyToAdd, List<Long> ids) throws ResponseStatusException { for (Long id : ids) { Optional<Project> project = projectRepository.findById(id); if (project.isPresent()) { Project projectToEdit = project.get(); List<Technology> technologyList = projectToEdit.getInternalTechnologies(); // 避免重复关联 if(!technologyList.contains(technologyToAdd)){ technologyList.add(technologyToAdd); projectToEdit.setInternalTechnologies(technologyList); // 同步维护Technology侧的集合 if(!technologyToAdd.getInternalProjects().contains(projectToEdit)){ technologyToAdd.getInternalProjects().add(projectToEdit); } } projectRepository.save(projectToEdit); } else { throw new ResponseStatusException(HttpStatus.NOT_FOUND, String.format(PROJECT_NOT_FOUND, id)); } } }
调整后内存中techToUpdate的关联集合和数据库状态一致,最后save返回的对象会携带正确的关联数据,无需额外查库。
方案2:保存前强制刷新实体状态
如果不想修改现有关联逻辑,可以在执行完所有关联操作后,强制清空持久化上下文的旧缓存,重新加载实体状态:
首先将TechnologyRepository的继承接口从CrudRepository改为JpaRepository(JpaRepository自带实体刷新方法),然后在Controller的update方法中,执行完关联操作后加两行代码:
// 执行完关联增删逻辑后 techRep.flush(); techRep.refresh(techToUpdate); // 再执行save和返回 Technology savedTech = techRep.save(techToUpdate); return new TechReturnDTO(savedTech);
优化建议
当前先全量删除所有关联、再批量新增关联的逻辑在关联数据量大时性能较差,可以优化为先对比传入的项目ID和现有已关联项目ID,仅删除需要移除的关联、仅新增需要添加的关联,减少不必要的数据库写操作。
内容的提问来源于stack exchange,提问作者CodingPandaBear

