(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.
- Tenga una tabla separada que actúe como el contador para proporcionar el siguiente valor. Tendrá dos columnas,
document_id
y counter
. counter
será DEFAULT 0
Alternativamente, si ya tiene una document
entidad que agrupa a todas las versiones, una counter
podrían añadirse allí.
- Agregue un
BEFORE INSERT
disparador a la document_versions
tabla que incremente atómicamente el contador ( UPDATE document_revision_counters SET counter = counter + 1 WHERE document_id = ? RETURNING counter
) y luego se establezca NEW.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):
WITH version AS (
UPDATE document_revision_counters
SET counter = counter + 1
WHERE document_id = 1
RETURNING counter
)
INSERT
INTO document_revisions (document_id, rev, other_data)
SELECT 1, version.counter, 'some other data'
FROM "version";
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:
scratch=# CREATE TABLE document_revisions (document_id integer, rev integer, other_data text, PRIMARY KEY (document_id, rev));
CREATE TABLE
scratch=# CREATE TABLE document_revision_counters (document_id integer PRIMARY KEY, counter integer DEFAULT 0);
CREATE TABLE
scratch=# WITH version AS (
INSERT INTO document_revision_counters (document_id) VALUES (2)
ON CONFLICT (document_id)
DO UPDATE SET counter = document_revision_counters.counter + 1
RETURNING counter;
)
INSERT
INTO document_revisions (document_id, rev, other_data)
SELECT 2, version.counter, 'doc 1 v1'
FROM "version";
INSERT 0 1
scratch=# WITH version AS (
INSERT INTO document_revision_counters (document_id) VALUES (2)
ON CONFLICT (document_id)
DO UPDATE SET counter = document_revision_counters.counter + 1
RETURNING counter;
)
INSERT
INTO document_revisions (document_id, rev, other_data)
SELECT 2, version.counter, 'doc 1 v2'
FROM "version";
INSERT 0 1
scratch=# WITH version AS (
INSERT INTO document_revision_counters (document_id) VALUES (2)
ON CONFLICT (document_id)
DO UPDATE SET counter = document_revision_counters.counter + 1
RETURNING counter;
)
INSERT
INTO document_revisions (document_id, rev, other_data)
SELECT 2, version.counter, 'doc 2 v1'
FROM "version";
INSERT 0 1
scratch=# SELECT * FROM document_revisions;
document_id | rev | other_data
-------------+-----+------------
2 | 1 | doc 1 v1
2 | 2 | doc 1 v2
2 | 1 | doc 2 v1
(3 rows)
Como puede ver, debe tener cuidado con cómo INSERT
sucede, de ahí la versión de activación, que se ve así:
CREATE OR REPLACE FUNCTION set_doc_revision()
RETURNS TRIGGER AS $$ BEGIN
WITH version AS (
INSERT INTO document_revision_counters (document_id, counter) VALUES (NEW.document_id, 1)
ON CONFLICT (document_id)
DO UPDATE SET counter = document_revision_counters.counter + 1
RETURNING counter
)
SELECT INTO NEW.rev counter FROM version; RETURN NEW; END;
$$ LANGUAGE 'plpgsql';
CREATE TRIGGER set_doc_revision BEFORE INSERT ON document_revisions
FOR EACH ROW EXECUTE PROCEDURE set_doc_revision();
Eso hace que sea INSERT
mucho más sencillo y la integridad de los datos más robusta frente a INSERT
los que se originan en fuentes arbitrarias:
scratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (1, 'baz');
INSERT 0 1
scratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (1, 'foo');
INSERT 0 1
scratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (1, 'bar');
INSERT 0 1
scratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (42, 'meaning of life');
INSERT 0 1
scratch=# SELECT * FROM document_revisions;
document_id | rev | other_data
-------------+-----+-----------------
1 | 1 | baz
1 | 2 | foo
1 | 3 | bar
42 | 1 | meaning of life
(4 rows)