Tratamento de Erros no PostgreSQL

PostgreSQLBeginner
Pratique Agora

Introdução

Neste laboratório, você explorará técnicas de tratamento de erros no PostgreSQL usando sua linguagem procedural, PL/pgSQL. Você aprenderá a criar funções que podem lidar com erros potenciais de forma elegante, em vez de falhar. O laboratório abrange o envio de mensagens informativas, a captura de exceções específicas como "divisão por zero" e o registro de detalhes de erros em uma tabela de banco de dados para revisão posterior. Ao final, você entenderá como construir funções de banco de dados mais robustas e confiáveis.

Este é um Laboratório Guiado, que fornece instruções passo a passo para ajudá-lo a aprender e praticar. Siga as instruções cuidadosamente para completar cada etapa e obter experiência prática. Dados históricos mostram que este é um laboratório de nível iniciante com uma taxa de conclusão de 90%. Ele recebeu uma taxa de avaliação positiva de 100% dos alunos.

Emitindo Avisos (Notices) e Alertas (Warnings)

Nesta etapa, você aprenderá a usar a instrução RAISE em uma função PL/pgSQL. A instrução RAISE é útil para fornecer mensagens informativas, avisos ou saída de depuração sem interromper a execução da função.

Primeiro, conecte-se ao terminal interativo do PostgreSQL, psql, como o usuário postgres. Todos os comandos SQL subsequentes neste laboratório serão executados dentro deste terminal.

sudo -u postgres psql

Agora você verá o prompt do psql, que se parece com postgres=#.

Vamos criar uma função chamada greet que recebe um nome como entrada e emite um NOTICE. Notices são mensagens de baixa prioridade.

Execute o seguinte comando SQL no shell psql:

CREATE OR REPLACE FUNCTION greet(name TEXT)
RETURNS TEXT AS $$
BEGIN
  RAISE NOTICE 'Greeting function called with name: %', name;
  RETURN 'Hello, ' || name;
END;
$$ LANGUAGE plpgsql;

Esta função recebe um argumento name. A linha RAISE NOTICE '...', name; gera uma mensagem de aviso (notice). O % é um placeholder que é substituído pelo valor da variável name.

Agora, chame a função para vê-la em ação:

SELECT greet('LabEx');

A saída mostra tanto a mensagem de aviso (notice) quanto o valor de retorno da função:

NOTICE:  Greeting function called with name: LabEx
   greet
-----------
 Hello, LabEx
(1 row)

Você também pode emitir mensagens com um nível de severidade mais alto, como WARNING. Vamos modificar a função para emitir um aviso se o nome de entrada estiver vazio.

CREATE OR REPLACE FUNCTION greet(name TEXT)
RETURNS TEXT AS $$
BEGIN
  IF name IS NULL OR name = '' THEN
    RAISE WARNING 'Input name is empty or NULL.';
    RETURN 'Hello, stranger';
  ELSE
    RAISE NOTICE 'Greeting function called with name: %', name;
    RETURN 'Hello, ' || name;
  END IF;
END;
$$ LANGUAGE plpgsql;

Agora, chame a função com uma string vazia para acionar o aviso:

SELECT greet('');

Desta vez, você verá uma mensagem WARNING em vez de um NOTICE:

WARNING:  Input name is empty or NULL.
      greet
------------------
 Hello, stranger
(1 row)

Você usou com sucesso RAISE para gerar diferentes tipos de mensagens de dentro de uma função.

Disparando e Capturando uma Exceção

Nesta etapa, você aprenderá a lidar com erros de tempo de execução, conhecidos como exceções. Uma exceção não tratada encerrará imediatamente sua função. Primeiro, você criará uma função que falha e, em seguida, a modificará para capturar a exceção de forma elegante.

Vamos criar uma função simple_divide que realiza divisão inteira. Esta função tem uma falha potencial: ela travará se você tentar dividir por zero.

CREATE OR REPLACE FUNCTION simple_divide(numerator INTEGER, denominator INTEGER)
RETURNS INTEGER AS $$
BEGIN
  RETURN numerator / denominator;
END;
$$ LANGUAGE plpgsql;

Primeiro, chame a função com entradas válidas para confirmar que ela funciona corretamente:

SELECT simple_divide(10, 2);

A saída será o resultado da divisão:

 simple_divide
---------------
             5
(1 row)

Agora, chame a função com um denominador zero para acionar uma exceção:

SELECT simple_divide(10, 0);

Este comando falhará e retornará uma mensagem de erro. A execução da função é abortada.

ERROR:  division by zero
CONTEXT:  PL/pgSQL function simple_divide(integer,integer) line 3 at RETURN

Esta é uma exceção não tratada. Para evitar que a função trave, você pode usar um bloco EXCEPTION. Vamos criar uma nova função safe_divide que inclua tratamento de erros.

CREATE OR REPLACE FUNCTION safe_divide(numerator INTEGER, denominator INTEGER)
RETURNS INTEGER AS $$
BEGIN
  RETURN numerator / denominator;
EXCEPTION
  WHEN division_by_zero THEN
    RAISE NOTICE 'Error: Cannot divide by zero.';
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

Veja como o bloco EXCEPTION funciona:

  • BEGIN...END;: Isso define um bloco de código.
  • EXCEPTION: Esta palavra-chave inicia a seção de tratamento de exceções.
  • WHEN division_by_zero THEN: Isso especifica que o código a seguir deve ser executado apenas quando ocorrer um erro division_by_zero.
  • O código dentro do manipulador emite um aviso (notice) e retorna NULL em vez de travar.

Agora, chame a nova função safe_divide com um denominador zero:

SELECT safe_divide(10, 0);

Desta vez, a função não trava. Ela captura a exceção, exibe seu aviso personalizado e retorna NULL.

