Introducción a Spring Batch. Toma 1

1. ¿Qué es un proceso batch?

Un proceso batch es una tarea automática para procesar un gran de volumen de datos. Este proceso se puede ejecutar sin interacción humana y repetirse periódicamente.

Qué no es un proceso batch: no es una tarea programada (un cron). Es bastante común programar un proceso batch, pero no es necesario hacerlo.

2. ¿Qué debemos tener en cuenta con un proceso batch?

  • Transaccionalidad porque queremos hacer roll back cuando los datos han sido invalidados.
  • Tolerancia a fallos porque no queremos que la aplicación termine cuando ocurra una excepción.
  • Reintentos porque… sometimes shit happens.
  • Logs y estadísticas porque de vez en cundo necesitamos saber qué ocurre dentro del proceso batch.
  • Parada y arranque de los procesos batch.
  • Administración web porque mola.
  • Particionamiento porque el trabajo puede ser compartido entre diferentes máquinas.

Cada uno de estos aspectos será comentado más tarde. He preparado unos cuantos commits en un repo para repasar todos estos puntos mostrando las diferencias en el código.

3. Conceptos de Spring Batch

Spring Batch es un proyecto que nos proporciona un framework para desarrollar aplicaciones batch. Este proyecto tiene un largo recorrido y es el resultado de la contribución de varias empresas con mucha experiencia en procesamiento batch.

Jobs, Steps, ItemReader, ItemProcessor and ItemWriter:

Fuente: Spring Batch Reference Documentation https://docs.spring.io/spring-batch/trunk/reference/html/domain.html

En Spring Batch se ejecutan jobs que se dividen en steps. Cada step tiene un componente para obtener los objetos (ItemReader), otro para procesarlos (ItemProcessor) y uno más para persistirlos (ItemWriter). El componente de procesamiento de datos es opcional.

Normalmente necesitaremos una instancia de JobBuilderFactory para declarar el Job y un StepBuilderFactory  para declarar el Step. No hay problema, Spring nos proporciona ambas.

JobExecution y JobLauncher

Un job puede ejecutarse por una instancia de JobLauncher. Durante su ejecución la información es almacenada y compartida en el JobExecutionContext y el StepExecutionContext.

El JobLauncher devuelve un JobExecution que nos da información sobre la ejecución, como por ejemplo el resultado de la misma: COMPLETED, FAILED…

JobInstance, JobParameters y RunIdIncrementer

JobInstance es la combinación de un Job y sus JobParameters. Una de las reglas de Spring Batch es que no se puede volver a ejecutar un Job si su JobExecution tiene estado COMPLETED. Sin embargo, se puede utilizar un RunIdIncrementer para ejecutar el mismo job varias veces ya que éste modifica internamente sus parámetros.

JobRepository

Spring Batch gestiona por sí sólo una base de datos con información sobre la ejecución de los jobs instanciando un bean de JobRepository. Para ello sólo necesitamos declarar la dependencia de H2 en el entorno de desarrollo.

4. Requisitos mínimos para comprobar que todo esto funciona

  • Dependencia de Spring Boot Starter Batch (aquí es donde reside toda la magia).
  • Dependencia del driver de la base de datos.
  • Anotación @EnableBatchProcessing en la clase de configuración de Spring.
  • Bean que define del job.
  • Bean que define el step con una tarea que escriba un mensaje “Reading…”. Cuando esa tarea devuelva un null indicará el fin de la fuente de datos y por tanto que el job ha terminado.
@Bean
public Job job(Step step1) throws Exception {
    return jobBuilderFactory.get("job1")
        .incrementer(new RunIdIncrementer())
        .start(step1)
        .build();
}

@Bean
public Step step1() {
    return stepBuilderFactory.get("step1")
        .tasklet(new Tasklet() {
            public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
                System.out.println("Reading...");
                return null;
            }
        })
        .build();
}

Puedes clonar el repo que he preparado para este post y ver el código añadido en el primer commit para ejecutar Spring Batch con lo mínimo necesario.

Este artículo es una traducción del que escribí hace unos meses en el blog de Fintonic Engineering.

SOLID + IntelliJ IDEA

De los tan famosos principios SOLID hay 2 que quiero tratar:

– S de Single Responsibility Principle

Nos recuerda que mantengamos las work units (para mí son las clases) con una única responsabilidad. Este concepto puede sonar un poco abstracto y yo pienso en responsabilidad como motivos para cambiar. En cuanto me huelo que el código que estoy escribiendo va a ser extendido más adelante, trato de separarlo de lo que ya conozco (que no se modifica con demasiada frecuencia).

– I de Interface Segregation

