【问题标题】:Handling of resource links in collection of sub-entities with Spring Data REST使用 Spring Data REST 处理子实体集合中的资源链接
【发布时间】:2015-10-26 17:55:24
【问题描述】:

我正在评估 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"
    }
  }
}

顺便说一句,在第一个示例中,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 上找不到任何有类似问题的人。另外,也许这是我的模型中的一个根本缺陷,如果是这样,我已经准备好听取您的论点:-)

【问题讨论】:

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


    【解决方案1】:

    由于 Spring Data REST 本身不处理问题中描述的用例,所以第一步是停用对 Task 的属性的管理,并确保它们默认不序列化。这里@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”的新 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(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”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
        }
      }
    }
    

    【讨论】:

      【解决方案2】:

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

      所以你的方案是行不通的。我会尝试创建一个 TaskRepository,您可以导出并删除属性存储库。

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

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

      【讨论】:

      • 您好 Mathias,感谢您的回答。实际上我的任务没有设计的存储库。它们不是资源,可以看作是价值对象。我的问题是让两个内部对象引用相同资源的不同集合,并且能够区分相应的链接。
      • 我想我遇到了你的问题。如果你想要这样一个链接'localhost:8080/models/1/tasks/2/attributes'任务需要是一个资源 - 因为属性将是任务的子资源,因此在 spring 数据休息中它需要存储库。至少如果您不想要自定义控制器方法。您可以尝试处理“models/1/tasks/2/attributes”的自定义控制器方法,然后您可以尝试实现ResourceProcessor 并在 ResourceProcessor 中手动添加指向此控制器方法的链接。不确定这是否可行 - 但值得一试。
      • 是的,我得出的结论与实现我需要的方法有些相同,因为似乎无法原生处理用例。我会进行实验并带着我的发现回到这里,谢谢。
      猜你喜欢
      • 2015-11-29
      • 1970-01-01
      • 2023-04-09
      • 1970-01-01
      • 2016-08-27
      • 2018-09-04
      • 2018-08-22
      • 1970-01-01
      • 2015-03-14
      相关资源
      最近更新 更多