This guide shows a more detailed foray into linking resources with hypermedia. It includes automated links, custom ones, and retaining legacy links to support older clients.
Before proceeding, have you read these yet?
You may wish to read them first before reading this one.
Note
|
This example uses Project Lombok to reduce writing Java code. |
This example takes off where Basics and API Evolution end: an employee payroll system. Only this time, you’ll introduce a new domain: managers.
You’ll explore how to create create REST representations for a manager and tie it into employees.
For starters, here is the basic definition:
@Data
@Entity
@NoArgsConstructor
class Manager {
@Id @GeneratedValue
private Long id;
private String name;
/**
* To break the recursive, bi-directional interface, don't serialize {@literal employees}.
*/
@JsonIgnore
@OneToMany(mappedBy = "manager")
private List<Employee> employees = new ArrayList<>();
Manager(String name) {
this.name = name;
}
}
This is very similar to Employee
:
-
Uses the same
@Data
Lombok annotation to reduce boilerplate in defining a mutable value object. -
They are stored in a JPA data store using
@Entity
,@Id
, and@GeneratedValue
. -
Has a
@NoArgsConstructor
to support Jackson’s serializers.
But it contains a new aspect: a 1-to-many relationship with Employee
in the form a List<Employee>
.
This domain object initializes the field with an empty list to avoid NPEs. The JPA @OneToMany
annotation indicates
that the relationship between Manager
and Employee
is stored in the database tables in the Employee
entity’s
manager property, i.e. the manager’s primary key will be stored as a foreign key in the EMPLOYEE table.
Warning
|
Bi-directional relationships can be modeled in JPA, but you must carefully handle this. Jackson tends to
navigate as far as possible when serializing, so you have to tell it to stop with the @JsonIgnore directive. Otherwise,
it will generate a stack overflow exception when hopping Manager → Employee → Manager → etc.
|
A handy constructor is also added to support loading the database with sample data.
A corresponding Spring Data JPA repository is defined:
interface ManagerRepository extends CrudRepository<Manager, Long> {
}
To round things out, you need to make some updates to the Employee
domain object:
@Data
@Entity
@NoArgsConstructor
class Employee {
@Id @GeneratedValue
private Long id;
private String name;
private String role;
/**
* To break the recursive, bi-directional relationship, don't serialize {@literal manager}.
*/
@JsonIgnore
@OneToOne
private Manager manager;
Employee(String name, String role, Manager manager) {
this.name = name;
this.role = role;
this.manager = manager;
}
}
This is very similar to what you saw in Basics, except that now there is a 1-to-1 JPA relationship in the manager field.
The constructor call has also been updated. Finally, the same stack overflow is blocked from this end by also putting a @JsonIgnore
Jackson annotation on the manager field.
With these changes in place, you can now define a ResourceAssembler
for the Manager
:
@Component
class ManagerResourceAssembler extends SimpleIdentifiableRepresentationModelAssembler<Manager> {
ManagerResourceAssembler() {
super(ManagerController.class);
}
}
If you follow the same paradigm of extending Spring HATEOAS’s SimpleIdentifiableRepresentationModelAssembler
and applying the Manager
type,
you can easily inherit links for /managers and /managers/{id}
Before we go any further, we need to define those links!
@RestController
class ManagerController {
private final ManagerRepository repository;
private final ManagerResourceAssembler assembler;
ManagerController(ManagerRepository repository, ManagerResourceAssembler assembler) {
this.repository = repository;
this.assembler = assembler;
}
/**
* Look up all managers, and transform them into a REST collection resource using
* {@link ManagerResourceAssembler#toCollectionModel(Iterable)}. Then return them through
* Spring Web's {@link ResponseEntity} fluent API.
*/
@GetMapping("/managers")
ResponseEntity<CollectionModel<EntityModel<Manager>>> findAll() {
return ResponseEntity.ok(
assembler.toCollectionModel(repository.findAll()));
}
/**
* Look up a single {@link Manager} and transform it into a REST resource using
* {@link ManagerResourceAssembler#toEntityModel(Object)}. Then return it through
* Spring Web's {@link ResponseEntity} fluent API.
*
* @param id
*/
@GetMapping("/managers/{id}")
ResponseEntity<EntityModel<Manager>> findOne(@PathVariable long id) {
return ResponseEntity.ok(
assembler.toEntityModel(repository.findOne(id)));
}
}
This controller should look familar, since it’s almost identical to EmployeeController
as seen in API Evolution.
You have simply swapped /employees with /managers and plugged in ManagerRepository
and ManagerResourceAssembler
.
Important
|
It’s not a requirement to use a ResourceAssembler . But having one place to define all links for a given domain object
ensures a consistent representation.
|
With the basic routes defined, you could say we have an operational REST service. But it’s not fleshed out very well. To truly power up the hypermedia and serve clients, you need to add links between the relevant domain types.
Note
|
Up until this point, we’ve been using the term "domain types" or "domain objects". This is lingo found in Domain Driven Design. What you are building are REST resources and how the various media types they are represented in. The paradigm of REST is to construct resources that contain both data for the client to consume as well as controls to navigate to related data. |
The first link to navigate from a Manager
resource to its related Employee
resources would be a /managers/{id}/employees
route. Since a controller that yields employee objects would be found in the EmployeeController
, we need to make the following alterations:
@RestController
class EmployeeController {
...
/**
* Find an {@link Employee}'s {@link Manager} based upon employee id. Turn it into a context-based link.
*
* @param id
* @return
*/
@GetMapping("/managers/{id}/employees")
public ResponseEntity<CollectionModel<EntityModel<Employee>>> findEmployees(@PathVariable long id) {
return ResponseEntity.ok(
assembler.toCollectionModel(repository.findByManagerId(id)));
}
}
We’ve added another route, but how are we getting the data? Oh yeah, we need to add another finder!
interface EmployeeRepository extends CrudRepository<Employee, Long> {
List<Employee> findByManagerId(Long id);
}
With Spring Data, we can define a new finder just by writing it’s method signature! This custom finder will navigate by property and find a list of employees pointed at the chosen manager id.
Note
|
Navigation by property is analogous to writing select EMPLOYEE.* from EMPLOYEE join MANAGER on MANAGER.PK = EMPLOYEE.FK where MANAGER.PK == :id .
It makes it super simple to navigate over JPA relationships and find what we need.
|
This newly minted route needs to be added to every Manager
representation we render. To do that, we need to make an alteration
to ManagerResourceAssembler
:
@Component
class ManagerResourceAssembler extends SimpleIdentifiableRepresentationModelAssembler<Manager> {
...
/**
* Retain default links provided by {@link SimpleIdentifiableRepresentationModelAssembler}, but add extra ones to each {@link Manager}.
*
* @param resource
*/
@Override
protected void addLinks(EntityModel<Manager> resource) {
/**
* Retain default links.
*/
super.addLinks(resource);
// Add custom link to find all managed employees
resource.add(linkTo(methodOn(EmployeeController.class).findEmployees(resource.getContent().getId())).withRel("employees"));
}
...
}
SimpleIdentifiableRepresentationModelAssembler
has methods to alter a resource representation for single items or collections. It has pre-baked
renderings to create a self link to a single item as well as a link back to the collection. In this code, you are extending that
method and invoking super.addLinks()
in order to include those links. Then you add the link to the manager’s employees you just created.
Important
|
You can either add to the links defined by SimpleIdentifiableRepresentationModelAssembler as shown, or you can totally replace them by not
invoking super.addLinks() . Your choice.
|
There is a corresponding combination of a route/repository finder/assembler to allow an employee to find his or her manager. It’s left as an exericise
for you to discover it in ManagerController
, ManagerRepository
, and EmployeeResourceAssembler
.
Some critics of REST will point to certain toolkits or coded solutions and argue that "hopping" can be inefficient. A common example is a relational set of tables that through 3NF (3rd Normal Form) split up data between a parent/child relationship. In essence, part of the data is in the parent table, part in the child table. The parent table’s data is shown along with a link to navigate to the child table’s data.
This is a false comparison, because REST wholely supports merging data if it makes sense. In DDD, such items are referred to as aggregates. Nothing about a REST resource is confined by the rules of 3NF, written forty years ago. That can simply be shortfall of certain toolkits (but not Spring HATEOAS!)
What if you wanted a detailed Employee
representation that included the Manager
details? No problem! Just model it.
@Value
@JsonPropertyOrder({"id", "name", "role", "manager"})
public class EmployeeWithManager {
@JsonIgnore
private final Employee employee;
public Long getId() {
return this.employee.getId();
}
public String getName() {
return this.employee.getName();
}
public String getRole() {
return this.employee.getRole();
}
public String getManager() {
return this.employee.getManager().getName();
}
}
This immutable value object (thanks to Lombok’s @Value
annotation) is initialized with an Employee
object. It defines
how it gets rendered through various getter methods. It also subtly does not render the Employee
object itself.
Important
|
Employee and Manager both have a name field. With combined representations, there has to be agreement on how these
two fields will appear. In this case, Employee.name is kept and Manager.name is turned into manager.
|
To support this, we can write the corresponding route in EmployeeController
:
@GetMapping("/employees/detailed")
public ResponseEntity<CollectionModel<EntityModel<EmployeeWithManager>>> findAllDetailedEmployees() {
return ResponseEntity.ok(
employeeWithManagerResourceAssembler.toCollectionModel(
StreamSupport.stream(repository.findAll().spliterator(), false)
.map(EmployeeWithManager::new)
.collect(Collectors.toList())));
}
@GetMapping("/employees/{id}/detailed")
public ResponseEntity<EntityModel<EmployeeWithManager>> findDetailedEmployee(@PathVariable Long id) {
Employee employee = repository.findOne(id);
return ResponseEntity.ok(
employeeWithManagerResourceAssembler.toEntityModel(
new EmployeeWithManager(employee)));
}
This shows both a collection of "detailed" employees as well as a single one. The collection fetches all employees, uses a Java 8
stream to convert each Employee
into an EmployeeWithManager
, and wraps it into a Spring HATEOAS Resources
collection.
The single employee version does the corresponding transformation against a single Employee
.
To support building REST resources, you also need a ResourceAssembler
for EmployeeWithManager
. This should appear very
familiar by now:
@Component
class EmployeeWithManagerResourceAssembler extends SimpleRepresentationModelAssembler<EmployeeWithManager> {
/**
* Define links to add to every individual {@link Resource}.
*
* @param resource
*/
@Override
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"));
resource.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withRel("detailedEmployees"));
}
/**
* Define links to add to the {@link Resources} collection.
*
* @param resources
*/
@Override
protected void addLinks(CollectionModel<EntityModel<EmployeeWithManager>> resources) {
resources.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withSelfRel());
resources.add(linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"));
resources.add(linkTo(methodOn(ManagerController.class).findAll()).withRel("managers"));
resources.add(linkTo(methodOn(RootController.class).root()).withRel("root"));
}
}
This has a handful of differences from the ResourceAssembler
objects you’ve built up to this point:
-
Since the routes are different than traditional /employees and /employees/{id}, it makes no sense to use
SimpleIdentifiableRepresentationModelAssembler<T>
. So instead, you want to fall back toSimpleRepresentationModelAssembler<EmployeeWithManager>
, in which NO links are defined out of the box. -
Because there are no defined routes, you are in full control.
-
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
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(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 to code something like this:
@GetMapping("/employees/{id}")
public ResponseEntity<?> findOne(@PathVariable long id,
@RequestParam(value = "detailed", required = false,
defaultValue = false) boolean detailed) {
if (detailed) {
Employee employee = repository.findOne(id);
return ResponseEntity.ok(
employeeWithManagerResourceAssembler.toEntityModel(
new EmployeeWithManager(employee)));
} else {
return ResponseEntity.ok(
assembler.toEntityModel(repository.findOne(id)));
}
}
This type of solution allows serving two different representations from the same URI based on an optional ?detailed=true
parameter.
There are tradeoffs either way, but this option lends itself to supporting existing routes that you may already have.
To find the other places where detailed EmployeeWithManager
links have been added, inspect all the ResourceAssembler
objects
in the example’s code base.
In order to "start at the top" and hop, you must include a RootController
:
@RestController
@RestController
class RootController {
@GetMapping("/")
ResponseEntity<RepresentationModel> root() {
RepresentationModel model = new RepresentationModel();
model.add(linkTo(methodOn(RootController.class).root()).withSelfRel());
model.add(linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"));
model.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withRel("detailedEmployees"));
model.add(linkTo(methodOn(ManagerController.class).findAll()).withRel("managers"));
return ResponseEntity.ok(model);
}
}
Because there is no data at the top, just links, returning back a ResourceSupport
is perfect. This allows defining all the top links.
And it’s easy to go into the various ResourceAssemblers
and add a link back to the top as needed. It’s up to you to see which
bits of hypermedia serve such a link.
What if you started with one set of routes and migrated things to another set? This is the type of scenario that drives people screaming to version their APIs.
Instead of shouting "don’t version APIs" from the rooftops, and appealing to the authority of Roy Fielding, it’s better to see how it’s not that hard to support both old and new routes.
For this example, assume that before the Manager
entity and it’s ManagerController
existed, there was a Supervisor
with a
matching SupervisorController
. It had similar data but fewer links. A bit more RPC-like. If the original Supervisor
entity
was gone, we can add a DTO to represent the old format based on Manager
like this:
/**
* Legacy representation. Contains older format of data. Fewer links because hypermedia at the time was an after
* thought.
*
* @author Greg Turnquist
*/
@Value
@JsonPropertyOrder({"id", "name", "employees"})
class Supervisor {
@JsonIgnore
private final Manager manager;
public Long getId() {
return this.manager.getId();
}
public String getName() {
return this.manager.getName();
}
public List<String> getEmployees() {
return manager.getEmployees().stream()
.map(employee -> employee.getName() + "::" + employee.getRole())
.collect(Collectors.toList());
}
}
This representation assumes old record had:
-
Supervisor’s id, name and a somewhat sloppy display of employee’s name and role.
-
It’s powered by the new
Manager
object, so no need to store multiple copies of data. -
The
Manager
itself is not rendered thanks to the@JsonIgnore
annotation.
To honor the old route (/supervisors/{id}), create a new controller:
/**
* Represent an older controller that has since been replaced with {@link ManagerController}.
* This controller is used to provide legacy routes, i.e. backwards compatibility.
*
* @author Greg Turnquist
*/
@RestController
public class SupervisorController {
private final ManagerController controller;
public SupervisorController(ManagerController controller) {
this.controller = controller;
}
@GetMapping("/supervisors/{id}")
public ResponseEntity<EntityModel<Supervisor>> findOne(@PathVariable Long id) {
EntityModel<Manager> managerResource = controller.findOne(id).getBody();
EntityModel<Supervisor> supervisorResource = EntityModel.of(
new Supervisor(managerResource.getContent()),
managerResource.getLinks());
return ResponseEntity.ok(supervisorResource);
}
}
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 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<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 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.
Important
|
This example also assumes the clients can handle new links as long as the legacy ones are also there. For a different scenario, that assumption can be adjusted. |
With this amount of linking between related objects and DTOs, it’s easy to see how Spring HATEOAS can be used to model a link-driven API. And with the flexible nature of REST, more links can be added in the future along with additional representations. As long as the existing links are maintained, clients can have a much easier path of migration.