NOTICE:  Error: Cannot divide by zero.
 safe_divide
-------------

(1 row)

Você capturou com sucesso uma exceção específica e controlou a saída da função.

Registrando Erros em uma Tabela

Capturar exceções é bom, mas para auditoria e depuração, geralmente é melhor registrar erros em um local persistente, como uma tabela. Nesta etapa, você modificará a função safe_divide para gravar detalhes do erro na tabela error_log que foi criada durante a configuração do laboratório.

Primeiro, vamos examinar a estrutura da tabela error_log usando o comando \d no psql.

\d error_log

Você verá as colunas da tabela, que incluem um id auto-incrementável, um timestamp, a message de erro e o function_name.

                                              Table "public.error_log"
    Column     |            Type             | Collation | Nullable |                     Default
---------------+-----------------------------+-----------+----------+----------------------------------------------------
 id            | integer                     |           | not null | nextval('error_log_id_seq'::regclass)
 timestamp     | timestamp without time zone |           |          | (now() AT TIME ZONE 'utc'::text)
 message       | text                        |           |          |
 function_name | text                        |           |          |
Indexes:
    "error_log_pkey" PRIMARY KEY, btree (id)

Agora, vamos modificar a função safe_divide. No bloco EXCEPTION, você adicionará uma instrução INSERT para registrar o erro antes de retornar.

CREATE OR REPLACE FUNCTION safe_divide(numerator INTEGER, denominator INTEGER)
RETURNS INTEGER AS $$
BEGIN
  RETURN numerator / denominator;
EXCEPTION
  WHEN division_by_zero THEN
    INSERT INTO error_log (message, function_name)
    VALUES ('Division by zero occurred!', 'safe_divide');

    RAISE NOTICE 'Error: Cannot divide by zero. Details logged.';
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

Agora, chame a função novamente para acionar o erro e a nova lógica de registro:

SELECT safe_divide(10, 0);

A função retornará NULL como antes, mas também realizou uma escrita no banco de dados. Para verificar isso, consulte a tabela error_log para ver a nova entrada.

SELECT function_name, message FROM error_log;

A saída deve mostrar o erro registrado:

 function_name |          message
---------------+----------------------------
 safe_divide   | Division by zero occurred!
(1 row)

Isso confirma que sua lógica de tratamento de erros agora é capaz de persistir informações de erro para análise posterior.

Tratando Exceções Gerais com OTHERS

Até agora, você lidou com um erro muito específico: division_by_zero. Mas e quanto a outros erros inesperados? PL/pgSQL fornece um manipulador de exceção de propósito geral OTHERS para capturar qualquer erro que não tenha sido capturado por uma cláusula WHEN mais específica.

Nesta etapa, você aprimorará a função safe_divide para lidar com qualquer exceção possível. Você também usará duas variáveis especiais, SQLSTATE (o código de erro SQL) e SQLERRM (a mensagem de erro SQL), para registrar informações mais detalhadas.

Vamos modificar a função safe_divide mais uma vez.

CREATE OR REPLACE FUNCTION safe_divide(numerator INTEGER, denominator INTEGER)
RETURNS INTEGER AS $$
BEGIN
  RETURN numerator / denominator;
EXCEPTION
  WHEN division_by_zero THEN
    INSERT INTO error_log (message, function_name)
    VALUES ('Division by zero occurred!', 'safe_divide');

    RAISE NOTICE 'Error: Cannot divide by zero. Details logged.';
    RETURN NULL;

  WHEN OTHERS THEN
    INSERT INTO error_log (message, function_name)
    VALUES ('An unexpected error occurred: ' || SQLSTATE || ' - ' || SQLERRM, 'safe_divide');

    RAISE NOTICE 'An unexpected error occurred. Details logged.';
    RETURN -1;
END;
$$ LANGUAGE plpgsql;

O novo bloco WHEN OTHERS capturará qualquer outro erro. Ele constrói uma mensagem detalhada usando as variáveis SQLSTATE e SQLERRM e a insere no log. Em seguida, retorna -1 para sinalizar que ocorreu um erro genérico.

Embora nossa função atual só possa acionar um erro division_by_zero, essa estrutura a torna muito mais robusta contra modificações futuras ou entradas inesperadas. Por exemplo, se os tipos de entrada fossem alterados para um tipo que pudesse causar overflow, o bloco OTHERS capturaria o erro numeric_value_out_of_range.

Vamos testar o caminho division_by_zero novamente para garantir que ele ainda funcione como esperado. Primeiro, limpe a tabela de log para um teste limpo.

TRUNCATE error_log;

Agora, chame a função:

SELECT safe_divide(10, 0);

Finalmente, verifique o log. Ele deve conter a mensagem específica "Division by zero", confirmando que o primeiro bloco WHEN foi escolhido corretamente.

SELECT message FROM error_log;
          message
----------------------------
 Division by zero occurred!
(1 row)

Você agora construiu uma função resiliente que lida tanto com erros específicos e antecipados quanto com erros gerais e inesperados.

Resumo

Neste laboratório, você aprendeu os fundamentos do tratamento de erros em PL/pgSQL do PostgreSQL. Você começou usando a instrução RAISE para emitir mensagens NOTICE e WARNING para fins de depuração e informativos. Em seguida, progrediu para o tratamento de exceções em tempo de execução, criando uma função que poderia falhar e, em seguida, implementando um bloco BEGIN...EXCEPTION...END para capturar um erro específico de division_by_zero. Com base nisso, você aprimorou a função para registrar detalhes do erro em uma tabela de banco de dados, fornecendo um registro persistente para auditoria. Finalmente, você tornou a função mais robusta adicionando um manipulador geral WHEN OTHERS para capturar quaisquer erros inesperados, garantindo que sua função possa falhar graciosamente sob várias condições.