8 de mayo de 2020

Despliega tu API en la nube gratis

Ahora que tenemos nuestra API lista para ejecutarse vamos a ver cómo desplegarla en la nube. Básicamente tenemos que cambiar nuestra BD (ya no nos sirve con H2 en local) y exponer la API públicamente en red. Los servicios que voy a usar para ello son ElephantSQL para la BD y Heroku para la API. Para ello vamos a hacer lo siguiente:
  1. Creo una base de datos en ElephantSQL
  2. Cambio el datasource de JPA y el dialecto org.hibernate.dialect.PostgreSQLDialect. Puedes ver qué debes poner en la documentación de ElephantSQL para Java.
  3. Una vez cambiado eso voy a cargar los datos de los participantes en la BD ya en la nube. Descomento las líneas del main que lo hacen y lanzo la API. Ya podré ver que todo funciona correctamente pero ahora ya no tengo H2.
  4. Una vez cargada la BD y funcionando toca subir la API.
  5. Creo una aplicación en Heroku. Se puede desplegar todo usando el CLI.
  6. Lo más cómodo es enlazar el repositorio de GitHub donde tengamos el código fuente (para que se construya debe tener acceso a todas las dependencias, por eso ya cambiamos la dependencia datos-deportivos a un repositorio). Así incluso podemos tener nuestro despliegue continuo (cada commit se despliega)
  7. Primero tenemos que subir nuestro código con los últimos cambios, pero sin las credenciales de la BD (¡ojo con subir credenciales!)
  8. Una vez enlazado el repositorio ya veremos que podemos desplegar la aplicación. Pero nos faltan las credenciales para conectar con la BD. Vamos a ponerlas como una variable en Heroku para que la use sobrescribiendo lo que haya en el archivo de propiedades.
    DATABASE_URL: postgres://usuario:password@server.com:5432/usuario
  9. Y con esto ya podemos darle a deploy.
Cuando termine de ejecutarse, si todo ha ido bien, podemos acceder a nuestra API RESTful en la url que nos proporcione Heroku. Todo en la nube, sin coste alguno y sin administrar ninguna máquina.

Puedes ver el código hasta aquí en su repositorio.

7 de mayo de 2020

Empaquetar la API en un fichero.jar

Ahora que ya tenemos nuestro MVP desde la entrada anterior vamos a ver cómo empaquetarlo todo. La idea es que podamos ejecutarlo desde cualquier máquina con la JVM.

Spring Boot nos proporciona una tarea a nuestro proyecto de gradle que se llama bootJar y que es suficiente con ejecutarla. Para ello podemos usar la pestaña "Gradle Tasks" en Eclipse o simplemente ejecutar la tarea bootJar con el mismo gradle wrapper que tenemos en nuestro proyecto. Para esto último usamos desde la carpeta raíz de nuestro proyecto el comando siguiente:
gradlew bootJar
Si todo va bien tendremos nuestro .jar en la carpeta /build/libs de nuestro proyecto:

Así lo ejecutamos en la consola con el comando:
java -jar datosdeportivosapi-VERSION.jar
Para que se pueda levantar el servicio se necesita tener la BD levantada.

Para cualquier duda dejarlo en los comentarios.

6 de mayo de 2020

Código útil: clase ConfiguracionRest

Como hemos visto en las entradas anteriores, hemos añadido un link a un método personalizado pero de una manera muy manual y nos tocaría añadir otro más. No obstante, el código que se ha utilizado puede generalizarse más teniendo en cuenta lo siguiente:
  1. Los métodos que queremos enlazar tienen la anotación @ResponseBody
  2. Los parámetros que deben formar parte del path los anotaremos con @PathVariable.
  3. Los parámetros que deben formar parte de la query string los anotaremos con @RequestParam.
  4. Podemos configurar a qué recursos queremos añadirles enlaces en su /recursos/search y relacionarlas con su RecursoController que tenga los métodos a los que queremos enlazar.
Siguiendo esa guía podemos usar Reflection para conseguir toda la información y añadir los enlaces haciendo que:
  1. Filtre el recurso para el que debe añadirse los links (sólo los registrados)
  2. Recupere todos los métodos del Controller asociado a ese recurso
  3. Filtre aquellos marcados con ResponseBody
  4. Recupere el link con linkTo pasando como argumentos el valor "(" + nombre + ")" para cada PathVariable (se usan paréntesis para evitar el "escape" de las llaves)
  5. Cambiamos los paréntesis por llaves en las variables de la ruta.
  6. Utilice sólo los nombres de los parámetros RequestParam como query params
  7. Use el nombre del método como rel
El código está en el gist para la clase ConfiguracionRest:

También incluye un filtro CORS (CorsFilter) para permitir cualquier petición y así poder realizar pruebas sin preocuparse de las políticas Cross-Origin por defecto.

Cómo usar el bean correctamente

En nuestro repositorio vamos a eliminar el código de la sesión anterior para sustituirlo por esta clase de configuración que añadimos a nuestra aplicación.

Ahora personalizamos nuestro bean para los links añadiendo al mapa de controladores para enlazar la entidad como clave (PartidoConId.class) y el controlador asociado como valor (PartidoController.class). De aquí se deduce que lo más simple es poner todos los métodos para una entidad en un único Controller.

Si se quieren añadir otros enlaces aparte de los aporta este bean, se puede crear otro bean del mismo tipo en otra clase de configuración y al final todos los enlaces convivirán en el mismo /search.

Puedes encontrar el código hasta aquí en su repositorio.

Este punto finaliza nuestro Producto Mínimo Viable (PMV/MVP) y establece la release v1.0.0. En el README del repositorio se pueden ver las instrucciones para ejecutar la API en local.

5 de mayo de 2020

Rutas con @PathVariable

En esta entrada vamos a rematar la exposición de nuestros endpoints añadiendo variables a la ruta. Esto es muy normal cuando queremos referirnos a un recurso y nuestras rutas pueden tener fragmentos que sigan el patrón /recursos/:id.

En el endpoint expuesto la entrada anterior sólo hemos usado query params. En esta ocasión vamos a pedir los sucesos de un participante entre dos fechas y el id del participante estará en el path. La consulta ya está hecha en nuestro repositorio (SucesoDAO), pero no lo estamos exponiendo. Lo hicimos cuando vimos cómo personalizar rutas con @RestResource. Ésta anotación nos permitía cambiar el fragmento del path que se obtiene en /recursos/search e incluso personalizar los query params con @Param, pero no permite el uso de variables en el path. En este ejemplo queremos que el path sea /search/participante/{id}/entre-fechas.

Para esto vamos a tener que seguir usando @RequestMapping o cualquiera de sus variantes en un controlador. Como es una consulta sobre sucesos voy a crearme la clase SucesoController. El código se parece muchísimo al que vimos en la última sesión y queda así:
@RepositoryRestController
@RequestMapping(path = "/sucesos/search")
public class SucesoController {

    private SucesoDAO sucesoDAO;

    SucesoController(SucesoDAO sucesoDAO) {
        this.sucesoDAO = sucesoDAO;
    }

    @GetMapping("/participante/{id}/entre-fechas")
    @ResponseBody
    public CollectionModel<PersistentEntityResource> getSucesosConIdParticipanteEntreFechas(
            @PathVariable("id") String id,
            @RequestParam Instant comienzo, @RequestParam Instant fin,
            PersistentEntityResourceAssembler assembler) {
        List sucesos = sucesoDAO.findByIdParticipanteAndTemporalBetween(id, comienzo, fin);

        return assembler.toCollectionModel(sucesos);
    }

}
Me voy a centrar en lo nuevo. Si nos fijamos en el path aparece {id}. Cuando ponemos una parte del path entre llaves estamos indicando que se trata de una variable en el path y su nombre es el texto que hay dentro. Para hacer referencia a esa variable se usa @PathVariable pudiendo anotar un parámetro igual que hicimos con @RequestParam.

Para poder usarlo desde la ruta /sucesos/search nos toca modificar nuestro bean que procesa el recurso para búsquedas y añadirlo al código que ya tenemos para cuando el tipo gestionado sea SucesoConId. Esto empeora el mantenimiento así que, como lo prometido es deuda, en la siguiente entrada vamos a ver cómo añadir una configuración que nos descubra estos links personalizados automáticamente.

