Tema: Procedimientos almacenados y cursores. Facultad : Ingeniería Escuela : Computación Asignatura: SQL SERVER GUÍA 2 Pág. 1 I. OBJETIVOS Utilizar procedimientos almacenados Conocer el uso de los cursores Realizar operaciones utilizando transacciones II. INTRODUCCIÓN Programación con Transact-SQL Transact-SQL no es realmente un lenguaje de programación similar a las herramientas de tercera y cuarta generación sin embargo permite utilizar SQL para realizar tareas complejas que requieren saltos, bucles, decisiones. Transact-SQL se utiliza a menudo en la creación de procedimientos almacenados y triggers de tal forma que las aplicaciones clientes que se conectan a SQL Server solo se preocupan por la presentación de los datos para el usuario final, mientras que la lógica de los procesos se maneja en el servidor. Variables: Las variables locales se identifican como aquellos objetos que comienzan con el carácter arroba '@' una vez; las variables globales se identifican como los objetos que tienen 2 arrobas al inicio '@@', como ejemplo de variables globales tenemos: @@rowcount, @@error. Las variables locales se declaran al inicio de un proceso por lotes o un procedimiento almacenado, la forma de asignarle valores a una variable es con la instrucción SELECT. El control de flujo en Transact-SQL Construcción IF..ELSE GOTO etiqueta WAITFOR WHILE BREAK CONTINUE RETURN [n] Descripción Define una decisión. Define un salto incondicional Establece un tiempo para la ejecución de una instrucción. El tiempo puede ser un intervalo de retardo o un instante especificado de ejecución (una hora concreta del día) Bucle básico de SQL Acompaña al bucle WHILE y le indica finalizarlo inmediatamente. Acompaña al bucle WHILE y le indica continuar con la siguiente iteración. Salida incondicional del procedimiento o GUÍA 2 Pág. 2 proceso por lotes, se puede definir un número entero como estado devuelto y puede asignarse a cualquier variable. Utilizado en conjunto con IF..ELSE o WHILE para agrupar un conjunto de instrucciones. Implementada en la instrucción SELECT y UPDATE y permite realizar consultas y actualizaciones condicionales. BEGIN..END CASE PRINT Es una instrucción para imprimir un dato en la pantalla, la sintaxis es: PRINT "cadena" ; cadena puede ser también una variable de tipo varchar. Por ejemplo: PRINT „Hola a todos‟ RAISERROR Es similar a PRINT, pero permite especificar un número de error y la severidad del mensaje. RAISERROR también permite que los errores se registren en el servicio de sucesos de Windows NT haciendo posible leerlos a través del visor de sucesos de Windows NT. La sintaxis es: RAISERROR({id_mensaje | cadena_mensaje}, severidad, estado [, argumento1 [,argumento2]]) WITH LOG. Después de llamar a RAISERROR, la variable global @@ERROR tendrá el valor de id_mensaje, si no se pasa ningún id_mensaje, asumirá 5000. Procedimientos almacenados. Dos de las cuestiones más importantes para el usuario de bases de datos son la velocidad y la eficiencia. Por ello surge una pregunta: ¿Cómo puedo proporcionar a los usuarios la velocidad y eficiencia que necesitan y merecen? Esa herramienta diseñada principalmente para optimizar la obtención de datos, es el procedimiento almacenado. Un procedimiento almacenado es una consulta que se almacena en una base de datos en SQL Server en lugar de almacenarse en el código cliente (normalmente C# o Java) en el equipo cliente. Creación de procedimientos almacenados (Store Procedures) La instrucción general para crear procedimientos almacenados es la siguiente: CREATE PROC nombre_proc parametros GUÍA 2 Pág. 3 AS INSTRUCCION SQL Es necesario aclarar, que un procedimiento almacenado puede recibir parámetros de entrada y devolver parámetros de salida. Ejemplo 1 Instrucción SQL USE AdventureWorks Select name, Color, ListPrice, SellStartDate FROM Production.Product WHERE SellStartDate > '1/1/2003' ORDER BY SellStartDate, Name Procedimiento con instrucción anterior CREATE PROCEDURE Production.ShowProduct AS Select name, Color, ListPrice, SellStartDate FROM Production.Product WHERE SellStartDate > '1/1/2003' ORDER BY SellStartDate, Name GO Para probar el nuevo procedimiento, abra una nueva consulta de SQL Server y escriba y ejecute el código siguiente. USE AdventureWorks EXEC Production.ShowProduct Nota: los procedimientos almacenados los puede encontrar en la base de datos donde los trabaja, en la opción programación. Ejemplo2: --Obteniendo Ganancia sobre las ventas CREATE PROC Ganancia_de_Venta @id_Venta int, -- parametro de entrada @id_Prod_bodega int, -- parametro de entrada @ganancia decimal (8,2) OUTPUT -- parametro de salida AS declare @unidades int --Asignando un valor a la variable unidades SELECT @unidades = unidades FROM detalleventa WHERE idventa=@id_Venta AND idprodbod=@id_Prod_bodega SELECT @ganancia = (preciov-precioc)*@unidades FROM bodega WHERE idprodbod=@id_Prod_bodega GO GUÍA 2 Pág. 4 Reglas de procedimientos almacenados Entre las reglas para la programación de procedimientos almacenados, cabe citar las siguientes: La propia definición CREATE PROCEDURE puede incluir cualquier número y tipo de instrucciones SQL, excepto las siguientes instrucciones CREATE, que no pueden ser utilizadas nunca dentro de un procedimiento almacenado: CREATE DEFAULT CREATE PROCEDURE CREATE RULE CREATE TRIGGER CREATE VIEW Se puede crear otros objetos de base de datos dentro de un procedimiento almacenado. Puede hacer referencia a un objeto creado en el mismo procedimiento almacenado, siempre que se cree antes de que se haga referencia al objeto. Puede hacer referencia a tablas temporales dentro de un procedimiento almacenado. Si ejecuta un procedimiento almacenado que llama a otro procedimiento almacenado, el procedimiento al que se llama puede tener acceso a todos los objetos creados por el primer procedimiento, incluidas las tablas temporales. El número máximo de parámetros en un procedimiento almacenado es de 1,024. El número máximo de variables locales en un procedimiento almacenado está limitado únicamente por la memoria disponible. Procedimientos almacenados del sistema Muchas de las actividades administrativas de SQL Server se realizan mediante un tipo especial de procedimiento denominado procedimiento almacenado del sistema. Los procedimientos almacenados del sistema se crean y se almacenan en la base de datos master, con el prefijo sp_ (stored procedure). Estos procedimientos se pueden ejecutar desde cualquier base de datos. Por ejemplo: sp_help, sp_heldb, sp_droplogin, etc. Cursores Una base de datos relaciona como SQL Server está orientada a conjuntos de manera natural, por ejemplo una instrucción SELECT regresa un conjunto de datos; sin embargo muchas veces es necesario utilizar no el enfoque de conjunto de datos sino de registros para realizar ciertas operaciones no complejas, es donde se implementan los cursores. Un cursor es un conjunto de resultados donde existe la posibilidad de desplazarse registro por registro en cualquier dirección. GUÍA 2 Pág. 5 Trabajar con cursores implica algunos pasos básicos como son el declararlos, abrirlos, capturar filas en el cursor, opcionalmente se puede modificar o eliminar registros, luego se debe cerrar el cursor y por ultimo retirarlo de la memoria. Cuando se crean cursores, se debe considerar lo siguiente: - - - Para declarar un cursor se utiliza la instrucción DECLARE. Para abrir el cursor se utiliza la instrucción OPEN Para capturar filas de un cursor se utiliza la instrucción FETCH, es necesario declarar variables e indicarle a FETCH cuales son las variables., existe una variable global llamada @@FETCH_STATUS que indica el estado de la última instrucción FETCH, así si @@ FETCH_STATUS = 0 la captura fue un éxito, si @@ FETCH_STATUS = -1 no hay mas filas (se ha llegado al inicio o al final) y si @@FETCH_STATUS = -2 significa que la fila ya no existe en el cursor (probablemente se eliminó). Para actualizar o eliminar el registro que esta siendo apuntado por el cursor, se utiliza la instrucción UPDATE / DELETE tabla WHERE CURRENT OF nombre_cursor. Para cerrar el cursor, se utiliza la instrucción: CLOSE. Para liberar la memoria, se utiliza la instrucción DEALLOCATE. III. MATERIAL Y EQUIPO A UTILIZAR Guía de Laboratorio Nº 2 de SQL Server Computadora con SQL SERVER 2005 Disquete o memoria USB IV. PROCEDIMIENTO En esta práctica utilizaremos los procedimientos almacenados, cursores y transacciones utilizando la base de datos de SQL Server 2005, para ello se plantea la siguiente base de datos: Definición. Esta base de datos se desea para un almacén que tiene varios productos a la venta, los cuales adquiere de varios proveedores. Un producto puede ser distribuido por varios proveedores y un proveedor distribuye varios productos, como es una relación de muchos a muchos se tiene una tercera tabla llamada "catalogo" donde se indica el detalle del producto. Lo interesante es que el almacén adquiere productos a cierto precio y los almacena en bodega, y a medida que las existencias del producto van disminuyendo, vuelve a adquirir nuevos productos, sin embargo el costo del producto comprado por el almacén puede variar y el almacén registra el precio de costo y de venta de los productos GUÍA 2 Pág. 6 adquiridos en una compra y maneja la política de vender primero los productos más antiguos. Para poder crear la base de datos anterior, deberá crear una base de datos con el nombre almacen_carnet, y ejecute los scripts para generar las tablas que se listan a continuación: -- creacion de las tablas create table proveedor ( idproveedor char(8) not null, nombre char(60) not null, constraint pk_idproveedor primary key (idproveedor) ) create table producto ( idproducto char(8) not null, nombre char(60) not null, constraint pk_idproducto primary key (idproducto) ) create table catalogo ( idprodcat char(16) not null, idproducto char(8) not null, idproveedor char(8) not null, precio decimal(8,2) not null, constraint pk_idprodcat primary key clustered (idprodcat), GUÍA 2 Pág. 7 constraint fk_idproveedor foreign key (idproveedor) references proveedor(idproveedor), constraint fk_idproducto foreign key (idproducto) references producto(idproducto), constraint u_prodcat unique (idproducto,idproveedor), constraint ck_precio_catalogo check (precio>=0), constraint ck_idprodcat_catalogo check (idprodcat=idproducto+idproveedor) ) create table bodega ( idprodbod integer not null identity, idprodcat char(16) not null, fecha datetime not null, precioc decimal(8,2) not null, -- precio de costo preciov decimal(8,2) not null, -- precio de venta unidades integer not null, -- unidades constraint pk_idprodbod primary key (idprodbod), constraint fk_idprodcat foreign key (idprodcat) references catalogo(idprodcat), constraint ck_precios_bodega check (precioc >=0 and preciov >=0), constraint ck_unidades_bodega check (unidades >=0) ) create table cliente ( idcliente char(8) not null, nombre char(60) not null, constraint pk_idcliente primary key (idcliente) ) create table venta ( idventa integer not null identity, fecha datetime not null, idcliente char(8) not null, constraint pk_idventa primary key (idventa), constraint fk_idcliente foreign key (idcliente) references cliente(idcliente) ) create table detalleventa ( idventa integer not null, idprodbod integer not null, unidades integer not null, constraint fk_idprodbod foreign key (idprodbod) references bodega(idprodbod), constraint fk_idventa foreign key (idventa) references venta(idventa), constraint ck_unidades_detalleventa check (unidades>0) ) Ejecute las siguientes instrucciones SQL para introducir datos a las tablas de trabajo: GUÍA 2 Pág. 8 -- productos que ofrecen los proveedores insert into producto values ('prod0001', 'producto a') insert into producto values ('prod0002', 'producto b') insert into producto values ('prod0003', 'producto c') insert into producto values ('prod0004', 'producto d') -- nuestros proveedores insert into proveedor values ('prov0001','proveedor 1') insert into proveedor values ('prov0002','proveedor 2') insert into proveedor values ('prov0003','proveedor 3') -- insertando los clientes insert into cliente values ('clie0001','cliente 1') insert into cliente values ('clie0002','cliente 2') insert into cliente values ('clie0003','cliente 3') En la tabla de catálogo, se generará un código para cada producto distribuido por cada proveedor, dicho código se almacenará en el campo llamado 'idprodcat': -- insertando que productos distribuye que proveedor y el costo insert into catalogo VALUES('prod0001'+'prov0001','prod0001','prov0001',5.25) insert into catalogo VALUES('prod0001'+'prov0002','prod0001','prov0002',6.00) insert into catalogo VALUES('prod0001'+'prov0003','prod0001','prov0003',5.50) insert into catalogo VALUES('prod0002'+'prov0001','prod0002','prov0001',3.15) insert into catalogo VALUES('prod0002'+'prov0002','prod0002','prov0002',3.10) insert into catalogo VALUES('prod0003'+'prov0002','prod0003','prov0002',15.25) insert into catalogo VALUES('prod0003'+'prov0003','prod0003','prov0003',14.90) insert into catalogo VALUES('prod0004'+'prov0001','prod0004','prov0001',3.85) insert into catalogo VALUES('prod0004'+'prov0003','prod0004','prov0003',2.00) GUÍA 2 Pág. 9 Para insertar datos en la bodega, se creara un procedimiento almacenado que toma 5 parámetros de entrada los cuales son: @fecha de tipo datetime, @proveedor,@producto de tipo char, @ganancia de tipo decimal y @unidades de tipo entero(int). Ejecute el siguiente query para crear el procedimiento almacenado “producto_bodega”: -- procedimiento almacenado que inserta productos en bodega create procedure sp_producto_bodega @fecha datetime, @proveedor char(8), @producto char(8), @ganancia decimal(4,2), @unidades int as declare @idprodcat char(16) declare @precio decimal(8,2) select @idprodcat = idprodcat from catalogo where idproducto=@producto and idproveedor=@proveedor if @@rowcount=0 goto error select @precio = precio from catalogo where idprodcat = @idprodcat insert into bodega values (@idprodcat,@fecha,@precio,@precio+(@precio *@ganancia),@unidades) return(0) error: print 'No existe un producto en el catalogo' return(1) GO Algunas características del procedimiento almacenado creado anteriormente son: a) La línea declare @idprodcat char(16) esta declarando una variable local utilizada dentro del SP (Store Procedure). No confundir una variable local con un parámetro de entrada o de salida. b) Para asignarle un valor a una variable se utiliza la sentencia SELECT seguido de la consulta o valor que será almacenado en dicha variable. Por ejemplo: - SELECT @X = 10 ( aquí se le asigna a la variable X el valor de 10 ). SELECT @precio = precio from catalogo where idprodcat = @idprodcat (aquí se le esta asignando a la variable precio el dato que retorna la consulta hecha a la tabla catalogo. En este caso el query retorna el campo precio de dicha tabla). c) La línea if @@rowcount = 0 indica que si el ultimo query realizado no devolvió ningún registro (@@rowcount=0) entonces se hace un salto incondicional a la etiqueta error. GUÍA 2 Pág. 10 d) Si hubo error la sentencia print muestra el mensaje 'No existe un producto en el catalogo', de lo contrario se insertan los datos. Utilice el procedimiento almacenado creado anteriormente para insertar los siguientes datos: -- haciendo las compras a los proveedores en los meses de enero y febrero -- del 2000 y ganando un 20% sobre el costo. exec sp_producto_bodega '01/01/2000','prov0001','prod0001',0.2,5 exec sp_producto_bodega '28/01/2000','prov0002','prod0003',0.2,15 exec sp_producto_bodega '01/01/2000','prov0002','prod0002',0.2,10 exec sp_producto_bodega '01/01/2000','prov0003','prod0004',0.2,20 exec sp_producto_bodega '01/02/2000','prov0003','prod0001',0.2,15 exec sp_producto_bodega '01/02/2000','prov0001','prod0002',0.2,5 exec sp_producto_bodega '07/01/2000','prov0001','prod0001',0.2,15 Ahora procederemos a realizar un procedimiento almacenado que hará referencia a la tabla detalleventa de tal forma que si insertamos la venta de un producto, primero se debe verificar que haya existencia de dicho producto en nuestra bodega; si hay existencia en bodega se permitirá la acción y se actualizarán las existencias en la tabla bodega para que exista consistencia entre los datos (si vendo productos, disminuye mi bodega). El procedimiento almacenado seria el siguiente: create proc sp_actualizar_bodega @idprodbod int, @unidades int as declare @existencia int select @existencia = unidades from bodega where idprodbod = @idprodbod begin tran if (@existencia<@unidades) goto errores update bodega set unidades = unidades - @unidades where idprodbod = @idprodbod commit tran goto fin errores: rollback tran --se elimina la transaccion fin: print 'Bodega actualizada' Por ultimo crearemos un procedimiento almacenado más sofisticado, que permita agregar fácilmente las unidades de un producto a nuestra tabla detalleventa, los parámetros a pasar deben ser: el id de la venta, el idprodcat (es decir el codigo del GUÍA 2 Pág. 11 producto distribuido por un proveedor determinado) y las unidades vendidas. Además dicho procedimiento llamara al SP actualizar_bodega. Nuestro procedimiento almacenado consultará nuestra bodega para revisar las compras que se han efectuado de ese producto, y como la política es vender el producto más antiguo, se venderán los productos comprados en las fechas más antiguas. El procedimiento almacenado necesita crear un cursor, ya que se deben explorar todas las compras realizadas de ese producto ordenadas por fechas y decrementar las entradas (compras) más antiguas. El procedimiento almacenado queda de la siguiente forma: create procedure sp_producto_detalleventa @idventa int, @idprodcat char(16), @unidades int as declare @idprodbod char(8) declare @existencia int declare @unidadestmp int select @existencia=sum(unidades) from bodega where idprodcat=@idprodcat if @existencia < @unidades goto errores declare ctmp cursor for select idprodbod,unidades from bodega where idprodcat=@idprodcat order by fecha asc --comienza una transaccion begin tran open ctmp --Ejecutando el primer fetch y almacenando los valores en variables --Las variables estan en el mismo orden de las columnas de la --sentencia SELECT del cursor fetch next from ctmp into @idprodbod,@unidadestmp while (@unidades>0 and @@fetch_status=0) begin if @unidadestmp <= @unidades begin select @unidades=@unidades-@unidadestmp insert into detalleventa values(@idventa,@idprodbod,@unidadestmp) --actualizando bodega llamando al SP EXEC sp_actualizar_bodega @idprodbod,@unidadestmp end else begin GUÍA 2 Pág. 12 insert into detalleventa values(@idventa,@idprodbod,@unidades) --actualizando bodega llamando al SP EXEC sp_actualizar_bodega @idprodbod,@unidades select @unidades=0 end fetch next from ctmp into @idprodbod,@unidadestmp end close ctmp deallocate ctmp --la transaccion finaliza commit tran return 0 errores: return 1 Para comprobar la ejecución de los procedimientos almacenados anteriores, se harán unas pruebas registrando una venta para el cliente con código „clie0001‟. Primero visualicemos cuantas compras se han realizado „prod0001prov0001‟ (producto 1 distribuido por el proveedor 1) del producto select * from bodega where idprodcat='prod0001prov0001' Observe que existen dos compras, una realizada el 1 de enero del 2000 por 5 unidades y otra realizada el 7 de enero del 2000 por 15 por lo que se tienen 20 unidades en total. Registrando la venta para el cliente 1: insert into venta values ('28/03/2001','clie0001') Ahora se ingresaran datos en la tabla detalleventa utilizando el procedimiento almacenado sp_ producto_detalleventa. Ejecute los siguientes querys como un solo batch (ambos querys de una sola vez): -- obteniendo el valor del codigo idventa en una variable declare @idventa int select @idventa=idventa from venta where idcliente='clie0001' --suponiendo que el cliente 01 compra 4 unidades EXEC sp_producto_detalleventa @idventa, 'prod0001prov0001', 4 GO Ahora ejecute este query para ver el estado de la tabla bodega: -- visualizando como cambiaron nuestros registros en bodega select * from bodega where idprodcat='prod0001prov0001' GUÍA 2 Pág. 13 Observe como se disminuyo en 4 el primer lote de compras. Suponga ahora que el cliente 02 realiza una compra por 7 unidades del mismo producto: -- se registra primero la venta del cliente 02 insert into venta values ('28/03/2001','clie0002') declare @idventa int select @idventa=idventa from venta where idcliente='clie0002' -- el cliente 02 compra 7 unidades Exec sp_producto_detalleventa @idventa, 'prod0001prov0001', 7 Observe como se han modificado los registros en la tabla bodega : select * from bodega where idprodcat='prod0001prov0001' Se puede observar, del primer lote de compras ya no hay existencia, mientras que del segundo se decremento en 6. Ejemplo Cursores A la misma base de datos insertar la siguiente tabla create table nombrecito( cod int, nombre varchar(25), apellido varchar(25), nombrecompleto varchar(50)) insert insert insert insert into into into into nombrecito nombrecito nombrecito nombrecito values('1','Carlos','Castro','no') values('2','Jose','Abarca','no') values('3','Lissette','Jimenez','no') values('4','Juan','Elias','no') Como puede ver se insertaron registros y el campo de nombre completo no se ingreso correctamente, por lo que tendremos que actualizar registro por registro, para ello haremos uso de cursores -- declaramos las variables declare @cod as int declare @nombre as varchar(25) declare @apellido as varchar(25) -- declaramos un cursor llamado "MICURSOR". El select debe contener sólo los campos a utilizar. declare MICURSOR cursor for select cod, nombre,apellido from nombrecito GUÍA 2 Pág. 14 open MICURSOR --Avanzamos un registro y cargamos en las variables los valores encontrados en el primer registro fetch next from MICURSOR into @cod, @nombre, @apellido while @@fetch_status = 0 begin update nombrecito set nombrecompleto= @nombre+' '+@apellido where cod=@cod -- Avanzamos otro registro fetch next from MICURSOR into @cod, @nombre, @apellido end -- cerramos el cursor close MICURSOR deallocate MICURSOR V. INVESTIGACIÓN Y EJERCICIOS COMPLEMENTARIOS Investigue que son los desencadenadores Cual es la diferencia entre un desencadenador DDL y DML Implemente un trigger en la tabla producto el cual se debe activar cuando halla una actualizacion de datos en dicha tabla GUÍA 2 Pág. 15