Cómo modificar registros en aplicaciones CRUD
Por Jacobo Tarrío
18 de abril de 2020

Todo el mundo piensa que escribir una aplicación CRUD es la tarea de programación más fácil del mundo, pero muchísima gente lo hace mal.

Una aplicación CRUD es la típica que trata con datos en forma de registros. El nombre CRUD está formado por las iniciales de “Create, Read, Update, Delete”, que son las cuatro operaciones que se pueden realizar sobre un registro: crearlo, leerlo, modificarlo y borrarlo.

Todos los programadores hemos escrito aplicaciones de este tipo. Llevamos décadas haciéndolas e, incluso hoy en día, muchas de las aplicaciones modernas son aplicaciones CRUD encubiertas. ¿Un blog? CRUD. ¿Una consola para contenedores Docker? Si te permite crearlos y gestionarlos, CRUD. ¿Facebook? Otro CRUD.

Uno podría pensar que, si las aplicaciones CRUD son tan normales y tanto tiempo llevamos haciéndolas, a estas alturas sabríamos escribir una con los ojos cerrados, ¿no? Y, sin embargo, hay varios errores que casi todo el mundo comete cuando escribe sus aplicaciones CRUD. El artículo de hoy es sobre uno de esos errores: no implementamos la operación de modificación de registros correctamente.

Id a buscar tutoriales de creación de aplicaciones CRUD en Internet. Puede ser un tutorial de Ruby on Rails o un tutorial de Hibernate o lo que queráis. En algún punto del tutorial dirán algo parecido a esto:

Como la creación y modificación de un registro son operaciones tan parecidas, escribiremos una sola función, llamada insertOrUpdate, para ambas. Esta función recibe un registro. Si éste no tiene un identificador, la función realizará una operación INSERT en la base de datos. Si, en cambio, el registro tiene un identificador, la función realizará una operación UPDATE.

Estaría dispuesto a apostar que el 99% de todos los tutoriales de aplicaciones CRUD en Internet tiene algo parecido, y todos ellos están equivocados. El problema es que crear y modificar un registro no son operaciones parecidas. La operación de modificación tiene una particularidad que la hace mucho más compleja que la operación de creación de un registro: dos usuarios pueden intentar modificar el mismo registro al mismo tiempo.

Modificación simultánea de un registro

Imaginad una pastelería con una página web en la que la gente puede realizar pedidos y modificarlos después. Juan y Ana han encargado una tarta de nata con glaseado de chocolate y el texto “feliz cumpleaños”, pero ahora quieren cambiarla por una tarta de vainilla con glaseado de fresa y el texto “por muchos años”. Como no se coordinan bien, los dos se ponen a hacer el cambio. Aún peor: además de descoordinados, son olvidadizos, y mientras Juan se olvida de cambiar el texto, Ana se olvida de cambiar el sabor.

Juan y Ana se conectan al mismo tiempo desde sus respectivos teléfonos móviles, pulsan el botón “modificar” y ven el formulario con el pedido actual.

sabor= “nata”, glaseado= “chocolate”, texto= “feliz cumpleaños”

Ahora hacen sus cambios y pulsan el botón “submit”. ¿Cuál es el resultado? ¿Cómo es su pedido ahora? Dependiendo de quién haya sido el último en enviar sus cambios podrían tener una tarta con el sabor correcto pero el texto equivocado (sabor= “vainilla”, glaseado= “fresa”, texto= “feliz cumpleaños”) o una tarta con la inscripción correcta pero el sabor incorrecto (sabor= “nata”, glaseado= “fresa”, texto= “por muchos años”). Uno de ellos habrá pisado los cambios del otro sin darse cuenta, y cuando por fin reciban la tarta se llevarán una sorpresa desagradable.

Si el código de modificación de registros de nuestra aplicación CRUD consiste, como en tantas aplicaciones, en leer el registro, ponerlo en el formulario, recibir el nuevo valor del formulario y sobreescribir el registro con el nuevo valor, esto mismo es lo que nos va a ocurrir cuando dos usuarios quieran modificar el registro al mismo tiempo. Tenemos que escribir nuestro código de modificación de registros de manera que pueda, al menos, detectar este tipo de situaciones para no perder o corromper datos. El resto de este artículo detalla dos estrategias para conseguirlo.

Detección de modificaciones simultáneas

La primera estrategia consiste en añadir un nuevo campo al registro. Este campo contiene un “identificador de versión” cuyo valor debe cambiar cada vez que haya una modificación en el registro. Puede ser un contador, un número aleatorio o cualquier otra cosa, siempre y cuando, cada vez que se modifique el registro, también se cambie el valor de este campo a un valor nuevo.