Puedes conseguir el código hasta aquí en su repositorio.

4 de mayo de 2020

Añadir link a /resources/search

Para conseguir en nuestra API REST el nivel 3, HATEOAS, hypermedia o RESTful hace falta que nuestros enlaces se puedan autodescrubrir. Esto no lo digo yo. El padre de REST explica que hypermedia es una condición mínima para llamar a un servicio RESTful. Una traducción (por google) de lo que ha dicho Roy Fielding al respecto:

Me siento frustrado por la cantidad de personas que llaman a cualquier interfaz basada en HTTP una API REST. El ejemplo de hoy es la API REST de SocialSite. Eso es RPC. Grita RPC. Hay tanto acoplamiento en la pantalla que se le debe dar una calificación X.

¿Qué se debe hacer para dejar claro el estilo arquitectónico REST sobre la noción de que el hipertexto es una restricción? En otras palabras, si el motor del estado de la aplicación (y, por lo tanto, la API) no está siendo impulsado por hipertexto, entonces no puede ser RESTful y no puede ser una API REST. Período. ¿Hay algún manual roto en algún lugar que deba repararse?

Con esto claro, en nuestra API no hay ningún enlace que dirija al método que expusimos en la entrada anterior: no se puede autodescubrir, o conoces la URL o navegando no hay un camino hasta ella. Vamos a añadir un link a nuestro nuevo endpoint para que no se frustre Roy.

Para ello vamos a usar RepositorySearchesResource. Éste bean permite añadir links a todos los que se crean automáticamente por Spring Data Rest. Hay varias formas de hacerlo, aunque las que he encontrado por Internet normalmente usan la versión antigua de HATEOAS (ResourceProcessor como veíamos también en la documentación de la entrada anterior). Voy a hacerlo migrado ya a la versión actual y de la forma que me parece mejor por ser más reutilizable.

Me voy a crear un bean del tipo RepresentationModelProcessor<RepositorySearchesResource> en mi clase de configuración. Es decir, voy a indicar el modo en el que se debe procesar el recurso utilizado para las búsquedas. El código quedaría así:
@Bean
RepresentationModelProcessor<RepositorySearchesResource> searchLinks(RepositoryRestConfiguration config) {
    return new RepresentationModelProcessor<RepositorySearchesResource>() {

        @Override
        public RepositorySearchesResource process(RepositorySearchesResource searchResource) {
            if (searchResource.getDomainType().equals(PartidoConId.class)) {
                try {
                    String nombreMetodo = "getPartidosConParticipanteComo";
                    Method method = PartidoController.class.getMethod(nombreMetodo, String.class,
                            PersistentEntityResourceAssembler.class);
                    URI uri = org.springframework.hateoas.server.mvc.WebMvcLinkBuilder
                            .linkTo(method, null, null).toUri();
                    String url = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),
                            config.getBasePath() + uri.getPath(), uri.getQuery(), uri.getFragment()).toString();
                    searchResource.add(new Link(url + "{?txt}", nombreMetodo));
                } catch (NoSuchMethodException | URISyntaxException e) {
                    e.printStackTrace();
                }
            }

            return searchResource;
        }

    };
}
Lo importante de todo este código es que el objeto searchResource se usa para todos los /search. Eso quiere decir que si añadimos un enlace aparecerá en todos ellos. Si queremos añadir el enlace en sólo uno (en nuestro caso para partidos) debemos filtrar para que coincida con el tipo correcto: PartidoConId. Podría ponerse el enlace a mano, pero entonces cualquier cambio posterior nos obligaría a recordar que hay que cambiarlo aquí también.

Lo mejor es recuperar el enlace que apunta a un método. Voy a usar linkTo y le pasaré el método al que quiero apuntar y los parámetros con null (estos son para incluir valores en las variables del path y completar la URL, pero nuestro método sólo tiene query params).

NOTA: Para usar linkTo se puede usar la importación estática para evitar poner su paquete, pero lo he dejado en el código para que quede claro de dónde viene el método.

Lamentablemente, el link no incluye el basePath y hay que añadírselo (como dicen ellos: "por ahora") por eso se construye de nuevo la URI incluyéndolo. Para recuperar ese valor lo mejor es usar RepositoryRestConfiguration que puede ser inyectada en el método.

Ya con la información completa podemos añadir el enlace en searchResource incluyendo los query params.

Si ejecutamos veremos que nuestro enlace aparece al final de los que se han añadido automáticamente.

No obstante, supongo que a cualquiera le dolerá ver esos "magic number" y pensará que debe haber una forma más automática de hacerlo. Personalmente no la he encontrado y tiene sentido (no se puede saber cómo se van a añadir los endpoints porque hay varias formas). Si alguno encuentra algo oficial agradecería que lo compartiera en los comentarios para completar la entrada.

Además en la siguiente entrada voy a añadir otro endpoint usando una variable en el path y también habrá que añadir el link.

Para arreglar esta falta de automatización, después de la siguiente entrada voy a compartir una clase de configuración base que puede utilizarse de manera generalizada en los proyectos igual que hicimos con el jpa-config.xml y explicaré la forma en que debe usarse. Así veremos que sólo con eso detecta y añade este endpoint y el que veamos a continuación que incluye path variable (y es otro parámetro que debemos controlar).

Puedes obtener el código hasta aquí en su repositorio.

30 de abril de 2020

Exponer metodo con @RepositoryRestController

En la entrada anterior hemos añadido un método personalizado a nuestro repositorio para una consulta más compleja que las que nos resuelve JPA en automático. Incluso luego la hemos optimizado utilizando datos cacheados en memoria.

Pero para que los usuarios de la API puedan utilizarlo hace falta exponerla. Esto no es automático, si revisamos los links generados por HATEOAS no aparecerá. La funcionalidad está en la capa de persistencia pero no es parte de la interfaz de la API.

Lo tradicional es tener un @Controller donde se indica el @RequestMapping que corresponda para conducir la llamada HTTP hasta el método concreto y derivar a un @Service. Más adelante veremos con más detalle y compararemos esta forma con la que vamos a usar aquí. Ahora quiero centrarme en la forma de hacerlo con Data Rest.

En esta entrada se va a utilizar la anotación @RepositoryRestController para continuar dentro del módulo Data Rest. El javadoc lo deja muy claro:

Anotación para marcar los controladores Spring MVC proporcionados por Spring Data REST. Permite detectarlos fácilmente y excluirlos del manejo estándar de Spring MVC.
Esta anotación sólo debe ser utilizada por los controladores de aplicaciones que se asignan a los URI administrados por Spring Data REST, ya que los maneja una implementación especial que aplica funcionalidad adicional.

Esto la convierte en el camino correcto y así lo indica la documentación. Lamentablemente la documentación no está actualizada y sigue utilizando el HATEOAS antiguo (Resource/s) así que aquí se va a ver cómo hacerlo migrado ya a la versión actual.

Me creo una clase PartidoController en mi paquete de rest con el siguiente código:
@RepositoryRestController
public class PartidoController {

    private PartidoDAO partidoDAO;

    PartidoController(PartidoDAO partidoDAO) {
        this.partidoDAO = partidoDAO;
    }

    @GetMapping("/partidos/search/con-nombre-participante")
    @ResponseBody
    public CollectionModel<PersistentEntityResource> getPartidosConParticipanteComo(@RequestParam String txt,
            PersistentEntityResourceAssembler assembler) {
        List<PartidoConId> partidos = partidoDAO.getEventosConParticipanteConTexto(txt);

        return assembler.toCollectionModel(partidos);
    }

}
Al marcar la clase se excluyen de la gestión de Spring MVC (la que usa @RestController por ejemplo) y pasan a ser gestionados por Data Rest. Con ello se consigue:
  1. El controlador utilizará la configuración basePath de Data Rest (en nuestro caso /api)
  2. Se podrá inyectar en sus métodos el objeto PersistentEntityResourceAssembler. Este objeto va a permitir la creación de EntityModel o CollectionModel (lo que sustituye a Resource/s) en función de si hay que representar un elemento o una colección de elementos. Así la representación será la misma que la que se genera para el resto de métodos, dando consistencia a la API.
