You are on page 1of 8

Transacciones en SQL Server

1 . Introducción
2 . Sentencias para una transacción
3 . Transacciones anidadas
4 . Un par de ejemplos más
5 . Transacciones y procedimientos almacenados.

Transacciones en SQL Server

Entre las habilidades de todo Sistema Gestor de Bases de Datos Relaciones tiene que estar la de permitir al programador
crear transacciones. Y aunque el SQL Server nos permite trabajar con transacciones de manera sencilla y eficaz siempre
hay dificultades...

Introducción
Una transacción es un conjunto de operaciones que van a ser tratadas como una única unidad. Estas transacciones deben
cumplir 4 propiedades fundamentales comúnmente conocidas como ACID (atomicidad, coherencia, asilamiento y
durabilidad).

La transacción más simple en SQL Server es una única sentencia SQL. Por ejemplo una sentencia como esta:

UPDATE Products SET UnitPrice=20 WHERE ProductName ='Chai'

Es una transacción. (Como siempre ejemplos de Northwind) Esta es una transacción 'autocommit', una transacción
autocompletada.

Cuando enviamos esta sentencia al SQL Server se escribe en el fichero de transacciones lo que va a ocurrir y a
continuación realiza los cambios necesarios en la base de datos. Si hay algún tipo de problema al hacer esta operación el
SQL Server puede leer en el fichero de transacciones lo que se estaba haciendo y si es necesario puede devolver la base de
datos al estado en el que se encontraba antes de recibir la sentencia.

Por supuesto este tipo de transacciones no requieren de nuestra intervención puesto que el sistema se encarga de todo. Sin
embargo si hay que realizar varias operaciones y queremos que sean tratadas como una unidad tenemos que crear esas
transacciones de manera explícita.

Sentencias para una transacción


Como decíamos una transacción es un conjunto de operaciones tratadas como una sola. Este conjunto de operaciones debe
marcarse como transacción para que todas las operaciones que la conforman tengan éxito o todas fracasen.

La sentencia que se utiliza para indicar el comienzo de una transacción es 'BEGIN TRAN'. Si alguna de las operaciones de
una transacción falla hay que deshacer la transacción en su totalidad para volver al estado inicial en el que estaba la base
de datos antes de empezar. Esto se consigue con la sentencia 'ROLLBACK TRAN'.

Si todas las operaciones de una transacción se completan con éxito hay que marcar el fin de una transacción para que la
base de datos vuelva a estar en un estado consistente con la sentencia 'COMMIT TRAN'.

Un ejemplo
Trabajaremos con la base de datos Northwind en nuestros ejemplos. Vamos a realizar una transacción que modifica el
precio de dos productos de la base de datos.
USE NorthWind
DECLARE @Error int
--Declaramos una variable que utilizaremos para almacenar un posible
código de error

BEGIN TRAN
--Iniciamos la transacción
UPDATE Products SET UnitPrice=20 WHERE ProductName ='Chai'
--Ejecutamos la primera sentencia
SET @Error=@@ERROR
--Si ocurre un error almacenamos su código en @Error
--y saltamos al trozo de código que deshara la transacción. Si, eso de
ahí es un
--GOTO, el demonio de los programadores, pero no pasa nada por usarlo
--cuando es necesario
IF (@Error<>0) GOTO TratarError

--Si la primera sentencia se ejecuta con éxito, pasamos a la segunda


UPDATE Products SET UnitPrice=20 WHERE ProductName='Chang'
SET @Error=@@ERROR
--Y si hay un error hacemos como antes
IF (@Error<>0) GOTO TratarError

--Si llegamos hasta aquí es que los dos UPDATE se han completado con
--éxito y podemos "guardar" la transacción en la base de datos
COMMIT TRAN

TratarError:
--Si ha ocurrido algún error llegamos hasta aquí
If @@Error<>0 THEN
BEGIN
PRINT 'Ha ecorrido un error. Abortamos la transacción'
--Se lo comunicamos al usuario y deshacemos la transacción
--todo volverá a estar como si nada hubiera ocurrido
ROLLBACK TRAN
END

