41.10. Funções de gatilho #

41.10.1. Gatilhos de alteração de dados
41.10.2. Gatilhos de evento

O PL/pgSQL pode ser usado para definir funções de gatilho em alterações de dados, ou eventos de banco de dados. A função de gatilho é criada pelo comando CREATE FUNCTION, declarando-a como uma função sem argumentos, e retornando o tipo de dados trigger (para gatilhos de alteração de dados) ou event_trigger (para gatilhos de evento de banco de dados). São definidas, automaticamente, variáveis locais especiais chamadas TG_alguma_coisa para descrever a condição que acionou a chamada.

41.10.1. Gatilhos de alteração de dados #

Um gatilho de alteração de dados é declarado como uma função sem argumentos, e retornando o tipo trigger. Note que a função deve ser declarada sem argumentos, mesmo que espere receber alguns argumentos especificados em CREATE TRIGGER — estes argumentos são passados via TG_ARGV, conforme descrito abaixo.

Quando uma função PL/pgSQL é chamada como gatilho, são criadas automaticamente diversas variáveis especiais no bloco de nível superior. Elas são:

NEW record #

Nova linha de banco de dados para operações de INSERT/UPDATE em gatilhos no nível de linha. Esta variável é nula em gatilhos no nível de instrução e para operações de DELETE.

OLD record #

Linha antiga do banco de dados para operações de UPDATE/DELETE em gatilhos no nível de linha. Esta variável é nula em gatilhos no nível de instrução e para operações de INSERT.

TG_NAME name #

Nome do gatilho disparado.

TG_WHEN text #

BEFORE, AFTER ou INSTEAD OF, dependendo da definição do gatilho.

TG_LEVEL text #

ROW ou STATEMENT, dependendo da definição do gatilho.

TG_OP text #

Operação que disparou o gatilho: INSERT, UPDATE, DELETE ou TRUNCATE.

TG_RELID oid (references pg_class.oid) #

ID do objeto da tabela que causou o disparo do gatilho.

TG_RELNAME name #

Tabela que causou o disparo do gatilho. Está obsoleta no memento podendo desaparecer em uma versão futura. Deve ser usado TG_TABLE_NAME em seu lugar.

TG_TABLE_NAME name #

Tabela que causou o disparo do gatilho.

TG_TABLE_SCHEMA name #

Esquema da tabela que causou o disparo do gatilho.

TG_NARGS integer #

Número de argumentos fornecidos à função de gatilho na instrução CREATE TRIGGER.

TG_ARGV text[] #

Argumentos da instrução CREATE TRIGGER. A contagem do índice começa em 0. Índices inválidos (menor que 0 ou maior ou igual a tg_nargs) resultam em um valor nulo.

A função de gatilho deve retornar NULL, ou um valor de registro/linha com a mesma estrutura da tabela para a qual o gatilho foi acionado.

Os gatilhos no nível de linha disparados antes (BEFORE), podem retornar nulo para informar ao gerenciador de gatilhos para pular o restante da operação para esta linha (ou seja, os gatilhos subsequentes não são acionados e INSERT/UPDATE/DELETE não ocorre para esta linha). Se for retornado um valor não nulo, a operação continuará com este valor de linha. Retornar um valor de linha diferente do valor original de NEW altera a linha que será inserida ou atualizada. Portanto, se a função de gatilho desejar que a ação de gatilho prossiga normalmente sem alterar o valor da linha, deverá ser retornado NEW (ou um valor igual a ele). Para alterar a linha a ser armazenada, é possível substituir valores individuais diretamente em NEW, e retornar o NEW modificado, ou construir um novo registro/linha completo para retornar. No caso do gatilho anterior ter sido para DELETE, o valor retornado não tem efeito direto, mas deve ser não nulo para permitir que a ação do gatilho continue. Note que NEW é nulo nos gatilhos DELETE, portanto, retornar a mesma coisa geralmente não faz sentido. O modo usual em gatilhos DELETE é retornar OLD.

