1. 概述

在本文中,我们将深入探讨 Spring Data REST 提供的两个核心功能:Projection(投影)Excerpt(摘录)

  • Projection:允许我们为实体创建自定义视图,用于控制返回给客户端的数据字段。
  • Excerpt:是应用于资源集合的默认 Projection,常用于优化列表接口的响应内容。

这两个功能在构建 RESTful API 时非常实用,尤其适用于需要精简响应数据聚合展示信息的场景。


2. 域模型定义

我们以两个实体为例:BookAuthor,它们之间是多对多关系。

Book 实体

@Entity
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(nullable = false)
    private String title;

    private String isbn;

    @ManyToMany(mappedBy = "books", fetch = FetchType.EAGER)
    private List<Author> authors;
}

Author 实体

@Entity
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(nullable = false)
    private String name;

    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(
        name = "book_author",
        joinColumns = @JoinColumn(name = "book_id", referencedColumnName = "id"),
        inverseJoinColumns = @JoinColumn(name = "author_id", referencedColumnName = "id"))
    private List<Book> books;
}

Repository 接口

public interface BookRepository extends CrudRepository<Book, Long> {}
public interface AuthorRepository extends CrudRepository<Author, Long> {}

启动项目后,我们可以通过如下接口访问书籍详情:

GET http://localhost:8080/books/1

默认返回内容如下:

{
  "title": "Animal Farm",
  "isbn": "978-1943138425",
  "_links": {
    "self": { "href": "http://localhost:8080/books/1" },
    "book": { "href": "http://localhost:8080/books/1" },
    "authors": { "href": "http://localhost:8080/books/1/authors" }
  }
}

可以看到,authors 字段并未直接嵌入,而是以链接形式存在。如果我们希望自定义返回字段,就需要使用 Projection。


3. 创建 Projection

Projection 允许我们为实体创建定制化的数据视图,适用于需要返回特定字段或组合信息的场景。

定义一个 Projection 接口

@Projection(name = "customBook", types = { Book.class })
public interface CustomBook {
    String getTitle();
}

@Projection 注解用于定义一个投影接口,name 是投影名称,types 指定适用的实体类型。

访问方式如下:

GET http://localhost:8080/books/1?projection=customBook

返回结果只包含 title 字段:

{
  "title": "Animal Farm",
  "_links": {
    ...
  }
}

配置 Projection 所在包(可选)

如果你的 Projection 没有放在 Spring Boot 主类的子包下,可以通过如下方式注册:

@Configuration
public class RestConfig implements RepositoryRestConfigurer {

    @Override
    public void configureRepositoryRestConfiguration(
        RepositoryRestConfiguration config, CorsRegistry cors) {
        config.getProjectionConfiguration().addProjection(CustomBook.class);
    }
}

4. 在 Projection 中添加额外字段

Projection 不仅可以筛选字段,还能添加原本不返回的数据,甚至可以嵌入关联对象或计算值。

4.1 显示隐藏字段(如 id)

默认情况下,Spring Data REST 不会返回 id 字段,但我们可以通过 @Value("#{target.id}") 强制显示:

@Projection(name = "customBook", types = { Book.class })
public interface CustomBook {
    @Value("#{target.id}")
    long getId();

    String getTitle();
}

返回结果:

{
  "id": 1,
  "title": "Animal Farm",
  "_links": { ... }
}

⚠️ 如果字段被 @JsonIgnore 注解修饰,也可以通过这种方式强制显示。

4.2 添加计算字段(如作者数量)

我们可以使用 SpEL 表达式来添加计算字段:

@Projection(name = "customBook", types = { Book.class })
public interface CustomBook {
    @Value("#{target.id}")
    long getId();

    String getTitle();

    @Value("#{target.authors.size()}")
    int getAuthorCount();
}

返回结果:

{
  "id": 1,
  "title": "Animal Farm",
  "authorCount": 1,
  "_links": { ... }
}

4.3 嵌入关联资源(如作者信息)

如果我们希望直接嵌入关联对象,而不是通过链接访问,可以如下定义:

@Projection(name = "customBook", types = { Book.class })
public interface CustomBook {
    @Value("#{target.id}")
    long getId();

    String getTitle();

    List<Author> getAuthors();

    @Value("#{target.authors.size()}")
    int getAuthorCount();
}

返回结果:

{
  "id": 1,
  "title": "Animal Farm",
  "authors": [
    {
      "name": "George Orwell"
    }
  ],
  "authorCount": 1,
  "_links": { ... }
}

5. 使用 Excerpt 作为集合资源的默认视图

Excerpt 是一种特殊的 Projection,用于设置资源集合的默认视图

设置默认视图

修改 BookRepository

@RepositoryRestResource(excerptProjection = CustomBook.class)
public interface BookRepository extends CrudRepository<Book, Long> {}

访问书籍列表接口:

GET http://localhost:8080/books

返回结果将自动使用 CustomBook 投影:

{
  "_embedded": {
    "books": [
      {
        "id": 1,
        "title": "Animal Farm",
        "authors": [
          {
            "name": "George Orwell"
          }
        ],
        "authorCount": 1,
        "_links": { ... }
      }
    ]
  },
  "_links": { ... }
}

✅ Excerpt 只对集合资源生效,单个资源仍需使用 ?projection=xxx 参数。

踩坑提醒

  • ❌ 不建议将 Projection 作为单个资源的默认视图,因为这样会导致部分更新困难
  • ✅ Projection 和 Excerpt 都是只读的,不适用于写入操作。

6. 总结

通过本文,你应该已经掌握了:

  • ✅ 如何使用 Projection 创建自定义实体视图
  • ✅ 如何在 Projection 中添加隐藏字段、计算字段、嵌入关联对象
  • ✅ 如何使用 Excerpt 设置资源集合的默认视图
  • ✅ Projection 和 Excerpt 的适用场景与限制

这些功能在构建轻量级、高定制化的 RESTful API 时非常实用,建议在实际项目中根据需求灵活使用。

完整示例代码可参考 GitHub:Spring Data REST Projections 示例


原始标题:Projections and Excerpts in Spring Data REST