kb.erickguedes.com
PostgreSQL: Banco Relacional Avançado

Funções, Triggers e Views

Aula 4 de 7

PL/pgSQL — Estrutura

-- Estrutura básica
CREATE OR REPLACE FUNCTION minha_funcao()
RETURNS void AS $$
<<block>>
DECLARE
    var_nome TEXT := 'Maria';
    var_idade INT;
    var_registro RECORD;
BEGIN
    -- Lógica
    RAISE NOTICE 'Olá, %', var_nome;
END;
$$ LANGUAGE plpgsql;

DECLARE, BEGIN, EXCEPTION, END

CREATE OR REPLACE FUNCTION processar_pedido(pedido_id INT)
RETURNS TEXT AS $$
DECLARE
    v_status TEXT;
    v_total NUMERIC;
BEGIN
    -- Buscar dados
    SELECT status, total INTO v_status, v_total
    FROM pedidos
    WHERE id = pedido_id;

    IF NOT FOUND THEN
        RAISE EXCEPTION 'Pedido % não encontrado', pedido_id;
    END IF;

    -- Lógica de negócio
    IF v_status != 'pendente' THEN
        RETURN 'Pedido já processado';
    END IF;

    UPDATE pedidos SET status = 'confirmado'
    WHERE id = pedido_id;

    RETURN format('Pedido %s confirmado. Total: %s', pedido_id, v_total);

EXCEPTION
    WHEN OTHERS THEN
        RAISE WARNING 'Erro ao processar pedido %: %', pedido_id, SQLERRM;
        RETURN 'Erro no processamento';
END;
$$ LANGUAGE plpgsql;

RETURN QUERY

-- Retornar conjunto de resultados diretamente de uma query
CREATE OR REPLACE FUNCTION pedidos_por_periodo(
    data_ini DATE,
    data_fim DATE
)
RETURNS TABLE(
    id INT,
    cliente TEXT,
    total NUMERIC,
    data TIMESTAMPTZ
) AS $$
BEGIN
    RETURN QUERY
    SELECT p.id, c.nome, p.total, p.criado_em
    FROM pedidos p
    JOIN clientes c ON c.id = p.cliente_id
    WHERE p.criado_em::DATE BETWEEN data_ini AND data_fim
    ORDER BY p.criado_em;
END;
$$ LANGUAGE plpgsql;

-- Uso
SELECT * FROM pedidos_por_periodo('2024-01-01', '2024-01-31');

PERFORM

-- PERFORM: executar sem retornar resultado (como SELECT sem INTO)
CREATE OR REPLACE FUNCTION atualizar_estoque(produto_id INT, qtde INT)
RETURNS void AS $$
BEGIN
    UPDATE produtos SET estoque = estoque - qtde
    WHERE id = produto_id;

    IF NOT FOUND THEN
        RAISE EXCEPTION 'Produto % não encontrado', produto_id;
    END IF;

    -- PERFORM para chamar função sem resultado
    PERFORM log_auditoria('estoque', 'atualizado', produto_id);

    -- Verificar estoque baixo
    IF (SELECT estoque FROM produtos WHERE id = produto_id) < 10 THEN
        PERFORM notificar_baixo_estoque(produto_id);
    END IF;
END;
$$ LANGUAGE plpgsql;

RAISE

-- Níveis de NOTICE/INFO/DEBUG/WARNING/EXCEPTION
CREATE OR REPLACE FUNCTION testar_raise()
RETURNS void AS $$
DECLARE
    v_valor INT := 42;
BEGIN
    RAISE DEBUG 'Debug: processando %', v_valor;
    RAISE LOG 'Log: valor = %', v_valor;
    RAISE INFO 'Info: operação iniciada';
    RAISE NOTICE 'Notice: processando %', v_valor;
    RAISE WARNING 'Warning: valor alto: %', v_valor;

    -- RAISE EXCEPTION aborta a transação
    RAISE EXCEPTION 'Erro crítico: valor inválido %', v_valor
        USING HINT = 'Valores devem ser menores que 40',
              DETAIL = format('Recebido: %s, Esperado: < 40', v_valor);
END;
$$ LANGUAGE plpgsql;

Funções — Volatility Classes

-- IMMUTABLE: resultado depende apenas dos argumentos (mesmo input = mesmo output)
CREATE OR REPLACE FUNCTION calcular_imposto(valor NUMERIC)
RETURNS NUMERIC AS $$
BEGIN
    RETURN valor * 0.15;
END;
$$ LANGUAGE plpgsql IMMUTABLE;

-- STABLE: resultado estável dentro da transação
CREATE OR REPLACE FUNCTION nome_usuario_logado()
RETURNS TEXT AS $$
BEGIN
    RETURN current_user;
END;
$$ LANGUAGE plpgsql STABLE;