Os gatilhos INSTEAD OF (sendo sempre gatilhos no nível de linha, só podendo ser usados em visões) podem retornar nulo para indicar que não foi realizada nenhuma atualização, e que o restante da operação para esta linha deve ser ignorado (ou seja, os gatilhos subsequentes não serão acionados, e a linha não será contada no status de linhas afetadas pelo INSERT/UPDATE/DELETE circundante). Caso contrário, um valor não nulo deverá ser retornado para indicar que o gatilho executou a operação solicitada. Para as operações INSERT e UPDATE, o valor retornado deverá ser NEW, que a função do gatilho poderá modificar para dar suporte a INSERT RETURNING e UPDATE RETURNING (isto também afetará o valor da linha passado para quaisquer gatilhos subsequentes, ou passado para o alias especial EXCLUDED em uma instrução INSERT com a cláusula ON CONFLICT DO UPDATE). Para operações DELETE, o valor retornado deverá ser OLD.

O valor retornado por um gatilho no nível de linha disparado após (AFTER), ou um gatilho no nível de instrução disparado antes (BEFORE) ou após (AFTER) é sempre ignorado; pode muito bem ser nulo. Entretanto, qualquer um desses tipos de gatilho ainda pode interromper toda a operação levantando um erro.

O Exemplo 41.5 mostra uma função de gatilho escrita em PL/pgSQL.

Exemplo 41.5. Função de gatilho escrita em PL/pgSQL

O gatilho desse exemplo garante que, sempre que uma linha for inserida ou atualizada na tabela, o nome do usuário e a hora corrente sejam estampados na linha. E verifica se é fornecido o nome do funcionário e se o salário é um valor positivo.

CREATE TABLE emp (
    empname           text,
    salary            integer,
    last_date         timestamp,
    last_user         text
);

CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
    BEGIN
        -- Verificar se o nome do funcionário e o salário foram fornecidos.
        IF NEW.empname IS NULL THEN
            RAISE EXCEPTION 'empname não pode ser nulo';
        END IF;
        IF NEW.salary IS NULL THEN
            RAISE EXCEPTION '% não pode ter um salário nulo', NEW.empname;
        END IF;

        -- Quem trabalha para nós quando precisa pagar por isto?
        IF NEW.salary < 0 THEN
            RAISE EXCEPTION '% não pode ter um salário negativo', NEW.empname;
        END IF;

        -- Lembre-se de quem mudou a folha de pagamento e quando
        NEW.last_date := current_timestamp;
        NEW.last_user := current_user;
        RETURN NEW;
    END;
$emp_stamp$ LANGUAGE plpgsql;

CREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON emp
    FOR EACH ROW EXECUTE FUNCTION emp_stamp();

Outra maneira de registrar alterações em uma tabela envolve a criação de uma nova tabela contendo uma linha para cada inserção, atualização ou exclusão ocorrida. Esta abordagem pode ser vista como auditoria de alterações em uma tabela. O Exemplo 41.6 mostra uma função de gatilho para auditoria escrita em PL/pgSQL.

Exemplo 41.6. Função de gatilho para auditoria escrita em PL/pgSQL

O gatilho desse exemplo garante que qualquer inserção, atualização ou exclusão de uma linha na tabela emp seja registrada (ou seja, auditada) na tabela emp_audit. O nome do usuário e a hora corrente são estampados na linha de auditoria, mais o tipo de operação realizada na tabela emp.

CREATE TABLE emp (
    empname           text NOT NULL,
    salary            integer
);

CREATE TABLE emp_audit(
    operation         char(1)   NOT NULL,
    stamp             timestamp NOT NULL,
    userid            text      NOT NULL,
    empname           text      NOT NULL,
    salary            integer
);

CREATE OR REPLACE FUNCTION process_emp_audit() RETURNS TRIGGER AS $emp_audit$
    BEGIN
        --
        -- Criar uma linha em emp_audit para refletir a operação
        -- realizada em emp, utilizando a variável especial TG_OP
        -- para calcular a operação.
        --
        IF (TG_OP = 'DELETE') THEN
            INSERT INTO emp_audit SELECT 'D', now(), current_user, OLD.*;
        ELSIF (TG_OP = 'UPDATE') THEN
            INSERT INTO emp_audit SELECT 'U', now(), current_user, NEW.*;
        ELSIF (TG_OP = 'INSERT') THEN
            INSERT INTO emp_audit SELECT 'I', now(), current_user, NEW.*;
        END IF;
        RETURN NULL; -- O resultado é ignorado, porque se trata de um gatilho AFTER.
    END;
$emp_audit$ LANGUAGE plpgsql;

