2012-03-15 14 views
12

Tengo un procedimiento almacenado que necesita establecer un punto de guardado para que pueda, en ciertas circunstancias, deshacer todo lo que hizo y devolver un código de error a la persona que llama , o lo acepta/confirma y devuelve el éxito a la persona que llama. Pero necesito que funcione si la persona que llama ya inició 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.GUARDAR TRANSACCIÓN vs COMIENZO TRANSACCIÓN (Servidor SQL) cómo anidar transacciones muy bien

Lo que pasa es que este Stored Procedure (SP) es llamado por otros. Así que no sé si iniciaron una transacción o no ... Incluso si requiero que los usuarios inicien una transacción para usar mi SP, aún tengo dudas sobre el uso correcto de Save Points ...

Mi SP probará si una transacción está en curso, y si no, comience una con BEGIN TRANSACTION. Si una transacción ya está en progreso, creará un punto de guardado con SAVE TRANSACTION MySavePointName, y guarde el hecho de que esto es lo que hice.

Luego, si tengo que retrotraer 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 bien.

Aquí es donde me confundo un poco: si quiero mantener el trabajo que he hecho, si comencé una transacción ejecutaré COMMIT TRANSACTION. Pero si creé un punto de guardado? Probé COMMIT TRANSACTION MySavePointName, pero entonces la persona que llama intenta cometer su transacción y obtiene un error:

The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.

Así que me pregunto entonces - un punto de guardado se puede deshacer (que funciona: ROLLBACK TRANSACTION MySavePointName no va a hacer retroceder la persona que llama de transacción). Pero tal vez uno nunca necesita "comprometerlo"? Simplemente permanece allí, en caso de que necesite retroceder, pero desaparece una vez que la transacción original se ha comprometido (o revertido).

Si hay una "mejor" manera de "anidar" una transacción, por favor, arroje algo de luz también. No he descubierto cómo anidar con BEGIN TRANSACTION, pero solo retrotrajo o confirmo mi transacción interna. Parece que ROLLBACK siempre retrocederá a la transacción superior, mientras que COMMIT simplemente decrementa @@trancount.

+0

Su hallazgo podría valer la pena publicar como respuesta. –

+0

@Andriy Ok, eso es lo que hice, eliminé mi edición y la utilicé como respuesta. Gracias. –

Respuesta

19

creo que he descubierto todo esto ahora, así que voy a responder a mi propia pregunta ...

Incluso he blogged mis resultados si quieres más detalles en http://geekswithblogs.net/bbiales/archive/2012/03/15/how-to-nest-transactions-nicely---quotbegin-transactionquot-vs-quotsave.aspx

Así que mi SP comienza con algo como esto, para iniciar una nueva transacción si no hay ninguno, pero el uso de un punto de guardado, si uno ya está en curso:

DECLARE @startingTranCount int 
SET @startingTranCount = @@TRANCOUNT 

IF @startingTranCount > 0 
    SAVE TRANSACTION mySavePointName 
ELSE 
    BEGIN TRANSACTION 
-- … 

Luego, cuando esté listo para confirmar los cambios, sólo es necesario para cometer si comenzamos la transacción nosotros mismos:

IF @startingTranCount = 0 
    COMMIT TRANSACTION 

Y, por último, para hacer retroceder simplemente los cambios hasta el momento:

-- Roll back changes... 
IF @startingTranCount > 0 
    ROLLBACK TRANSACTION MySavePointName 
ELSE 
    ROLLBACK TRANSACTION 
+0

alguna razón para no iniciar una nueva transacción anidada después de 'guardar transacción'? – sotn

+0

A continuación se muestra un enlace a un artículo sobre transacciones de anidación. Si se confirma una transacción anidada, se reduce el recuento de transacciones anidado pero NO se compromete nada. Entonces, si la transacción externa está comprometida, todo es así, si se retrotrae, todo se retrotrae, incluido el "comprometido". Si la transacción anidada realiza una reversión, se reinicia TANTO y las transacciones externas. Si SOLO desea deshacer su propia transacción sin afectar las transacciones en las que ya se encuentra, utilice la técnica que se describe aquí. –

+0

Este es el artículo sobre transacciones anidadas: https://technet.microsoft.com/en-us/library/ms189336%28v=sql.105%29.aspx?f=255&MSPPError=-2147217396 –

10

Extendiendo Brian B's answer.

Esto asegura que el nombre del punto de guardado es único y utiliza las nuevas características 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 
+0

No había vuelto a esta publicación en un momento, pero amo la plantilla que puede reutilizar textualmente en la siguiente consulta. Buena mejora. –

+0

He visto sugerencias de MSDN para poner la declaración de transacción en el bloque try. ¿Haría eso en este patrón causar algunos problemas o es totalmente seguro hacerlo? –

+0

Estoy confundido acerca de la instrucción if en el bloque catch. Por qué deshacer la transacción si xact state = -1. ¿No es <> - 1? –

2

He utilizado este tipo de gestor 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 
Cuestiones relacionadas