Disparadores y Cursores¶
Propuesta didáctica¶
En esta UT terminaremos de trabajar el RA5: Desarrolla procedimientos almacenados evaluando y utilizando las sentencias del lenguaje incorporado en el sistema gestor de bases de datos.
Criterios de evaluación¶
- CE5h: Se han definido eventos y disparadores.
- CE5i: Se han utilizado cursores.
- CE5j: Se han utilizado excepciones.
Contenidos¶
Programación de bases de datos:
- Eventos y disparadores.
- Excepciones.
- Cursores.
Cuestionario inicial
- ¿Qué es un evento?
- ¿Qué es el programador de eventos? ¿Está siempre activo? ¿Cómo puedo comprobarlo?
- ¿Cómo creo un evento para se ejecute dentro de 5 minutos?
- ¿Cómo creo un evento que debe comenzar el lunes a las 8:00 y finalizar el viernes a las 17:00 y que se ejecute cada hora?
- ¿Cómo podemos ver todos los eventos programados actualmente en la base de datos?
- ¿Qué es un disparador/trigger?
- ¿A qué posibles eventos responde un trigger?
- ¿Puedo lanzar un trigger después de otro? ¿Cómo?
- ¿Cuál es la sintaxis básica para crear un disparador en MariaDB?
- ¿Qué es la gestión de errores en PL/SQL y por qué es importante?
- ¿Qué palabra clave se utiliza en PL/SQL para manejar excepciones?
- Hablando de gestión de errores, ¿Para qué sirven las señales?
- ¿En qué casos es útil relanzar una señal?
- ¿Cómo se gestionan las transacciones dentro de un PL/SQL?
- ¿Qué son los cursores en PL/SQL y para qué se utilizan?
- ¿Cómo se declara un cursor en PL/SQL?
- ¿Cuáles son los pasos a la hora de utilizar un cursor?
Programación de Aula (15h)¶
Esta unidad es la onceava, impartiéndose a principios de la tercera evaluación, a mitad de marzo, con una duración estimada de 15 sesiones lectivas:
Sesión | Contenidos | Actividades | Criterios trabajados |
---|---|---|---|
1 | Eventos | ||
2 | Supuesto eventos | AC1101 | CE5h |
3 | Disparadores | AC1103 | CE5h |
4 | Supuesto disparadores | AC1104 | CE5h |
5 | Gestión de errores | AC1106 | CE5j |
6 | Supuesto errores | AC1107 | CE5j |
7 | Señales | AC1108 | CE5j |
8 | Transacciones en PL/SQL | AC1109 | CE5j |
9 | Cursores | AC1111 | CE5i |
10 | Supuesto cursores | AC1112 | CE5i |
11 | Supuesto examen | AC1113 | RABD.5 |
12 | Reto PL/SQL | PY1114 | RABD.5 |
13 | Prueba escrita I | PO1115 | RABD.5 |
14 | Prueba escrita II | ||
15 | Revisión prueba escrita |
Eventos¶
Un evento en MariaDB es una tarea programada que se ejecuta de forma automática en momentos específicos o de manera periódica. Funciona de forma similar a un cron job en sistemas Linux, permitiendo definir instrucciones SQL que se ejecutarán sin intervención manual, dentro del propio motor de base de datos.
Se utilizan para realizar acciones planificadas, como la actualización de datos, la eliminación de registros obsoletos o la generación de informes en momentos predeterminados. Además, facilitan la automatización del mantenimiento y la administración de la base de datos, ya que centralizan tareas repetitivas en un único punto. Pueden configurarse para ejecutarse una sola vez o en intervalos regulares, brindando flexibilidad para adaptarse a las necesidades específicas del sistema y asegurando que las operaciones críticas se realicen de forma oportuna y consistente.
Para su funcionamiento, es obligatorio que el programador de eventos esté habilitado, y si no, hacerlo:
-- Verificar estado actual
SHOW VARIABLES LIKE 'event_scheduler';
-- Habilitar el programador de eventos (método 1 - sesión actual)
SET GLOBAL event_scheduler = ON;
-- Habilitar el programador de eventos (método 2 - permanente en my.cnf)
-- Agregar en archivo de configuración:
-- event_scheduler=ON
Para comprobar los procesos en ejecución, ejecutaremos SHOW PROCESSLIST
:
SHOW PROCESSLIST;
-- +----+-----------------+-----------+---------+---------+------+------------------------+------------------+----------+
-- | Id | User | Host | db | Command | Time | State | Info | Progress |
-- +----+-----------------+-----------+---------+---------+------+------------------------+------------------+----------+
-- | 3 | s8a | localhost | pruebas | Query | 0 | starting | show processlist | 0.000 |
-- | 4 | event_scheduler | localhost | NULL | Daemon | 51 | Waiting on empty queue | NULL | 0.000 |
-- +----+-----------------+-----------+---------+---------+------+------------------------+------------------+----------+
-- 2 rows in set (0.001 sec)
Creación de eventos¶
Para crear un evento usaremos la sentencia CREATE EVENT
, con la siguiente sintaxis:
CREATE EVENT nombre_evento
ON SCHEDULE horario
[ON COMPLETION [NOT] PRESERVE]
[ENABLE | DISABLE | DISABLE ON SLAVE]
[COMMENT 'comentario']
DO
sentencias_sql;
horario:
AT timestamp [+ INTERVAL intervalo] ... | EVERY intervalo
[STARTS timestamp [+ INTERVAL intervalo] ...]
[ENDS timestamp [+ INTERVAL intervalo] ...]
intervalo:
cantidad {YEAR | QUARTER | MONTH | DAY | HOUR | MINUTE |
WEEK | SECOND | YEAR_MONTH | DAY_HOUR | DAY_MINUTE |
DAY_SECOND | HOUR_MINUTE | HOUR_SECOND | MINUTE_SECOND}
donde:
horario
: Define cuándo y con qué frecuencia se ejecuta, pudiendo ser en una fecha concreta (AT
) o de forma repetida (EVERY
), definiendo un inicio (STARTS
) y/o un fin (ENDS
), incluso detallando el instante temporal desde años a minutos.ON COMPLETION
: Determina si el evento se conserva después de su ejecuciónENABLE/DISABLE
: Estado inicial del eventoDO
: indica el bloque de código SQL a ejecutar
Preparando
Para probar los eventos, vamos a crear una tabla histórico que admita una fecha y una cadena:
CREATE TABLE historico (
id INT AUTO_INCREMENT PRIMARY KEY,
fecha DATETIME NOT NULL,
descripcion VARCHAR(255) NOT NULL
);
Evento sencillo¶
Podemos crear eventos que se ejecutan una sola vez, configurando un horario concreto, ya sea un timestamp determinado con AT TIMESTAMP
, o un instante temporal calculado (CURRENT_TIMESTAMP + INTERVAL ...
).
CREATE EVENT evento_unico
ON SCHEDULE AT TIMESTAMP '2025-02-27 06:55:00'
DO
INSERT INTO historico(fecha, descripcion) VALUES (NOW(), 'Evento único ejecutado');
CREATE EVENT evento_5min
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 5 MINUTE
DO
INSERT INTO historico(fecha, descripcion) VALUES (NOW(), 'Evento ejecutado dentro de 5 minutos');
Evento recurrente¶
Pero la potencia de los eventos es que se ejecuten de forma periódica. Para ello, en el horario usaremos la cláusula EVERY
para indicar la repetición
CREATE EVENT evento_recurrente
ON SCHEDULE EVERY 1 DAY
STARTS '2025-03-01 00:00:00' ENDS '2025-03-31 23:59:59'
DO
DELETE FROM historico WHERE fecha < DATE_SUB(NOW(), INTERVAL 30 DAY);
También podemos indicar una fecha de inicio para comenzar el evento periódico mediante STARTS
, que finalice con ENDS
o usando ambos para indicar un periodo determinado:
CREATE EVENT evento_recurrente_con_inicio
ON SCHEDULE EVERY 12 HOUR STARTS '2025-03-01 00:00:00'
DO
CALL actualizar_inventario();
CREATE EVENT evento_recurrente_con_fin
ON SCHEDULE EVERY 1 WEEK ENDS '2025-12-31 23:59:59'
DO
CALL actualizar_inventario();
CREATE EVENT evento_recurrente_con_inicio_fin
ON SCHEDULE EVERY 1 DAY + INTERVAL 12 HOUR STARTS '2025-03-01 00:00:00' ENDS '2025-12-31 23:59:59'
DO
CALL actualizar_inventario();
Una vez creados los eventos, cuando llegue el momento adecuado, se ejecutarán. En el fichero de log de MariaDB (por ejemplo, desde la consola de Docker Desktop o en /var/log/mysql/
) podremos comprobar su ejecución:
2025-02-27 17:50:30 2025-02-27 16:50:30 4 [Note] Event Scheduler: scheduler thread started with id 4
2025-02-27 17:55:00 2025-02-27 16:55:00 4 [Note] Event Scheduler: Last execution of pruebas.evento_unico. Dropping.
2025-02-27 17:55:00 2025-02-27 16:55:00 5 [Note] Event Scheduler: Dropping pruebas.evento_unico
2025-02-27 17:57:22 2025-02-27 16:57:22 4 [Note] Event Scheduler: Last execution of pruebas.evento_5min. Dropping.
2025-02-27 17:57:22 2025-02-27 16:57:22 6 [Note] Event Scheduler: Dropping pruebas.evento_5min
Gestión de eventos¶
Para listar los eventos existentes, usaremos SHOW EVENTS
, donde veremos los eventos que están pendiente de ejecutar:
SHOW EVENTS;
-- +---------+-------------------+---------------+-----------+-----------+------------+----------------+----------------+---------------------+---------------------+---------+------------+----------------------+----------------------+-----------------------+
-- | Db | Name | Definer | Time zone | Type | Execute at | Interval value | Interval field | Starts | Ends | Status | Originator | character_set_client | collation_connection | Database Collation |
-- +---------+-------------------+---------------+-----------+-----------+------------+----------------+----------------+---------------------+---------------------+---------+------------+----------------------+----------------------+-----------------------+
-- | pruebas | evento_recurrente | s8a@localhost | SYSTEM | RECURRING | NULL | 1 | DAY | 2025-03-01 00:00:00 | 2025-03-31 23:59:59 | ENABLED | 1 | utf8mb3 | utf8mb3_general_ci | utf8mb4_uca1400_ai_ci |
-- +---------+-------------------+---------------+-----------+-----------+------------+----------------+----------------+---------------------+---------------------+---------+------------+----------------------+----------------------+-----------------------+
-- 1 row in set (0.002 sec)
En el caso de querer obtener únicamente los eventos de una base de datos concreta, necesitamos filtrar mediante SHOW EVENTS FROM nombre_base_datos;
.
Si queremos modificar un evento, ya sea su periodicidad o su código, así como habilitarlos o deshabilitarlos, usaremos la sentencia ALTER EVENT
:
-- Modificar la programación o el código
ALTER EVENT nombre_evento
ON SCHEDULE EVERY 2 DAY
DO
/* Nuevo código */;
-- Habilitar o deshabilitar un evento
ALTER EVENT nombre_evento ENABLE;
ALTER EVENT nombre_evento DISABLE;
Destacar que en ocasiones nos interesará deshabilitar un determinado evento (con la idea de volver a habilitarlo en el futuro), pero en otros eliminarlo completamente. Para ello, usaremos DROP EVENT
:
DROP EVENT nombre_evento;
Consideraciones
Cuando un desarrollador se adentra en el mundo de los eventos en MariaDB, debe tener en cuenta varias consideraciones importantes para asegurar que estos procesos automatizados funcionen de manera óptima. Ante todo, es fundamental verificar que el Event Scheduler esté activado en el sistema. Si se reinicia el servidor, dependiendo de la configuración, puede ser que el programador de eventos esté deshabilitado.
Otro aspecto crucial es el manejo de los errores (lo estudiaremos más adelante en esta unidad) capturando excepciones y registrando la información relevante en tablas de logs, ofreciendo mecanismos para monitorizar los eventos en ejecución y cuál ha sido su estado de ejecución, mediante alguna tabla específica que registre información detallada sobre la ejecución de cada evento (incluyendo tiempos, resultados y cualquier anomalía), lo que nos proporcionará una ayuda invaluable cuando surjan los problemas (que tarde o temprano, sucederán).
Finalmente, siempre deberemos controlar los tiempos de ejecución, para evitar operaciones muy largas, así como evitar solapamientos entre diferentes eventos que sobrescriban la misma información.
Triggers¶
Directamente relacionados con los eventos, tenemos los disparadores (triggers) que son fragmentos de código que se activan automáticamente en respuesta a operaciones específicas sobre las tablas, tales como inserciones, actualizaciones o eliminaciones.
Estos mecanismos permiten insertar lógica personalizada que se ejecuta antes (before) o después (after) de que se realice la orden SQL que provoca su ejecución, lo que es fundamental para garantizar la integridad de los datos, validar condiciones de negocio y llevar a cabo auditorías automáticas. Por ejemplo, un disparador puede registrar cada cambio en una tabla de auditoría o prevenir modificaciones que no cumplan con ciertos criterios, asegurando así la consistencia y confiabilidad de la información almacenada.
Los posibles eventos que podemos asociar a un disparador son:
INSERT
: El trigger se activa cuando se inserta una nueva fila sobre la tabla asociada.UPDATE
: Se activa cuando se actualiza una fila.DELETE
: Se activa cuando se elimina una fila.

