Spring Data REST中@ManyToOne关联数据未在分页JSON返回的解决咨询
问题
刚接触Spring Boot,使用Spring Data REST JPA的默认Repository API获取数据库数据并转为JSON格式。已在Song与Artist实体中配置@ManyToOne关联关系,但服务器返回的带分页的JSON结果中未包含Artist对象;测试编写的无分页Controller接口可正常返回Artist,但希望保留PagingAndSortingRepository的分页功能同时返回关联的Artist对象。
当前返回结果
{ "_embedded": { "songs": [ { "id": 1, "title": "SongTitle", "genre": "Rap", "length": 500, "_links": { "self": { "href": "http://localhost:8080/api/songs/1" }, "song": { "href": "http://localhost:8080/api/songs/1" }, "artist": { "href": "http://localhost:8080/api/songs/1/artist" } } } ] }, "_links": { "first": { "href": "http://localhost:8080/api/songs?page=0&size=1" }, "self": { "href": "http://localhost:8080/api/songs?size=1" }, "next": { "href": "http://localhost:8080/api/songs?page=1&size=1" }, "last": { "href": "http://localhost:8080/api/songs?page=19&size=1" }, "profile": { "href": "http://localhost:8080/api/profile/songs" } }, "page": { "size": 1, "totalElements": 20, "totalPages": 20, "number": 0 } }
期望返回结果
{ "_embedded": { "songs": [ { "id": 1, "title": "SongTitle", "genre": "Rap", "length": 500, "artist": { "id": 1, "name": "Artistname" }, "_links": { "self": { "href": "http://localhost:8080/api/songs/1" }, "song": { "href": "http://localhost:8080/api/songs/1" }, "artist": { "href": "http://localhost:8080/api/songs/1/artist" } } } ] }, "_links": { "first": { "href": "http://localhost:8080/api/songs?page=0&size=1" }, "self": { "href": "http://localhost:8080/api/songs?size=1" }, "next": { "href": "http://localhost:8080/api/songs?page=1&size=1" }, "last": { "href": "http://localhost:8080/api/songs?page=19&size=1" }, "profile": { "href": "http://localhost:8080/api/profile/songs" } }, "page": { "size": 1, "totalElements": 20, "totalPages": 20, "number": 0 } }
相关代码
Song.java
@Getter @Setter @Entity @Table(name = "song") public class Song { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false, unique = true) private Long id; @NotNull @NotBlank(message = "The song has to have a title") private String title; @NotNull @NotBlank(message = "The song has to have a genre") private String genre; @NotNull @Min(value = 1, message = "The song has to have a song length in seconds") private int length; @ManyToOne(cascade = CascadeType.ALL) @JoinColumn(name = "artist_id", referencedColumnName = "id") private Artist artist; public Song() { } public Song(String title, Artist artist, String genre, int length) { this.title = title; this.artist = artist; this.genre = genre; this.length = length; } public void setArtist(Artist artist) { this.artist = artist; } public Artist getArtist() { return artist; } }
Artist.java
@Getter @Setter @Entity @Table(name = "artist") public class Artist { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) private Long id; @NotNull @NotBlank(message = "The artist has to have a name") private String name; @JsonIgnore @OneToMany(mappedBy = "artist") private List<Song> songs; public Artist() { } public Artist(String name) { this.name = name; } }
测试用Controller方法
@GetMapping List<Song> getSongs() { return songRepository.findAll(); }
解决方案
方法一:使用Spring Data REST投影(Projection)
投影是Spring Data REST提供的自定义返回字段的方式,既能保留默认分页和HAL链接,又能指定返回关联对象。
- 创建投影接口:
import org.springframework.data.rest.core.config.Projection; @Projection(name = "withArtist", types = Song.class) public interface SongWithArtistProjection { Long getId(); String getTitle(); String getGenre(); int getLength(); Artist getArtist(); // 直接返回关联的Artist对象 }
- 调用API时指定投影:
请求URL改为:GET http://localhost:8080/api/songs?projection=withArtist
返回结果会包含Artist对象,同时保留分页信息和所有默认链接。
方法二:全局配置默认投影
如果希望所有请求默认返回Artist对象,无需在URL指定参数,可以通过配置类设置全局默认投影:
import org.springframework.context.annotation.Configuration; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer; import org.springframework.web.servlet.config.annotation.CorsRegistry; @Configuration public class RestConfig implements RepositoryRestConfigurer { @Override public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) { // 暴露实体ID(可选) config.exposeIdsFor(Song.class, Artist.class); // 设置Song的默认投影为包含Artist的投影 config.getProjectionConfiguration().setDefaultProjection(SongWithArtistProjection.class); } }
配置后,直接访问GET http://localhost:8080/api/songs就会返回包含Artist的分页结果。
方法三:自定义带分页的Controller
如果不想依赖Spring Data REST的默认API,可以自己编写Controller,利用PagingAndSortingRepository的分页能力,同时返回包含关联对象的结果:
import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/songs") public class SongController { private final SongRepository songRepository; // 构造函数注入Repository public SongController(SongRepository songRepository) { this.songRepository = songRepository; } @GetMapping public Page<Song> getSongs(Pageable pageable) { // 直接调用分页查询,若存在懒加载N+1问题,可使用下面的自定义Repository方法 return songRepository.findAll(pageable); } }
优化:解决懒加载N+1问题
为避免多次查询数据库,可在Repository中定义带JOIN FETCH的查询方法:
import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.PagingAndSortingRepository; public interface SongRepository extends PagingAndSortingRepository<Song, Long> { @Query("SELECT s FROM Song s JOIN FETCH s.artist") Page<Song> findAllWithArtist(Pageable pageable); }
然后修改Controller中的调用:
@GetMapping public Page<Song> getSongs(Pageable pageable) { return songRepository.findAllWithArtist(pageable); }
内容的提问来源于stack exchange,提问作者Dusto
相关产品推荐
相关产品推荐

