Manejo de Errores en PostgreSQL

PostgreSQLBeginner
Practicar Ahora

Introducción

En este laboratorio, explorará técnicas de manejo de errores en PostgreSQL utilizando su lenguaje procedural, PL/pgSQL. Aprenderá a crear funciones que puedan manejar errores potenciales de manera elegante en lugar de fallar. El laboratorio cubre cómo generar mensajes informativos, capturar excepciones específicas como "división por cero" y registrar detalles de errores en una tabla de base de datos para su posterior revisión. Al final, comprenderá cómo construir funciones de base de datos más robustas y confiables.

Este es un Laboratorio Guiado, que proporciona instrucciones paso a paso para ayudarle a aprender y practicar. Siga las instrucciones cuidadosamente para completar cada paso y obtener experiencia práctica. Los datos históricos muestran que este es un laboratorio de nivel principiante con una tasa de finalización del 90%. Ha recibido una tasa de revisión positiva del 100% por parte de los estudiantes.

Emitir Notificaciones y Advertencias

En este paso, aprenderá a utilizar la sentencia RAISE en una función PL/pgSQL. La sentencia RAISE es útil para proporcionar mensajes informativos, advertencias o salida de depuración sin detener la ejecución de la función.

Primero, conéctese a la terminal interactiva de PostgreSQL, psql, como el usuario postgres. Todos los comandos SQL subsiguientes en este laboratorio se ejecutarán dentro de esta terminal.

sudo -u postgres psql

Ahora verá el prompt de psql, que se parece a postgres=#.

Creemos una función llamada greet que toma un nombre como entrada y genera un NOTICE. Los NOTICE son mensajes de baja prioridad.

Ejecute el siguiente comando SQL en el shell de 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 función toma un argumento name. La línea RAISE NOTICE '...', name; genera un mensaje de aviso. El % es un marcador de posición que se reemplaza por el valor de la variable name.

Ahora, llame a la función para verla en acción:

SELECT greet('LabEx');

La salida muestra tanto el mensaje de aviso como el valor de retorno de la función:

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

También puede generar mensajes con un nivel de severidad más alto, como WARNING. Modifiquemos la función para generar una advertencia si el nombre de entrada está vacío.

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;

Ahora, llame a la función con una cadena vacía para activar la advertencia:

SELECT greet('');

Esta vez, verá un mensaje WARNING en lugar de un NOTICE:

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

Ha utilizado RAISE con éxito para generar diferentes tipos de mensajes desde dentro de una función.

Desencadenar y Capturar una Excepción

En este paso, aprenderá a manejar errores en tiempo de ejecución, conocidos como excepciones. Una excepción no manejada terminará inmediatamente su función. Primero creará una función que falla y luego la modificará para capturar la excepción de manera elegante.

Creemos una función simple_divide que realiza una división entera. Esta función tiene un defecto potencial: fallará si intenta dividir por cero.

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

Primero, llame a la función con entradas válidas para confirmar que funciona correctamente:

SELECT simple_divide(10, 2);

La salida será el resultado de la división:

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

Ahora, llame a la función con un denominador cero para activar una excepción:

SELECT simple_divide(10, 0);

Este comando fallará y devolverá un mensaje de error. La ejecución de la función se aborta.

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

Esta es una excepción no manejada. Para evitar que la función falle, puede usar un bloque EXCEPTION. Creemos una nueva función safe_divide que incluya el manejo de errores.

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;

Así es como funciona el bloque EXCEPTION:

  • BEGIN...END;: Esto define un bloque de código.
  • EXCEPTION: Esta palabra clave inicia la sección de manejo de excepciones.
  • WHEN division_by_zero THEN: Esto especifica que el código siguiente solo se ejecutará cuando ocurra un error de division_by_zero.
  • El código dentro del manejador genera un aviso y devuelve NULL en lugar de fallar.

Ahora, llame a la nueva función safe_divide con un denominador cero:

