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(数据传输对象)
推荐这种方式,从根源上避免实体直接暴露给前端,同时更灵活控制返回字段:
- 创建
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 }
- 在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; } }
- 修改Controller返回DTO:
@GetMapping(value = "/{id}") public ResponseEntity<?> getById(@PathVariable("id") Long id) { // 其他逻辑... PlaygroundCommentDTO result = playgroundCommentService.getById(id); // 其他逻辑... return ResponseEntity.ok(result); }
内容的提问来源于stack exchange,提问作者Dstat93
相关产品推荐
相关产品推荐