Como se puede ver para cada sentencia que se ejecuta miramos si se ha producido o no un error, y si detectamos un error
ejecutamos el bloque de código que deshace la transacción.

Hay una interpretación incorrecta en cuanto al funcionamiento de las transacciones que esta bastante extendida. Mucha
gente cree que si tenemos varias sentencias dentro de una transacción y una de ellas falla, la transacción se aborta en su
totalidad.

¡Nada más lejos de la realidad! Si tenemos dos sentencias dentro de una transacción.

USE NorthWind
BEGIN TRAN
UPDATE Products SET UnitPrice=20 WHERE ProductName='Chang'
UPDATE Products SET UnitPrice=20 WHERE ProductName='Chang'
COMMIT TRAN

Estas dos sentencias se ejecutarán como una sola. Si por ejemplo en medio de la transacción (después del primer update y
antes del segundo) hay un corte de electricidad, cuando el SQL Server se recupere se encontrará en medio de una
transacción y, o bien la termina o bien la deshace, pero no se quedará a medias.
El error está en pensar que si la ejecución de la primera sentencia da un error se cancelará la transacción. El SQL Server
sólo se preocupa de ejecutar las sentencias, no de averiguar si lo hacen correctamente o si la lógica de la transacción es
correcta. Eso es cosa nuestra.

Por eso en el ejemplo que tenemos más arriba para cada sentencia de nuestro conjunto averiguamos si se ha producido un
error y si es así actuamos en consecuencia cancelando toda la operación.

Transacciones anidadas
Otra de las posibilidades que nos ofrece el SQL Server es utilizar transacciones anidadas. Esto quiere decir que podemos
tener transacciones dentro de transacciones, es decir, podemos empezar una nueva transacción sin haber terminado la
anterior.

Asociada a esta idea de anidamiento existe una variable global @@TRANCOUNT que tiene valor 0 si no existe ningún
nivel de anidamiento, 1 si hay una transacción anidada, 2 si estamos en el segundo nivel de anidamiento. y así
sucesivamente.

La dificultad de trabajar con transacciones anidadas está en el comportamiento que tienen ahora las sentencias 'COMMIT
TRAN' y 'ROLLBACK TRAN'

 ROLLBACK TRAN: Dentro de una transacción anidada esta sentencia deshace todas las transacciones
internas hasta la instrucción BEGIN TRANSACTION más externa.
 COMMIT TRAN: Dentro de una transacción anidada esta sentencia únicamente reduce en 1 el valor de
@@TRANCOUNT, pero no "finaliza" ninguna transacción ni "guarda" los cambios. En el caso en el que
@@TRANCOUNT=1 (cuando estamos en la última transacción) COMMIT TRAN hace que todas las
modificaciones efectuadas sobre los datos desde el inicio de la transacción sean parte permanente de la base de
datos, libera los recursos mantenidos por la conexión y reduce @@TRANCOUNT a 0.

Como siempre un ejemplo es lo mejor para entender como funciona.

CREATE TABLE Test (Columna int)


GO
BEGIN TRAN TranExterna -- @@TRANCOUNT ahora es 1
SELECT 'El nivel de anidamiento es', @@TRANCOUNT
INSERT INTO Test VALUES (1)
BEGIN TRAN TranInterna1 -- @@TRANCOUNT ahora es 2.
SELECT 'El nivel de anidamiento es', @@TRANCOUNT
INSERT INTO Test VALUES (2)
BEGIN TRAN TranInterna2 -- @@TRANCOUNT ahora es 3.
SELECT 'El nivel de anidamiento es', @@TRANCOUNT
INSERT INTO Test VALUES (3)
COMMIT TRAN TranInterna2 -- Reduce @@TRANCOUNT a 2.
-- Pero no se guarda nada en la base de datos.
SELECT 'El nivel de anidamiento es', @@TRANCOUNT
COMMIT TRAN TranInterna1 -- Reduce @@TRANCOUNT a 1.
-- Pero no se guarda nada en la base de datos.
SELECT 'El nivel de anidamiento es', @@TRANCOUNT
COMMIT TRAN TranExterna -- Reduce @@TRANCOUNT a 0.
-- Se lleva a cabo la transacción externa y todo lo que conlleva.
SELECT 'El nivel de anidamiento es', @@TRANCOUNT
SELECT * FROM Test

