Use CollectionModel/EntityModel instead of Resources/Resource.
This commit is contained in:
@@ -125,10 +125,10 @@ class EmployeeController {
|
||||
...
|
||||
|
||||
@GetMapping("/employees")
|
||||
ResponseEntity<Resources<Resource<Employee>>> findAll() {
|
||||
ResponseEntity<CollectionModel<EntityModel<Employee>>> findAll() {
|
||||
|
||||
List<Resource<Employee>> employeeResources = StreamSupport.stream(repository.findAll().spliterator(), false)
|
||||
.map(employee -> new Resource<>(employee,
|
||||
List<EntityModel<Employee>> employeeResources = StreamSupport.stream(repository.findAll().spliterator(), false)
|
||||
.map(employee -> new EntityModel<>(employee,
|
||||
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel()
|
||||
.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, employee.getId())))
|
||||
.andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(employee.getId()))),
|
||||
@@ -136,7 +136,7 @@ class EmployeeController {
|
||||
))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(new Resources<>(employeeResources,
|
||||
return ResponseEntity.ok(new CollectionModel<>(employeeResources,
|
||||
linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel()
|
||||
.andAffordance(afford(methodOn(EmployeeController.class).newEmployee(null)))));
|
||||
}
|
||||
@@ -146,7 +146,7 @@ class EmployeeController {
|
||||
|
||||
Employee savedEmployee = repository.save(employee);
|
||||
|
||||
return new Resource<>(savedEmployee,
|
||||
return new EntityModel<>(savedEmployee,
|
||||
linkTo(methodOn(EmployeeController.class).findOne(savedEmployee.getId())).withSelfRel()
|
||||
.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, savedEmployee.getId())))
|
||||
.andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(savedEmployee.getId()))),
|
||||
@@ -171,7 +171,7 @@ class EmployeeController {
|
||||
Look at these controller details:
|
||||
|
||||
* A *GET* call for the aggregate collection is laid out. It uses the repository's `findAll()` method and transforms it
|
||||
into a `Resources<Resource<Employee>>`.
|
||||
into a `CollectionModel<EntityModel<Employee>>`.
|
||||
* A *POST* call for creating new employees is also defined, on the same URI. `@RequestBody` tells Spring MVC to deserialize
|
||||
the request body into an `Employee` object, which is then sent through the repository's `save()` operation. From there,
|
||||
it's wrapped as a `Resource` with links added to itself and to the aggregate root. Finally a `Location` response header
|
||||
@@ -204,10 +204,10 @@ class EmployeeController {
|
||||
...
|
||||
|
||||
@GetMapping("/employees/{id}")
|
||||
ResponseEntity<Resource<Employee>> findOne(@PathVariable long id) {
|
||||
ResponseEntity<EntityModel<Employee>> findOne(@PathVariable long id) {
|
||||
|
||||
return repository.findById(id)
|
||||
.map(employee -> new Resource<>(employee,
|
||||
.map(employee -> new EntityModel<>(employee,
|
||||
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel()
|
||||
.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, employee.getId())))
|
||||
.andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(employee.getId()))),
|
||||
@@ -225,7 +225,7 @@ class EmployeeController {
|
||||
|
||||
Employee updatedEmployee = repository.save(employeeToUpdate);
|
||||
|
||||
return new Resource<>(updatedEmployee,
|
||||
return new EntityModel<>(updatedEmployee,
|
||||
linkTo(methodOn(EmployeeController.class).findOne(updatedEmployee.getId())).withSelfRel()
|
||||
.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, updatedEmployee.getId())))
|
||||
.andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(updatedEmployee.getId()))),
|
||||
|
||||
@@ -106,7 +106,7 @@ From there, we need to add the ability to create new employees:
|
||||
[source,java]
|
||||
----
|
||||
@PostMapping("/employees")
|
||||
public ResponseEntity<Resource<Employee>> newEmployee(@RequestBody Employee employee) {
|
||||
public ResponseEntity<EntityModel<Employee>> newEmployee(@RequestBody Employee employee) {
|
||||
|
||||
Employee savedEmployee = repository.save(employee);
|
||||
|
||||
@@ -223,9 +223,9 @@ To construct a listing of all employees, check out the following controller meth
|
||||
public String index(Model model) throws URISyntaxException {
|
||||
|
||||
Traverson client = new Traverson(new URI(REMOTE_SERVICE_ROOT_URI), MediaTypes.HAL_JSON);
|
||||
Resources<Resource<Employee>> employees = client
|
||||
CollectionModel<EntityModel<Employee>> employees = client
|
||||
.follow("employees")
|
||||
.toObject(new ResourcesType<Resource<Employee>>(){});
|
||||
.toObject(new ResourcesType<EntityModel<Employee>>(){});
|
||||
|
||||
model.addAttribute("employee", new Employee());
|
||||
model.addAttribute("employees", employees);
|
||||
@@ -237,7 +237,7 @@ public String index(Model model) throws URISyntaxException {
|
||||
Presuming you already understand Spring MVC, let's focus on the RESTful bits.
|
||||
|
||||
* `Traverson` is used to start from the root node (*REMOTE_SERVICE_ROOT_URI*) and "hop" to *employees*.
|
||||
Then it fetches an object, and transforms it into Spring HATEOAS's vendor neutral `Resources<Resource<Employee>>` structure.
|
||||
Then it fetches an object, and transforms it into Spring HATEOAS's vendor neutral `CollectionModel<EntityModel<Employee>>` structure.
|
||||
* Using this, we are able to construct a `Model` object for the template.
|
||||
** An *employee* object is created to hold an empty, form-backed bean.
|
||||
** *employees* is loaded up with the entire Spring HATEOAS structure, allowing the template to use what bits it wants.
|
||||
@@ -274,7 +274,7 @@ It isn't necessary to post ALL of the Thymeleaf template `index.html`, but the c
|
||||
This shows the employee data being served up inside an HTML table.
|
||||
|
||||
* `th:each="employee : ${employees}"` lets your iterate over each one.
|
||||
* `th:text="${employee.content.name}"` navigates the `Resource<Employee>` structure (remmeber, you're iterating over each entry of `Resources<>`).
|
||||
* `th:text="${employee.content.name}"` navigates the `EntityModel<Employee>` structure (remmeber, you're iterating over each entry of `CollectionModel<>`).
|
||||
* `${employee.links}` gives each entry access to a Spring HATEOAS `Link`.
|
||||
* `<a th:text="${link.rel}" th:href="${link.href}" />` lets you show the end user each link, both name and URI.
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ class ManagerController {
|
||||
* Spring Web's {@link ResponseEntity} fluent API.
|
||||
*/
|
||||
@GetMapping("/managers")
|
||||
ResponseEntity<Resources<Resource<Manager>>> findAll() {
|
||||
ResponseEntity<CollectionModel<EntityModel<Manager>>> findAll() {
|
||||
return ResponseEntity.ok(
|
||||
assembler.toCollectionModel(repository.findAll()));
|
||||
|
||||
@@ -159,7 +159,7 @@ class ManagerController {
|
||||
* @param id
|
||||
*/
|
||||
@GetMapping("/managers/{id}")
|
||||
ResponseEntity<Resource<Manager>> findOne(@PathVariable long id) {
|
||||
ResponseEntity<EntityModel<Manager>> findOne(@PathVariable long id) {
|
||||
return ResponseEntity.ok(
|
||||
assembler.toEntityModel(repository.findOne(id)));
|
||||
}
|
||||
@@ -197,7 +197,7 @@ class EmployeeController {
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/managers/{id}/employees")
|
||||
public ResponseEntity<Resources<Resource<Employee>>> findEmployees(@PathVariable long id) {
|
||||
public ResponseEntity<CollectionModel<EntityModel<Employee>>> findEmployees(@PathVariable long id) {
|
||||
return ResponseEntity.ok(
|
||||
assembler.toCollectionModel(repository.findByManagerId(id)));
|
||||
}
|
||||
@@ -237,7 +237,7 @@ class ManagerResourceAssembler extends SimpleIdentifiableResourceAssembler<Manag
|
||||
* @param resource
|
||||
*/
|
||||
@Override
|
||||
protected void addLinks(Resource<Manager> resource) {
|
||||
protected void addLinks(EntityModel<Manager> resource) {
|
||||
/**
|
||||
* Retain default links.
|
||||
*/
|
||||
@@ -313,7 +313,7 @@ To support this, we can write the corresponding route in `EmployeeController`:
|
||||
[source,java]
|
||||
----
|
||||
@GetMapping("/employees/detailed")
|
||||
public ResponseEntity<Resources<Resource<EmployeeWithManager>>> findAllDetailedEmployees() {
|
||||
public ResponseEntity<CollectionModel<EntityModel<EmployeeWithManager>>> findAllDetailedEmployees() {
|
||||
|
||||
return ResponseEntity.ok(
|
||||
employeeWithManagerResourceAssembler.toCollectionModel(
|
||||
@@ -323,7 +323,7 @@ public ResponseEntity<Resources<Resource<EmployeeWithManager>>> findAllDetailedE
|
||||
}
|
||||
|
||||
@GetMapping("/employees/{id}/detailed")
|
||||
public ResponseEntity<Resource<EmployeeWithManager>> findDetailedEmployee(@PathVariable Long id) {
|
||||
public ResponseEntity<EntityModel<EmployeeWithManager>> findDetailedEmployee(@PathVariable Long id) {
|
||||
|
||||
Employee employee = repository.findOne(id);
|
||||
|
||||
@@ -352,7 +352,7 @@ class EmployeeWithManagerResourceAssembler extends SimpleResourceAssembler<Emplo
|
||||
* @param resource
|
||||
*/
|
||||
@Override
|
||||
protected void addLinks(Resource<EmployeeWithManager> resource) {
|
||||
protected void addLinks(EntityModel<EmployeeWithManager> resource) {
|
||||
|
||||
resource.add(linkTo(methodOn(EmployeeController.class).findDetailedEmployee(resource.getContent().getId())).withSelfRel());
|
||||
resource.add(linkTo(methodOn(EmployeeController.class).findOne(resource.getContent().getId())).withRel("summary"));
|
||||
@@ -365,7 +365,7 @@ class EmployeeWithManagerResourceAssembler extends SimpleResourceAssembler<Emplo
|
||||
* @param resources
|
||||
*/
|
||||
@Override
|
||||
protected void addLinks(Resources<Resource<EmployeeWithManager>> resources) {
|
||||
protected void addLinks(CollectionModel<EntityModel<EmployeeWithManager>> resources) {
|
||||
|
||||
resources.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withSelfRel());
|
||||
resources.add(linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"));
|
||||
@@ -380,8 +380,8 @@ This has a handful of differences from the `ResourceAssembler` objects you've bu
|
||||
* Since the routes are different than traditional */employees* and */employees/{id}*, it makes no sense to use `SimpleIdentifiableResourceAssembler<T>`.
|
||||
So instead, you want to fall back to `SimpleResourceAssembler<EmployeeWithManager>`, in which NO links are defined out of the box.
|
||||
* Because there are no defined routes, you are in full control.
|
||||
** `addLinks(Resource<EmployeeWithManager> resource)` defines links for single items
|
||||
** `addLinks(Resources<Resource<EmployeeWithManager>> resources)` defines links for collections
|
||||
** `addLinks(EntityModel<EmployeeWithManager> resource)` defines links for single items
|
||||
** `addLinks(CollectionModel<EntityModel<EmployeeWithManager>> resources)` defines links for collections
|
||||
|
||||
In this case, single `EmployeeWithManager` items include a self link to itself, a hop to it's parallel record that only has `Employee` info known as *summary*,
|
||||
and a link to the detailed collection. To avoid semantic confusion, this is called *detailedEmployees* given *employees* is the common reference to
|
||||
@@ -389,7 +389,7 @@ a collection of summary `Employee` records.
|
||||
|
||||
It also makes sense to add links from the other existing REST resources to this detailed `EmployeeWithManager`.
|
||||
|
||||
WARNING: Even though `addLinks(Resources<Resource<EmployeeWithManager>> resources)` gives you access to a single item's `Resource<T>` object,
|
||||
WARNING: Even though `addLinks(CollectionModel<EntityModel<EmployeeWithManager>> resources)` gives you access to a single item's `EntityModel<T>` object,
|
||||
it is recommended to NOT manipulate individual item links this way. Instead, use the other method.
|
||||
|
||||
Is this the _only_ way to display a detailed record? Not at all. Spring MVC supports request parameters, so it's not that difficult
|
||||
@@ -522,10 +522,10 @@ public class SupervisorController {
|
||||
}
|
||||
|
||||
@GetMapping("/supervisors/{id}")
|
||||
public ResponseEntity<Resource<Supervisor>> findOne(@PathVariable Long id) {
|
||||
public ResponseEntity<EntityModel<Supervisor>> findOne(@PathVariable Long id) {
|
||||
|
||||
Resource<Manager> managerResource = controller.findOne(id).getBody();
|
||||
Resource<Supervisor> supervisorResource = new Resource<>(
|
||||
EntityModel<Manager> managerResource = controller.findOne(id).getBody();
|
||||
EntityModel<Supervisor> supervisorResource = new EntityModel<>(
|
||||
new Supervisor(managerResource.getContent()),
|
||||
managerResource.getLinks());
|
||||
|
||||
@@ -535,17 +535,17 @@ public class SupervisorController {
|
||||
----
|
||||
|
||||
In this example, the assumption is that there was a route for individual supervisors, but not a link for a collection.
|
||||
This controller has that route, and serves up a `Resource<Supervisor>` record. But instead of fetching the data directly,
|
||||
This controller has that route, and serves up a `EntityModel<Supervisor>` record. But instead of fetching the data directly,
|
||||
it leverages the `ManagerController`.
|
||||
|
||||
Is that a good idea or a bad idea?
|
||||
|
||||
Again, there are tradeoffs. This example is meant to illustrate other options. In this case, leveraging `ManagerController`
|
||||
allows all links to be generated courtesy of the `ManagerResourceAssembler`. When a `ResponseEntity<Resource<Manager>>` object
|
||||
allows all links to be generated courtesy of the `ManagerResourceAssembler`. When a `ResponseEntity<EntityModel<Manager>>` object
|
||||
is returned by the controller, its wrapped REST resource is extracted by Spring MVC's `getBody()` method.
|
||||
|
||||
A new `Supervisor` REST resource is constructed by injecting the `Manager` into a `Supervisor` DTO. The provided links are
|
||||
then copied into that `Resource<Supervisor>` object.
|
||||
then copied into that `EntityModel<Supervisor>` object.
|
||||
|
||||
Hence, this controller will respond to calls for */supervisors/{id}*, but provide links onto the new system should the client
|
||||
want to gracefully start migrating.
|
||||
|
||||
@@ -55,7 +55,7 @@ operations.
|
||||
In REST, the "thing" being linked to is a *resource*. Resources provide both information as well as details on _how_ to
|
||||
retrieve and update that information.
|
||||
|
||||
Spring HATEOAS defines a generic `Resource<T>` container that lets you store any domain object (`Employee` in this example), and
|
||||
Spring HATEOAS defines a generic `EntityModel<T>` container that lets you store any domain object (`Employee` in this example), and
|
||||
add additional links.
|
||||
|
||||
IMPORTANT: Spring HATEOAS's `Resource` and `Link` classes are *vendor neutral*. HAL is thrown around a lot, being the
|
||||
@@ -93,22 +93,22 @@ The route for the https://martinfowler.com/bliki/DDD_Aggregate.html[aggregate ro
|
||||
* Then return them through Spring Web's {@link ResponseEntity} fluent API.
|
||||
*/
|
||||
@GetMapping("/employees")
|
||||
ResponseEntity<Resources<Resource<Employee>>> findAll() {
|
||||
ResponseEntity<CollectionModel<EntityModel<Employee>>> findAll() {
|
||||
|
||||
List<Resource<Employee>> employees = StreamSupport.stream(repository.findAll().spliterator(), false)
|
||||
.map(employee -> new Resource<>(employee,
|
||||
List<EntityModel<Employee>> employees = StreamSupport.stream(repository.findAll().spliterator(), false)
|
||||
.map(employee -> new EntityModel<>(employee,
|
||||
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(),
|
||||
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(
|
||||
new Resources<>(employees,
|
||||
new CollectionModel<>(employees,
|
||||
linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel()));
|
||||
}
|
||||
----
|
||||
|
||||
It retrieves a collection of `Employee` objects, streams through a Java 8 spliterator, and converts them into a collection
|
||||
of `Resource<Employee>` objects by using Spring HATEOAS's `linkTo` and `methodOn` helpers to build links.
|
||||
of `EntityModel<Employee>` objects by using Spring HATEOAS's `linkTo` and `methodOn` helpers to build links.
|
||||
|
||||
* The natural convention with REST endpoints is to serve a *self* link (denoted by the `.withSelfRel()` call).
|
||||
* It's also useful for any single item resource to include a link back to the aggregate (denoted by the `.withRel("employees")`).
|
||||
@@ -117,7 +117,7 @@ The whole collection of single item resources is then wrapped in a Spring HATEOA
|
||||
|
||||
NOTE: `Resources` is Spring HATEOAS's vendor neutral representation of a collection. It has it's
|
||||
own set of links, separate from the links of each member of the collection. That's why the whole
|
||||
structure is `Resources<Resource<Employee>>` and not `Resources<Employee>`.
|
||||
structure is `CollectionModel<EntityModel<Employee>>` and not `CollectionModel<Employee>`.
|
||||
|
||||
To build a single resource, the `/employees/{id}` route is shown below:
|
||||
|
||||
@@ -130,10 +130,10 @@ To build a single resource, the `/employees/{id}` route is shown below:
|
||||
* @param id
|
||||
*/
|
||||
@GetMapping("/employees/{id}")
|
||||
ResponseEntity<Resource<Employee>> findOne(@PathVariable long id) {
|
||||
ResponseEntity<EntityModel<Employee>> findOne(@PathVariable long id) {
|
||||
|
||||
return repository.findById(id)
|
||||
.map(employee -> new Resource<>(employee,
|
||||
.map(employee -> new EntityModel<>(employee,
|
||||
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(),
|
||||
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")))
|
||||
.map(ResponseEntity::ok)
|
||||
@@ -142,7 +142,7 @@ ResponseEntity<Resource<Employee>> findOne(@PathVariable long id) {
|
||||
----
|
||||
|
||||
This code is almost identical. It fetches a single item `Employee` from the database and that wraps up into a
|
||||
`Resource<Employee>` object with the same links, but that's it. No need to create a `Resources` object since is NOT a
|
||||
`EntityModel<Employee>` object with the same links, but that's it. No need to create a `Resources` object since is NOT a
|
||||
collection.
|
||||
|
||||
IMPORTANT: Does this look like duplicate code found in the aggregate root? Sures it does. That's why Spring HATEOAS
|
||||
|
||||
Reference in New Issue
Block a user