Hay otras dos anotaciones necesarias según se ve en la documentación:
  • @GetMapping: usamos esta anotación que equivale a @RequestMapping(method=GET,...). Ambas sirven para indicar el endpoint que dirigirá hasta el método. La ruta la ponemos sin el basePath. Si se pone a nivel de clase el fragmento afecta a todos los mapeos internos.
  • @ResponseBody: significa que el método devuelve una respuesta web. Si no se pone no se expondrá el endpoint. También se puede poner a nivel de clase para que todos los métodos de dentro tomen ese valor por defecto.

Ahora al arrancar la API podemos probar ese endpoint y ver que existe y funciona correctamente. Sin embargo si acudimos a /partidos/search no se va a mostrar y no se puede autodescubrir. En la próxima entrada vamos a ver cómo añadir este nuevo endpoint a los link de /partidos/search.

Puedes encontrar el código hasta aquí en su repositorio.

29 de abril de 2020

Añadir método personalizado en repositorio

Ya conocemos lo básico de Spring Data Rest y cómo nos facilita exponer los recursos que tenemos almacenados en nuestra BD con JPA. Sin embargo todos los métodos que hemos usado hasta ahora tienen una relación directa con las columnas de la BD (por ejemplo busco Participantes usando su campo nombre) y en nuestro proyecto seguramente habrá aspectos que no podamos solucionar con lo visto hasta ahora. Como ejemplo nos gustaría recuperar los Partidos de un Participante con un texto en su nombre. En este caso tenemos un problema: en la tabla PARTIDOS no están los nombres de los participantes, sólo sus ids y ya no es suficiente con usar los Query Methods de JPA para solucionarlo.

En este caso necesitamos hacer un método personalizado para nuestro repositorio.

Para implementar esta funcionalidad vamos a hacer lo siguiente:
  1. Crear una búsqueda para buscar los participantes conteniendo un texto (eso ya lo tenemos: ParticipanteDAO.findByNombreIgnoreCaseContaining)
  2. Con los participantes recuperados buscaremos los partidos en los que aparecen
  3. Añadiremos todos los partidos de esos participantes a una lista
  4. Esa lista será el payload de nuestra respuesta HTTP
Para añadir un método personalizado Data Rest permite que nuestro DAO se complete usando fragmentos implementados de varias interfaces, además de la que estamos usando JpaRepository. Esas interfaces tendrán los métodos que queremos añadir. En nuestro caso me creo la interface EventoDAOCustom:
public interface EventoDAOCustom<T extends Evento> {
    List<T> getEventosConParticipanteConTexto(String txt);
}
De esta forma esta interface me servirá para otro tipo de eventos, no sólo para partidos.
Ahora me creo una clase que implemente este método. Lo más importante es que debe tener el sufijo "Impl" e implementar nuestra nueva interface. Nos quedará así:
class PartidoDAOImpl implements EventoDAOCustom<PartidoConId> {

    @Autowired
    ParticipanteDAO participanteDAO;

    @PersistenceContext
    EntityManager entityManager;

    @Override
    public List<PartidoConId> getEventosConParticipanteConTexto(String txt) {
        List<Participante> participantes = participanteDAO.findByNombreIgnoreCaseContaining(txt);
        Set<PartidoConId> partidos = new HashSet<PartidoConId>();
        Query query = entityManager.createNativeQuery(
            "SELECT p.* FROM partidos as p " +
            "WHERE p.local = ?1 OR p.visitante = ?1", PartidoConId.class);
        participantes.forEach(p -> {
            query.setParameter(1, p.getIdentificador());
            partidos.addAll(query.getResultList());
        });

        return new ArrayList<PartidoConId>(partidos);
    }

}
Para implementar este método usamos ParticipanteDAO para recuperar los participantes y luego una Query que es bastante parecida a lenguaje SQL que ya conocemos como se ve en el ejemplo.

NOTA: la query necesita del EntityManager y lo inyectamos con @PersistenceContext.

Usamos la query para ir llamando a nuestra BD con cada participante que cumpla con el texto pasado y lo añadimos a un Set para que no se repita. Finalmente devolvemos todos los partidos. Podrían hacerse más cosas como ordenar por algún campo, etc...

Sólo nos queda añadir la interface a nuestro DAO para que Spring lo complete:
public interface PartidoDAO extends JpaRepository<PartidoConId, Long>, EventoDAOCustom<PartidoConId> {...}

Lo importante de todo esto es que puedo utilizar código propio para implementar aquello que ya se escapa de los Query Methods de JPA. Sin necesidad de añadirlo como bean ni nada parecido, Data Rest va a escanear las clases con el sufijo Impl (se puede configurar) buscando la implementación de EventoDAOCustom que no puede hacer automáticamente. Así irá formando con fragmentos de código el DAO completo.

Ahora probamos que todo funciona correctamente en nuestro main:
partidoDAO.getEventosConParticipanteConTexto("m").stream()
    .map(PartidoConId::toString)
    .forEach(log::trace);
Para que esto funcione de esta forma debemos indicar que este método es transaccional. Como habitualmente estas consultas personalizadas son lecturas de datos, marcamos toda la clase como sólo lectura.
@Transactional(readOnly = true)
class PartidoDAOImpl implements EventoDAOCustom<PartidoConId> {...}
NOTA: ver más sobre cómo funciona @PersistentContext y @Transactional.

Puedes conseguir el código hasta aquí en su repositorio.

Añadida la funcionalidad a nuestra persistencia, lo que nos falta es exponerla en nuestra API. En la siguiente entrada vamos a ver cómo hacerlo.

Práctica: mejorar cómo se consiguen los participantes
Ahora mismo si buscamos un equipo donde su nombre incluya una tilde en el fragmento buscado y no coincida en el valor de txt (ejemplo: buscando "?txt=atletico" no devolverá "Atlético...") parece que no obtenemos el resultado esperado. Se pide modificar el código para que no sólo sea IgnoreCase sino que también ignore las tildes.

NOTA: aparte de ParticipanteDAO tenemos otro origen de datos disponible para ello en forma de bean. Para facilitar el tratamiento de las tildes existe un método en lanyu.commons que se encarga de ello: StringUtils.eliminarTildes.

La solución está en github.

28 de abril de 2020

Personalizar endpoints con @RestResource

En estos momentos tenemos un servicio HATEOAS que puede ser descubierto navegando por sus enlaces desde el raíz. Sin embargo sólo hemos configurado las propiedades de @RepositoryRestResource. En nuestras aplicación probablemente querremos poder personalizar alguna URL (sobretodo de los métodos que nos hacemos con query methods de JPA), no permitir algunas operaciones (por ejemplo no dejar borrar un participante) o al menos no permitirlas para todos los usuarios (sólo podrá borrar el adminitrador).

Vamos a ver cómo personalizar algunos de estos aspectos utilizando la anotación @RestResource.

En ParticipanteDAO voy poner las siguientes líneas:
@RestResource(path="nombre")
List<Participante> findByNombreIgnoreCaseContaining(String txt);

@RestResource(exported=false)
void deleteById(String id);

@RestResource(exported=false)
void delete(Participante entity);

// Mejor ponerselo a todo lo que tenga que aplicarse
// void deleteAll(Iterable<? extends Participante> entities);
// ...
Estas líneas hacen dos cosas:
  1. Modifica la URL para que el fragmento del path del primer método sea nombre.
  2. Prohibe el uso de DELETE sobre Participantes.
NOTA: JpaRepository tiene 6 distintos métodos de borrado, formalmente y por rendimiento es mejor aplicar la anotación a todos ellos aunque con uno sólo sería suficiente. Aquí se indica pero no se ha hecho para ahorrar código.

Ahora quiero buscar un Partido donde participe un idParticipante que le pase. Pongo esta línea en PartidoDAO:
@RestResource(path="participante")
List<PartidoConId> findByIdLocalOrIdVisitante(@Param("idParticipante") String idLocal, @Param("idParticipante") String idVisitante);
Aquí utilizo además la anotación @Param. Esta anotación me permite enlazar un query parameter de la petición HTTP a un parámetro de mi método. La sintaxis del query method de JPA me va a obligar a tener tantos parámetros como vea que necesita su nombre (idLocal e idVisitante). Sin embargo puedo enlazar el mismo query parameter a los dos parámetros.

