双向Portfolio关联引发StackOverflowError的修复及需求实现咨询
JPA双向循环关联导致栈溢出的问题解决
问题背景
使用JPA构建Portfolio实体关联时,因双向循环关联(如ID33和34的Portfolio互相作为子级),调用PortfolioService.getAllPortfolios()时,PortfolioResponseDto.toDTO方法触发无限递归,导致栈溢出错误。
相关实体及代码如下:
Portfolio实体
@Entity @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor public class Portfolio { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Integer id; @OneToMany(mappedBy="parent" ,fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) @JsonManagedReference private Set<PortfolioChildMap> parents; }
PortfolioChildMap实体
@Entity @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class PortfolioChildMap { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Integer id; @ManyToOne @JsonBackReference @JoinColumn(name = "parent_id") private Portfolio parent; @ManyToOne @JoinColumn(name = "child_id") private Portfolio child; }
PortfolioResponseDto
@Data @Builder public class PortfolioResponseDto { private Integer id; private List<PortfolioResponseDto> children; public static PortfolioResponseDto toDTO(Portfolio entity) { return PortfolioResponseDto.builder() .id(entity.getId()) .children(entity.getParents().stream().map(parent-> toDTO(parent.getChild())).collect(Collectors.toList())) .build(); } }
PortfolioService
public class PortfolioService { private final PortfolioRepository portfolioRepository; @Override public List<PortfolioResponseDto> getAllPortfolios() { return portfolioRepository.findAll().stream() .map(PortfolioResponseDto::toDTO).collect(Collectors.toList()); } }
问题解答
1. 修复栈溢出问题
核心原因是无限递归转换DTO:循环关联的Portfolio会互相调用toDTO方法,直到栈内存耗尽。解决思路是跟踪已处理的Portfolio ID,避免重复递归:
修改PortfolioResponseDto的toDTO方法,新增一个集合记录已访问的ID:
import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; @Data @Builder public class PortfolioResponseDto { private Integer id; private List<PortfolioResponseDto> children; // 对外暴露的入口方法,初始化已访问集合 public static PortfolioResponseDto toDTO(Portfolio entity) { return toDTO(entity, new HashSet<>()); } // 内部递归方法,携带已访问ID集合 private static PortfolioResponseDto toDTO(Portfolio entity, Set<Integer> visited) { // 如果当前Portfolio已处理过,直接返回仅含ID的DTO,避免循环 if (visited.contains(entity.getId())) { return PortfolioResponseDto.builder() .id(entity.getId()) .children(Collections.emptyList()) .build(); } visited.add(entity.getId()); return PortfolioResponseDto.builder() .id(entity.getId()) .children(entity.getParents().stream() .map(parent -> toDTO(parent.getChild(), visited)) .collect(Collectors.toList())) .build(); } }
这样处理后,遇到循环关联的节点时会停止递归,避免栈溢出。
2. 仅获取父级第一个子级是否需要自定义Repository?
不需要自定义Repository,直接在DTO转换层处理即可,更简单高效:
修改toDTO方法中children的生成逻辑,只取parents集合的第一个元素进行转换:
// 内部递归方法,携带已访问ID集合 private static PortfolioResponseDto toDTO(Portfolio entity, Set<Integer> visited) { if (visited.contains(entity.getId())) { return PortfolioResponseDto.builder() .id(entity.getId()) .children(Collections.emptyList()) .build(); } visited.add(entity.getId()); return PortfolioResponseDto.builder() .id(entity.getId()) .children(entity.getParents().stream() .findFirst() // 仅取第一个关联的子级 .map(parent -> toDTO(parent.getChild(), visited)) .map(Collections::singletonList) // 转成List格式 .orElse(Collections.emptyList())) // 无关联时返回空List .build(); }
如果担心EAGER加载所有parents的性能问题,才需要自定义Repository查询(比如用JPQL只查询每个Portfolio的第一条关联记录),但单纯满足“取第一个子级就停止”的需求,DTO层处理足够。
内容的提问来源于stack exchange,提问作者Romillion
相关产品推荐
相关产品推荐

