Exception PL/SQL parte II - tratamiento de errores a fondo

Oracle, pionero en BD relacionales.

Exception PL/SQL parte II - tratamiento de errores a fondo

Notapor Pere » Jue Ene 16, 2014 5:36 pm

Puede consultar la primera parte del artículo aquí

Introducción

A lo largo de mi experiencia como profesional visitando clientes para mantener o evolucionar aplicaciones con tecnología Oracle, me sorprende observar lo mal que suele abordarse en general el control de errores en la programación, y en particular en la programación PL/SQL. En ocasiones son los propios estándares de las compañías los que enfocan la cuestión de un modo digamos que poco práctica, suelen reinventan la rueda y al final esta acaba funcionando peor que la que proporcionan los propios entornos de programación. Otras veces es la falta de estándares lo que provoca una anarquía en cuestión del control de errores dejando en manos de cada desarrollador como abordarlos.

La consecuencia de gestionar mal el control de errores es que cuando algún proceso provoca un error, cuesta acotarlo. Es habitual que el técnico no puede reproducirlo fácilmente debido a que el error que le reportan como incidencia se ha dado en un entorno de producción al cual no tiene acceso o lo tiene limitado, y debe reproducirlo en un entorno de pruebas o de desarrollo donde los datos no son los mismos y por tanto es complicado sacar el agua clara.

Por tanto gestionar de forma práctica los errores es importante, puesto que le permitirá ser ágil acotando el problema cuando se de un error en algún proceso. Lo ideal es saber de forma rápida que ha provocado el error y donde, para posteriormente tomar decisiones al respecto.

Si usted no tiene claro como tratar los errores, no lo haga. Es preferible no tratarlos que hacerlo mal, de este modo NO se corre el riesgo de que nadie se entere si se produce un error. No tratar los errores podría ser perfectamente un estándar de control de errores de una compañía, y a mi entender, mucho mejor que otras estrategias o estándares que he visto implantados.


Malas prácticas

Algunos colegas afirman que es muy “feo” ver un mensaje de error sin tratar en una aplicación, que es poco profesional y que da mala imagen. No podría estar más en desacuerdo, en primer lugar porque no creo que haya ninguna aplicación que se libre de mostrar en alguna ocasión un mensaje de error sin tratar (los he visto en aplicaciones webs de bancos online serios y profesionales), y en segundo lugar porque tratando los errores es posible que nadie se entere que se ha producido un error si no se hace bien, precisamente por querer evitar ver un mensaje “feo” por pantalla.

En ocasiones tratar el error significa ver por pantalla el siguiente mensaje:

Se ha producido un error, contacte con el soporte.

Y entonces empieza la odisea de saber que ha pasado.

Francamente, para mostrar mensajes como el anterior es mejor ver errores sin tratar como el siguiente:

ORA-00001: unique constraint PEDIDOS_PK violated

Con este mensaje de error al menos el técnico que debe resolver la indecencia sabrá que se encuentra ante el intento de duplicar una clave primaria o un índice único, también sabrá en que restricción o constraint: PEDIDOS_PK. Pero no sabrá en que punto del código se ha producido el error, y podemos estar hablando de miles de líneas de código.

Algunos desarrolladores son realmente buenos en esto, son expertos en tratar errores, de modo que mientras están desarrollando un producto lo prueban al mismo tiempo, y les resulta sorprendentemente fácil tratar las excepciones no haciendo nada, no les importa si la funcionalidad queda cubierta, han solucionado el problema, ahora su programa ya no aborta. Otros estándares y/o desarrolladores, no tan buenos como los anteriores tratando errores, asignan a un parámetro de salida: P_ERROR, un número, si vale cero es que todo ha ido bien, de lo contrario se ha producido un error:

Mensaje:

Se ha producido un error, código: 7

No sabemos el tipo de error, pero tenemos un número que deberemos buscar por el código y rezar para que solo este en un punto y el error 7 no sea ambiguo. Además es posible que debamos modificar el programa con el propósito de obtener más información y saber el tipo de error, mostrándose entonces un mensaje como el siguiente:

Mensaje:

Se ha producido un error, código: 7 -> ORA-00001: unique constraint PEDIDOS_PK violated

