Este é um problema complicado. Mas isso pode ser feito com gatilhos por coluna e execução de gatilho condicional introduzidos no PostgreSQL 9.0 .
Você precisa de um sinalizador "atualizado" por linha para esta solução. Use um
boolean
coluna na mesma tabela para simplificar. Mas pode ser em outra tabela ou até em uma tabela temporária por transação. O payload caro é executado uma vez por linha onde o contador é atualizado (uma ou várias vezes).
Isso também deve funcionar bem, porque...
- ... evita várias chamadas de gatilhos na raiz (escala bem)
- ... não altera linhas adicionais (minimiza o inchaço da tabela)
- ... não precisa de tratamento de exceção caro.
Considere o seguinte
Demonstração
Testado no PostgreSQL 9.1 com um esquema separado
x
como ambiente de teste. Tabelas e linhas fictícias
-- DROP SCHEMA x;
CREATE SCHEMA x;
CREATE TABLE x.tbl (
id int
,counter int
,trig_exec_count integer -- for monitoring payload execution.
,updated bool);
Insira duas linhas para demonstrar que funciona com várias linhas:
INSERT INTO x.tbl VALUES
(1, 0, 0, NULL)
,(2, 0, 0, NULL);
Funções de gatilho e gatilhos
1.) Executar carga útil cara
CREATE OR REPLACE FUNCTION x.trg_upaft_counter_change_1()
RETURNS trigger AS
$BODY$
BEGIN
-- PERFORM some_expensive_procedure(NEW.id);
-- Update trig_exec_count to count execution of expensive payload.
-- Could be in another table, for simplicity, I use the same:
UPDATE x.tbl t
SET trig_exec_count = trig_exec_count + 1
WHERE t.id = NEW.id;
RETURN NULL; -- RETURN value of AFTER trigger is ignored anyway
END;
$BODY$ LANGUAGE plpgsql;
2.) Sinalize a linha como atualizada.
CREATE OR REPLACE FUNCTION x.trg_upaft_counter_change_2()
RETURNS trigger AS
$BODY$
BEGIN
UPDATE x.tbl
SET updated = TRUE
WHERE id = NEW.id;
RETURN NULL;
END;
$BODY$ LANGUAGE plpgsql;
3.) Redefina o sinalizador "atualizado".
CREATE OR REPLACE FUNCTION x.trg_upaft_counter_change_3()
RETURNS trigger AS
$BODY$
BEGIN
UPDATE x.tbl
SET updated = NULL
WHERE id = NEW.id;
RETURN NULL;
END;
$BODY$ LANGUAGE plpgsql;
Os nomes dos gatilhos são relevantes! Chamados para o mesmo evento são executados em ordem alfabética.
1.) Payload, somente se ainda não estiver "atualizado":
CREATE CONSTRAINT TRIGGER upaft_counter_change_1
AFTER UPDATE OF counter ON x.tbl
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
WHEN (NEW.updated IS NULL)
EXECUTE PROCEDURE x.trg_upaft_counter_change_1();
2.) Sinalizar a linha como atualizada, somente se ainda não estiver "atualizada":
CREATE TRIGGER upaft_counter_change_2 -- not deferred!
AFTER UPDATE OF counter ON x.tbl
FOR EACH ROW
WHEN (NEW.updated IS NULL)
EXECUTE PROCEDURE x.trg_upaft_counter_change_2();
3.) Redefinir Sinalizador. Sem loop infinito por causa da condição de disparo.
CREATE CONSTRAINT TRIGGER upaft_counter_change_3
AFTER UPDATE OF updated ON x.tbl
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
WHEN (NEW.updated) --
EXECUTE PROCEDURE x.trg_upaft_counter_change_3();
Teste
Execute
UPDATE
&SELECT
separadamente para ver o efeito diferido. Se executado em conjunto (em uma transação) o SELECT mostrará o novo tbl.counter
mas o antigo tbl2.trig_exec_count
. UPDATE x.tbl SET counter = counter + 1;
SELECT * FROM x.tbl;
Agora, atualize o contador várias vezes (em uma transação). A carga útil será executada apenas uma vez. Voilá!
UPDATE x.tbl SET counter = counter + 1;
UPDATE x.tbl SET counter = counter + 1;
UPDATE x.tbl SET counter = counter + 1;
UPDATE x.tbl SET counter = counter + 1;
UPDATE x.tbl SET counter = counter + 1;
SELECT * FROM x.tbl;