Nos recuerda que mantengamos las interfaces con el menor número de métodos posible ya que quien extiende/herede está obligado a implementarlos todos. Así, los métodos tienen que ser muy cohesivos entre sí y, por supuesto, las interfaces más bonitas son las @FunctionalInterface.

Ahora bien, partirse el pecho por estos principios es duro y cansa. La mayoría del tiempo uno va con tanta prisa que extender código significa escribir más abajo de la clase sin pararse a pensar que quizás vaya siendo hora de trocear.

Afortunadamente el IDE es nuestro amigo y podemos reducir mucho el esfuerzo utilizando la opción de refactorizacion Delegate (en IDEA) que no es más que crear un delegado dueño del código que se quiere sacar de la clase.

El ejemplo -más tonto imposible- es una clase CalculatorImpl con los métodos add() y multiply(). Intuimos que el método add() va a crecer, va a ganar importancia por sí solo y puede que se reutilice en otra parte del código. Motivos suficientes para extraerlo a una clase Adder.

public class CalculatorImpl implements Calculator {

    @Override
    public Long add(Long a, Long b) {
        return a + b;
    }

    @Override
    public Long multiply(Long a, Long b) {
        long product = a;
        for (int i = 1; i < b; i++) {
            product = add(product, a);
        }
        return product;
    }
}

Paso 1: Ctrl + T > Delegate… y escribimos el nombre de la nueva clase: AdderImpl.

  • El método en rojo indica que depende del método toBeSubtracted() y es necesario llevarlo también a la clase AdderImpl por lo que lo marcamos. En caso de no hacerlo, el delegado incluiría un campo de tipo CalculatorImpl para acceder al método.
  • El método en color azul indica que sus dependencias están satisfechas y que su código se modifica referenciando a la nueva clase AdderImpl.
  • La dependencia en campos estáticos no está analizada pero como sabemos que el método add() depende de toBeAdded lo marcamos para que lo mueva.

Ventana Delegate... de IntelliJ

Paso 2: Eliminamos el método toBeSubtracted() que no se ha eliminado de la clase CalculatorImpl.

Paso 3: Corregimos el error que se marca en @Override de AdderImpl con la opción Extract method ‘add’ to new interface y creamos la interface Adder.

Paso 4: Sobre AdderImpl Ctrl + T > Use Interface Where Possible para que la referencia en CalculatorImpl sea a la interfaz Adder. De paso, renombramos el campo con Shift + F6 a adder.

public class CalculatorImpl implements Calculator {

    private final Adder adder = new AdderImpl();

    @Override
    public Long add(Long a, Long b) {
        return adder.add(a, b);
    }

    @Override
    public Long multiply(Long a, Long b) {
        long product = a;
        for (int i = 1; i < b; i++) {
            product = adder.add(product, a);
        }
        return product;
    }
}

La magia definitiva sería que se pudiese implementar una interfaz directamente al delegar uniendo los pasos 1 y 4. Pero lo mismo es demasiado pedir…

Por qué aún desarrollo con ORMs

Hace poco más de un año escribí “La capa de persistencia, el lastre del desarrollo de software”, artículo que no publiqué porque lo consideré un borrado por terminar. Estaba muy convencido de que la persistencia era un recurso más de la aplicación y los esfuerzos deberían centrarse en un diseño de dominio ordenado y centralizado.

En este año que ha pasado he leído artículos que han hecho cambiar mi forma de pensar respecto a SQL y las bases de datos, en especial 3 reasons why it’s okay to stick with SQL. Comencé a seguir a Lukas Eder y el blog de jOOQ donde he leído cosas que me han encantado por lo que recomiendo sí o sí seguir el blog.

También he trabajado en proyectos con una cantidad ingente de datos y he visto código de proyectos enormes que poco tienen que ver con los proyectos a los que estaba acostumbrado y en cuanto a la evolución tecnológica, por ejemplo, en el mundo SAP el rey es SAP HANA, a grandes rasgos, una base de datos en memoria y una fuerte filosofía detrás: La base de datos es tan potente que la lógica hay que acercarla al origen de los datos cuanto más mejor.

Viendo todo esto en perspectiva, he de decir que soy muy cabezón: Cierto que he relajado mi postura a favor de los ORMs porque, aunque haya pasado un año, Hibernate sigue dando dolores de cabeza. Aún así, no me desharía del ORM para evitar diseminar la lógica de negocio entre el modelo de datos, la capa de persistencia y las consultas SQL y para evitar además que el dominio se vea afectado por decisiones de diseño de la base de datos (Ej. El nombre de usuario sólo puede tener 30 caracteres porque lo ordena un numerito que hay en la configuración de la base de datos y así…).

