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.

Compárteme

Entradas populares