Spring Data Rest – 序列化實體 ID
一、概述
眾所周知,當我們想快速開始使用 RESTful Web 服務時,Spring Data Rest 模塊可以讓我們的生活更輕鬆。但是,此模塊帶有默認行為,有時可能會令人困惑。
在本教程中,我們將了解為什麼 Spring Data Rest 默認不序列化實體 ID。此外,我們將討論改變這種行為的各種解決方案。
2. 默認行為
在我們進入細節之前,讓我們通過一個簡單的例子來理解序列化實體 id 的含義。
因此,這是一個示例實體Person
:
@Entity
public class Person {
@Id
@GeneratedValue
private Long id;
private String name;
// getters and setters
}
此外,我們還有一個存儲庫PersonRepository
:
public interface PersonRepository extends JpaRepository<Person, Long> {
}
如果我們使用 Spring Boot,只需添加spring-boot-starter-data-rest
依賴項即可啟用 Spring Data Rest 模塊:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
有了這兩個類和 Spring Boot 的自動配置,我們的 REST 控制器就可以自動使用了。
下一步,讓我們請求資源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
}
}
}, ...]
...
}
為簡潔起見,我們省略了一些部分。我們注意到,只有name
字段為實體Person.
不知何故, id
字段被剝離了。
因此,這是 Spring Data Rest 中的設計決策。在大多數情況下,暴露我們的內部 id 並不理想,因為它們對外部系統沒有任何意義。
在理想情況下,身份是 RESTful 架構中該資源的 URL 。
我們還應該看到,只有當我們使用 Spring Data Rest 的端點時才會出現這種情況。我們的自定義@Controller
或@RestController
端點不會受到影響,除非我們使用 Spring HATEOAS 的RepresentationModel
及其子項(如CollectionModel
和EntityModel
)來構建我們的響應。
幸運的是,公開實體 ID 是可配置的。因此,我們仍然可以靈活地啟用它。
在接下來的部分中,我們將看到在 Spring Data Rest 中公開實體 id 的不同方式。
3. 使用RepositoryRestConfigurer
公開實體 ID 的最常見解決方案是配置RepositoryRestConfigurer
:
@Configuration
public class RestConfiguration implements RepositoryRestConfigurer {
@Override
public void configureRepositoryRestConfiguration(
RepositoryRestConfiguration config, CorsRegistry cors) {
config.exposeIdsFor(Person.class);
}
}
在 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);
}
}
雖然類似,但要注意版本。附帶說明一下,由於 Spring Data Rest 版本 3.1 RepositoryRestConfigurerAdapter
已棄用,並且已在最新的4.0.x分支中刪除。
在我們為實體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
}
}
}, ...]
...
}
顯然,當我們想要為所有實體啟用 id 公開時,如果我們有很多實體,這個解決方案是不切實際的。
因此,讓我們通過通用方法改進我們的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);
}
}
當我們使用 JPA 來管理持久性時,我們可以以通用方式訪問實體的元數據。 JPA 的EntityManager
已經存儲了我們需要的元數據。因此,我們實際上可以通過entityManager.getMetamodel()
方法收集實體類類型。
因此,這是一個更全面的解決方案,因為每個實體的 id 公開都是自動啟用的。
4. 使用@Projection
另一種解決方案是使用@Projection
註釋。通過定義PersonView
接口,我們也可以公開id
字段:
@Projection(name = "person-view", types = Person.class)
public interface PersonView {
Long getId();
String getName();
}
但是,我們現在應該使用不同的請求進行測試, 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
}
}
}, ...]
...
}
**要為存儲庫生成的所有端點啟用投影,我們@RepositoryRestResource
**在PersonRepository
上使用 @RepositoryRestResource 註釋:
@RepositoryRestResource(excerptProjection = PersonView.class)
public interface PersonRepository extends JpaRepository<Person, Long> {
}
在此更改之後,我們可以使用我們通常的請求http://localhost:8080/persons
來列出人員實體。
但是,我們應該注意excerptProjection
不會自動應用單項資源。我們仍然必須使用http://localhost:8080/persons/1?projection=person-view
來獲取單個Person
及其實體 ID 的響應。
此外,我們應該記住,我們的投影中定義的字段並不總是按順序排列的:
{
...
"persons" : [ {
"name" : "John Doe",
"id" : 1,
...
}, ...]
...
}
為了保持字段順序,我們可以將@JsonPropertyOrder
註釋放在我們的PersonView
類上:
@JsonPropertyOrder({"id", "name"})
@Projection(name = "person-view", types = Person.class)
public interface PersonView {
//...
}
5. 在 Rest 存儲庫中使用 DTO
覆蓋休息控制器處理程序是另一種解決方案。 Spring Data Rest 允許我們插入自定義處理程序。因此,我們仍然可以使用底層存儲庫來獲取數據,但在響應到達客戶端之前覆蓋它。在這種情況下,我們將編寫更多代碼,但我們將擁有完全定制的能力。
5.1。執行
首先,我們定義一個 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
}
如我們所見,我們在這裡添加了一個id
字段,它對應於Person
的實體 id。
下一步,我們將使用一些內置的幫助類來重用 Spring Data Rest 的響應構建機制,同時盡可能保持響應結構相同。
所以,讓我們定義我們的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);
}
}
我們應該注意這裡的一些要點,以確保 Spring 將我們的控制器類識別為 插件,而不是獨立的控制器:
- 必須使用
@Controller
而不是@RestController
或@RepositoryRestController
-
PersonController
類必須放在 Spring 的組件掃描可以拾取的包下。或者,我們可以使用@Bean.
-
@GetMapping
路徑必須與PersonRepository
提供的路徑相同。如果我們使用@RepositoryRestResource(path = “…”),
那麼控制器的get 映射也必須反映這一點。
最後,讓我們試試我們的端點http://localhost:8080/persons
:
{
"_embedded" : {
"personDtoes" : [ {
"id" : 1,
"name" : "John Doe"
}, ...]
}, ...
}
我們可以在響應中看到id
字段。
5.2.缺點
如果我們在 Spring Data Rest 的存儲庫上使用 DTO,我們應該考慮幾個方面。
一些開發人員不習慣將實體模型直接序列化到響應中。當然,它有一些缺點。公開所有實體字段可能會導致數據洩漏、意外延遲提取和性能問題。
但是,為所有端點編寫@RepositoryRestController
是一種妥協。它帶走了框架的一些好處。此外,在這種情況下,我們需要維護更多的代碼。
六,結論
在本文中,我們討論了在使用 Spring Data Rest 時公開實體 ID 的多種方法。
像往常一樣,我們可以在 Github 上找到本文中使用的所有代碼示例。