Spring Data Rest – Serializing the Entity ID – Spring Data Rest – 序列化实体ID

最后修改: 2022年 7月 16日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

As we know, the Spring Data Rest module can make our life easier when we want to start quickly with RESTful web services. However, this module comes with a default behavior, which can sometimes be confusing.

正如我们所知,当我们想快速开始使用RESTful Web服务时,Spring Data Rest模块可以使我们的生活更加轻松。然而,这个模块带有一个默认行为,这有时会让人感到困惑。

In this tutorial, we’ll learn why Spring Data Rest doesn’t serialize entity ids by default. Also, we’ll discuss various solutions to change this behavior.

在本教程中,我们将了解为什么Spring Data Rest默认不对实体ID进行序列化。此外,我们还将讨论改变这一行为的各种解决方案。

2. Default Behavior

2.默认行为

Before we get into details, let’s understand what we mean by serializing the entity id with a quick example.

在我们讨论细节之前,让我们通过一个简单的例子来理解我们对实体ID进行序列化的含义。

So, here is a sample entity, Person:

因此,这里有一个实体样本,Person

@Entity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    // getters and setters

}

Additionally, we have a repository, PersonRepository:

此外,我们还有一个资源库,PersonRepository

public interface PersonRepository extends JpaRepository<Person, Long> {

}

In case we’re using Spring Boot, simply adding the spring-boot-starter-data-rest dependency enables the Spring Data Rest module:

如果我们使用的是Spring Boot,只需添加spring-boot-starter-data-rest依赖,就可以启用Spring Data Rest模块。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>

With those two classes and Spring Boot’s auto-configuration, our REST controllers are automatically ready to use.

有了这两个类和Spring Boot的自动配置,我们的REST控制器就自动准备好使用了。

As a next step, let’s request the resource, http://localhost:8080/persons, and check the default JSON response generated by the framework:

作为下一步,让我们请求资源,http://localhost:8080/persons,并检查由框架生成的默认JSON响应。

{
    "_embedded" : {
        "persons" : [ {
            "name" : "John Doe",
            "_links" : {
                "self" : {
                    "href" : "http://localhost:8080/persons/1"
                },
                "person" : {
                    "href" : "http://localhost:8080/persons/1{?projection}",
                    "templated" : true
                }
            }
        }, ...]
    ...
}

We’ve omitted some parts for brevity. As we notice, only the name field is serialized for the entity Person. Somehow, the id field is stripped out.

为了简洁起见,我们省略了一些部分。正如我们注意到的,对于实体Person,只有name字段被序列化了。id字段则被剥离出来。

Accordingly, this is a design decision in Spring Data Rest. Exposing our internal ids, in most cases, isn’t ideal because they mean nothing to external systems.

因此,这是Spring Data Rest的一个设计决定。在大多数情况下,暴露我们的内部ID并不理想,因为它们对外部系统毫无意义

In an ideal situation, the identity is the URL of that resource in RESTful architectures.

在理想情况下,身份是RESTful架构中该资源的URL

We should also see that this is only the case when we use Spring Data Rest’s endpoints. Our custom @Controller or @RestController endpoints aren’t affected unless we use Spring HATEOAS‘s RepresentationModel and its children — like CollectionModel, and EntityModel — to build our responses.

我们还应该看到,只有当我们使用Spring Data Rest的端点时才会出现这种情况。我们自定义的@Controller@RestController端点不会受到影响,除非我们使用Spring HATEOASRepresentationModel及其子代–如CollectionModelEntityModel–来构建我们的响应。

Luckily, exposing entity ids is configurable. So, we still have the flexibility to enable it.

幸运的是,暴露实体ID是可配置的。所以,我们仍然可以灵活地启用它。

In the next sections, we’ll see different ways of exposing entity ids in Spring Data Rest.

在接下来的章节中,我们将看到在Spring Data Rest中暴露实体ID的不同方式。

3. Using RepositoryRestConfigurer

3.使用RepositoryRestConfigurer

The most common solution for exposing entity ids is configuring RepositoryRestConfigurer:

暴露实体ID的最常见的解决方案是配置RepositoryRestConfigurer

@Configuration
public class RestConfiguration implements RepositoryRestConfigurer {

    @Override
    public void configureRepositoryRestConfiguration(
      RepositoryRestConfiguration config, CorsRegistry cors) {
        config.exposeIdsFor(Person.class);
    }
}

Before Spring Data Rest version 3.1 – or Spring Boot version 2.1 – we’d use RepositoryRestConfigurerAdapter:

在Spring Data Rest 3.1版–或Spring Boot 2.1版之前,我们会使用RepositoryRestConfigurerAdapter

@Configuration
public class RestConfiguration extends RepositoryRestConfigurerAdapter {
    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.exposeIdsFor(Person.class);
    }
}

