(Przyszedłem do tego pytania, próbując ponownie odkryć artykuł na ten temat. Teraz, gdy go znalazłem, zamieszczam go tutaj, na wypadek, gdyby inni szukali alternatywy dla obecnie wybranej odpowiedzi - okienko z row_number()
)
Mam ten sam przypadek użycia. Dla każdego rekordu wstawionego do konkretnego projektu w naszym SaaS potrzebujemy unikalnej, rosnącej liczby, która może być wygenerowana w obliczu równoczesnych INSERT
s i jest idealnie bez przerwy.
W tym artykule opisano fajne rozwiązanie , które streszczę tutaj dla ułatwienia i potomności.
- Mają osobną tabelę, która działa jak licznik, aby podać następną wartość. Będzie miał dwie kolumny
document_id
i counter
. counter
będzie DEFAULT 0
Alternatywnie, jeśli masz już document
encję, która grupuje wszystkie wersje, counter
można tam dodać a.
- Dodaj
BEFORE INSERT
wyzwalacz do document_versions
tabeli, która atomowo zwiększa licznik ( UPDATE document_revision_counters SET counter = counter + 1 WHERE document_id = ? RETURNING counter
), a następnie ustawia NEW.version
tę wartość licznika.
Alternatywnie możesz użyć CTE, aby to zrobić w warstwie aplikacji (chociaż wolę, aby był wyzwalaczem ze względu na spójność):
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";
Zasadniczo jest to podobne do tego, w jaki sposób próbowałeś go rozwiązać na początku, z tą różnicą, że modyfikując wiersz licznika w pojedynczej instrukcji, blokuje odczyty nieaktualnej wartości, aż do zatwierdzenia INSERT
.
Oto zapis psql
pokazujący to w akcji:
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)
Jak widać, musisz uważać na INSERT
to, co się dzieje, stąd wersja wyzwalacza, która wygląda następująco:
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();
To sprawia, że INSERT
s jest znacznie prostszy, a integralność danych bardziej niezawodna w porównaniu z danymi INSERT
pochodzącymi z dowolnych źródeł:
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)