(Cheguei a essa pergunta ao tentar redescobrir um artigo sobre esse tópico. Agora que o encontrei, estou postando aqui caso outras pessoas procurem uma opção alternativa para a resposta atualmente escolhida - dando um row_number()
)
Eu tenho esse mesmo caso de uso. Para cada registro inserido em um projeto específico em nosso SaaS, precisamos de um número único e incremental que possa ser gerado em face de INSERT
s concorrentes e seja idealmente contínuo.
Este artigo descreve uma boa solução , que resumirei aqui para facilitar e posterizar.
- Tenha uma tabela separada que atue como contador para fornecer o próximo valor. Ele terá duas colunas
document_id
e counter
. counter
será DEFAULT 0
Alternativamente, se você já tiver uma document
entidade que agrupa todas as versões, um counter
poderia ser acrescentadas.
- Adicione um
BEFORE INSERT
gatilho à document_versions
tabela que incrementa atomicamente o contador ( UPDATE document_revision_counters SET counter = counter + 1 WHERE document_id = ? RETURNING counter
) e depois define NEW.version
esse valor do contador.
Como alternativa, você pode usar um CTE para fazer isso na camada de aplicativo (embora eu prefira que seja um gatilho por uma questão de consistência):
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";
Isso é semelhante em princípio a como você estava tentando resolvê-lo inicialmente, exceto que, modificando uma linha do contador em uma única instrução, ele bloqueia as leituras do valor obsoleto até que o mesmo INSERT
seja confirmado.
Aqui está uma transcrição psql
mostrando isso em ação:
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 você pode ver, você precisa ter cuidado com o que INSERT
acontece, daí a versão do acionador, que se parece com isso:
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();
Isso torna INSERT
s muito mais direto e a integridade dos dados mais robusta diante de INSERT
s originários de fontes arbitrárias:
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)