Spring Boot中Author与Book双向关联避免循环引用方案咨询
解决双向关联序列化循环引用+双向展示关联信息的三种方案
方案一:用DTO分层(推荐,最灵活可控)
实体类只负责映射数据库,不处理JSON序列化逻辑,完全用DTO来控制返回的字段:
- 定义精简的子DTO:
AuthorSimpleDTO:只包含id、name字段BookSimpleDTO:只包含id、title字段
- 定义主DTO:
BookDTO:包含书籍本身的所有字段,加上AuthorSimpleDTO authorAuthorDTO:包含作者本身的所有字段,加上List<BookSimpleDTO> books
- 服务层查询后,把实体转换为对应的DTO(可以用MapStruct简化转换)
代码示例:
// Author实体(无JSON注解) @Entity public class Author { @Id @GeneratedValue private Long id; private String name; @OneToMany(mappedBy = "author") private List<Book> books; // getter/setter } // Book实体(无JSON注解) @Entity public class Book { @Id @GeneratedValue private Long id; private String title; @ManyToOne private Author author; // getter/setter } // AuthorSimpleDTO public class AuthorSimpleDTO { private Long id; private String name; // getter/setter } // BookSimpleDTO public class BookSimpleDTO { private Long id; private String title; // getter/setter } // BookDTO public class BookDTO { private Long id; private String title; private AuthorSimpleDTO author; // getter/setter } // AuthorDTO public class AuthorDTO { private Long id; private String name; private List<BookSimpleDTO> books; // getter/setter } // 服务层转换示例 @Service public class BookService { public List<BookDTO> getAllBooks() { List<Book> books = bookRepository.findAll(); return books.stream().map(book -> { BookDTO dto = new BookDTO(); dto.setId(book.getId()); dto.setTitle(book.getTitle()); AuthorSimpleDTO authorDto = new AuthorSimpleDTO(); authorDto.setId(book.getAuthor().getId()); authorDto.setName(book.getAuthor().getName()); dto.setAuthor(authorDto); return dto; }).collect(Collectors.toList()); } }
优点:完全解耦实体和返回格式,想返回什么字段就定义什么,彻底避免循环引用,适合复杂业务场景。
方案二:用@JsonIdentityInfo注解(最简单快捷)
给两个实体类加上@JsonIdentityInfo,让Jackson识别对象的唯一ID,遇到重复引用时只输出ID,不会递归序列化:
@Entity @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public class Author { @Id @GeneratedValue private Long id; private String name; @OneToMany(mappedBy = "author") private List<Book> books; // getter/setter } @Entity @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public class Book { @Id @GeneratedValue private Long id; private String title; @ManyToOne private Author author; // getter/setter }
效果:查询书籍时,书籍里的作者会输出完整信息,作者里的书籍列表只会输出每本书的id;查询作者时,作者里的书籍会输出完整信息,书籍里的作者只会输出id。既满足双向展示,又不会循环溢出。
优点:代码改动最小,不用额外写DTO,适合简单场景。
方案三:用@JsonView实现不同场景的序列化
定义两个视图接口,分别对应查询书籍和查询作者的场景,然后在实体的字段上标注对应的视图:
- 定义视图接口:
public class Views { public interface BookWithAuthor {} public interface AuthorWithBooks {} }
- 实体类上标注视图:
@Entity public class Author { @Id @GeneratedValue private Long id; @JsonView({Views.BookWithAuthor.class, Views.AuthorWithBooks.class}) private String name; @JsonView(Views.AuthorWithBooks.class) @OneToMany(mappedBy = "author") private List<Book> books; // getter/setter } @Entity public class Book { @Id @GeneratedValue private Long id; @JsonView({Views.BookWithAuthor.class, Views.AuthorWithBooks.class}) private String title; @JsonView(Views.BookWithAuthor.class) @ManyToOne private Author author; // getter/setter }
- 控制器上指定视图:
@RestController public class BookController { @GetMapping("/books") @JsonView(Views.BookWithAuthor.class) public List<Book> getAllBooks() { return bookRepository.findAll(); } } @RestController public class AuthorController { @GetMapping("/authors") @JsonView(Views.AuthorWithBooks.class) public List<Author> getAllAuthors() { return authorRepository.findAll(); } }
效果:查询/books时,返回的书籍会包含完整作者信息,但作者里不会带出书籍列表;查询/authors时,返回的作者会包含书籍列表,但书籍里不会带出作者信息。刚好满足需求,同时避免循环。
优点:不用写DTO,通过视图控制序列化字段,适合需要多场景返回不同格式的情况。
内容的提问来源于stack exchange,提问作者Greeed
相关产品推荐
相关产品推荐

