sábado, 1 de octubre de 2016

Manejo de Excepciones

Objetivos:
• Definir las excepciones de PL/SQL.
• Reconocer las excepciones no controladas(unhandled exceptions).
• Listar y utilizar los distintos manejadores de excepciones en PL/SQL.
• Atrampar errores no anticipados.
• Describir el efecto de propagación de excepciones en bloques anidados.
• Personalizar los mensajes en excepciones de PL/SQL.


NOTA: Usamos como ejemplo la Base de Datos: ORCL, la cual viene por defecto en cualquier versión de ORACLE.
____________________________________________________________________________________
Que es una Excepción?
En programación, una excepción es la indicación de un problema que ocurre durante la ejecución de un programa. Sin embargo, la palabra excepción se refiere a que este problema ocurre con poca frecuencia generalmente cuando existe algún dato o instrucción que no se apega al funcionamiento del programa por lo que se produce un error.[1]

Ejemplo:
DECLARE
    v_lname VARCHAR2(15);
BEGIN
    SELECT last_name INTO v_lname 
    FROM employees
    WHERE first_name = 'John'
    DBMS_OUTPUT.PUT_LINE ('El Apellido de John es:' ||v_lname);
END;
---OUTPUT:
Considere el anterior ejemplo. No hay errores de sintaxis en el código, lo que significa que el bloque debería ser capaz de ejecutar con éxito. La sentencia SELECT trata de recuperar el apellido de John,
sin embargo, al ejecutar el código se presenta el siguiente error:

Se espera recuperar sólo una fila; Sin embargo, el SELECT recupera múltiples filas. Tales errores que se producen en tiempo de ejecución se llaman excepciones. Cuando se produce una excepción, el bloque PL/SQL se termina. Puede manejar este tipo de excepciones en su bloque PL/SQL.

Manejando Excepciones.
Ya conocemos cómo escribir bloques PL/SQL con una parte declarativa (que comienza con la palabra clave DECLARE) y una sección ejecutable (que empieza con BEGIN  y termina con END).

Para el manejo de excepciones, se incluye otra sección(opcional) llamada: Sección de excepciones.
• Esta sección comienza con la palabra clave EXCEPTION.
• Si está presente, esta debe ser la última sección del bloque PL/SQL.

Ejemplo:
SET SERVEROUTPUT ON
DECLARE
    v_lname VARCHAR2(15);
BEGIN
    SELECT last_name INTO v_lname 
    FROM employees
    WHERE first_name = 'John'
    DBMS_OUTPUT.PUT_LINE ('El Apellido de John es:' ||v_lname);
EXCEPTION
    WHEN TOO_MANY_ROWS THEN
      DBMS_OUTPUT.PUT_LINE ('Tu consulta retorna mas de un registro. Usa un Cursor.');
END;
---OUTPUT:
Escribimos el mismo ejemplo, pero ahora manejamos la excepción que había ocurrido.
Al añadir la sección excepción, el programa PL/SQL no termina abruptamente. Cuando se produce la excepción, el flujo de ejecución se mueve a la sección de excepciones. Si la excepción ocurrida tiene una condición de control, el bloque PL/SQL finaliza normal, con éxito.
____________________________________________________________________________________
Excepciones en PL/SQL.
Una excepción es un error en PL/SQL, que se propaga durante la ejecución de un bloque. Un bloque siempre termina cuando PL/SQL genera una excepción, pero se puede especificar un manejador de excepciones para realizar acciones finales antes que el bloque finalice.

Métodos para levantar excepciones.
• Se produce un error de Oracle y la excepción asociada se levanta automáticamente. Por ejemplo, cuando ocurre el error ORA-01403, se levanta la excepción NO_DATA_FOUND, la cual indica que la consulta SELECT no retorno ni una fila. Estos errores tienen excepciones predefinidas.

• Dependiendo de la funcionalidad de negocio que implementa su programa, es posible que tenga que levantar una excepción explícitamente. Puede levantar una excepción explícitamente mediante la emisión de la sentencia RAISE en el bloque. Tal excepción puede ser definida por el usuario o predefinida por Oracle. También hay algunos errores de Oracle no predefinidos, para esos errores, se puede declarar excepciones explícitas y asociarlas a ellos.

