El problema de N+1 en Hibernate

Hibernate es uno de los frameworks ORM (Object-Relational Mapping) más populares en el ecosistema de Java, facilita la vida de los desarrolladores al permitirles interactuar con bases de datos usando entidades en lugar de complejas consultas SQL. Sin embargo, nada de esto sale gratis. Los ORM tienen sus propios retos, siendo el problema N+1 uno de los más notorios y potencialmente perjudiciales para el rendimiento de las aplicaciones.

El problema N+1 se refiere a una situación en la que una operación que debiera requerir una sola consulta a la base de datos termina realizando N consultas adicionales, una por cada elemento resultante de la primera consulta. Este comportamiento puede pasar desapercibido con pocos datos, pero puede degradar significativamente el rendimiento de la aplicación, especialmente en contextos con grandes volúmenes de datos o bajo condiciones de carga elevada. La ironía del problema N+1 radica en que, a menudo, se origina por una de las principales ventajas de Hibernate: la facilidad para obtene objetos relacionados automáticamente sin intervención explícita del desarrollador.

Entendiendo el problema N+1

El problema N+1 se refiere a un patrón de acceso a datos que da como resultado un número desproporcionado de consultas a la base de datos. Ocurre cuando un sistema realiza una petición para recuperar N entidades de una tabla y, luego, por cada entidad recuperada, realiza consultas adicionales para cargar detalles asociados de otra tabla. En esencia, lo que debiera ser una operación de recuperación de datos se convierte en N+1 operaciones: una consulta principal para obtener todas las entidades requeridas y luego, al menos, una consulta adicional para cada entidad, sumando un total de N consultas adicionales.

Ejemplo

Nada mejor que un ejemplo para entenderlo. Imaginemos que tenemos una aplicación que gestiona libros y autores. Cada libro tiene asociado un autor.

@Entity
public class Libro {

  @ManyToOne
  private Autor autor
  //otras variables y métodos get y set
}

@Entity
public class Autor {
  
  @OneToMany
  private List<Libro> libros;
  //otras variables y métodos get y set
}

Si queremos cargar 10 libros con sus respectivos autores utilizando un enfoque simple de Hibernate, podríamos terminar ejecutando 1 consulta para obtener los libros y luego 10 consultas adicionales, una por cada libro, para cargar los datos de sus autores. Así, en lugar de ejecutar una sola consulta, el sistema acaba realizando 11 consultas. Las famosas N+1

List<Libro> libros = session.createQuery("FROM Libro").list();
for(Libro libro : libros) {
    Autor autor = libro.getAutor(); // Esto desencadena una consulta adicional por cada libro
}

En este ejemplo, el problema es fácil de localizar, el bucle donde se recorren los libros. Sin embargo, hay otros ejemplos donde no es tan fácil identificar el problema.

List<Libro> libros = session.createQuery("FROM Libro").list();
return mapper.toDto(libros);

En este caso, el mapeador va por todos los libros convirtiéndolos a DTOs. Al llegar a los autores, el ORM detecta la carga perezosa y vuelve a lanzar la consulta extra. Volvemos a tener el problema de N+1.

Pero… ¿Tan grave es?

Pues sí. El problema N+1, aunque puede parecer inicialmente como un inconveniente menor o un detalle técnico, tiene el potencial de impactar significativamente el rendimiento y la escalabilidad de una aplicación. Este problema, junto al de los índices en la base de datos, es el típico que no se detecta en el entorno de desarrollo por la baja carga de datos y hunde la aplicación en producción.

Además, hay que tener en cuenta que, donde hay un N+1, acostumbran a haber más problemas, como el llamado producto cartesiano u otras consultas N+1.

Degradación del rendimiento

El impacto más directo del problema N+1 es la degradación del rendimiento. Cada consulta adicional a la base de datos introduce latencia adicional, no solo debido al tiempo de ejecución de la consulta misma, sino también por la sobrecarga de establecer la conexión a la base de datos, la serialización de datos, y más. En entornos de producción, donde múltiples usuarios pueden estar interactuando con la aplicación simultáneamente, estas consultas adicionales pueden acumularse rápidamente, llevando a tiempos de respuesta lentos y a una experiencia de usuario frustrante.

Consumo de recursos

Además de afectar la latencia, el problema N+1 puede conducir a un uso ineficiente de los recursos, tanto en el servidor de la aplicación como en el sistema de gestión de bases de datos (DBMS). La ejecución de múltiples consultas incrementa el uso de CPU y memoria, pudiendo llegar a saturar estos recursos bajo carga pesada. Esto no solo afecta a las operaciones relacionadas con el problema N+1 sino también a otras operaciones de la aplicación que compiten por los mismos recursos.

Escalabilidad

El problema N+1 puede ser un obstáculo significativo para la escalabilidad de una aplicación. A medida que el volumen de datos crece, el número de consultas adicionales necesarias para realizar operaciones simples puede aumentar exponencialmente, haciendo que la base de datos se convierta en un cuello de botella. Esto limita la capacidad de la aplicación para manejar un mayor número de usuarios o para procesar datos más rápidamente, lo cual es especialmente problemático en aplicaciones destinadas a escalar.

¿Me está pasando a mí?

Pasa en las mejores familias, así que sí, te puede estar pasando a ti. Detectar y diagnosticar el problema N+1 puede ser un reto, especialmente en aplicaciones grandes y complejas. Identificar el problema en tiempo de desarrollo es clave para mitigar el problema antes de que afecte significativamente el rendimiento de tu aplicación.

Análisis de logs

Son la primera línea de detección de N+1. Habilitar el logging de las consultas SQL generadas por Hibernate te permite ver exactamente qué consultas se están ejecutando. Una señal clara del problema N+1 es observar una secuencia repetitiva de consultas similares, variando solo en un parámetro, como el ID de una entidad.

Profilers

Las profilers de aplicaciones y bases de datos son esenciales para detectar el problema N+1. La clave es que estos proporcionan una vista detallada del número de consultas generadas por cada operación de tu aplicación, permitiéndote ver claramente cuando se realiza una cantidad excesiva de consultas en relación con una sola acción.

  • Hibernate Statistics: Activar las estadísticas de Hibernate puede ofrecer una visión interna de las operaciones de base de datos, incluyendo el conteo de consultas, lo cual es útil para identificar problemas N+1.
    Puedes activarlas desde Spring poniendo a true la propiedad spring.jpa.properties.hibernate.generate_statistics. Desde el hibernate.cfg.xml o el persistence.xml puedes hacerlo poniendo a true la propiedad hibernate.generate_statistics
  • Herramientas de Monitorización de Bases de Datos: Cualquier base de datos cuenta con herramientas específicas que te indican las consultas realizadas y su número. Estos pueden ayudar a identificar patrones de consultas repetitivas que son indicativos del problema N+1.

Pruebas de carga

Las gran olvidadas. Hacer pruebas de carga en la aplicación ayuda a identificar problemas de rendimiento que no son evidentes durante el desarrollo o las pruebas unitarias. Las pruebas de carga simulan un número elevado de solicitudes a la aplicación, lo que puede ayudar a revelar problemas N+1 que solo se manifiestan bajo condiciones de carga significativa.

¡Tengo un N+1!

Calma. Ante todo calma. Resolver un problema de N+1 no es difícil. Te doy algunas soluciones fáciles de implementar.

JOIN FETCH en HQL o JPQL

Mi preferida es utilizar la cláusula JOIN FETCH en las consultas HQL o JPQL. Esta técnica permite cargar la entidad principal y sus relaciones en una sola consulta, eliminando la necesidad de consultas adicionales para cada entidad relacionada.

String hql = "SELECT l FROM Libro l JOIN FETCH l.autor WHERE l.categoria = :categoria";
List<Libro> libros = session.createQuery(hql)
                            .setParameter("categoria", categoria)
                            .list();

Uso de Entity Graphs

Aunque no son santo de mi devoción, también sirve para resolver el problema. Los Entity Graphs fueron introducidos en la especificación JPA 2.1 y ofrecen una manera dinámica de definir qué atributos o relaciones de una entidad deben ser cargados en una consulta. Esta técnica proporciona control sobre la carga de datos y puede ser utilizada para prevenir el problema N+1.

