HATEOAS en una API Spring Boot

Recursos vs Representación

HATEOAS es el acrónimo de Hypermedia as the Engine of Application State y resulta bastante útil cuando necesitamos que una API represente de forma sencilla un recurso seleccionado, además de contener links a otros recursos que están relacionados con el actual, lo que facilita para el cliente navegar una API REST.

Diremos que existen diferencias en cuanto a lo que es un recurso y la representación de ese recurso. Una base de datos podría tener una tabla Ingresos, esa tabla tener 10000 registros que pueden presentarse al usuario que solicita ese recurso de diferentes formas. El siguiente ejemplo podría ser resultado de mandar una petición GET a la URL https://codigochido.com/ingresos :

{
  "_embedded" : {
    "ingresos" : [ {
      "nombre" : "Angela Merkel",
      "fecha" : "20-01-2019 10:00:45 AM"
    }, {
      "nombre" : "Donald Trump",
      "fecha" : "20-01-2019 12:38:44 PM"
    } ]
  },
  "_links" : {
    "first" : {
      "href" : "https://codigochido.com/ingresos?page=0&size=20"
    },
    "self" : {
      "href" : "https://codigochido.com/ingresos"
    },
    "next" : {
      "href" : "https://codigochido.com/ingresos?page=1&size=20"
    },
    "last" : {
      "href" : "https://codigochido.com/ingresos?page=499&size=20"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 9981,
    "totalPages" : 500,
    "number" : 0
  }
}

Para el ejemplo anterior tenemos un recurso que se llama ingresos que representamos en páginas de 20 registros cada una, podríamos representarlo en páginas de 50 o 100 o prácticamente cualquier número que quisiéramos, pero al final es un único recurso (/ingresos) que representamos de distintas maneras dependiendo de los parámetros que nos manden. No es lo mismo tener una URL /ingresos/fecha/20-01-2019 a tener una URL /ingresos?fecha=20-01-2019 y es que la primera es un recurso y necesitas un método dentro de tu clase para manejarla, de esta forma tendrías al menos dos métodos para resolver las peticiones GET, uno de estos métodos recibiría una fecha y el otro no; para la segunda liga solo tienes un método que posiblemente reciba una fecha y de ser así puede filtrar el total de resultados.

Implementación

build.gradle

dependencies {
  ...
  implementation('org.springframework.boot:spring-boot-starter-data-rest')
  implementation('org.springframework.boot:spring-boot-starter-hateoas')
  ...
}

IngresoRepositorio.java

public interface IngresoRepositorio extends JpaRepository<Ingreso, Integer> {
  @Query("SELECT a FROM Ingreso a WHERE (:nombre is null or a.nombre = :nombre) AND (:fecha is null or trunc(a.fecha) = TO_DATE(:fecha,'dd-MM-yyyy'))")
  Page<Ingreso> findBy(Pageable pageable,@Param("nombre") String nombre, @Param("fecha") String fecha);
}

IngresoControlador.java

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PagedResourcesAssembler;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.PagedResources;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
...

public class IngresoControlador {
  @Autowired private EntityLinks links;

@GetMapping(value = "", produces = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity < PagedResources < Ingreso >> consultar(Pageable pageable, PagedResourcesAssembler assembler,@RequestParam(value = "nombre", required = false) String nombre,@RequestParam(value = "fecha", required = false) String fecha) {
    Page<Ingreso> ingresos;
    ingresos = actividadRepositorio.findBy(pageable,nombre, fecha);
    PagedResources < Ingreso > pr = assembler.toResource(ingresos, linkTo(methodOn(IngresoControlador.class).consultar(pageable,assembler,nombre,fecha)).withSelfRel());
    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.add("Link", createLinkHeader(pr));
    return new ResponseEntity < > (assembler.toResource(ingresos, linkTo(methodOn(IngresoControlador.class).consultar(pageable,assembler,nombre,fecha)).withSelfRel()), responseHeaders, HttpStatus.OK);
  }

  ...
  ...

  private String createLinkHeader(PagedResources < Ingreso > pr) {
      final StringBuilder linkHeader = new StringBuilder();
      linkHeader.append(buildLinkHeader(pr.getLinks("first").get(0).getHref(), "first"));
      linkHeader.append(", ");
      linkHeader.append(buildLinkHeader(pr.getLinks("next").get(0).getHref(), "next"));
      return linkHeader.toString();
     }

     public static String buildLinkHeader(final String uri, final String rel) {
      return "<" + uri + ">; rel=\"" + rel + "\"";
    }
  }

Algo importante mencionar es que el query escrito para este ejemplo en el repositorio no es un query SQL sino que la consulta se realizará sobre el Entity Ingreso que tiene por alias a y utilizará los parámetros nombre y fecha si los recibe con la notación :parámetro. El controlador contiene dos métodos (createLinkHeader y buildLinkHeader) para generar la posición “_links” en el JSON de respuesta.

Si tienen alguna duda por favor comenten en la discusión que se encuentra abajo y les responderemos prontamente.

Saludos.

comments powered by Disqus