Qualquer desenvolvedor e/ou usuário de bando de dados, mesmo os que tem pouca experiência com os mesmos, sabe que o SQL é sua linguagem básica e fundamental e que o mesmo é regido por comandos e por algumas estruturas básicas de lógica de programação, tais quais as que vemos em linguagens famosas, como o Java ou o Delphi. Trabalhar com bancos de dados é uma tarefa relativamente simples, mas exige algumas capacidades como memorização (dos comandos, das estruturas, etc), associação (para entender como funcionam relacionamentos, formas normais, etc) e raciocínio lógico (para criar estruturas lógicas flexíveis e adaptáveis aos diferentes modelos de negócio). E nesta última capacidade que iremos trabalhar nesse artigo.

Trataremos de entender aqui não somente como o banco de dados lida com certas estruturas de decisão, de iteração, etc., mas também analisar como funcionam os agrupamentos, a criação e manutenção de blocos de código em instruções, loops, etc. tudo encapsulado nas famosas estruturas de Stored Procedures e Triggers. E para essa experiência estaremos usando o banco de dados Firebird, por razões de maturidade e documentação da tecnologia (vide seção Links para encontrar a documentação oficial do Firebird).

A linguagem das Stored Procedures e Triggers, por sua vez, é uma linguagem criada para “ser executada em um banco de dados”, literalmente. Por esta razão, a sua atuação é limitada às operações de base de dados e às funções necessárias; já a PSQL é, em si, contudo, uma linguagem poderosa, e oferece mais funcionalidades do que você pode usar. Você pode encontrar uma gama completa de palavras-chave e funções disponíveis para uso em procedures e triggers na seção Links, e para não estender demais o artigo, vamos nos ater às funções e recursos mais básicos como forma de introdução.

Stored Procedures

O Firebird utiliza as stored procedures (ou procedimentos) como ambiente de programação para a integração de processos ativos no banco de dados.

Existem dois tipos de stored procedures: executável e selecionável. Uma “procedure executável” retorna não mais do que um conjunto de variáveis. Já uma “procedure de seleção” pode, usando a palavra-chave SUSPEND, recuperar variáveis, um conjunto de dados por vez. Se uma instrução EXECUTE PROCEDURE contém um SUSPEND, então ele terá o mesmo efeito que a instrução EXIT, por exemplo, encerrando a execução. Este uso é permitido pelo Banco, mas não é recomendável.

A sintaxe para declarar os dois tipos de procedures é a mesma, mas existem duas maneiras de chamá-la:

  1. Uma procedure pode agir como um procedimento funcional em outra linguagem, da mesma forma que você pode executá-la e ela lhe dar uma resposta ou não. Veja o código a seguir para tal (considere que limpar_clientes é o nome da nossa procedure e ela já está criada):
    execute procedure limpar_clientes
  2. A outra maneira mais básica de fazer isso é criar uma procedure como se você estivesse fazendo uma consulta básica a uma tabela:
    select * from limpar_clientes

Dessa forma, você pode recuperar os dados retornados como uma subtabela em forma de resposta.

Para exemplificar melhor, imaginemos um cenário onde temos quatro entidades distintas no banco de dados, descritas conforme a Figura 1. Nela, podemos observar as seguintes tabelas:

  • Cliente: Que guardará as informações dos clientes cadastrados no sistema;
  • Venda: Terá relação direta com os clientes e salvará todas as informações relacionadas a todas as vendas de cada um;
  • Item: Constitui cada um dos itens de venda e tem um relacionamento muitos-para-muitos com a tabela de Venda. A tabela Detalhe_Venda, por sua vez, faz o trabalho de interligar ambas.

Modelo Entidade-Relacionamento
para exemplo de teste

Figura 1. Modelo Entidade-Relacionamento para exemplo de teste.

Suponha também que queremos trazer uma listagem de todas as compras feitas por um cliente x dentro de um dado período de tempo. Nesse caso, em vez de fazer todo o processamento via linguagem de programação, podemos usar um stored procedure para simplificar as coisas. Veja na Listagem 1 como ficaria essa procedure encaixada nesse cenário específico.