Por cierto que lo de usar nombre para las transacciones es por claridad, puesto que COMMIT TRAN como ya hemos
dicho solamente reduce en 1 el valor de @@TRANCOUNT.

Veamos ahora un ejemplo de transacción anidada con ROLLBACK TRAN


BEGIN TRAN TranExterna -- @@TRANCOUNT ahora es 1
SELECT 'El nivel de anidamiento es', @@TRANCOUNT
INSERT INTO Test VALUES (1)
BEGIN TRAN TranInterna1 -- @@TRANCOUNT ahora es 2.
SELECT 'El nivel de anidamiento es', @@TRANCOUNT
INSERT INTO Test VALUES (2)
BEGIN TRAN TranInterna2 -- @@TRANCOUNT ahora es 3.
SELECT 'El nivel de anidamiento es', @@TRANCOUNT
INSERT INTO Test VALUES (3)
ROLLBACK TRAN --@@TRANCOUNT es 0 y se deshace
--la transacción externa y todas las internas
SELECT 'El nivel de anidamiento es', @@TRANCOUNT
SELECT * FROM Test

En este caso no se inserta nada puesto que el ROLLBACK TRAN deshace todas las transacciones dentro de nuestro
anidamiento hasta la transacción más externa y además hace @@TRANCOUNT=0

¿Supone este funcionamiento asimétrico del COMMIT y del ROLLBACK un problema? Pues la verdad es que no. La
manera de tratar las transacciones por el SQL Server es la que nos permite programar de manera natural los anidamientos.
De todos modos, si queremos ir un poco más lejos hay una cuarta sentencia para trabajar con transacciones: SAVE TRAN

Save Tran

Esta sentencia crea un punto de almacenamiento dentro de una transacción. Esta marca sirve para deshacer una
transacción en curso sólo hasta ese punto. Por supuesto nuestra transacción debe continuar y terminar con un COMMIN
TRAN (o los que hagan falta) para que todo se guarde o con un ROLLBACK TRAN para volver al estado previo al
primer BEGIN TRAN.

BEGIN TRAN TranExterna -- @@TRANCOUNT ahora es 1


SELECT 'El nivel de anidamiento es', @@TRANCOUNT
INSERT INTO Test VALUES (1)
BEGIN TRAN TranInterna1 -- @@TRANCOUNT ahora es 2.
SELECT 'El nivel de anidamiento es', @@TRANCOUNT
INSERT INTO Test VALUES (2)
SAVE TRAN Guadada
BEGIN TRAN TranInterna2 -- @@TRANCOUNT ahora es 3.
SELECT 'El nivel de anidamiento es', @@TRANCOUNT
INSERT INTO Test VALUES (3)
ROLLBACK TRAN Guadada -- se deshace lo hecho el punto guardado.
SELECT 'El nivel de anidamiento es', @@TRANCOUNT
--Ahora podemos decidir si la transacción se lleva a cabo
--o se deshace completamente
--Para deshacerla un ROLLBACK bastará como hemos visto
--Pero para guardar la transacción hace falta reducir @@TRANCOUNT a 0
COMMIT TRAN TranInterna1 -- Reduce @@TRANCOUNT a 2.
SELECT 'El nivel de anidamiento es', @@TRANCOUNT
COMMIT TRAN TranInterna1 -- Reduce @@TRANCOUNT a 1.
-- Pero no se guarda nada en la base de datos.
SELECT 'El nivel de anidamiento es', @@TRANCOUNT
COMMIT TRAN TranExterna -- Reduce @@TRANCOUNT a 0.
-- Se lleva a cabo la transacción externa y todo lo que conlleva.
SELECT 'El nivel de anidamiento es', @@TRANCOUNT
SELECT * FROM Test

