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

Spring Data JPA根据ID查询实体时出现无限递归问题求助

问题原因及解决方案

问题根源

无限递归的本质是实体双向关联导致的JSON序列化循环引用:

  • PlaygroundComment 持有 Playground 对象引用
  • Playground 又持有 List<PlaygroundComment> 集合引用
    当Spring MVC将查询到的PlaygroundComment实体序列化为JSON返回时,序列化框架(默认是Jackson)会不断遍历这两个关联对象,触发无限递归,最终导致栈溢出。

虽然你在Playground的@OneToMany上设置了FetchType.LAZY,但在JSON序列化时会调用getPlaygroundComments()方法,触发懒加载初始化,进而引发循环。

解决办法

方法1:用Jackson注解中断循环引用

方式A:使用@JsonIgnore

在PlaygroundComment的playground字段上添加@JsonIgnore,序列化时忽略该字段,避免反向引用:

@Entity
@Table(name = "playground_comments")
public class PlaygroundComment {
    // 其他字段...
    @ManyToOne
    @JoinColumn(name = "playground_id", nullable = false)
    @JsonIgnore // 添加这个注解
    private Playground playground;
    // getter/setter...
}

方式B:使用@JsonManagedReference和@JsonBackReference

这种方式更适合保留部分关联信息,区分双向关联的"管理端"和"反向端":

  • 在Playground的playgroundComments字段添加@JsonManagedReference(主动管理的一端,会被序列化)
  • 在PlaygroundComment的playground字段添加@JsonBackReference(反向端,不会被序列化)

修改Playground类:

@Entity
@Table(name = "playgrounds")
public class Playground {
    // 其他字段...
    @OneToMany(mappedBy = "playground", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JsonManagedReference // 添加这个注解
    private List<PlaygroundComment> playgroundComments;
    // getter/setter...
}

修改PlaygroundComment类:

@Entity
@Table(name = "playground_comments")
public class PlaygroundComment {
    // 其他字段...
    @ManyToOne
    @JoinColumn(name = "playground_id", nullable = false)
    @JsonBackReference // 添加这个注解
    private Playground playground;
    // getter/setter...
}

方法2:使用DTO(数据传输对象)

推荐这种方式,从根源上避免实体直接暴露给前端,同时更灵活控制返回字段:

  1. 创建PlaygroundCommentDTO类:
public class PlaygroundCommentDTO {
    private Long id;
    private String content;
    private Date timestamp;
    // 只保留User的必要信息
    private UserDTO user;
    // 只保留Playground的必要信息
    private PlaygroundDTO playground;

    // 构造函数、getter/setter
}

// 对应的UserDTO示例
public class UserDTO {
    private Long id;
    private String username;
    // getter/setter
}

// 对应的PlaygroundDTO示例
public class PlaygroundDTO {
    private Long id;
    private String street;
    private String city;
    // getter/setter
}
  1. 在Service层将实体转换为DTO返回:
@Override
public PlaygroundCommentDTO getById(Long id) {
    if (id == null) {
        logger.error(LogBuilder.parameterNotSet(ParameterEnum.ID.name()));
        return null;
    }
    logger.info(LogBuilder.attemptToGet(PlaygroundComment.class.getSimpleName(), id));

    Optional<PlaygroundComment> optionalResult = playgroundCommentRepository.findById(id);

    if (optionalResult.isPresent()) {
        PlaygroundComment comment = optionalResult.get();
        // 手动转换或用MapStruct等工具类简化转换
        PlaygroundCommentDTO dto = new PlaygroundCommentDTO();
        dto.setId(comment.getId());
        dto.setContent(comment.getContent());
        dto.setTimestamp(comment.getTimestamp());
        
        UserDTO userDTO = new UserDTO();
        userDTO.setId(comment.getUser().getId());
        userDTO.setUsername(comment.getUser().getUsername());
        dto.setUser(userDTO);
        
        PlaygroundDTO playgroundDTO = new PlaygroundDTO();
        playgroundDTO.setId(comment.getPlayground().getId());
        playgroundDTO.setStreet(comment.getPlayground().getStreet());
        playgroundDTO.setCity(comment.getPlayground().getCity());
        dto.setPlayground(playgroundDTO);
        
        logger.info(LogBuilder.objectGetSuccess(comment));
        return dto;
    } else {
        logger.error(LogBuilder.objectGetFail(PlaygroundComment.class.getSimpleName(), id));
        return null;
    }
}
  1. 修改Controller返回DTO:
@GetMapping(value = "/{id}")
public ResponseEntity<?> getById(@PathVariable("id") Long id) {
    // 其他逻辑...
    PlaygroundCommentDTO result = playgroundCommentService.getById(id);
    // 其他逻辑...
    return ResponseEntity.ok(result);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 16:25:17