CREATE TRIGGER emp_audit
AFTER INSERT OR UPDATE OR DELETE ON emp
    FOR EACH ROW EXECUTE FUNCTION process_emp_audit();

Uma variação do exemplo anterior usa uma visão juntando a tabela principal à tabela de auditoria, para mostrar quando cada entrada foi modificada pela última vez. Esta abordagem ainda registra a trilha de auditoria completa das alterações na tabela, mas também apresenta uma visão simplificada da trilha de auditoria, mostrando apenas o registro de data e hora da última modificação derivada da trilha de auditoria para cada entrada. O Exemplo 41.7 mostra uma Função de gatilho de visão para auditoria escrita em PL/pgSQL.

Exemplo 41.7. Função de gatilho de visão para auditoria escrita em PL/pgSQL

Este exemplo usa um gatilho na visão para torná-la atualizável, e garantir que qualquer inserção, atualização ou exclusão de uma linha na visão seja registrada (ou seja, auditada) na tabela emp_audit. O nome do usuário e a hora corrente são registrados, mais o tipo de operação realizada. A visão mostra a hora da última modificação de cada linha.

CREATE TABLE emp (
    empname           text PRIMARY KEY,
    salary            integer
);

CREATE TABLE emp_audit(
    operation         char(1)   NOT NULL,
    userid            text      NOT NULL,
    empname           text      NOT NULL,
    salary            integer,
    stamp             timestamp NOT NULL
);

CREATE VIEW emp_view AS
    SELECT e.empname,
           e.salary,
           max(ea.stamp) AS last_updated
      FROM emp e
      LEFT JOIN emp_audit ea ON ea.empname = e.empname
     GROUP BY 1, 2;

CREATE OR REPLACE FUNCTION update_emp_view() RETURNS TRIGGER AS $$
    BEGIN
        --
        -- Executar a operação necessária em emp e criar uma linha em
        -- emp_audit para refletir a alteração feita em emp.
        --
        IF (TG_OP = 'DELETE') THEN
            DELETE FROM emp WHERE empname = OLD.empname;
            IF NOT FOUND THEN RETURN NULL; END IF;

            OLD.last_updated = now();
            INSERT INTO emp_audit VALUES('D', current_user, OLD.*);
            RETURN OLD;
        ELSIF (TG_OP = 'UPDATE') THEN
            UPDATE emp SET salary = NEW.salary WHERE empname = OLD.empname;
            IF NOT FOUND THEN RETURN NULL; END IF;

            NEW.last_updated = now();
            INSERT INTO emp_audit VALUES('U', current_user, NEW.*);
            RETURN NEW;
        ELSIF (TG_OP = 'INSERT') THEN
            INSERT INTO emp VALUES(NEW.empname, NEW.salary);

            NEW.last_updated = now();
            INSERT INTO emp_audit VALUES('I', current_user, NEW.*);
            RETURN NEW;
        END IF;
    END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER emp_audit
INSTEAD OF INSERT OR UPDATE OR DELETE ON emp_view
    FOR EACH ROW EXECUTE FUNCTION update_emp_view();

Outro uso de gatilho é manter uma tabela de resumo de outra tabela. O resumo resultante pode ser usado no lugar da tabela original para determinadas consultas — muitas vezes com tempos de execução muito reduzidos. Esta técnica é comumente usada em Armazéns de Dados (Data Warehousing), onde as tabelas de dados medidos ou observados (chamadas de tabelas de fatos) podem ser extremamente grandes. O Exemplo 41.8 mostra uma função de gatilho escrita em PL/pgSQL que mantém uma tabela de resumo para uma tabela de fatos em um armazém de dados.

Exemplo 41.8. Função de gatilho para manter a tabela de resumo escrita em PL/pgSQL

O esquema detalhado aqui é parcialmente baseado no exemplo Grocery Store do livro The Data Warehouse Toolkit de Ralph Kimball.

--
-- Tabelas principais - dimensão temporal e fatos de vendas.
--
CREATE TABLE time_dimension (
    time_key                    integer NOT NULL,
    day_of_week                 integer NOT NULL,
    day_of_month                integer NOT NULL,
    month                       integer NOT NULL,
    quarter                     integer NOT NULL,
    year                        integer NOT NULL
);
CREATE UNIQUE INDEX time_dimension_key ON time_dimension(time_key);

