Triggers: automatizando ações no banco

1. O que são Triggers e por que usá-los?

Triggers (ou gatilhos) são procedimentos armazenados no banco de dados que são executados automaticamente em resposta a eventos específicos ocorridos em uma tabela ou visão. Eles representam uma poderosa ferramenta para garantir a integridade dos dados, implementar regras de negócio no nível do banco e automatizar tarefas como auditoria e sincronização.

Os principais benefícios dos triggers incluem:
- Consistência de dados: regras de negócio são aplicadas independentemente da aplicação cliente
- Auditoria automática: registro de alterações sem necessidade de código na aplicação
- Centralização da lógica: regras ficam no banco, acessíveis a todos os sistemas

É importante diferenciar triggers de constraints. Enquanto constraints como CHECK e UNIQUE impõem restrições simples e declarativas, triggers permitem lógica procedural complexa, acesso a múltiplas tabelas e validações que dependem de estado externo.

2. Anatomia de uma Trigger no PostgreSQL

No PostgreSQL, um trigger é composto por duas partes: a trigger function (função que contém a lógica) e o trigger propriamente dito (que associa a função a um evento).

-- Primeiro, criamos a trigger function
CREATE OR REPLACE FUNCTION log_insercao()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO log_auditoria(tabela, operacao, dados, usuario, data_hora)
    VALUES ('clientes', 'INSERT', row_to_json(NEW), current_user, NOW());
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Depois, criamos o trigger que chama a função
CREATE TRIGGER trigger_log_insert
AFTER INSERT ON clientes
FOR EACH ROW
EXECUTE FUNCTION log_insercao();

A trigger function deve retornar um tipo TRIGGER e pode acessar variáveis especiais como NEW e OLD. O trigger em si especifica o momento (BEFORE, AFTER, INSTEAD OF), o evento (INSERT, UPDATE, DELETE, TRUNCATE) e o escopo (FOR EACH ROW ou FOR EACH STATEMENT).

3. Tipos de Triggers: Eventos e Tempo de Disparo

Os triggers podem ser classificados pelo momento de disparo e pela operação que os ativa:

Quanto ao momento:
- BEFORE: executado antes da operação, útil para validações e modificações preventivas
- AFTER: executado após a operação, ideal para auditoria e ações que dependem do resultado
- INSTEAD OF: usado em views para substituir a operação padrão

Quanto à operação:
- INSERT: quando uma nova linha é inserida
- UPDATE: quando uma linha existente é modificada
- DELETE: quando uma linha é removida
- TRUNCATE: quando a tabela é truncada

-- Trigger BEFORE para validação
CREATE TRIGGER valida_email
BEFORE INSERT OR UPDATE ON usuarios
FOR EACH ROW
EXECUTE FUNCTION verificar_email_unico();

-- Trigger AFTER para auditoria
CREATE TRIGGER auditoria_delete
AFTER DELETE ON pedidos
FOR EACH ROW
EXECUTE FUNCTION registrar_exclusao();

4. Escopo: FOR EACH ROW vs FOR EACH STATEMENT

A escolha entre FOR EACH ROW e FOR EACH STATEMENT impacta diretamente a performance e o comportamento:

FOR EACH ROW: dispara uma vez para cada linha afetada pelo comando SQL. Ideal para validações linha a linha e operações que dependem de valores específicos.

-- Dispara para cada linha inserida
CREATE TRIGGER valida_estoque
BEFORE INSERT ON itens_pedido
FOR EACH ROW
EXECUTE FUNCTION verificar_disponibilidade();

FOR EACH STATEMENT: dispara uma única vez por comando SQL, independentemente do número de linhas afetadas. Útil para auditoria agregada e operações que não dependem de dados específicos.

-- Dispara uma vez, mesmo que 1000 linhas sejam deletadas
CREATE TRIGGER auditoria_massiva
AFTER DELETE ON pedidos
FOR EACH STATEMENT
EXECUTE FUNCTION registrar_operacao_em_lote();

5. Acessando Dados Dentro da Trigger Function

As variáveis especiais NEW e OLD permitem acessar os valores das linhas envolvidas na operação:

  • NEW: contém os novos valores (disponível em INSERT e UPDATE)
  • OLD: contém os valores antigos (disponível em UPDATE e DELETE)
