O que são triggers?
À medida que nossas capacidades de programação melhoram, acabamos exigindo um controle mais preciso e garantido sobre os dados - apenas inserir dados no banco de dados e torcer para que tudo dê certo já não é o bastante. Neste artigo será mostrado como a introdução de codificação de lógica no BD pode facilitar e melhorar a integridade dos dados. Isto será demonstrado mediante um gráfico de Gantt (ver Figura 1).
Usar transações e chaves estrangeiras são os primeiros bons passos para garantir integridade de dados, mas com alguns dos bancos de dados mais avançados, podemos ir além e passar lógica de programação ao próprio banco de dados, extraindo esta lógica do próprio código PHP por exemplo.
Recentemente, tive contato com o desenvolvimento do GForge, uma ferramenta de desenvolvimento/gerenciamento de projeto de software. Parte do que queríamos fazer era criar um “Gerente de Projeto" de primeira classe, semelhante ao MS Project ou a ferramentas do mesmo tipo, que constroem gráficos de Gantt utilizando a interface web via PHP.
Nos gráficos de Gantt, temos uma série de tarefas que podem ser restringidas por tarefas anteriores, como no exemplo mostrado na Figura 1. A conclusão da Tarefa B é dependente da Tarefa A, e a conclusão da Tarefa C é dependente da Tarefa B.
Figura 1. Gráfico de gantt.
Isto soa bastante fácil, mas, e se um usuário tentar inserir uma data de início (start date) na Tarefa B anterior à data de término da Tarefa A? Quando formos inserir ou atualizar a Tarefa B no banco de dados, poderíamos ter lógica PHP para examinar o mesmo e conferir a data de término da tarefa A. Esta lógica seria representada por algumas poucas linhas de código PHP.
Mas, e se precisarmos adiar a Tarefa A por mais uma semana? Agora necessitaremos de uma função de recursividade em PHP, que irá adiar as Tarefas B e C pelo espaço de tempo apropriado, e efetuar todas as alterações no BD. De repente, teremos muitas consultas e recursão em PHP e conseqüentemente um problema de desempenho, especialmente se tivermos uma aplicação web de alto tráfego.
A abordagem alternativa é passar estas restrições para triggers no banco de dados. Temos lidado com triggers que atualizam contadores e criam linhas em tabelas relacionadas, mas isto é o máximo que fizemos com PL/pgsql, a linguagem de programação do PostgreSQL.
Há dois tipos de triggers programáveis no pl/pgsql, os triggers "antes de" e “depois que”. Utilizamos o trigger "antes de" quando queremos modificar uma linha de dados antes de inseri-la no banco de dados. Isto se assemelha ao modo como queremos controlar as datas de início/término das tarefas. Se um usuário tentar inserir a Tarefa B com uma data de início anterior à data de término da Tarefa A, temos que impedi-lo e fazer a restauração para as datas originais.
O outro tipo de trigger é o "depois que", que utilizamos quando queremos executar alguma ação após a inserção de uma linha no banco de dados. Isto é semelhante a adiarmos as datas de término das Tarefas B e C caso aconteça um adiamento da data de término da Tarefa A.
Fica bem claro que estamos essencialmente obtendo recursão “de graça”. Se a Tarefa A for atualizada, o trigger dispara e atualiza a Tarefa B. O trigger para a Tarefa B será desligado, o que provoca a atualização da Tarefa C, e assim por diante, até que tudo tenha sido validado.
Exemplo do uso de triggers no PostgreSQL
Podemos começar com o trigger e a função "antes de" que valida as datas de início/término (ver Listagem 1).
CREATE OR REPLACE FUNCTION projtask_insert_depend () RETURNS OPAQUE AS '
DECLARE
dependon RECORD;
delta INTEGER;
BEGIN
--
-- FORÇAR lógica DE DATA DE INÍCIO/TÉRMINO
--
IF NEW.start_date >= NEW.end_date THEN
RAISE EXCEPTION ''START DATE CANNOT BE AFTER END DATE'';
END IF;
--
-- Garantir que estamos começando com ou após a data de término de tarefas
-- dependentes
--
FOR dependon IN SELECT * FROM project_dependon_vw
WHERE project_task_id=NEW.project_task_id LOOP
--
-- Verificar se a tarefa “dependon” termina depois que deveria iniciar
--
IF dependon.end_date > NEW.start_date THEN
delta := dependon.end_date-NEW.start_date;
RAISE NOTICE ''Bumping Back: % Delta: % '',
NEW.project_task_id,delta;
NEW.start_date := NEW.start_date+delta;
NEW.end_date := NEW.end_date+delta;
END IF;
END LOOP;
RETURN NEW;
END;
' LANGUAGE 'plpgsql';
Como podemos observar, temos lógica para levantar uma exceção se um usuário tenta designar uma data de início posterior à data de término. Também temos um loop FOR... IN que examina as tarefas dependentes e ajusta a data de início/término caso tentemos iniciar antes que a tarefa antecessora seja completada.
NEW é uma referência à linha em que este trigger está agindo. Se isto estivesse em um evento UDPATE, teríamos também acesso à linha OLD. Como podemos ver, podemos referenciar todos os campos em NEW, como NEW.start_date, e referenciar todas as linhas que estamos consultando como linhas "dependon".
Estas poucas linhas de código muito simples garantirão agora que os dados sempre estarão corretos. A seguir, o trigger o que ativará esta função:
CREATE TRIGGER projtask_insert_depend_trig
BEFORE INSERT OR UPDATE ON project_task
FOR EACH ROW EXECUTE PROCEDURE projtask_insert_depend();
Vejamos agora o processo mais confuso. Quando uma tarefa for atualizada, precisaremos verificar as tarefas dependentes em relação à mesma. Se a Tarefa B tem uma data de início igual à data de término, isto significa que restringimos a Tarefa B, e precisaremos retroceder as datas se houver adiamentos, ou adiantá-las, caso haja antecipações.
Porém, e se a Tarefa B também é dependente de outra tarefa, digamos a Tarefa D? Se tentarmos adiantar demais a Tarefa B poderíamos estar violando a restrição - estaríamos tentando iniciar a Tarefa B antes que a Tarefa D fosse completada.
Sem problemas! O trigger "antes de" irá nos proteger, haja o que houver. Se tentarmos adiantar demais a Tarefa B, o seu gatilho irá disparar e a restaurará! Estamos obtendo tudo isto “de graça” ao inserirmos algumas linhas de código dentro do próprio banco de dados. Toda linha se torna sua própria chefe. Tentar a execução desta manobra dentro do PHP seria no mínimo bastante complicado. A Listagem 2 apresenta a lógica inserida no banco para lidar com a atualização.
CREATE OR REPLACE FUNCTION projtask_update_depend () RETURNS OPAQUE AS '
DECLARE
dependent RECORD;
dependon RECORD;
delta INTEGER;
BEGIN
--
-- Verificar se as tarefas dependentes estão OK
-- Verificar se houve alteração da data de término
--
IF NEW.end_date > OLD.end_date THEN
--
-- Se a data de término foi antecipada, antecipar as tarefas dependentes
--
FOR dependent IN SELECT * FROM project_depend_vw
WHERE is_dependent_on_task_id=NEW.project_task_id LOOP
--
-- algumas tarefas dependentes podem não começar imediatamente
--
IF dependent.start_date > OLD.end_date THEN
IF dependent.start_date < NEW.end_date THEN
delta := NEW.end_date-dependent.start_date;
UPDATE project_task
SET start_date=start_date+delta,
end_date=end_date+delta
WHERE project_task_id=dependent.project_task_id;
END IF;
ELSE
IF dependent.start_date = OLD.end_date THEN
delta := NEW.end_date-OLD.end_date;
UPDATE project_task
SET start_date=start_date+delta,
end_date=end_date+delta
WHERE project_task_id=dependent.project_task_id;
END IF;
END IF;
END LOOP;
ELSIF NEW.end_date < OLD.end_date THEN
--
-- Se a data de término foi adiada, adiar as tarefas dependentes
--
FOR dependent IN SELECT * FROM project_depend_vw
WHERE is_dependent_on_task_id=NEW.project_task_id LOOP
IF dependent.start_date = OLD.end_date THEN
--
-- tarefas dependents foram restringidas, antecipar
--
delta := OLD.end_date-NEW.end_date;
UPDATE project_task
SET start_date=start_date-delta,
end_date=end_date-delta
WHERE project_task_id=dependent.project_task_id;
END IF;
END LOOP;
END IF;
--
-- PODEMOS INSERIR AQUI RASTREAMENTO PARA AUDITORIA DE ALTERACOES EM DATAS DE INICIO/TERMINO
--
RETURN NEW;
END;
' LANGUAGE 'plpgsql';
Vejamos agora o trigger que ativará a função. Este gatilho só precisa ser disparado nas "atualizações" do banco de dados, pois nenhuma outra tarefa pode ser dependente de uma nova tarefa a ser inserida.
CREATE TRIGGER projtask_update_depend_trig
AFTER UPDATE ON project_task
FOR EACH ROW EXECUTE PROCEDURE projtask_update_depend();
Aqui podemos cair em uma armadilha séria. E se um usuário final mal-intencionado tentar tornar a Tarefa A dependente da Tarefa C, criando uma dependência circular? Com toda esta lógica no banco de dados, poderíamos criar um loop infinito de triggers disparando e competindo entre si no banco de dados.
Não tentamos prevenir isto com lógica de PL/pgsql. Em lugar disso, criamos algumas linhas de código recursivas PHP para eliminar qualquer chance de dependências circulares (ver Listagem 3).