Cuando el usuario pulsa “submit” para grabar los cambios, el servidor sólo tiene que comparar el número de versión del formulario con el número de versión del registro en la base de datos. Si coinciden, todo está bien y se pueden grabar los datos. Si no coinciden, eso indica que alguien modificó el registro en algún momento entre que se abrió el formulario de edición y se pulsó “submit”, así que el usuario debería recibir un mensaje de error avisando de la circunstancia.

Ni que decir tiene que todo el código que lee el registro actual, compara el identificador de versión y graba el nuevo registro debe ejecutarse en una transacción de la base de datos para asegurarnos de que todas las operaciones son atómicas.

En pseudocódigo, esto sería así:

form_id := POSTDATA["id"]
form_version := POSTDATA["version"]
form_pedido := POSTDATA["pedido"]

# Abrir una transacción.
transacción := base_datos.ComenzarTransacción()
# Obtener el identificador de versión del registro.
current_version := SELECT version FROM pedidos WHERE id = form_id
if current_version == form_version
  # Si coincide con el recibido, actualizar el registro.
  UPDATE pedidos SET version = version + 1, sabor = form_pedido.sabor, etc
  resultado := transacción.Commit()
  return resultado
else
  # Si no, indicar que hubo un problema.
  transacción.Cancel()
  return fail

Cuando Juan y Ana abren el formulario de modificación, el identificador de versión tiene el valor 1, por lo que éste será el identificador de versión que enviarán al pulsar “submit” en sus formularios.

versión= 1, sabor= “nata”, glaseado= “chocolate”, texto= “feliz cumpleaños”

A continuación, Juan envía su cambio y el servidor compara el identificador de versión enviado por Juan con el identificador de versión del pedido; ambos valen 1, por lo que el servidor acepta el cambio y actualiza el registro, con lo que el número de versión cambia.

versión= 2, sabor= “vainilla”, glaseado= “fresa”, texto= “feliz cumpleaños”

Ahora Ana envía su cambio y el servidor compara los identificadores de versión. El enviado por Ana vale 1 pero el pedido tiene el identificador de versión 2, por lo que el servidor rechaza el cambio y le muestra un mensaje de error a Ana.

Esta estrategia funciona bien para detectar cambios simultáneos, pero le falta sutileza. Los cambios que Juan y Ana quieren hacer son compatibles (ambos quieren poner el mismo glaseado y, aparte de eso, modifican distintos elementos del pedido) pero el servidor no toma nada de esto en cuenta, así que Ana recibe un mensaje de error. Es posible darle un poco más de inteligencia al servidor usando una estrategia distinta.

Detección de cambios individuales

Para aplicar esta estrategia no es necesario modificar el registro de ninguna manera. Lo que tenemos que cambiar es el formulario de edición para que cuando el usuario pulse “submit” le envíe al servidor tanto el valor modificado del registro como el valor original que recibió al abrir el formulario.

El servidor recibe ambos registros (original y modificado) y los compara campo a campo para elaborar una lista de cambios. Después lee el valor actual del registro y, por cada cambio, comprueba si ese cambio se puede hacer en ese registro. Si todos los cambios son factibles, se aplican; si hay algún cambio que no se pueda hacer, el usuario recibe un mensaje de error.

En pseudocódigo, esta estrategia tiene un aspecto similar a este:

form_id := POSTDATA["id"]
form_original := POSTDATA["original"]
form_nuevo := POSTDATA["nuevo"]

# Crear una lista de cambios.
cambios := comparar(form_original, form_nuevo)
if len(cambios) == 0
  # No hay cambios, así que no hace falta hacer nada más.
  return ok

# Abrir una transacción y obtener el registro actual.
transacción := base_datos.ComenzarTransacción()
pedido_actual := SELECT * FROM pedidos WHERE id = form_id
for cambio in cambios
  # Comprobar si cada cambio es compatible con el registro actual.
  valor_actual := cambio.valor(pedido_actual)
  valor_original := cambio.original
  valor_nuevo := cambio.nuevo
  if valor_actual != valor_original and valor_actual != valor_nuevo
    # Si no lo es, cancelar e indicar que hubo un problema.
    transacción.Cancel()
    return fail
# Aplicar los cambios al registro.
UPDATE pedidos SET cambios
resultado := transacción.Commit()
return resultado

Volvamos al pedido de la tarta de Juan y Ana. Originalmente era un pedido de una tarta de nata con glaseado de chocolate y “feliz cumpleaños” escrito. En la base de datos, el registro tendría el siguiente valor:

sabor= “nata”, glaseado= “chocolate”, texto= “feliz cumpleaños”

Juan y Ana abren el editor del pedido al mismo tiempo y Juan envía sus cambios primero. El servidor recibe la siguiente información:

El servidor compara ambos registros y crea la siguiente lista de cambios:

  1. sabor: “nata” → “vainilla”
  2. glaseado: “chocolate” → “fresa”

El servidor lee el registro actual y comprueba si se pueden aplicar los cambios:

  1. sabor: “nata” → “vainilla”; actual= “nata”
  2. glaseado: “chocolate” → “fresa”; actual= “chocolate”

Como los valores actuales coinciden con los valores originales, el servidor aplica esos cambios al registro, con lo que adquiere el valor:

sabor= “vainilla”, glaseado= “fresa”, texto= “feliz cumpleaños”

Ahora Ana envía sus cambios y el servidor recibe esta información:

El servidor compara los registros y crea la lista de cambios:

  1. glaseado: “chocolate” → “fresa”
  2. texto: “feliz cumpleaños” → “por muchos años”

Después de crear la lista, el servidor lee el registro actual y comprueba si se pueden aplicar los cambios:

  1. glaseado: “chocolate” → “fresa”; actual= “fresa”
  2. texto: “feliz cumpleaños” → “por muchos años”; actual= “feliz cumpleaños”

Ambos cambios son compatibles con el registro actual. Para el texto, el valor original coincide con el valor actual, con lo que se puede realizar el cambio. Para el glaseado, el valor modificado coincide con el valor actual, por lo que el cambio no tiene efecto. Como ambos cambios son compatibles, el servidor actualiza el registro, que, finalmente, se queda en:

sabor= “vainilla”, glaseado= “fresa”, texto= “por muchos años”

¡Albricias! A pesar de que Juan y Ana estén tan descoordinados y sean tan olvidadizos, al final han conseguido cambiar el pedido de la forma que querían.

Para ver qué ocurre si los cambios no son compatibles, imaginad por un segundo que Ana quería cambiar el glaseado a pistacho en lugar de fresa. Después de generar la lista de cambios y leer el registro actual, el servidor tendría lo siguiente:

  1. glaseado: “chocolate” → “pistacho”; actual= “fresa”
  2. texto: “feliz cumpleaños” → “por muchos años”; actual= “feliz cumpleaños”

Como, en el glaseado, ni el valor original ni el modificado coinciden con el valor actual, este cambio no es compatible, por lo que Ana debería recibir un mensaje de error.

Más refinamientos

Si el valor actual de un campo no coincide con el valor original o modificado, no está todo perdido necesariamente. Dependiendo del contenido del campo, podemos añadir código para combinar los cambios automáticamente y no tener que rechazarlos tan a menudo.

Por ejemplo, si el campo contiene un texto largo, podemos utilizar un algoritmo de “three way merge". Este algoritmo detecta los cambios entre el texto original y el texto modificado y luego los aplica sobre el texto actual. Wikipedia hace esto, igual que la mayoría de los sistemas de control de versiones.

Si el campo contiene una lista de elementos, podemos hacer algo parecido: extraer una lista de adiciones y eliminaciones entre la lista original y modificada, y aplicarla sobre la lista actual.

Conclusiones

La creación y modificación de un registro no son dos casos de la misma operación, sino dos operaciones separadas y, a la hora de escribir una aplicación CRUD, hay que darle a la operación de modificación el respeto que se merece.

Es importante recordar que dos o más usuarios podrían intentar modificar el mismo registro al mismo tiempo y nuestra aplicación tiene que ser capaz de, como mínimo, detectar esta situación y mostrarle un mensaje de error a uno de los usuarios. Si no lo hacemos, podríamos perder datos o tener datos inconsistentes sin darnos cuenta.

Es fácil añadir código para detectar cambios simultáneos. Si tenemos muchos usuarios o los usuarios tienden a modificar los mismos registros muy a menudo, esto podría no ser suficiente. Los usuarios estarían pisándose los unos a los otros todo el tiempo, intentando modificar el mismo registro una y otra vez hasta que, por fin, consigan ser los primeros en enviar sus cambios.

Para solucionarlo, podemos tener estrategias más complejas para detectar e integrar cambios en un registro, ya sea campo a campo o incluso con granularidad más fina. De esta manera, incluso si hay varios usuarios haciendo cambios en el mismo registro, podríamos acomodarlos a todos si sus cambios no se solapan demasiado.


¿Qué os ha parecido este artículo? ¿Os gustaría que explorara más este tema? ¿Tenéis preguntas, comentarios, opiniones, sugerencias? Escribidme a mi nombre arroba mi apellido punto org.

Otros artículos sobre “programación”.
Índice.
Salvo indicación en contrario, esta página y su contenido son Copyright © Jacobo Tarrío Barreiro. Todos los Derechos Reservados. Información sobre tratamiento de datos y condiciones de uso.