Capturando Excepciones.
Atrapando una excepción.
Debe incluir una sección de excepciones en su programa PL/SQL para atrapar excepciones. Si la excepción se levanta en la sección ejecutable del bloque, el flujo se mueve al control correspondiente dentro de la sección EXCEPTION. Si PL/SQL maneja con éxito la excepción, está no se propaga al ambiente y el bloque PL/SQL termina con éxito.

Propagar una excepción.
Si la excepción se levanta en la sección ejecutable del bloque y no hay ningún controlador de excepciones correspondiente, el bloque PL/SQL fracasa y la excepción se propaga al entorno de llamada. El entorno de llamada puede ser cualquier aplicación (como SQL*Plus).

Tipos de Excepciones.

Excepción

Descripción
Instrucciones de Manejo
Errores Predefinidos de Oracle.
Errores que ocurren con frecuencia en PL/SQL.
No hay necesidad de declarar esta excepciones. Son predefinidas por Oracle y se levantan de forma implicita.
Errores No-Predefinidos de Oracle.
Cualquier otro error estándar del servidor de Oracle.
Es necesario declararlos dentro de la parte declarativa; el servidor Oracle levanta el error de forma implícita, y luego puedes capturarlo en el controlador de excepciones.
Errores definidos por e usuario.
X condición que el programador entiende como anormal.
Deben ser declarados y levantados de forma explicita.
Nota: Algunas herramientas/aplicaciónes que interactuan con la Base de Dato Oracle (como Oracle Developer Forms) tienen sus propias excepciones.

Sintaxis:
EXCEPTION
  WHEN exception1 [OR exception2 . . .] THEN
    statement1;
    statement2;
    . . .
  [WHEN exception3 [OR exception4 . . .] THEN
    statement1;
    statement2;
    . . .]
  [WHEN OTHERS THEN
    statement1;
    statement2;
    . . .]

Puede atrapar cualquier error mediante la inclusión de un controlador correspondiente dentro de la sección de control de excepciones del bloque PL/SQL. Cada controlador consiste en una cláusula WHEN, que especifica el nombre de la excepción, seguido de una secuencia de sentencias que se ejecutan cuando se levanta tal excepción.

Puede incluir cualquier número de manejadores dentro de una sección de excepciones para controlar excepciones específicas. Sin embargo, no se puede tener varios controladores para una sola excepción.

En la sintaxis:
exception
Es el nombre una excepción(predefinida por Oracle o definida por el usuario).

statement
Es una o mas sentencias SQL PL/SQL.

OTHERS
Es una cláusula(opcional) de control de excepciones que atrapa cualquier excepción que no ha sido manejada de manera explícita.
Cláusula WHEN OTHERS.
Para atrapar cualquier excepción no especificada, se utiliza el controlador de excepciones OTHERS. Esta opción atrapa cualquier excepción no manejada. Por esta razón, si se utiliza el controlador OTHERS, debe ser el último controlador de excepciones.

Ejemplo:
WHEN NO_DATA_FOUND THEN
    statement1;
    ...
WHEN TOO_MANY_ROWS THEN
    statement1;
    ...
WHEN OTHERS THEN
    statement1;

Considere el ejemplo anterior. Si la excepción NO_DATA_FOUND se eleva, las sentencias en el controlador correspondiente se ejecutan; por igual, si se produce la excepción TOO_MANY_ROWS, las sentencias en el controlador correspondiente se ejecutan. Sin embargo, si se levanta alguna otra excepción,  son ejecutadas las sentencias en el controlador de excepciones OTHERS.

El controlador OTHERS captura todas las excepciones no manejadas. Algunas herramientas de Oracle tienen sus propias excepciones predefinidas las cuales controlan ciertos eventos en la aplicación. El manejador OTHERS también atrapa a estas excepciones.