EntityGraph<Libro> graph = em.createEntityGraph(Libro.class);
graph.addAttributeNodes("autor");
Map<String, Object> hints = new HashMap<>();
hints.put("javax.persistence.loadgraph", graph);
Libro libro = em.find(Libro.class, libroId, hints);

Configuración del Batch Size

Otra estrategia efectiva es configurar el tamaño de lote (batch size) para las asociaciones. Esto permite a Hibernate cargar las relaciones de varias entidades en menos consultas, agrupando las entidades en lotes. Esta configuración se puede hacer a nivel de mapeo de la entidad con anotaciones o en el archivo de configuración de Hibernate.

@OneToMany
@BatchSize(size = 10)
private Set<Autor> autores;

Esto construirá una SELECT IN con 10 valores (el aportado por la anotación) y reduce el número de consultas de N+1 a 2, siempre que N sea menor que 10. En general, no te recomiendo esta solución si no sabes muy bien qué estás haciendo (y si estás leyendo esto…)

Uso de Subselects

Hibernate permite configurar una asociación para que use un subselect para cargar todas las relaciones con una sola consulta secundaria. Esta estrategia es útil cuando se accede a todas las entidades relacionadas al mismo tiempo, pero puede ser menos eficiente si solo se necesita acceder a una parte de ellas (el nombre completo del autor, pero no su edad, por ejemplo).

@OneToMany
@Fetch(FetchMode.SUBSELECT)
private List<Autor> autores;

Estrategias de Caché

Bueno, supongamos que nada de lo anterior te funciona o, por el motivo X, no puedes aplicarlo. Vale, no desesperemos, aún podemos solucionarlo.

El uso de cachés de segundo nivel y cachés de consulta puede reducir significativamente la cantidad de consultas necesarias a la base de datos. En nuestro ejemplo, la consulta del autor de un libro es fácilmente cacheable y es muy probable que se ejecute en múltiples lugares de la aplicación. Cuando Hibernate haga el N+1, detectará que las N consultas extras, o una gran parte de ellas, están cacheadas y no las repetirá cuando haga el N+1. Hará las que no tenga en memoria, pero las dejará almacenadas para las subsiguientes consultas.

Es una solución parcial: Soluciona o mitiga el problema, pero a costa de un mayor uso de memoria. Además, si un autor se modifica, Hibernate invalidará la cache de ese autor y volverá a hacer la consulta; así, si nuestra colección de datos tiene un número elevado de modificaciones, el parche será poco eficiente.

¿Cómo prevenir el N+1?

Prevenir el problema N+1 desde el principio del desarrollo de una aplicación te va a ahorrar mucho tiempo y recursos. Implementar las siguientes mejores prácticas no solo ayuda a evitar este problema específico, sino que también contribuye a un diseño de software más limpio y eficiente.

Diseño del modelo de datos

El diseño del modelo de datos es fundamental para cualquier aplicación y, en mi experiencia, se hace sin prestar mucha atención.

  • Evaluación de relaciones: Analiza cuidadosamente las relaciones entre entidades en tus modelos de datos. Considera si todas las relaciones son necesarias y si su cardinalidad está correctamente definida. En algunos casos, una relación OneToMany puede ser sustituida por una relación ManyToMany, o viceversa, para optimizar las operaciones de consulta.
  • Carga perezosa vs. Carga ansiosa: Decide conscientemente entre carga perezosa (LAZY) y carga ansiosa (EAGER) para cada relación en tus entidades. La carga perezosa es preferible para la mayoría de las situaciones, pero en casos donde siempre necesitas acceder a entidades relacionadas, la carga ansiosa puede ser más eficiente.

Consultas

En muchas ocasiones el problema sucede porque el programador no quiere complicarse la vida. Ve que existe una consulta que obtiene los datos que ya quiere y no se preocupa en verificar que esa consulta se trae todos los datos y no es una carga perezosa la que lo hace. Desgraciadamente, este tipo de programación despreocupada no es posible. Para cada situación has de utilizar consultas optimizadas para recuperar solo los datos necesarios. Aprovecha las cláusulas JOIN FETCH, SELECT NEW, y subconsultas para reducir el número de consultas y evitar cargar datos innecesarios.

Pruebas y monitoreo continuo

Mucha gente piensa que las pruebas y monitorización de la aplicación se hacen una vez esta ha concluido. Nada más lejos de la realidad.

  • Pruebas de Rendimiento: Efectúa pruebas de rendimiento regularmente a medida que desarrollas y mantienes tu aplicación. Esto te ayudará a identificar problemas de rendimiento, incluido el problema N+1, en etapas tempranas y te ahorrará muchos dolores de cabeza posteriores.
  • Monitoreo de la aplicación: Utiliza herramientas de monitoreo y profiling en desarrollo, pruebas y preproducción para detectar y analizar el comportamiento de las consultas. Esto facilita la identificación de problemas N+1 antes de que afecten a los usuarios finales.

Equipo

Seamos sinceros, el problema principal de N+1 es que sucede por el desconocimiento de los programadores. La gente ve que Hibernate funciona automagicamente y no se paran a verificar que lo que hacen tiene sentido.

  • Capacitación del equipo: Asegúrate de que todos los miembros del equipo de desarrollo comprendan qué es el problema N+1 y cómo evitarlo. Reuniones periódicas donde se explican problemas típicos y se muestran sobre el código actual del desarrollo son extremadamente útiles.
  • Revisión de código: Implementa un proceso de revisión de código que incluya la verificación de posibles problemas N+1. Las revisiones de código por pares son una excelente oportunidad para fortalecer las habilidades del equipo.

Conclusiones

El problema N+1 es, junto con el del producto cartesiano del que hablaré en otra ocasión, uno de los grandes problemas de los ORM. Los programadores tienden a tener la ilusión de que con un ORM ya no hace falta crear consultas en base de datos, puesto que el ORM las crea según las necesita. En tiempo de desarrollo la ilusión se mantiene hasta que se llega a producción donde estalla el problema.

Detectar y solucionar el problema de las N+1 puede ser un reto en algunas ocasiones, pero no es técnicamente difícil. Tenemos herramientas que nos proporcionan pistas para detectar el problema y la solución acostumbra a ser simple.

En cualquier caso, la mejor manera de prevenir el problema es que todo el mundo sea consciente de él e intentar prevenirlo desde el tiempo de desarrollo.

Publicado en Programación | Deja un comentario

El precio hora del programador y la del servicio de la limpieza

«Nuestro objetivo es que la hora del programador cueste lo mismo que la del servicio de limpieza.»Reloj

Esto lo dijo un consultor de una gran empresa a un gerente de un proyecto en una empresa del IBEX35. Este mismo gerente me lo comentó durante una comida, mientras hablábamos de por qué no puede haber grandes diferencias entre los sueldos de los programadores.

Mi postura era que, si sabemos que existe un programador que saca el trabajo equivalente de tres personas adelante (y existen), lo normal es que cobrase el doble de sueldo (estaba siendo prudente, porque el sentido común dice que habría de cobrar el triple). Él lo rebatía con un caso muy simple: La hora del programador es fija. Esto es, un programador de una categoría determinada cuesta, pongamos, 35 € la hora y eso es independientemente de su rendimiento.

Existe un baremo inferior, que es básicamente que, si el programador no da la talla, se le sustituye, pero no existe un baremo superior. Así, para un mismo puesto, un programador pasable cuesta lo mismo que un programador excelente. Por lo tanto, es imposible pagarle el doble a nadie, ya que enseguida se entra en pérdidas. ¿Que saca el doble de trabajo adelante? No importa, lo que importa es el precio por hora.

Todo este pensamiento está basado en una falacia: los programadores son perfectamente intercambiables entre ellos. Solo se necesita alguien que les diga lo que tienen que hacer y ellos se dedicarán a mecanografiar el código. Por eso hay gente ilusionada con mano de obra barata de factorías de la India, con la que pretenden bajar más el precio por hora hasta el nivel del servicio de la limpieza. (Quisiera aclarar que no tengo nada contra el servicio de la limpieza. La comparación no es mía).

