(Sono arrivato a questa domanda quando ho cercato di riscoprire un articolo su questo argomento. Ora che l'ho trovato, lo sto pubblicando qui nel caso in cui altri siano alla ricerca di un'opzione alternativa alla risposta attualmente scelta - finestra con row_number()
)
Ho questo stesso caso d'uso. Per ogni record inserito in un progetto specifico nel nostro SaaS abbiamo bisogno di un numero univoco e crescente che può essere generato di fronte a messaggi simultanei INSERT
ed è idealmente gapless.
Questo articolo descrive una bella soluzione , che riassumerò qui per facilità e posterità.
- Avere una tabella separata che funge da contatore per fornire il valore successivo. Avrà due colonne
document_id
e counter
. counter
sarà DEFAULT 0
In alternativa, se si dispone già di document
un'entità che raggruppa tutte le versioni, è counter
possibile aggiungere a lì.
- Aggiungi un
BEFORE INSERT
trigger alla document_versions
tabella che incrementa atomicamente il contatore ( UPDATE document_revision_counters SET counter = counter + 1 WHERE document_id = ? RETURNING counter
) e quindi imposta il NEW.version
valore di quel contatore.
In alternativa, potresti essere in grado di utilizzare un CTE per farlo a livello di applicazione (anche se preferisco che sia un trigger per motivi di coerenza):
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";
Questo è simile in linea di principio a come stavi cercando di risolverlo inizialmente, tranne per il fatto che modificando una riga del contatore in una singola istruzione blocca le letture del valore non aggiornato fino a quando non INSERT
viene eseguito il commit.
Ecco una trascrizione da psql
mostrare questo in azione:
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)
Come puoi vedere, devi stare attento a come INSERT
succede, quindi alla versione trigger, che assomiglia a questa:
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();
Ciò rende INSERT
molto più semplice e l'integrità dei dati più solida rispetto a quella INSERT
proveniente da fonti arbitrarie:
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)