我正在评估 Spring Data REST 作为基于 AngularJS 的应用程序的后端。我快速将我们的域建模为一组聚合根,并遇到了以下设计障碍:
- 模型资源具有
- 多个任务实体
- 引用多个属性资源
我希望将属性的 HAL _links 放置在每个任务 JSON 对象内,但遗憾的是这些属性仅作为 JSON 构造根部的链接可见。
例如我明白了:
{
"version": 0,
"name": "myModel",
"tasks": [
{
"name": "task1"
},
{
"name": "task2"
}
],
"_links": {
"self": {
"href": "http://localhost:8080/models/1"
},
"attributes": {
"href": "http://localhost:8080/models/1/attributes"
}
}
}
我想象的可能是这样的:
{
"version": 0,
"name": "myModel",
"tasks": [
{
"name": "task1",
"_links": {
"attributes": {
"href": "http://localhost:8080/models/1/tasks/1/attributes"
}
}
},
{
"name": "task2",
"_links": {
"attributes": {
"href": "http://localhost:8080/models/1/tasks/2/attributes"
}
}
],
"_links": {
"self": {
"href": "http://localhost:8080/models/1"
},
"attributes": {
"href": "http://localhost:8080/models/1/attributes"
}
}
}
顺便说一句,在第一个示例中,属性链接以 404 结尾。
我没有在 HAL 规范中看到任何处理这种情况的内容,也没有在 Spring Data REST 文档中看到任何内容。显然,我可以将任务定义为解决问题的资源,但我的模型不需要这样做。我觉得这是一个合法的用例。
我创建了一个简单的 Spring Boot 应用程序来重现此问题。型号:
@Entity
public class Model {
@Id @GeneratedValue public Long id;
@Version public Long version;
public String name;
@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
public List<Task> tasks;
}
@Entity
public class Task {
@Id @GeneratedValue public Long id;
public String name;
@ManyToMany
public Set<Attribute> attributes;
}
@Entity
public class Attribute {
@Id @GeneratedValue public Long id;
@Version public Long version;
public String name;
}
和存储库:
@RepositoryRestResource
public interface ModelRepository extends PagingAndSortingRepository<Model, Long> {
}
@RepositoryRestResource
public interface AttributeRepository extends PagingAndSortingRepository<Attribute,Long> {
}
在那里,我可能错过了一些东西,因为这似乎是一个非常简单的用例,但找不到任何有类似问题的人。另外,也许这是我的模型中的一个根本缺陷,如果是这样,我准备听听你的论点:-)
最佳答案
由于 Spring Data REST 本身不处理问题中描述的用例,因此第一步是停用任务属性的管理,并确保默认情况下它们不会被序列化。这里是@RestResource(exported=false)
确保不会为“属性”rel 自动生成(非工作)链接,并且 @JsonIgnore
确保默认情况下不会渲染属性。
@Entity
public class Task {
@Id
@GeneratedValue
public Long id;
public String name;
@ManyToMany
@RestResource(exported = false)
@JsonIgnore
public List<Attribute> attributes;
}
接下来,_links
属性仅在我们资源的根部可用,因此我选择实现一个名为“taskAttributes”的新关系,它将有多个值,每个值对应一个任务。为了将这些链接添加到资源,我构建了一个自定义 ResourceProcessor
,并实现实际端点,自定义 ModelController
:
@Component
public class ModelResourceProcessor implements ResourceProcessor<Resource<Model>> {
@Override
public Resource<Model> process(Resource<Model> modelResource) {
Model model = modelResource.getContent();
for (int i = 0; i < model.tasks.size(); i++) {
modelResource.add(linkTo(ModelController.class, model.id)
.slash("task")
.slash(i)
.slash("attributes")
.withRel("taskAttributes"));
}
return modelResource;
}
}
@RepositoryRestController
@RequestMapping("/models/{id}")
public class ModelController {
@RequestMapping(value = "/task/{index}/attributes", method = RequestMethod.GET)
public ResponseEntity<Resources<PersistentEntityResource>> taskAttributes(
@PathVariable("id") Model model,
@PathVariable("index") int taskIndex,
PersistentEntityResourceAssembler assembler) {
if (model == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
if (taskIndex < 0 || taskIndex >= model.tasks.size()) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
List<Attribute> attributes = model.tasks.get(taskIndex).attributes;
List<PersistentEntityResource> resources = attributes.stream()
.map(t -> assembler.toResource(t))
.collect(toList());
return ResponseEntity.ok(new Resources(resources));
}
}
这会调用 http://localhost:8080/api/models/1
返回类似这样的内容:
{
"name": "myModel",
"tasks": [
{
"name": "task1"
},
{
"name": "task2"
}
],
"_links": {
"self": {
"href": "http://localhost:8080/models/1"
},
"model": {
"href": "http://localhost:8080/models/1{?projection}",
"templated": true
},
"taskAttributes": [
{
"href": "http://localhost:8080/models/1/task/0/attributes"
},
{
"href": "http://localhost:8080/models/1/task/1/attributes"
}
]
}
}
最后,为了使所有这些在 UI 中更可用,我在模型资源上添加了一个投影:
@Projection(name = "ui", types = {Model.class, Attribute.class})
public interface ModelUiProjection {
String getName();
List<TaskProjection> getTasks();
public interface TaskProjection {
String getName();
List<AttributeUiProjection> getAttributes();
}
public interface AttributeUiProjection {
String getName();
}
}
这可以让我们获得属性属性的子集,而无需从“taskAttributes”相关中获取它们:
http://localhost:8080/api/models/1?projection=ui
返回类似这样的内容:
{
"name": "myModel",
"tasks": [
{
"name": "task1",
"attributes": [
{
"name": "attrForTask1",
"_links": {
"self": {
"href": "http://localhost:8080/attributes/1{?projection}",
"templated": true
}
}
}
]
},
{
"name": "task2",
"attributes": [
{
"name": "attrForTask2",
"_links": {
"self": {
"href": "http://localhost:8080/attributes/2{?projection}",
"templated": true
}
}
},
{
"name": "anotherAttrForTask2",
"_links": {
"self": {
"href": "http://localhost:8080/attributes/3{?projection}",
"templated": true
}
}
},
...
]
}
],
"_links": {
"self": {
"href": "http://localhost:8080/models/1"
},
"model": {
"href": "http://localhost:8080/models/1{?projection}",
"templated": true
}
}
}
关于java - 使用 Spring Data REST 处理子实体集合中的资源链接,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33352437/