Spring多对多关联保存实体时出现无限递归与持久化异常
问题分析与解决方案
一、核心问题原因
1. 单向添加无数据库修改的原因
在JPA多对多关联中,关联关系由**维护端(Owning Side)**负责同步到数据库。你的代码中:
User实体通过@JoinTable定义了关联表,是维护端;Course实体用mappedBy="courses"声明自己是反向端(Inverse Side),JPA不会监听反向端集合的修改,所以仅执行course.get().getStudents().add(user.get())时,不会生成关联表的插入语句,数据库无变化。
2. 双向添加触发StackOverflowError的原因
报错栈显示是hashCode循环调用导致的:
- 你的
User类的hashCode方法包含了courses集合; Course类的hashCode方法包含了students集合;- 当双向添加关联后,计算
User的hashCode时会遍历courses集合,每个Course的hashCode又会遍历students集合,形成无限循环,最终触发栈溢出。
二、具体修复步骤
1. 修正实体的hashCode和equals方法
实体的hashCode和equals必须基于稳定的业务主键或数据库主键(比如id、slug),绝对不能包含双向关联的集合。示例:
// User类的hashCode和equals @Override public int hashCode() { return Objects.hash(slug); // 用slug或id,不要用courses集合 } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return Objects.equals(slug, user.slug); }
// Course类的hashCode和equals @Override public int hashCode() { return Objects.hash(slug); // 用slug或id,不要用students集合 } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Course course = (Course) o; return Objects.equals(slug, course.slug); }
2. 初始化实体中的集合
在实体类中初始化集合,避免空指针,同时确保懒加载集合能正常操作:
// User类中 private Set<Course> courses = new HashSet<>(); // Course类中 private Set<User> students = new HashSet<>();
3. 正确维护双向关联
在实体中添加辅助方法,统一处理双向关联的同步,避免手动操作两边集合时出错:
// User类中 public void addCourse(Course course) { this.courses.add(course); course.getStudents().add(this); } // Course类中 public void addStudent(User user) { this.students.add(user); user.getCourses().add(this); }
注意:调用时只需要执行其中一个方法即可,比如user.addCourse(course),两边集合会自动同步。
4. 修改业务方法,操作维护端集合
因为User是维护端,所以业务逻辑中应该修改User的courses集合,或者调用辅助方法:
@Override @Transactional public ResponseEntity<String> allocateCoursesToStudents(AllocateCoursesToStudents allocateCoursesToStudents) { for (String courseSlug : allocateCoursesToStudents.getCourseSlugs()) { Optional<Course> courseOpt = coursesRepository.findBySlug(courseSlug); if (courseOpt.isEmpty()) { return ResponseEntity.badRequest().body(null); } Course course = courseOpt.get(); for (String userSlug : allocateCoursesToStudents.getUserSlugs()) { Optional<User> userOpt = userRepository.findBySlug(userSlug); if (userOpt.isEmpty()) { continue; } User user = userOpt.get(); if (!UserRolesEnum.ROLE_STUDENT.equals(user.getRole().getName())) { continue; } // 方式1:直接操作维护端集合 user.getCourses().add(course); // 方式2:调用辅助方法 // user.addCourse(course); } } return ResponseEntity.ok().body(null); }
三、补充说明
- 事务结束时,JPA会自动将持久态实体的变更同步到数据库,无需手动调用
save方法; - 懒加载集合在调用
getXxx()时会自动初始化(因为在事务内),无需额外处理; - 若需要批量操作,建议使用批量插入语句优化性能,但当前场景下先保证功能正常即可。
内容的提问来源于stack exchange,提问作者TasteMyBomb
相关产品推荐
相关产品推荐

