如何在更新链的末尾触发触发器?

时间:2017-12-17 06:32:47

标签: postgresql postgresql-9.6

我有几个使用触发器相互交互的表,而我处理触发器执行的当前方式使用了丑陋的pg_trigger_depth() < 2。 我真的希望最终的触发器只运行一次,并且在所有的每行事件发生之后结束。不幸的是,CONSTRAINT TRIGGER只是FOR EACH ROWFOR STATEMENT触发器实际上在触发器中每个语句触发一次,而不是每个初始语句启动它一次。

我已经围绕这个主题查看了其他几个SO问题,并且找不到与我正在做的事情相似的东西。

以下是设置:

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();

因此,通过此设置,我希望能够:

  • 插入报告记录
  • 保证报告名称在任何单个时间点只能存在一次(在valid_range上的EXCLUDE)
  • 在订阅者表中插入报告订阅者
  • 保证订阅者一次不能订阅多个报告。
  • 允许多人订阅报告。
  • 每当将记录添加到订阅者表时,请将该名称添加到报告表中的订阅者列表中。
  • 每当从订阅者表中删除记录时,请从报告表中的订阅者列表中删除该名称。
  • 每当从报告表中删除记录时,删除相应的订户记录(由ON DELETE CASCADE
  • 处理)

如果在单个语句(通常情况)中对订阅者表进行了大量编辑,则最好只运行一个简单查询,以使用来自其中的新记录和剩余记录的聚合来更新报告表。订阅者表。

我的原始解决方案涉及向订阅者表添加needs_update标志并触发该标记以执行更新,然后关闭标记。当然,这导致触发器的另一次触发,我用pg_trigger_depth() < 2停止(2是因为插入可能是由系统中的其他触发器引起的)。 除了丑陋之外,触发器函数中的语句导致更多FOR EACH STATEMENT次发生也令人讨厌。

我尝试使用我在其他一个SO答案(https://stackoverflow.com/a/8950639/2340769)中看到的技巧来创建临时表并捕获欺骗表异常以防止进一步执行。我不认为它确实改善了这个问题。

有没有办法以干净的方式做我想做的事情?虽然这是一个明显的玩具示例,但我真正的应用程序确实需要构建#34;打包数组&#34;表示数据,以高效的方式这样做会很棒。

1 个答案:

答案 0 :(得分:5)

我认为你最好使用一个单独的挂起更改队列,而不是在report_subscriber本身使用一个标志。这有一些好处:

  • 无触发器递归
  • 在幕后,UPDATE只是DELETE +重新INSERT,因此插入队列实际上比翻转旗帜更便宜
  • 可能要便宜一点,因为你只需要对不同的report_id进行排队,而不是克隆整个report_subscriber记录,你可以在临时表中进行,因此存储是连续的没有什么需要同步到磁盘
  • 在翻转标志时没有竞争条件,因为队列是当前事务的本地队列(在您的实现中,受UPDATE report_subscriber影响的记录不一定与您在SELECT中记录的记录相同{1}} ...)

因此,初始化队列表:

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不提供有关更新订单的任何保证。这意味着,如果同时运行这两个语句:

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}中} array)。

如果你认为填写它是值得的,那么这是一个“on commit”触发器的通用大纲:

report_subscribers