Al final (y al principio) todo es cuestión de ser ordenado para ahorrar esfuerzo personal y $$: He visto cómo solucionar un error de lógica de negocio ha supuesto en un proyecto movilizar 3 ingenieros, coordinarse (a.k.a. horas y cadenas de correos) y modificar en diferentes sitios por algo que de haber tenido toda la lógica de negocio centralizada hubiese ocupado sólo a una persona.

La capa de persistencia, el lastre de la calidad en desarrollo de software en los últimos años

Comienza un nuevo proyecto, se vislumbra una idea de negocio y una aplicación que puede facilitar la vida a mucha gente. Lo primero que hacen los desarrolladores: Pensar en el modelo relacional y en qué tipos de datos guardará la base de datos. Lo segundo: Escribir las consultas en SQL.

En la era dorada de IBM, los empleados de finanzas no conocían ningún lenguaje de programación para hacer consultas en bases de datos -obviamente-. Además, en aquel momento, los lenguajes de programación eran horribles y cuanto menos, fáciles de aprender. Por ello, se desarrolló SQL y fue de lo mejorcito que le pudo ocurrir a la industria del software y a la empresarial. En ese momento, los empleados sólo tenían que aprender SQL que no es complicado para obtener los datos que necesitaban.

En el siglo XXI tenemos lenguajes de prorgramación que son más fáciles de aprender de lo que resulta SQL y ORMs que permiten olvidarse (en gran parte) de la capa de persistencia. Mantener código en Java es más sencillo que mantener código SQL incrustado en código Java. El código escrito en el mismo lenguaje que la lógica de negocio mantiene la cohesión en el software.

La complejidad es muy alta cuando para unos mismos tipos abstractos de datos es necesario pensar desde 2 dominios con diferentes estructuras: El dominio del software que se está desarrollando y el dominio de los datos dentro de la base de datos. Al final, lo que se obtiene es un diseño del dominio del problema marcado por cómo se almacenará el estado de los objetos.

Crear una base de datos a mano preocupándose por el tipo de datos y por las relaciones entre las entidades es un engorro y las máquinas de hoy en día cuentan con las prestaciones necesarias como para que casi todas las aplicaciones puedan permitirse no aprovechar al máximo dichas prestaciones y no someter el dominio del problema a los requerimientos de la capa de persistencia.

Cuando aparecieron las PaaS, las IaaS y NoSQL mi percepción de la capa de persistencia cambió a la de una cómoda caja negra a la que se le pasa un objeto para que lo guarde y se le pide otro objeto para que lo cargue y mientras se enfocan todos los esfuerzos en el dominio del problema.

En algún momento tras el despliegue en producción y si ha habido suerte con la aplicación, puede que el ORM no arroje los datos de rendimiento que debiera para soportar la escalada de usuarios y habrá que sustituir la capa de persistencia por consultas SQL o por un servicio externo de persistencia como Firebase.

Por lo tanto, de cara a posibles problemas de escalabilidad, mi postura no es la de “Me ahorro el ORM porque al final caerá” si no la de “Dejaré el software preparado para que la sustitución del ORM no sea frustrante” como plan de riesgos para cuando el sistema escale. Porque no somos magos para predecir el futuro y dónde fallará la aplicación ni ricos para permitirnos desarrollar lo que no aportará valor a corto plazo.

Cuidado con las responsabilidades que das a Hibernate

En el capítulo 5 del libro Code Complete se discute sobre el tema de la ocultación de información. El autor sugiere que nos preguntemos «¿Qué necesita ocultar esta clase?» antes que pensar sólo en orientación a objetos.

Pone como ejemplo los identificadores de instancias y el hecho de que a menudo se elige como tipo del identificador int. De esta manera no se oculta la generación de nuevos identificadores así que sugiere que se envuelva el identificador en una clase.

Esto me ha recordado que yo caí en ese error en un proyecto con Hibernate. Al comienzo del desarrollo usaba fakes con identificadores asignados manualmente para probar el sistema. En el momento en que integré Hibernate dejé que fuese el ORM el que asignase los identificadores. Y ahí empezaron los problemas:

Cuando establecía manualmente un identificador el primero era el 0. En cambio, para Hibernate era el 1. Parece que no es tan grave, pero cargó de complejidad el proyecto porque desde ese momento era necesario pararse a pensar qué identificador tenía cada dispositivo, cambiarlo para hacer pruebas y más aún: Pensar cómo gestionar las pruebas sin Hibernate y con Hibernate. La complejidad en cuanto a gestión de configuración es mucho más alta que la que hubiese supuesto implementar un generador de identificadores.

La cosa es que el identificador de un objeto parece un valor muy insignificante como para preocuparse por él. Pero la experiencia me ha enseñado que asignar un identificador es demasiada responsabilidad como para otorgársela a un módulo de la capa de persistencia.