Use CollectionModel/EntityModel instead of Resources/Resource.

This commit is contained in:
Greg Turnquist
2019-07-30 12:42:16 -05:00
parent 4d44c9264b
commit 19f0ffd77c
4 changed files with 41 additions and 41 deletions

View File

@@ -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()))),

View File

@@ -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.

View File

@@ -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.

View File

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