Ni que decir tiene que los desarrolladores realmente buenos tratando errores ignoran los parámetros de salida que los desarrolladores no tan buenos se preocupan en retornar en sus programas, y el proceso sigue corriendo como si nada.


Estándares de control de errores

Y yo me pregunto, ¿por qué tengo que preocuparme de reinventar la acotación de errores cuando el propio SGBD Oracle lo hace por mi? Véase el siguiente procedimiento de ejemplo que usa el recurso del parámetro de salida P_ERROR:

Código: Seleccionar todo
create or replace procedure buscar_mejor_precio(p_id_origen in number, p_id_destino in number, p_error out number) is
   v_error         number
;
   v_importe_vuelo number;
   v_importe_hotel number;
   v_importe_coche number;
begin
   
   v_error 
:= 1;
      
   v_importe_vuelo 
:= buscar_mejor_vuelo(p_id_origen, p_id_destino);
   
   v_error 
:= 2;
   
   v_importe_hotel 
:= buscar_mejor_hotel(p_id_destino);
   
   v_error 
:= 3;

   v_importe_coche := buscar_mejor_alquiler(p_id_destino);
   
   v_error 
:= 4;
   
   insert into mejor_precio
(IMP_VUELO, IMP_HOTEL, IMP_COCHE)
        values (v_importe_vuelo, v_importe_hotel, v_importe_coche);

   p_error = 0;
exception
   when others then
      p_error 
: = v_error;
end;

Supongamos que se llama a este procedimiento desde el siguiente bloque anónimo de código:

Código: Seleccionar todo
declare
   v_error number;
begin
   buscar_mejor_precio
(3434, 23323, v_error);
   
   if v_error 
!= 0 then
      mostrar_error
('Se ha producido un error en procedimiento buscar_mejor_precio, código: '||v_error);
   else
      mostrar_mejor_precio
;   

   end if
;

end;

¿Qué gano con esta estrategia de control de errores? Pues poco, al contrario, nos estamos complicando la vida sobremanera. Si lo analizamos se observa que todo son inconvenientes y no hay prácticamente ninguna ventaja:

  1. El desarrollador que programa o mantiene el procedimiento debe mantener el parámetro de salida p_error.
  2. El desarrollador que programa la invocación a este procedimiento debe preocuparse de evaluar el parámetro de salida p_error y generar un mensaje de error. Si no lo hace, cosa más que posible, el proceso fallará seguramente más adelante, y cuesta mucho acotar el origen del error en algoritmos de cientos de líneas de código.
  3. En el caso de evaluar el parámetro de salida y mostrar el mensaje de error , este solo es orientativo:

Se ha producido un error en procedimiento buscar_mejor_precio, código: 3

Ahora programemos lo mismo sin tratar los posibles errores.

Código: Seleccionar todo
create or replace procedure buscar_mejor_precio(p_id_origen in number, p_id_destino in number) is
   v_importe_vuelo number
;
   v_importe_hotel number;
   v_importe_coche number;
begin

   v_importe_vuelo 
:= buscar_mejor_vuelop_id_origen, p_id_destino);
   v_importe_hotel := buscar_mejor_hotel(p_id_destino);
   v_importe_coche := buscar_mejor_alquiler(p_id_destino);
   
   insert into mejor_precio
(IMP_VUELO, IMP_HOTEL, IMP_COCHE)
        values (v_importe_vuelo, v_importe_hotel, v_importe_coche);
end;

Y lo invocamos del siguiente modo:

Código: Seleccionar todo
begin
   buscar_mejor_precio
(3434, 23323);
   mostrar_mejor_precio;   
end
;

Y el mensaje de error sin tratar que se muestra ahora supongamos que es:

ORA-01403: no data found

Mensaje típico de una aplicación de gestión que se conecta a una BD Oracle.

En ninguno de los casos me ahorro tener que acotar el error, en el primer caso tendré que empezar a estirar del hilo a partir del mensaje:

Se ha producido un error en procedimiento buscar_mejor_precio, código: 3

No se que tipo de error es, pero se en que procedimiento a ocurrido, y también tengo un numero 3 que no se exactamente que significa hasta que mire el programa.

En el segundo caso:

ORA-01403: no data found

Se que tipo de error a ocurrido pero no donde, por tanto deberé pedirle al usuario que me explique que ha hecho para reproducir el error y empezar a estirar por ahí del hilo.

