使用Spring Data REST处理子实体集合中的资源链接

时间:2015-10-26 17:55:24

标签: java spring-data-rest hateoas spring-hateoas

我正在评估Spring Data REST作为基于AngularJS的应用程序的后端。我很快将我们的域建模为一组聚合根,并遇到以下设计障碍:

  • 模型资源
  • 多个任务实体
  • 引用多个属性资源

我希望HAL _links 将属性放在每个任务JSON对象中,但遗憾的是,这些属性只能作为JSON构造根目录的链接显示。

E.g。我明白了:

{
  "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> {
}

在那里,我可能错过了一些东西,因为这似乎是一个非常简单的用例,但在SO上找不到任何有类似问题的人。也许,这可能是我模型中的一个根本缺陷,如果是这样,我准备好听你的论点: - )

2 个答案:

答案 0 :(得分:1)

由于Spring Data REST本身不处理问题中描述的用例,因此第一步是停用Task任务属性的管理,并确保默认情况下不对它们进行序列化。这里@RestResource(exported=false)确保不会为&#34;属性&#34;自动生成(非工作)链接。 rel,@JsonIgnore确保默认情况下不会呈现属性。

@Entity
public class Task {
    @Id
    @GeneratedValue
    public Long id;

    public String name;

    @ManyToMany
    @RestResource(exported = false)
    @JsonIgnore
    public List<Attribute> attributes;
}

接下来,_links属性仅在我们资源的根目录中可用,因此我选择实现名为&#34; taskAttributes&#34;的新rel,它将具有多个值,每个值一个任务。要将这些链接添加到资源,我构建了自定义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:

@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();
    }
}

这使得人们可以获得属性属性的子集,而无需从&#34; taskAttributes&#34;中获取它们。 REL:

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
    }
  }
}

答案 1 :(得分:0)

您没有任务的存储库 - 在Spring数据中,如果您没有存储库,则没有控制器。我认为如果任务只包含一个属性,你会得到一个链接 - 但你有一个Set - 所以对属性的访问将是任务资源的子资源。

所以你的场景不起作用。我会尝试使用您导出的TaskRepository并删除属性存储库。

然后您的模型资源将包含其任务的链接,任务资源将嵌入属性。

如果您仍希望将任务内联到模型资源中,则可以使用投影。