CREATE TABLE sales_fact (
    time_key                    integer NOT NULL,
    product_key                 integer NOT NULL,
    store_key                   integer NOT NULL,
    amount_sold                 numeric(12,2) NOT NULL,
    units_sold                  integer NOT NULL,
    amount_cost                 numeric(12,2) NOT NULL
);
CREATE INDEX sales_fact_time ON sales_fact(time_key);

--
-- Tabela resumo - vendas por período.
--
CREATE TABLE sales_summary_bytime (
    time_key                    integer NOT NULL,
    amount_sold                 numeric(15,2) NOT NULL,
    units_sold                  numeric(12) NOT NULL,
    amount_cost                 numeric(15,2) NOT NULL
);
CREATE UNIQUE INDEX sales_summary_bytime_key ON sales_summary_bytime(time_key);

--
-- Função e gatilho para alterar a(s) coluna(s) sumarizada(s)
-- em operações de UPDATE, INSERT e DELETE.
--
CREATE OR REPLACE FUNCTION maint_sales_summary_bytime() RETURNS TRIGGER
AS $maint_sales_summary_bytime$
    DECLARE
        delta_time_key          integer;
        delta_amount_sold       numeric(15,2);
        delta_units_sold        numeric(12);
        delta_amount_cost       numeric(15,2);
    BEGIN

        -- Cálculo do(s) valor(es) do incremento/decremento.
        IF (TG_OP = 'DELETE') THEN

            delta_time_key = OLD.time_key;
            delta_amount_sold = -1 * OLD.amount_sold;
            delta_units_sold = -1 * OLD.units_sold;
            delta_amount_cost = -1 * OLD.amount_cost;

        ELSIF (TG_OP = 'UPDATE') THEN

            -- proibir atualizações que alterem time_key -
            -- (provavelmente não muito trabalhoso, já que DELETE + INSERT
            --  será como a maioria das alterações serão feitas).
            IF ( OLD.time_key != NEW.time_key) THEN
                RAISE EXCEPTION 'Update of time_key : % -> % not allowed',
                                                      OLD.time_key, NEW.time_key;
            END IF;

            delta_time_key = OLD.time_key;
            delta_amount_sold = NEW.amount_sold - OLD.amount_sold;
            delta_units_sold = NEW.units_sold - OLD.units_sold;
            delta_amount_cost = NEW.amount_cost - OLD.amount_cost;

        ELSIF (TG_OP = 'INSERT') THEN

            delta_time_key = NEW.time_key;
            delta_amount_sold = NEW.amount_sold;
            delta_units_sold = NEW.units_sold;
            delta_amount_cost = NEW.amount_cost;

        END IF;


        -- Inserir ou atualizar a linha de sumário com os novos valores.
        <<insert_update>>
        LOOP
            UPDATE sales_summary_bytime
                SET amount_sold = amount_sold + delta_amount_sold,
                    units_sold = units_sold + delta_units_sold,
                    amount_cost = amount_cost + delta_amount_cost
                WHERE time_key = delta_time_key;

            EXIT insert_update WHEN found;

            BEGIN
                INSERT INTO sales_summary_bytime (
                            time_key,
                            amount_sold,
                            units_sold,
                            amount_cost)
                    VALUES (
                            delta_time_key,
                            delta_amount_sold,
                            delta_units_sold,
                            delta_amount_cost
                           );

                EXIT insert_update;

            EXCEPTION
                WHEN UNIQUE_VIOLATION THEN
                    -- não fazer nada
            END;
        END LOOP insert_update;

        RETURN NULL;

    END;
$maint_sales_summary_bytime$ LANGUAGE plpgsql;

CREATE TRIGGER maint_sales_summary_bytime
AFTER INSERT OR UPDATE OR DELETE ON sales_fact
    FOR EACH ROW EXECUTE FUNCTION maint_sales_summary_bytime();

INSERT INTO sales_fact VALUES(1,1,1,10,3,15);
INSERT INTO sales_fact VALUES(1,2,1,20,5,35);
INSERT INTO sales_fact VALUES(2,2,1,40,15,135);
INSERT INTO sales_fact VALUES(2,3,1,10,1,13);
SELECT * FROM sales_summary_bytime;
DELETE FROM sales_fact WHERE product_key = 1;
SELECT * FROM sales_summary_bytime;
UPDATE sales_fact SET units_sold = units_sold * 2;
SELECT * FROM sales_summary_bytime;