Directrices para la captura de excepciones.
• Comience la sección de control de excepciones con la palabra clave EXCEPTION.
• Defina varios controladores de excepciones, cada uno con su propio conjunto de acciones.
• Cuando se produce una excepción, sólo se ejecuta un controlador antes de abandonar el bloque.
• Coloque la cláusula OTHERS después de todas las demás cláusulas de manejo de excepciones.
• Sólo puede haber una cláusula OTHERS.
• Las excepciones no pueden aparecer en las sentencias de asignación o sentencias SQL.

Algunas Excepciones Predefinidas de Oracle.

Excepción

Número de Error en Oracle

Descripción

ACCESS_INTO_NULL
ORA-06530
Intento de asignar valores a los atributos de un objeto no inicializado.
CASE_NOT_FOUND
ORA-06592
No se selecciona ninguna de las opciones en las cláusulas WHEN de una sentencia CASE, y no hay una cláusula ELSE.
COLLECTION_IS_NULL
ORA-06531
Intentado de aplicar un método diferente de EXISTS a una colección(Nested Table, VARRAY) no inicializada.
CURSOR_ALREADY_OPEN
ORA-06511
Intento de abrir un Cursor ya abierto.
DUP_VAL_ON_INDEX
ORA-00001
Intento de insertar un valor duplicado.
INVALID_CURSOR
ORA-01001
Se produjo una operación inválida con un Cursor.
INVALID_NUMBER
ORA-01722
Conversión de carácter a numero fallida.
LOGIN_DENIED
ORA-01017
Intento de iniciar sesión en el servidor de Oracle con un nombre de usuario o contraseña no válidos.
NO_DATA_FOUND
ORA-01403
Sentencia SELECT no retorna datos.
NOT_LOGGED_ON
ORA-01012
El programa PL/SQL emite una llamada de Base de Datos sin estar conectado al servidor de Oracle.
PROGRAM_ERROR
ORA-06501
PL/SQL tiene un problema interno.
ROWTYPE_MISMATCH
ORA-06504
La variable host cursor y la variable PL/SQL envueltas en una asignación tienen tipos incompatibles.
STORAGE_ERROR
ORA-06500
PL/SQL sin memoria o la memoria está dañada.
SUBSCRIPT_BEYOND_COUNT
ORA-06533
Referencia a un elemento de un Nested Table o VARRAY mediante el uso de un índice mayor que el número de elementos de la colección.
SUBSCRIPT_OUTSIDE_LIMIT
ORA-06532
Referencia a un elemento de un Nested Table o VARRAY mediante el uso de un índice que está fuera del rango legal (Ejemplo: –1).
SYS_INVALID_ROWID
ORA-01410
La conversión de una cadena de caracteres a ROWID universal falla porque la cadena de caracteres no representa un ROWID válido.
TIMEOUT_ON_RESOURCE
ORA-00051
Se Agotó el tiempo de espera(Time-Out) mientras el Servidor Oracle esperaba por un recurso.
TOO_MANY_ROWS
ORA-01422
Sentencia SELECT retorna mas de una fila.
VALUE_ERROR
ORA-06502
Se produjo un error: aritmético, de conversión, de truncamiento o de restricción de tamaño.
ZERO_DIVIDE
ORA-01476
Se ha intentado dividir por cero.
Capturando Errores No-Predefinidos de Oracle.
Las excepciones predefinidas son similares a las No-Predefinidas; sin embargo, estas ultimas no son definidas como excepciones PL/SQL en el servidor Oracle. Mas bien son errores estándar de Oracle y para manejarlos es necesario crear excepciones y asociarlas con dichos errores mediante el uso de la función de PRAGMA EXCEPTION_INIT. Tales excepciones se llaman excepciones no-predefinidas.

Primero debe declarar el error no-predefinido para así poder manejarlo. Una vez declarada, la excepción se levanta de forma implícita. En PL/SQLPRAGMA EXCEPTION_INIT le dice al compilador que asocie el nombre de la excepción con un número de error Oracle. Esto permite hacer referencia a cualquier excepción interna y así asignar un controlador específico para ella.