Puedo encontrar estos enlaces en el path /search de cada recurso.

Puedes encontrar el código hasta aquí en su repositorio.

Esta entrada es muy corta pero ya se puede practicar mucho con ella. Propongo hacer los siguientes métodos de búsqueda y ponerles el nombre apropiado:
  1. Buscar sucesos por id de Participante
  2. Buscar sucesos entre un Instant comienzo y otro fin
  3. Buscar sucesos para un idParticipante entre comienzo y fin. Este método no se expondrá en la API.
  4. Buscar sucesos después de un Instant instant. El query param tendrá el nombre start.
La solución está en github.

15 de abril de 2020

Inyectar un bean a un RestResource

Seguimos con el Paso 3 de la entrada anterior. Ya tenemos nuestra bean ServicioEntidad cargada e inyectándose en las entidades (Sucesos) que se leen de la BD. ¿Qué pasa con los objetos que se crean al deserializarse el JSON de la llamada HTTP? Esos objetos también deben ser inyectados con el ServicioEntidad, no podemos esperar a que sean leídos desde la BD pues podría fallar el proceso que necesitara esa llamada.

Ahora nuestro punto de entrada es el controlador que recibe esa llamada. Si lo hicieramos a mano podríamos gestionarlo en el @Controller, cuando Jackson lo convirtiera y se lo pasara como argumento a nuestro método del controlador. Sin embargo nosotros usamos Data Rest, pero podemos inyectarlo justo antes, personalizando el bean que se encarga de leer ese JSON y convertirlo en nuestro objeto de negocio (en el ejemplo SucesoConId o sus subtipos). Para ello vamos a crear un bean que al deserializar el JSON inyecte nuestro bean ServicioEntidad. Para esto jackson nos da una anotación perfecta: @JsonComponent.

La anotación @JsonComponent se puede usar sobre una clase que implemente (de)serialización o sobre una clase que contenga varias implementaciones internas que cargará todas. Hay varias formas de aprovecharlo, nosotros nos vamos a centrar en crear en una clase todas las implementaciones de deserializador que necesitamos para nuestros distintos sucesos usando StdDeserializer.

Ya que vamos a hacernos un serializador para cada tipo de suceso vamos a aprovechar la herencia para reutilizar código. El esquema general quedará:
@JsonComponent // Registra la clase/clases internas con (de)serializadores
public class JsonDeserializers {

    public static class JsonSucesoSerializer<T extends SucesoConId> extends StdDeserializer<T> {... }

    public static class JsonGolSerializer extends JsonSucesoSerializer<GolConId> {...}

    public static class JsonTarjetaSerializer extends JsonSucesoSerializer<TarjetaConId> {...}
 
}
NOTA: los deserializadores de gol y tarjeta van a heredar del serializador de suceso.

Ya vimos un ejemplo en clase haciendo un deserializador personalizado. En este caso lo vamos a repetir pues se trata de ir leyendo el JSON e ir asignando los valores en cada campo. Para ello vamos a tener que añadir algún setter más a los Sucesos si no queremos hacer uso de Reflection. Lo interesante va a ser el uso de genéricos para que se adapten al resto de implementaciones para GolConId y TarjetaConId y el uso de un callback para poder añadir lo que falte en tipos que lo necesitan (añadir el tipo de tarjeta en TarjetaConId por ejemplo). La implementación de JsonSucesoSerializer<T extends SucesoConId> resumida quedará así:
public static class JsonSucesoSerializer<T extends SucesoConId> extends StdDeserializer<T> {

    // Constructores heredados del padre StdDeserializer<T>

    @Override
    public T deserialize(JsonParser jsonParser, DeserializationContext context)
                            throws IOException, JsonProcessingException {
        return deserializarSuceso(jsonParser, (Class<T>) handledType());
    }

    protected T deserializarSuceso(JsonParser jsonParser, Class<T> tipo)
                            throws IOException, JsonProcessingException {
        T sucesoDeserializado;
        // Construir y asignar valores a campos. Ver a continuacion.

        postDeserializarSuceso(nodo, sucesoDeserializado);

        return sucesoDeserializado;
    }

    // Callback para completar la deserializacion
    protected T postDeserializarSuceso(JsonNode nodo, T sucesoDeserializado) {
        return sucesoDeserializado;
    }

}
Lo más importante de implementar StdDeserializer, aparte del parseo en sí, se puede decir que es el método handledType(). Usando ese método vamos a tener acceso al tipo de objeto que hay que deserializar y con ello podremos tener acceso al constructor que necesitemos. Para eso sí que vamos a usar Class. Con estas herramientas la construcción de un SucesoConId quedará:
protected T deserializarSuceso(JsonParser jsonParser, Class<T> tipo)
                        throws IOException, JsonProcessingException {
    T sucesoDeserializado;
    try {
        // Variable intermedia final para los lambdas
        // El constructor vendra marcado por el tipo a construir y el parametro indicado
        T suceso = tipo.getConstructor(ServicioEntidad.class).newInstance(servicioEntidad);
        JsonNode nodo = jsonParser.getCodec().readTree(jsonParser);
        // Podemos usar Optional para seguir ejecutando
        // o no si no hay campo
        Optional.ofNullable(nodo.get("timestamp")).ifPresent(n -> suceso.setTimestamp(n.asLong()));
        Optional.ofNullable(nodo.get("idParticipante")).ifPresent(n -> suceso.setIdParticipante(n.asText()));
        Optional.ofNullable(nodo.get("partido")).ifPresent(n -> {
            String idPartido = n.asText();
            idPartido = idPartido.substring(idPartido.lastIndexOf("/") + 1, idPartido.length());
            suceso.setPartido(partidoDAO.getOne(Long.parseLong(idPartido)));
        });
        postDeserializarSuceso(nodo, suceso);

        sucesoDeserializado = suceso;
    } catch (Exception e) {
        e.printStackTrace();
        sucesoDeserializado = null;
    }

    return sucesoDeserializado;
}
Lo destacable de aquí es que este método recibe el tipo del método handledType() que será implementado distinto en sus clases hijas. Con el tipo se construye con el constructor que admite un ServicioEntidad y ya estrará inyectado. Lo demás es ir leyendo y asignando valores hasta la invocación de postDeserializarSuceso(nodo, suceso). Con este callback dejamos la puerta abierta a completar la deserialización en las clases hijas.


Ahora que ya hemos completado el serializador padre podemos aprovecharlo para rematar con los otros dos que necesitamos:
// Basta con devolver el tipo a construir
public static class JsonGolSerializer extends JsonSucesoSerializer<GolConId> {
    @Override public Class<?> handledType() { return GolConId.class; }
}

public static class JsonTarjetaSerializer extends JsonSucesoSerializer<TarjetaConId> {
    @Override public Class<?> handledType() { return TarjetaConId.class; }

    @Override
    protected TarjetaConId postDeserializarSuceso(JsonNode nodo, TarjetaConId sucesoDeserializado) {
        Optional.ofNullable(nodo.get("tipoTarjeta"))
            .ifPresent(n -> sucesoDeserializado.setTipoTarjeta(TipoTarjeta.valueOf(n.asText())));
        return sucesoDeserializado;
    }
}
Vemos que nos quedan muy simples: para Gol sólo necesita decir el tipo que maneja y para Tarjeta también utilizar el callback para asignar el tipo de tarjeta (amarilla o roja) que traiga el nodo.

Ahora ya nos pueden hacer llamadas HTTP que Jackson se encargará de inyectar el ServicioEntidad.

Puedes conseguir el código hasta aquí en su repositorio.

Como práctica se propone hacer lo mismo para los partidos, ya que si se invocase código que necesitase del ServicioEntidad en la clase Partido nuestro código fallaría si no lo ha cargado ya @PostLoad en la práctica que vimos en la entrada anterior.

14 de abril de 2020

