GUARDAR TRANSACCIÓN vs BEGIN TRANSACTION (SQL Server) cómo anidar bien las transacciones
-
14-12-2019 - |
Pregunta
Tengo un procedimiento almacenado que necesita establecer un punto de guardado para que pueda, bajo ciertas circunstancias, deshacer todo lo que hizo y devolver un código de error a la persona que llama, o aceptarlo/confirmarlo y devolver el éxito a la persona que llama.Pero necesito que funcione independientemente de que la persona que llama ya haya iniciado una transacción o no.El documento es extremadamente confuso sobre este tema.Esto es lo que creo que funcionará, pero no estoy seguro de todas las ramificaciones.
La cosa es - esto Stored Procedure (SP)
es llamado por otros.Entonces no sé si han iniciado una transacción o no...Incluso si pido a los usuarios que inicien una transacción para usar mi SP, todavía tengo preguntas sobre el uso adecuado de Save Points
...
Mi SP probará si hay una transacción en progreso y, si no, comenzará una con BEGIN TRANSACTION
.Si una transacción ya está en progreso, se creará un punto de guardado con SAVE TRANSACTION MySavePointName
, y guarde el hecho de que esto es lo que hice.
Entonces, si tengo que revertir mis cambios, si hice un BEGIN TRANSACTION
antes, entonces lo haré ROLLBACK TRANSACTION
.Si hice el punto de guardado, entonces lo haré ROLLBACK TRANSACTION MySavePointName
.Este escenario parece funcionar muy bien.
Aquí es donde me confundo un poco: si quiero conservar el trabajo que he realizado, si comencé una transacción, la ejecutaré. COMMIT TRANSACTION
.¿Pero si creé un punto de guardado?Lo intenté COMMIT TRANSACTION MySavePointName
, pero luego la persona que llama intenta confirmar su transacción y obtiene un error:
La solicitud COMMIT TRANSACTION no tiene BEGIN TRANSACTION correspondiente.
Entonces me pregunto: se puede revertir un punto de guardado (eso funciona: ROLLBACK TRANSACTION MySavePointName
NO revertirá la transacción de la persona que llama).¿Pero quizás nunca sea necesario "cometerlo"?¿Simplemente permanece allí, en caso de que necesite volver a él, pero desaparece una vez que se confirma (o revierte) la transacción original?
Si existe una forma "mejor" de "anidar" una transacción, arroje algo de luz también.No he descubierto cómo anidar BEGIN TRANSACTION
pero solo revertir o confirmar mi transacción interna.Parece ROLLBACK
siempre volverá a la transacción superior, mientras que COMMIT
simplemente disminuye @@trancount
.
Solución
Creo que ya he descubierto todo esto, así que responderé mi propia pregunta...
Incluso escribí mis hallazgos en un blog si quieres más detalles en http://geekswithblogs.net/bbiales/archive/2012/03/15/how-to-nest-transactions-nicely---quotbegin-transactionquot-vs-quotsave.aspx
Entonces mi SP comienza con algo como esto, para iniciar una nueva transacción si no hay ninguna, pero usar un Punto de guardado si ya hay una en progreso:
DECLARE @startingTranCount int
SET @startingTranCount = @@TRANCOUNT
IF @startingTranCount > 0
SAVE TRANSACTION mySavePointName
ELSE
BEGIN TRANSACTION
-- …
Luego, cuando esté listo para confirmar los cambios, solo necesita confirmar si iniciamos la transacción nosotros mismos:
IF @startingTranCount = 0
COMMIT TRANSACTION
Y finalmente, para revertir sólo los cambios hasta el momento:
-- Roll back changes...
IF @startingTranCount > 0
ROLLBACK TRANSACTION MySavePointName
ELSE
ROLLBACK TRANSACTION
Otros consejos
Extensión La respuesta de Brian B..
Esto garantiza que el nombre del punto de guardado sea único y utilice las nuevas funciones TRY/CATCH/THROW de SQL Server 2012.
DECLARE @mark CHAR(32) = replace(newid(), '-', '');
DECLARE @trans INT = @@TRANCOUNT;
IF @trans = 0
BEGIN TRANSACTION @mark;
ELSE
SAVE TRANSACTION @mark;
BEGIN TRY
-- do work here
IF @trans = 0
COMMIT TRANSACTION @mark;
END TRY
BEGIN CATCH
IF xact_state() = 1 OR (@trans = 0 AND xact_state() <> 0) ROLLBACK TRANSACTION @mark;
THROW;
END CATCH
He utilizado este tipo de administrador de transacciones en mis procedimientos almacenados:
CREATE PROCEDURE Ardi_Sample_Test
@InputCandidateID INT
AS
DECLARE @TranCounter INT;
SET @TranCounter = @@TRANCOUNT;
IF @TranCounter > 0
SAVE TRANSACTION ProcedureSave;
ELSE
BEGIN TRANSACTION;
BEGIN TRY
/*
<Your Code>
*/
IF @TranCounter = 0
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @TranCounter = 0
ROLLBACK TRANSACTION;
ELSE
IF XACT_STATE() <> -1
ROLLBACK TRANSACTION ProcedureSave;
DECLARE @ErrorMessage NVARCHAR(4000);
DECLARE @ErrorSeverity INT;
DECLARE @ErrorState INT;
SELECT @ErrorMessage = ERROR_MESSAGE();
SELECT @ErrorSeverity = ERROR_SEVERITY();
SELECT @ErrorState = ERROR_STATE();
RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState);
END CATCH
GO