Ejemplo:
DECLARE
    e_insert_excep EXCEPTION;
    PRAGMA EXCEPTION_INIT(e_insert_excep, -01400);
BEGIN
    INSERT INTO departments (department_id, department_name)
    VALUES (280, NULL);
  EXCEPTION
    WHEN e_insert_excep THEN
      DBMS_OUTPUT.PUT_LINE('INSERCIÓN FALLIDA.');
      DBMS_OUTPUT.PUT_LINE(SQLERRM);
END;
/*El ejemplo intenta insertar el valor NULL en la columna de department_name de la tabla departments. Sin embargo, la operación no tiene éxito porque department_name es una columna NOT NULL.*/
---OUTPUT:
El ejemplo ilustra las tres etapas asociadas con la captura de un error no predefinido:
1. Declara el nombre de la excepción en la parte declarativa, utilizando la sintaxis:
          excepción_name   EXCEPCIÓN;
2. Asocia la excepción declarada con el número de error estándar de Oracle utilizando la función PRAGMA EXCEPTION_INIT. Utilice la siguiente sintaxis:
          PRAGMA EXCEPTION_INIT (excepción_name, error_number);
3. Referencia la excepción declarada dentro del control de excepciones correspondiente.

La función SQLERRM se utiliza para recuperar el mensaje de error.

SET SERVEROUTPUT ON
SET VERIFY OFF
DECLARE
    e_numero_invalido   EXCEPTION;
    v_entrada           VARCHAR2(4);
    v_numero            NUMBER;
    PRAGMA EXCEPTION_INIT(e_numero_invalido, -06502);
BEGIN
    v_entrada  :=  '&numero';
    v_numero  :=  v_entrada;
--
    DBMS_OUTPUT.PUT_LINE('Tabla del '||v_numero);
    FOR i IN 1..12 LOOP
      DBMS_OUTPUT.PUT_LINE(v_numero||' X '||i||': '||i*v_numero);
    END LOOP;
    
    EXCEPTION
      WHEN e_numero_invalido THEN
        DBMS_OUTPUT.PUT_LINE('Entrado invalida: '||v_entrada);
        DBMS_OUTPUT.PUT_LINE('Debe introducir un valor numérico.');
END;
/*El ejemplo pide al usuario introducir un valor numérico para luego mostrar la tabla de multiplicación de dicho número, si el usuario introduce algo diferente un mensaje de alerta se mostrara en la salida.*/
---OUTPUT:
Funciones relacionas con las Excepciones.
Cuando se produce una excepción, es posible identificar el código o el mensaje de error mediante el uso de dos funciones. Esto nos permite tomar decisiones basadas en X escenario.

• SQLCODE devuelve el número de error Oracle para las excepciones internas.
• SQLERRM devuelve el mensaje asociado con el número de error.

Ejemplo:
CREATE TABLE comision_emp
(
    codigo_emp    NUMBER(6) CONSTRAINT pk_comi PRIMARY KEY,
    comision      NUMBER(2,2) NOT NULL,
    usuario       VARCHAR2(20) NOT NULL,
    fecha         DATE NOT NULL
);
/*Tabla que usaremos para insertar la comisión de todos los empleados, notar que todos los campos son NOT NULL.*/
---
CREATE TABLE comision_error
(
    codigo_error    NUMBER,
    mensaje_error   VARCHAR2(1500),
    codigo_emp      NUMBER,
    usuario         VARCHAR2(20),
    fecha           DATE
);
/*Tabla que usaremos para insertar información sobres las inserciones que no se realizaron en la tabla: comision_emp.*/
---
DECLARE
    v_codigo_error    NUMBER;
    v_mensaje_error   VARCHAR2(1500);
--
    CURSOR c_comision_emp IS
      SELECT employee_id AS emp, commission_pct AS comision
      FROM employees;
BEGIN
    FOR rec IN c_comision_emp LOOP
      BEGIN
        INSERT INTO comision_emp
        VALUES(rec.emp,rec.comision,USER,SYSDATE);
