Em vez de usar um sinalizador em
report_subscriber
em si, acho que seria melhor ter uma fila separada de alterações pendentes. Isso tem alguns benefícios:- Sem recursão de gatilho
- Nos bastidores,
UPDATE
é apenasDELETE
+ re-INSERT
, portanto, inserir em uma fila será realmente mais barato do que lançar um sinalizador - Possivelmente um pouco mais barato, já que você só precisa enfileirar o distinto
report_id
s, em vez de clonar oreport_subscriber
inteiro registros, e você pode fazer isso em uma tabela temporária, para que o armazenamento seja contíguo e nada precise ser sincronizado com o disco - Nenhuma condição de corrida para se preocupar ao inverter as bandeiras, pois a fila é local para a transação atual (em sua implementação, os registros afetados pelo
UPDATE report_subscriber
não são necessariamente os mesmos registros que você pegou noSELECT
...)
Então, inicialize a tabela de filas:
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();
... enfileirar as alterações conforme elas chegam, ignorando qualquer coisa já enfileirada:
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();
...e processe a fila no final da instrução:
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();
Há um pequeno problema com isso:
UPDATE
não oferece nenhuma garantia sobre o pedido de atualização. Isso significa que, se essas duas instruções fossem executadas simultaneamente: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');
...então há uma chance de um impasse, se eles tentarem atualizar o
report
registros em ordens opostas. Você pode evitar isso impondo uma ordem consistente para todas as atualizações, mas infelizmente não há como anexar um ORDER BY
para um UPDATE
declaração; Acho que você precisa recorrer aos cursores: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
$$;
Isso ainda tem o potencial de travamento se o cliente tentar executar várias instruções na mesma transação (já que a ordem de atualização é aplicada apenas em cada instrução, mas os bloqueios de atualização são mantidos até a confirmação). Você pode contornar isso (mais ou menos) disparando
process_pending_changes()
apenas uma vez no final da transação (a desvantagem é que, nessa transação, você não verá suas próprias alterações refletidas no report_subscribers
variedade). Aqui está um esboço genérico para um gatilho "ao confirmar", se você acha que vale a pena preenchê-lo:
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();