Si no ponemos el nombre del punto salvado con SAVE TRAN al hacer un ROLLBACK TRAN se deshace la transacción
más externa y @@TRANCOUNT se pone a 0.
Como podemos ver el uso de transacciones no es complicado, e incluso las transacciones anidadas si se tratan con cuidado
son fáciles de manejar.

Un par de ejemplos más


Veamos otro par de ejemplos para dejar claro como funcionan las sentencias BEGIN, COMMIT y SAVE

BEGIN TRAN
-- Primer BEGIN TRAN y ahora @@TRANCOUNT = 1
BEGIN TRAN
-- Ahora @@TRANCOUNT = 2
COMMIT TRAN
-- Volvemos a @@TRANCOUNT = 1
-- Pero no se guarda nada ni se hacen efectivos los posibles
cambios
COMMIT TRAN
-- Por fin @@TRANCOUNT = 0
-- Si hubiera cambios pendientes se llevan a la base de datos
-- Y volvemos a un estado normal con la transacción acabada

Uso del COMMIT

BEGIN TRAN
-- Primer BEGIN TRAN y @@TRANCOUNT = 1
BEGIN TRAN
-- Ahora @@TRANCOUNT = 2
COMMIT TRAN
-- Como antes @@TRANCOUNT = 1
--Y como antes nada se guarda
ROLLBACK TRAN
-- Se cancela TODA la transacción. Recordemos que el COMMIT
-- de antes no guardo nada, solo redujo @@TRANCOUNT
-- Ahora @@TRANCOUNT = 0
COMMIT TRAN
-- No vale para nada porque @@TRANCOUNT es 0 por el efecto del ROLLBACK

Uso del ROLLBACK

En cuanto al SAVE TRAN podemos recordarlo con el siguiente ejemplo:

CREATE TABLE Tabla1 (Columna1 varchar(50))


GO

BEGIN TRAN
INSERT INTO Tabla1 VALUES ('Primer valor')
SAVE TRAN Punto1
INSERT INTO Tabla1 VALUES ('Segundo valor')
ROLLBACK TRAN Punto1
INSERT INTO Tabla1 VALUES ('Tercer valor')
COMMIT TRAN

SELECT * FROM Tabla1


Columna1
--------------------------------------------------
Primer valor
Tercer valor
(2 filas afectadas)

Un ROLLBACK a un SAVE TRAN no deshace la transacción en curso ni modifica @@TRANCOUNT, simplemente


cancela lo ocurrido desde el 'SAVE TRAN nombre' hasta su 'ROLLBACK TRAN nombre'

Transacciones y procedimientos almacenados.


Cuando trabajamos con procedimientos almacenados debemos recordar que cada procedimiento almacenado es una
unidad. Cuando se ejecuta lo hace de manera independiente de quien lo llama. Sin embargo si tenemos un ROLLBACK
TRAN dentro de un procedimiento almacenado cancelaremos la transacción en curso, pero si hay una transacción externa
al procedimiento en el que estamos trabajando se cancelará esa transacción externa.

Con esto no quiero decir que no se pueda usar, simplemente que hay que tener muy claras las 4 normas sobre las
sentencias BEGIN, ROLLBACK y COMMIT que comentamos al principio de este artículo. Veamos como se comporta
una transacción en un procedimiento almacenado llamado desde una transacción.

CREATE PROCEDURE Inserta2


AS
BEGIN TRAN --Uno
INSERT INTO Tabla1 VALUES ('Valor2')
ROLLBACK TRAN --Uno
GO

CREATE PROCEDURE Inserta1


AS
BEGIN TRAN --Dos
INSERT INTO Tabla1 VALUES ('Valor 1')
EXEC Inserta2
INSERT INTO Tabla1 VALUES ('Valor3')
COMMIT TRAN --Dos
GO

