Вместо да използвате флаг в report_subscriber
мисля, че ще е по-добре с отделна опашка с чакащи промени. Това има няколко предимства:
- Без рекурсия на задействане
- Под капака,
UPDATE
е простоDELETE
+ повторноINSERT
, така че вмъкването в опашка всъщност ще бъде по-евтино от обръщането на флаг - Вероятно доста по-евтино, тъй като трябва само да поставите на опашка отделния
report_id
s, вместо да клонира целияreport_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
не предлага никакви гаранции относно реда на актуализиране. Това означава, че ако тези два израза се изпълняват едновременно:
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()
само веднъж в края на транзакцията (недостатъкът е, че в рамките на тази транзакция няма да видите вашите собствени промени, отразени в 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();