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.

No hay comentarios:

Publicar un comentario

Compárteme

Entradas populares