Ninguno de los dos ejemplos me convence porque ninguno de los dos métodos permite acotar el error sin investigación, por lo tanto prefiero el segundo estándar al primero, es decir, para reinventar el control de errores y no acotarlo, mejor no darle tratamiento, con ello consigo lo siguiente:

  1. Me resulta mucho más cómodo programar sin preocuparme de gestionar un parámetro de salida para controlar los errores.
  2. Evito dejar en manos de los desarrolladores, con la fuente de errores que ello supone, tener que evaluar constantemente si las llamadas a las funciones provocan error. Por tanto es más cómodo programar las llamadas a estas funciones sabiendo que no tratan las excepciones si no que las propagan.
  3. No se corre el riesgo de que si por descuido no se evalúa el parámetro de retorno para la gestión de errores en las llamadas a procedimiento y funciones, el proceso corra como si nada.


DBMS_UTILITY.FORMAT_ERROR_BACKTRACE

Hemos visto que el segundo método que se ha presentado con anterioridad ofrece muchas más ventajas que el primero. Vamos a mejorarlo con ayuda de la función nativa que el SGBD Oracle proporciona: DBMS_UTILITY.FORMAT_ERROR_BACKTRACE.
Esta función permite obtener toda la traza del error, es una función de suma utilidad, puesto que permite generar un mensaje de error con toda la información y los detalles. Para ilustrar esto véase el siguiente procedimiento al que no se le da ningún tratamiento a los posibles errores, tampoco los tienen las funciones que se invocan en él:

Código: Seleccionar todo
create or replace procedure buscar_mejor_precio(p_id_origen in number, p_id_destino in number) is
   v_importe_vuelo number
;
   v_importe_hotel number;
   v_importe_coche number;
begin

   v_importe_vuelo 
:= buscar_mejor_vuelop_id_origen, p_id_destino);
   v_importe_hotel := buscar_mejor_hotel(p_id_destino);
   v_importe_coche := buscar_mejor_alquiler(p_id_destino);
   
   insert into mejor_precio
(IMP_VUELO, IMP_HOTEL, IMP_COCHE)
        values (v_importe_vuelo, v_importe_hotel, v_importe_coche);
end;

y ahora programemos la invocación del siguiente modo:

Código: Seleccionar todo
begin
   buscar_mejor_precio
(3434, 23323);
   mostrar_mejor_precio; 
exception
   when others then
      raise_application_error
(-20000, SQLERRM||' -> traza: '||DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);   
end
;

El programa aborta, y el mensaje de error que se muestra ahora desde la aplicación es:

ORA-20000: ORA-01403: no data found -> traza: ORA-06512: at "DB_TEST.BUSCAR_MEJOR_ALQUILER", line 4
ORA-06512: at "DB_TEST.BUSCAR_MEJOR_PRECIO", line 9
ORA-06512: at line 2
ORA-06512: at line 6


Observe como solo con dotar al bloque principal, o de nivel superior, de un apartado de excepciones usando debidamente los recursos que el SGBD Oracle proporciona, obtenemos la localización exacta del error, y además, al programar toda la lógica que compone el algoritmo no debemos preocuparnos en ningún momento de los posibles errores. Lo centralizamos en el procedimiento principal, es decir, el que se invocará desde la aplicación. En este caso el error se produjo exactamente en:

"DB_TEST.BUSCAR_MEJOR_ALQUILER", line 4

En cierto modo se esta personalizando el mensaje de error con ayuda de la función “raise_application_error”. Y el mensaje que generamos es: tipo de error + toda la traza de los saltos que se han producido hasta darse la excepción. Por tanto tenemos toda la información de lo ocurrido, esto nos permitirá ser mucho más ágiles para solucionar el problema.

Eso sí, el mensaje es “feo” para el usuario que trabaja con la aplicación, pero estos mensajes no los tiene que entender el usuario, si no el técnico responsable de solucionar las incidencias. No se preocupe por ello, cuando un usuario se enfrente a tal mensaje será plenamente consciente de que se ha producido un error, y reportara el mensaje al servicio técnico para que lo solucione.


Tratamiento de excepciones

