Necesito mantener un número de revisión único (por fila) en una tabla document_revisions, donde el número de revisión está limitado a un documento, por lo que no es exclusivo de toda la tabla, solo del documento relacionado.
Inicialmente se me ocurrió algo como:
current_rev = SELECT MAX(rev) FROM document_revisions WHERE document_id = 123;
INSERT INTO document_revisions(rev) VALUES(current_rev + 1);
¡Pero hay una condición de carrera!
Estoy tratando de resolverlo pg_advisory_lock
, pero la documentación es un poco escasa y no lo entiendo completamente, y no quiero bloquear algo por error.
¿Es aceptable lo siguiente, o lo estoy haciendo mal, o hay una mejor solución?
SELECT pg_advisory_lock(123);
current_rev = SELECT MAX(rev) FROM document_revisions WHERE document_id = 123;
INSERT INTO document_revisions(rev) VALUES(current_rev + 1);
SELECT pg_advisory_unlock(123);
¿No debería bloquear la fila de documentos (clave1) para una operación dada (clave2)? Entonces esa sería la solución adecuada:
SELECT pg_advisory_lock(id, 1) FROM documents WHERE id = 123;
current_rev = SELECT MAX(rev) FROM document_revisions WHERE document_id = 123;
INSERT INTO document_revisions(rev) VALUES(current_rev + 1);
SELECT pg_advisory_unlock(id, 1) FROM documents WHERE id = 123;
¿Tal vez no estoy acostumbrado a PostgreSQL y una SERIAL puede tener un alcance, o tal vez una secuencia y funcionaría nextval()
mejor?
fuente
Respuestas:
Suponiendo que almacene todas las revisiones del documento en una tabla, un enfoque sería no almacenar el número de revisión sino calcularlo en función del número de revisiones almacenadas en la tabla.
Es, esencialmente, un valor derivado , no algo que necesita almacenar.
Se puede usar una función de ventana para calcular el número de revisión, algo así como
y necesitará una columna similar
change_date
a la de realizar un seguimiento del orden de las revisiones.Por otro lado, si solo tiene
revision
como propiedad del documento e indica "cuántas veces ha cambiado el documento", entonces optaría por el enfoque de bloqueo optimista, algo como:Si esto actualiza 0 filas, ha habido una actualización intermedia y debe informar al usuario de esto.
En general, trate de mantener su solución lo más simple posible. En este caso por
update
declaración en lugar de unaselect
seguida de unainsert
oupdate
fuente
SEQUENCE garantiza que será único, y su caso de uso parece aplicable si su número de documentos no es demasiado alto (de lo contrario, tiene muchas secuencias para administrar). Use la cláusula RETURNING para obtener el valor generado por la secuencia. Por ejemplo, usando 'A36' como document_id:
La gestión de las secuencias deberá manejarse con cuidado. Tal vez podría mantener una tabla separada que contenga los nombres de los documentos y la secuencia asociada con eso
document_id
para hacer referencia al insertar / actualizar ladocument_revisions
tabla.fuente
Esto a menudo se resuelve con un bloqueo optimista:
Si la actualización devuelve 0 filas actualizadas, ha perdido su actualización porque alguien más ya la actualizó.
fuente
(Llegué a esta pregunta cuando intenté volver a descubrir un artículo sobre este tema. Ahora que lo he encontrado, lo estoy publicando aquí en caso de que otros busquen una opción alternativa a la respuesta elegida actualmente, haciendo una ventana con
row_number()
)Tengo este mismo caso de uso. Para cada registro insertado en un proyecto específico en nuestro SaaS, necesitamos un número incremental único que se pueda generar frente a mensajes concurrentes
INSERT
y que, idealmente, no tenga espacios.Este artículo describe una buena solución , que resumiré aquí por facilidad y posteridad.
document_id
ycounter
.counter
seráDEFAULT 0
Alternativamente, si ya tiene unadocument
entidad que agrupa a todas las versiones, unacounter
podrían añadirse allí.BEFORE INSERT
disparador a ladocument_versions
tabla que incremente atómicamente el contador (UPDATE document_revision_counters SET counter = counter + 1 WHERE document_id = ? RETURNING counter
) y luego se establezcaNEW.version
en ese valor de contador.Alternativamente, es posible que pueda usar un CTE para hacer esto en la capa de aplicación (aunque prefiero que sea un desencadenante por razones de coherencia):
Esto es similar en principio a cómo intentaba resolverlo inicialmente, excepto que al modificar una fila de contador en una sola declaración, bloquea las lecturas del valor obsoleto hasta que
INSERT
se confirma.Aquí hay una transcripción que
psql
muestra esto en acción:Como puede ver, debe tener cuidado con cómo
INSERT
sucede, de ahí la versión de activación, que se ve así:Eso hace que sea
INSERT
mucho más sencillo y la integridad de los datos más robusta frente aINSERT
los que se originan en fuentes arbitrarias:fuente