La realidad es que programar no es equivalente a mecanografiar. Un programa se puede hacer de muchas maneras distintas y hay algunos programadores que siempre tienden a hacerlo de la mejor manera posible. La diferencia entre hacerlo normal y bien pueden ser días de trabajo en proyectos normales y semanas en grandes proyectos. Imaginemos la diferencia entre hacerlo pasable y excelente. Además, los buenos programadores tienden a crear un código sin grandes errores y fácilmente mantenible, lo que repercute en el mantenimiento posterior.

Entrados en este vicio del precio por hora, hay programadores que se frustran al ver que sacan proyectos adelante y lo único que obtienen son problemas. ¿El proyecto no funciona? Pongamos a este que sabe. ¿La cosa va bien? Pongamos a este que no sabe tanto. Al final, muchos emigran a sitios donde el sueldo es mucho más elevado (léase Estados Unidos o Gran Bretaña), otros buscan clientes finales donde pueden romper el vicio del precio por hora y otros dejan la programación y se dedican a gestionar.

Y es que, amigos míos, gestionar tiene algo llamado primas, beneficios, comisiones o como se quiera llamar, que puede hacer que los sueldos se disparen. El porqué un programador que termina un programa en la mitad de tiempo lo único que puede esperar es que le pongan a hacer otro programa y no una prima es algo que se escapa a mi razonamiento.

Publicado en Artículos | Deja un comentario

El jedi que sabía Hibernate

Confieso que cuando me dicen que evalúe de 1 a 10 mis conocimientos de Java dudo bastante qué poner.

Sable láser

Poner un 10 me parece excesivo. Sí, tengo buenos conocimientos de Java, conozco sus instrucciones, tengo un razonable conocimiento de la JDK y de la JVM, y sé leer el bytecode de un programa compilado para ver qué hace. ¿Es eso un conocimiento de 10? Bueno, un 10 representa la perfección. Si me dijeran que programase directamente en bytecode, habría de documentarme primero. Además, no conozco todas y cada una de las cuatro mil y pico clases de la JDK; siempre encuentro alguna función nueva que desconocía. ¿Es eso un 7? ¿Un 8? Ni idea. Por otra parte, hay mucha gente que no duda en poner un 10 y, seamos sinceros, hay más personas que se autoevalúan con un 10 que con un 8. Si yo no pongo un 10, parecerá que no domino suficiente. ¿Qué hacer? Siempre dudo.

Por si fuera poco, hay gente que ya no tiene suficiente con un 10: existen los gurús, los ninjas, los jedis de nuestros tiempos. Sus conocimientos ya no se pueden evaluar de 0 a 10; al parecer, hay que inventar una nueva categoría. Y no solo eso, ¡cada vez hay más! Regocijaos, porque sin duda es una señal de que la llegada del Javatar está próxima. ¿Tendré yo un nivel gurú y no me habré dado cuenta? Lo dudo.

A lo largo del tiempo, he visto gente que se autoevaluaba con nivel 10 de Java, incluso con un nivel gurú o que decían ser un Java Ninja, y he tenido la oportunidad de hacerles algunas preguntas. La realidad es que ninguno me ha parecido que dominase Java. Si pregunto para qué sirve volatile, la respuesta no puede ser «Esto lo vi en la certificación, pero ya no me acuerdo». ¿No estaba hablando yo con un gurú? Si sus conocimientos son de 10 o más, habría de saber todas las instrucciones de Java, ¿no? ¿O será que soy demasiado estricto?

Para mí, el problema es que si alguien dice que es un Jedi, tiende a creer que sus opiniones son las más válidas y no se le piden explicaciones. Después de todo, estamos hablando de un Jedi, los mismos que destruyeron la estrella de la muerte y liberaron la galaxia. ¿Qué podría ir mal con uno de ellos al mando?

Bueno, permitidme una historia personal. Una vez, en un proyecto en el que estuve, se encontró con un error de Hibernate. Esto no es de por sí raro; es normal encontrarse errores en librerías de terceros. En este caso, el error estaba en la cola de acciones (ActionQueue) de Hibernate. No quiero aburriros con los detalles; la cuestión es que, bajo ciertas condiciones muy pero que muy concretas, al lanzar una consulta, Hibernate fallaba con un ArrayIndexOutOfBoundsException. La solución era añadirle un parámetro particular (quitar el flush) a esa consulta y ya está. Pero no todo podía ser tan fácil: ese proyecto tenía unas librerías de arquitectura que eran las encargadas de lanzar las consultas, y esas librerías estaban a cargo de otra empresa que tenía un gurú de Hibernate como arquitecto. Y lo más importante, todos los jefes (que no sabían nada de Hibernate) creían que era un gurú y su opinión era canon. Resumiendo una historia de meses, el error no se corrigió porque no nos dejaba poner ese parámetro. En cambio, impuso una solución que leyó en StackOverflow, que era modificar el flush en toda la aplicación en lugar del de solo esa consulta. Si alguien entiende de Hibernate, ya se puede imaginar el impacto de ese cambio. Ni que decir tiene que su solución no funcionó. No funcionó porque el arquitecto no comprendía lo que sucedía; no entendía por qué fallaba Hibernate porque no entendía cómo funcionaba la cola de acciones. Sí, esa consulta en concreto ya no daba error, pero había otras consultas que hasta entonces no daban problemas y dejaron de dar el resultado esperado. Las siguientes correcciones no arreglaron nada, sino que hicieron que nos pusiéramos en modo «huida hacia adelante». Había correcciones sobre correcciones, y ya nada tenía sentido. No recuerdo el nombre del arquitecto, pero sí el apodo que le pusieron: «El Jinete del Apocalipsis».

Y aquí es donde quería llegar. Un gurú de Hibernate (y digo Hibernate como podría ser Java, Spring o lo que sea) es aquel que lo conoce todo sobre Hibernate. Conoce cómo funciona internamente, conoce sus clases principales; si tienen algún error, sabe crear un parche que lo solucione y, llegado el caso, sería capaz de volverlo a programar desde cero. ¿Sabes hacer todo eso? Entonces tienes mi respeto. Pero si no es así, siento decirte que no eres un gurú, no eres un ninja, y tus posibilidades de convertirte en Jedi y derrotar a Darth Vader tienden a cero.

Publicado en Artículos | Deja un comentario

Netflix killed the Big Data star

La programación siempre se ha movido por modas. Unas veces, un lenguaje o framework son el futuro y, al año siguiente, ya nadie habla de ellos. ¿O acaso alguien se acuerda de Backbone.js o de cuando Ruby on Rails iba a desbancar a PHP? Logo de Netflix

Hace unos años, se empezó a hablar del Big Data y, por ende, de las bases de datos NoSQL. Big Data estaba de moda y se notaba. Aparecía en cualquier noticia del telediario, los alcaldes hablaban de cómo el Big Data nos iba a cambiar la ciudad y hasta la tienda de la esquina decía que la utilizaba para analizar comportamientos de compra y así reponer productos con antelación. De repente, en todos los proyectos parecía necesario Redis o MongoDB. Vi arquitectos que no eran capaces de hacer una simple join convertirse en expertos en Big Data. Y no solo eso, te daban sabios consejos del tipo: si la base de datos tenía más de un millón de registros, no se podía utilizar SQL y se debía utilizar Big Data. Por supuesto, el problema era que, al no dominar SQL, cualquier consulta con un millón de registros era más lenta que un caracol. ¿Solución? ¡Big Data para todos!

Pero ahora, Big Data ya no es lo más en la programación. Una nueva tecnología ha ascendido y le ha arrebatado su popularidad. Ahora, la nueva estrella es la arquitectura Netflix. ¿Su anterior proyecto fracasó? Eso es porque no utilizó la arquitectura Netflix. ¿O quizás no fue por eso?

La arquitectura Netflix se llama así porque la desarrolló Netflix buscando una solución a un problema concreto: el suyo. Por ejemplo, ¿cuánta gente mira la televisión a las cinco de la mañana? Muy poca. ¿Y a las nueve y media de la noche? Muchísima más. ¿Cuántos servidores necesitaba Netflix? Tantos como para dar servicio al máximo pico de usuarios simultáneos. El problema, claro, es que después de unas horas los servidores reducen su carga y ya no son necesarios. Como Netflix alquilaba los servidores a Amazon, se les ocurrió que lo que tenían que hacer era poder encender o apagar los servidores automáticamente según el número de clientes conectados. ¿Hay pocos usuarios? Cierro servidores y de paso me ahorro dinero. ¿La gente se pone a ver la televisión después de la cena? Los servidores se van encendiendo según necesidad. Elegante y útil. Y esto es solo un problema al que dan solución; existen muchos más.