Although it’s similar, we should pay attention to versions. As a side note, since Spring Data Rest version 3.1  RepositoryRestConfigurerAdapter is deprecated and it has been removed in the latest 4.0.x branch.

虽然它是相似的,但我们应该注意版本。顺便提一下,自Spring Data Rest 3.1版本以来,RepositoryRestConfigurerAdapter已被废弃,在最新的4.0.x分支中,它已被移除。

After our configuration for the entity Person, the response gives us the id field as well:

在我们对实体Person进行配置后,响应也给了我们id字段。

{
    "_embedded" : {
        "persons" : [ {
            "id" : 1,
            "name" : "John Doe",
            "_links" : {
                "self" : {
                    "href" : "http://localhost:8080/persons/1"
                },
                "person" : {
                    "href" : "http://localhost:8080/persons/1{?projection}",
                    "templated" : true
                }
            }
        }, ...]
    ...
}

Apparently, when we want to enable id exposure for all of them, this solution isn’t practical if we have many entities.

显然,当我们想为所有的人启用id曝光时,如果我们有很多实体,这个解决方案并不实用。

So, let’s improve our RestConfiguration via a generic approach:

因此,让我们通过一个通用方法来改进我们的RestConfiguration

@Configuration
public class RestConfiguration implements RepositoryRestConfigurer {

    @Autowired
    private EntityManager entityManager;

    @Override
    public void configureRepositoryRestConfiguration(
      RepositoryRestConfiguration config, CorsRegistry cors) {
        Class[] classes = entityManager.getMetamodel()
          .getEntities().stream().map(Type::getJavaType).toArray(Class[]::new);
        config.exposeIdsFor(classes);
    }
}

As we use JPA to manage persistence, we can access the metadata of the entities in a generic way. JPA’s EntityManager already stores the metadata that we need. So, we can practically collect the entity class types via entityManager.getMetamodel() method.

由于我们使用JPA来管理持久性,我们可以以一种通用的方式来访问实体的元数据。JPA的EntityManager已经存储了我们需要的元数据。因此,我们实际上可以通过entityManager.getMetamodel()方法收集实体类的类型

As a result, this is a more comprehensive solution since the id exposure for every entity is automatically enabled.

因此,这是一个更全面的解决方案,因为每个实体的id暴露都是自动启用的。

4. Using @Projection

4.使用@Projection

Another solution is to use the @Projection annotation. By defining a PersonView interface, we can expose the id field too:

另一个解决方案是使用@Projection注解。通过定义一个PersonView接口,我们也可以公开id字段。

@Projection(name = "person-view", types = Person.class)
public interface PersonView {

    Long getId();

    String getName();

}

However, we should now use a different request to test, http://localhost:8080/persons?projection=person-view:

然而,我们现在应该使用一个不同的请求来测试,http://localhost:8080/persons?projection=person-view

{
    "_embedded" : {
        "persons" : [ {
            "id" : 1,
            "name" : "John Doe",
            "_links" : {
                "self" : {
                    "href" : "http://localhost:8080/persons/1"
                },
                "person" : {
                    "href" : "http://localhost:8080/persons/1{?projection}",
                    "templated" : true
                }
            }
        }, ...]
    ...
}

To enable projections for all the endpoints generated by a repository, we can use the @RepositoryRestResource annotation on the PersonRepository:

为了启用由存储库生成的所有端点的投影,我们可以在PersonRepository上使用@RepositoryRestResource注解

@RepositoryRestResource(excerptProjection = PersonView.class)
public interface PersonRepository extends JpaRepository<Person, Long> {

}

After this change, we can use our usual request, http://localhost:8080/persons, to list the person entities.

在这个改变之后,我们可以使用我们通常的请求,http://localhost:8080/persons,来列出人的实体。

However, we should note that excerptProjection doesn’t apply single item resources automatically. We still have to use http://localhost:8080/persons/1?projection=person-view to get the response for a single Person together with its entity id.

然而,我们应该注意,excerptProjection并不能自动应用单项资源。我们仍然必须使用http://localhost:8080/persons/1?projection=person-view来获取单个Person及其实体ID的响应。

Besides, we should keep in mind that the fields defined in our projection aren’t always in order:

此外,我们应该记住,在我们的预测中定义的字段并不总是按顺序排列。

{
    ...            
    "persons" : [ {
        "name" : "John Doe",
        "id" : 1,
        ...
    }, ...]
    ...
}

In order to preserve the field order, we can put the @JsonPropertyOrder annotation on our PersonView class:

为了保留字段顺序,我们可以把@JsonPropertyOrder注解放在我们的PersonView

@JsonPropertyOrder({"id", "name"})
@Projection(name = "person-view", types = Person.class)
public interface PersonView { 
    //...
}

5. Using DTOs Over Rest Repositories

5.在Rest存储库上使用DTOs

Overwriting rest controller handlers is another solution. Spring Data Rest lets us plug in custom handlers. Hence, we can still use the underlying repository to fetch the data, but overwrite the response before it reaches the client. In this case, we’ll write more code, but we’ll have the power of full customization.

覆盖休息控制器处理程序是另一种解决方案。Spring Data Rest允许我们插入自定义处理程序。因此,我们仍然可以使用底层资源库来获取数据,但在响应到达客户端之前覆盖它。在这种情况下,我们会写更多的代码,但我们会有完全自定义的能力。

5.1. Implementation

5.1.实施

First of all, we define a DTO object to represent our Person entity:

首先,我们定义一个DTO对象来代表我们的Person实体。

public class PersonDto {

    private Long id;

    private String name;

    public PersonDto(Person person) {
        this.id = person.getId();
        this.name = person.getName();
    }
    
    // getters and setters
}

As we can see, we add an id field here, which corresponds to Person‘s entity id.

我们可以看到,我们在这里添加了一个id字段,它与Person的实体ID相对应。

As a next step, we’ll use some built-in helper classes to reuse Spring Data Rest’s response building mechanism while keeping the response structure same as much as possible.

下一步,我们将使用一些内置的辅助类来重用Spring Data Rest的响应构建机制,同时尽可能地保持响应结构不变。

So, let’s define our PersonController to override the built-in endpoints:

因此,让我们定义我们的PersonController来覆盖内置的端点。

@RepositoryRestController
public class PersonController {

    @Autowired
    private PersonRepository repository;

    @GetMapping("/persons")
    ResponseEntity<?> persons(PagedResourcesAssembler resourcesAssembler) {
        Page<Person> persons = this.repository.findAll(Pageable.ofSize(20));
        Page<PersonDto> personDtos = persons.map(PersonDto::new);
        PagedModel<EntityModel<PersonDto>> pagedModel = resourcesAssembler.toModel(personDtos);
        return ResponseEntity.ok(pagedModel);
    }

}

We should notice some points here to be sure that Spring recognizes our controller class as a plug-in, rather than an independent controller:

我们应该在这里注意一些要点,以确保Spring将我们的控制器类识别为 插件,而不是一个独立的控制器。

  1. @RepositoryRestController must be used instead of @RestController or @Controller
  2. PersonController class must be placed under a package that Spring’s component scan can pick up Alternatively, we can define it explicitly using @Bean.
  3. @GetMapping path must be the same as the PersonRepository provides. If we customize the path with @RepositoryRestResource(path = “…”), then the controller’s get mapping must also reflect this.

Finally, let’s try our endpoint, http://localhost:8080/persons:

最后,让我们试试我们的端点,http://localhost:8080/persons

{
    "_embedded" : {
        "personDtoes" : [ {
            "id" : 1,
            "name" : "John Doe"
        }, ...]
    }, ...
}

We can see the id fields in the response.

我们可以看到响应中的id字段。

5.2. Drawbacks

5.2.缺点

If we go with DTO over Spring Data Rest’s repositories, we should consider a few aspects.

如果我们使用DTO而不是Spring Data Rest的存储库,我们应该考虑几个方面。

Some developers aren’t comfortable with serializing entity models directly into the response. Surely, it has some drawbacks. Exposing all entity fields could cause data leaks, unexpected lazy fetches, and performance issues.

一些开发者对直接将实体模型序列化到响应中并不习惯。当然,这也有一些缺点。暴露所有的实体字段可能会导致数据泄露、意外的懒惰获取和性能问题

However, writing our @RepositoryRestController for all endpoints is a compromise. It takes away some of the benefits of the framework. Besides, we need to maintain a lot more code in this case.

然而,为所有端点编写我们的@RepositoryRestController是一种妥协。它带走了框架的一些好处。此外,在这种情况下,我们需要维护更多的代码。

6. Conclusion

6.结语

In this article, we discussed multiple approaches for exposing entity ids when using Spring Data Rest.

在这篇文章中,我们讨论了使用Spring Data Rest时暴露实体ID的多种方法。

As usual, we can find all the code samples used in this article over on Github.

像往常一样,我们可以在Github上找到本文中使用的所有代码样本