Por salirme un poco de javascript y PHP, ahora una de bases de datos.
Este artículo trata sobre como cambiar el orden de un elemento dentro de una tabla, manteniendo el orden relativo de todos los demás elementos y actualizando únicamente un campo. Como esta descripcción es un poco densa, vayamos por partes.
Por simplificar el ejemplo, digamos que estoy haciendo un sitio web relacionado con el mundo del motor, y como todo sitio web que se precie, tengo una base de datos. En esa base de datos hay dos tablas, una tabla marcas y otra tabla modelos. En la tabla marcas hay una fila por cada marca de coches (Toyota, Honda, Hyunday, Mercedes, Chrysler, BMW, Audi, …), y en la tabla de modelos de coches, pues hay una serie de modelos para cada tipo de coche. Por ejemplo, para Audi, estaría el modelo A6 2.0 TDIe , A5 2.0 TDI, A4, TT …, y la lista sigue hasta donde uno quiera y más para cada marca. Creo que con esto uno ya se hace una idea de las tablas que hay.
El tema es que en mi sitio web, yo quiero que cuando el usuario haga click en una marca, le salga una lista ordenada de los modelos disponibles para esa marca, pero no quiero un orden cualquiera, no quiero que salgan ni por nombre, ni por votos, ni por precio, quiero que por defecto salga por el orden específico que yo haya definido.
Pensareis, pues fácil, simplemente hay que añadir un campo orden dentro de la tabla modelos, o si os gusta tener las tablas normalizadas al máximo, pues creariais una tabla extra donde meteriais dos claves ajenas, una a modelos y otra a marcas y luego pondriais el campo orden.
Se haga de la forma A o de la forma B, el concepto final es que hay un campo que se llama orden, y que para lo que me sirve es para definir manualmente el orden en el que quiero que salgan los modelos en la página principal.
| Marca |
|---|
| Nombre |
| Descripción |
| Imagen |
| … |
| Modelo |
|---|
| Nombre |
| Descripción |
| Imagen |
| … |
| Orden |
| … |
Hasta aquí sólo es introducción al artículo, lo bueno viene ahora.
Imaginad que en marcas sólo está Audi (por decir una), y que sólo hay 4 modelos en la base de datos A3, A4, A5 y TT. A partir de aquí vamos a hacer como si las marcas no existieran (por simplificar aún más). Así que se nos queda una tabla de modelos como sigue:
| Nombre | Descripción | … | Orden | … |
|---|---|---|---|---|
| A3 | El A3 mola | … | 1 | … |
| A4 | El A4 mola más | … | 2 | … |
| A5 | El A5 mola mucho más | … | 3 | … |
| TT | Sin duda es el mejor | … | 4 | … |
Ahora, si yo en mi sitio web, hago un select, y ordeno por el campo orden, pues como es lógico, viendo esa tabla, primero saldrá el A3, luego el A4, luego el A5 y seguidamente el Audi TT.
El problema:
¿Cómo hago ahora, si quiero que el Audi TT salga el primero?
Se debe tener en cuenta, que yo sólo quiero mover el Audi TT a la primera posición, pero quiero que el A3, A4 y A5 no cambien su orden relativo. Y he dicho la primera posición por decir algo, pero también podría querer que el TT apareciera entre el A3 y el A4.
Solución 1: la fácil
Bueno, pues lo primero que a uno le viene a la cabeza (al menos a mi), es básicamente incrementar en 1 todos los campos cuyo orden sea mayor al que yo voy a poner para TT, y luego actualizar el orden del TT.
UPDATE modelo SET ORDER = ORDER + 1 WHERE ORDER >= 1; UPDATE modelo SET ORDER = 1 WHERE name = 'TT';
Básicamente arriba he utilizado 2 queries, uno de complejidad lineal que modifica una ristra de filas, y otro que sólo toca una tabla.
Como esta solución no me convencía demasiado, no era muy bonita, pues me puse a pensar y se me ocurrió otra.
Solución 2: la magia de los float
¿Y si en vez de un entero, se usa un número real?
Si el campo orden fuera real, entonces podría saber cual es el orden de los elementos inmediatamente anterior y posterior (puesto que los he cargado en el interfaz de usuario de mi web), y podría actualizar sólo el campo del TT sin tocar ningún otro elemento de la tabla.
Es decir, si tenemos la configuración inicial (A3, A4, A5, TT), y quiero que el TT esté entre A3 y A4, como A3 tiene el valor de orden igual a 1.0, y A4 tiene el valor de orden de 2.0, y quiero que TT esté enmedio, el tema quedará de la siguiente manera.
| Nombre | Descripción | … | Orden | … |
|---|---|---|---|---|
| A3 | El A3 mola | … | 1 | … |
| TT | Sin duda es el mejor | … | 1.5 | … |
| A4 | El A4 mola más | … | 2 | … |
| A5 | El A5 mola mucho más | … | 3 | … |
Básicamente lo que he hecho es calcular el valor intermedio entre A3 y A4, vease (1.0 + 2.0) / 2
Nótese que esto se puede hacer tantas veces como se quiera, que siempre habrá un orden entre los elementos y “jamás” habrá dos elementos con el mismo orden. Por ejemplo, si ahora quermos que el A4 esté entre el A3 y el TT, actualizaríamos el orden de A4 a (1.0 + 1.5)/2 = 1.25.
| Nombre | Descripción | … | Orden | … |
|---|---|---|---|---|
| A3 | El A3 mola | … | 1 | … |
| A4 | El A4 mola más | … | 1.25 | … |
| TT | Sin duda es el mejor | … | 1.5 | … |
| A5 | El A5 mola mucho más | … | 3 | … |
Como veis, el procedimiento está chupado, y sólo hay que tocar un campo.
Claro, ahora surgen varias preguntas. Me direis, que bonito, ¿y que pasa si quieres ponerlo en la primera posición? Fácil, si desde el principio se asume que el orden de los elementos que insertamos empieza en 1.0, entonces el elemento posterior será el que ya exista en la base de datos, y el elemento anterior será cero (0.0)
¿Y que pasa si ahora quieres insertar el A6? Pues pasa que si hay 4 elementos, el A6 habrá que insertarlo con el valor 5.0 (nunca habrá colisión si se hace como número de elementos + 1).
Y la pregunta estrella es: “oye, los números de punto flotante tenían un límite, ¿no? ¿que pasa si se desbordan?” Ah, amigo Sancho ¡Con la iglesia hemos topado!
Si os fijais, hacer que se desborde es tan fácil como ir moviendo todos los elementos siempre a la misma posición. Por ejemplo, si está el orden 1 y 2 inicialmente, y movemos el 3 a la segunda posición, ahora será 1, 1.5 y 2, luego si movemos el 2 a la segunda posición será 1, 1.25 y 1.5, si volvemos a mover el 1.5 a la segunda posición será 1, 1.125 y 1.25, y así sucesivamente hasta que el punto flotante diga hasta aquí (que puede ser mas o menos despues de unas 20 veces, según el número de bits que se usen para decimales).
Lo cual nos lleva a la solución 3
Solución 3: tapando agujeros
Solución 3.1: proceso en background
Una solución podría ser tener un proceso que una vez al día, a la semana o al mes, actualice todos los elementos de la tabla a números enteros respetando su orden actual. Aunque para eso casi mejor implementar la solución 1 que es más mantenible. Lo cual nos lleva a buscar otra alternativa.
Solución 3.2: actualización entera
Básicamente es igual que la solución 2 (no me repetiré), pero con una salvedad, y es que cuando se mueva un elemento a una posición, si no hay ningún número entero en esa posición se tratará de redondear al número entero hacia abajo (o hacia arriba, o ambos), para mitigar esa degradación de los flotantes que van teniendo cada vez más decimales.
Vease un ejemplo. Imaginemos que el tema se ha degradado un poco y parto de la siguiente configuración:
| Nombre | Descripción | … | Orden | … |
|---|---|---|---|---|
| A3 | El A3 mola | … | 1.125 | … |
| TT | Sin duda es el mejor | … | 1.0394 | … |
| A4 | El A4 mola más | … | 1.7628 | … |
| A5 | El A5 mola mucho más | … | 3.78 | … |
Si ahora muevo el A3, entre el A4 y el A5, de normal haría lo siguiente: (1.7628 + 3.78)/2 = 2.7714. Pero oh magia, si redondeo el 2.7714 hacia abajo me da 2, y resulta que el sigue estando entre 1.7628 y 3.78, por lo tanto en vez de actualizar a 2.7714 actualizaría a 2. De esta manera, si más tarde moviera el TT entre el A3 y el A5, en vez de hacer esta operación (2.7714 + 3.78) / 2, tendría que hacer esta (2 + 3.78) / 2, que obviamente tiene menos decimales.
Nótese que para esta solución símplemente hay que mirar si el número entero sigue estando entre el número anterior y el posterior (que son conocidos) y no es igual a ninguno de ambos.
Además, si se asume que movimiento de los elementos se hará de forma aleatoria, de esta manera es poco probable (aunque sigue siendo posible) que al final el punto flotante se desborde.
Por cierto, esta solución ahora mismo la he presentado con números reales, pero se pueden usar perfectamente números enteros, aunque siguiendo otro esquema.
Conclusión
Si quieres robustez, usa la solución 1 para mantener el orden de todos los elementos; si quieres actualizar un único elemento cada vez, porque es una operación que se va a realizar miles de veces por miles de personas al día lo recomendable es la solución 3.2, aunque tendría problemas si hay varias personas concurrentemente actualizando campos.
Finalmente, aunque en el artículo se han visto un par de soluciones (con sus parches), me encantaría saber si alguien conoce otras soluciones o sabe que problemas tienen en la práctica las que yo he puesto.
English
5 Comments on "Truco Manso: Actualizar el orden de una fila sin tocar el resto"