Listagem 1. Stored Procedure para listagem de vendas/clientes em certo período.


  CREATE OR ALTER PROCEDURE CLIENTE_PRODUTO_VENDAS (
      data_inicio DATE,
      data_fim DATE)
  RETURNS (
      cliente d_nome,
      item d_nome,
      qtde d_qtde)
  AS
  DECLARE VARIABLE cliente_id d_id;
  DECLARE VARIABLE item_id d_id;
  BEGIN
    /* Texto da Procedure */
    FOR SELECT id, nome
    FROM cliente
    ORDER BY nome
    INTO :cliente_id, :cliente
    DO
      BEGIN
      FOR SELECT id, nome
      FROM item
      ORDER BY nome
      INTO :item_id, :item
      DO
        BEGIN
        SELECT COALESCE(SUM(dv.qtde), 0)
        FROM detalhe_venda dv LEFT JOIN venda v ON dv.venda_id = v.id
        WHERE v.status <> 'CANCELAR' AND v.cliente_id = :cliente_id AND dv.item_id = :item_id AND v.data_venda BETWEEN :data_inicio AND :data_fim
        INTO :qtde;
        SUSPEND;
        END
      END
  END

Obter um mesmo resultado a partir de simples consultas SELECT seria muito mais trabalhoso nesse tipo de situação. Logo na primeira linha observamos a sintaxe muito semelhante à de outros BDs no que se refere à criação/alteração de qualquer tipo de estrutura DDL. A palavra reservada RETURNS serve para definir o que será retornado dentro da procedure, bem como os nomes e alias de cada coluna do resultado final. Já DECLARE VARIABLE se encarrega de inicializar duas variáveis que serão usadas durante a execução do procedimento.

Veja que, após a criação desse código inicial, nós já temos o uso do convencional BEGIN-END, muito semelhante à que usamos em linguagens estruturadas, como o Pascal, e serve para delimitar o início e fim de cada bloco de instrução. Após essa estrutura, nós sempre devemos informar qual a consulta (SELECT SQL) que retornará os dados a serem processados. A partir daí, fazemos uma consulta comum, sem nenhuma novidade, exceto pela cláusula INTO que enxerta os valores da consulta exatamente dentro das variáveis que criamos.

Como o nosso exemplo envolve relacionamentos mais complexos, perceba que mais de um BEGIN-END precisou ser criado, porém a lógica de associação é a mesma do anterior. A função COALESCE() usada na segunda estrutura, serve para verificar qual do primeiro dos parâmetros passados é não-nulo. Ela é muito usada para quando não queremos nunca que uma listagem retorne um valor null em seus resultados. Finalmente, temos os JOINS finais referentes aos relacionamentos entre as entidades que serão abstraídos aqui por não serem o foco do artigo.

Caso você tenha populado a base com valores de teste, a execução dessa procedure resultaria em algo como:


  CLIENTE    ITEM        QTDE 
  ABC        ITEM 111    0   
  ABC       ITEM 222    10  
  DEF        ITEM 555    3

Triggers

As triggers (ou gatilhos, em inglês) são estruturas de código estrutural que são automaticamente executadas em resposta a certos eventos sobre uma tabela ou view particular no banco de dados. Ela é usada, na grande maioria das vezes, para manter a integridade das informações no banco. Por exemplo, quando um novo registro é adicionado a uma tabela de clientes, nós podemos definir uma trigger que automaticamente também registre certas ações em tabelas relacionadas, como salários, RH, ou batimento de ponto, etc.

Essas estruturas nunca podem ser chamadas diretamente, mas somente através de comandos DML, como INSERT, UPDATE ou DELETE. Dentre as vantagens que elas trazem ao seu projeto, podemos citar:

  • Execução automática de condições e limitações para conteúdo;
  • Redução de manutenção nas aplicações;
  • Você pode implementar um mecanismo de log de rastreio de modificações para suas tabelas;
  • Generators (geradores) podem ser chamados automaticamente e seus valores associados aos campos;
  • Conversão automática da caixa alta dos campos para habilitar buscas case-insensitive (que não distinguem maiúsculas de minúsculas).