SELECT safe_divide(10, 0);

Esta vez, la función no falla. Captura la excepción, muestra su aviso personalizado y devuelve NULL.

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

(1 row)

Ha capturado con éxito una excepción específica y controlado la salida de la función.

Registrar Errores en una Tabla

Capturar excepciones es bueno, pero para auditoría y depuración, a menudo es mejor registrar los errores en una ubicación persistente, como una tabla. En este paso, modificará la función safe_divide para escribir los detalles del error en la tabla error_log que se creó durante la configuración del laboratorio.

Primero, examinemos la estructura de la tabla error_log usando el comando \d en psql.

\d error_log

Verá las columnas de la tabla, que incluyen un id autoincremental, una timestamp, el message del error y el 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)

Ahora, modifiquemos la función safe_divide. En el bloque EXCEPTION, agregará una sentencia INSERT para registrar el error antes de devolver el valor.

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;

Ahora, llame a la función nuevamente para activar el error y la nueva lógica de registro:

SELECT safe_divide(10, 0);

La función devolverá NULL como antes, pero también habrá realizado una escritura en la base de datos. Para verificar esto, consulte la tabla error_log para ver la nueva entrada.

SELECT function_name, message FROM error_log;

La salida debería mostrar el error registrado:

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

Esto confirma que su lógica de manejo de errores ahora es capaz de persistir la información de errores para un análisis posterior.

Manejar Excepciones Generales con OTHERS

Hasta ahora, ha manejado un error muy específico: division_by_zero. Pero, ¿qué pasa con otros errores inesperados? PL/pgSQL proporciona un manejador de excepciones de propósito general OTHERS para capturar cualquier error que no haya sido capturado por una cláusula WHEN más específica.

En este paso, mejorará la función safe_divide para manejar cualquier excepción posible. También utilizará dos variables especiales, SQLSTATE (el código de error SQL) y SQLERRM (el mensaje de error SQL), para registrar información más detallada.

Modifiquemos la función safe_divide por última 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;

El nuevo bloque WHEN OTHERS capturará cualquier otro error. Construye un mensaje detallado utilizando las variables SQLSTATE y SQLERRM y lo inserta en el registro. Luego devuelve -1 para indicar que ocurrió un error genérico.

Si bien nuestra función actual solo puede generar un error de division_by_zero, esta estructura la hace mucho más robusta ante futuras modificaciones o entradas inesperadas. Por ejemplo, si los tipos de entrada se cambiaran a un tipo que pudiera desbordarse, el bloque OTHERS capturaría el error numeric_value_out_of_range.

Probemos nuevamente la ruta de division_by_zero para asegurarnos de que todavía funciona como se esperaba. Primero, limpie la tabla de registro para una prueba limpia.

TRUNCATE error_log;

Ahora, llame a la función:

SELECT safe_divide(10, 0);

Finalmente, verifique el registro. Debería contener el mensaje específico "Division by zero", lo que confirma que el primer bloque WHEN fue elegido correctamente.

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

Ahora ha creado una función resiliente que maneja tanto errores específicos y anticipados como errores generales e inesperados.

Resumen

En este laboratorio, ha aprendido los fundamentos del manejo de errores en PL/pgSQL de PostgreSQL. Comenzó utilizando la sentencia RAISE para emitir mensajes NOTICE y WARNING con fines de depuración e informativos. Luego, pasó a manejar excepciones en tiempo de ejecución creando una función que podía fallar e implementando un bloque BEGIN...EXCEPTION...END para capturar un error específico de division_by_zero. Basándose en esto, mejoró la función para registrar los detalles del error en una tabla de base de datos, proporcionando un registro persistente para auditoría. Finalmente, hizo la función más robusta agregando un manejador general WHEN OTHERS para capturar cualquier error inesperado, asegurando que su función pueda fallar de manera controlada bajo diversas condiciones.