SQL Server en la práctica

Evitar cambios perdidos con rowversion en SQL Server

Protege los formularios con rowversion, detecta actualizaciones concurrentes y resuelve conflictos sin sobrescribir silenciosamente el trabajo de otros.

Dos personas abren la misma ficha de producto. La primera corrige una indicación del precio y guarda. La segunda cambia la puntuación de una copia anterior y envía el formulario completo. Ambas solicitudes terminan correctamente, pero la segunda elimina la primera corrección. Una transacción alrededor de cada UPDATE no evita esta pérdida: la lectura antigua ocurrió antes.

Incluir la versión leída en la escritura

La solicitud debe indicar qué versión vio el usuario. Una columna rowversion proporciona un token binario de ocho bytes generado por la base de datos. Devuélvelo con los campos editables y exígelo al guardar. La clave primaria y el token esperado deben aparecer en el predicado del mismo UPDATE.

No basta con consultar el token, compararlo en la aplicación y ejecutar después una actualización incondicional. Otra sesión podría escribir entre ambas operaciones. El ejemplo simula dos editores en una conexión: ambos guardan el token inicial, pero después del primer cambio la condición del segundo ya no coincide.

CREATE TABLE #Draft
(
    DraftId int NOT NULL PRIMARY KEY,
    Title nvarchar(100) NOT NULL,
    Revision rowversion NOT NULL
);
INSERT #Draft (DraftId, Title) VALUES (1, N'Initial title');

DECLARE @SeenByA binary(8), @SeenByB binary(8);
SELECT @SeenByA = Revision, @SeenByB = Revision
FROM #Draft WHERE DraftId = 1;

UPDATE #Draft SET Title = N'Editor A'
OUTPUT inserted.DraftId, inserted.Title, inserted.Revision
WHERE DraftId = 1 AND Revision = @SeenByA;

UPDATE #Draft SET Title = N'Editor B'
WHERE DraftId = 1 AND Revision = @SeenByB;
DECLARE @Changed int = @@ROWCOUNT;

SELECT @Changed AS RowsChanged;
SELECT DraftId, Title, Revision FROM #Draft;
DROP TABLE #Draft;

El segundo UPDATE afecta a cero filas y el título sigue siendo Editor A. El primer OUTPUT devuelve el token nuevo para la respuesta correcta. Si utilizas @@ROWCOUNT, captura su valor inmediatamente, pues las instrucciones posteriores pueden reemplazarlo. La clave primaria garantiza que la solicitud afecte como máximo a una fila.

Trata el token como datos binarios opacos. En una API JSON puedes representarlo mediante Base64 o hexadecimal de longitud fija. Decodifica exactamente ocho bytes y enlaza un parámetro binario. No lo conviertas en un número JavaScript. El token no representa una fecha y el cliente no debe calcular el siguiente valor. Usa otra columna datetime2 para mostrar cuándo se modificó el registro.

Diseñar una respuesta útil al conflicto

Cero filas afectadas significa que la combinación solicitada de clave y versión no estaba disponible. Puede haber una edición concurrente, una eliminación o una restricción de acceso. El resultado del UPDATE no distingue estos casos. Aplica la autorización en el predicado o mediante una comprobación transaccional igualmente fiable, sin revelar registros restringidos mediante consultas de diagnóstico.

Para un usuario autorizado, conserva el texto enviado antes de cargar la versión actual. Cuando tenga sentido combinar cambios, presenta los valores originales, la propuesta y los valores vigentes. Recargar la pantalla y descartar el trabajo pendiente protege la base de datos, pero deja al usuario con otro problema.

No repitas automáticamente un formulario obsoleto utilizando el token más reciente. Eso convierte el protocolo en una sobrescritura silenciosa. Algunas operaciones tienen una intención más precisa: un contador puede incrementarse de forma atómica, sin reemplazar un total leído anteriormente. Elige expresamente ese contrato en lugar de tratar cualquier comando como el guardado de un documento entero.

Proteger toda la operación de negocio

El token protege una fila. Si una pantalla modifica la cabecera de un pedido y varias líneas, comprobar solamente la cabecera no detecta una edición independiente de una línea. Cada cambio relevante debe actualizar una versión compartida, o cada línea modificada debe comprobar su propio token dentro de una transacción. Si la operación exige éxito completo, un solo conflicto debe provocar la reversión de todos sus cambios.

Los desencadenadores necesitan una prueba de integración. OUTPUT devuelve valores anteriores a la ejecución de los desencadenadores AFTER. Si uno vuelve a modificar la misma fila, el token puede avanzar otra vez. En ese caso, lee el token final dentro de la transacción después del desencadenador. No confirmes el éxito antes del commit: recibir una fila mediante OUTPUT no demuestra que la escritura quedó confirmada.

Prueba dos guardados simultáneos, una eliminación seguida de un guardado y un reintento tras perder la respuesta de red. Registra los conflictos por separado de los errores del servidor. Una frecuencia alta puede indicar que la interfaz reemplaza demasiados campos o que un proceso de fondo actualiza filas sin necesidad. Es información útil para mejorar la granularidad de las operaciones, además de evitar pérdida de datos.

Referencias técnicas: Microsoft Learn: rowversion · Microsoft Learn: OUTPUT clause.

Pregunta sobre este artículo

¿Tiene alguna pregunta sobre este tema?

Cuéntenos qué está evaluando o dónde tiene dificultades. Le responderemos con una recomendación práctica.

Inquiries are not enabled in this preview.

Hacer una pregunta sobre este artículo