BD UD7 Gestion de errores.Excepciones

De MediaWiki
Ir a la navegación Ir a la búsqueda

Introducción

  • Ya hemos visto en secciones anteriores la necesidad de 'capturar' los errores que se puedan producir en el gestor MYSQL para intentar dar una respuesta mediante programación al resultado de dicho error.
La captura de los errores es necesaria para que el programa o script que se ejecute pueda continuar.
  • Si estáis desarrollando una aplicación cliente (PHP, Java, ASP.NET,C# ....) en el que desarrolláis las pantallas que va a manejar el usuario y cuyos datos son obtenidos de llamadas al gestor, el tratamiento de errores los podéis gestionar el dichos programas clientes con órdenes del tipo TRY .... CATCH.
Mod BD Prog Errores 1.jpg
Imagen obtenida de este vídeo
  • A la hora de programar desde el punto de vista del gestor, por medio de procedimientos/funciones, podemos realizar dos aproximaciones diferentes a la gestión de errores:
  • Capturar las excepciones que se puedan dar y mandar un mensaje al programa cliente (por medio de un parámetro de salida o un valor concreto devuelto con un SELECT).
En este caso, el tratamiento del error lo gestionaremos dentro del procedimiento/función del gestor y programaremos la consecuencia que provoca dicho error.
  • No capturar excepciones y que sea el programa cliente el que gestione las mismas. En este caso, incluso, podemos ser nosotros mismos los que provoquemos una excepción dentro del código del procedimiento/función (por ejemplo, el cliente envía un nif de un artista que no existe).
Podría darse una combinación de las dos aproximaciones.
Si trabajamos directamente contra el gestor Mysql sin hacer uso de un programa cliente, llamaremos directamente a los procedimientos/funciones por lo que lo normal sería intentar capturar nosotros las excepciones para que la ejecución de los scripts no parasen si se produjera algún error.


Declarando excepciones. DECLARE CONDITION

  • Cuando capturamos excepciones, podremos capturar excepciones:
  • Ya definidas por el gestor (por ejemplo, DUPLICATE ENTRY FOR KEY, que se produce cuando intentamos añadir dos filas con la misma clave primaria)
  • Excepciones personalizadas por el usuario. Por ejemplo, puedo lanzar una excepción cuando intente añadir un animal con una altura negativa.
En cualquiera de los dos casos, cada excepción va a estar asociada a un número ya predefinido por el gestor.
Para hacer mas legible el manejo de excepciones, puedo asociar dicho número a un nombre de excepción y después controlar la captura por dicho nombre y no por el número asociado.
  • Podéis consultar la lista de códigos con el tipo de excepción asociado en este enlace.
Por ejemplo, el código de error que aparece cuando se intenta borrar una table que no existe es el 1051.
Mod BD Prog Errores 2.jpg
Dentro de un procedimiento voy a poder capturar esta excepción, pero en vez de capturarla (veremos la instrucción SQL a continuación) por su número (1051) puedo hacerlo por un nombre de la forma:
1 DECLARE no_existe_tabla CONDITION FOR 1051;

Veremos a continuación como capturar la excepción empleando este nombre.

  • Error Code: Son números específicos de Mysql que no valen para otros gestores.
  • SQLState Code: Cadena de 5 dígitos basado en ANSI SQL y ODBC y por lo tanto sus códigos son 'estandarizados'.
Podemos consultar la diferencia entre un tipo y otro en este enlace.
Emplear uno u otro va a depender de nuestro programa cliente. Si este va a ser empleado en sistemas gestores diferentes (por ejemplo ORACLE, SQL Server) podría ser mejor emplear los SQLState Code. En caso contrario, mejor los específicos de cada gestor.
En el caso de querer dar un nombre a la excepción empleando el SQLState la sintaxis cambia:
1 DECLARE no_existe_tabla CONDITION FOR SQLSTATE '42S02';


Lanzando excepciones. SIGNAL

Mod BD Prog Errores 3.jpg
SIGNAL es la manera que tiene Mysql de que un usuario pueda lanzar una excepción.
Puede llevar como dato, un nombre de excepción (definida como vimos antes con DECLARE CONDITION o un código SQLState de 5 caracteres.
Si empleamos el código de 5 caracteres, este no debe comenzar con '00' (tanto si empleamos un código directamente como si empleamos un nombre de excepción basado en un código que empiece por 00) ya que esto indica que es correcto y por tanto no lanzará la excepción.
Existe un código estándar para indicar que la excepción está definida por el usuario (no es ninguna de las que ya existen): 45000
Veamos varios ejemplos:
1 SIGNAL SQLSTATE '42S02';
DECLARE no_existe_tabla CONDITION FOR SQLSTATE '42S02';
SIGNAL no_existe_tabla;
DECLARE no_existe_tabla CONDITION FOR 1051;
SIGNAL no_existe_tabla;
A mayores, a parte de provocar una excepción, podemos realizar diferentes acciones que se encuentran indicadas en la sintaxis de la sentencia.
Por ejemplo, estamos a provocar una excepción en la que mostramos el SQLSTATE, pero también podemos indicar el número de error MYSQL y un texto asociado a la excepción.
SIGNAL SQLSTATE '42S02'
	SET MESSAGE_TEXT = 'Esa tabla no existe', 
            MYSQL_ERRNO = 1051
Como siempre, el uso que hagamos de las excepciones va a depender del tipo de programa que estemos desarrollando.
Como comenté antes, si estamos a desarrollar un programa cliente, el control de las excepciones la haremos en dicho programa, y tendremos que programar una respuesta a la misma.
Si por ejemplo, estamos desarrollando una aplicación en Java, podremos capturar las excepciones y consultar en el objeto Exception los datos de la excepción, como su número, mensaje o SQLState, como se muestra en este ejemplo.


Capturando excepciones. DECLARE HANDLER

Mod BD Prog Errores 5.jpg
  • Viene a ser como un TRY...CATCH en los programas clientes.
  • Debemos de declarar la excepción e indicar que queremos hacer cuando se produzca:
  • CONTINUE: Sigue la ejecución del programa.
  • EXIT: Sale del programa
Al mismo tiempo, debemos indicar una o un conjunto de instrucciones que queremos que se ejecuten cuando se produzca la excepción (si son varias deben ir dentro de un BEGIN...END).
Podemos llegar a capturar los siguientes tipos de excepciones:
  • Un Error Code (visto antes)
  • Un SQLState Code (visto antes)
  • Un nombre de condición (visto antes)
  • SQLWARNING: Cualquier SQLState que empiece por '01' (warning).
  • NOT FOUND: Cualquier SQLState que empiece por '02' (por ejemplo, en los cursores, cuando no hay más datos devuelve el SQLState '02000').
  • SQLEXCEPTION: Cualquier SQLState que NO empiece por '00','01' o '02'.
Por ejemplo, el siguiente código captura el Error Code
1 DECLARE CONTINUE HANDLER FOR 1051
2   BEGIN
3     -- Instrucciones a ejecutar en caso de excepción
4   END;
O por ejemplo, si queremos capturar una excepción por un nombre:
1 DECLARE no_existe_tabla CONDITION FOR SQLSTATE '42S02';
2 
3 DECLARE CONTINUE HANDLER FOR no_existe_tabla
4   BEGIN
5     -- Instrucciones a ejecutar en caso de excepción
6   END;
Otro ejemplo:
Si queremos que se salga del procedimiento o función (siempre que la instrucción DECLARE HANDLER esté puesta dentro de este bloque o no de otro interior) podemos poner lo siguiente:
1 DELIMITER $$
2 CREATE PROCEDURE prueba()
3 BEGIN
4      DECLARE EXIT HANDLER FOR 1051 SELECT -1;
5 
6 END$$
7 DELIMITER ;
Siempre debe de definirse al menos una instrucción a ejecutar dentro del HANDLER.
En el ejemplo, mostraríamos el valor -1 y en un programa cliente, se podría indicar que en caso de llegar el valor -1 se ha producido el tipo de excepción 'Nombre de tabla desconocido' y programar la consecuencia...


Ejercicios propuestos Excepciones

Nota: Recordar que el tratamiento que vamos a dar a los 'errores' podría ser el de no lanzar una excepción o capturar las que se produjeran, y enviar al equipo 'cliente' un código de error (con un SELECT o un parámetro de salida) y que sea desde el equipo cliente donde, en función de ese código de error, avisar al usuario y realizar la acción oportuna.

En este caso, vamos a lanzar una excepción. Recordar que si estamos dentro de un guion, el lanzar una excepción provocaría el fin de ejecución del mismo.
Desde el punto de vista del programador, en el lado cliente, tendría que realizar la captura de excepciones (TRY...CATCH), y en función del ErrorCode o SQLState Code programar la respuesta adecuada al error.

Ejercicio 1

Crea un procedimiento de nombre animales_Delete que dado el nombre de un animal, lo borre. Antes tendrá que borrar todas las tablas relacionadas. En el caso de que el animal tenga menos de 2 años no estará permitido borrarlo y lanzará una excepción (recordar que el código de error para excepciones definidas por el usuario es el 45000) con el texto: No es posible dar de baja a animales con menos de dos años. En el caso de que el animal no exista, deberá lanzar una excepción con el texto 'Ese animal no existe' y un ErrorCode 1643.

Nota: Fijarse que este procedimiento borra los datos de varias tablas, por lo que necesitaríamos hacer uso de una transacción como veremos en la siguiente sección de la Wiki.

Ejercicio 2

Crea un procedimiento de nombre animales_addArtista al que se le pase el nombre de un animal y el nif de un artista y asigne el cuidador al animal. Deberá comprobar que el animal y el artista existen. En caso de que no, deberá lanzar una excepción con el ErrorCode 1643 y texto 'El animal no existe' o 'El artista no existe'.

Nota: Ahora pasamos a capturar las excepciones y enviamos al cliente (o a quien llame al procedimiento) un código y/o mensaje sobre el error capturado.

Debemos de tener en cuenta que la captura de excepciones siempre es recomendable ya que pueden producirse errores que no controlamos, por lo tanto, si estamos haciendo uso de un procedimiento/función desde un programa cliente, mejor siempre con TRY...CATCH.

Ejercicio 3

Crea un procedimiento de nombre animales_add que añade un nuevo animal. En caso de intentar dar de alta un animal con el mismo nombre, captura la excepción y haz que el procedimiento devuelva (con un select) -1. En caso de que el alta sea correcta, que devuelva 0.

En el caso de que el nombre de la pista o de la atracción no exista, captura la excepción y devuelve el valor -2.
Trabaja con nombres de excepciones en vez de con los números asociados. Crea las siguientes excepciones: ex_claveDuplicada, ex_pista_atracc_no_existe
Para saber el número de excepción que tienes que capturar, provoca el fallo y anota el número.
Fijarse que este procedimiento está pensado para llamarlo desde un programa cliente (con Java, C#,...) que guarde el valor del SELECT y en base a dicho valor avise al usuario. Si quisiéramos emplearlo como parte de un guion en Mysql necesitaríamos que fuera una función para poder guardar el valor de retorno o emplear un parámetro de salida.

Ejercicio 4

Crea un procedimiento de nombre artistas_add que añade un nuevo artista. Se debe comprobar si el nif exista. Esta comprobación se hará capturando la excepción correspondiente. En caso de error, se mandará en un parámetro de salida el valor -1. Si todo está correcto se mandará el valor 0.

Crea un nombre de excepción: ex_artista_duplicado.
Atención: la tabla ARTISTAS tiene un trigger (ejercicio 6) para evitar que se añada un jefe que no exista, lo cambia por el valor null. Como amplicación a este ejercicio lo tanto podéis borrar y capturar la excepción de que el jefe no exista (clave foránea no válida).
Junto a los datos del artista se debe enviar el nombre de un animal (debe estar previamente dado de alta) que va a cuidar el artista.
Se debe llamar al método 'animales_addArtista' (creado previamente) y capturar la posible excepción que lance dicho método empleando el SQLSTATE. Si el animal no existe debe de enviar en el parámetro de salida el valor -2.
Crea un nombre de excepción: ex_animal_no_existe.
Cualquier excepción que se produzca debe hacer que se salga del procedimiento.
Llama al método y comprueba el valor del parámetro de salida en cada caso.
Nota: Fijarse que este método es un candidato para realizar una transacción ya que va a afectar a dos tablas (va a añadir dos filas).


Solución Ejercicios propuestos Excepciones

Ejercicio 1

Crea un procedimiento de nombre animales_Delete que dado el nombre de un animal, lo borre. Antes tendrá que borrar todas las tablas relacionadas. En el caso de que el animal tenga menos de 2 años no estará permitido borrarlo y lanzará una excepción (recordar que el código de error para excepciones definidas por el usuario es el 45000) con el texto: No es posible dar de baja a animales con menos de dos años. En el caso de que el animal no exista, deberá lanzar una excepción con el texto 'Ese animal no existe' y un ErrorCode 1643.

USE CIRCO;
DROP PROCEDURE IF EXISTS animales_delete;

DELIMITER $$
CREATE PROCEDURE animales_delete(p_nombreAnimal VARCHAR(50))
COMMENT 'Da de baja un animal siempre que su edad sea superior a 2 años'
bloque_proc:BEGIN
	DECLARE v_anos TINYINT default -1;
    
    SELECT anhos
    INTO v_anos
    FROM ANIMALES
    WHERE nombre = p_nombreAnimal;
    
    CASE 
	WHEN v_anos = -1 THEN
		SIGNAL SQLSTATE '45000' 
		SET MESSAGE_TEXT='Ese animal no existe',
                    MYSQL_ERRNO = 1643;
                -- LEAVE bloque_proc;     NO HACE FALTA YA QUE SIGNAL HACE SALIR DEL PROCEDIMIENTO
        WHEN v_anos < 2 THEN
		SIGNAL SQLSTATE '45000' 
		SET MESSAGE_TEXT='No se puede dar de baja a animales con menos de dos años';
                -- LEAVE bloque_proc;     NO HACE FALTA YA QUE SIGNAL HACE SALIR DEL PROCEDIMIENTO
         ELSE BEGIN END;
    END CASE;

    -- BORRAMOS LOS ANIMALES. Necesitaríamos una transacción. 
    DELETE FROM ANIMALES_ARTISTAS
    WHERE nombre_animal = p_nombreAnimal;
    
    DELETE FROM ANIMALES
    WHERE nombre = p_nombreAnimal;

END$$
DELIMITER ;

Ejemmplo de uso:

1 call animales_delete('no_existe');   -- Devuelve el código 1643
2 
3 call animales_delete('Berni');   -- No cumple que la edad sea superior a 2 años.
4 
5 call animales_delete('Princesa2');   -- Lo da de baja de todas las tablas

Ejercicio 2

Crea un procedimiento de nombre animales_addArtista al que se le pase el nombre de un animal y el nif de un artista y asigne el cuidador al animal. Deberá comprobar que el animal y el artista existen. En caso de que no, deberá lanzar una excepción con el ErrorCode 1643 y texto 'El animal no existe' o 'El artista no existe'.

En el caso de que ya exista ese artista con ese animal el propio Mysql lanzará una excepción de clave primaria duplicada.
 1 USE CIRCO;
 2 DROP PROCEDURE IF EXISTS animales_addArtista;
 3 
 4 DELIMITER $$
 5 CREATE PROCEDURE animales_addArtista(p_nombreAnimal VARCHAR(50),p_nif char(9))
 6 COMMENT 'Asigna un nuevo artista a un animal'
 7 bloque_proc:BEGIN
 8     
 9     IF (SELECT COUNT(*)
10 		FROM ANIMALES
11 		WHERE nombre = p_nombreAnimal)=0 THEN
12 	
13 		SIGNAL SQLSTATE '45000' 
14 			SET MESSAGE_TEXT='Ese animal no existe',
15                 MYSQL_ERRNO = 1643;
16                 -- LEAVE bloque_proc;     NO HACE FALTA YA QUE SIGNAL HACE SALIR DEL PROCEDIMIENTO
17     END IF;
18         
19     IF (SELECT COUNT(*)
20 		FROM ARTISTAS
21 		WHERE nif = p_nif)=0 THEN
22 		SIGNAL SQLSTATE '45000' 
23 			SET MESSAGE_TEXT='Ese artista no existe',
24                 MYSQL_ERRNO = 1643;
25                 -- LEAVE bloque_proc;     NO HACE FALTA YA QUE SIGNAL HACE SALIR DEL PROCEDIMIENTO
26     END IF;
27 		
28     INSERT INTO ANIMALES_ARTISTAS (nombre_animal,nif_artista)
29     VALUES (p_nombreAnimal,p_nif);
30 END$$
31 DELIMITER ;

Ejemmplo de uso:

1 call animales_addArtista('no_existe','11111111A'); 
2 call animales_addArtista('Princesa1','no_existe'); 
3 
4 call animales_addArtista('Leo','33333333C'); -- Si lanzamos dos veces esta orden Mysql lanzará una excepción de clave duplicada.


Nota: Ahora pasamos a capturar las excepciones y enviamos al cliente (o a quien llame al procedimiento) un código y/o mensaje sobre el error capturado.

Debemos de tener en cuenta que la captura de excepciones siempre es recomendable ya que pueden producirse errores que no controlamos, por lo tanto, si estamos haciendo uso de un procedimiento/función desde un programa cliente, mejor siempre con TRY...CATCH.
A partir de ahora somos nosotros los que decidimos que excepciones capturamos y el tratamiento que vamos a hacer cuando se produzca (paramos de ejecutar el procedimiento, continuamos,...va a depender del tipo de error y de nuestro programa).

Ejercicio 3

Crea un procedimiento de nombre animales_add que añade un nuevo animal. En caso de intentar dar de alta un animal con el mismo nombre, captura la excepción y haz que el procedimiento devuelva (con un select) -1. En caso de que el alta sea correcta, que devuelva 0.

En el caso de que el nombre de la pista o de la atracción no exista, captura la excepción y devuelve el valor -2.
Trabaja con nombres de excepciones en vez de con los números asociados. Crea las siguientes excepciones: ex_claveDuplicada, ex_pista_atracc_no_existe
Para saber el número de excepción que tienes que capturar, provoca el fallo y anota el número.
Si provocamos los errores podemos comprobar que:
  • Error 1062: Clave primaria duplicada
  • Error 1452: Error de clave foránea.
Como el procedimiento sólo va a tener una orden INSERT, no hace falta hacer que salga del mismo al producirse la excepción.
USE CIRCO;
DROP PROCEDURE IF EXISTS animales_add;

DELIMITER $$
CREATE PROCEDURE animales_add(p_nombreAnimal VARCHAR(50),p_tipo varchar(9),p_anos tinyint, p_peso float, p_estatura float, p_atraccion varchar(50),p_pista varchar(50))
COMMENT 'Añade un nuevo animal'
BEGIN

    DECLARE ex_claveDuplicada CONDITION FOR 1062;
    DECLARE ex_pista_atracc_no_existe CONDITION FOR 1452;
    
    DECLARE EXIT HANDLER FOR ex_claveDuplicada
    BEGIN
        SELECT -1;
    END;
    DECLARE EXIT HANDLER FOR ex_pista_atracc_no_existe
    BEGIN
        SELECT -2;
    END;
    
    INSERT INTO ANIMALES (nombre,tipo,anhos,peso,estatura,nombre_atraccion,nombre_pista)
    VALUES (p_nombreAnimal,p_tipo,p_anos,p_peso,p_estatura,p_atraccion,p_pista);

    SELECT 0;
END$$
DELIMITER ;

Ejemmplo de uso: Fijarse que no se produce una excepción en la llamada al procedimiento, por lo que la ejecución con órdenes posteriores seguiría.

1 call animales_add('Leo','León',3,230,1.34,null,null);   -- Clave duplicada
2 call animales_add('Leo2','León',3,230,1.34,'NO EXISTE',null);   -- Atraccion que no existe
3 
4 call animales_add('Leo2','León',3,230,1.34,'El gran felino','SUPER');   -- Todo correcto

Ejercicio 4

Crea un procedimiento de nombre artistas_add que añade un nuevo artista. Se debe comprobar si el nif exista. Esta comprobación se hará capturando la excepción correspondiente. En caso de error, se mandará en un parámetro de salida el valor -1. Si todo está correcto se mandará el valor 0.

Crea un nombre de excepción: ex_artista_duplicado.
Atención: la tabla ARTISTAS tiene un trigger (ejercicio 6) para evitar que se añada un jefe que no exista, lo cambia por el valor null. Como amplicación a este ejercicio lo tanto podéis borrar y capturar la excepción de que el jefe no exista (clave foránea no válida).
Junto a los datos del artista se debe enviar el nombre de un animal (debe estar previamente dado de alta) que va a cuidar el artista.
Se debe llamar al método 'animales_addArtista' (creado previamente) y capturar la posible excepción que lance dicho método empleando el SQLSTATE. Si el animal no existe debe de enviar en el parámetro de salida el valor -2.
Crea un nombre de excepción: ex_animal_no_existe.
Cualquier excepción que se produzca debe hacer que se salga del procedimiento.
Llama al método y comprueba el valor del parámetro de salida en cada caso.
Nota: Fijarse que este método es un candidato para realizar una transacción ya que va a afectar a dos tablas (va a añadir dos filas).
 1 USE CIRCO;
 2 DROP PROCEDURE IF EXISTS artistas_add;
 3 
 4 DELIMITER $$
 5 CREATE PROCEDURE artistas_add(p_nif CHAR(9),p_apellidos varchar(100), p_nombre varchar(50),p_nifJefe char(9),p_nombreAnimal varchar(50), OUT p_resultado tinyint)
 6 COMMENT 'Añade un nuevo artista'
 7 BEGIN
 8     DECLARE ex_artista_duplicado CONDITION FOR 1062;
 9     DECLARE ex_animal_no_existe CONDITION FOR SQLSTATE '45000';
10     
11     DECLARE EXIT HANDLER FOR ex_artista_duplicado
12        SET p_resultado = -1;
13     DECLARE EXIT HANDLER FOR ex_animal_no_existe
14         SET p_resultado = -2;
15     
16     INSERT INTO ARTISTAS (nif, apellidos, nombre, nif_jefe)
17     VALUES (p_nif, p_apellidos, p_nombre, p_nifJefe);
18 
19     CALL animales_addArtista(p_nombreAnimal,p_nif);
20     SET p_resultado = 0;
21 
22 END$$
23 DELIMITER ;

Ejemmplo de uso: Fijarse que no se produce una excepción en la llamada al procedimiento, por lo que la ejecución con órdenes posteriores seguiría.

1 CALL artistas_add('11111111A','Orten Sanlat','Cristina',null, 'Leo', @resultado);   -- Devuelve -1 => Clave primaria duplicada
2 SELECT @resultado;
1 CALL artistas_add('11111122F','Orten Sanlat','Cristina', '92828822A', 'Leo', @resultado);   -- Devuelve 0 => Nif jefe no existe pero el trigger lo cambia por null
2 SELECT @resultado;
1 CALL artistas_add('11111144F','Jiménez Royal','Lucas', null, 'Leo_NO_EXISTE', @resultado);   -- Devuelve -2 => Animal no existe PERO al no estar en una transacción, el artista se ha añadido
2 SELECT @resultado;


Enlace a la página principal de la UD7

Enlace a la página principal del curso


-- Ángel D. Fernández González -- (2020).