En principio parece que si ejecutamos el procedimiento 'Inserta1' el resultado sería:

EXECUTE inserta1
SELECT * FROM tabla1

txt
--------------------------------------------------
Valor1
Valor3
(2 filas afectadas)

Pero lo que obtenemos es:

EXECUTE inserta1
SELECT * FROM tabla1

(1 filas afectadas)
(1 filas afectadas)
Servidor: mensaje 266, nivel 16, estado 2, procedimiento Inserta2, línea
8
El recuento de transacciones después de EXECUTE
indica que falta una instrucción COMMIT o ROLLBACK TRANSACTION.
Recuento anterior = 1, recuento actual = 0.
(1 filas afectadas)
Servidor: mensaje 3902, nivel 16, estado 1, procedimiento Inserta1, línea
8
La petición COMMIT TRANSACTION no tiene la correspondiente BEGIN
TRANSACTION.
txt
--------------------------------------------------
Valor3
(1 filas afectadas)

Si analizamos estos mensajes vemos que se inserta la primera fila, se salta al segundo procedimiento almacenado y se
inserta la segunda fila. Se ejecuta el 'ROLLBACK TRAN -Dos' del segundo procedimiento almacenado y se deshacen las
dos inserciones porque este ROLLBACK cancela la transacción exterior, la del primer procedimiento almacenado. A
continuación se termina el segundo procedimiento almacenado y como este procedimiento tiene un 'BEGIN TRAN -Dos'
y no tiene un COMMIT o un ROLLBACK (recordemos que el 'ROLLBACK TRAN -Dos' termina el 'BEGIN TRAN
-Uno') se produce un error y nos avisa que el procedimiento almacenado termina con una transacción pendiente. Al volver
al procedimiento almacenado externo se ejecuta el INSERT que inserta la tercera fila y a continuación el 'COMMIT
TRAN --Uno'. Aquí aparece otro error puesto que este 'COMMIT TRAN -Uno' estaba ahí para finalizar una transacción
que ya ha sido cancelada anteriormente.

El modo correcto
Para que nuestras transacciones se comporten como se espera dentro de un procedimiento almacenado podemos recurrir al
SAVE TRAN.

Veamos como:

CREATE PROCEDURE Inserta2


AS
SAVE TRAN Guardado
INSERT INTO Tabla1 VALUES ('Valor2')
ROLLBACK TRAN Guardado
GO

CREATE PROCEDURE Inserta1


AS
BEGIN TRAN
INSERT INTO Tabla1 VALUES ('Valor 1')
EXEC Inserta2
INSERT INTO Tabla1 VALUES ('Valor 3')
COMMIT TRAN
GO

Ahora el 'ROLLBACK TRAN guardado' del segundo procedimiento almacenado deshace la transacción sólo hasta el
punto guardado. Además este ROLLBACK no decrementa el @@TRANCOUNT ni afecta a las transacciones en curso.

El resultado obtenido al ejecutar este procedimiento almacenado es:

EXECUTE inserta1
SELECT * FROM tabla1

(1 filas afectadas)
(1 filas afectadas)
(1 filas afectadas)
txt
--------------------------------------------------
Valor 1
Valor 3
(2 filas afectadas)

Una vez vistos estos ejemplos queda claro que no hay problema en utilizar transacciones dentro de procedimientos
almacenados siempre que tengamos en cuenta el comportamiento del ROLLBACK TRAN y que utilicemos el SAVE
TRAN de manera adecuada.

Para mas información sobre transacciones:

 Más información en MICROSOFT http://msdn.microsoft.com/library/default.asp?url=/library/en-


us/acdata/ac_8_md_06_2it2.asp
 En SQLTEAM podemos encontrar http://www.sqlteam.com/item.asp?ItemID=15583
 En CODEPROJECT tenemos http://www.codeproject.com/database/sqlservertransactions.asp
 Y en DOTNETJUNKIES http://dotnetjunkies.com/WebLog/darrell.norton/archive/2003/12/24/4921.aspx

You might also like