Ahora bien, nada de esto es gratis. Para que esto funcione, Netflix ha tenido que utilizar tecnologías (Microservicios, por ejemplo) que dificultan cierto tipo de programación. La integridad referencial en bases de datos es una de ellas, las transacciones es otra. Quizás puede parecer poca cosa, pero los que llevamos algo de tiempo en esto y hemos programado sin integridad referencial o sin transacciones les tenemos cierto cariño porque facilitan la vida mucho. Pero mucho, mucho. ¿Por qué hace esto Netflix? Porque, en su modelo de negocio, el video streaming, estas tecnologías no son críticas.

Ahora pensemos. ¿Es buena la arquitectura Netflix? Sin lugar a dudas. ¿Significa eso que en mi próximo proyecto debo utilizar la arquitectura Netflix? No. Como he dicho, la arquitectura Netflix se pensó para un problema concreto: el de Netflix. ¿Tiene tu modelo de negocios alguna similitud con el de Netflix? En la mayoría de los casos, la respuesta es no. ¿Entonces, qué te hace suponer que necesitas su arquitectura? He visto gente proponer arquitectura Netflix para proyectos de no más de cinco usuarios concurrentes. Menos de cinco y ponen una arquitectura para dar soporte a millones. En otros casos, la utilizan en proyectos donde las transacciones o la integridad referencial son críticas. Pero oye, si Netflix está de moda, será por algo.

¿Cuál es la siguiente moda? Solo hace falta ver LinkedIn para darse cuenta: Blockchain. Las criptomonedas están de moda, por lo tanto, Blockchain mola. Ya he visto propuestas de Blockchain para todo y se habla de ella como “la tecnología que cambiará los negocios”.

Así que ya sabes, cuando alguien te proponga utilizar una nueva tecnología como Netflix o Blockchain en tu próximo proyecto, pregúntate: ¿Realmente lo necesito? ¿Qué me aporta respecto a otras tecnologías? ¿Qué me quita? Y, basándote en esas respuestas, toma tu decisión. Pero, por favor, no lo hagas solo porque está de moda.

Publicado en Artículos | Deja un comentario

El día en que Ko Isono se suicidó para no tener que programar más

En 1993, Apple lanzó al mercado el Newton, un dispositivo de un tamaño sin precedentes que, entre otras funciones, era capaz de convertir la escritura manuscrita en texto digital.

El desarrollo del software del Newton estuvo marcado por un ambiente de trabajo extremadamente exigente, con jornadas interminables y sin descanso, ni siquiera durante los días festivos. Se estima que unos treinta desarrolladores programaron un millón de líneas de código. Entre ellos estaba Ko Isono. El 12 de diciembre de 1992, Isono regresó a su hogar, tomó una pistola y se suicidó disparándose en el corazón. Una semana más tarde, otro programador sufrió un ataque de nervios, agredió a su compañero de piso y fue encarcelado. Estos son solo algunos ejemplos de las severas crisis emocionales y de ansiedad que afectaron al equipo.

Ko Isono trabajaba en un proyecto crítico para Apple, no era un becario y, se presume, tenía un salario considerable. Para sorpresa de sus colegas, se había casado recientemente en un viaje a Japón y había traído a su esposa a vivir con él en Silicon Valley. Pronto, su esposa se sintió desolada: estaba en un país extranjero y prácticamente abandonada por su marido, que apenas pasaba tiempo en casa. Aunque el trabajo no fue la única causa, parece razonable considerar que contribuyó significativamente al suicidio de Isono.

¿Por qué el desarrollo del Newton resultó ser tan problemático? Intentaré detallar las tres principales causas que, en mi opinión, hicieron la vida insufrible para el equipo de desarrollo.

  • Falsas expectativas: Se prometieron características revolucionarias para el Newton, incluido el reconocimiento de escritura manuscrita. Lo que ahora puede parecer trivial era, hace 25 años, un desafío monumental. De un concepto inicial se intentó pasar directamente a un producto de consumo sin dedicar el tiempo necesario a la investigación. Tras descartar cuatro años de trabajo, se empezó de nuevo desde cero. Sin embargo, esto no mejoró la situación. En una prueba, por ejemplo, al escribir a mano «¿Catching on?» (¿Entendiendo?), el Newton lo interpretó como «Egg freckles» (Pecas de huevo), lo cual se convirtió en una broma interna en Apple.
  • Plazos de entrega irreales: Según el New York Times, John Sculley prometió demasiado en muy poco tiempo. Mientras Sculley anticipaba una nueva era en la informática, el desarrollo apenas estaba comenzando. Se fijó como objetivo abril de 1992 para finalizar el Newton, un plazo completamente irreal. Esto llevó a jornadas laborales de 18 horas sin vacaciones. Después del suicidio de Ko Isono, muchos programadores tomaron vacaciones en Navidad, aunque un pequeño grupo permaneció bajo las órdenes de Steven Capps en un «esfuerzo heroico». Finalmente, el Newton se lanzó en agosto de 1993.
  • Falta de comunicación: Apple depositó todas sus esperanzas en el Newton, un producto tan adelantado a su tiempo que precedió al iPad por dos décadas. Con la alta dirección presionando para su lanzamiento, era difícil admitir lo que todos los desarrolladores ya sabían antes de comenzar: el Newton no estaría listo a tiempo. Alguien debería haber intervenido y gestionado las expectativas adecuadamente. La falta de comunicación o la negativa a escuchar llevó a una espiral de decisiones equivocadas en un intento desesperado por cumplir con los plazos y las expectativas.

A pesar de que el Newton finalmente se lanzó al mercado, John Sculley ya no estaba al frente de Apple. Aunque el dispositivo contó con un núcleo de seguidores leales, la nueva dirección de la compañía tenía otras prioridades y no promocionó suficientemente el producto. Poco después de regresar a Apple, Steve Jobs descontinuó el Newton, en lo que algunos interpretaron como una venganza contra Sculley, quien lo había despedido en 1985.

La importancia de esta historia

Conocí la historia de Ko Isono en una exposición de Doug Menuez en el Palacio de la Virreina, en Barcelona, y me impactó profundamente. La exposición sugería que Isono se suicidó el día que se decidió descartar todo el trabajo realizado hasta ese momento y empezar de nuevo desde cero, una decisión impactante. Aunque no he podido confirmar si ese fue exactamente el día de su suicidio, los motivos detrás de su trágica decisión siguen presentes. Meses antes de la exposición, presencié a una colega sufrir un ataque de nervios al salir del trabajo. Nos encontrábamos en un proyecto que, inicialmente previsto para tres meses, se extendió por tres años. Las constantes horas extra, el descarte y reprogramación del trabajo, y los plazos de entrega irreales generaron un ambiente de estrés, ataques de nervios, desmayos e insomnio. La irresponsabilidad de algunos tuvo graves repercusiones en la salud de muchos.

Esta situación no es única de aquel tiempo; proyectos actuales siguen enfrentando desafíos similares. Es responsabilidad de todos, desde los programadores hasta los jefes de proyecto y directores de operaciones, identificar y abordar estos problemas.

Publicado en Artículos | Deja un comentario

Inyección de dependencias

La inyección de dependencias es un patrón de programación que, a pesar de lo que mucha gente cree, no requiere ninguna anotación del tipo @Inject o @Autowired para aplicarse.

La idea del patrón es crear las dependencias de una clase en tiempo de ejecución en lugar de en la compilación.

Por ejemplo, supongamos la siguiente clase:

public class MiClase {

  private List miLista;

  public MiClase() {
    this.miLista = new ArrayList();
  }
}

La dependencia, miLista, se crea en el tiempo de compilación, ya que la propia clase es la encargada de la construcción de sus dependencias.

Mientras que en el siguiente ejemplo:

public class MiClase {

  private List miLista;

  public MiClase(List lista) {
    this.miLista = lista;
  }
}

