JPA多对多关系中Spring REST控制器避免JSON递归的最优实现方案
JPA多对多关联REST返回的最佳实践
针对你遇到的多对多关联JSON序列化循环引用问题,同时需要两边都展示关联列表但仅保留第一层级的需求,以下是几个实用方案:
1. DTO(数据传输对象)模式(最推荐)
这是解耦领域模型和API返回结构的标准做法,完全避免循环引用问题,还能灵活控制返回字段:
- 定义
StudentDTO:包含学生的核心字段(如id、name),再加上List<CourseSimpleDTO> courses;CourseSimpleDTO只保留课程的基础信息(如id、courseName),不包含students字段。 - 定义
CourseDTO:包含课程的核心字段,再加上List<StudentSimpleDTO> students;StudentSimpleDTO只保留学生的基础信息,不包含courses字段。 - 服务层查询到JPA实体后,将实体映射为对应的DTO。可以手动映射,也可以用
MapStruct这类工具自动生成映射代码,减少重复工作。
示例代码片段:
// StudentSimpleDTO public class StudentSimpleDTO { private Long id; private String name; // 省略getter/setter } // CourseDTO public class CourseDTO { private Long id; private String courseName; private List<StudentSimpleDTO> students; // 省略getter/setter } // 服务层映射示例 public CourseDTO getCourseById(Long id) { Course course = courseRepository.findById(id).orElseThrow(); CourseDTO dto = new CourseDTO(); dto.setId(course.getId()); dto.setCourseName(course.getCourseName()); dto.setStudents(course.getStudents().stream() .map(s -> { StudentSimpleDTO sDto = new StudentSimpleDTO(); sDto.setId(s.getId()); sDto.setName(s.getName()); return sDto; }).collect(Collectors.toList())); return dto; }
2. Jackson动态过滤器(@JsonFilter)
利用Jackson的过滤器机制,在不同场景下动态控制序列化的字段:
- 先定义两个过滤器名称:
public class FilterNames { public static final String STUDENT_FILTER = "studentFilter"; public static final String COURSE_FILTER = "courseFilter"; } - 在实体类上标注对应的过滤器:
@Entity @JsonFilter(FilterNames.STUDENT_FILTER) public class Student { private Long id; private String name; @ManyToMany private List<Course> courses; // 省略其他代码 } @Entity @JsonFilter(FilterNames.COURSE_FILTER) public class Course { private Long id; private String courseName; @ManyToMany(mappedBy = "courses") private List<Student> students; // 省略其他代码 } - 在控制器中,根据返回类型配置过滤器规则:返回
Student时,过滤掉Course中的students字段;返回Course时,过滤掉Student中的courses字段。可以通过MappingJacksonValue来实现:@GetMapping("/students/{id}") public MappingJacksonValue getStudent(@PathVariable Long id) { Student student = studentRepository.findById(id).orElseThrow(); MappingJacksonValue mapping = new MappingJacksonValue(student); FilterProvider filters = new SimpleFilterProvider() .addFilter(FilterNames.COURSE_FILTER, SimpleBeanPropertyFilter.serializeAllExcept("students")); mapping.setFilters(filters); return mapping; } @GetMapping("/courses/{id}") public MappingJacksonValue getCourse(@PathVariable Long id) { Course course = courseRepository.findById(id).orElseThrow(); MappingJacksonValue mapping = new MappingJacksonValue(course); FilterProvider filters = new SimpleFilterProvider() .addFilter(FilterNames.STUDENT_FILTER, SimpleBeanPropertyFilter.serializeAllExcept("courses")); mapping.setFilters(filters); return mapping; }
3. @JsonView(视图模式)
如果需要针对不同API场景定义固定的字段集合,可以用Jackson的@JsonView:
- 定义视图接口:
public class Views { public interface StudentWithCourses {} public interface CourseWithStudents {} } - 在实体字段上标注对应的视图:
@Entity public class Student { @JsonView({Views.StudentWithCourses.class, Views.CourseWithStudents.class}) private Long id; @JsonView({Views.StudentWithCourses.class, Views.CourseWithStudents.class}) private String name; @JsonView(Views.StudentWithCourses.class) @ManyToMany private List<Course> courses; // 省略其他代码 } @Entity public class Course { @JsonView({Views.StudentWithCourses.class, Views.CourseWithStudents.class}) private Long id; @JsonView({Views.StudentWithCourses.class, Views.CourseWithStudents.class}) private String courseName; @JsonView(Views.CourseWithStudents.class) @ManyToMany(mappedBy = "courses") private List<Student> students; // 省略其他代码 } - 在控制器方法上指定要使用的视图:
@GetMapping("/students/{id}") @JsonView(Views.StudentWithCourses.class) public Student getStudent(@PathVariable Long id) { return studentRepository.findById(id).orElseThrow(); } @GetMapping("/courses/{id}") @JsonView(Views.CourseWithStudents.class) public Course getCourse(@PathVariable Long id) { return courseRepository.findById(id).orElseThrow(); }
注意:这种方式需要确保关联的实体在对应视图下不会触发嵌套序列化,比如StudentWithCourses视图下,Course的students字段没有被标注,所以不会被序列化。
内容的提问来源于stack exchange,提问作者Bruno Vignola
相关产品推荐
相关产品推荐