--
        EXCEPTION
          WHEN OTHERS THEN
            v_codigo_error := SQLCODE;
            v_mensaje_error :=  SQLERRM;
--
            INSERT INTO comision_error
            VALUES(v_codigo_error,v_mensaje_error,rec.emp,USER,SYSDATE);
      END;
    END LOOP;
    COMMIT;
END;
/*El ejemplo trata de insertar la comisión de cada empleado en la tabla comision_emp, pero dicha tabla tiene todos los campos NOT NULL y no todos los empleados tienen comisión, por lo cual es de esperar errores con algunos registros; para tener control de esto, agregamos un control de excepciones que insertara los registros con errores en la tabla: comision_error. Nota: si ejecuta el SCRIPT dos veces el error cambiaría: ORA-00001: unique constraint (HR.PK_COMI) violated.*/
---OUTPUT:
Cuando una excepción es atrapada en el manejador WHEN OTHERS, puede utilizar un conjunto de funciones genéricas para identificar esos errores. El ejemplo asigna los valores de SQLCODE y SQLERRM a variables, y luego usamos las variables en la sentencia SQL.

Nota: No es posible utilizar SQLCODE SQLERRM directamente en una instrucción SQL. En su lugar, debe asignar sus valores a variables locales, y luego usar las variables en la sentencia SQL.
____________________________________________________________________________________
Excepciones Definidas por el Usuario.
PL/SQL permite definir sus propias excepciones dependiendo de los requerimientos de su aplicación. Por ejemplo, puede pedir al usuario que introduzca un número de departamento, luego definir una excepción para manejar condiciones de error en los datos de entrada, comprobar si tal número de departamento existe, y si no, es posible que tenga que levantar la excepción definida por el usuario.

Las excepciones definidas por el usuario deben ser:
• Declaradas en la parte declarativa del bloque.
• Levantadas explícitamente con la sentencia RAISE.
• Manejadas en la sección EXCEPTION.

Ejemplo:
SET SERVEROUTPUT ON
DECLARE 
    v_deptno NUMBER := 500;
    v_name VARCHAR2(20) := 'Testing';
    e_invalid_department EXCEPTION;
BEGIN
    UPDATE departments
    SET department_name = v_name
    WHERE department_id = v_deptno;
    IF SQL%NOTFOUND THEN
      RAISE e_invalid_department;
    END IF;
    COMMIT;
EXCEPTION
  WHEN e_invalid_department THEN
    DBMS_OUTPUT.PUT_LINE(v_deptno||' No es un número de departamento Valido.');
END;
/*El ejemplo trata de actualizar el department_name de un departamento. Si no existe el número de departamento suministrado, no se actualizan filas en la tabla de departments y ninguna excepción se dispara por defecto; como vemos en el ejemplo, por medio de las excepciones definidas por el usuario, podemos cambiar ese comportamiento, esto permite informar al usuario si la actualización fue exitosa.*/
---
Nota: Utilice la sentencia RAISE dentro del mismo manejador de la excepción para elevar la misma nuevamente y propagar el error al entorno de la llamada.
____________________________________________________________________________________
Excepciones en Bloques Anidados o SubBloques.
Es posible tener tantas secciones EXCEPTION como bloques(BEGIN/END) en tu código; Cuando un subbloque maneja una excepción, este termina normalmente. El control se reanuda al bloque que la contiene inmediatamente después de la instrucción END.

Sin embargo, si PL/SQL levanta una excepción y el bloque actual no tiene un controlador para ella, la excepción se propaga a los bloques padres hasta que encuentra un controlador. Si ninguno de estos bloques controla la excepción, la misma es lanzada al ambiente de llamada.

Cuando una excepción se propaga al bloque padre, las sentencias ejecutables del bloque en cuestión(SubBloque) son pasadas por alto.

Una de las ventajas de este comportamiento es que permite incluir sentencias que manejen sus propios errores dependiendo del bloque, dejando el manejo de excepciones generales a los bloques padres.

Ejemplo:
SET SERVEROUTPUT ON
SET VERIFY OFF
DECLARE
    v_entrada     VARCHAR2(6);
    v_contador    NUMBER;
    e_bloque_abuelo   EXCEPTION;