Se inyecta la dependencia en tiempo de ejecución, ya que la dependencia miLista se le proporciona a la hora de crearse como parámetro en el constructor.

Existiría otro ejemplo donde la dependencia se le proporciona mediante un set.

public class MiClase {

  private List miLista;

  public void setMiLista(List miLista) {
    this.miLista = lista;
 }
} 

La ventaja del patrón es que el código es más flexible, puesto que podemos proporcionar diferentes implementaciones de la interfaz, en este caso List, y no limitarnos a uno solo, como en el ejemplo sin dependencia.

Como punto negativo del patrón se añade un acoplamiento entre clases, puesto que se crea una dependencia entre la clase que construimos y la clase que le inyecta los valores necesarios para su construcción.

Publicado en Glosario | Etiquetado , , | Deja un comentario

Thread safe

Un código es thread-safe si puede ser ejecutado por varios hilos de ejecución simultáneamente de manera segura. Esto significa que la ejecución de un hilo no corrompe los datos ni interfiere indebidamente con los procesos que se ejecutan en paralelo.

Un código se puede hacer thread-safe mediante diferentes aproximaciones. La más conocida es el uso del modificador synchronized, que básicamente impide que más de un hilo ejecute simultáneamente un segmento de código específico. Pero esta no es la única técnica; el uso de la palabra clave final, volatile, variables de clases inmutables o de objetos sin estado también son técnicas útiles para asegurar la correcta ejecución en entornos de múltiples hilos.

Así que ya sabéis, no hagáis como aquel consultor que, en medio de una reunión de dirección, dijo que un código no era thread-safe solo porque, tras echarle un vistazo, le pareció que había pocos synchronized.

Publicado en Glosario | Etiquetado | 3 comentarios

¿Es un int? ¿Es un String? ¡Es Superman!

Esta es una de esas cosas que veo demasiado a menudo

private int telefono;

Es común ver que datos como el teléfono, el código postal, o el DNI se representen mediante variables numéricas (int) en la programación, cuando en realidad deberían tratarse como cadenas de texto (String). Este error surge del razonamiento de que, al consistir únicamente de números, tales datos deben ser representados como tal. Sin embargo, los tipos de datos numéricos están diseñados para realizar operaciones aritméticas, algo que no aplicamos a datos como el número de teléfono o el DNI. ¿Alguna vez alguien ha visto algo del tipo telefono++? o ¿telefono = (telef1 + telef2)/2 ? Evidentemente no. Pues entonces el número de teléfono, el código postal o el DNI son un String.

La decisión de usar int o String debería basarse en la función del dato dentro de la aplicación. Si no se van a realizar cálculos matemáticos con el dato, entonces su naturaleza es textual, no numérica. Las preocupaciones sobre la optimización y el rendimiento son, en muchos casos, infundadas.

Y si aún no estás convencido:

  • Estos tipos de datos acostumbran a tener un significado (los dos primeros dígitos del código postal son la provincia, por ejemplo) que se puede extraer mucho más fácilmente con un String que con int .
  • Muchos de estos «números» pueden empezar por cero: códigos postales, teléfonos… lo que complica mucho su tratamiento como número.
  • Los requisitos de formato pueden cambiar, haciendo que un diseño inicial basado en números enteros sea insuficiente o inflexible. Cuantas veces he visto que alguien ha puesto puerta (en una clase Dirección) como int y luego no podía poner 4B y tienen que añadir un nuevo registro, este sí como un String.

En conclusión, a menos que un dato se vaya a usar en operaciones aritméticas, su representación más adecuada es como una cadena de texto (String).

Publicado en Diseño | Deja un comentario

Expresiones regulares útiles

Las expresiones regulares son un gran invento que nos permiten validar de manera fácil una cadena e caracteres  Lamentablemente su sintaxis puede parecer algo esotérica lo que hace que mucha gente se líe al utilizarlas. A continuación van algunos de los ejemplos más comunes que utilizo en mis validaciones.

Las explicaciones de las expresiones regulares son la traducción de las ofrecidas por My RegexTester, lugar ideal donde hacer pruebas.

Código postal

El código postal en España son cinco números. Los dos primeros van del 01 al 52 y los tres restantes pueden ser cualquier valor numérico

0[1-9][0-9]{3}|[1-4][0-9]{4}|5[0-2][0-9]{3}

Explicación

------------------------------------------------------------------
	0		'0'
------------------------------------------------------------------
	[1-9]		cualquier carácter del '1' al '9'
------------------------------------------------------------------
	[0-9]{3}	cualquier carácter del '0' al '9'(3 veces)
------------------------------------------------------------------
|			O
------------------------------------------------------------------
	[1-4]		cualquier carácter del '1' al '4'
------------------------------------------------------------------
	[0-9]{4}	cualquier carácter del '0' al '9'(4 veces)
------------------------------------------------------------------
|			O
------------------------------------------------------------------
	5		'5'
------------------------------------------------------------------
	[0-2]		cualquier carácter del '0' al '2'
------------------------------------------------------------------
	[0-9]{3}	cualquier carácter del '0' al '9'(3 veces)
------------------------------------------------------------------

Fecha

Una fecha en formato día mes año con los siguientes separadores posibles: »,’/’,’ ‘ o ‘-‘. Se permiten tanto valores del tipo 1/1/2001 como 01/01/2001

Notad un detalle que casi nunca veo en expresiones regulares de otros ejemplos: el formato del primer separador se almacena para asegurar que el segundo separador es del mismo estilo. Es decir, valores como 12/12-2012 son incorrectos ya que, a pesar de que el primer y segundo separador tienen caracteres permitidos, no son iguales.

(?:0?[1-9]|[12][0-9]|3[01])([/ -\])(?:0?[1-9]|1[012])\1[12][0-9]{3}

Explicación

------------------------------------------------------------------
(?: 			agrupa pero no captura el valor
------------------------------------------------------------------
	0? 		'0' (opcional)
------------------------------------------------------------------
	[1-9]		cualquier carácter del '1' al '9'
------------------------------------------------------------------
|			O
------------------------------------------------------------------
	[12]		cualquier de los siguientes caracteres:
			'1', '2'
------------------------------------------------------------------
	[0-9]		cualquier carácter del '0' al '9'
------------------------------------------------------------------
|			O
------------------------------------------------------------------
	3		'3'
------------------------------------------------------------------
	[01]		cualquier de los siguientes caracteres:
			'0', '1'
------------------------------------------------------------------
) 			fin del grupo
------------------------------------------------------------------
(			agrupa y guarda el valor en 1:
------------------------------------------------------------------
	[/ -\]		cualquier de los siguientes caracteres:
			'/', ' ', '-', ''
------------------------------------------------------------------
)			fin del grupo 1
------------------------------------------------------------------
(?:			agrupa pero no captura el valor
------------------------------------------------------------------
	0?		'0' (opcional)
------------------------------------------------------------------
	[1-9]		cualquier carácter del '1' al '9'
------------------------------------------------------------------
|			O
------------------------------------------------------------------
	1		'1'
------------------------------------------------------------------
	[012]		cualquier de los siguientes caracteres:
			'0', '1', '2'
------------------------------------------------------------------
)			fin del grupo
------------------------------------------------------------------
\1			el valor que se guardó en el grupo 1
------------------------------------------------------------------
[12] 			cualquier de los siguientes caracteres:
			'1', '2'
------------------------------------------------------------------
[0-9]{3}		cualquier carácter del '0' al '9'(3 veces)
------------------------------------------------------------------

Email

Posiblemente uno de los patrones más utilizados. Fuente: Mykong

[_A-Za-z0-9-]+(?:\.[_A-Za-z0-9-]+)*@[A-Za-z0-9]+(?:\.[A-Za-z0-9]+)*(?:\.[A-Za-z]{2,})

Explicación

------------------------------------------------------------------
[_A-Za-z0-9-]+		cualquiera de los siguientes caracteres:
			'_', 'A' a 'Z', 'a' a 'z', '0' a '9', '-'
			(una o más veces)