-- VOLATILE: resultado pode mudar a cada chamada (padrão)
CREATE OR REPLACE FUNCTION gerar_timestamp()
RETURNS TIMESTAMPTZ AS $$
BEGIN
    RETURN NOW();
END;
$$ LANGUAGE plpgsql VOLATILE;

SQL Functions

-- Funções SQL simples (sem PL/pgSQL)
CREATE OR REPLACE FUNCTION total_pedidos_cliente(cliente_id INT)
RETURNS NUMERIC AS $$
    SELECT COALESCE(SUM(total), 0)
    FROM pedidos
    WHERE cliente_id = $1;
$$ LANGUAGE SQL STABLE;

CREATE OR REPLACE FUNCTION produtos_por_categoria(cat_id INT)
RETURNS TABLE(id INT, nome TEXT, preco NUMERIC) AS $$
    SELECT id, nome, preco
    FROM produtos
    WHERE categoria_id = cat_id
    ORDER BY nome;
$$ LANGUAGE SQL STABLE;

OUT Parameters

CREATE OR REPLACE FUNCTION calcular_estatisticas(
    IN pedido_id INT,
    OUT total_itens INT,
    OUT valor_medio NUMERIC,
    OUT valor_total NUMERIC
) AS $$
BEGIN
    SELECT
        COUNT(*),
        AVG(subtotal),
        SUM(subtotal)
    INTO total_itens, valor_medio, valor_total
    FROM itens_pedido
    WHERE pedido_id = $1;
END;
$$ LANGUAGE plpgsql;

-- Uso
SELECT * FROM calcular_estatisticas(123);
-- Retorna: total_itens, valor_medio, valor_total

Triggers

-- Estrutura: trigger function (retorna TRIGGER) + trigger na tabela

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

CREATE TRIGGER trg_pedidos_timestamp
    BEFORE UPDATE ON pedidos
    FOR EACH ROW
    EXECUTE FUNCTION trigger_atualizar_timestamp();

TG_OP e Audit Trigger

-- Trigger de auditoria genérico
CREATE TABLE audit_log (
    id          BIGSERIAL PRIMARY KEY,
    tabela      TEXT NOT NULL,
    operacao    TEXT NOT NULL,
    registro_id INT,
    dados_antigos JSONB,
    dados_novos  JSONB,
    usuario     TEXT DEFAULT current_user,
    criado_em   TIMESTAMPTZ DEFAULT NOW()
);

CREATE OR REPLACE FUNCTION audit_trigger()
RETURNS TRIGGER AS $$
DECLARE
    v_dados_antigos JSONB;
    v_dados_novos   JSONB;
    v_id INT;
BEGIN
    -- Capturar ID independente do nome da coluna
    v_id := COALESCE(NEW.id, OLD.id);

    IF TG_OP = 'INSERT' THEN
        v_dados_novos = to_jsonb(NEW);
    ELSIF TG_OP = 'UPDATE' THEN
        v_dados_antigos = to_jsonb(OLD);
        v_dados_novos = to_jsonb(NEW);
    ELSIF TG_OP = 'DELETE' THEN
        v_dados_antigos = to_jsonb(OLD);
    END IF;

    INSERT INTO audit_log (tabela, operacao, registro_id, dados_antigos, dados_novos)
    VALUES (TG_TABLE_NAME, TG_OP, v_id, v_dados_antigos, v_dados_novos);

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Aplicar a tabelas
CREATE TRIGGER trg_pedidos_audit
    AFTER INSERT OR UPDATE OR DELETE ON pedidos
    FOR EACH ROW
    EXECUTE FUNCTION audit_trigger();

CREATE TRIGGER trg_clientes_audit
    AFTER INSERT OR UPDATE OR DELETE ON clientes
    FOR EACH ROW
    EXECUTE FUNCTION audit_trigger();

INSTEAD OF

-- INSTEAD OF: usado em views (não suporta BEFORE/AFTER em views)
CREATE OR REPLACE FUNCTION inserir_cliente_view()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO clientes (nome, email, cpf)
    VALUES (NEW.nome, NEW.email, NEW.cpf)
    RETURNING id INTO NEW.id;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_vw_clientes_insert
    INSTEAD OF INSERT ON vw_clientes_ativos
    FOR EACH ROW
    EXECUTE FUNCTION inserir_cliente_view();

Views

-- View simples
CREATE VIEW vw_pedidos_recentes AS
SELECT p.id, c.nome AS cliente, p.total, p.status, p.criado_em
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
WHERE p.criado_em > NOW() - INTERVAL '30 days'
ORDER BY p.criado_em DESC;

-- Updatable view (se não tiver JOIN, GROUP BY, etc)
CREATE VIEW vw_clientes_ativos AS
SELECT id, nome, email, telefone
FROM clientes
WHERE ativo = true;

INSERT INTO vw_clientes_ativos (nome, email) VALUES ('Novo', '[email protected]');
-- Funciona!

