Tinselcity

Esto me ha costado un poco creérmelo, a pesar de que lo estaba viendo con mis propios ojos. En fin, empecemos por el principio.

Paginas de datos

La aplicación es, básicamente, unos cuantos miles de búsquedas en BBDD. A ver, es una plataforma de gestión logística muy amplia, así que es normal. Está hecha para eso.

Las consultas en BBDD nunca se hacen con querys directamente. Siempre hay un proceso almacenado que guarda gran parte de la lógica de negocio de la aplicación. La relevancia de esto para el tema que nos ocupa es casi accidental. Resulta que las decenas de miles de procesos almacenados de la aplicación están bastante mal escritos1). El rendimiento desde el principio era “no muy bueno”. En los comienzos establecieron algunas prácticas de paginación, de modo que las búsquedas de los procesos almacenados siempre llevaban un…

where rn between :P_INICIO and :P_FINAL order by rn

…de modo que se le pasaban los parámetros P_INICIO y P_FINAL con los valores apropiados y devolvían el segmento de resultados apropiado.

Es decir, que la paginación se hacía en la propia consulta a la BBDD y a cliente sólo se traía una página cada vez. El usuario hacía una consulta que tardaba quizá 20 segundos y le traía la primera página. Si quería ver la siguiente página de resultados, le tocaba esperar otro tanto.

Así que decidieron que esto era mala idea y trataron de matar dos pájaros de un tiro con una solución sencilla. Primero: se pagina en cliente; es decir la consulta trae todos los resultados y se hace la paginación en JavaScript en cliente. Obviamente esto solo es casi peor idea porque la parte de cliente en JavaScript era casi peor que la de PL y encima el navegador oficial era Internet Explorer. Imagina cargar de una tacada, no sé, 50.000 filas en una tabla… Peor aún, en un componente tabla que tiene toda una funcionalidad dinámica de interacción, de filtros, de búsqueda, de mil cosas más. Claro, una vez cargado un listado de resultados la paginación era casi instantánea, pero a cambio la carga inicial era insoportable y bloqueaba muchas veces el navegador. Ahí entra en juego la segunda parte de la solución: Limitar siempre el número de resultados. Y por defecto con un límite relativamente bajo: 500 resultados.

No voy a insistir en el curioso hecho de que, en realidad, en la mayoría de búsquedas no se traen simplemente los primeros 500 resultados. No. Lo que se hace es que primero se cuenta cuántos resultados se obtendrán con los criterios introducidos. Si van a ser más de 500, se aborta y se devuelve al usuario un mensaje para que ponga criterios más específicos. En fin, es una decisión muy discutible, pero no es el momento de discutirla.

Lo que sí hay que señalar es que, a pesar de que se haga eso, se sigue manteniendo esa cláusula WHERE sobre el número de resultados. El único cambio es que desde entonces P_INICIO siempre se pasa a 0 y P_FINAL al máximo de resultados permitidos.

Value Objects, herencia, trastos por el suelo

Como digo el cambio se abordó de forma sencilla, sin tener que cambiar todos esos procesos almacenados, simplemente pasando siempre el intervalo de 0 a MAX_RESULTS (500, casi siempre). Es interesante de todos modos ver un poco la forma en que se hizo esto.

La parte Java de la aplicación apenas tiene lógica de negocio… bueno, apenas tiene lógica y punto. De hecho apenas tiene casi nada que tenga sentido. Pero dejando eso a un lado… Básicamente es un tubo directo desde las peticiones que recibe una caótica capa de Struts hasta unos DAOs que llaman a los procesos almacenados. El camino de vuelta el 50% de las veces es el mismo tubo directo. El otro 50% ni siquiera existe porque se escribe en la response desde el propio DAO “para optimizar”. En fin, digo que es un tubo porque realmente no hay lógica ni nada. Se reciben la petición HTTP, se meten los datos que vengan en un Value Object y este se manda hasta el DAO. A la vuelta el DAO devuelve el mismo VO relleno con los resultados hasta que se mete en la respuesta. El algunos casos hay alguna pequeña lógica más como agregar un par de búsquedas juntas o cosas así, pero poco más.

Tampoco conviene detenerse demasiado a pensar que los VOs no representan nada demasiado estricto. Y no me refiero a que sean simplemente una bolsa de datos sin demasiada forma que simplemente guarda todos los datos de una búsqueda. Me refiero más bien a que es habitual que los VOs se utilicen para media docena de búsquedas relacionadas o no tanto. Así son realmente bolsas de datos sin forma. Algunos de ellos tienen no sé, doscientas, trescientas propiedades. Algunas, obviamente, repetidas por descuido.

Bueno, pero volvamos a eso de la paginación.

El caso es que si todos los VOs van a transportar un intervalo con esos parámetros P_INICIO y P_FINAL, alguien tuvo un momento de inspiración y decidió que esto estaba pidiendo aplicar herencia. Se hace que todos los VOs hereden de un PaginableSortableVO 2) que será el que defina unas propiedades desde y hasta. Todos los VOs heredan esas propiedades y se ponen como valores por defecto 0 y 500 y un montón de trabajo ahorrado.

En los DAOs, en todas las búsquedas, aparece siempre:

mapa.put("P_INICIO", someVO.getDesde());
mapa.put("P_FINAL", someVO.getHasta());

Cierto es que, por ejemplo, P_INICIO se podría haber eliminado por completo ya que es innecesario. Sí, hay cuatro o cinco cosas que se podrían haber simplificado en el momento de hacer esto pero una de las máximas del proyecto siempre ha sido que no se limpia nada. Que se vive con la basura porque si se tiene tempo para limpiar es porque no se está yendo suficientemente rápido como para que el mareo de la velocidad oculte la nausea de la suciedad. Eh…

Pues nada, esta es la situación. Existe un PaginableSortableVO que todos los VOs extienden y del que heredan desde y hasta.

Nuevos límites

Una búsqueda rápida por el código deja ver que el límite por defecto de 500 no es necesariamente el más frecuente. Porque, obviamente un determinado VO siempre puede sobre escribir ese valor con el suyo propio.

En muchos casos tiene cierto sentido encontrar que muchos VOs tienen como hasta un valor de 1 porque solo deben sacar un único resultado. Sí, sí, ya sé. Si el resultado debe ser único, qué necesidad hay de coger sólo el primer resultado. Si lo hacemos y en realidad no es único lo que estaremos haciendo es ignorar algo que probablemente es un problema de inconsistencia de los datos, ¿no? Puede ser, pero precisamente para eso está el Arquitecto, para ignorar problemas. Es más, toda la gestión del proyecto se basa ya en ignorar problemas. Sigamos, sigamos.

Hay otros casos en que se amplía el máximo a, por ejemplo, 999, a 1000 o a 9999 resultados. Hay también algunos donde se establece el máximo en 50000, 99999, 99999999, 99997, 256489 o 2343445 resultados. No pongo estos números por poner “unos números cualquiera”. Estos son números que están puestos en la aplicación y que he encontrado en una búsqueda por el código. No sé qué significan, pero aparecen múltiples veces. También hay muchos casos -unos cuantos cientos- en los que se pone el hasta a un valor de Integer.MAX_VALUE.

También hay algunos casos en que se limita más, a 150, 50, o incluso a cantidades sospechosamente concretas como 12, 2, 3. Hay un único caso que he preferido no investigar más:

someVO.setHasta(0);

Un día que me aburra más igual lo miro.

ArticuloVO

ArticuloVO es un VO bastante popular. Quiero decir, obviamente Artículo es algo fundamental en una aplicación cuyo objetivo principal es gestionar artículos.

ArticuloVO hasta este año tenía el límite por defecto de 500. En Mayo, Mathilda, del equipo de analistas, abre una tarea solicitando que se amplíe el límite para ArticuloVO hasta 6000. Algo grande este año es que se actualizaba la versión de Oracle y a la vez se ha hecho un cambio amplio en una parte del modelo de datos, así que se pensó que se podía ampliar a 6000 sin problema.

La tarea se encarga de realizarla Stephanie, del equipo de programación. Stephanie lleva muchos años en el proyecto. Stephanie realiza el cambio así:

Primero, en el VO añade esto:

private int hasta6000 = 6000;

public int getHasta6000() {
    return hasta6000;
}

public void setHasta6000(int hasta6000) {
    this.hasta6000 = hasta6000;
}

Luego, en el DAO modifica esta línea así:

mapa.put("P_FINAL", articuloVO.getHasta6000());

Sube el cambio al repositorio, se despliega en los entornos de preproducción y todos felices.

En Septiembre, Mathilda crea una nueva tarea. Han pensado que se puede subir el límite a 10000. No sé quién lo ha pensado pero alguien lo ha decidido así. En fin, tarea que va a Stephanie de nuevo porque, obviamente ya se sabe cómo va porque hizo el cambio anterior hace unos meses.

Stephanie, hace el cambio: Sin borrar nada, añade a ArticuloVO:

private int hasta6000 = 6000;
private int hasta10000 = 10000;

public int getHasta6000() {
    return hasta6000;
}

public void setHasta6000(int hasta6000) {
    this.hasta6000 = hasta6000;
}

public int getHasta10000() {
    return hasta10000;
}

public void setHasta10000(int hasta10000) {
    this.hasta10000 = hasta10000;
}

Luego, en el DAO cambia:

mapa.put("P_FINAL", articuloVO.getHasta10000());

Lo sube al repositorio, despliegue, todos felices.

Addendum

Oh, no había mirado una parte del cambio de Stephanie porque es en el proceso almacenado y suelo evitar meterme por ahí si no hay necesidad. Pero esta mañana lo he mirado porque estaba Stephanie hablando de ello con alguien y he temido que me faltara algún detalle importante.

Después de todo, de hacer el cambio como lo ha hecho, para conseguir pasar ese 6000 y luego 10000 de límite al PL, en esa verificación que decía que se hace primero para no devolver demasiados resultados… en el PL el límite de 10000 va puesto a fuego en el código.

IF F_Max_Registros(..., 10000) THEN -- ampliamos a 6000
                                    -- ampliamos a 10000

1)
oh, sorpresa
2)
Lo de “sortable” porque lo mismo que pasaba con la paginación pasaba con la ordenación, que también se pasó a hacer en cliente. Lo de la ordenación tiene también su historia pero por ahora lo voy a dejar pasar.