Las excepciones deben tratarse por algo, por algún motivo, y no solamente porque se ha producido un error. En el ejemplo anterior ese motivo es: que el proceso aborte mostrando un mensaje de error detallado.
Cuando usted cree un apartado de excepciones en un bloque de código, hágalo con un propósito que no sea únicamente lo que ya hace el SGBD por defecto. No reinvente la rueda. Véase el siguiente procedimiento:

Código: Seleccionar todo
create or replace procedure insertar_vehiculo(p_vehiculo in vehiculos%rowtype) is
begin

   insert into vehiculos
(ID_VEHICULO, DESCRIPCION)
        values (p_vehiculo.id_vehiculo, p_vehiculo.descripcion);

exception
   when dup_val_on_index then
      update vehiculo
         set descripcion 
= p_vehiculo.descripcion
       where id_vehiculo 
= p_vehiculo.id_vehiculo;
end;


El anterior procedimiento inserta un nuevo registro en la tabla VEHICULOS. En el caso de que el identificador ya exista en la tabla se produce una excepción de clave primaria duplicada, en tal caso el algoritmo no debe abortar, sino que debe actualizar la descripción del vehículo con ese identificador. Por eso se ha implementado un tratamiento a tal excepción. Y cualquier otra excepción que se pueda dar debe propagarse, por lo que no se trata.

Si el programa que invoca a este procedimiento considera que no es crítico que falle, puede encapsular la llamada en un bloque de código y tratar cualquier excepción para que no aborte.

Código: Seleccionar todo
declare
   v_vehiculo in vehiculos%rowtype;
begin
   v_vehiculo
.id_vehiculo := 4;
   v_vehiculo.descripcion := 'Super buga';
   
   begin
      insertar_vehiculo
(v_vehiculo);
   exception
      when others then
         null
;
   end;
   
-- resto de lineas del algoritmo  

end

Fíjese que esta filosofía es justamente opuesta a la de programar procedimientos con parámetros de salida que informen si la ejecución ha ido bien y su evaluación post llamada. En este caso no debe evaluarse si ha ido bien o no, sino que deben encapsularse las llamadas o instrucciones para que en lugar de abortar se le de el tratamiento que proceda a esa parte del código. Este modo de proceder, a parte de ser mucho más cómodo, es más seguro, dado que si descuidamos encapsular la parte de código afectado para que no aborte, no es ni de largo tan crítico como descuidar evaluar el parámetro de salida para que aborte si dicho parámetro así lo indica. Recuerde que solo hay una cosa peor que un programa que no hace lo que debe: un programa que hace lo que no debe.

Por tanto mi consejo es que base su estándar de control de errores en no tratar las excepciones salvo para evitar que el proceso aborte ya que sabe como tratarlas. Y no las trate salvo que quiera capturar el error o mostrar un mensaje. En cuyo caso incluya toda la información con ayuda de las funciones nativas de Oracle. El servicio de incidencias se lo agradecerá.


Registro de errores

Si usted decide darle un tratamiento a la excepción de modo que el error quede registrado en una tabla de la BD. Recuerde que no puede realizar un commit tras realizar la inserción en la tabla de errores. Quizás se le ocurra ejecutar un rollback antes de la inserción, pero quizás tampoco sea lo correcto, ya que esta tomando decisiones que quizás no le competen, dado que depende de lo que el usuario decida. Un modo de registrar el error dejando en manos del usuario guardar los cambios o no tras un error es programar un procedimiento que realice la inserción del error en una transacción autónoma e independiente de la transacción que ha provocado el error. Esto se consigue con ayuda de la instrucción “PRAGMA AUTONOMOUS_TRANSACTION”. Véase el siguiente procedimiento:

Código: Seleccionar todo
create procedure guardar_error(p_error in varchar2) is

   PRAGMA AUTONOMOUS_TRANSACTION
;

begin
   insert into errores
(fecha, error, usuario)
        values (sysdate, p_error, user);
   commit;
end;

Este procedimiento insertará el error en la tabla errores y guardará los cambios en una transacción independiente a la que invoca el procedimiento. Por lo que no corre el riesgo de confirmar o descartar cambios cuando le compete al usuario hacerlo y no al programador.

Un ejemplo de uso:

Código: Seleccionar todo
begin
   buscar_mejor_precio
