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

双向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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 18:22:05