29 de marzo de 2020

La potencia de Spring Data Rest: @RestResource

Ya hemos estado viendo lo básico del Core de Spring, también hemos estado viendo lo básico de JPA/ORM para dar persistencia a los datos. Ahora vamos a ver la capa de presentación mostrando nuestros datos por una API REST.

REST se clasifica en varios niveles en función de su madurez. En general basta con un servicio web que atienda peticiones por protocolo HTTP y devuelva datos serializados (normalmente en JSON, pero puede ser otro formato también): Nivel 0. Estos dos puntos parecen antagónicos con SOAP que permite cualquier protocolo para el transporte, pero los datos deben intercambiarse por XML con un esquema rígido.

URLs bien definidas identificando recursos: colecciones, elementos individuales y otras "operaciones": Nivel 1.

Para las peticiones se usarán verbos HTTP para operaciones CRUD (GET, POST, PUT, DELETE,...): Nivel 2.

El nivel más alto se logra consiguiendo HATEOAS/Hipermedia. Esto significa que, desde la raíz de nuestro servicio REST, todas las llamadas que se puedan realizar deben ser autodescubribles por medio de enlaces URL que representan las asociaciones de los distintos recursos: Nivel 3. Un ejemplo sería la API con datos sobre Star Wars donde se ven los enlaces de Luke con su planeta natal, peliculas donde sale, sus vehículos, raza, etc... siendo todos ellos urls al recurso (pero no incrustando sus datos).

Otra característica es que REST no mantiene estado. Cada petición y respuesta debe contener toda la información necesaria.

Después de esta introducción, recomiendo ver un video que habla de todo ello de una forma muy práctica y directa llamado "Horneando APIs" de Paradigma Digital. y que para mí es como un resumen en español del libro REST API Design Rulebook. Nosotros vamos a poner en práctica todo esto con:
  1. Spring Data Rest: que es una de las dependencias que usamos cuando generamos el proyecto inicialmente con Spring Initialzr. Si miramos nuestro build.gradle se identifica perfectamente la dependencia
  2. y nuestro ejemplo de Datos Deportivos
Empezamos este tutorial dando persistencia a Participantes y a Partidos que tenía una colección de Sucesos. Cargamos los datos de 237 participantes usando su DAO correpondiente y también cargamos unos partidos aleatorios con sucesos (en general sin decir si son goles, tarjetas, etc...). Esta forma de trabajar no permite la interacción que se espera de una arquitectura cliente-servidor. Al final de esta entrada tendremos una colección para Postman que nos permitirá interaccionar con nuestra aplicación mediante llamadas HTTP y nos permitirá hacer pruebas más fácil. Por tanto el punto inicial es este commit de nuestro repositorio.

Si en este punto no cerramos la aplicación con nuestra última linea del main veríamos que ya se están exponiendo los repositorios. Hacemos una llamada a http://localhost:8080/ y tendremos la siguiente respuesta:
{
  "_links": {
    "partidoConIds": {
      "href": "http://localhost:8080/partidoConIds{?page,size,sort}",
      "templated": true
    },
    "participantes": {
      "href": "http://localhost:8080/participantes{?page,size,sort}",
      "templated": true
    },
    "sucesoConIds": {
      "href": "http://localhost:8080/sucesoConIds{?page,size,sort}",
      "templated": true
    },
    "profile": {
      "href": "http://localhost:8080/profile"
    }
  }
}
Es decir, por defecto, todos los repositorios serán mostrados por Spring Data Rest si no se configura lo contrario. Voy añadir dos propiedades para configurar dos aspectos: que sólo se muestre lo anotado con @(Repository)RestResource y el prefijo de la API:
spring.data.rest.detection-strategy=annotated
spring.data.rest.basePath=/api
Esto lo pondré en un archivo llamada rest.properties que añadiré con @PropertySource como vimos anteriormente mediante una clase de configuración llamada ConfiguracionJava que luego usaré para más cosas:
@Configuration
@PropertySource({ "classpath:config/rest.properties" })
public class ConfiguracionPorJava {}
Y la añadimos a nuestra aplicación:
@Import(ConfiguracionPorJava.class)
public class DatosdeportivosapiApplication {...}
Ahora ya no se mostrará lo anterior, así que lo siguiente será modificar nuestras anotaciones @Repository por @RepositoryRestResource que es de Spring Data Rest.

@RepositoryRestResource admite varios valores para configurar nuestro repositorio. Pongo el ejemplo para SucesoConId:
@RepositoryRestResource(path="sucesos",
//                        exported=false,
                        itemResourceRel="suceso",
                        collectionResourceRel="sucesos")
public interface SucesoDAO extends JpaRepository<SucesoConId, Long> {
}
Aquí se puede ver que podemos decidir el path para este recurso y los nombres que tendrán las relaciones tanto para colección como para elemento (al final de la entrada se ve qué es esto de las relaciones). Recomiendo usar al menos esta configuración porque de lo contrario generará esos nombres automáticamente y puede que el resultado no nos guste (ver cómo se veía antes más arriba).

He comentado el valor exported=false ya que evita que se exponga el recurso. Este valor habría que usarlo si no quisiéramos mostrar este recurso. Por defecto tiene valor true.

Con esto termina el Paso 1, pero los recursos de Partidos y Sucesos no se pueden visitar ya que Jackson no es capaz de serializarlos correctamente porque no son simples POJOs. Vamos a customizar cómo debe serializarse aplicando propiedades para jackson, implementando unos MixIn y añadiéndolos a nuestro ObjectMapper. Como la entrada no va de esto y para no alargarla, lo mejor es ver este segundo paso en vivo en el webinar, pero aquí nos vamos a colocar en el commit del Paso 2 para continuar y terminar con nuestra API RESTful HATEOAS.

El resultado hasta aquí muestra una API con un array de Sucesos en cada Partido y sin mostrar el partido al que corresponde cada Suceso para no entrar en bucle. También vemos links de paginación dando cierta navegabilidad, pero no se puede navegar entre Partidos y Sucesos correctamente. Para implementar HATEOAS correctamente Spring necesita la siguiente información sobre las relaciones:
  1. Anotar con @OneToMany y @ManyToOne los miembros necesarios. No sirve sólo con que tenga la información JPA.
  2. Los recursos para asociar deben tener repositorio propio (esto ya está hecho)
  3. Se debe conocer la implementación concreta si la relación hace referencia a un tipo abstracto (debe saber qué Suceso debe deserializarse: deserialización polimórfica)
Para el primer punto no hace falta anotar las clases con @Entity ni @Id. Con anotar el miembro correspondiente es suficiente. Como no se tiene acceso al campo sucesos lo que hacemos es sobre escribirlo y anotarlo incluyendo la entidad objetivo (punto 3):
@Override
@OneToMany(targetEntity=SucesoConId.class)
public Collection<Suceso> getSucesos() {
    return super.getSucesos();
}
NOTA: también podría hacerse con (de)serializadores personalizados, añadiendo información con TypeResolver (y TypeResolverBuilder, sobre @JsonType(Id)Resolver o ver este snippet). Es la solución más genérica que se encuentra (aunque rebuscando mucho, una búsqueda me dio sólo 3 resultados) en Internet, pero en nuestro caso particular usando semántica esta forma es muy simple y funciona.

Para el caso de SucesoConId basta con anotar el campo con @ManyToOne.
@ManyToOne
PartidoConId partido;
Comparto también una colección, para probar la API en el punto actual.

No hay comentarios:

Publicar un comentario

Compárteme

Entradas populares