Si la operación SQL que va a provocar la ejecución del trigger afecta a múltiples filas (por ejemplo un borrado de varios registros), el trigger se ejecutará una vez por cada fila afectada.
Creación de disparadores¶
A la hora de crear un disparador, necesitamos definir:
- Sobre qué tabla se va a aplicar
- Sobre qué operación se va a realizar (
INSERT
,DELETE
,UPDATE
) - Y cuando queremos que se ejecute, antes (
BEFORE
) o después (AFTER
) de la operación sobre la tabla.
Para ello, usaremos la sentencia CREATE TRIGGER
con la siguiente sintaxis:
CREATE [OR REPLACE] TRIGGER nombreTrigger cuandoTrigger eventoTrigger
ON nombreTabla FOR EACH ROW
[ordenTrigger]
cuerpo
cuandoTrigger: { BEFORE | AFTER }
eventoTrigger: { INSERT | UPDATE | DELETE }
ordenTrigger: { FOLLOWS | PRECEDES } otroNombreTrigger
Dentro del cuerpo del disparador, vamos a poder acceder a los valores del registro de la tabla anterior y posterior a la orden SQL. Para ello, el alias NEW
referencia al nuevo registro, y OLD
al antiguo. Dependiendo de la operación, podremos emplear uno u otro, o los dos:
Operación | NEW |
OLD |
---|---|---|
UPDATE |
Valores nuevos | Valores antiguos |
INSERT |
Valores nuevos | No existe |
DELETE |
No existe | Valores antiguos |
Cabe destacar que los triggers de tipo AFTER DELETE
se activan solo cuando realmente se eliminan filas. Si el DELETE
no afecta a ninguna fila (por ejemplo, porque la condición WHERE
no coincide con ninguna fila), entonces el trigger no se dispara.
Veamos un ejemplo donde almacenamos las notas de los estudiantes de un curso, y queremos comprobar si al insertar los valores son correctos, y si no lo son, ajustarlos:
create or replace table estudiantes (
id int unsigned auto_increment primary key,
nombre varchar(50) not null,
apellidos varchar(50) not null,
nota float
);
DELIMITER //
CREATE OR REPLACE TRIGGER triggerCheckNotaBeforeInsert
BEFORE INSERT ON estudiantes FOR EACH ROW
begin
if NEW.nota < 0 then -- NEW referencia al nuevo registro
set NEW.nota = 0;
elseif NEW.nota > 10 then
set NEW.nota = 10;
end if;
end
//
DELIMITER ;
insert into estudiantes values (1, 'Andreu', 'Medrano', -1);
insert into estudiantes values (2, 'José Manuel', 'Pérez', 12);
insert into estudiantes values (3, 'Pedro', 'Casas', 8.5);
select * from estudiantes;
-- +----+--------------+-----------+------+
-- | id | nombre | apellidos | nota |
-- +----+--------------+-----------+------+
-- | 1 | Andreu | Medrano | 0 |
-- | 2 | José Manuel | Pérez | 10 |
-- | 3 | Pedro | Casas | 8.5 |
-- +----+--------------+-----------+------+
-- 3 rows in set (0.000 sec)
Podemos comprobar como dentro del cuerpo del disparador hemos usado NEW
para referenciar el registro que vamos a insertar. Además, hemos configurado el disparador para que se ejecute antes de la inserción.
Nomenclatura
En MariaDB, no hay una nomenclatura obligatoria para nombrar los triggers, pero se recomienda seguir una convención clara y estructurada para facilitar su identificación y mantenimiento.
Algunas recomendaciones:
- Usar un prefijo indicativo, como por ejemplo
trg_
otrigger_
, ya sea usando como separador_
o una notación camelCase. - Incluir el nombre de la tabla afectada
- Especificar el evento que lo dispara, ya sea
BEFORE
oAFTER
y la operaciónINSERT
,UPDATE
oDELETE
Algunos ejemplos podrían ser:
Nombre del Trigger | Explicación |
---|---|
trg_usuarios_before_insert |
Se ejecuta antes de una inserción en usuarios . |
triggerPedidosAfterUpdate |
Se activa después de actualizar pedidos , posiblemente para registrar cambios. |
trgProductosBeforeDeleteCascade |
Antes de eliminar en productos , quizás para eliminar dependencias. |
De la misma manera, podemos crear un disparador para las operaciones de modificación:
DELIMITER //
CREATE OR REPLACE TRIGGER triggerCheckNotaBeforeUpdate
BEFORE UPDATE ON estudiantes FOR EACH ROW
BEGIN
if NEW.nota < 0 then -- NEW referencia al nuevo registro
set NEW.nota = 0;
elseif NEW.nota > 10 then
set NEW.nota = 10;
end if;
end
//
DELIMITER ;
update estudiantes set nota = -4 where id = 3;
update estudiantes set nota = 14 where id = 3;
update estudiantes set nota = 9.5 where id = 3;
select * from estudiantes;
-- +----+--------------+-----------+------+
-- | id | nombre | apellidos | nota |
-- +----+--------------+-----------+------+
-- | 1 | Andreu | Medrano | 0 |
-- | 2 | José Manuel | Pérez | 10 |
-- | 3 | Pedro | Casas | 9.5 |
-- +----+--------------+-----------+------+
-- 3 rows in set (0.000 sec)
Triggers encadenados
Es posible tener más de un trigger sobre la misma tabla y con el mismo evento. En estos casos, la ejecución se hará uno después de otro en el orden en que fueron creados.
Si queremos modificar dicho orden, a la hora de crear el trigger tendremos que utilizar la opción ordenTrigger: { FOLLOWS | PRECEDES } otroNombreTrigger
. De tal forma que el trigger que se está creando se ejecute antes (PRECEDES
) o después (FOLLOWS
) que el trigger que ya existe (otroNombreTrigger
).
Es importante destacar que solo puedes encadenar triggers del mismo evento (INSERT
, UPDATE
, DELETE
) y tiempo (BEFORE
, AFTER
).
Supongamos el siguiente ejemplo donde definimos tres disparadores que actúan sobre la misma tabla de ventas
: el primero que inserta en el log, el segundo que modifica el inventario y por último, el tercero que inserta en la tabla de estadísticas, y que queremos que se realice antes de insertar en el log:
-- Primer trigger
CREATE TRIGGER after_insert_ventas_1
AFTER INSERT ON ventas
FOR EACH ROW
BEGIN
INSERT INTO log_ventas (venta_id, accion) VALUES (NEW.id, 'Nueva venta registrada');
END;
-- Segundo trigger que debe ejecutarse DESPUÉS del primero
CREATE TRIGGER after_insert_ventas_2
AFTER INSERT ON ventas
FOR EACH ROW
FOLLOWS after_insert_ventas_1
BEGIN
UPDATE inventario SET cantidad = cantidad - NEW.cantidad WHERE producto_id = NEW.producto_id;
END;
-- Tercer trigger que debe ejecutarse ANTES del primero
CREATE TRIGGER after_insert_ventas_3
AFTER INSERT ON ventas
FOR EACH ROW
PRECEDES after_insert_ventas_1
BEGIN
INSERT INTO estadisticas_ventas (fecha, monto) VALUES (CURRENT_DATE(), NEW.monto);
END;
Si comprobamos bien las cláusulas PRECEDES
y FOLLOWS
, tenemos que el orden de ejecución sería:
after_insert_ventas_3
(porquePRECEDES
after_insert_ventas_1
)after_insert_ventas_1
after_insert_ventas_2
(porqueFOLLOWS
after_insert_ventas_1
)
Gestión de disparadores¶
Los comandos que podemos utilizar para gestionar los disparadores son:
- DROP TRIGGER nombreTrigger: permite eliminar un disparador
-
SHOW TRIGGERS: muestra los disparadores existentes
SHOW TRIGGERS; -- +------------------------------+--------+-------------+------------------------------------------------------------------------------------------------------------------------+--------+------------------------+-------------------------------------------------------------------------------------------+---------------+----------------------+----------------------+-----------------------+ -- | Trigger | Event | Table | Statement | Timing | Created | sql_mode | Definer | character_set_client | collation_connection | Database Collation | -- +------------------------------+--------+-------------+------------------------------------------------------------------------------------------------------------------------+--------+------------------------+-------------------------------------------------------------------------------------------+---------------+----------------------+----------------------+-----------------------+ -- | triggerCheckNotaBeforeInsert | INSERT | estudiantes | BEGIN -- IF NEW.nota < 0 THEN -- set NEW.nota = 0; -- ELSEIF NEW.nota > 10 THEN -- set NEW.nota = 10; -- END IF; -- END | BEFORE | 2025-02-23 18:32:28.15 | STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION | s8a@localhost | utf8mb3 | utf8mb3_general_ci | utf8mb4_uca1400_ai_ci | -- | triggerCheckNotaBeforeUpdate | UPDATE | estudiantes | BEGIN -- IF NEW.nota < 0 THEN -- set NEW.nota = 0; -- ELSEIF NEW.nota > 10 THEN -- set NEW.nota = 10; -- END IF; -- END | BEFORE | 2025-02-23 18:36:51.74 | STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION | s8a@localhost | utf8mb3 | utf8mb3_general_ci | utf8mb4_uca1400_ai_ci | -- +------------------------------+--------+-------------+------------------------------------------------------------------------------------------------------------------------+--------+------------------------+-------------------------------------------------------------------------------------------+---------------+----------------------+----------------------+-----------------------+ -- 2 rows in set (0.003 sec)
-
SHOW CREATE TRIGGER nombreTrigger: muestra el código fuente de un determinado disparador
SHOW CREATE TRIGGER triggerCheckNotaBeforeUpdate; -- +------------------------------+-------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------+----------------------+-----------------------+------------------------+ -- | Trigger | sql_mode | SQL Original Statement | character_set_client | collation_connection | Database Collation | Created | -- +------------------------------+-------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------+----------------------+-----------------------+------------------------+ -- | triggerCheckNotaBeforeUpdate | STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION | CREATE DEFINER=`s8a`@`localhost` TRIGGER triggerCheckNotaBeforeUpdate -- BEFORE UPDATE ON estudiantes FOR EACH ROW -- BEGIN -- IF NEW.nota < 0 THEN -- set NEW.nota = 0; -- ELSEIF NEW.nota > 10 THEN -- set NEW.nota = 10; -- END IF; -- END | utf8mb3 | utf8mb3_general_ci | utf8mb4_uca1400_ai_ci | 2025-02-23 18:36:51.74 | -- +------------------------------+-------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------+----------------------+-----------------------+------------------------+
De estas operaciones podemos deducir que los disparadores no se pueden modificar; es necesario borrarlos y volverlos a crear.
Gestión de Errores¶
Hasta ahora hemos supuesto que nuestros fragmentos de código nunca fallan. Pero ¿qué sucede cuando un procedimiento o un evento tienen un error? Ya sea porque no encuentra una tabla, accedemos erróneamente a un campo que no existe, etc..
Una gestión de errores efectiva en MariaDB mejora significativamente la robustez y mantenibilidad de las aplicaciones. Para ello, MariaDB ofrece varios mecanismos para la gestión de errores, que incluyen señales, manejadores, códigos de error y excepciones, que permiten:
- Controlar el flujo de ejecución ante situaciones excepcionales.
- Proporcionar retroalimentación clara a los usuarios y desarrolladores.
- Mantener la integridad de los datos mediante transacciones.
- Implementar reglas de negocio complejas.
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 (incluso una combinación de las dos):
- 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).
Cuando trabajamos con errores y excepciones, podemos capturar:
- Error y excepciones definidas por el SGBD (por ejemplo,
DUPLICATE ENTRY FOR KEY
, que se produce cuando intentamos añadir dos filas con la misma clave primaria). - Excepciones creadas por el usuario. Por ejemplo, puedo lanzar una excepción cuando intente añadir una persona 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 sistema gestor.
Códigos de error
Cuando sucede algún tipo de fallo, MariaDB devolverá un error con un código. Cada código tiene un mensaje asociado y un estado SQLSTATE
correspondiente (los códigos son números enteros, mientras que los estados SQL con cadenas y por tanto irán entre comilla). Algunos códigos son:
Código | Mensaje | SQLSTATE |
---|---|---|
1007 | No se puede crear la base de datos | 42S01 |
1045 | Acceso denegado para el usuario | 28000 |
1050 | La tabla ya existe | 42S01 |
1062 | Entrada duplicada | 23000 |
1064 | Error de sintaxis SQL | 42000 |
1146 | La tabla no existe | 42502 |
Por ejemplo, si intento hacer una consulta sobre una tabla que no existe recibo un mensaje de error con su código:
select * from tablamala;
-- ERROR 1146 (42S02): Table 'empresa.tablamala' doesn't exist
Tras ejecutar una consulta que ha devuelto un error, puedo recuperar el error mediante SHOW ERRORS
:
SHOW ERRORS;
-- +-------+------+-----------------------------------------+
-- | Level | Code | Message |
-- +-------+------+-----------------------------------------+
-- | Error | 1146 | Table 'pruebas.tablamala' doesn't exist |
-- +-------+------+-----------------------------------------+
-- 1 row in set (0.000 sec)
Los códigos de error del 1900 hacía arriba son específicos de MariaDB, mientras que los comprendidos entre 1000 y 1800 son los mismos que MySQL.
Alias de error
Algunos errores son tan comunes que existen unos alias para referenciar tanto a un código concreto como a un conjunto de ellos. Son tres:
SQLWARNING
: valores deSQLSTATE
que empiezan por01
.NOT FOUND
: valores deSQLSTATE
que empiezan por02
. Se utiliza con cursores cuando se ha llegado al final de los datos, lo que provoca una condiciónNO DATA
conSQLSTATE
igual a02000
.SQLEXCEPTION
: valores deSQLSTATE
que empiezan por00
,01
o02
.
Mejorando la legibilidad¶
Dentro de un procedimiento voy a poder capturar los errores, pero si no quiero hacerlo por su código (1050
), estado (42S01
) o alias genérico (SQLEXCEPTION
), puedo hacerlo por un nombre más específico que mejore la legibilidad del código, mediante DECLARE CONDITION
:
DECLARE no_existe_tabla CONDITION FOR 1051;
De esta manera, después podré controlar la captura por el nombre asignado y no por el número asociado.
Ya hemos visto que en la lista de mensajes de error aparecen dos tipos de errores asociados a cada mensaje:
- Error Code: Son números específicos de MariaDB/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 están estandarizados entre todos los SGBD.
Emplear uno u otro va a depender de nuestro programa cliente. Si este va a ser empleado en sistemas gestores diferentes (por ejemplo, Oracle o SQL Server) podría ser mejor emplear los código de estado SQL. En caso contrario, es recomendable utilizar 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:
DECLARE no_existe_tabla CONDITION FOR SQLSTATE '42S02';
Uso de señales¶
La instrucción SIGNAL
permite generar errores personalizados, lo que es útil para validaciones de negocio, es decir, operaciones que tienen sentido para nuestra base de datos, por ejemplo, una regla podría ser que "todo empleado debe tener al menos dos habilidades".
Su sintaxis es :
SIGNAL SQLSTATE 'valor_sqlstate'
SET MESSAGE_TEXT = 'mensaje_de_error' [,MYSQL_ERRNO = codigo_error];
Si retomamos el mismo ejemplo, podríamos hacer:
DELIMITER //
CREATE PROCEDURE verificarEmpleado(IN codigoEmpleado INT)
BEGIN
DECLARE cantHab INT;
SELECT COUNT(CodHab) into cantHab from habemp where CodEmp = codigoEmpleado;
IF cantHab < 2 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Todo empleado debe tener mínimo 2 habilidades';
ELSE
SELECT 'Usuario verificado correctamente' AS mensaje;
END IF;
END //
DELIMITER ;
Se recomienda que los código de SQLSTATE
asociados a reglas de negocio vayan desde 45000 a 45999.
Manejadores de errores¶
Para añadirle un comportamiento a la gestión de errores, necesitamos declarar un manejador de errores en nuestro código, mediante DECLARE HANDLER
, sentencia que tiene la siguiente sintaxis:
DECLARE accionManejador HANDLER FOR valorCondicion [, valorCondicion] ...
sentencia
accionManejador:
CONTINUE | EXIT
valorCondicion:
codigoErrorMariaDB
| SQLSTATE [VALUE] valorDeSQLState
| nombreCondicion
| SQLWARNING
| NOT FOUND
| SQLEXCEPTION
A tener en cuenta:
- El manejador se lanzará para una o más condiciones.
- Las acciones pueden ser
CONTINUE
para continuar con la ejecución normal, oEXIT
que ejecutar el manejador y finaliza el bloqueBEGIN-END
actual. - Al lanzarse, se puede ejecutar una única instrucción (por ejemplo
set x=5;
) o un bloque de instrucciones (entreBEGIN
yEND
). - Los manejadores se deben declarar después de las variables locales.
- Dentro de un manejador no podemos declarar variables. Sí que podemos usar variables de usuario (comienzan por
@
), o utilizar las variables declaradas antes de los manejadores.
Los manejadores se asocian mediante FOR
a códigos de error, estados de SQL (los cuales son cadenas y por tanto van entre comillas), las condiciones predefinidas a cualquier condición que hayamos creado anteriormente:
Por ejemplo, podemos definir un par que manejadores del siguiente modo:
DECLARE CONTINUE HANDLER FOR 1051
BEGIN
-- cuerpo del manejador
END;
-- Intenta acceder a una tabla que no existe
DECLARE CONTINUE HANDLER FOR SQLSTATE '42S02'
BEGIN
-- cuerpo del manejador
END;
-- Intenta acceder a una tabla que no existe
Por ejemplo, si queremos realizar una inserción en la tabla habilidad
con gestión de errores, podríamos hacer:
DELIMITER //
CREATE PROCEDURE insertarHabilidad(IN codigo VARCHAR(50), IN descripcion VARCHAR(50))
BEGIN
DECLARE CONTINUE HANDLER FOR 1062
SELECT 'Error: Valor duplicado' AS mensaje;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
SELECT 'Error SQL general' AS mensaje;
INSERT INTO habilidad (CodHab, NomHab) VALUES (nombre, descripcion);
SELECT 'Inserción exitosa' AS mensaje;
END //
DELIMITER ;
Autoevaluación
A partir del siguiente fragmento de código:
CREATE OR REPLACE TABLE t11 (s1 INT, PRIMARY KEY (s1));
DELIMITER //
CREATE PROCEDURE ejemploManejadores(out resultado int)
BEGIN
-- El código 23000 se lanza al violar la restricción de integridad
DECLARE EXIT HANDLER FOR SQLSTATE '23000' SET resultado = 1;
SET resultado = 1;
INSERT INTO t11 VALUES (1);
SET resultado = 2;
INSERT INTO t11 VALUES (1);
SET resultado = 3;
END
//
DELIMITER ;
call ejemploManejadores(@salida);
select @salida;
- ¿Cuánto valdrá
@salida
en la línea 18? - Si cambiamos el manejador de
CONTINUE
aEXIT
, ¿Cuánto valdríaresultado
?
Manejadores en Eventos¶
Anteriormente estudiamos como podíamos crear eventos para ejecutar ciertas acciones en un instante determinado o bien de forma recurrente. Pero ¿Qué sucede cuando se produce un error dentro de un evento? Una práctica muy común es utilizar una tabla de log donde almacenamos el error producido, y así poder monitorizarlos.
Así pues, necesitamos declarar un manejador que inserte en dicha tabla:
CREATE EVENT evento_con_manejo_errores
ON SCHEDULE EVERY 12 HOUR
DO
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
INSERT INTO errores (nombre, fecha, mensaje)
VALUES ('evento_con_manejo_errores', NOW(), 'Error durante la ejecución');
END;
UPDATE inventario SET stock = stock - reservas WHERE reservas > 0;
INSERT INTO registro_eventos (nombre, fecha, estado)
VALUES ('evento_con_manejo_errores', NOW(), 'Completado');
END;
Manejadores en Triggers¶
Del mismo modo, cuando trabajamos con disparadores, también podemos definir un manejador de errores para que, en el caso de error, inserte un registro en nuestra tabla de log:
CREATE TRIGGER triggerUpdateInventario
AFTER INSERT ON pedidos
FOR EACH ROW
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
INSERT INTO errores (nombre, fecha, mensaje)
VALUES ('triggerUpdateInventario', NOW(), CONCAT('Error durante la actualización del inventario del pedido ', NEW.idProducto));
END;
UPDATE inventario SET stock = stock - NEW.cantidad WHERE idProducto=NEW.idProducto;
END;
Dicho esto, MariaDB maneja automáticamente las transacciones para los triggers. Es decir, si un trigger falla, la operación que lo activó (INSERT
, UPDATE
, DELETE
) se deshace completamente. Si un trigger encuentra un error y no lo maneja internamente, el error se propaga a la consulta que activó el trigger.
Manejadores y Señales¶
Ahora que ya sabemos definir manejadores, condiciones y señales, vamos a ver cómo podemos utilizarlos de manera conjunta para hacer nuestro código más robusto.
Por ejemplo, podemos lanzar una señal con un mensaje más específico asociado a una regla de negocio:
DELIMITER //
CREATE OR REPLACE PROCEDURE validaFondos(codOp INT, fondos INT)
BEGIN
DECLARE mensaje VARCHAR(255);
IF fondos < 0 THEN
SET mensaje = CONCAT('Fondos insuficientes (', fondos, ') para la operación ', codOp);
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = mensaje;
END IF;
END
//
Y a continuación, en otro procedimiento, capturar dicha señal y mostrar el mensaje recibido. Para ello, usaremos GET DIAGNOSTICS
indicando que queremos recuperar la primera condición, y de ella, su mensaje de error:
DELIMITER //
CREATE OR REPLACE PROCEDURE gestionaFondos()
BEGIN
DECLARE EXIT HANDLER FOR SQLSTATE '45000'
BEGIN
-- Recuperamos el mensaje del error
GET DIAGNOSTICS CONDITION 1 @error_msg = MESSAGE_TEXT;
-- Lo mostramos por pantalla con un mensaje personalizado
SELECT 'Error en la gestión de Fondos' as mensaje_padre, @error_msg AS mensaje_senyal;
END;
call validaFondos(1, 100);
call validaFondos(2, -100);
-- +--------------------------------+--------------------------------------------------+
-- | mensaje_padre | mensaje_senyal |
-- +--------------------------------+--------------------------------------------------+
-- | Error en la gestión de Fondos | Fondos insuficientes (-100) para la operación 2 |
-- +--------------------------------+--------------------------------------------------+
-- 1 row in set (0.001 sec)
call validaFondos(3, 200);
END
//
DELIMITER ;
call gestionaFondos();
Si queremos mejorar la legibilidad, recuerda que podemos definir una condición para poner un nombre a un código de error o estado SQL:
CREATE PROCEDURE gestionaFondos()
BEGIN
DECLARE errorFondosInsuficientes CONDITION FOR SQLSTATE '45000';
DECLARE EXIT HANDLER FOR errorFondosInsuficientes
SELECT 'No hay fondos suficientes para la operación' AS mensaje;
call validaFondos(1, 100);
call validaFondos(2, -100);
call validaFondos(3, 200);
END
Propagando señales
Mediante RESIGNAL
podemos modificar o propagar un error capturado por un manejador. Debemos tener en cuenta que sólo podemos propagar una señal dentro de un manejador:
DELIMITER //
CREATE OR REPLACE PROCEDURE procesarDatos(IN _CodEmp INT)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
-- Recuperamos el mensaje de texto de la excepción capturada
GET DIAGNOSTICS CONDITION 1
@error_msg = MESSAGE_TEXT;
-- Personalizar el mensaje de error antes de re-lanzarlo
SET @nuevoMensaje = CONCAT('Error al procesar empleado ', _CodEmp, ': ', @error_msg);
RESIGNAL SET MESSAGE_TEXT = @nuevoMensaje;
END;
UPDATE empleado SET CodDep = "NOEXISTE" WHERE CodEmp = _CodEmp;
END //
DELIMITER ;
call procesarDatos(5);
-- ERROR 1406 (22001): Error al procesar empleado 5: Data too long for column 'CodDep' at row 1
Nos plantearemos propagar una señal cuando:
- queramos capturar un error, realizar alguna acción (como registrar en logs) y luego volver a lanzarlo.
- modificar el mensaje de error antes de propagarlo.
- necesitamos controlar errores en un nivel superior, por ejemplo, si un procedimiento llama a otro y queremos gestionar la excepción en el procedimiento principal.
Transacciones en PL/SQL¶
Cuando trabajamos con transacciones, necesitamos poder controlar cuando sucede un error para deshacer las operaciones. Para ello, se utiliza un manejador de tipo EXIT
para hacer ROLLBACK
y deshacer la transacción. Si no se produce ningún error, se realizará el COMMIT
.
A nivel de código, se traduce en el siguiente esquema de código:
CREATE PROCEDURE transaccionEnMariaDB()
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION, SQLWARNING
BEGIN
-- ERROR, WARNING
ROLLBACK;
END;
START TRANSACTION;
-- Sentencias SQL
COMMIT;
END
Por ejemplo, a partir de este esqueleto, definimos un ejemplo concreto, donde declaramos un manejador para gestionar un error y deshacer la transacción, o realizar un COMMIT
si llega al final de la operación:
CREATE PROCEDURE procesoTransaccional(IN id_pedido INT)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SELECT 'Error en la transacción. Se ha revertido.' AS resultado;
END;
START TRANSACTION;
-- Operaciones que podrían fallar
UPDATE pedidos SET estado = 'procesado' WHERE id = id_pedido;
UPDATE inventario SET stock = stock - (SELECT cantidad FROM pedidos WHERE id = id_pedido);
COMMIT;
SELECT 'Transacción completada con éxito' AS resultado;
END
Veamos un ejemplo sobre nuestra base de datos de empresa
. Supongamos que queremos dar de alta a la vez a un empleado y a su hijo. Al insertar un empleado, se le generará automáticamente un código de empleado, el cual recuperaremos y lo usaremos a la hora de crear su primer hijo. Para recuperar el valor de un campo autoincrementable usaremos la función LAST_INSERT_ID()
:
CREATE PROCEDURE insertEmpleadoHijo(_NomEmp VARCHAR(40), _FecNaEmp DATE, _NifEmp VARCHAR(9),
_SalEmp DECIMAL(12,2), _CodDep CHAR(5), _NomHi VARCHAR(40), _FecNaHi DATE)
BEGIN
DECLARE newCodEmp INT;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
END;
START TRANSACTION;
INSERT INTO empleado (CodDep, FecInEmp, FecNaEmp, NifEmp, NomEmp, NumHi, SalEmp)
VALUES (_CodDep, now(), _FecNaEmp, _NifEmp, _NomEmp, 1, _SalEmp);
SET newCodEmp = LAST_INSERT_ID(); -- Recuperamos el código generado
INSERT INTO hijo (CodEmp, NumHij, FecNaHi, NomHi)
VALUES (newCodEmo, 1, _FecNaHi, _NomHi);
COMMIT;
END;
El siguiente paso es ver un ejemplo completo del uso de manejador con transacciones y señales, simulando una transferencia entre dos cuentas bancarias:
DELIMITER //
CREATE PROCEDURE transferir_fondos(
IN cuenta_origen INT,
IN cuenta_destino INT,
IN monto DECIMAL(10,2)
)
BEGIN
DECLARE saldo_actual DECIMAL(10,2);
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SELECT 'Transacción cancelada por error' AS resultado;
END;
START TRANSACTION;
SELECT saldo INTO saldo_actual FROM cuentas WHERE id = cuenta_origen;
-- Verificamos si hay fondos suficientes
IF saldo_actual < monto THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Fondos insuficientes para realizar la transferencia';
END IF;
UPDATE cuentas SET saldo = saldo - monto WHERE id = cuenta_origen;
UPDATE cuentas SET saldo = saldo + monto WHERE id = cuenta_destino;
COMMIT;
SELECT 'Transferencia realizada con éxito' AS resultado;
END //
DELIMITER ;
Recomendaciones¶
Algunas consideraciones y consejos que debemos tener en mente a la hora de diseñar procedimiento y gestionar los errores son:
- Categorizar errores: Establece diferentes manejadores según la categoría del error, ya sean errores de datos (duplicados, restricciones), errores de sintaxis o permisos o errores de lógica de negocio.
-
Registrar los errores en un sistema de logging para registrar los errores importantes:
CREATE TABLE error_log ( id INT AUTO_INCREMENT PRIMARY KEY, fecha TIMESTAMP DEFAULT CURRENT_TIMESTAMP, procedimiento VARCHAR(100), codigo_error INT, mensaje TEXT ); DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN GET DIAGNOSTICS CONDITION 1 @errno = MYSQL_ERRNO, @text = MESSAGE_TEXT; INSERT INTO error_log (procedimiento, codigo_error, mensaje) VALUES ('nombre_procedimiento', @errno, @text); SELECT 'Ha ocurrido un error. Contacte al administrador.' AS mensaje; END;
-
Cuando definimos varios gestores de error, primero declararemos los manejadores para errores específicos y luego los generales.
-- Manejador específico DECLARE CONTINUE HANDLER FOR 1062 SELECT 'Error: Entrada duplicada' AS mensaje; -- Manejador general (se ejecutará para otros errores) DECLARE EXIT HANDLER FOR SQLEXCEPTION SELECT 'Error SQL general' AS mensaje;
-
Proporcionar mensajes de error claros e informativos que ayuden a diagnosticar el problema.
- Utilizar condiciones con nombres significativos para mejora la legibilidad del código.
- Realizar validaciones preventivas, es decir, antes de realizar operaciones críticas para evitar errores.
- Documentar los posibles errores que puede generar cada procedimiento y cómo manejarlos.
Cursores¶
Los cursores nos permiten evaluar un conjunto de datos fila a fila. Dicho con otras palabras, un cursor almacena un conjunto de filas de una tabla en una estructura de datos que podemos ir recorriendo de forma secuencial.
Al utilizarse sobre un conjunto de datos, sólo se pueden utilizar con sentencias SELECT
.
Propiedades:
- Sólo lectura: No permiten actualizar los datos.
- Nonscrollable: sólo pueden ser recorridos en una dirección y no podemos saltarnos filas.
Cursores en otros lenguajes
En el módulo de Programación accederás a una base de datos desde Java, en principio, utilizando JDBC. Al realizar una consulta, recuperarás un ResultSet que no es más que un cursor al resultado de la sentencia SQL ejecutada. De ahí que el concepto que vamos a trabajar en este apartado sea extrapolable a otros módulos profesionales.
Declaración¶
El primer paso es declarar el cursor mediante DECLARE CURSOR
, del mismo modo que antes declarábamos una variable, crearemos nuestro cursor mediante una consulta SELECT
con la siguiente sintaxis:
DECLARE nombreCursor [(parámetros)] CURSOR
FOR consultaSQL
parámetros:
nombreParametro tipo
Por ejemplo:
DECLARE curClientes CURSOR FOR
select codigo_cliente, nombre_cliente from cliente
Una vez declarado, el cursor se abre y se recorre fila a fila hasta llegar al final. Para ello, realizaremos los siguientes pasos:
- Abriremos el cursor mediante
OPEN nombreCursor
, momento en el que reserva memoria y ejecuta la consulta, colocando el puntero en la primera fila. Es importante recalcar que la definición del cursor no ejecuta la consulta, ya que la consulta realmente se realiza en este paso, al abrirlo. Una vez realizada, obtendrá los resultados y los mantendrá en memoria del SGBD mientras utilizamos el cursor. - A continuación, se recorre con
FETCH nombreCursor INTO listaVariables
, almacenando el contenido de la fila a la que apunta el puntero. De este manera,FETCH
recupera las filas una a una. Esta es la parte principal del cursor, donde rellenamos variables, modificamos datos y hacemos cualquier otra cosa que queramos hacer con ellos. Trabajamos con esa única fila hasta que volvamos a hacerFETCH
de otra fila mientras recorremos el conjunto de resultados, y pararemos cuando lleguemos al final del conjunto de datos. - Para comprobar el final dentro de un bloque utilizaremos
DECLARE HANDLER FOR NOT FOUND
, donde bien gestionaremos el final del bucle o cerraremos el cursor medianteCLOSE nombreCursor
, el cual libera el contenido del cursor de la memoria del servidor. Este manejador se lanzará cuandoFETCH
no encuentre más filas.
---
config:
look: handDrawn
---
flowchart TD
A([DECLARE nombreCursor CURSOR FOR sql]) --> B[OPEN nombreCursor]
B --> C{WHILE}
C -- Siguiente registro --> D[FETCH nombreCursor INTO variables]
D --> E{{Operaciones con los datos}}
E --> C
C -- No quedan registros ----> F[CLOSE]
Probando los cursores¶
Comencemos con un ejemplo muy sencillo donde únicamente vamos a copiar el codigo y el nombre de un empleado en una nueva tabla. Para ello, creamos la tabla empleado_copia
mediante:
CREATE TABLE empleado_copia (
CodEmp INT PRIMARY KEY,
NomEmp VARCHAR(40)
)
A continuación, vamos a recorrer la tabla empleado
para recuperar cada empleado y copiar únicamente el código y el nombre e insertarlos en la tabla copia que acabamos de crear (este ejemplo no tienen ninguna utilidad más allá de demostrar cómo se emplean los cursores, ya que esta misma operación se podría realizar mediante un INSERT ... SELECT
):
DELIMITER //
CREATE OR REPLACE PROCEDURE ejemploCursorEmpleado()
BEGIN
DECLARE fin INT DEFAULT FALSE; -- (1)!
DECLARE cod INT;
DECLARE nom VARCHAR(40);
DECLARE cur CURSOR FOR SELECT CodEmp, NomEmp FROM empleado; -- (2)!
DECLARE CONTINUE HANDLER FOR NOT FOUND SET fin = TRUE; -- (3)!
OPEN cur; -- (4)!
WHILE fin = FALSE DO -- (5)!
FETCH cur INTO cod, nom; -- (6)!
IF fin = FALSE THEN
INSERT INTO empleado_copia VALUES (cod, nom);
END IF;
END WHILE;
CLOSE cur; -- (7)!
END;
//
DELIMITER ;
- Declaramos una variable
fin
a modo de flag para controlar el final del cursor - Declaramos un cursor para recorrer la tabla empleado
- Declaramos el manejador de errores
- Abrimos el cursor
- Creamos el bucle para recorrer el cursor
- Recuperamos las columnas del registro al que apunta el cursor
- Cerramos el cursor
Cabe destacar la necesidad de comprobar el valor de la variable flag dentro del bucle, ya que el gestor de errores se ejecutará tras la operación FETCH
, y por lo tanto, necesitamos en la línea 15 comprobar el valor de la variable fin
antes de realizar nuestra operación de negocio.
Sigamos con un caso más completo. Supongamos que tenemos dos tablas con la misma cantidad de registros, y queremos rellenar una tercera a partir de los datos de estas dos:
DROP DATABASE IF EXISTS pruebas;
CREATE DATABASE pruebas;
USE pruebas;
CREATE TABLE t1 (
id INT UNSIGNED PRIMARY KEY,
datos VARCHAR(16)
);
CREATE TABLE t2 (
id INT UNSIGNED
);
CREATE TABLE t3 (
datos VARCHAR(16),
id INT UNSIGNED
);
INSERT INTO t1 VALUES (1, 'A');
INSERT INTO t1 VALUES (2, 'B');
INSERT INTO t1 VALUES (30, 'C');
INSERT INTO t2 VALUES (10);
INSERT INTO t2 VALUES (20);
INSERT INTO t2 VALUES (3);
En este caso, vamos a rellenar la tercera tabla con el menor identificador, y los datos de la primera:
DELIMITER //
CREATE OR REPLACE PROCEDURE ejemploDosCursores()
BEGIN
DECLARE fin INT DEFAULT FALSE; -- (1)!
DECLARE a VARCHAR(16);
DECLARE b, c INT;
DECLARE cur1 CURSOR FOR SELECT id, datos FROM pruebas.t1; -- (2)!
DECLARE cur2 CURSOR FOR SELECT id FROM pruebas.t2;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET fin = TRUE; -- (3)!
OPEN cur1; -- (4)!
OPEN cur2;
WHILE fin = FALSE DO -- (5)!
FETCH cur1 INTO b, a; -- (6)!
FETCH cur2 INTO c;
IF fin = FALSE THEN
IF b < c THEN
INSERT INTO pruebas.t3 VALUES (a,b);
ELSE
INSERT INTO pruebas.t3 VALUES (a,c);
END IF;
END IF;
END WHILE;
CLOSE cur1; -- (7)!
CLOSE cur2;
END;
//
- Declaramos una variable
fin
a modo de flag para controlar el final del cursor - Declaramos dos cursores, uno por cada tabla a recorrer
- Declaramos el manejador de errores
- Abrimos los cursores
- Creamos el bucle para recorrer el cursor
- Recuperamos los registros
- Cerramos los cursores
Si invocamos al procedimiento, obtendremos:
DELIMITER ;
CALL ejemploDosCursores();
SELECT * FROM t3;
-- +-------+------+
-- | datos | id |
-- +-------+------+
-- | A | 1 |
-- | B | 2 |
-- | C | 3 |
-- +-------+------+
-- 3 rows in set (0.000 sec)
Pensando en conjuntos
Desde el principio del bloque de consultas, hemos aprendido a trabajar con datos pensando en conjuntos de datos completos, ya que le decimos al SGBD lo que queremos recuperar o evaluar, y el motor de consultas averigua la mejor manera de completar la consulta.
Cuando en la unidad anterior empezamos a utilizar las estructuras de control dentro de los procedimientos almacenados, empleamos un enfoque imperativo, al indicarle al SGBD cómo realizar las operaciones.
El motivo por el que lo cursores se siguen utilizando es que en ocasiones es necesario indicarle al SGBD los pasos que queremos realizar. Dicho esto, este enfoque imperativo normalmente supone un peor rendimiento al realizar un bloqueo excesivo y hacer un mayor consumo de los recursos del motor de base de datos que si hiciéramos un enfoque basado en conjuntos.
Dicho esto, siempre que sea posible, tendrá mejor rendimiento una o más consultas que el uso de cursores, aunque en contadas ocasiones necesitarás evaluar cada fila de un conjunto de forma individual y necesitarás el uso de cursores.
Una vez que ya sabemos cómo se crean, veamos un ejemplo sobre nuestra base de datos empresa
. La empresa se está planteando ampliar la cantidad de departamentos para fomentar la conciliación familiar. De esta manera, aquellos departamentos que tengan más de 2 trabajadores con hijos se dividirán en dos, y sus empleados se repartirán entre ellos. Para ello, en una nueva tabla departamentoFamiliar
almacenaremos una copia de los departamentos, junto a las nuevas versiones familiares. Para crear una tabla con la misma estructura, haciendo uso de CREATE TABLE ... LIKE
podemos crear una tabla que copia la estructura de otra:
ROW TYPE OF
Cuando queremos recuperar todos los campos de un registro, en vez de definir una variable por cada campo, podemos declarar una variable cuyo tipo será un registro de una tabla. Para ello, declararemos una variable mediante DECLARE ... ROW TYPE OF
:
DECLARE rowDep ROW TYPE OF departamento;
Y posteriormente accederemos a los campos mediante la notación .
:
SET newPreAnu = rowDep.PreAnu / 2;
USE empresa;
CREATE OR REPLACE TABLE departamentoFamiliar LIKE departamento;
DELIMITER //
CREATE OR REPLACE PROCEDURE dividirDepartamentos()
BEGIN
DECLARE rowDep ROW TYPE OF departamento; -- colocamos toda la fila en una variable
DECLARE newCodDep CHAR(5);
DECLARE newNomDep VARCHAR(40);
DECLARE newPreAnu DECIMAL(12,2);
DECLARE fin INT DEFAULT FALSE;
DECLARE cur CURSOR FOR select d.* from departamento d join empleado e on d.CodDep = e.CodDep
where e.NumHi>0 having (count(e.CodEmp>1));
DECLARE CONTINUE HANDLER FOR NOT FOUND SET fin = TRUE;
OPEN cur;
WHILE fin = FALSE DO
FETCH cur INTO rowDep;
IF fin = FALSE THEN
SET newPreAnu = rowDep.PreAnu / 2; -- dividimos el presupuesto entre 2
INSERT INTO departamentoFamiliar select * from departamento where CodDep = rowDep.CodDep;
UPDATE departamentoFamiliar set PreAnu = newPreAnu where CodDep = rowDep.CodDep;
SET newNomDep = CONCAT( rowDep.NomDep, " Familiar" ); -- añadimos Familiar al nombre segundo departamento
SET newCodDep = CONCAT( SUBSTRING(rowDep.CodDep, 1, 4), "2"); -- cambiamos el código del nuevo departamento
INSERT INTO departamentoFamiliar VALUES (newCodDep, rowDep.CodEmpDir, rowDep.CodDep, rowDep.CodCen, newNomDep, newPreAnu, rowDep.TiDir);
END IF;
END WHILE;
CLOSE cur;
END;
//
DELIMITER ;
CALL dividirDepartamentos();
SELECT * FROM departamentoFamiliar;
-- +--------+-----------+-----------+--------+-----------------------------+-------------+-------+
-- | CodDep | CodEmpDir | CodDepDep | CodCen | NomDep | PreAnu | TiDir |
-- +--------+-----------+-----------+--------+-----------------------------+-------------+-------+
-- | DIRG2 | 1 | DIRGE | DIGE | Dirección General Familiar | 13000000.00 | P |
-- | DIRGE | 1 | NULL | DIGE | Dirección General | 13000000.00 | P |
-- +--------+-----------+-----------+--------+-----------------------------+-------------+-------+
-- 2 rows in set (0.000 sec)
Uso de parámetros¶
Los cursores pueden utilizarse junto con parámetros de entrada y salida en procedimientos almacenados.
Para ello, al declarar el cursor indicaremos los parámetros, que usaremos en la sentencia del cursor. Posteriormente, cuando abrimos el cursor, le pasaremos los argumentos al cursor que sustituirán a los parámetros.
Por ejemplo, supongamos que tenemos la tabla t1
con valores diversos, y queremos seleccionar los que estén comprendidos entre un determinado rango (el cual definimos mediante parámetros), y asignarles a todos ellos el valor s8a
a un columna determinada:
CREATE OR REPLACE TABLE t1 (a INT PRIMARY KEY, b VARCHAR (32));
INSERT INTO t1(a) VALUES (1),(2),(3),(4),(5),(6),(7);
DELIMITER //
CREATE OR REPLACE PROCEDURE procCursorConParametros(min INT, max INT)
BEGIN
DECLARE fin INT DEFAULT FALSE;
DECLARE va INT;
DECLARE cur CURSOR(pmin INT, pmax INT) FOR
SELECT a FROM t1 WHERE a BETWEEN pmin AND pmax;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET fin = TRUE;
OPEN cur(min, max);
FETCH cur INTO va;
WHILE NOT fin DO
UPDATE t1 SET b="s8a" WHERE a=va;
FETCH cur INTO va;
END WHILE;
CLOSE cur;
END;
//
DELIMITER ;
call procCursorConParametros(3,5);
select * from t1;
-- +---+------+
-- | a | b |
-- +---+------+
-- | 1 | NULL |
-- | 2 | NULL |
-- | 3 | s8a |
-- | 4 | s8a |
-- | 5 | s8a |
-- | 6 | NULL |
-- | 7 | NULL |
-- +---+------+
-- 7 rows in set (0.001 sec)
Referencias¶
-
Sintaxis SQL oficial de PostgreSQL y MariaDB.
-
Materiales sobre el módulo de BD:
- Programació de base de dades - Institut Obert de Catalunya.
- Triggers, procedimientos y funciones en MySQL de José Juan Sánchez.
- Programación de bases de datos de Javier Gutiérrez.
- Introducción a PL/SQL de gestionbasesdatos.readthedocs.io
Actividades¶
Bases de datos empleadas
Recuerda que estas actividades se basan en las siguientes bases de datos
- DDL y DML:
bd-empresa.sql
- DDL y DML:
bd-retail.sql
- Claves ajenas:
bd-retail-fk.sql
-
AC1101. (RABD.5 // CE5h // 3p) En la base de datos
empresa
, crea las siguientes tablas:estadisticas
: tabla con estadísticas acumuladasid
: entero autoincrementable (clave primaria).fecha
: timestamp con la fecha actual.totalEmpleados
: entero que almacena el total de empleado en la empresaultimoMesEmpleados
: entero que almacena los empleados dados de alta en el último mes;totalHabilidadEmp
: entero que almacena el total de habilidades de los empleados en la empresa;
contador
: tabla que contendrá sólo una filaid
: entero autoincrementable (clave primaria).fecha
: timestamp con la fecha actual.total
: entero inicializado a 0, almacenará el total de ejecución de los eventos,mensual
: entero inicializado a 0, almacenará el total de ejecución de los eventos durante el último mes.
Sobre estas tabla, se pide:
- Crea un evento (
ac1101evento3min
) que se ejecute dentro de tres minutos y que inserte encontador
un registro con todos los valores a 0. - Crea un evento (
ac1101evento5min
) que se ejecute cada 5 minutos y que, haciendo uso de un procedimiento almacenado, inserte enestadisticas
un registro con los datos actualizados. Además, modificará la tablacontador
incrementando en uno los valores detotal
ymensual
. - Crea un evento (
ac1101eventoDiarioLunes
) que se ejecute de forma diaria, comenzando su ejecución el Lunes a las 8:00, y que, haciendo uso de un procedimiento almacenado, inserte un nuevo registro enestadisticas
. Además, modificará la tablacontador
incrementando en uno los valores detotal
ymensual
. - Crea un evento (
ac1101eventoMensual
) que se ejecute cada mes a partir del momento de su lanzamiento y que, haciendo uso de un procedimiento almacenado, elimine las estadísticas que tienen más de un año y ponga el valor del contador mensual a 0. - Deshabilita el evento
ac1101eventoDiarioLunes
.
Además del código fuente de las sentencias, tras haberlos creado y lanzado, se pide comprobar qué eventos están pendientes de ejecutar. ¡No olvides habilitar el programador de eventos!
-
AR1102. (RABD.5 // CE5h // 3p) Sobre la base de datos
empresa
, haciendo uso de eventos, se pide:- Haz que el día 1 de Mayo del 2025 a las 23:15 se guarde una copia de los datos de la tabla
departamento
en una nueva tabla llamadadept_copia_temp
. - Modifica el evento para que se ejecute desde hoy hasta dentro de un año y que lo haga cada mes.
- Ejecuta la orden que muestre el momento en que se va a ejecutar.
- Haz que el día 1 de enero de cada año se incrementan los salarios de los directivos de cada departamento un 5%. Esta actualización se debe producir durante 5 años a partir del actual.
- Haz que el día 1 de Mayo del 2025 a las 23:15 se guarde una copia de los datos de la tabla
-
AC1103. (RABD.5 // CE5h // 3p) Utilizando la tabla
alumnado
y las funciones y procedimientos creados en la actividad AC1010 de la unidad anterior, se pide:-
Crea un trigger (
ac1103triggerCrearEmailBeforeInsert
) sobre la tablaalumnado
, para que si el email a insertar es nulo, le asigne uno automáticamente. Debes utilizar la funcióncrearEmail
. -
Crea un trigger (
ac1103triggerGuardarEmailAfterUpdate
) sobre la tablaalumnado
, para que cada vez que se modifica el email, inserte un nuevo registro en la tablalogCambiosEmail
, cuyos campos son:id
: clave primaria (entero autonumérico)idAlumno
: id del alumno (entero)fechaHora
: marca de tiempo con el instante del cambio (fecha y hora)oldEmail
: valor anterior del email (cadena de caracteres)newEmail
: nuevo valor con el que se ha actualizado
Además de código de los disparadores, se deben adjuntar algunas sentencias SQL para comprobar que funcionan correctamente, mostrando el contenido de las tablas antes y después de las operaciones.
-
-
AC1104. (RABD.5 // CE5h // 3p) Utilizando las tablas, funciones y procedimiento de la actividad anterior, se pide:
-
Crea un trigger (
ac1104triggerGuardarAlumnosAfterDelete
) sobre la tablaalumnado
, para que cada vez que se elimine un alumno, inserte un nuevo registro en la tablalogAlumnosEliminados
, cuyos campos son:id
: clave primaria (entero autonumérico)idAlumno
(entero)fechaHora
: marca de tiemponombre
: nombre del alumno (cadena de caracteres)apellido
(cadena de caracteres)email
(cadena de caracteres)
-
Crea un trigger (
ac1104triggerMensajeAfterDelete
) sobre la tablaalumnado
y que se ejecute trasac1104triggerGuardarAlumnosAfterDelete
, para que cada vez que se elimine un alumno, almacene en la tablamensajes
un texto similar a "Se ha eliminado al alumno XXX con email YYY a las ZZ:ZZ", a partir de la información almacenada en la tablalogAlumnosEliminados
.La estructura de la tabla
mensajes
es:id
: clave primaria (entero autonumérico)fechaHora
: marca de tiempotexto
(cadena de caracteres)
Además del código de los disparadores, se deben adjuntar algunas sentencias SQL para comprobar que funcionan correctamente, mostrando el contenido de las tablas antes y después de las operaciones.
-
-
AP1105. (RABD.5 // CE5j // 3p) Sobre la base de datos
empresa
, se pide:- Crea el trigger
ap1105triggerHolaHijo
de manera que al insertar un nuevo hijo en la tablahijo
, automáticamente incremente el atributoNumHi
de la tablaempleado
del empleado correspondiente. - Crea el trigger homónimo
ap1105triggerAdiosHijo
por si se diera el caso que falleciera el hijo de un empleado/a.
- Crea el trigger
-
AC1106. (RABD.5 // CE5j // 3p) Siguiendo con la tabla
alumnado
, se pide:-
Crea un procedimiento llamado
ac1106insertarAlumno
que reciba como entrada los cuatro datos de un alumno (id
,nombre
,apellidos
,curso
) como parámetros de entrada y los inserte en la tabla. El procedimiento devolverá como salida un parámetro llamadoerror
que tendrá un valor igual a0
si la operación se ha podido realizar con éxito o1
en caso contrario. Deberá manejar los errores que puedan ocurrir cuando se intenta insertar una fila que contiene una clave primaria repetida. -
Escribe un procedimiento (
ac1106nombreAlumnoIfCount
) que muestre el nombre de un alumno dado su código. En caso de que no se encuentre, devolveráAlumn@ no encontrado
en un parámetro de salida.- Utiliza
IF
para contar si ha encontrado al alumno. - Crea un nuevo procedimiento (
ac1106nombreAlumnoHandler
) pero ahora utiliza un manejador de errores (sin usarIF
) - Debes comprobar el error
NOT FOUND
. - La consulta debe guardar el resultado en la variable de salida mediante
SELECT … INTO
.
- Utiliza
-
Con la misma idea que el anterior, escribe un procedimiento (
ac1106nombreAlumnoIfNombre
) el cual directamente recupere el nombre del alumno (sin contar previamente), y dependiendo de si lo encuentra o no, rellene el parámetro de salida con el valor adecuado. -
Crea un procedimiento llamado
ac1106upsertAlumno
a partir deac1106insertarAlumno
, pero en el caso de que el alumno ya exista, y gestionando la captura deSQLException
, modificará los datos de dicho alumno con los datos recibidos como parámetros. -
Con la misma idea que el anterior, crea un procedimiento llamado
ac1106upsertAlumnoCallInsert
que invoque aac1106insertarAlumno
y que realice la actualización a partir del error devuelto por el procedimiento de inserción.
-
-
AC1107. (RABD.5 // CE5j // 3p) Sobre la base de datos
empresa
, crea una tabla que se llamesalarios
a partir del siguiente DDL:CREATE TABLE salarios ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, fecha TIMESTAMP DEFAULT CURRENT_TIMESTAMP, salario DECIMAL(12,2) check (salario > 0), codigo_empleado INT );
Además, crea una tabla
log_errores
a partir del siguiente DDL:CREATE TABLE log_errores ( id INT AUTO_INCREMENT PRIMARY KEY, fecha DATETIME DEFAULT CURRENT_TIMESTAMP, codigo_error INT, mensaje VARCHAR(255), origen VARCHAR(100) );
Escribe los triggers
ac1107triggerSalariosEmpleadoAfterInsert
yac1107triggerSalariosEmpleadoAfterUpdate
que nos permita llevar un control de las modificaciones en los salarios de los empleados. Los disparadores se ejecutarán después de cada inserción o modificación, insertando un registro en la tablasalarios
.Los triggers deben realizar una gestión correcta de los errores de manera que, si sucede algo inesperado, se inserte un registro en
log_errores
. Para recuperar el código y el mensaje de error debes ejecutarGET DIAGNOSTICS CONDITION 1 @error_msg = MESSAGE_TEXT, @error_code = MYSQL_ERRNO;
. Finalmente, la columnaorigen
almacenará el nombre del trigger que ha lanzado el error.Además del código de los disparadores, se deben adjuntar algunas sentencias SQL para comprobar que funcionan correctamente, mostrando el contenido de las tablas antes y después de las operaciones.
-
AC1108. (RABD.5 // CE5h, CE5j // 3p) Sobre la base de datos
empresa
y haciendo uso de señales, se pide:-
El procedimiento
ac1108salariosHabilidad
que reciba como argumento el nombre de un habilidad y muestre el total de los salarios de los empleados que tienen dicha habilidad. Si no hubiera ningún empleado, lanzará una señal con el mensaje "Para la habilidad XXXX no hay ningún empleado existente". Si la habilidad no existe, el mensaje será "La habilidad XXX no existe". -
El trigger
ac1108triggerHijoAfterInsert
que, tras insertar un hijo, compruebe que si el número de hijo es el mayor (de entre sus herman@s), tenga una fecha de nacimiento menor a la de sus herman@s. Si no fuera así, lanzará una señal con el mensaje "El hijo XXX debe ser menor que su hermano YYY".
-
-
AC1109. (RABD.5 // CE5j // 3p) Crea la base de datos
cine
con las siguientes tablas:- Tabla
cuentas
:idCuenta
: entero sin signo (PK).saldo
: real sin signo.
- Tabla
entradas
:idButaca
: entero sin signo (PK).nif
: cadena de 9 caracteres.
Crear un procedimiento llamado
ac1109comprarEntrada
que reciba 3 parámetros de entrada (nif
,idCuenta
,idButaca
) y devuelva como salida un parámetro llamadoerror
que tendrá un valor igual a0
si la operación se ha podido realizar con éxito o1
en caso contrario.Pasos del procedimiento de compra:
- Inicia una transacción.
- Actualiza
cuentas.saldo
cobrando5
euros a la cuenta con elidCuenta
adecuado. - Inserta una fila en la tabla
entradas
indicando la butaca (idButaca
) que acaba de comprar el usuario (nif
). - Comprueba si ha ocurrido algún error en las operaciones anteriores. Si todo va bien aplica un
COMMIT
a la transacción y si ha ocurrido algún error hazROLLBACK
.
Debe manejar los errores
ERROR 1264
(Out of range value) yERROR 1062
(Duplicate entry for PRIMARY KEY).¿Qué ocurre si se compra una entrada y le pasamos como parámetro un número de cuenta inexistente? ¿Ocurre algún error o podemos comprar la entrada? En caso de que exista algún error, ¿Cómo podríamos resolverlo?
- Tabla
-
AP1110. (RABD.5 // CE5h, CE5j // 3p) En la actividad AP1105 se planteaba actualizar la cantidad de hijos de un empleado mediante un disparador asociado a la inserción de un nuevo registro en la tabla
hijo
. Realmente, es mejor que ambas operaciones formen parte de una transacción y que se gestionen dentro de un procedimiento almacenado. Dicho esto, se pide:-
Elimina el trigger
ap1105triggerHolaHijo
. -
Crea la función
ap1110funcCompruebaHijo
que valide, para un empleado, que la cantidad de hijos almacenados en la tablahijo
concuerda con el atributoNumHi
de la tablaempleado
. En caso contrario, debe lanzar una señal con un mensaje adecuado. -
Crea el procedimiento almacenado
ap1110procInsertaHijo
, que haciendo un buen uso de las transacciones y la gestiones de errores, y tras validar que los datos existen previamente, tanto inserte los datos en la tablahijo
como modifique la columnaNumHi
de la tablaempleado
. En caso de error, debe lanzar una señal con un mensaje adecuado (o relanzar la existente, si fuera el caso).
Además del código de los programas, se deben adjuntar algunas sentencias SQL para comprobar que funcionan correctamente, mostrando el contenido de las tablas antes y después de las operaciones.
-
-
AC1111. (RABD.5 // CE5h, CE5i, CE5j // 3p) Haciendo uso de cursores y la base de datos
empresa
, se pide:- Crea la tabla
empleado_copia
con la misma estructura que la tablaempleado
(medianteCREATE TABLE .... LIKE ...
). - Crea el procedimiento
ac1111empleadosSinHijos
, para que, utilizando cursores y recuperando todos los empleados en la consulta, recorra la tabla de empleados e inserte enempleado_copia
aquellos empleados que no tiene hijos. - Crea el procedimiento
ac1111empleadosNumHijos
, para que, utilizando cursores, recorra la tabla de empleados e inserte enempleado_copia
aquellos empleados que tienen la cantidad de hijos recibidos como un parámetro de entrada del procedimiento.
Además del código, se deben adjuntar algunas sentencias SQL para comprobar que funciona correctamente, mostrando el contenido de las tablas antes y después de las operaciones.
- Crea la tabla
-
AC1112. (RABD.5 // CE5h, CE5i, CE5j // 3p) Haciendo uso de cursores y la base de datos
empresa
, se pide:- Crea la tabla
empleado_plus
con los mismos datos que la tablaempleado
(medianteCREATE TABLE .... AS SELECT ...
) - Sobre la nueva tabla
empleado_plus
, añade la columnaHijMayor
que permita almacenar el nombre del hijo mayor de cada empleado. Para ello, crea el procedimiento almacenadoac1112procHijos
, y mediante un cursor, sobre los hijos mayores de cada empleado, modifique el campo del empleado y almacene su nombre. - Sobre la nueva tabla
empleado_plus
, añade la columnaDescHabs
que permita almacenar los códigos de las habilidades de dicho empleado separadas por comas. Es decir, para el empleado1
deberá almacenarGEREN,RELPU
. Si no tuviera ninguna habilidad, debe dejar el campo nulo. Crea el procedimiento almacenadoac1112procHabilidadesEmpleados
que complete dicha columna.
- Crea la tabla
-
AC1113. (RABD.5 // CE5h, CE5i, CE5j // 3p) Haciendo uso de la base de datos
retail
, se pide:- Crea un procedimiento llamado
ac1113insertaCategoriaDepartamento
que inserte una categoría y su departamento dentro de una transacción a partir del nombre de una categoría y de su departamento. Dentro del procedimiento hemos de comprobar previamente si existen tanto la categoría como el departamento, y actuar en consecuencia. -
Crea un procedimiento llamado
ac1113desnormalizarCategorias
que, utilizando cursores, rellene la tablacategorias_desnorm
con la información proveniente de las tablas de categorías y departamentos. Podemos crear la tablacategorias_desnorm
mediante el siguiente DDL:CREATE TABLE categorias_desnorm ( category_id INT(11) PRIMARY KEY, category_name VARCHAR(45), department_id INT(11), department_name VARCHAR(45) );
-
Crea el evento
ac1113evento
que se ejecute cada día a las 12 de la noche, que elimine la tabla desnormalizada, la vuelva a crear y la rellene con los datos.
- Crea un procedimiento llamado
-
PY1114. (RABD.5 // todos los CE // 10p) Siguiendo el reto y la actividad de proyecto PY813, en esta actividad nos vamos a centrar en crear una serie de funcionalidades:
Para ello, se pide un informe que detalle:
- Definición de 2 eventos que automaticen tareas con diferente periodicidad
- Definición de 2 disparadores sobre operaciones asociadas al modelo de datos.
- Definición de 2 procedimientos almacenados que realicen más de una operación dentro de una transacción, haciendo una gestión adecuada de los errores, ya sea mediante señales o excepciones, y sus consiguientes manejadores.
- Definición de 2 procedimientos almacenados que utilicen cursores que recorran cierta cantidad de datos, realizando operaciones sobre una o más tablas, haciendo una gestión adecuada de los errores, ya sea mediante señales o excepciones, y sus consiguientes manejadores.
- Resultados de su ejecución sobre el modelo físico.
Se utilizará una rúbrica para su evaluación en base a la siguiente lista de cotejo:
- Limpieza y calidad del código fuente.
- Documentación del código fuente.
- El informe entregado no contiene faltas de ortografía.
- El informe entregado tiene un formato adecuado (portada, apartados, autores, etc...).
- El informe debe indicar cómo se han repartido las tareas y qué ha realizado cada alumno/a.
-
PO1115. (RABD.5 // todos los CE // 60p) La prueba objetiva, que cubre todo el RABD.5 asociada a la programación mediante SQL, consiste en:
- (10p) Crear y gestionar disparadores (triggers).
- (10p) Crear y gestionar eventos.
- (20p) Desarrollar procedimientos almacenados y/o funciones que hagan uso de transacciones, realizando una buena gestión del manejo de errores mediante señales y/o excepciones.
- (20p) Desarrollar procedimientos almacenados y/o funciones que hagan uso de cursores, realizando una buena gestión del manejo de errores mediante señales y/o excepciones.
- AR1116. (RABD.5 // CE5h, CE5i, CE5j // 3p) Una vez finalizada la unidad, responde todas las preguntas del cuestionario inicial, con al menos un par de líneas para cada una de las cuestiones.