Inyectar un bean a una Entidad leída desde BD

Con lo que estamos viendo, lo normal es guardar y recuperar datos de una BD usando JPA. También hemos visto la flexibilidad que nos proporciona trabajar con inyección de dependencias. La inyección se produce para los beans y sin embargo nuestros "objetos de negocio" no lo son y no se puede hacer Autowired sobre ellos (me refiero a los objetos que se recuperan de una BD o los que nos llegan en formato JSON por una petición HTTP y deserializa nuestro ObjectMapper).

¿Qué importancia tienes esto?

Nuestra tabla Participantes tiene pocos datos y se modificará poco (los participantes podrán ser modificados como mucho cada comienzo de liga). Aquí tendría sentido tener todos esos datos cargados (o al menos un subconjunto como id y nombre) en memoria porque sin embargo van a ser consultados muy a menudo (clasificación, enfrentamientos históricos, jornada actual, etc...). La BD cuenta con una cache para todos los datos, pero en particular estos valores darán mejor rendimiento si los tenemos en memoria (incluso se podrían leer de un archivo sin necesidad de la BD). Esto lo vamos a hacer creando un bean del tipo ServicioEntidad que no es más que una interface que puede almacenar objetos ordenados por clase y por id usando mapas/diccionarios.

@Autowired sólo funciona en los beans. Nuestras entidades no lo son.


Nuestra librería de datos-deportivos tiene un campo ServicioEntidad en los tipos Partido y SucesoImpl y necesitan un objeto que implemente esa interfaz para hacer el "lazy loading" de participantes (sólo se recupera de la BD el idParticipante, pero no se referencia al objeto hasta que no se tiene que usar). Ni Partidos ni Sucesos son beans gestionados por el contenedor así que ¿Cómo puedo inyectar en ellos tanto por el lado web como por la BD el bean ServicioEntidad? Para ello voy a tener la ayuda de los callbacks, los listeners y los deserializadores personalizados.

He dividido esta sesión también en varios pasos:
  • Paso 1: Creo mi bean ServicioEntidad y modifico mis sucesos para poder darles este valor a su campo. Este código no tiene ningún misterio y empezaremos desde este punto.
  • Paso 2: Inyecto el bean ServicioEntidad en los sucesos cuando los lea desde la BD. Ésta es la finalidad de esta entrada.
  • Paso 3: La otra vía de entrada es desde mi API REST. Los objetos json vendrán sin un ServicioEntidad que tendré que inyectar. Este paso lo haré en la siguiente entrada para no alargarme, pero on quería desvincularlo de esta funcionalidad de inyectar un bean.

Para explicar lo que voy a hacer voy a servirme de la siguiente figura:

Lo que representa la figura son los caminos de entrada y salida de nuestros objetos de negocio en Java teniendo nuestra aplicación en el centro y por fuera:
  1. Las operaciones con la BD: Las entidades tienen un ciclo de vida con una serie de eventos
  2. Las llamadas HTTP que nos llegarán a la API: vimos que la salida la modificabamos con MixIns, ahora toca configurar la entrada

Ciclo de vida de un Entity

Las entidades pasan por un ciclo de vida con los eventos/callbacks PostLoad, PrePersist, PostPersist, PreUpdate, PostUpdate, PreRemove o PostRemove en función de las operaciones que se ejecuten en la BD. Básicamente son eventos anteriores y posteriores a las operaciones guardar, actualizar y borrar y luego otro después de cargarse en el entityManager.

NOTA: Estos eventos son de JPA. Data Rest tiene otros eventos y hay otros muchos de otras librerías. En el Paso 3 implementaremos uno y lo veremos con más detalle. Lo bueno de estos eventos es que te servirán para gestionar todas las entidades cuando uses JPA.

Para lo que queremos hacer vamos a usar el evento @PostLoad. Así después de leer un SucesoConId voy a asignarle el ServicioEntidad que he creado en mi contenedor Spring.

NOTA: Mirando la documentación puede parecer que la anotación @PersistenceConstructor es exactamente lo que necesitamos. Esta anotación es de Spring pero JPA en la actualidad obliga a tener un constructor sin parámetros. De hecho si lo quitais Hibernate os dará ese error y, aunque Hibernate pueda contruir una entidad sin un constructor sin parámetros, JPA que es la API que estamos usando si lo necesita.

Voy a crearme una clase llamada SucesoListener que será un bean y lo voy a cargar con @Component. JPA no obliga a implementar ninguna interface así que esta clase no heredará de nadie pero va a tener un método que será anotado con @PostLoad. Esto indicará que ese método debe ejecutarse justo después de leer un SucesoConId (del tipo que sea) desde la BD. Para poder modificar ese suceso leído me llega por el parámetro de ese método con un simple setServicioEntidad(servicioEntidad) que añado a SucesoConId (y de ahí heredarán el resto). Sólo falta recuperar el bean ServicioEntidad de mi contenedor que puedo inyectárselo al Listener porque también lo gestiona Spring. Mi Listener queda así:
@Component
public class SucesoListener {
    private static ServicioEntidad servicioEntidad;

    @Autowired
    public void init(ServicioEntidad servicioEntidad) {
        SucesoListener.servicioEntidad = servicioEntidad;
    }

    @PostLoad
    void setServicioEntidadEnSuceso(SucesoConId suceso) {
        suceso.setServicioEntidad(servicioEntidad);
    }
}
Queda relacionar esta clase con la entidad a la que debe suscribirse para recibir el evento. Con una simple anotación en la clase SucesoConId estará hecho:
@EntityListeners(SucesoListener.class)
public class SucesoConId extends SucesoImpl { ... }
Y cargar el bean SucesoListener en el contenedor como cualquier otro @Component escaneando su paquete (usamos "es.lanyu.eventos" que servirá también para el @JsonComponent del paso 3).
@ComponentScan("es.lanyu.eventos") // Tambien para registrar JsonSerializers
public class ConfiguracionPorJava {...}
Si imprimimos nuestros partidos por consola veremos que ya aparecen enriquecidos con el toString() de Participante con lo que nuestro ServicioEntidad ha sido inyectado en cada suceso y está recuperando correctamente nuestros participantes.

SucesoConId #35, participante: Oviedo OVIEDO(ESP) fecha=2020-04-13T17:25:53.297Z
Gol para el Oviedo OVIEDO(ESP)
Tarjeta AMARILLA Heerenveen HERENV(HOL)

Puedes ver el código hasta aquí en su repositorio. En la siguiente entrada continuamos con el Paso 3, pero no olvides hacer la práctica que te pongo más abajo.

Para asimilar este contenido dejo una práctica en la que habrá que utilizar lo que hemos visto:

Se pide implementar lo necesario para PartidoConId el toString() que muestre el id y los detallesDelPartido() (ya implementado y comentado en la última línea del toString() pero habrá que inyectar ServicioEntidad). Además si ahora se intenta usar la API REST para hacer un DELETE sobre un partido con sucesos ocurrirá un error 500: implementar lo necesario para poder borrar un partido con sucesos (también se deben borrar antes sus sucesos). Como ya tenemos cierto nivel con Spring se pide al menos hacerlo de la forma que no hemos visto pero es la más desacoplada: por XML. En el wikibook viene un ejemplo.

31 de marzo de 2020

ORM por XML: Guardar subclases en SINGLE_TABLE

Ya vimos en una sesión anterior la capacidad de heredar la definición de persistencia desde las superclases con la etiqueta "mapped-superclass". En esta entrada vamos a ver cómo se guardan especializaciones distintas de un mismo tipo pudiendo recuperar todas ellas de forma única o recuperándolas como un tipo específico.

En nuestro ejemplo nuestras especializaciones son los distintos tipos de sucesos que hay en un partido (goles, tarjetas, corners, etc...) ya que todos heredan de Suceso, en nuestra API concretamente de la implementación SucesoConId.