------------------------------------------------------------------
(?:			agrupa pero no captura el valor
------------------------------------------------------------------
	.		'.'
------------------------------------------------------------------
	[_A-Za-z0-9-]+	cualquiera de los siguientes caracteres:
			'_', 'A' a 'Z', 'a' a 'z', '0' a '9', '-'
			(una o más veces)
------------------------------------------------------------------
)*			fin del grupo (0 o más veces)
------------------------------------------------------------------
@			'@'
------------------------------------------------------------------
[A-Za-z0-9]+		cualquiera de los siguientes caracteres:
			'A' a 'Z', 'a' a 'z', '0' a '9'
			(una o más veces)
------------------------------------------------------------------
(?:			agrupa pero no captura el valor
------------------------------------------------------------------
	.		'.'
------------------------------------------------------------------
	[A-Za-z0-9]+	cualquiera de los siguientes caracteres:
			'A' a 'Z', 'a' a 'z', '0' a '9'
			(una o más veces)
------------------------------------------------------------------
)*			fin del grupo (0 o más veces)
------------------------------------------------------------------
(?:			agrupa pero no captura el valor
------------------------------------------------------------------
	.		'.'
------------------------------------------------------------------
	[A-Za-z]{2,}	cualquiera de los siguientes caracteres:
			'A' a 'Z', 'a' a 'z' (al menos 2 veces)
------------------------------------------------------------------
) fin del grupo 
------------------------------------------------------------------

DNI

Uno fácil. Nueve dígitos más una letra mayúscula o, si eres extranjero, una letra, siete números y otra letra. La letra del DNI se calcula mediante un algoritmo que es de sobras conocido pero que en este tipo de validaciones por patrones no se puede calcular.

Respecto a dicha validación, mucha gente no sabe es que existe alguna partida de DNIs que tienen la letra mal calculada (sin comentarios, pero supongo que a la mayoría no le sorprende). Si hacemos que la validación de la letra cuadre con el algoritmo un día nos encontraremos que una persona con un DNI legal no se puede dar de alta en nuestro sistema. Para solucionarlo es mejor que la comprobación de la letra se marque como un aviso pero que sea posible insertar un DNI con letra incorrecta para hacerle la vida más simple a esos pobres desdichados que van por el mundo con un DNI incorrecto.

[0-9A-Z][0-9]{7}[A-Z]

Explicación

------------------------------------------------------------------
[0-9A-Z]		cualquier carácter del '0' a '9', 
			'A' a la 'Z'
------------------------------------------------------------------
[0-9]{7}		cualquier carácter del '0' a '9' (7 veces)
------------------------------------------------------------------
[A-Z]			cualquier carácter de la 'A' a la 'Z'
------------------------------------------------------------------

Teléfono

Permite prefijo internacional (hasta 5 dígitos) que pueden estar precedidos de un carácter ‘+’ y pueden estar entre paréntesis, más el número en si. El número está en formato de dos números seguidos, espacio opcional, un ciclo de 6 números más espacio opcional y un número al terminar. De esta manera se permiten formatos como XXXXXXXXX, XXX XXX XXX, XX XXX XX XX o XXX XX XX XX .

(?:[+]?(?:[0-9]{1,5}|\x28[0-9]{1,5}\x29)[ ]?)?[0-9]{2}(?:[0-9][ ]?){6}[0-9]

------------------------------------------------------------------
(?:				agrupa pero no captura el valor
------------------------------------------------------------------
	[+]?			'+' (opcional)
------------------------------------------------------------------
	(?:			agrupa pero no captura el valor
------------------------------------------------------------------
		[0-9]{1,5}	cualquier carácter del '0' al '9'
				(1 a 5 veces)
------------------------------------------------------------------
	|			O
------------------------------------------------------------------
		\x28		carácter 40 ( '(' )
------------------------------------------------------------------
		[0-9]{1,5}	cualquier carácter del '0' al '9'
				(1 a 5 veces)
------------------------------------------------------------------
		\x29		carácter 41 ( ')' )
------------------------------------------------------------------
	)			fin del grupo
------------------------------------------------------------------
	[ ]?			' ' (opcional)
------------------------------------------------------------------
)?				fin del grupo
------------------------------------------------------------------
(?:				agrupa pero no captura el valor
------------------------------------------------------------------
	[0-9]			cualquier carácter del '0' al '9'
------------------------------------------------------------------
	[ ]?			' ' (opcional)
------------------------------------------------------------------
){8}				fin del grupo (repetir 8 veces)
------------------------------------------------------------------
[0-9]				cualquier carácter del '0' al '9'
------------------------------------------------------------------
Publicado en Programación | Etiquetado , , , | Deja un comentario

Transacciones con Spring

El tratar transacciones con Spring e Hibernate es una de las casuísticas más utilizadas por los proyectos de programación y, aun así, continúa siendo una de las menos conocidas.

Introducción

Una transacción de base de datos es un conjunto de instrucciones ejecutadas en bloque. Por ejemplo, realizo una consulta, modifico un registro A en la base de datos y elimino un registro B. Si se produce un error en alguna de estas instrucciones, todo el proceso se revierte. Así, si luego consulto la base de datos, observaré que el registro A no ha sido alterado. A este proceso de revertir las instrucciones realizadas se le denomina «hacer un rollback«, mientras que el proceso de confirmar todas las instrucciones en bloque, una vez comprobado que no se ha producido ningún error, se le llama «hacer un commit«.

Las transacciones se inician y finalizan a nivel de servicio, nunca a nivel de DAO (Objeto de Acceso a Datos, por sus siglas en inglés). Esto tiene sentido, ya que el servicio es el encargado de gestionar toda la lógica de negocio: llama a los DAOs necesarios para consultar, guardar o modificar registros y debe hacerlo de manera atómica. Por ejemplo, consideremos un proceso batch que opera durante la noche y se encarga de recalcular las hipotecas de los clientes. En el primer paso, obtiene el registro de la hipoteca; en el segundo, recalcula la hipoteca; y en el tercero, actualiza el registro del cliente con una marca para indicar que el proceso ya ha sido ejecutado para ese cliente. El DAO se ocupa de la consulta, de la actualización de la hipoteca y de la actualización del registro con la marca de manera individual: primero con un método que ejecute una consulta, luego con una actualización de la hipoteca y, posteriormente, con una actualización en la tabla de avisos del cliente. Sin embargo, es el servicio, o su equivalente en procesos batch, el que asegura que todo este proceso se maneje como una unidad. Si la transacción comenzase a nivel del DAO, no se podría controlar la atomicidad de todo el proceso o, peor aún, se tendría que trasladar la lógica de negocio a la capa del DAO.

Además, con JPA/Hibernate, cada vez que deseamos realizar una modificación en la base de datos, necesitamos una transacción activa. Esto se aplica incluso si solo vamos a insertar un dato y no un bloque de acciones.

Transacciones con Spring

La manera más común de gestionar las transacciones en Spring es mediante la anotación @Transactional en la cabecera del método de una clase (nunca de una interfaz) administrada por Spring:

@Transactional
public void realizaAccionTransaccionalmente() {
	// Soy transaccional.
}

Además, es necesario incluir la propiedad <tx:annotation-driven> en el archivo de contexto de Spring.

Sin embargo, la gestión de transacciones no es tan sencilla como parece. Spring opera con los beans a través de proxies, lo que añade complejidad al asunto. Aquí no entraremos en detalle sobre qué es un proxy, pero es importante señalar que, debido a esto, la anotación solo funcionará en métodos públicos que accedan a la clase. Por lo tanto:

@Transactional
private void realizaAccionTransaccionalmente() {
	// No soy transaccional a pesar de la anotación.
}

no funcionará porque el método es privado. Lo peor es que no se avisará del error, ya que @Transactional se trata como metadato: se utiliza si es aplicable, pero se ignora en caso contrario.

Otro ejemplo que tampoco funcionará:

@Transactional
protected void realizaAccionTransaccionalmente() {
	// No soy transaccional a pesar de la anotación.
}

En este caso, el método tampoco es público.

Veamos un escenario que puede causar confusiones:

public class A {

	@Autowired
	private B b;

	public void ejecutaAccionEnB() {
		b.realizaAccion();
	}
}

public class B {

	public void realizaAccion() {
		realizaAccionTransaccionalmente();
	}

	@Transactional
	public void realizaAccionTransaccionalmente() {
		// ¿Soy transaccional?
	}
}

