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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 03:50:33