Tabla de Contenidos
Programación de Bases de Datos
Introducción al lenguaje de programación
Variables
Declaración de variables:
DECLARE nombre_variable tipo_variable [DEFAULT valor];
Asignación de valor a una variable directamente:
SET nombre_variable = valor_variable;
Asignación de valor a una o más variables como resultado de una consulta. (La consulta debe devolver una sola fila)
SELECT campo1, campo2, . . . INTO variable1, variable2, . . . FROM nombre_tabla WHERE . . .
Estructuras de control de flujo
Hay que destacar que MySQL, actualmente, no soporta bucles \emph{FOR}
- Sentencia IF
IF condicion THEN sentencias; [ELSEIF condicion2 THEN sentencias;] . . . [ELSE sentencias;] END IF
- Sentencia CASE
CASE variable WHEN valor1 THEN sentencias; [WHEN valor2 THEN sentencias;] . . . [ELSE sentencias;] END CASE CASE WHEN condicion THEN sentencias; [WHEN condicion2 THEN sentencias;] . . . [ELSE sentencias;] END CASE
- Sentencia LOOP
Los bucles LOOP no incoporan condición de salida, sino que debe ser implementada utilizando la instrucción LEAVE
[etiqueta_inicio:] LOOP sentencias; END LOOP [etiqueta_fin]
- Sentencia LEAVE
Se utiliza para romper la ejecución de cualquier instrucción de control de flujo que se haya etiquetado, normalmente bucles LOOP
LEAVE etiqueta;
- Sentencia ITERATE
Sólo puede aparecer en instrucciones de bucle ({LOOP, REPEAT y WHILE). Indica que el bucle debe volver a ejecutarse
ITERATE etiqueta;
- Sentencia REPEAT
[etiqueta_inicio:] REPEAT sentencias; UNTIL condicion END REPEAT [etiqueta_fin]
- Sentencia WHILE
[etiqueta_inicio:] WHILE condicion DO sentencias; END WHILE [etiqueta_fin]
Procedimientos y funciones almacenadas
Los procedimientos y funciones almacenadas son conjuntos de comandos SQL que pueden ser almacenados en el servidor. Se asocia un nombre a un conjunto determinado de instrucciones para, posteriormente, ejecutar dicho conjunto tantas veces como se desee sin necesidad de volver a escribirlas.
Rutinas DETERMINISTIC y NO DETERMINISTIC
Si estoy utilizando el servidor MySQL 8.0, debo tener en cuenta unos aspectos para que me permita crear este tipo de rutinas. Está indicado en el siguiente enlace de la sección de referencias.
CREATE PROCEDURE nombre_procedimiento([param1 tipo, param2 tipo, . . .]) BEGIN sentencias; END
CREATE FUNCTION nombre_funcion([param1 tipo, param2 tipo, . . .]) RETURNS tipo BEGIN sentencias; END
Para eliminar estos elementos utilizo la sentencia DROP
:
DROP PROCEDURE nombre; DROP FUNCTION nombre;
Ejemplos de funciones:
-- Función que devuelva el número de reservas que ha realizado un usuario CREATE FUNCTION get_numero_reservas(p_id_usuario INT) RETURNS INT BEGIN DECLARE cantidad INT; SET cantidad = (SELECT COUNT(*) FROM reservas R, usuario_reserva UR WHERE R.id = UR.id_reserva AND UR.id_usuario = p_id_usuario); RETURN cantidad; END;
-- Función que devuelva el número de reservas que ha realizado un usuario -- determinado. Si el usuario no existe, devuelve -1 CREATE FUNCTION get_numero_reservas(p_id_usuario INT) RETURNS INT BEGIN DECLARE cantidad INT; DECLARE existe_usuario INT; SET existe_usuario = (SELECT COUNT(*) FROM usuarios WHERE id = p_id_usuario); IF existe_usuario = 0 THEN -- Si el usuario no existe se devuelve valor de error RETURN -1; END IF; -- Si todo va bien, se calcula la cantidad y se devuelve SET cantidad = (SELECT COUNT(*) FROM reservas R, usuario_reserva UR WHERE R.id = UR.id_reserva AND UR.id_usuario = p_id_usuario); RETURN cantidad; END;
Ejemplos de procedimientos:
-- Muestra toda la información sobre los usuarios CREATE PROCEDURE lista_usuarios() BEGIN SELECT * FROM usuarios; END
-- Muestra la información sobre las reservas de una pista determinada -- (se pasa como parámetro) CREATE PROCEDURE ver_reservas_pista(p_id_pista INT) BEGIN SELECT * FROM reservas WHERE id_pista = p_id_pista; END;
-- Procedimiento para dar de alta una nueva pista en un polideportivo -- determinado. Se pasan como parámetros todos los datos necesarios -- para dar de alta la nueva pista asumiendo que se trata de una -- pista abierta al público CREATE PROCEDURE nueva_pista(p_codigo VARCHAR(10), p_tipo VARCHAR(255), p_precio FLOAT, p_id_polideportivo INT) BEGIN INSERT INTO pistas (codigo, tipo, precio, id_polideportivo) VALUES (p_codigo, p_tipo, p_precio, p_id_polideportivo); INSERT INTO pistas_abiertas (id_pista, operativa) VALUES (LAST_INSERT_ID(), TRUE); END;
Salir de un procedimiento: Etiquetas
Si deseo terminar un procedimiento en un momento dado, puedo usar la sentencia LEAVE
. Como podemos querer salir o terminar también un bucle, es necesario poner etiquetas a los bloques de código para indicar de qué bloque queremos salir.
-- Procedimiento para dar de alta una nueva pista en un polideportivo -- determinado. Se pasan como parámetros todos los datos necesarios -- para dar de alta la nueva pista asumiendo que se trata de una -- pista abierta al público CREATE PROCEDURE nueva_pista(p_codigo VARCHAR(10), p_tipo VARCHAR(255), p_precio FLOAT, p_id_polideportivo INT) procedimiento: BEGIN DECLARE existe_polideportivo TINYINT(1); SET existe_polideportivo = (SELECT COUNT(*) FROM polideportivos WHERE id = p_id_polideportivo); IF existe_polideportivo = 0 THEN LEAVE procedimiento; -- indico el bloque del que quiero salir END IF; INSERT INTO pistas (codigo, tipo, precio, id_polideportivo) VALUES (p_codigo, p_tipo, p_precio, p_id_polideportivo); INSERT INTO pistas_abiertas (id_pista, operativa) VALUES (LAST_INSERT_ID(), TRUE); END procedimiento;
Función LAST_INSERT_ID
Cuando queremos ejecutar varias operaciones que modifican la base de datos es recomendable agruparlas en una Transacción. Aparte de esto, es posible que al una inserción en una tabla cuya clave primaria es AUTO_INCREMENT
necesitemos conocer el valor del id generado, para poder utilizarlo en la siguiente inserción en caso de que las tablas estén relacionadas.
La función LAST_INSERT_ID()
nos devuelve el valor del último campo AUTO_INCREMENT
generado en nuestra sesión, manteniendo el control concurrente de diferentes usuarios a la misma base de datos:
-- Se desea insertar una nueva pista abierta. Para ello debo -- insertar la pista en la tabla pistas, y también en la tabla -- pistas abiertas: -- Primero hago la inserción en la tabla padre: pistas INSERT INTO pistas (codigo, tipo, precio, id_polideportivo) VALUES (p_codigo, p_tipo, p_precio, p_id_polideportivo); -- Después hago la inserción en la tabla hija: pistas_abiertas -- En este caso necesito conocer el id que se generó en la inserción -- anterior, ya que es clave ajena en esta tabla INSERT INTO pistas_abiertas (id_pista, operativa) VALUES (LAST_INSERT_ID(), TRUE);
Ejecutar procedimientos y funciones
Usamos CALL para procedimientos y SELECT para funciones. Podemos usar una función en la cláusula SELECT de una consulta.
CALL nombre_procedimiento(); SELECT nombre_funcion();
Sin embargo, podemos utilizar el potencial de las funciones cuando las usamos junto con otras consultas, o en consultas dentro de procedimientos, del mismo modo que hemos hecho con el resto de funciones de MySQL.
CREATE FUNCTION cantidad_reservas(p_id_usuario INT) RETURNS INT BEGIN DECLARE v_cantidad INT; SELECT COUNT(*) INTO v_cantidad FROM usuario_reserva WHERE id_usuario = p_id_usuario; RETURN v_cantidad; END; -- Usamos la función anterior en otras consultas SELECT nombre, apellidos, cantidad_reservas(id) FROM usuarios; -- Añadimos un campo (cantidad_reservas) a la tabla usuarios UPDATE usuarios SET cantidad_reservas = cantidad_reservas(id);
Ventajas y Desventajas
- Resultan útiles, por ejemplo, en casos en los que varias aplicaciones diferentes trabajan sobre la misma Base de Datos y deben ejecutar el mismo código. En vez de escribir ese código para cada una de las diferentes aplicaciones, se puede escribir una sola vez en el servidor y ejecutarse desde las diferentes aplicaciones clientes.
- Además, aportan mayor seguridad puesto que las aplicaciones o usuarios no necesitan acceder directamente a la información de la Base de Datos, sino que solamente acceden a ejecutar determinados procedimientos o funciones.
- Y por último, se reduce el tráfico de red generado entre la aplicación cliente y el servidor al no tener que mandar multiples consultas, sino tan solo el nombre de un procedimiento o función.
- Como principal desventaja se podría destacar la simplicidad del código que se puede utilizar en su implementación, puesto que no se pueden combinar con otros lenguajes más potentes como Java, C++, . . .
- Además, resultan bastante complicados de escribir y mantener puesto que requieren conocimientos bastante precisos que no todos los programadores suelen tener.
Creación de scripts
A la hora de implementar nuevos procedimientos y funciones hay que tener en cuenta algunas cuestiones. Puesto que el delimitador ;
se utiliza para finalizar cualquier orden sobre el motor MySQL, éste debe ser modificado mientras se implementa cualquier procedimiento o función, ya que cualquier instrucción SQL que forme parte del código, sería interpretada de forma independiente.
Así, la forma habitual de escribir procedimientos o funciones es a través de la creación de scripts SQL utilizando la orden DELIMITER
que permite modificar el delimitador de fin de orden en MySQL. Por ejemplo, el código que se muestra a continuación permitiría crear un procedimiento desde un script (o bien pegando el código directamente en la consola). El delimitador de fin de orden se modifica al inicio del procedimiento y se vuelve a restaurar al final de la implementación de éste.
DELIMITER | CREATE PROCEDURE ver_pistas() BEGIN SELECT * FROM pistas; END | DELIMITER ;
Transacciones
Por defecto, en MySQL, se trabaja con la opción AUTOCOMMIT
activada. Eso significa que cualquier operación de escritura sobre la base de datos se valida automáticamente a disco. Así, si queremos tener un cierto control sobre cuando se deben realizar esas validaciones a disco, podemos modificar esa opción con el siguiente comando:
SET AUTOCOMMIT = 0;
Así, cualquier operación que realicemos sobre tablas transaccionales (InnoDB) tendrá que ser validada mediante la instrucción COMMIT
o bien ignorada mediante la instrucción ROLLBACK
, que devolverá los datos de esas tablas al estado donde se validó la última transacción.
También se puede desactivar temporalmente la opción AUTOCOMMIT
haciendo uso de la sentencia START TRANSACTION
hasta que se validen o ignoren las instrucciones contenidas en la transacción:
START TRANSACTION; UPDATE usuarios SET descuento = 0.10 WHERE ciudad = 'Zaragoza'; UPDATE usuarios SET descuento = 0.20 WHERE ciudad = 'Teruel'; COMMIT;
Si en un momento dado se quiere consultar el estado de la opción AUTOCOMMIT
se puede realizar con la instrucción SHOW VARIABLES
Control de Excepciones
Como ocurre con muchos lenguajes de programación, MySQL también es capaz de gestionar, mediante excepciones, los errores que se puedan producir durante la ejecución de un fragmento de código en entornos transaccionales. En estos, si durante la ejecución de una transacción, se produce algún fallo, es posible deshacer toda la operación para evitar inconsistencias en los datos.
Veamos un ejemplo, basándonos en un ejemplo anterior:
CREATE PROCEDURE nueva_pista(p_codigo VARCHAR(10), p_tipo VARCHAR(255), p_precio FLOAT, p_id_polideportivo INT) BEGIN -- Primero se declaran las variables necesarias -- Después se declara el Handler DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; -- Si se quiere mostrar un mensaje: (opcional) SELECT 'An error has occurred, operation rollbacked'; END; START TRANSACTION; INSERT INTO pistas (codigo, tipo, precio, id_polideportivo) VALUES (p_codigo, p_tipo, p_precio, p_id_polideportivo); INSERT INTO pistas_abiertas (id_pista, operativa) VALUES (LAST_INSERT_ID(), TRUE); COMMIT; END;
Con el control de las excepciones hemos conseguido manejar cualquier posible fallo que puedan generar las instrucciones de la transacción y, en su lugar, lanzar una orden ROLLBACK
que deshará todos los pasos intermedios de dicha transacción que ya se hubieran ejecutado. Hay que tener en cuenta que el control de excepciones hace que el fallo nunca se propague hacia quién hubiera ejecutado este código, por lo que el programa que invoque a este código no se quedará en ningún estado inconsistente.
Triggers (Disparadores)
Los disparadores o triggers son procedimientos de la Base de Datos que se ejecutan o activan cada vez que ocurren un evento determinado sobre una tabla determinada, según se haya indicado en el momento de su implementación. Debemos indicar el momento en que se deben ejecutar (timing) y el evento que queremos que lo dispare.
Eventos
Los eventos que ocurren en una tabla ante los que podemos asociar a la ejecución de un trigger son:
- INSERT
- UPDATE
- DELETE
Esto quiere decir que un trigger se puede ejecutar al producirse uno de los eventos anteriores en una tabla concreta.
Timing
También podemos decidir que se activen antes o después del evento en cuestión, utilizando las palabras reservadas BEFORE
y AFTER
.
Respecto al timing debemos tener algunas consideraciones:
- Un trigger
BEFORE
se activa ante el simple intento de unINSERT
,UPDATE
, oDELETE
independientemente de si estas sentencias pueden lanzar un error. - Un trigger
AFTER
se ejecuta solamente si la operación que lo dispara se realiza correctamente. - Un error durante la ejecución de cualquier trigger, cancela automáticamente la operación que lo disparó. Es útil para controlar valores no deseados.
Codificación
-- Crear trigger CREATE TRIGGER nombre_trigger {BEFORE | AFTER} {INSERT | UPDATE| DELETE } ON nombre_tabla [FOR EACH ROW] BEGIN ... ... END; -- Mostrar triggers SHOW TRIGGERS; -- Borrar trigger DROP TRIGGER nombre_trigger;
La sentencia FOR EACH ROW
indica que el trigger se ejecuta por cada una de las filas afectadas. Es decir, si se borran 15 filas, se insertan ó se actualizan, el trigger se ejecutará 15 veces.
Palabras clave NEW y OLD
Las palabras NEW
y OLD
se emplean para referirse a las filas afectadas por el disparador, es decir, a las filas de la tabla sobre la que se activa, para referirse al estado de esa fila, antes (OLD
) o después (NEW
) de haber actuado el disparador.
Las referencias NEW y OLD no están disponibles siempre, ya que si se borran o insertan registros no existe modificación:
Evento del Trigger | OLD | NEW |
---|---|---|
INSERT | NO | SI |
UPDATE | SI | SI |
DELETE | SI | NO |
Hay que tener en cuenta que cuando nos referimos a una columna precedida por OLD
, el acceso es de sólo lectura, por lo que se podrá hacer referencia a ella sólo para leerla.
En el caso de las columnas precedidas por NEW
, su valor se podrá leer y también modificar con la instrucción SET
, siempre que el trigger se active antes de la operación (BEFORE
).
Ejemplos:
-- Calcula automáticamente la edad de los usuarios -- en el mismo momento en el que se dan de alta -- a partir de la fecha de nacimiento que introduzca -- el usuario CREATE TRIGGER nuevo_usuario BEFORE INSERT ON usuarios FOR EACH ROW BEGIN IF NEW.fecha_nacimiento IS NOT NULL THEN SET NEW.edad = YEAR(CURRENT_DATE()) - YEAR(NEW.fecha_nacimiento); END IF; END;
-- Actualiza la fecha de última reserva de una pista -- cada vez que ésta se reserva CREATE TRIGGER anota_ultima_reserva AFTER INSERT ON reservas FOR EACH ROW BEGIN UPDATE pistas_abiertas SET fecha_ultima_reserva = CURRENT_TIMESTAMP() WHERE id_pista = NEW.id_pista; END;
-- Registra una pista como pista clausurada al público cuando -- ésta se elimina de la Base de Datos CREATE TRIGGER retira_pista AFTER DELETE ON pistas_abiertas FOR EACH ROW BEGIN INSERT INTO pistas_cerradas (id_pista, fecha_clausura, motivo) VALUES (OLD.id_pista, CURRENT_TIMESTAMP(), 'Eliminada'); END;
También podemos terminar un trigger mediante la sentencia LEAVE
, del mismo modo que en los procedimientos y funciones almacenadas.
Control de errores con Triggers
Es interesante conocer el Timing de actuación de los triggers ya que podemos modificar los valores en los casos en que el trigger actue antes (BEFORE) del evento.
En los triggers que se disparan ante un UPDATE o un INSERT en una tabla, podemos acceder a la información del registro NEW y modificar sus valores:
-- Modificar valores incorrectos CREATE TRIGGER comprobar_nota_examen BEFORE INSERT ON examenes FOR EACH ROW BEGIN IF NEW.nota < 0 THEN SET NEW.edad = 0 ELSEIF NEW.nota > 10 THEN SET NEW.nota = 10; END IF; END; -- También puedo infringir una restricción para impedir la operación -- En este caso modifico la clave primaria para que sea NULL -- en caso de que la pista cerrada que se pretende insertar -- figure también en la tabla pistas_abiertas CREATE TRIGGER control_pistas_cerradas BEFORE INSERT ON pistas_cerradas FOR EACH ROW BEGIN DECLARE v_existe INT; SELECT COUNT(*) INTO v_existe FROM pistas_abiertas WHERE id_pista = NEW.id_pista; IF v_existe <> 0 THEN SET NEW.id_pista = NULL; END IF; END;
Usos y limitaciones de los disparadores
Antes de comenzar a utilizar los disparadores, conviene conocer cuándo deben ser utilizados, y cuáles son sus limitaciones.
Uno de los usos más comunes de los disparadores es el utilizarlos para mantener actualizados los campos calculados, de manera que cuando ocurra algún cambio en los datos se pueda actualizar automaticamente dicho campo calculado, si tuviera que verse afectado.
Además, permiten realizar tareas de auditoría, puesto que es posible registrar la actividad que ocurre en una o varias tablas en otra tabla, con el fin de registrar las operaciones que se realizan sobre ella, cuando se hacen, quién las hace, . . .
Por último, los cambios realizados por los triggers son difíciles de anular (ROLL BACK) por lo que se desaconseja implementar triggers que ejecuten procedimiento almacenados, aunque en las versiones actuales de MySql/MariaDB se esté permitiendo.
Eventos
Un evento no es más que una tarea la cual se ejecuta de forma automática en un momento temporal previamente programado.
Permiten a los administradores de bases de datos programar ciertas tareas que queremos que se ejecuten de forma periódica o en un momento concreto.
Implementación
Lo primero es habilitar nuestro servidor para ejecutar Eventos:
SET GLOBAL event_scheduler = [ON|OFF];
Las opciones de creación de eventos son las siguientes:
CREATE EVENT [IF NOT EXISTS] nombre_evento ON SCHEDULE schedule [ON COMPLETION [NOT] PRESERVE] DO operaciones_evento; schedule: AT TIMESTAMP [+ INTERVAL INTERVAL] ... | EVERY INTERVAL [STARTS TIMESTAMP [+ INTERVAL INTERVAL] ...] [ENDS TIMESTAMP [+ INTERVAL INTERVAL] ...] INTERVAL: quantity {YEAR | QUARTER | MONTH | DAY | HOUR | MINUTE | WEEK | SECOND | YEAR_MONTH | DAY_HOUR | DAY_MINUTE | DAY_SECOND | HOUR_MINUTE | HOUR_SECOND | MINUTE_SECOND}
Para definir un evento que se ejecuta dentro de un minuto, y que inserta 3 registros en una tabla de registro:
DELIMITER // CREATE EVENT insertar_evento ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 MINUTE DO BEGIN INSERT INTO registro VALUES ('Evento 1', NOW()); INSERT INTO registro VALUES ('Evento 2', NOW()); INSERT INTO registro VALUES ('Evento 3', NOW()); END // DELIMITER ;
Una vez que el evento se ha ejecutado se elimina automáticamente. Si queremos preservarlo debemos indicar la cláusula ON COMPLETION PRESERVE
en su definición.
Són utiles para ejecutar procedimientos almacenados:
CREATE EVENT nombre_evento ON SCHEDULE AT fecha_de_ejecución DO CALL procedimiento_almacenado();
Si queremos mostrar los eventos, eliminar un evento, detener temporalmente un evento o parar todos los eventos:
SHOW events; DROP EVENT nombre_evento; ALTER EVENT nombre_evento DISABLE|ENABLE; SET GLOBAL event scheduler = OFF;
Para crear eventos que se ejecuten periodicamente:
CREATE EVENT insertion_event ON SCHEDULE EVERY 1 MINUTE STARTS '2018-07-07 18:30:00' DO INSERT INTO registro VALUES ('Evento 1', NOW());
Ejercicios
- Con las siguientes tablas, implementa los procedimientos/funciones que se enumeran a continuación
Empleados (#id, nombre, apellidos, oficio, fecha_alta, salario, comision, -id_departamento) Departamentos (#id, nombre, ubicacion)
- Muestra todos los empleados de un departamento determinado
- Da de alta un empleado
- Da de baja un empleado
- Sube el salario a todos los empleados de un determinado departamento
- Función que devuelva el salario total de los empleados de un departamento determinado
- Función que devuelva el número de empleados que trabajan en un departamento determinado
- Sobre el modelo relacional del Ejercicio 1 del tema anterior, realiza los siguientes procedimientos/funciones:
- Da de alta un jugador
- Da de alta un equipo
- Registra el resultado de un partido
- Elimina un jugador
- Anota las incidencias de un partido determinado
- Función que devuelva el número de goles que un equipo ha metido en un partido
- Función que devuelva el número de goles que un equipo ha metido a otro equipo, comprobando que ambos equipos existen
- Dadas estas tablas, realiza los siguientes procedimientos/funciones:
Empleados (#id, dni, nombre, salario) Vendedores (#id, nro_vendedor, zona, -id_empleado) Polizas (#id, nro_poliza, importe, beneficiario, -id_vendedor, fecha, fecha_vencimiento) Empleado_Jefe (#(-id_empleado, -id_jefe))
- Da de alta un empleado como vendedor. Comprueba que no existe otro empleado con el mismo DNI
- Asigna un jefe a un empleado determinado. Comprueba que ambos existen
- Registra una nueva póliza, comprobando si existe el vendedor que se le asigna
- Elimina una póliza determinado. Comprueba antes que existe
- Función que devuelva el número de vendedores
- Función que devuelva cuantas pólizas tiene asignadas un vendedor determinado. Comprobar si existe el vendedor
- Función que compruebe si un determinado empleado es jefe de otro empleado
- Función que devuelva cuantos vendedores hay en una zona determinada
- Realiza los siguientes procedimientos/funciones sobre estas tablas:
Autores (#id, nombre, fecha_nacimiento, fecha_fallecimiento, nacionalidad) Obras (#id, titulo, fecha, -id_museo) Museos (#id, nombre, direccion, ciudad, pais) Obra_Autor (#(-id_obra, -id_autor))
- Da de alta un autor. Comprueba que no existe anteriormente
- Da de alta una obra. Comprueba que no existe anteriormente y que el museo asignado existe
- Asigna una obra determinado a un autor
- Elimina un autor. Si éste tiene obras asignadas no podrá ser eliminado
- Elimina una obra. Si está asignada a algún autor, se desvinculará antes de éste
- Modifica el museo de una obra. Comprueba que el nuevo museo existe
- Elimina un museo. Asigna todas las obras de dicho museo a otro determinado
- Función que devuelva el número de obras de un museo determinado. Comprobar que existe dicho museo
- Función que devuelva el número de obras que ha creado un autor determinado. Comprobar que existe dicho autor
- Función que devuelva el autor de una obra determinada
Prácticas
- Práctica 4.1 Procedimientos almacenados, funciones almacenadas, disparadores y eventos
© 2024 Santiago Faci y Fernando Valdeón