2017-12-17 7 views
6

私は、トリガーを使ってお互いにやりとりするテーブルをいくつか持っています。私がトリガーの実行を処理している現在の方法は、醜いpg_trigger_depth() < 2を使用しています。 最終的なトリガーが1回だけ実行され、最後にすべての行の処理が行われた後、残念なことに、CONSTRAINT TRIGGERFOR EACH ROWのみであり、FOR STATEMENTトリガーはトリガー内の1つのステートメントにつき実際に起動します。起動した最初のステートメントにつき1回だけではありません。更新プログラムのチェーンの終了時にトリガーを起動するにはどうすればよいですか?

私はこのトピックに関する他のいくつかの質問を見てきましたが、私がやっていることに似たものは見つけられませんでした。ここで

はセットアップです:

CREATE TABLE report(
    report_tk SERIAL PRIMARY KEY, 
    report_id UUID NOT NULL, 
    report_name TEXT NOT NULL, 
    report_data INT NOT NULL, 
    report_subscribers TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], 
    valid_range TSTZRANGE NOT NULL DEFAULT '(,)', 
    EXCLUDE USING GIST ((report_id :: TEXT) WITH =, report_name WITH =, valid_range WITH &&) 
); 
CREATE TABLE report_subscriber(
    report_id INT NOT NULL REFERENCES report ON DELETE CASCADE; 
    subscriber_name TEXT NOT NULL, 
    needs_sync BOOLEAN NOT NULL DEFAULT TRUE, 
    EXCLUDE USING GIST (subscriber_name WITH =, valid_range WITH &&) 
); 
CREATE OR REPLACE FUNCTION sync_subscribers_to_report() 
    RETURNS TRIGGER LANGUAGE plpgsql SET SEARCH_PATH TO dwh, public AS $$ 
BEGIN 
    RAISE INFO 'Running sync to report trigger'; 

    BEGIN 
    CREATE TEMPORARY TABLE lock_sync_subscribers_to_report(
    ) ON COMMIT DROP; 
    RAISE INFO 'syncing to report, stack depth is: %', pg_trigger_depth(); 
    UPDATE report r 
    SET report_subscribers = x.subscribers 
    FROM (
      SELECT 
      report_tk 
      , array_agg(DISTINCT u.subscriber_name ORDER BY u.subscriber_name) AS subscribers 
      FROM report_subscriber s 
      WHERE s.report_tk IN (
      SELECT DISTINCT report_tk 
      FROM report_subscriber s2 
      WHERE s.needs_sync 
      ) 
      GROUP BY s.report_tk 
     ) x 
    WHERE r.report_tk = x.report_tk; 
    RAISE INFO 'turning off sync flag, stack depth is: %', pg_trigger_depth(); 
    UPDATE report_subscriber 
    SET needs_sync = FALSE 
    WHERE needs_sync = TRUE; 
    RETURN NULL; 
    EXCEPTION WHEN DUPLICATE_TABLE THEN 
    RAISE INFO 'skipping recursive call, stack depth is: %', pg_trigger_depth(); 
    RETURN NULL; 
    END; 
END; 
$$; 
CREATE TRIGGER sync_subscribers_to_report 
    AFTER INSERT OR UPDATE OR DELETE 
    ON report_subscriber 
    FOR STATEMENT 
EXECUTE PROCEDURE sync_subscribers_to_report(); 

したがって、この設定で、私のことができるようにしたいと思います:

  • は、レポート名だけができることをレポートレコード
  • 保証を挿入します任意の時点で1回存在する(有効範囲のEXCLUDE)
  • サブスクライバテーブルにレポートサブスクライバを挿入する
  • は、加入者が一度に複数のレポートに加入できないことを保証します。
  • 複数の人がレポートを購読できるようにします。
  • レコードがサブスクライバテーブルに追加されるたびに、その名前をレポートテーブルのサブスクライバリストに追加します。
  • レコードがサブスクライバテーブルから削除されるたびに、レポートテーブルのサブスクライバリストからその名前を削除します。単一のステートメント内の加入者テーブルの編集の多くは(一般的なケースがある場合は、レコードがレポートテーブルから削除されるたびに
  • は、ON DELETE CASCADE

により(の世話を対応する加入者レコードを削除します)、単純なクエリを実行して、サブスクライバテーブルの新しいレコードと残りのレコードの集計を使用してレポートテーブルを更新するのが最善の方法です。

私の元のソリューションはサブスクライバテーブルにneeds_updateフラグを追加し、それを更新してからフラグをオフにしてください。もちろん、これは別のトリガを起動させますpg_trigger_depth() < 2が付いています(この2つは挿入がシステムの他のトリガーによって引き起こされる可能性があるためです)。 醜いことに加えて、トリガー関数のステートメントがさらに多くのエラーを引き起こすことも迷惑になります。

私は、他のSO答え(https://stackoverflow.com/a/8950639/2340769)の中で見たトリックを使って別のバージョンを試しました。一時テーブルを作成し、さらに実行を防ぐためにdupeテーブル例外をキャッチします。私はそれが本当に問題をかなり改善するとは思わない。

私がやっていることをきれいにする方法はありますか?これは明らかなおもちゃの例ですが、私の実際のアプリケーションでは、データの "パックドアレイ"表現を構築する必要があります。効率的な方法で行うことは素晴らしいことです。

+0

:あなたは、それはそれを埋めるために、トラブル価値があると思うなら

はここで、「上のコミット」トリガーのための一般的な概要です。私は一時テーブルが 'ON COMMIT DROP'によって削除された後に起動することを期待して' SQL_DROP'に 'EVENT TRIGGER'を作成しようとしましたが、トリガーはその自動ドロップでは起動しません。 – deinspanjer

+0

'needs_sync'フラグは何か他に必要ですか?それとも、このトリガーのためだけですか? –

+0

トリガーのためだけにあります。 – deinspanjer

答えて

5

report_subscriberにフラグを使用するのではなく、変更が保留中の別のキューを使用する方がよいと思います。

  • フードの下には、トリガー再帰
  • は、UPDATEはそうキューに挿入すると、実際にはおそらくかなりフラグ
  • をめくるよりも安くなり、ちょうどDELETE +再INSERTではありません:これは、いくつかの利点がありますレコードを完全に複製するのではなく、report_idの別のキューに入れる必要があるだけで、一時テーブルで実行することができるので、ストレージは連続しているため、ディスクに何も同期する必要はありません。
  • 競合条件なしフラグをひっくり返すときに心配するキューが現在のトランザクションに対してローカルであるとしてsが、

ので、キュー表を初期化(実装では、UPDATE report_subscriberによって影響を受けるレコードが...あなたはSELECTで拾っ必ずしも同じレコードがありません) :文の末尾にキューを

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 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 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は、更新の順序を保証していません。これは、これらの二つの文が同時に実行された場合には、ことを意味します

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レコードを更新しようとすると...その後、デッドロックの可能性があります。残念ながら、UPDATEステートメントにORDER BYを添付する方法はありません。私はあなたがカーソルに頼る必要があると思う:

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(); 
関連する問題