Para exemplificar, suponhamos que sempre que um dos nossos clientes do modelo representado na Figura 1 sofrer qualquer alteração (UPDATE) em seus registros de salário, nós salvemos também automaticamente essa alteração em uma tabela Historico_Salario para consultas futuras. Para isso, precisaríamos criar a referida tabela com alguns campos básicos como data, id_cliente, salario_antigo, salario_novo, etc. Veja na Listagem 2 um exemplo de como ficaria a nossa trigger de histórico de atualizações nos salários dos clientes.

Listagem 2. Trigger exemplo de registro de alterações nos salários.

CREATE TRIGGER SALARIO_HISTORICO_ALTERACAO FOR CLIENTE
     AFTER UPDATE
  AS
  BEGIN
     IF (OLD.SALARIO <> NEW.SALARIO) THEN
        INSERT INTO HISTORICO_SALARIO
           (CLIENTE_ID, DATA_ALT, SALARIO_ANTIGO, SALARIO_NOVO)
           VALUES (
              OLD.CLIENTE_ID, 'now', OLD.SALARIO, NEW.SALARIO
           );
  END;

As criações das tabelas e inserções de massa teste serão pulados por não fazerem parte do escopo do artigo. Perceba que a estrutura da trigger é razoavelmente mais simples do que as stored procedures. Nela, podemos ver a sintaxe de criação que também pode levar o UPDATE, e logo após a palavra reservada AFTER para dizer quando esse gatilho será executado (após qualquer UPDATE na tabela Cliente, no nosso caso).

Depois disso, o resto é puramente lógica de programação. Testamos se o salário antigo é diferente do novo e, em caso positivo, inserimos o registro no banco com os dados do próprio UPDATE original. Perceba que nesse tipo de estrutura, nós também fazemos isso do definidor de início-fim dos blocos: BEGIN-END. Os comandos OLD e NEW, precedidos do nome da coluna, servem para referenciar o dado sendo atualizado no momento e está disponível para que você possa usá-lo.

Os if-else encadeados também funcionam normalmente nesse tipo de modelo. Basta que ao final do IF você inicie a próxima sentença com uma cláusula ELSE IF precedida da próxima condição.

Comentários

Os comentários, assim como nas queries SQL básicas, também são permitidos nas procedures e triggers, cuja sintaxe depende de cada banco usado. Em se tratando do Firebird, os comentários podem ser apresentados de duas formas:

  • Através do uso dos operadores /* ... */ para comentários de mais de uma linha, como mostra o código a seguir:
    /*Apresentando a Procedure principal
                  que ira auto incrementar*/  
  • Ou através do uso de dois traços (--) para comentários de uma linha só.

Declarações Condicionais

IF...THEN...ELSE

Assim como nas linguagens de programação, as estruturas condicionais IF...THEN...ELSE servem para estabelecer condições no fluxo de execução do código. Se a expressão de teste de uma instrução IF for NULL, a cláusula, então, é ignorada e a cláusula ELSE, se houver, é executada. Em outras palavras, NULL e falso tem o mesmo efeito no presente contexto.

Por essa razão, podemos ter desde a simples execução de um teste condicional com operadores de “maior que” ou “menor ou igual que”, até estruturas de comparação entre variáveis criadas pela procedure ou trigger em questão. Vejamos o código da Listagem 3 para um exemplo disso.

Listagem 3. Exemplo de condições no Firebird

if (a > 18) then
         resultado = ‘Maior de idade’;
    else
         resultado = ‘Menor de idade’;
     
    -- ou...
     
    if (varA = varB) then
      minhaVar = 'Iguais';
    else
      minhaVar = 'Não iguais';

Se uma das duas variáveis varA ou varB forem nulas, então o valor da variável minhaVar continuará sendo “Não iguais”, o que comprova nossa afirmativa anterior. Você pode encontrar uma lista completa dos operadores e mais exemplos condicionais no site oficial do Firebird.

