You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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链接,又能指定返回关联对象。

  1. 创建投影接口:
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对象
}
  1. 调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 21:20:37