Para persistir varias clases que heredan de un mismo tipo se puede hacer de tres formas:
  1. SINGLE_TABLE: Es la opción por defecto y la implementada por todas las librerías. Los datos de todos los subtipos se guardan en una misma tabla. Tiene el mejor rendimiento a la hora de consultar datos porque no hay que realizar ninguna unión, pero como desventaja tendremos campos null en las columnas que no usen especificaciones concretas (tienen que existir columnas para todos los campos de todas las implementaciones)
  2. JOINED: Todos los datos comunes se volcarán en una única tabla y el resto de campos se guardará en otra tabla aparte con una FK a la tabla común que lo relacione.
  3. TABLE_PER_CLASS: Esta es una opción de JPA y puede que algunas implementaciones no la contempen. Se trata de guardar cada clase entera en su propia tabla.
De estas tres formas nos vamos a centrar en SINGLE_TABLE por ser la más implementada y de mejor rendimiento aunque implique desnormalizar nuestro modelo de datos en la BD.

¿Cómo se identifica qué tipo hay en cada fila?

Como vamos a mezclar tipos distintos en la misma tabla debe haber algún mecanismo que permita saber qué tipo concreto hay en cada fila. Para esto, además de todas las columnas para los datos de todas las distintas especializaciones se va a crear una columna que sirva para discriminar el tipo. Para implementar todo lo necesario lo voy a dividir en 4 pasos:
  1. Creación de las dos especializaciones y sus DAO
  2. ORM por XML para SucesoConId y especialización de Tarjeta
  3. ORM por anotaciones de especialización de Gol
  4. Modificar nuestro ObjectMapper como sea necesario
Paso 1: Vamos a crearnos dos de estas especificaciones: GolConId y TarjetaConId que heredarán de SucesoConId y además implementarán sus respectivas interfaces (Gol y Tarjeta). También vamos a crear los DAO correspondientes como lo hicimos en la sesión anterior sobre Spring Data Rest. Éste código no tiene ningún misterio y se puede ver fácilmente en el commit.

Como ya dijimos que para JPA las interfaces no existen, no caigamos en la tentación de asignar las definiciones a las interfaces Gol y Tarjeta (ver fichero Sucesos.orm.xml): deben ser clases que se puedan instanciar. De hecho podríamos tener implementaciones distintas a GolConId y TarjetaConId (y que tuvieran que guardarse de forma distinta también) lo que implicaría otro valor para discriminarlas. Como hay dos especificaciones voy a hacer ORM de formas distintas con cada una:

Paso 2: Tarjeta por XML (lo añado a SucesoConId.orm.xml para que se vea que puede haber varias entidades en el mismo fichero):
<entity class="es.lanyu.eventos.repositorios.SucesoConId" access="FIELD">
    <table name="SUCESOS"/>
    <!-- No hace falta strategy es el valor por defecto -->
    <inheritance strategy="SINGLE_TABLE"/>
    <discriminator-column name="TIPO"/>
    <discriminator-value>S</discriminator-value>
    <attributes>
        ...
    </attributes>
</entity>

<entity class="es.lanyu.eventos.repositorios.TarjetaConId" access="FIELD">
    <discriminator-value>T</discriminator-value>
    <attributes>
        <basic name="tipoTarjeta"/>
    </attributes>
</entity>
Paso 3: Y Gol lo haré por anotaciones añadiéndole lo que hace falta a la clase GolConId:
@Entity
@Access(value=AccessType.FIELD)
@DiscriminatorValue("G")
public class GolConId extends SucesoConId implements Gol { ... }
NOTA: ver @Access.

Paso 4: Ahora toca tunear nuestro MixIns para añadir y reutilizar código (implementación y extensión de ContadorDeMinutos y refactorizado de Datables-Partidos-Sucesos). Como no es parte de esta sesión mejor verlo en el commit o el video del webinar.

Con esto ya podemos levantar el servicio y empezar a añadir también goles y tarjetas. Veremos que crea URLs para los recursos /goles y /tarjetas donde podemos ver cada uno de ellos por separado o todos juntos en el recurso de /sucesos pero con relaciones agrupándo cada tipo distinto.

Se puede obtener el código hasta aquí en su repositorio. Se han añadido peticiones a la colección de Postman para generar goles y tarjetas aleatorias.

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.

26 de marzo de 2020

ORM por XML con relaciones @OneToMany

Los dos casos vistos hasta ahora son entidades que sólo tienen campos básicos de tipo String, pero nuestros objetos de negocio muchas veces tienen relaciones con otros objetos con un campo de su tipo o incluso contienen una lista de ellos.

En esta entrada nos vamos a centrar en objetos que tengan una lista de entidades lo que forma una relación "Uno a Muchos" que en JPA se conoce con la anotación @OneToMany o la etiqueta one-to-many (y viceversa @ManyToOne).

La relación con @OneToMany puede hacerse con una Join Table o con una clave ajena en la tabla de entidades del tipo contenido en la lista (Join Column). En esta entrada se va a ver esto último.

Para verlo voy a usar la clase Partido de datos-deportivos, ya que tiene una Collection<Suceso> que hereda de EventoImpl.

Lo primero que tenemos que hacer es implementar un código necesario previo para centrarnos después en el objeto de esta entrada. El código está en el commit previo a empezar con la definición de estas relaciones y contiene:
  1. Creación de sendas clases entidad para Partido (PartidoConId) y Suceso (SucesoConId) ya que estos tipos no tienen ningún campo que pueda usarse como Id y todas las entidades lo necesitan.
  2. En este caso los Ids serán autogenerados por la base de datos con @GeneratedValue e IDENTITY.
  3. Como todos los tipos de los que heredan Partido y Suceso son de una librería de terceros tengo que usar la configuración XML porque no puedo anotar los campos.
  4. Una vez creados los ficheros ORM hay que añadirlos a la configuración.
Estos puntos es lo que hemos visto en las entradas anteriores y veremos que se parece bastante. Viendo el vídeo hay una explicación de este código.


Usando nuestros conocimientos de modelado de datos vamos a representar la relación PartidoConId-SucesoConId añadiendo a la tabla de Sucesos la clave ajena (FK) de PartidoConId al que pertenece ese Suceso.

Para ello definimos con la etiqueta one-to-many el nombre de nuestro campo con la colección de Sucesos, el tipo de entidad destino (SucesoConId) y con qué campo se mapea (con partido - veremos qué significa esto más abajo). Usamos la siguiente etiqueta dentro de los atributos de EventoImpl:
<one-to-many name="sucesos" target-entity="es.lanyu.eventos.repositorios.SucesoConId" mapped-by="partido"/>
NOTA: Es importante destacar que el tipo objetivo debe ser una entidad, no vale con la interface Suceso, debe ser la entidad SucesoConId.

Al hacerlo de esta forma (usando una FK en una Join Column) la relación debe ser bidireccional: se puede navegar de Partido a Suceso y viceversa. Esto implica que nuestra clase SucesoConId tiene que añadir un campo partido para saber cómo se mapea esa FK. Este campo debe tener el nombre que hemos puesto en mapped-by="partido" y ser del tipo de la entidad que contiene la colección (PartidoConId).
PartidoConId partido;

public PartidoConId getPartido() {
 return partido;
}

public void setPartido(PartidoConId partido) {
 this.partido = partido;
}
Además se deben añadir los respectivos accesores (getter/setter) para este campo. Esto se debe a que hay que mantener sincronizadas las dos entidades cuando se modifique (añadir en PartidoConId otro Suceso):
public void addSucesoConId(SucesoConId suceso) {
    super.addSuceso(suceso);
    suceso.setPartido(this);
}
Por último, debemos añadir en nuestro ORM para SucesoConId la etiqueta many-to-one para cerrar la relación en los dos sentidos e indicar la columna que se usa para hacer el join:
<many-to-one name="partido" optional="false"><!-- fetch="LAZY">-->
    <join-column name="ID_PARTIDO" referencedColumnName="ID"/>
</many-to-one>
NOTA: Dejo comentado el atributo fetch="LAZY" para ver cómo se pondría si se quisiera que la relación no se traiga los datos hasta que no se necesiten.

En el código hasta aquí se puede encontrar comentadas las anotaciones que corresponden a esta configuración XML en SucesoConId. Sin embargo, podemos ver que seríamos incapaces de añadírselo a EventoImpl al no ser código que gestionemos nosotros.