Os gatilhos AFTER também podem usar tabelas de transição para inspecionar todo o conjunto de linhas alteradas pela instrução de disparo. O comando CREATE TRIGGER atribui nomes a uma, ou as duas tabelas de transição e, em seguida, a função pode se referir a estes nomes como se fossem tabelas temporárias de leitura-apenas. O Exemplo 41.9 mostra o uso de tabela de transição.

Exemplo 41.9. Auditoria com tabelas de transição

Este exemplo produz os mesmos resultados que o Exemplo 41.6, mas em vez de usar um gatilho que dispara para cada linha, usa um gatilho que dispara uma vez por instrução, após coletar as informações relevantes em uma tabela de transição. Pode ser muito mais rápido do que a abordagem de gatilho de linha quando a instrução de chamada modifica muitas linhas. Note que deve ser feita uma declaração de gatilho separada para cada tipo de evento, porque as cláusulas REFERENCING devem ser diferentes para cada caso. Mas isto não impede de usar uma única função de gatilho, se assim se desejar. (Na prática, pode ser melhor usar três funções separadas, evitando os testes de tempo de execução em TG_OP.)

CREATE TABLE emp (
    empname           text NOT NULL,
    salary            integer
);

CREATE TABLE emp_audit(
    operation         char(1)   NOT NULL,
    stamp             timestamp NOT NULL,
    userid            text      NOT NULL,
    empname           text      NOT NULL,
    salary            integer
);

CREATE OR REPLACE FUNCTION process_emp_audit() RETURNS TRIGGER AS $emp_audit$
    BEGIN
        --
        -- Criar linhas na tabela emp_audit para refletir as operações
        -- realizadas em emp, utilizando a variável especial TG_OP
        -- para determinar a operação.
        --
        IF (TG_OP = 'DELETE') THEN
            INSERT INTO emp_audit
                SELECT 'D', now(), current_user, o.* FROM old_table o;
        ELSIF (TG_OP = 'UPDATE') THEN
            INSERT INTO emp_audit
                SELECT 'U', now(), current_user, n.* FROM new_table n;
        ELSIF (TG_OP = 'INSERT') THEN
            INSERT INTO emp_audit
                SELECT 'I', now(), current_user, n.* FROM new_table n;
        END IF;
        RETURN NULL; -- o resultado é ignorado, porque se trata de um gatilho AFTER.
    END;
$emp_audit$ LANGUAGE plpgsql;

CREATE TRIGGER emp_audit_ins
    AFTER INSERT ON emp
    REFERENCING NEW TABLE AS new_table
    FOR EACH STATEMENT EXECUTE FUNCTION process_emp_audit();
CREATE TRIGGER emp_audit_upd
    AFTER UPDATE ON emp
    REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
    FOR EACH STATEMENT EXECUTE FUNCTION process_emp_audit();
CREATE TRIGGER emp_audit_del
    AFTER DELETE ON emp
    REFERENCING OLD TABLE AS old_table
    FOR EACH STATEMENT EXECUTE FUNCTION process_emp_audit();

41.10.2. Gatilhos de evento #

O PL/pgSQL pode ser usado para definir Gatilhos de evento. O PostgreSQL requer que a função a ser chamada como gatilho de evento seja declarada como uma função sem argumentos, e retornando o tipo event_trigger.

Quando uma função PL/pgSQL é chamada como gatilho de evento, são criadas automaticamente diversas variáveis especiais no bloco de nível superior. Elas são:

TG_EVENT text #

evento em que o gatilho foi disparado

TG_TAG text #

etiqueta do comando para o qual o gatilho foi disparado

Exemplo 41.10 mostra um exemplo de uma função de gatilho de evento no PL/pgSQL.

Exemplo 41.10. Função de gatilho de evento no PL/pgSQL

Este gatilho de exemplo simplesmente lança uma mensagem de NOTICE sempre que um comando compatível é executado [126].

CREATE OR REPLACE FUNCTION snitch() RETURNS event_trigger AS $$
BEGIN
    RAISE NOTICE 'snitch: % %', tg_event, tg_tag;
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER snitch ON ddl_command_start EXECUTE FUNCTION snitch();



[126] É necessário ser um superusuário para criar gatilho de evento. (N. T.)