(Я пришел к этому вопросу, когда пытался заново открыть статью на эту тему. Теперь, когда я нашел ее, я публикую ее здесь на тот случай, если другие будут искать альтернативный вариант для выбранного в данный момент ответа - окно с row_number()
)
У меня такой же вариант использования. Для каждой записи, вставленной в конкретный проект в нашем SaaS, нам нужен уникальный, увеличивающийся номер, который может быть сгенерирован перед лицом одновременных операций INSERT
и в идеале не имеет пробелов.
В этой статье описывается хорошее решение , которое я приведу здесь для простоты и потомства.
- Имейте отдельную таблицу, которая действует как счетчик для предоставления следующего значения. У него будет две колонки,
document_id
и counter
. counter
будет DEFAULT 0
вариант, если у вас уже есть document
объект , который группирует все версии, counter
могут быть там добавлены.
- Добавьте
BEFORE INSERT
к document_versions
таблице триггер, который атомарно увеличивает счетчик ( UPDATE document_revision_counters SET counter = counter + 1 WHERE document_id = ? RETURNING counter
), а затем устанавливает NEW.version
значение этого счетчика.
В качестве альтернативы вы можете использовать CTE для этого на прикладном уровне (хотя я предпочитаю, чтобы он был триггером для согласованности):
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";
В принципе это похоже на то, как вы пытались решить его изначально, за исключением того, что, изменяя строку счетчика в одном операторе, он блокирует чтение устаревшего значения до тех пор, пока не INSERT
будет зафиксировано.
Вот расшифровка стенограммы, psql
показывающая это в действии:
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)
Как вы можете видеть, вы должны быть осторожны INSERT
с тем, как это происходит, отсюда и версия триггера, которая выглядит так:
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();
Это делает INSERT
s намного более прямым и целостность данных более надежной перед лицом INSERT
s, происходящим из произвольных источников:
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)