Aquí, aunque el método realizaAccionTransaccionalmente esté marcado con @Transactional, no se iniciará una transacción porque se accedió a él a través de un método no transaccional (realizaAccion).

Por el contrario:

public class A {

	@Autowired
	private B b;

	public void ejecutaAccionEnB() {
		b.realizaAccion();
	}
}

public class B {

	@Transactional
	public void realizaAccion() {
		realizaAccionTransaccionalmente();
	}

	public void realizaAccionTransaccionalmente() {
		// Solo soy transaccional si se me llama
		// desde el método realizaAccion.
	}
}

En este caso, la transacción se activa correctamente porque @Transactional está sobre el método de entrada a la clase.

Es crucial tener en cuenta este detalle sobre los métodos de entrada, ya que puede causar frustraciones. La regla es clara: la anotación solo tiene efecto en la cabecera de un método a través del cual se accede a la clase. Aunque es posible marcar todos los métodos o incluso la clase entera como transaccionales, esto puede llevar a problemas, por lo que es recomendable proceder con cautela.

Ámbito de la transacción

Es esencial entender la duración de una transacción, es decir, cuándo comienza y termina. Si un método está anotado con @Transactional, la transacción comenzará justo antes de la primera línea del método y terminará después de la última. Si este método llama a otros, estas llamadas se ejecutan dentro de la misma transacción sin necesidad de anotarlos con @Transactional.

Cuando un método anotado con @Transactional invoca a otro método que también está anotado con @Transactional, entra en juego el concepto de propagación de transacción. Por defecto, la transacción tiene una propagación de PROPAGATION_REQUIRED, que significa que si no existe una transacción, se crea una nueva; si ya existe, se aprovecha la existente. En el ejemplo proporcionado, el primer método abre la transacción y el segundo la utiliza, manteniendo ambas operaciones dentro de la misma transacción.

Si se desea que el método de la clase B ejecute una nueva transacción, permitiendo que, si algo falla en B, no afecte a la transacción de A, es necesario modificar la propagación en B como se muestra a continuación:

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void hazAlgoB() {
	hazAlgoTransaccionalmente();
}

Con esta configuración, al entrar en B se generará una nueva transacción.

Spring soporta varios tipos de propagación de transacciones:

  • PROPAGATION_REQUIRED: Comportamiento predeterminado. Utiliza la transacción existente o crea una nueva si no hay ninguna.
  • REQUIRES_NEW: Inicia una nueva transacción, suspendiendo cualquier transacción existente hasta que la nueva transacción finalice.
  • PROPAGATION_SUPPORTS: Ejecuta dentro de una transacción si ya existe una; de lo contrario, se ejecuta fuera de una transacción.
  • PROPAGATION_MANDATORY: Exige que exista una transacción; si no hay ninguna, lanza una excepción.
  • PROPAGATION_NEVER: Asegura que no se ejecute dentro de una transacción; si hay una transacción activa, lanza una excepción.
  • PROPAGATION_NOT_SUPPORTED: Ejecuta fuera de la transacción existente; cualquier transacción activa se suspende hasta que finalice el método.

Cada una de estas opciones de propagación permite un control detallado sobre cómo y cuándo se deben manejar las transacciones dentro de aplicaciones Spring, permitiendo ajustar el comportamiento transaccional a los requisitos específicos de cada situación.

Errores comunes con @Transactional

Transacción obligatoria

Un malentendido frecuente es pensar que se necesita una transacción para realizar cualquier operación con la base de datos, incluyendo consultas simples. Aunque técnicamente Hibernate maneja estas operaciones bajo una transacción implícita, desde el punto de vista del desarrollador, no es necesario marcar explícitamente las consultas de solo lectura con @Transactional. Esto se debe a que Hibernate puede abrir y manejar transacciones de manera transparente, incluso si no se indica explícitamente.

Transacciones read-only

Otra confusión es la propiedad readonly. Si ponemos

@Transactional(readonly=true)
public void hazAlgoTransaccionalmente() {
	// Soy transaccional!
}

¿Qué se supone que hace? Según la documentación, una transacción readonly puede ser utilizada cuando quieres que tu código lea, pero no modifique ningún dato y deja intuir que puede ser beneficioso en términos de rendimiento.

Ahora bien, ¿cómo afecta a esto a la hora de hacer inserciones y/o actualizaciones? Supongamos el siguiente código lanzado con JUnit sobre una base de datos HSQLDB:

@Test
@Transactional(readonly=true)
public void insertTest() {
	List resultados = genericDao.getAll(Persona.class);
	Assert.assertEquals(3, resultados.size());

	Persona p = new Persona();
	p.setNombre(&quot;Test&quot;);
	p.setEdad(33);
	Assert.assertEquals(0, p.getId());

	genericDao.insert(p);
	resultados = genericDao.getAll(Persona.class);
	Assert.assertEquals(4, resultados.size());
}

¿Qué sucede? La verdad es que esto tiene algo de trampa. Uno puede pensar que si he marcado la transacción como readonly la inserción de la persona no debería hacerse y el test fallaría. Pero no, Hibernate pasa del readonly e inserta el registro sin problema. Otros pensarán: No, que no te enteras, lo que sucede es que la entidad persona queda marcada como de solo lectura y lo que no puedo es modificarla y luego hacer un update. Pues tampoco, le cambio la edad y para la base de datos. Los más avispados pueden pensar, lo que sucede es que no haces un flush antes de la query. Vale, aceptamos barco. Eso sí que es cierto. O sea, que parece que la documentación no es correcta y el readonly prácticamente no hace nada.

El problema es cuando sales del test y vas a producción y el comportamiento es diferente y empiezan a saltarte errores. WTF! ¿Qué sucede aquí? ¡Si mis tests funcionan perfectamente! Pues sucede que el parámetro readonly es dependiente de a) la implementación JPA y b) el dirver jdbc que estés utilizando.

Por ejemplo, el test anterior lo lancé en una base de datos HSQLDB, pero el comportamiento en Oracle puede ser diferente. Con Oracle 11 sí que puede generar una transacción de solo lectura, con HSQLDB no existe esa posibilidad. Ojo, esto no pasa en todas las versiones de Oracle, ya que depende del driver que utilices. Si el driver sobreescribe correctamente el método setReadOnly del interfaz Connection tendremos una transacción readonly. Si no lo tenemos, pues solo tendremos que no nos hace un flush antes de una query.

Y lo malo de todo esto es que no está documentado en ninguna parte. Para que uno se fíe de la documentación. Así que ojo.

Modificar la transacción

Intentar cambiar los parámetros de una transacción después de haber sido abierta es un error común. Por ejemplo, algo que ya he visto alguna vez:

public class A {

	@Autowired
	private B b;

	public void hazAlgoA() {
		b.hazAlgoB();
	}

}

class B {

	@Autowired
	private C c;

	@Transactional
	public void hazAlgoB() {
		// inserto un dato en bbdd
		try {
 			c.hazAlgoC();
		catch( NullPointerExecption e) {
		}
		// inserto otro dato en bbdd

	}
}

class C {

	@Transactional(readonly=true)
	public void hazAlgoC() {
		// leo un dato en bbdd
	}
}

Se abre una transacción para B se llama a C y cómo se quiere hacer una lectura se marca la transacción como readonly. Bueno, esto no es posible; una vez que una transacción está abierta, sus parámetros no pueden ser modificados hasta que se complete.

Commit y Rollback

Bueno, ya lo tenemos (casi) todo claro. Ahora lo que queremos es controlar el commit o el rollback de nuestra transacción. La cosa es simple, si un método anotado como @Transaccional lanza una excepción que herede de RuntimeException se producirá un rollback. En caso contrario, un commit.

Vamos a dar vueltas a esto. Primero lo más evidente:

public class A {

	@Autowired
	private B b;

	public void hazAlgoA() {
		try {
			b.hazAlgoB();
		}
		catch( NullPointerExecption e) {
		}
	}

}

public class B {

	@Transactional
	public void hazAlgo(Entidad entidad) {
		// inserto un dato en bbdd

 		throw new NullPointerException();

		// inserto otro dato en bbdd
	}
}

lanza una NullPointerException, que hereda de RuntimeException, luego se produce un rollback y la primera inserción no se realiza (ni la segunda, claro).

Veamos esto

public class A {

