Reputation: 41
I am in the process of evaluating Spring Data REST as a backend for an AngularJS based application. I quickly modeled our domain as a set of aggregate roots and hit the following design roadblock:
I expected the HAL _links for the attributes to be placed inside each of the task JSON object, but sadly the attributes are only visible as a link at the root of the JSON construct.
E.g. I get this:
{
"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"
}
}
}
Instead of something I would image could be like:
{
"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"
}
}
}
Incidentally, in the first example, the attributes link ends in a 404.
I haven't seen anything in the HAL spec to handle this kind of cases, nor in the Spring Data REST documentation. Obviously, I could define the task as a resource to workaround the problem, however my model does not require this. I feel like this is a legitimate use case.
I created a simple Spring Boot application that reproduces this issue. The models:
@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;
}
And repositories:
@RepositoryRestResource
public interface ModelRepository extends PagingAndSortingRepository<Model, Long> {
}
@RepositoryRestResource
public interface AttributeRepository extends PagingAndSortingRepository<Attribute,Long> {
}
There, I may have missed something as this seems like quite a simple use case but could not find anyone with a similar problem on SO. Also, maybe this is a fundamental flaw in my model, and if so I'm ready to hear your arguments :-)
Upvotes: 2
Views: 3410
Reputation: 41
Because Spring Data REST does not handle natively the use case described in the question, the first step is to deactivate the management of the Task's Attributes, and ensure they are not serialized by default. Here the @RestResource(exported=false)
ensures that a (non working) link will not get automatically generated for an "attributes" rel, and the @JsonIgnore
ensures that attributes will not be rendered by default.
@Entity
public class Task {
@Id
@GeneratedValue
public Long id;
public String name;
@ManyToMany
@RestResource(exported = false)
@JsonIgnore
public List<Attribute> attributes;
}
Next, the _links
attribute is only available at the root of our resource, so I chose to implement a new rel named "taskAttributes", that will have multiple values, one for each of the task. To add those links to the resource, I built a custom ResourceProcessor
, and to implement the actual endpoints, a custom 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));
}
}
This makes a call to http://localhost:8080/api/models/1
return something like this:
{
"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"
}
]
}
}
Finally, to make all this more usable from the UI, I added a Projection on the Model resource:
@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();
}
}
Which lets one get a subset of the Attribute's properties without the need to fetch them from the "taskAttributes" rel:
http://localhost:8080/api/models/1?projection=ui
returns something like this:
{
"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
}
}
}
Upvotes: 1
Reputation: 12184
You do not have a repository for Tasks - in spring data rest you do not have a controller if you do not have a repository. I think you would get a link if a task would just hold one attribute - but you have a Set - so the access to the attributes would be a sub-resource of the task resource.
So your scenario just does not work. I would try to have a TaskRepository that you export and remove the attribute repository.
Then your model resource would contain the link to its task and the task resource would embed the attributes.
You could work with projections if you still wanted to have the tasks inlined into your model resource.
Upvotes: 0