IF com AND/OR

É muito comum nesse tipo de estrutura lidarmos com situações onde é preciso fazer mais de uma verificação ao mesmo tempo. Além dos ifs encadeados que vimos (em conjunto com as cláusulas else) também é possível utilizar as estruturas de and/or encadeadas em uma mesma condição.

Considerando o exemplo ilustrado na Listagem 2, se quiséssemos, por exemplo, validar se o salário é maior que um salário mínimo e menor que o teto da profissão do emprego, teríamos uma condição IF mais ou menos assim:

IF (OLD.SALARIO <> NEW.SALARIO AND (OLD.SALARIO < 870 OR OLD.SALARIO < OLD.TETO_SALARIAL)) THEN

Perceba que fazemos uso simples de operadores já conhecidos pelo SQL em cláusulas WHERE, porém aplicados ao escopo de triggers e procedures. Lembre-se: essas estruturas trabalham tal como as linguagens de programação estruturadas.

Além disso, temos diversas outras variações que podem ser utilizadas com o bloco IF-THEN-ELSE. Veja algumas delas:

  • Valor LIKE valor: O valor à direita pode incluir um ou mais curingas. Use % para zero ou mais caracteres e _ para um caractere;
  • Valor IN (valor1, valor2, valor3, &): Membro de uma lista de valores;
  • Valor EXISTS (subquery): Verdadeiro se o valor combinar com um dos valores retornados pela subquery;
  • Valor ANY (subquery): Verdadeiro se o valor combinar com qualquer das linhas retornadas pela subquery;
  • Valor ALL (subquery): Verdadeiro se o valor combinar com todas as linhas retornadas pela subquery;
  • Valor IS NULL: Verdadeiro se o valor for nulo;
  • Valor IS NOT NULL: Verdadeiro se o valor não for nulo;
  • Valor CONTAINING valor: Busca de substring sem diferenciar maiúsculas e minúsculas;
  • Valor STARTING WITH valor: Verdadeiro se o valor a esquerda iniciar com o valor a direita. Diferencia maiúsculas e minúsculas.

CASE

O Firebird introduziu a construção CASE na versão 1.5, com duas variantes sintáticas. O primeiro é chamado de “CASE Simples” (Simple Case) e age diretamente nos dados finais de cada coluna da consulta. Nesse tipo de situação, só podemos ter sempre um resultado válido, podendo fazer uso também da cláusula ELSE para incutir um retorno padrão quando nenhum for válido. Suponha que, ainda no exemplo dos clientes, queiramos listar os mesmos identificando qual o seu sexo (coluna essa que teria de ser adicionada à tabela) e definindo um valor padrão para o caso deste dada não estar preenchido na base. Veja na Listagem 4 o código para isso.

Listagem 4. Código de exemplo do uso do CASE Simples.


  SELECT nome,
         saldo,
         endereco,
         CASE upper(sexo)
           WHEN 'M' then 'Masculio'
           WHEN 'F' then 'Feminino'
           ELSE 'Não informado'
  FROM Cliente

Perceba que a estrutura da consulta é básica, porém logo nas primeiras linhas já identificamos o uso do operador CASE. Veja como ele se equipara ao operador switch-case das linguagens OO atuais. Cada condição é definida através do operador WHEN e a condição padrão é representada pela cláusula ELSE.

A segunda variante, também conhecida como “CASE de pesquisa” (Searched CASE) trabalha com o mesmo conceito da primeira, porém se atendo a testar condições, em vez de valores fechados. Nesse tipo de estrutura, nós criamos cada condição com uma subcondição que irá retornar verdadeiro ou falso, e de acordo com esse dado, retornamos o valor de cada um. Veja na Listagem 5 um exemplo de aplicação disso, onde temos uma consulta que retorna os clientes com base na sua renda.