Todo esto puede verse funcionando en el video anterior con un main modificado que irá insertando partidos con un suceso y lo imprime para ver el resultado.

23 de marzo de 2020

ORM por XML de clases con herencia

En la entrada anterior vimos ORM por XML de una clase simple, sin relaciones y con campos de tipos básicos (String). En esta entrada vamos a ver cómo hacer ORM por XML de una clase con herencia usando la etiqueta mapped-superclass.

Para ello vamos a usar la clase Participante del proyecto datos-deportivos que es bastante simple aunque no es un POJO pues hereda de AbstractNombrable (y pertenece a otra librería). En definitiva se trata de usar la API de JPA para definir la relación entre los campos de nuestro objeto de negocio (incluidos los declarados en la superclase) y columnas de nuestras tablas de la BD.

Para ello hay que estudiar cómo se define la clase participante. No dispone de ningún campo propio: sólo tiene identificador y nombre pero pertenecen a las clases IdentificableString y AbstractNombrable respectivamente.

Vamos a añadir a nuestra carpeta resources/jpa los archivos IdentificableString.orm.xml:
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm 
                                     http://java.sun.com/xml/ns/persistence/orm_1_0.xsd"
                 version="1.0">

  <mapped-superclass class="es.lanyu.commons.identificable.IdentificableString"
                     access="FIELD">
    <attributes>
      <id name="id" />
    </attributes>
  </mapped-superclass>

</entity-mappings>
Y AbstractNombrable.orm.xml:
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm 
                                     http://java.sun.com/xml/ns/persistence/orm_1_0.xsd"
                 version="1.0">

  <mapped-superclass class="es.lanyu.commons.identificable.AbstractNombrable"
                     access="FIELD">
    <attributes>
      <basic name="nombre" optional="false" />
    </attributes>
  </mapped-superclass>

</entity-mappings>
De esta forma ya tenemos definido cómo se persisten las clases de las que hereda. En esta ocasión no se usa la etiqueta entity como se va a seguir usando en Participante sino que usamos mapped-superclass para las superclases que no son entidades. Con esta etiqueta vamos a definir el mapeo de todo lo que es común al resto de entidades que hereden. Si nos fijamos cada superclase tiene definidos sus propios campos.

Sólo nos quedaría añadir el archivo con al mapeo de Participante, Participante.orm.xml:
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm 
                                     http://java.sun.com/xml/ns/persistence/orm_1_0.xsd"
                 version="1.0">

  <entity class="es.lanyu.participante.Participante" access="FIELD">
    <table name="PARTICIPANTES"/>
    <!-- <attributes>
      <id name="id">
        <generated-value strategy="IDENTITY"/>
      </id>
      <basic name="nombre" optional="false">
        <column length="32"/>
      </basic>
    </attributes>-->
  </entity>

</entity-mappings>
Una vez definido el mapeo nos toca añadir el DAO correspondiente con las operaciones para Participante, con lo que nos creamos el sencillo PanticipanteDAO en el paquete es.lanyu.participante.repositorios. ¿Sabes crear el código por tí mismo?


package es.lanyu.participante.repositorios;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import es.lanyu.participante.Participante;

@Repository
public interface ParticipanteDAO extends JpaRepository {}

Y no hay que olvidarse de añadir el escaneo de este repositorio a nuestra configuración (añadimos otro elemento jpa:repositories). ¿Sabes dónde hay que añadirlo?



En el fichero resources/config/jpa-config.xml

Con esto ya estamos listos para realizar operaciones CRUD con participantes en nuestra BD.

Puedes encontrar el código hasta aquí en su repositorio y ver el video del webinar:

Te propongo como ejercicio que cargues todos los participantes que están en el fichero participantes.json en la BD programando lo que haga falta para leerlos (leyendo línea a línea cogiendo las que tienen datos y descartando las líneas de comentarios), persearlo con jackson (desde una línea con datos parsearla a Participante usando el bean ObjectMapper, ojo que hay un campo que habrá que ignorar - hashcode - usando un MixIn) y luego guardarlo con ParticipanteDAO.save. Finalmente imprime por el log con nivel TRACE los participantes que recuperes que tengan en su nombre la palabra "Real" (definir método con la query en DAO). Deben verse por consola (modificando el nivel de log correspondiente en propiedades).

Inténtalo a ver hasta donde llegas y, si no lo acabas o quieres compararla con otra solución, puedes ver el siguiente vídeo:

22 de marzo de 2020

ORM por XML de POJO simple

La entrada anterior vimos una persistencia muy básica sobre una clase propia que anotábamos. En nuestras aplicaciones usamos librerías de terceros y puede que debamos definir como se realizará la persistencia con una configuración externa al código de esa librería con lo que no se pueden usar anotaciones. En ese caso se puede:
  1. Usar Data Transfer Objects (DTO): POJOs que se utilicen para enviar información desde la capa de persistencia a la de servicio haciendo mapeos en ambos sentidos o
  2. Definir el mapeo de persistencia (ORM) del mismo tipo que se usa en el servicio usando configuración por XML.
En esta entrada empezamos a ver ORM por XML empezando por lo más sencillo: un POJO simple con campos de tipos básicos. Voy a usar la clase Usuario de la entrada anterior.

Lo primero es comentar las anotaciones que usamos para marcar la entidad (@Entity) y su clave primaria (@Id). En este momento si ejecutamos nos dirá que no existe una entidad gestionada para el tipo Usuario:

java.lang.IllegalArgumentException: Not a managed type: class es.lanyu.usuarios.repositorios.Usuario

Eso ocurre porque al intentar crear el bean para UsuarioDAO no es capaz de encontrar la definición de entidad para el tipo variable que le hemos pasado (Usuario):

Error creating bean with name 'usuarioDAO': Invocation of init method failed

Vamos entonces a definir por XML lo que hemos comentado. Lo haremos en un fichero con el nombre Usuario.orm.xml que pondremos en nuestros resources dentro de una carpeta nueva que llamaremos jpa:
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm 
                                     http://java.sun.com/xml/ns/persistence/orm_1_0.xsd"
                 version="1.0">

  <entity class="es.lanyu.usuarios.repositorios.Usuario" access="FIELD">
    <table name="USUARIOS"/>
    <attributes>
      <id name="nombre">
        <!-- <generated-value strategy="IDENTITY"/> -->
        <column length="16"/>
      </id>
      <basic name="correo" optional="false" />
    </attributes>
  </entity>
</entity-mappings>
Este fichero utiliza una definición que no habíamos usado hasta ahora y tiene que ver con JPA, no con Spring Framework. Ahí está definida la etiqueta entity.

Para nuestro caso identificamos la etiqueta entity y su atributo class con el valor de la clase Usuario con la anotación @Entity comentado.

Por otro lado identificamos la etiqueta id, y su atributo name con valor nombre, dentro de la etiqueta attributes con la anotación @Id comentada.

Esta información es necesaria igual que vimos en el caso de las anotaciones ya que para guardar un objeto debe ser de una clase de entidad y poseer un campo que sirva de clave primaria.

El resto de campos no son necesarios añadirlos ya que por defecto serán guardados en la BD si no están marcados como transient igual que pasaba con la serialización en Java nativo. Para esto y para añadir otra información de persistencia a un campo se utiliza la etiqueta basic. En nuestro caso lo añadimos para obligar que el campo tenga un valor con optional="false".
NOTA: Haz la prueba y mira en H2 cómo están definidos los cambios. Luego borrar la tabla (DROP TABLE USUARIOS) y ejecútalo comentando el campo correo y mira la diferencia en la tabla (NOT NULL vs NULL).

Fíjate que he puesto un elemento table indicando el nombre de la tabla (USUARIOS), de esta forma nos guardará los usuarios en esa nueva tabla y podremos compararla con la tabla USUARIO. Si no ponemos nada le pondrá el nombre de la entidad como pasó con las anotaciones y de ahí salió nuestra tabla USUARIO. La traducción a anotación de este elemento table es @Table.

Ahora tenemos que decirle a nuestra aplicación que lo incluya, para eso lo añadimos a nuestra configuración en XML. ¿Sabréis decir dónde está?



En el fichero resources/config/jpa-config.xml

