(Je suis venu à cette question en essayant de redécouvrir un article sur ce sujet. Maintenant que je l'ai trouvé, je le poste ici au cas où d'autres chercheraient une option alternative à la réponse actuellement choisie - fenêtrage avec row_number()
)
J'ai ce même cas d'utilisation. Pour chaque enregistrement inséré dans un projet spécifique dans notre SaaS, nous avons besoin d'un nombre incrémentiel unique qui peut être généré face à des INSERT
s concurrents et est idéalement sans espace.
Cet article décrit une belle solution , que je résumerai ici pour plus de facilité et de postérité.
- Avoir une table séparée qui fait office de compteur pour fournir la valeur suivante. Il aura deux colonnes
document_id
et counter
. counter
sera DEFAULT 0
Alternativement, si vous avez déjà une document
entité qui regroupe toutes les versions, un counter
pourrait y être ajouté.
- Ajoutez un
BEFORE INSERT
déclencheur à la document_versions
table qui incrémente atomiquement le compteur ( UPDATE document_revision_counters SET counter = counter + 1 WHERE document_id = ? RETURNING counter
), puis définit NEW.version
cette valeur de compteur.
Alternativement, vous pourriez être en mesure d'utiliser un CTE pour le faire au niveau de la couche application (bien que je préfère que ce soit un déclencheur pour des raisons de cohérence):
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";
Cela est similaire en principe à la façon dont vous tentiez de le résoudre initialement, sauf qu'en modifiant une ligne de compteur dans une seule instruction, il bloque les lectures de la valeur périmée jusqu'à ce que le INSERT
soit validé.
Voici une transcription psql
montrant cela en action:
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)
Comme vous pouvez le voir, vous devez faire attention à la façon dont INSERT
cela se produit, d'où la version de déclenchement, qui ressemble à ceci:
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();
Cela rend INSERT
s beaucoup plus simple et l'intégrité des données plus robuste face à INSERT
s provenant de sources arbitraires:
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)