Document_revisionsテーブルに一意の(行ごとの)リビジョン番号を保持する必要があります。リビジョン番号はドキュメントにスコープが設定されているため、テーブル全体ではなく、関連するドキュメントに対してのみです。
私は最初に次のようなものを思いつきました:
_current_rev = SELECT MAX(rev) FROM document_revisions WHERE document_id = 123;
INSERT INTO document_revisions(rev) VALUES(current_rev + 1);
_
しかし、競合状態があります!
私は_pg_advisory_lock
_でそれを解決しようとしていますが、ドキュメントが少し不足していて、完全に理解していないため、誤って何かをロックしたくありません。
次は許容できますか、それとも間違っていますか、それともより良い解決策がありますか?
_SELECT pg_advisory_lock(123);
current_rev = SELECT MAX(rev) FROM document_revisions WHERE document_id = 123;
INSERT INTO document_revisions(rev) VALUES(current_rev + 1);
SELECT pg_advisory_unlock(123);
_
代わりに、特定の操作(key2)のドキュメント行(key1)をロックするべきではありませんか?だからそれは適切な解決策でしょう:
_SELECT pg_advisory_lock(id, 1) FROM documents WHERE id = 123;
current_rev = SELECT MAX(rev) FROM document_revisions WHERE document_id = 123;
INSERT INTO document_revisions(rev) VALUES(current_rev + 1);
SELECT pg_advisory_unlock(id, 1) FROM documents WHERE id = 123;
_
たぶん私はPostgreSQLに慣れておらず、SERIALのスコープを設定できますか、それともシーケンスとnextval()
のほうが適していますか?
ドキュメントのすべてのリビジョンをテーブルに保存すると仮定すると、アプローチはnotリビジョン番号を保存しますが、テーブルに保存されているリビジョンの数に基づいて計算します。
これは本質的に、派生値であり、保存する必要があるものではありません。
ウィンドウ関数を使用して、次のようなリビジョン番号を計算できます。
row_number() over (partition by document_id order by <change_date>)
リビジョンの順序を追跡するには、change_date
のような列が必要です。
一方、ドキュメントのプロパティとしてrevision
のみがあり、「ドキュメントが変更された回数」を示している場合、次のような楽観的ロックアプローチを採用します。
update documents
set revision = revision + 1
where document_id = <id> and revision = <old_revision>;
これが0行を更新する場合、中間更新があり、ユーザーにこれを通知する必要があります。
一般に、ソリューションをできるだけシンプルに保つようにしてください。この場合、
update
の後にselect
またはinsert
を続けたものではなく、単一のupdate
ステートメントを使用するSEQUENCEは一意であることが保証されており、ドキュメントの数が多すぎない場合(使用するシーケンスが多数ある場合)は、ユースケースが適切であるように見えます。シーケンスによって生成された値を取得するには、RETURNING句を使用します。たとえば、document_idとして「A36」を使用します。
シーケンスの管理は、注意して行う必要があります。 document_id
テーブルの挿入/更新時に参照するために、ドキュメント名とそのdocument_revisions
に関連付けられたシーケンスを含む別のテーブルを保持することができます。
CREATE SEQUENCE d_r_document_a36_seq;
INSERT INTO document_revisions (document_id, rev)
VALUES ('A36',nextval('d_r_document_a36_seq')) RETURNING rev;
これはしばしば楽観的ロックで解決されます:
SELECT version, x FROM foo;
version | foo
123 | ..
UPDATE foo SET x=?, version=124 WHERE version=123
更新によって0行の更新が返された場合、他の誰かがすでに行を更新しているため、更新を見逃しています。
(私はこのトピックに関する記事を再発見しようとしたときにこの質問に行きました。それを見つけたので、他の人が現在選択されている回答の代替オプションを追求している場合に備えて、ここに投稿します。 row_number()
)
これと同じ使用例があります。 SaaS=の特定のプロジェクトに挿入された各レコードについて、同時INSERT
sに直面して生成できる一意の増分番号が必要であり、理想的にギャップなし。
この記事では素敵なソリューションについて説明します 、簡単に、そして後世のためにここで要約します。
document_id
とcounter
の2つの列があります。 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
sがどのように発生するか、つまり次のようなトリガーバージョンに注意する必要があります。
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)