如何在避免递归溢出时返回带root_parent_id的层级Person数据?
问题描述
我需要在接口响应中包含root_parent_id字段,但该字段对应的rootParent属性是同实体关联,为避免StackOverflowError(无限递归)我给它加了@JsonIgnore注解。下面是当前的响应结果、期望的响应结果,以及我的相关代码和初始化数据,求指导实现方案。
当前响应
[ { "id": 1, "full_name": "William", "children": [ { "id": 2, "full_name": "Henry", "children": [ { "id": 3, "full_name": "Matt", "children": [ { "id": 7, "full_name": "Sophi", "children": [] } ] }, { "id": 4, "full_name": "Alisa", "children": [ { "id": 6, "full_name": "Alexa", "children": [] } ] } ] }, { "id": 5, "full_name": "May", "children": [] } ] }, { "id": 8, "full_name": "Olivia", "children": [ { "id": 9, "full_name": "John", "children": [ { "id": 11, "full_name": "Oliver", "children": [] }, { "id": 12, "full_name": "Mia", "children": [] } ] }, { "id": 10, "full_name": "Mary", "children": [ { "id": 13, "full_name": "Evelyn", "children": [] } ] } ] } ]
期望响应
[ { "id": 1, "full_name": "William", "root_parent_id": null, "children": [ { "id": 2, "full_name": "Henry", "root_parent_id": 1, "children": [ { "id": 3, "full_name": "Matt", "root_parent_id": 1, "children": [ { "id": 7, "full_name": "Sophi", "root_parent_id": 1, "children": [] } ] }, { "id": 4, "full_name": "Alisa", "root_parent_id": 1, "children": [ { "id": 6, "full_name": "Alexa", "root_parent_id": 1, "children": [] } ] } ] }, { "id": 5, "full_name": "May", "root_parent_id": 1, "children": [] } ] }, { "id": 8, "full_name": "Olivia", "root_parent_id": null, "children": [ { "id": 9, "full_name": "John", "root_parent_id": 8, "children": [ { "id": 11, "full_name": "Oliver", "root_parent_id": 8, "children": [] }, { "id": 12, "full_name": "Mia", "root_parent_id": 8, "children": [] } ] }, { "id": 10, "full_name": "Mary", "root_parent_id": 8, "children": [ { "id": 13, "full_name": "Evelyn", "root_parent_id": 8, "children": [] } ] } ] } ]
我的代码
MODEL
@Entity @JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties({"hibernate_lazy_initializer", "handler"}) @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class Person { @Id @Getter @Setter @EqualsAndHashCode.Include private Long id; @Getter @Setter private String fullName; @ManyToOne(fetch = FetchType.LAZY) @Getter @Setter @JsonIgnore private Person parent; @ManyToOne(fetch = FetchType.LAZY) @Getter @Setter @JsonIgnore private Person rootParent; @Transient @Getter @Setter public List<Person> children = new ArrayList<>(); }
REPOSITORY
@Repository public interface PersonRepo extends JpaRepository<Person, Long> { @Query("SELECT p FROM Person p " + " WHERE p.parent.id IS NULL") List<Person> findRoots(); @Query("SELECT p FROM Person p" + " WHERE p.rootParent.id IN :rootIds ") List<Person> findChildrenInRoots(@Param("rootIds") List<Long> rootIds); }
CONTROLLER
@RestController @RequestMapping("/api/v1/person") public class PersonController { @Autowired private PersonRepo personRepo; @GetMapping @Transactional(readOnly = true) public List<Person> getChildren() { List<Person> rootCategories = personRepo.findRoots(); List<Long> rootCategoryIds = rootCategories.stream().map(Person::getId).collect(Collectors.toList()); List<Person> children = personRepo.findChildrenInRoots(rootCategoryIds); children.forEach(subCategory -> { subCategory.getParent().getChildren().add(subCategory); }); return rootCategories; } }
DATA SQL
insert into PERSON values (1,'William',null, null); insert into PERSON values (2,'Henry',1,1); insert into PERSON values (3,'Matt',2,1); insert into PERSON values (4,'Alisa',2,1); insert into PERSON values (5,'May',1,1); insert into PERSON values (6,'Alexa',4,1); insert into PERSON values (7,'Sophi',3,1); insert into PERSON values (8,'Olivia',null,null); insert into PERSON values (9,'John',8,8); insert into PERSON values (10,'Mary',8,8); insert into PERSON values (11,'Oliver',9,8); insert into PERSON values (12,'Mia',9,8); insert into PERSON values (13,'Evelyn',10,8);
解决方案
以下几种方式都能实现需求,同时避免递归问题:
方法1:新增独立的root_parent_id字段(推荐)
直接在实体中添加一个映射数据库root_parent列的字段,不关联实体对象,从根源避免递归:
@Entity @JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties({"hibernate_lazy_initializer", "handler"}) @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class Person { // 原有属性不变... // 新增:直接映射数据库列,不关联实体 @Column(name = "root_parent") @Getter @Setter private Long rootParentId; // 保留原关联字段但继续忽略序列化 @ManyToOne(fetch = FetchType.LAZY) @Getter @Setter @JsonIgnore private Person rootParent; // 其他代码不变... }
Jackson会自动将rootParentId序列化为root_parent_id,完全不会触发递归。
方法2:用@JsonGetter手动返回ID
如果不想新增字段,给rootParent添加自定义Getter方法,只返回ID:
@Entity @JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties({"hibernate_lazy_initializer", "handler"}) @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class Person { // 原有属性不变... @ManyToOne(fetch = FetchType.LAZY) @Getter @Setter @JsonIgnore private Person rootParent; // 自定义Getter,返回rootParent的ID @JsonGetter("root_parent_id") public Long getRootParentId() { return rootParent != null ? rootParent.getId() : null; } // 其他代码不变... }
这种方式无需修改数据库映射,通过Jackson注解实现字段输出,不会触发递归。
方法3:使用DTO对象(分层架构推荐)
定义专门的DTO类控制响应格式,解耦实体与响应:
public class PersonDTO { private Long id; private String fullName; private Long rootParentId; private List<PersonDTO> children = new ArrayList<>(); // 构造方法:从实体转换为DTO public PersonDTO(Person person) { this.id = person.getId(); this.fullName = person.getFullName(); this.rootParentId = person.getRootParent() != null ? person.getRootParent().getId() : null; // 递归转换子节点 this.children = person.getChildren().stream().map(PersonDTO::new).collect(Collectors.toList()); } // Getter/Setter }
然后在Controller中返回DTO:
@GetMapping @Transactional(readOnly = true) public List<PersonDTO> getChildren() { List<Person> rootCategories = personRepo.findRoots(); List<Long> rootCategoryIds = rootCategories.stream().map(Person::getId).collect(Collectors.toList()); List<Person> children = personRepo.findChildrenInRoots(rootCategoryIds); children.forEach(subCategory -> { subCategory.getParent().getChildren().add(subCategory); }); return rootCategories.stream().map(PersonDTO::new).collect(Collectors.toList()); }
这种方式后续修改响应字段更灵活,适合大型项目。
内容的提问来源于stack exchange,提问作者Cleiton Freitas
相关产品推荐
相关产品推荐