CREATE OR REPLACE FUNCTION impede_reducao_salario()
RETURNS TRIGGER AS $$
BEGIN
    IF NEW.salario < OLD.salario THEN
        RAISE EXCEPTION 'Não é permitido reduzir o salário de % para %', 
                        OLD.salario, NEW.salario;
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_salario
BEFORE UPDATE ON funcionarios
FOR EACH ROW
EXECUTE FUNCTION impede_reducao_salario();

6. Casos de Uso Práticos

Auditoria automática

CREATE TABLE log_auditoria (
    id SERIAL PRIMARY KEY,
    tabela TEXT,
    operacao TEXT,
    dados_antigos JSONB,
    dados_novos JSONB,
    usuario TEXT,
    data_hora TIMESTAMP DEFAULT NOW()
);

CREATE OR REPLACE FUNCTION auditoria_geral()
RETURNS TRIGGER AS $$
BEGIN
    IF TG_OP = 'INSERT' THEN
        INSERT INTO log_auditoria(tabela, operacao, dados_novos, usuario)
        VALUES (TG_TABLE_NAME, TG_OP, row_to_json(NEW), current_user);
        RETURN NEW;
    ELSIF TG_OP = 'UPDATE' THEN
        INSERT INTO log_auditoria(tabela, operacao, dados_antigos, dados_novos, usuario)
        VALUES (TG_TABLE_NAME, TG_OP, row_to_json(OLD), row_to_json(NEW), current_user);
        RETURN NEW;
    ELSIF TG_OP = 'DELETE' THEN
        INSERT INTO log_auditoria(tabela, operacao, dados_antigos, usuario)
        VALUES (TG_TABLE_NAME, TG_OP, row_to_json(OLD), current_user);
        RETURN OLD;
    END IF;
END;
$$ LANGUAGE plpgsql;

Atualização automática de updated_at

CREATE OR REPLACE FUNCTION atualiza_updated_at()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_updated_at
BEFORE UPDATE ON clientes
FOR EACH ROW
EXECUTE FUNCTION atualiza_updated_at();

7. Cuidados, Boas Práticas e Performance

Evite triggers recursivos: um trigger que modifica a mesma tabela que o disparou pode criar loops infinitos. Use variáveis de sessão ou flags para controlar isso.

-- Exemplo de proteção contra recursão
CREATE OR REPLACE FUNCTION atualiza_total_pedido()
RETURNS TRIGGER AS $$
BEGIN
    IF current_setting('meu_app.evitar_recursao', TRUE) = 'true' THEN
        RETURN NEW;
    END IF;

    PERFORM set_config('meu_app.evitar_recursao', 'true', true);
    UPDATE pedidos SET total = (SELECT SUM(valor) FROM itens WHERE pedido_id = NEW.pedido_id)
    WHERE id = NEW.pedido_id;
    PERFORM set_config('meu_app.evitar_recursao', 'false', true);

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Impacto em operações em lote: triggers FOR EACH ROW em operações que afetam milhões de linhas podem degradar severamente a performance. Considere usar FOR EACH STATEMENT ou desabilitar temporariamente o trigger.

Documentação: triggers podem tornar o comportamento do banco "invisível" para desenvolvedores. Mantenha uma documentação clara e nomeie triggers de forma descritiva.

Debugging: use RAISE NOTICE para depuração temporária:

RAISE NOTICE 'Trigger disparado para % na tabela %', TG_OP, TG_TABLE_NAME;

8. Limitações e Alternativas

Limitações:
- Triggers não podem retornar dados diretamente ao cliente
- Podem tornar o banco mais complexo e difícil de depurar
- Não são portáveis entre diferentes SGBDs

Alternativas:
- Constraints: para validações simples e declarativas
- Regras de negócio na aplicação: mais flexíveis e testáveis
- Views materializadas: para sincronização de dados agregados
- Event triggers: para capturar eventos no nível do banco (DDL)

Quando evitar:
- Operações de alta frequência onde performance é crítica
- Lógica complexa que seria melhor implementada na aplicação
- Cenários onde a portabilidade entre bancos é essencial

Triggers são ferramentas poderosas quando usadas com moderação e planejamento. Eles brilham em cenários de auditoria, validações complexas e sincronização automática, mas devem ser evitados quando a lógica pode ser implementada de forma mais simples e eficiente em outros níveis da aplicação.

Referências