(3434, 23323);
   mostrar_mejor_precio; 
exception
   when others then
      guardar_error
(SQLERRM||' -> traza: '||DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);

      raise_application_error(-20000, SQLERRM||' -> traza: '||DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);   
end
;


Generación de excepciones

Se ha visto como con el procedimiento “raise_application_error” es posible forzar que un programa aborte personalizando el mensaje de error. Este recurso es muy útil para provocar una excepción y propagarla sin necesidad de poner condicionantes en el código. Veamos primero como trabajar sin aprovechar este recurso:

Código: Seleccionar todo
create or replace function calcular_precio(p_id_articulo in number, p_error out number) return number is
   v_error  number
;
   v_precio number;
begin

   if p_id_articulo is null then
      v_error 
:= 1;
   elsif articulo_descatalogado(p_id_articulo) then
      v_error 
:= 2;
   else      
      v_precio 
:= calcular_precio_art(p_id_articulo);
      v_error  := 0;
   end if;

   return v_precio; 

end
;

Fíjese que ahora no nos encontramos ante un error inesperado, sino que se implementa una lógica para validar el parámetro de entrada de la función calcular_precio.

Y lo invocamos del siguiente modo:

Código: Seleccionar todo
declare
   ARTICULO_NO_VALIDO exception;
   
   v_error  number
;
   v_precio number;  
begin
   
   v_precio 
:= calcular_precio(45, v_error);
   
   if v_error 
!= 0 then
      raise ARTICULO_NO_VALIDO
;
   else
      dbms_output
.put_line(v_precio);
   end if;

exception
   when ARTICULO_NO_VALIDO then
      raise_application_error
(-20000,'El código de artículo indicado no es válido');
end;

Estamos en una situación muy parecida a la vista con anterioridad, debemos evaluar el parámetro de salida para saber si el artículo es válido, en caso contrario se genera una excepción y el proceso aborta mostrando el mensaje de error personalizado.

En este caso la profundidad de llamadas a funciones es de un solo nivel, pero imagine usted que deba realizar varios saltos o llamadas y a la vuelta de cada una de ellas evaluar si se ha superado la validación de parámetros de entrada para seguir o no con el proceso y en caso contrario ir propagando este control hasta el bloque principal de código donde finalmente se aborta la ejecución. No es práctico. El procedimiento “raise_application_error” nos ahorra todo el tratamiento post llamada al margen de la profundidad de los saltos en llamadas a funciones que precise el algoritmo. Modifiquemos la anterior lógica con este propósito:

Código: Seleccionar todo
create or replace function calcular_precio(p_id_articulo in number) return number is
   v_precio number
;
begin

   if p_id_articulo is null then
      raise_application_error
(-20000,'El código de artículo indicado en el parámetro de entrada es nulo');
   elsif articulo_descatalogado(p_id_articulo) then
      raise_application_error
(-20000,'El código de artículo indicado en el parámetro de entrada está descatalogado');
   else      
      v_precio 
:= calcular_precio_art(p_id_articulo);
   end if;

   return v_precio; 

end
;

Y lo invocamos del siguiente modo:

Código: Seleccionar todo
begin
   
   dbms_output
.put_line(calcular_precio(45));

exception
   when others then
      raise_application_error
(-20000, SQLERRM||' -> traza: '||DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);   
end
;

Si el artículo 45 está descatalogado el proceso abortará mostrando el siguiente mensaje:

ORA-20000: ORA-20000: El código de articulo indicado en el parámetro de entrada está descatalogado -> traza: ORA-06512: at "BDD_MVV.CALCULAR_PRECIO", line 8
ORA-06512: at line 3
ORA-06512: at line 7


Con ello conseguimos liberarnos de condicionantes post llamada y de su propagación hasta la raíz o procedimiento principal. La verdad, resulta muy cómodo.

Y bueno hasta aquí el artículo, espero os resulte útil y confio en no encontrar más controles de errores mal diseñados en los futuros clientes a los que acuda, ahora ya tenéis la información para montaroslo bien. :)
Pere
 
Mensajes: 70
Registrado: Mar Feb 02, 2010 9:44 pm

Volver a SGBD Oracle

¿Quién está conectado?

Usuarios navegando por este Foro: No hay usuarios registrados visitando el Foro y 1 invitado