report_subscriber
でフラグを使用するのではなく それ自体、保留中の変更の別のキューを使用したほうがよいと思います。これにはいくつかの利点があります:
- トリガーの再帰なし
- 内部では、
UPDATE
ただのDELETE
+再-INSERT
、したがって、キューに挿入する方が、フラグを反転するよりも実際に安価になります - 個別の
report_id
をキューに入れるだけでよいため、おそらくかなり安価です。 sreport_subscriber
全体を複製するのではなく レコードを記録し、一時テーブルで実行できるため、ストレージは連続しており、ディスクに同期する必要はありません。 - キューは現在のトランザクションに対してローカルであるため、フラグを反転するときに競合状態を心配する必要はありません(実装では、
UPDATE report_subscriber
の影響を受けるレコードSELECT
で取得したレコードと必ずしも同じではありません ...)
したがって、キューテーブルを初期化します。
CREATE FUNCTION create_queue_table() RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
CREATE TEMP TABLE pending_subscriber_changes(report_id INT UNIQUE) ON COMMIT DROP;
RETURN NULL;
END
$$;
CREATE TRIGGER create_queue_table_if_not_exists
BEFORE INSERT OR UPDATE OF report_id, subscriber_name OR DELETE
ON report_subscriber
FOR EACH STATEMENT
WHEN (to_regclass('pending_subscriber_changes') IS NULL)
EXECUTE PROCEDURE create_queue_table();
...変更が到着したらキューに入れ、すでにキューに入れられているものはすべて無視します:
CREATE FUNCTION queue_subscriber_change() RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
IF TG_OP IN ('DELETE', 'UPDATE') THEN
INSERT INTO pending_subscriber_changes (report_id) VALUES (old.report_id)
ON CONFLICT DO NOTHING;
END IF;
IF TG_OP IN ('INSERT', 'UPDATE') THEN
INSERT INTO pending_subscriber_changes (report_id) VALUES (new.report_id)
ON CONFLICT DO NOTHING;
END IF;
RETURN NULL;
END
$$;
CREATE TRIGGER queue_subscriber_change
AFTER INSERT OR UPDATE OF report_id, subscriber_name OR DELETE
ON report_subscriber
FOR EACH ROW
EXECUTE PROCEDURE queue_subscriber_change();
...そしてステートメントの最後でキューを処理します:
CREATE FUNCTION process_pending_changes() RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
UPDATE report
SET report_subscribers = ARRAY(
SELECT DISTINCT subscriber_name
FROM report_subscriber s
WHERE s.report_id = report.report_id
ORDER BY subscriber_name
)
FROM pending_subscriber_changes c
WHERE report.report_id = c.report_id;
DROP TABLE pending_subscriber_changes;
RETURN NULL;
END
$$;
CREATE TRIGGER process_pending_changes
AFTER INSERT OR UPDATE OF report_id, subscriber_name OR DELETE
ON report_subscriber
FOR EACH STATEMENT
EXECUTE PROCEDURE process_pending_changes();
これにはわずかな問題があります:UPDATE
更新順序についての保証はありません。これは、これら2つのステートメントが同時に実行された場合:
INSERT INTO report_subscriber (report_id, subscriber_name) VALUES (1, 'a'), (2, 'b');
INSERT INTO report_subscriber (report_id, subscriber_name) VALUES (2, 'x'), (1, 'y');
...その後、report
を更新しようとすると、デッドロックが発生する可能性があります。 反対の順序で記録します。すべての更新に一貫した順序を適用することでこれを回避できますが、残念ながらORDER BY
を添付する方法はありません。 UPDATE
に 声明;カーソルに頼る必要があると思います:
CREATE FUNCTION process_pending_changes() RETURNS TRIGGER LANGUAGE plpgsql AS $$
DECLARE
target_report CURSOR FOR
SELECT report_id
FROM report
WHERE report_id IN (TABLE pending_subscriber_changes)
ORDER BY report_id
FOR NO KEY UPDATE;
BEGIN
FOR target_record IN target_report LOOP
UPDATE report
SET report_subscribers = ARRAY(
SELECT DISTINCT subscriber_name
FROM report_subscriber
WHERE report_id = target_record.report_id
ORDER BY subscriber_name
)
WHERE CURRENT OF target_report;
END LOOP;
DROP TABLE pending_subscriber_changes;
RETURN NULL;
END
$$;
クライアントが同じトランザクション内で複数のステートメントを実行しようとすると、これはデッドロックする可能性があります(更新順序は各ステートメント内でのみ適用されますが、更新ロックはコミットされるまで保持されます)。 process_pending_changes()
を起動することで、これ(一種)を回避できます。 トランザクションの最後に1回だけ(欠点は、そのトランザクション内で、自分の変更がreport_subscribers
に反映されないことです。 配列)。
「コミット時」トリガーの一般的な概要は、入力するのに苦労する価値があると思われる場合です。
CREATE FUNCTION run_on_commit() RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
<your code goes here>
RETURN NULL;
END
$$;
CREATE FUNCTION trigger_already_fired() RETURNS BOOLEAN LANGUAGE plpgsql VOLATILE AS $$
DECLARE
already_fired BOOLEAN;
BEGIN
already_fired := NULLIF(current_setting('my_vars.trigger_already_fired', TRUE), '');
IF already_fired IS TRUE THEN
RETURN TRUE;
ELSE
SET LOCAL my_vars.trigger_already_fired = TRUE;
RETURN FALSE;
END IF;
END
$$;
CREATE CONSTRAINT TRIGGER my_trigger
AFTER INSERT OR UPDATE OR DELETE ON my_table
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
WHEN (NOT trigger_already_fired())
EXECUTE PROCEDURE run_on_commit();