	@Autowired
	private B b;

	public void hazAlgoA() {
		try {
			b.hazAlgoB();
		}
		catch( FileNotFoundException e) {
		}
	}

}

public class B {

	@Transactional
	public void hazAlgo(Entidad entidad) {
		// inserto un dato en bbdd

 		throw new FileNotFoundException();

		// inserto otro dato en bbdd
	}
}

¿Qué hará? ¿Inserta un dato? ¿Dos? ¿Ninguno? Veamos, FileNotFoundException no hereda de RuntimeException por lo que no debería hacer rollback, pero lanzamos la excepción antes de insertar el segundo dato así que el flujo del programa no llegaría al código de insertarlo, pero si hace el primero, ergo solo insertamos un dato. Podemos imaginar que hay algo así:

public void hazAlgo(Entidad entidad) {
	try{
		tx.begin

		// inserto un dato en bbdd
		throw new FileNotFoundException();

		// inserto otro dato en bbdd
	}
	catch(Exception e ) {
		if( e instanceof RuntimeException ) {
			tx.rollback();
			tx.close();
		}
		else {
			tx.commit();
			tx.close();
		}
		throw e;
	}
}

¡Esto no es el código que hay realmente! Tan solo es poner en pseudocódigo lo explicado anteriormente.

Compliquémoslo un poco más:

public class A {

	@Autowired
	private B b;

	public void hazAlgoA() {
		b.hazAlgoB();
	}

}

class B {

	@Autowired
	private C c;

	@Transactional
	public void hazAlgoB() {
		// inserto un dato en bbdd
		try {
 			c.hazAlgoC();
		catch( NullPointerExecption e) {
		}
		// inserto otro dato en bbdd

	}
}

class C {

	@Transactional
	public void hazAlgoC() {
		// inserto un dato en bbdd

 		throw new NullPointerException();

		// inserto otro dato en bbdd
	}
}

Esta es más interesante. La clase C lanza una NullPointerException que hereda una RuntimeException ergo debería hacer un rollback. Pero, pero, pero, la clase B, donde se inicia la transacción, ya tenía previsto este error, así que captura la excepción y no la vuelve a lanzar. ¿Hará un rollback? ¿Hará un commit? Hagan sus apuestas.

Bueno, hace un rollback. El punto es que si una RuntimeExcepcition sale de un método anotado como @Transactional, toda la transacción queda marcada como rollback. No importa que no fuese el método desde el que se comenzó la transacción, si su propagación es normal (esto es, no es una REQUIRES_NEW), en cuanto lanzamos la excepción estamos vendidos.

Esto es importante por un motivo, hay gente que, ante la duda, anota todos los métodos o las clases con @Transactional; sin embargo, esto supone un problema de concepto de construcción. El encargado de saber si la transacción ha de marcarse como rollback o commit es siempre el método desde donde se inicia la transacción, nunca uno de los métodos a los que llama. Si yo llamo a un método externo que está marcado como transaccional y este por algún motivo me lanza una RuntimeExcepción, automáticamente me obliga a hacer un rollback. No me deja la opción de que sea yo el que valore si puedo continuar o no, quizás ya tenía previsto que podía saltar esa excepción y la capturaba para tratarla. Lamentablemente, al utilizar una librería que anotó su método con @Transactional me limita de cualquier intento de recuperarme de la excepción.

Moraleja, solo hay que poner @Transactional en métodos donde se abre, cierra y controla la transacción. En el resto de métodos su colocación puede ser contraproducente.

Controlar las excepciones que hagan commit o rollback

El hecho anterior, que una transacción haga rollback si se lanza una RuntimeExcecption y commit si no, es algo demasiado rígido para muchos casos. Hay momentos que no queremos que se haga un rollback para alguna excepción en concreto o lo contrario, no queremos que haga un commit cuando se lanza una excepción que no herede de RuntimeException. Para esto tenemos las propiedades noRollbackFor y rollbackFor, con estas opciones podemos configurar el commit y el rollback según nuestras necesidades.

Por ejemplo:

@Transactional(noRollbackFor={NumberFormatException.class,ArithmeticException.class})
public void hazAlgoTransaccionalmente() {
	// Soy transaccional!
}

Hará un commit incluso si lanza las excepciones NumberFormatException o ArithmeticException o excepciones que hereden de estas. Lo de las excepciones que hereden de esta es muy importante para tener en cuenta a su alcance. Hay gente que lo aprovecha para hacer cosas del tipo:

@Transactional(noRollbackFor={RuntimeException.class})
public void hazAlgoTransaccionalmente() {
	// Soy transaccional!
}

Lo cual es una muy pésima idea porque implica que el método nunca hará un rollback.

Tal como he dicho existe la propiedad inversa, rollbackFor. Con esta podremos conseguir que la transacción haga un rollback para las excepciones que no hereden de RuntimeException.

@Transactional(rollbackFor={FileNotFoundException.class})
public void hazAlgoTransaccionalmente() {
	// Soy transaccional!
}

Hará un rollback si lanza una FileNotFoundException a pesar de que esta no hereda de RuntimeException.

Transacciones programáticas

No siempre se pueden abrir transacciones con @Transactional. A veces necesitamos crear una transacción en algún lugar que la anotación no nos permite (un método privado, por ejemplo) así que hemos de buscarnos un método alternativo. Para esto utilizaremos las transacciones programáticas  Con estas podremos abrir y cerrar transacciones sin necesidad de anotaciones

Veamos el siguiente código:

Obejct o = new TransactionTemplate(transactionManager).execute(new TransactionCallback() {
	public Object doInTransaction(TransactionStatus status) {
		// Soy Transaccional!
	}
});

Con él podemos abrir la transacción sin necesidad de anotaciones.

El código se puede modificar para añadir diferentes tipos de transacciones (propagación o aislamiento), para que nos devuelva un resultado ((TransactionCallback) o no (TransactionCallbackWithoutResult), etc. Para más información leer el manual

Lo que me dejo en el tintero

Aislamiento de Transacciones (Isolation)

El nivel de aislamiento de una transacción define cómo las operaciones realizadas dentro de una transacción son visibles para otras transacciones y cómo se manejan las operaciones concurrentes. Cambiar el nivel de aislamiento puede tener implicaciones significativas, permitiendo, por ejemplo, que una consulta devuelva datos que han sido insertados pero aún no se han confirmado (commit). Los niveles de aislamiento (READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE) ofrecen un equilibrio entre rendimiento y consistencia de datos, afectando a fenómenos como las lecturas fantasma, las condiciones de carrera, y otros problemas de concurrencia.

Duración de la Transacción

Las transacciones muy largas, que involucran cientos o miles de inserciones o modificaciones, pueden ser problemáticas debido al consumo de memoria y a la complejidad en la gestión de la consistencia de datos a lo largo del tiempo. En general, es preferible mantener transacciones pequeñas y bien definidas, que limitan el alcance de los cambios y reducen la probabilidad de conflictos y errores.

Transacciones y Aspectos

La programación orientada a aspectos (AOP) permite superar algunas de las limitaciones de las anotaciones en Spring, como la incapacidad de aplicar @Transactional a métodos privados o la necesidad de anotar explícitamente el método de entrada de la clase. A través de bibliotecas como AspectJ, es posible interceptar llamadas a métodos y aplicar manejo transaccional de forma más flexible y potente, aunque este enfoque requiere una comprensión sólida de AOP y puede aumentar la complejidad del diseño.

Transacciones Declarativas en Configuración

Spring también ofrece la posibilidad de definir transacciones de manera declarativa en los archivos de configuración. Esto permite especificar políticas transaccionales para métodos o clases enteras basándose en patrones de nombres u otros criterios, como hacer que todos los métodos cuyo nombre comienza por «get» sean de solo lectura, y aquellos que comienzan por «insert» inicien transacciones normales. Aunque potente, este enfoque puede resultar demasiado genérico para ser útil en todos los casos y no es comúnmente usado en muchos proyectos. No obstante, es valioso conocer esta capacidad por las posibilidades que ofrece en términos de diseño y configuración de transacciones.

Publicado en Programación, Programación-JPA, Programación-Spring | Etiquetado , , , | 13 comentarios