Listagem 5. Exemplo de uso do CASE de pesquisa.


  SELECT CASE
     WHEN (salario < 850) THEN 'Baixa Renda' 
     WHEN (salario >= 850 AND salario < 3000) THEN 'Classe Média Baixa' 
     WHEN (salario >= 3000 AND salario < 8000) THEN 'Classe Média Alta' 
     WHEN (salario >= 8000) THEN 'Rico' 
    END AS renda, count (*) as TOTAL_CLIENTES
  from Cliente;

No mesmo exemplo é possível ver a facilidade no uso de tais condições, que podem se estender a cláusulas como IN, LIKE, operadores relacionais, etc.

Estruturas de Repetição

WHILE

A estrutura também se equipara às das linguagens de programação e funciona basicamente recebendo a condição e executando o bloco de código caso a mesma seja verdadeira. Ao avaliar a condição de um loop while, o NULL tem o mesmo efeito que em uma instrução IF: se a condição resolve para NULL, então a execução não entra novamente no laço – da mesma forma como se fosse falsa.

Veja na Listagem 6 como seria o código da nossa trigger caso desejássemos fazer uma verificação um determinado número de vezes.

Listagem 6. Exemplo de uso do WHILE em triggers.

CREATE TRIGGER SALARIO_HISTORICO_ALTERACAO FOR CLIENTE
     AFTER UPDATE
  AS
  DECLARE VARIABLE cont INTEGER;
  BEGIN
     WHILE (cont < 10) DO
     BEGIN
        INSERT INTO HISTORICO_SALARIO
           (CLIENTE_ID, DATA_ALT, SALARIO_ANTIGO, SALARIO_NOVO)
           VALUES (
              OLD.CLIENTE_ID, 'now', OLD.SALARIO, NEW.SALARIO
           );
     END;
  END;

A sintaxe basicamente se restringe ao uso de WHILE-DO nessa ordem com a condição no meio. Observe também o uso que fazemos dos operadores BEGIN-END para essa estrutura. O resultado seria o salvamento da mesma informação dez vezes na tabela de histórico.

FOR

Para evitar qualquer confusão possível, vamos enfatizar aqui que loops FOR no Firebird PSQL tem uma função totalmente diferente dos loops WHILE, ou loops em linguagens de programação em geral. Os loops FOR do Firebird têm a seguinte forma sintáxica:

for <declaraçao-do-select> into <lista-var> do <bloqueio-codigo>

Eles vão continuar a execução do bloco de código até que todas as linhas do conjunto de resultados forem recuperadas, a menos que ocorra uma exceção ou uma pausa, termine ou a instrução EXIT seja encontrada. O fato de encontrarmos um NULL durante a execução, ou até mesmo das filas serem preenchidas com valores nulos, não irá encerrar o ciclo.

Vejamos na Listagem 7 um exemplo real dessa estrutura no mesmo modelo de clientes que criamos antes. Nesse exemplo, nós iteramos sobre todos os dados da tabela de Cliente e verificamos se algum dos salários é maior que um valor muito alto, caso positivo, deletamos o registro da base.

Listagem 7. Exemplo de uso do for no Firebird.

BEGIN
    FOR SELECT salario FROM Cliente INTO :salario AS CURSOR tcur DO
    BEGIN
      IF (salario > 200000000)
        THEN DELETE FROM Cliente WHERE CURRENT OF tcur;
      ELSE SUSPEND;
    END
  END

Perceba que a deleção é feita, sobretudo, com base na cláusula CURRENT que nos traz exatamente a linha corrente do cursor.

Contudo, existem muitas outras funcionalidades, recursos e comandos para se trabalhar com Stored Procedures e Triggers no Firebird, e não se limitam apenas a um único artigo. Veja na seção Links alguns cursos bem completos sobre o assunto que podem te auxiliar a maximizar seus conhecimentos no assunto.

Bons estudos!

Links

Documentação oficial do Firebird
http://www.firebirdsql.org/?op=doc

Curso: Dominando o Firebird e InterBase
//www.devmedia.com.br/curso/administrando-firebird-e-interbase/16

Curso: Dominando o Firebird
//www.devmedia.com.br/curso/dominando-firebird/251