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

Spring Boot JPA+Flyway实现一对多关系遇无限递归问题求助

解决Spring Boot中JPA双向关联导致的JSON无限递归问题

你使用Spring Boot 3.0.6实现JPA一对多关联(membership_plan对应多个membership_plan_features),实体类和Flyway建表语句均已编写,但调用membershipPlanRepository.findAll()查询时,返回的JSON出现无限递归,同时抛出HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError)异常。

问题根源

问题出在JPA双向关联的循环引用:MembershipPlan包含planFeature列表,每个MembershipPlanFeature又引用回MembershipPlan,Jackson序列化JSON时会不断在两者之间循环嵌套,最终触发栈溢出。

解决方案

1. 用@JsonIgnore忽略反向关联

在MembershipPlanFeature的membershipPlan字段上添加@JsonIgnore,让Jackson序列化时跳过该字段,直接切断循环链:

@Entity
@Table(name = "membership_plan_feature")
@Getter
@Setter
public class MembershipPlanFeature {
    // 其他字段保持不变

    @ManyToOne
    @JoinColumn(name="plan_id")
    @JsonIgnore // 添加此注解
    private MembershipPlan membershipPlan;
}

2. 使用@JsonManagedReference + @JsonBackReference

这是Jackson专门处理双向关联的注解组合:

  • 在一对多的MembershipPlan中,给planFeature字段加@JsonManagedReference(主动引用,会被正常序列化)
  • 在多对一的MembershipPlanFeature中,给membershipPlan字段加@JsonBackReference(反向引用,序列化时会被忽略)

修改后的实体类:

@Entity
@Table(name = "membership_plan")
public class MembershipPlan {
    // 其他字段保持不变

    @OneToMany (mappedBy = "membershipPlan")
    @JsonManagedReference // 添加此注解
    private List<MembershipPlanFeature> planFeature;
}
@Entity
@Table(name = "membership_plan_feature")
@Getter
@Setter
public class MembershipPlanFeature {
    // 其他字段保持不变

    @ManyToOne
    @JoinColumn(name="plan_id")
    @JsonBackReference // 添加此注解
    private MembershipPlan membershipPlan;
}

3. 使用@JsonIdentityInfo生成唯一标识

给实体类添加唯一标识注解,Jackson再次遇到相同实体时,会用标识代替重复的对象序列化:

@Entity
@Table(name = "membership_plan")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "planId")
public class MembershipPlan {
    // 字段内容不变
}
@Entity
@Table(name = "membership_plan_feature")
@Getter
@Setter
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class MembershipPlanFeature {
    // 字段内容不变
}

序列化后的JSON会变成:

[
    {
        "planId": 1,
        "orgId": "101",
        "planName": "GOLD",
        "planPrice": "29",
        "planFeature": [
            {
                "id": 1,
                "orgId": "101",
                "featureName": "Kanban",
                "featureDesc": "Kanban",
                "membershipPlan": 1 // 用planId代替完整对象
            }
        ]
    }
]

4. 使用DTO(数据传输对象)解耦

创建仅包含前端所需字段的DTO类,避免直接序列化JPA实体:

// MembershipPlanDTO.java
public class MembershipPlanDTO {
    private Long planId;
    private String orgId;
    private String planName;
    private String planPrice;
    private List<MembershipPlanFeatureDTO> planFeature;

    // 构造方法、getter/setter
}

// MembershipPlanFeatureDTO.java
public class MembershipPlanFeatureDTO {
    private Long id;
    private String orgId;
    private String featureName;
    private String featureDesc;

    // 构造方法、getter/setter
}

查询后将实体转换为DTO返回(可手动转换或用MapStruct等工具):

List<MembershipPlan> plans = membershipPlanRepository.findAll();
List<MembershipPlanDTO> planDTOs = plans.stream().map(plan -> {
    MembershipPlanDTO dto = new MembershipPlanDTO();
    dto.setPlanId(plan.getPlanId());
    dto.setOrgId(plan.getOrgId());
    dto.setPlanName(plan.getPlanName());
    dto.setPlanPrice(plan.getPlanPrice());
    dto.setPlanFeature(plan.getPlanFeature().stream().map(feature -> {
        MembershipPlanFeatureDTO featureDTO = new MembershipPlanFeatureDTO();
        featureDTO.setId(feature.getId());
        featureDTO.setOrgId(feature.getOrgId());
        featureDTO.setFeatureName(feature.getFeatureName());
        featureDTO.setFeatureDesc(feature.getFeatureDesc());
        return featureDTO;
    }).collect(Collectors.toList()));
    return dto;
}).collect(Collectors.toList());
return planDTOs;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 14:17:09