-- WITH CHECK OPTION: impede inserções que violam a condição WHERE
CREATE VIEW vw_produtos_baratos AS
SELECT * FROM produtos WHERE preco < 50
WITH CHECK OPTION;

INSERT INTO vw_produtos_baratos (nome, preco) VALUES ('Caro', 100);
-- ERRO! Violou WITH CHECK OPTION

Materialized Views

-- View materializada: dados são armazenados fisicamente
CREATE MATERIALIZED VIEW mv_relatorio_vendas AS
SELECT
    c.nome AS cliente,
    COUNT(p.id) AS total_pedidos,
    SUM(p.total) AS total_gasto,
    MAX(p.criado_em) AS ultima_compra
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
GROUP BY c.id, c.nome;

-- Atualizar
REFRESH MATERIALIZED VIEW mv_relatorio_vendas;

-- Atualizar concorrentemente (requer índice único)
CREATE UNIQUE INDEX mv_relatorio_idx ON mv_relatorio_vendas(cliente);
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_relatorio_vendas;

Extensões

-- uuid-ossp (UUID)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
SELECT uuid_generate_v4();
SELECT gen_random_uuid();  -- pgcrypto, mais simples

-- pg_trgm (similaridade de texto)
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_cliente_nome_trgm ON clientes USING GIN (nome gin_trgm_ops);
SELECT * FROM clientes WHERE nome % 'Joao';  -- similaridade

-- pgcrypto (hash, criptografia)
CREATE EXTENSION IF NOT EXISTS pgcrypto;
SELECT crypt('senha123', gen_salt('bf'));     -- bcrypt hash
SELECT digest('mensagem', 'sha256');          -- SHA-256

-- citext (case-insensitive text)
CREATE EXTENSION IF NOT EXISTS citext;
CREATE TABLE usuarios (email CITEXT UNIQUE);
INSERT INTO usuarios VALUES ('[email protected]');
SELECT * FROM usuarios WHERE email = '[email protected]';  -- encontra!

Lab: Sistema de Auditoria Completo

-- 04-auditoria.sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- Tabela de auditoria
CREATE TABLE auditoria (
    id              UUID DEFAULT gen_random_uuid() PRIMARY KEY,
    tabela          TEXT NOT NULL,
    operacao        TEXT NOT NULL,
    registro_id     UUID,
    dados_antigos   JSONB,
    dados_novos     JSONB,
    usuario         TEXT DEFAULT current_user,
    ip_origem       TEXT,
    criado_em       TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_auditoria_tabela ON auditoria(tabela, criado_em);

-- Função trigger genérica
CREATE OR REPLACE FUNCTION fn_auditoria()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO auditoria (tabela, operacao, registro_id, dados_antigos, dados_novos)
    VALUES (
        TG_TABLE_NAME,
        TG_OP,
        COALESCE(NEW.id, OLD.id),
        CASE WHEN TG_OP IN ('UPDATE', 'DELETE') THEN to_jsonb(OLD) ELSE NULL END,
        CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN to_jsonb(NEW) ELSE NULL END
    );
    RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;

-- Aplicar a todas as tabelas do schema vendas
CREATE TRIGGER trg_audit_clientes
    AFTER INSERT OR UPDATE OR DELETE ON clientes
    FOR EACH ROW EXECUTE FUNCTION fn_auditoria();

CREATE TRIGGER trg_audit_pedidos
    AFTER INSERT OR UPDATE OR DELETE ON pedidos
    FOR EACH ROW EXECUTE FUNCTION fn_auditoria();

CREATE TRIGGER trg_audit_produtos
    AFTER INSERT OR UPDATE OR DELETE ON produtos
    FOR EACH ROW EXECUTE FUNCTION fn_auditoria();

-- Testar
INSERT INTO clientes (nome, email, cpf) VALUES ('Teste', '[email protected]', '12345678901');
UPDATE clientes SET nome = 'Teste Atualizado' WHERE email = '[email protected]';
DELETE FROM clientes WHERE email = '[email protected]';

-- Ver auditoria
SELECT tabela, operacao, registro_id, usuario, criado_em
FROM auditoria
ORDER BY criado_em;

-- Relatório de alterações por período
SELECT
    tabela,
    operacao,
    COUNT(*) AS total,
    MIN(criado_em) AS primeira,
    MAX(criado_em) AS ultima
FROM auditoria
WHERE criado_em >= NOW() - INTERVAL '7 days'
GROUP BY tabela, operacao
ORDER BY total DESC;

PL/pgSQL usa $$ para delimitar corpo, DECLARE para variáveis, BEGIN/EXCEPTION/END para blocos. RETURN QUERY retorna conjuntos. RAISE para logging. Triggers exigem função que retorna TRIGGER. TG_OP detecta INSERT/UPDATE/DELETE. Materialized views aceleram relatórios pesados.