BEGIN
v_entrada := '&Numero';
  v_contador := v_entrada;
v_contador := v_contador+1;

IF v_contador > 3 THEN
    RAISE e_bloque_abuelo;
END IF;
--
    DECLARE
        e_bloque_padre   EXCEPTION;
    BEGIN
 v_contador := v_contador+1;

 IF v_contador > 3 THEN
     RAISE e_bloque_padre;
 END IF;
--
      DECLARE
          e_bloque_hijo   EXCEPTION;
      BEGIN
          v_contador := v_contador+1;

          IF v_contador > 3 THEN
              RAISE e_bloque_hijo;
          END IF;

          EXCEPTION
            WHEN e_bloque_hijo THEN
              DBMS_OUTPUT.PUT_LINE('Valor: '||v_entrada||', Excepción: e_bloque_hijo');
      END;
--
  EXCEPTION
   WHEN e_bloque_padre THEN
     DBMS_OUTPUT.PUT_LINE('Valor: '||v_entrada||', Excepción: e_bloque_padre');
    END;
--
EXCEPTION
  WHEN e_bloque_abuelo THEN
   DBMS_OUTPUT.PUT_LINE('Valor: '||v_entrada||', Excepción: e_bloque_abuelo');
    WHEN OTHERS THEN
      DBMS_OUTPUT.PUT_LINE('Valor: '||v_entrada||', Para visualizar el ejemplo debe introducir un valor numérico mayor que 0.');
END;
/*El ejemplo muestra como crear programas de PL/SQL con manejo de excepciones por cada subbloque anidado; Este SCRIPT pide al usuario introducir un valor numérico para luego lanzar un excepción basada en el valor introducido.*/
---OUTPUT:
____________________________________________________________________________________
Procedimiento RAISE_APPLICATION_ERROR.
Utilice el procedimiento RAISE_APPLICATION_ERROR para lanzar una excepción predefinida de forma interactiva, excepción que tendrá un código de error no estándar y un mensaje personalizado. Con RAISE_APPLICATION_ERROR, puede reportar errores a la aplicación y evitar devolver excepciones no controladas.

Sintaxis:
RAISE_APPLICATION_ERROR (error_number, message[, {TRUE | FALSE}]);

En la sintaxis:
error_number

Es un número de excepción especificado por el usuario; rango entre –20,000 y –20,999.

message

Es el mensaje de excepción especificado por el usuario; con longitud máxima de 2,048 bytes.
TRUE | FALSE

Es un parámetro booleano opcional (Si es TRUE, el error se coloca en la pila de errores anteriores. Si es FALSE(por defecto), el error sustituye a todos los errores anteriores.)

El procedimiento RAISE_APPLICATION_ERROR se puede utilizar ya sea en la sección ejecutable como en la sección de excepciones de un programa de PL/SQL, o en ambos. El error devuelto es consistente con la forma en que el servidor Oracle produce un error predefinido, no predefinido o definido por el usuario. El número de error y el mensaje se muestran al usuario.

Ejemplo:
BEGIN
    DELETE FROM comision_emp
    WHERE codigo_emp = 100;
--
    IF SQL%NOTFOUND THEN
      RAISE_APPLICATION_ERROR(-20001, 'Ningún Registro Eliminado.');
    ELSE
      DBMS_OUTPUT.PUT_LINE(SQL%ROWCOUNT||'Registros Eliminados.');
    END IF;
END;
/*El ejemplo muestra como usar el procedimiento: RAISE_APPLICATION_ERROR. El SCRIPT trata de eliminar el registro con codigo_emp = 100 de la tabla comision_emp(Creada en ejemplos anteriores), como dicha tabla no contiene este registro, el atributo SQL%NOTFOUND es TRUE por lo cual se ejecuta la sentencia:  RAISE_APPLICATION_ERROR*/
---OUTPUT:
_____________________________________________________________________________________
Fuente: Oracle Database: PL/SQL Fundamentals