Si revisamos por encima el código enseguida vemos dónde se indican estos archivos de mapeo. Simplemente añadiendo el nuevo ya estará todo listo para que Spring coja la nueva configuración y pueda empezar a guardar usuarios de nuevo.

Puedes encontrar el código hasta aquí en su repositorio y ver el vídeo del webinar:

En la siguiente entrada veremos cómo hacer esto con clases que tienen superclases.

Logs Avanzado

Las propiedades del logging no se pueden separar en otro fichero .properties para añadirlo a nuestro contexto ya que el logging se crea antes que el ApplicationContext.

Siguiendo con el formato de la entrada de logging anterior, éste es el fichero logback-spring.xml que puede traducirse a lo que se vio en ella: formato de fecha, de patrón, color y volcado a disco. Puede verse cómo se configura en su documentación.

Además se importan otras configuraciones que se usan dentro de este fichero y se da nombre dinámicamente al archivo de log utilizando algunas propiedades leídas de application.properties.

El código de este commit se encuentra en su repositorio.

Otros temas de interés:

19 de marzo de 2020

Entidades y Repositorios con JPA

Ya sabemos que nuestros datos deben ser persistidos para poder ser recuperados al arrancar nuestro sistema, ser compartidos, almacenados para análisis por parte de otras herramientas, etc...

Hasta ahora hemos visto cómo serializar nuestros datos y guardarlos en ficheros de nuestro disco. Si tenemos una gran cantidad de datos sería bueno que en vez de leer todos los datos y tratarlos en nuestra aplicación para seleccionar el que de verdad necesito (a lo mejor un cliente de entre millones) sería bueno usar un Sistema Gestor de Base de Datos (SGBD), que aparte de ofrecerme las típicas operaciones SQL puedo consumirlo como un servicio que atienda muchas peticiones a la vez. En cualquier caso, está claro que no vale simplemente con guardar en un fichero unos cuantos datos y que Spring tiene su solución.

Me voy a centrar en las Bases de Datos Relacionales ya que JPA está pensado para estas (quedan fuera del contenido las BD NoSQL)

¿Qué es JPA?

JPA es el acrónimo de Java Persistence API lo cual deja bastante claro de qué se ocupa: Es una API para Persistencia en Java. Para saber cómo utilizarlo hay un wikibook que explica bastante clara y concisamente su uso proporcionando ejemplos en las dos formas distintas que hay de definir cómo ejecutar la persistencia de nuestros datos.

JPA es la definición de una API pero no una implementación. De la misma forma que vimos en la entrada de logging, usamos una API que nos proporciona una fachada frente a sus implementaciones y de esta forma es sencillo cambiar la implementación usada por la aplicación. Existen varias implementaciones:
Visitando sus páginas se puede ver que dan soporte a muchos SGBD distintos, incluidos NoSQL. Hay que tener en cuenta que estas implementaciones cumplen JPA pero no son sólo eso.

Lo normal es que este tipo de herramientas proporcionen un mapeo entre objetos de aplicación y registros en SGBD relacional. De esta forma la aplicación es capaz de volcar nuestros datos en memoria a una BD y viceversa. Esto se conoce como Object-Relacional Mapping (ORM) y veremos su uso más adelante.

El sentido de tener JPA, ORM, Hibernate, etc... es que hay también mucho código boilerplate y es muy laborioso de implementar, probar y repetitivo. Sirva como ejemplo ese snipet básico con JDBC de una implementación para el supuesto con el que vamos a trabajar: tiene cientos de líneas de código, hace bastante poco y está muy ligado a un SGBD concreto.

Usando JPA vamos a persistir nuestros datos en cualquier SGBD con muy poco código, de manera fiable y con una facilidad increíble de cambiar de SGBD sin tener que modificar el código. Para guardar una entidad podremos hacerlo simplemente con dos anotaciones sobre ella y declarar una interface que extienda otra con una anotación sobre ella. Voy a usar un POJO simple para una clase Usuario que pondré, para simplificar este ejemplo, en el paquete es.lanyu.usuarios.repositorios pero no es obligatorio:
@Entity
public class Usuario {

    @Id
//  @GeneratedValue
//  int id;

    String nombre;

    String correo;

}
Simplemente las anotaciones @Entity y @Id definen que esta clase es una entidad y que su clave principal es el nombre de usuario.

Para guardar la entidad Usuario hay que crear un Repositorio que tendrá las operaciones CRUD típicas. Será mi interface UsuarioDAO y lo pongo en el mismo paquete que Usuario:
@Repository
public interface UsuarioDAO extends JpaRepository {}
Con sólo estas dos líneas tengo todas las operaciones CRUD y además otras cosas como paginación y consultas personalizadas.

Si hacemos memoria, cuando vimos @Component había tres especializaciones de ella, aquí estamos usando una de ellas: @Repository.

Al marcar esta interface con esa anotación la estamos haciendo autodetectable y puede ser escaneada para ser añadida como un bean.

Si ejecutamos el código en este punto no hará nada, de hecho ni siquiera será escaneado. Nos falta el encargado de hacer que todo esto funcione, que sepa con que BD debe conectar, las credenciales para hacerlo, la implementación a usar y otras configuraciones necesarias. El responsable de todo esto en JPA se llama EntityManager.

Hay varias formas de crearlo. Nosotros vamos a usar XML y voy a incluir también dónde están nuestros repositorios de JPA. Si se nos pasa el momento de susto al ver un XML con muchas cosas que no entendemos y nos centramos en lo importante, veremos que es prácticamente un copy & paste de este snipet.


NOTA: Estoy usando la capacidad que tenemos de configurar con propiedades el XML para mejorar la reutilización y simplificarlo, pero podría estar escrito el valor directamente.

Sólo tenemos que establecer nuestras propiedades para decir dónde están nuestros repositorios y nuestras entidades:
es.lanyu.entities-package=es.lanyu.usuarios.repositorios
es.lanyu.jpa-package=${es.lanyu.entities-package}
Ya dijimos que con Spring Boot eliminamos la necesidad de usar XML. La traducción a anotación de estas dos informaciones se puede hacer sobre nuestra clase de configuración:
Nos creamos un usuario para ver que todo funciona correctamente y se nos guarda. Para simplificar las pruebas lo genero en automático así que queda nuestro código del main así:
public static void main(String[] args) {
    ConfigurableApplicationContext context =
            SpringApplication.run(DatosdeportivosapiApplication.class, args);

    UsuarioDAO usuarioDAO = context.getBean(UsuarioDAO.class);
    usuarioDAO.save(generaUsuario());
    List<Usuario> usuarios = usuarioDAO.findAll();
    usuarios.stream().map(Usuario::toString).forEach(log::info);

    context.close();
}

static Usuario generaUsuario() {
    int numero = 10000;
    String usuario = "user" + ThreadLocalRandom.current().nextInt(numero, numero*20);
    return new Usuario(usuario, usuario + "@mail.com");
}

¿Qué operaciones tiene mi repositorio?

Tiene las que se pueden esperar de un CRUD. Usando el asistente de contenido no hacemos una idea. Lo bueno de es que JPA tiene una sintaxis para el nombre de sus métodos que implica un mapeo a una sentencia SQL sin tener que implementarla. El siguiente diagrama lo describe:


Fuente: Libro Spring in Action

Podemos ver las palabras clave y posibilidades de esta sintaxis en la documentación.

En nuestro caso vamos a recuperar todos los usuarios que contengan un texto que digamos en su correo:
List<Usuario> findByCorreoContaining(String txt);
Si usamos este nuevo método, reemplazando al findAll() anterior, podemos ver que nos devuelve ya sólo los registros que coincidan:
List<Usuario> usuarios = usuarioDAO.findByCorreoContaining("5");
Todavía queda mucho para dominar JPA, pero creo que si comparamos el ejemplo usando JDBC a usando JPA la diferencia es abismal y merece la pena el esfuerzo de aprenderlo y usarlo.

Puedes ver el código hasta este punto en su repositorio.

Lo siguiente que veremos es cómo hacer ese mapeo ORM sobre entidades a las que no tenemos acceso al código.

Compárteme

Entradas populares