¿En qué casos se puede confirmar una transacción desde dentro del bloque CATCH cuando XACT_ABORT se establece en ON?

13

He estado leyendo MSDN sobre TRY...CATCHy XACT_STATE.

Tiene el siguiente ejemplo que se usa XACT_STATEen el CATCHbloque de una TRY…CATCHconstrucción para determinar si se debe confirmar o revertir una transacción:

USE AdventureWorks2012;
GO

-- SET XACT_ABORT ON will render the transaction uncommittable
-- when the constraint violation occurs.
SET XACT_ABORT ON;

BEGIN TRY
    BEGIN TRANSACTION;
        -- A FOREIGN KEY constraint exists on this table. This 
        -- statement will generate a constraint violation error.
        DELETE FROM Production.Product
            WHERE ProductID = 980;

    -- If the delete operation succeeds, commit the transaction. The CATCH
    -- block will not execute.
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    -- Test XACT_STATE for 0, 1, or -1.
    -- If 1, the transaction is committable.
    -- If -1, the transaction is uncommittable and should 
    --     be rolled back.
    -- XACT_STATE = 0 means there is no transaction and
    --     a commit or rollback operation would generate an error.

    -- Test whether the transaction is uncommittable.
    IF (XACT_STATE()) = -1
    BEGIN
        PRINT 'The transaction is in an uncommittable state.' +
              ' Rolling back transaction.'
        ROLLBACK TRANSACTION;
    END;

    -- Test whether the transaction is active and valid.
    IF (XACT_STATE()) = 1
    BEGIN
        PRINT 'The transaction is committable.' + 
              ' Committing transaction.'
        COMMIT TRANSACTION;   
    END;
END CATCH;
GO

Lo que no entiendo es, ¿por qué debería importarme y verificar qué XACT_STATEdevoluciones?

Tenga en cuenta que el indicador XACT_ABORTestá configurado ONen el ejemplo.

Si hay un error suficientemente grave dentro del TRYbloque, el control pasará a CATCH. Entonces, si estoy dentro de CATCH, sé que la transacción ha tenido un problema y realmente lo único sensato en este caso es revertirla, ¿no?

Pero, este ejemplo de MSDN implica que puede haber casos en los que se pasa el control CATCHy aún tiene sentido confirmar la transacción. ¿Podría alguien proporcionar algún ejemplo práctico cuando puede suceder, cuando tiene sentido?

No veo en qué casos se puede pasar el control al interior CATCHcon una transacción que se puede confirmar cuando XACT_ABORTse establece enON .

El artículo de MSDN sobre SET XACT_ABORTtiene un ejemplo cuando algunas declaraciones dentro de una transacción se ejecutan con éxito y otras fallan cuando XACT_ABORTse establece en OFF, lo entiendo. Pero conSET XACT_ABORT ON ¿cómo puede suceder que XACT_STATE()devuelva 1 dentro del CATCHbloque?

Inicialmente, habría escrito este código así:

USE AdventureWorks2012;
GO

-- SET XACT_ABORT ON will render the transaction uncommittable
-- when the constraint violation occurs.
SET XACT_ABORT ON;

BEGIN TRY
    BEGIN TRANSACTION;
        -- A FOREIGN KEY constraint exists on this table. This 
        -- statement will generate a constraint violation error.
        DELETE FROM Production.Product
            WHERE ProductID = 980;

    -- If the delete operation succeeds, commit the transaction. The CATCH
    -- block will not execute.
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    -- Some severe problem with the transaction
    PRINT 'Rolling back transaction.';
    ROLLBACK TRANSACTION;
END CATCH;
GO

Teniendo en cuenta una respuesta de Max Vernon, escribiría el código de esta manera. Mostró que tiene sentido verificar si hay una transacción activa antes de intentarlo ROLLBACK. Aún así, con SET XACT_ABORT ONel CATCHbloque puede haber una transacción condenada o ninguna transacción en absoluto. Entonces, en cualquier caso, no hay nada que hacer COMMIT. ¿Me equivoco?

USE AdventureWorks2012;
GO

-- SET XACT_ABORT ON will render the transaction uncommittable
-- when the constraint violation occurs.
SET XACT_ABORT ON;

BEGIN TRY
    BEGIN TRANSACTION;
        -- A FOREIGN KEY constraint exists on this table. This 
        -- statement will generate a constraint violation error.
        DELETE FROM Production.Product
            WHERE ProductID = 980;

    -- If the delete operation succeeds, commit the transaction. The CATCH
    -- block will not execute.
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    -- Some severe problem with the transaction
    IF (XACT_STATE()) <> 0
    BEGIN
        -- There is still an active transaction that should be rolled back
        PRINT 'Rolling back transaction.';
        ROLLBACK TRANSACTION;
    END;

END CATCH;
GO
Vladimir Baranov
fuente

Respuestas:

8

Resulta que la transacción no se puede confirmar desde el interior del CATCHbloque si XACT_ABORTse establece enON .

El ejemplo de MSDN es algo engañoso, porque la verificación implica que XACT_STATEpuede devolver 1 en algunos casos y que COMMITla transacción puede ser posible .

IF (XACT_STATE()) = 1
BEGIN
    PRINT 'The transaction is committable.' + 
          ' Committing transaction.'
    COMMIT TRANSACTION;   
END;

No es cierto, XACT_STATEnunca devolverá 1 dentro del CATCHbloque si XACT_ABORTse establece en ON.

Parece que el código de muestra de MSDN estaba destinado principalmente a ilustrar el uso de la XACT_STATE()función independientemente de la XACT_ABORTconfiguración. El código de muestra parece lo suficientemente genérico como para funcionar tanto con XACT_ABORTset ONcomo con OFF. Es solo que con XACT_ABORT = ONel cheque se IF (XACT_STATE()) = 1vuelve innecesario.


Hay un muy buen conjunto detallado de artículos sobre Error y manejo de transacciones en SQL Server por Erland Sommarskog. En la Parte 2 - Clasificación de errores , presenta una tabla completa que reúne todas las clases de errores y cómo SQL Server los maneja y cómo TRY ... CATCHy XACT_ABORTcambia el comportamiento.

+-----------------------------+---------------------------++------------------------------+
|                             |     Without TRY-CATCH     ||        With TRY-CATCH        |
+-----------------------------+-------+-------+-----+-----++------------------+-----+-----+
|              SET XACT_ABORT |  OFF  |  ON   | OFF | ON  ||    ON or OFF     | OFF | ON  |
+-----------------------------+-------+-------+-----+-----++------------------+-----+-----+
| Class Name                  |    Aborts     |   Rolls   ||    Catchable     |   Dooms   |
|                             |               |   Back    ||                  |transaction|
+-----------------------------+-------+-------+-----+-----++------------------+-----+-----+
| Fatal errors                |  Connection   |    Yes    ||       No         |    n/a    |
| Batch-aborting              |     Batch     |    Yes    ||       Yes        |    Yes    |
| Batch-only aborting         |     Batch     | No  | Yes ||       Yes        | No  | Yes |
| Statement-terminating       | Stmnt | Batch | No  | Yes ||       Yes        | No  | Yes |
| Terminates nothing at all   |    Nothing    |    No     ||       Yes        | No  | Yes |
| Compilation: syntax errors  |  (Statement)  |    No     ||       Yes        | No  | Yes |
| Compilation: binding errors | Scope | Batch | No  | Yes || Outer scope only | No  | Yes |
| Compilation: optimisation   |     Batch     |    Yes    || Outer scope only |    Yes    |
| Attention signal            |     Batch     | No  | Yes ||       No         |    n/a    |
| Informational/warning msgs  |    Nothing    |    No     ||       No         |    n/a    |
| Uncatchable errors          |    Varying    |  Varying  ||       No         |    n/a    |
+-----------------------------+-------+-------+-----+-----++------------------+-----+-----+

La última columna de la tabla responde a la pregunta. Con TRY-CATCHy conXACT_ABORT ON la transacción está condenada en todos los casos posibles.

Una nota fuera del alcance de la pregunta. Como dice Erland, esta coherencia es una de las razones para configurar XACT_ABORTa ON:

Ya he dado la recomendación de que sus procedimientos almacenados deben incluir el comando SET XACT_ABORT, NOCOUNT ON. Si observa la tabla anterior, verá que, XACT_ABORTen efecto, hay un mayor nivel de consistencia. Por ejemplo, la transacción siempre está condenada. En lo que sigue, voy a mostrar muchos ejemplos en los que me puse XACT_ABORTa OFF, de modo que se puede obtener una comprensión de por qué se debe evitar esta configuración predeterminada.

Vladimir Baranov
fuente
7

Yo abordaría esto de manera diferente. XACT_ABORT_ONes un mazo, puede utilizar un enfoque más refinado, consulte Manejo de excepciones y transacciones anidadas :

create procedure [usp_my_procedure_name]
as
begin
    set nocount on;
    declare @trancount int;
    set @trancount = @@trancount;
    begin try
        if @trancount = 0
            begin transaction
        else
            save transaction usp_my_procedure_name;

        -- Do the actual work here

lbexit:
        if @trancount = 0   
            commit;
    end try
    begin catch
        declare @error int, @message varchar(4000), @xstate int;
        select @error = ERROR_NUMBER(), @message = ERROR_MESSAGE(), @xstate = XACT_STATE();
        if @xstate = -1
            rollback;
        if @xstate = 1 and @trancount = 0
            rollback
        if @xstate = 1 and @trancount > 0
            rollback transaction usp_my_procedure_name;

        raiserror ('usp_my_procedure_name: %d: %s', 16, 1, @error, @message) ;
    end catch   
end
go

Este enfoque revertirá, cuando sea posible, solo el trabajo realizado dentro del bloque TRY y restaurará el estado al estado antes de ingresar al bloque TRY. De esta forma, puede realizar un procesamiento complejo, como iterar un cursor, sin perder todo el trabajo en caso de error. El único inconveniente es que, al usar los puntos de guardado de transacciones, no puede usar nada que sea incompatible con los puntos de guardado, como las transacciones distribuidas.

Remus Rusanu
fuente
Agradezco su respuesta, pero la cuestión no es realmente si debemos fijar XACT_ABORTa ONo OFF.
Vladimir Baranov
7

TL; DR / Resumen ejecutivo: Con respecto a esta parte de la pregunta:

No veo en qué casos se puede pasar el control dentro CATCHcon una transacción que se puede confirmar cuando XACT_ABORTse establece enON .

He hecho bastantes pruebas sobre esto ahora y no puedo encontrar ningún caso en el que XACT_STATE()regrese 1dentro de un CATCHbloque cuándo @@TRANCOUNT > 0 y la propiedad de sesión de XACT_ABORTes ON. Y, de hecho, según la página actual de MSDN para SET XACT_ABORT :

Cuando SET XACT_ABORT está activado, si una instrucción Transact-SQL genera un error de tiempo de ejecución, la transacción completa se termina y se revierte.

Esa declaración parece estar de acuerdo con su especulación y mis hallazgos.

El artículo sobre MSDN SET XACT_ABORTtiene un ejemplo cuando algunas declaraciones dentro de una transacción se ejecutan con éxito y otras fallan cuando XACT_ABORTse estableceOFF

Es cierto, pero las declaraciones en ese ejemplo no están dentro de un TRYbloque. Esas mismas declaraciones dentro de un TRYbloque serían aún impiden la ejecución de las manifestaciones después de la que ha provocado el error, pero suponiendo que XACT_ABORTes OFF, cuando se pasa el control al CATCHbloque de la transacción aún está físicamente válida en el que todos los cambios anteriores sucedió sin error y pueden comprometerse, si ese es el deseo, o pueden revertirse. Por otro lado, si XACT_ABORTes ONasí, cualquier cambio anterior se revierte automáticamente, y luego se le da la opción de: a) emitir unROLLBACKlo cual es principalmente una aceptación de la situación, ya que la transacción ya se ha revertido menos restablecer @@TRANCOUNTa 0ob) obtener un error. No hay mucha elección, ¿verdad?

Un detalle posiblemente importante para este rompecabezas que no es evidente en esa documentación SET XACT_ABORTes que esta propiedad de sesión, e incluso ese código de ejemplo, ha existido desde SQL Server 2000 (la documentación es casi idéntica entre las versiones), anterior a la TRY...CATCHconstrucción que fue introducido en SQL Server 2005. Al volver a ver esa documentación y ver el ejemplo ( sin el TRY...CATCH), el uso XACT_ABORT ONprovoca una reversión inmediata de la transacción: no hay estado de transacción de "no confirmable" (tenga en cuenta que no hay mención en todo un estado de transacción "no confirmable" en esa SET XACT_ABORTdocumentación).

Creo que es razonable concluir que:

  1. La introducción de la TRY...CATCHconstrucción en SQL Server 2005 creó la necesidad de un nuevo estado de transacción (es decir, "no confirmable") y la XACT_STATE()función para obtener esa información.
  2. verificar XACT_STATE()un CATCHbloque realmente solo tiene sentido si se cumple lo siguiente:
    1. XACT_ABORTes OFF(de lo contrario XACT_STATE(), siempre debe volver -1y @@TRANCOUNTsería todo lo que necesita)
    2. Tiene lógica en el CATCHbloque, o en algún lugar de la cadena si las llamadas están anidadas, eso hace un cambio (una COMMITo incluso cualquier instrucción DML, DDL, etc.) en lugar de hacer una ROLLBACK. (este es un caso de uso muy atípico) ** consulte la nota en la parte inferior, en la sección ACTUALIZACIÓN 3, con respecto a una recomendación no oficial de Microsoft de verificar siempre en XACT_STATE()lugar de @@TRANCOUNT, y por qué las pruebas muestran que su razonamiento no funciona.
  3. La introducción de la TRY...CATCHconstrucción en SQL Server 2005, en su mayor parte, ha obsoleto la XACT_ABORT ONpropiedad de la sesión, ya que proporciona un mayor grado de control sobre la transacción (al menos tiene la opción COMMIT, siempre que XACT_STATE()no regrese -1).
    Otra forma de ver esto es, antes de SQL Server 2005 , XACT_ABORT ONproporcionar una forma fácil y confiable de detener el procesamiento cuando se produjo un error, en comparación con la verificación @@ERRORdespués de cada declaración.
  4. El código de ejemplo de documentación para XACT_STATE()es erróneo o, en el mejor de los casos, engañoso, ya que muestra la comprobación de XACT_STATE() = 1cuándo XACT_ABORTes ON.

La parte larga ;-)

Sí, ese código de ejemplo en MSDN es un poco confuso (vea también: @@ TRANCOUNT (Rollback) vs. XACT_STATE ) ;-). Y, siento que es engañoso porque muestra algo que no tiene sentido (por la razón por la que está preguntando: ¿puede incluso tener una transacción "comprometible" en el CATCHbloque cuando lo XACT_ABORTes ON), o incluso si es posible? todavía se enfoca en una posibilidad técnica que pocos querrán o necesitarán, e ignora la razón por la cual es más probable que la necesite.

Si hay un error suficientemente grave dentro del bloque TRY, el control pasará a CATCH. Entonces, si estoy dentro de CATCH, sé que la transacción ha tenido un problema y realmente lo único sensato en este caso es revertirla, ¿no?

Creo que ayudaría si nos aseguramos de estar en la misma página con respecto a lo que se entiende por ciertas palabras y conceptos:

  • "error suficientemente grave": para ser claros, PRUEBE ... CATCH atrapará la mayoría de los errores. La lista de lo que no se detectará aparece en esa página de MSDN vinculada, en la sección "Errores no afectados por una construcción TRY ... CATCH".

  • "si estoy dentro de CATCH, sé que la transacción ha tenido un problema" ( se agrega em phase ): si por "transacción" se refiere a la unidad lógica de trabajo determinada por usted al agrupar las declaraciones en una transacción explícita, entonces más probable es que sí. Creo que la mayoría de nosotros, la gente de DB, tenderíamos a estar de acuerdo en que deshacernos es "la única cosa sensata que hacer", ya que probablemente tengamos una visión similar de cómo y por qué usamos transacciones explícitas y concebimos qué pasos deberían formar una unidad atómica. de trabajo.

    Pero, si te refieres a las unidades de trabajo reales que se están agrupando en la transacción explícita, entonces no, no sabes que la transacción en sí ha tenido un problema. Solo sabe que una instrucción que se ejecuta dentro de la transacción definida explícitamente ha provocado un error. Pero podría no ser una declaración DML o DDL. E incluso si se tratara de una declaración DML, la transacción en sí podría ser comprometible.

Teniendo en cuenta los dos puntos mencionados anteriormente, probablemente deberíamos hacer una distinción entre las transacciones que "no puede" comprometer y las que "no desea" comprometer.

Cuando XACT_STATE()devuelve un 1, eso significa que la transacción es "comprometible", que puede elegir entre COMMITo ROLLBACK. Es posible que no desee comprometerse, pero si por alguna razón difícil de encontrar con un ejemplo, por alguna razón, quisiera, al menos podría hacerlo porque algunas partes de la Transacción se completaron con éxito.

Pero cuando XACT_STATE()devuelve un -1, entonces realmente necesita hacerlo ROLLBACKporque una parte de la Transacción entró en mal estado. Ahora, estoy de acuerdo en que si el control se ha pasado al bloque CATCH, entonces tiene suficiente sentido simplemente verificarlo @@TRANCOUNT, porque incluso si pudiera confirmar la Transacción, ¿por qué querría hacerlo?

Pero si observa en la parte superior del ejemplo, la configuración de las XACT_ABORT ONcosas cambia un poco. Puede tener un error regular, después de hacerlo BEGIN TRANpasará el control al bloque CATCH cuando XACT_ABORTsea OFFy XACT_STATE () volverá 1. PERO, si XACT_ABORT es ON, entonces la transacción es "abortada" (es decir, invalidada) por cualquier error y luego XACT_STATE()regresará -1. En este caso, parece inútil verificar XACT_STATE()dentro del CATCHbloque, ya que siempre parece devolver un -1cuándo XACT_ABORTes ON.

Entonces, ¿ XACT_STATE()para qué sirve ? Algunas pistas son:

  • La página de MSDN para TRY...CATCH, en la sección "Transacciones no comprometidas y XACT_STATE", dice:

    Un error que normalmente finaliza una transacción fuera de un bloque TRY hace que una transacción entre en un estado no confirmable cuando el error ocurre dentro de un bloque TRY.

  • La página de MSDN para SET XACT_ABORT , en la sección "Comentarios", dice:

    Cuando SET XACT_ABORT está desactivado, en algunos casos solo se revierte la instrucción Transact-SQL que provocó el error y la transacción continúa procesándose.

    y:

    XACT_ABORT debe estar ACTIVADO para las declaraciones de modificación de datos en una transacción implícita o explícita contra la mayoría de los proveedores de OLE DB, incluido SQL Server.

  • La página de MSDN para COMENZAR TRANSACCIÓN , en la sección "Comentarios", dice:

    La transacción local iniciada por la instrucción BEGIN TRANSACTION se escala a una transacción distribuida si se realizan las siguientes acciones antes de que la declaración se confirme o se revierta:

    • Se ejecuta una instrucción INSERT, DELETE o UPDATE que hace referencia a una tabla remota en un servidor vinculado. La instrucción INSERT, UPDATE o DELETE falla si el proveedor OLE DB utilizado para acceder al servidor vinculado no es compatible con la interfaz ITransactionJoin.

El uso más aplicable parece estar dentro del contexto de las declaraciones DML del servidor vinculado. Y creo que me encontré con esto hace años. No recuerdo todos los detalles, pero tenía algo que ver con que el servidor remoto no estaba disponible, y por alguna razón, ese error no quedó atrapado dentro del bloque TRY y nunca se envió a CATCH, y así fue. un COMPROMISO cuando no debería haberlo hecho. Por supuesto, eso podría haber sido un problema de no haberse XACT_ABORTconfigurado en ONlugar de no verificar XACT_STATE(), o posiblemente ambos. Y recuerdo haber leído algo que decía que si usa Servidores Vinculados y / o Transacciones Distribuidas, entonces necesitaba usar XACT_ABORT ONy / o XACT_STATE(), pero parece que no puedo encontrar ese documento ahora. Si lo encuentro, actualizaré esto con el enlace.

Aún así, he intentado varias cosas y no puedo encontrar un escenario que tenga XACT_ABORT ONy pase el control al CATCHbloque con XACT_STATE()informes 1.

Pruebe estos ejemplos para ver el efecto de XACT_ABORTen el valor de XACT_STATE():

SET XACT_ABORT OFF;

BEGIN TRY
    BEGIN TRAN;

    SELECT 1/0 AS [DivideByZero]; -- error, yo!

    COMMIT TRAN;
END TRY
BEGIN CATCH
    SELECT @@TRANCOUNT AS [@@TRANCOUNT],
            XACT_STATE() AS [XactState],
            ERROR_MESSAGE() AS [ErrorMessage]

    IF (@@TRANCOUNT > 0)
    BEGIN
        ROLLBACK;
    END;
END CATCH;

GO ------------------------------------------------

SET XACT_ABORT ON;

BEGIN TRY
    BEGIN TRAN;

    SELECT 1/0 AS [DivideByZero]; -- error, yo!

    COMMIT TRAN;
END TRY
BEGIN CATCH
    SELECT @@TRANCOUNT AS [@@TRANCOUNT],
            XACT_STATE() AS [XactState],
            ERROR_MESSAGE() AS [ErrorMessage]

    IF (@@TRANCOUNT > 0)
    BEGIN
        ROLLBACK;
    END;
END CATCH;

GO ------------------------------------------------

SET XACT_ABORT ON;

BEGIN TRY
    SELECT 1/0 AS [DivideByZero]; -- error, yo!
END TRY
BEGIN CATCH
    SELECT @@TRANCOUNT AS [@@TRANCOUNT],
            XACT_STATE() AS [XactState],
            ERROR_MESSAGE() AS [ErrorMessage]
END CATCH;

ACTUALIZAR

Si bien no es parte de la Pregunta original, en base a estos comentarios en esta Respuesta:

He estado leyendo los artículos de Erland sobre Manejo de errores y transacciones donde dice que XACT_ABORTes OFFpor defecto por razones heredadas y que normalmente deberíamos configurarlo ON.
...
"... si sigue la recomendación y ejecuta SET XACT_ABORT ON, la transacción siempre estará condenada".

Antes de usar en XACT_ABORT ONtodas partes, me preguntaría: ¿qué se gana exactamente aquí? No he encontrado que sea necesario hacerlo y, en general, recomiendo que lo use solo cuando sea necesario. Si quiere o no ROLLBACKpuede manejarse con la suficiente facilidad usando la plantilla que se muestra en la respuesta de @ Remus , o la que he estado usando durante años que es esencialmente lo mismo pero sin el Punto de guardado, como se muestra en esta respuesta (que maneja llamadas anidadas):

¿Estamos obligados a manejar la transacción en el código C #, así como en el procedimiento almacenado?


ACTUALIZACIÓN 2

Hice un poco más de pruebas, esta vez al crear una pequeña aplicación de consola .NET, crear una transacción en la capa de la aplicación, antes de ejecutar cualquier SqlCommandobjeto (es decir, a través de using (SqlTransaction _Tran = _Connection.BeginTransaction()) { ...), así como usar un error de interrupción por lotes en lugar de solo una declaración -aborting error, y descubrió que:

  1. Una transacción "no conmutable" es aquella que, en su mayor parte, ya se ha revertido (los cambios se han deshecho), pero @@TRANCOUNT aún es> 0.
  2. Cuando tiene una Transacción "no conmutable", no puede emitir una, COMMITya que generará un error que dice que la Transacción es "no confirmable". Tampoco puede ignorarlo / no hacer nada, ya que se generará un error cuando el lote termine indicando que el lote se completó con una transacción persistente y no confirmable y se revertirá (por lo tanto, um, si se revierte automáticamente de todos modos, ¿Por qué molestarse en lanzar el error?). Por lo tanto, debe emitir un mensaje explícito ROLLBACK, tal vez no en el CATCHbloque inmediato , sino antes de que finalice el lote.
  3. En una TRY...CATCHconstrucción, cuando XACT_ABORTes así OFF, los errores que terminarían la transacción automáticamente si hubieran ocurrido fuera de un TRYbloque, como los errores de aborto por lotes, deshacerán el trabajo pero no terminarán la transacción, dejándola como "no transmisible". Emitir a ROLLBACKes más una formalidad necesaria para cerrar la transacción, pero el trabajo ya se ha revertido.
  4. Cuando XACT_ABORTes así ON, la mayoría de los errores actúan como aborto por lotes y, por lo tanto, se comportan como se describe en el punto de la viñeta directamente arriba (# 3).
  5. XACT_STATE(), al menos en un CATCHbloque, mostrará un -1error de aborto por lotes si hubo una transacción activa en el momento del error.
  6. XACT_STATE()a veces regresa 1incluso cuando no hay una transacción activa. Si @@SPID(entre otros) está en la SELECTlista junto con XACT_STATE(), entonces XACT_STATE()devolverá 1 cuando no haya una Transacción activa. Este comportamiento comenzó en SQL Server 2012 y existe en 2014, pero no lo he probado en 2016.

Con los puntos anteriores en mente:

  • Teniendo en cuenta los puntos 4 y 5, dado que la mayoría (¿o todos los errores?) Harán que una transacción sea "no conmutable", parece completamente inútil verificar XACT_STATE()en el CATCHbloque cuándo XACT_ABORTes, ONya que el valor devuelto siempre será-1 .
  • Comprobación XACT_STATE()en el CATCHbloque cuando XACT_ABORTse OFFtiene más sentido debido a que el valor de retorno tendrá al menos alguna variación, ya que volverá 1a errores de los estados-abortar. Sin embargo, si codifica como la mayoría de nosotros, entonces esta distinción no tiene sentido ya que llamará de ROLLBACKtodos modos simplemente por el hecho de que ocurrió un error.
  • Si encuentra una situación que hace orden de emitir una COMMITen el CATCHbloque, a continuación, comprobar el valor de XACT_STATE(), y asegúrese de SET XACT_ABORT OFF;.
  • XACT_ABORT ONparece ofrecer poco o ningún beneficio sobre la TRY...CATCHconstrucción.
  • No puedo encontrar ningún escenario en el que la verificación XACT_STATE()proporcione un beneficio significativo sobre la simple verificación @@TRANCOUNT.
  • Tampoco puedo encontrar ningún escenario donde XACT_STATE()regrese 1en un CATCHbloque cuando XACT_ABORTesON . Creo que es un error de documentación.
  • Sí, puede revertir una transacción que no comenzó explícitamente. Y en el contexto del uso XACT_ABORT ON, es un punto discutible ya que un error que ocurre en un TRYbloque revertirá automáticamente los cambios.
  • El TRY...CATCHconstructo tiene el beneficio XACT_ABORT ONde no cancelar automáticamente la Transacción completa y, por lo tanto, permitir que la Transacción (siempre que se XACT_STATE()devuelva 1) se confirme (incluso si este es un caso límite).

Ejemplo de XACT_STATE()regresar -1cuando XACT_ABORTes OFF:

SET XACT_ABORT OFF;

BEGIN TRY
    BEGIN TRAN;

    SELECT CONVERT(INT, 'g') AS [ConversionError];

    COMMIT TRAN;
END TRY
BEGIN CATCH
    DECLARE @State INT;
    SET @State = XACT_STATE();
    SELECT @@TRANCOUNT AS [@@TRANCOUNT],
            @State AS [XactState],
            ERROR_MESSAGE() AS [ErrorMessage];

    IF (@@TRANCOUNT > 0)
    BEGIN
        SELECT 'Rollin back...' AS [Transaction];
        ROLLBACK;
    END;
END CATCH;

ACTUALIZACIÓN 3

Relacionado con el artículo # 6 en la sección ACTUALIZACIÓN 2 (es decir, posible valor incorrecto devuelto por XACT_STATE()cuando no hay una Transacción activa):

  • El comportamiento extraño / erróneo comenzó en SQL Server 2012 (hasta ahora probado contra 2012 SP2 y 2014 SP1)
  • En las versiones 2005, 2008 y 2008 R2 de SQL Server, XACT_STATE()no se informaban los valores esperados cuando se usaban en Disparadores o INSERT...EXECescenarios: xact_state () no se puede usar de manera confiable para determinar si una transacción está condenada . Sin embargo, en estos 3 versiones (sólo probado en 2008 R2), XACT_STATE()no no informar incorrectamente 1cuando se utiliza en una SELECTcon @@SPID.
  • Hay un error de conexión presentado contra el comportamiento mencionado aquí, pero está cerrado como "Por diseño": XACT_STATE () puede devolver un estado de transacción incorrecto en SQL 2012 . Sin embargo, la prueba se realizó al seleccionar de un DMV y se concluyó que hacerlo naturalmente tendría una transacción generada por el sistema, al menos para algunos DMV. También se indicó en la respuesta final de MS que:

    Tenga en cuenta que una instrucción IF, y también un SELECT sin FROM, no inician una transacción.
    por ejemplo, si ejecuta SELECT XACT_STATE () si no tiene una transacción previamente existente, devolverá 0.

    Esas declaraciones son incorrectas dado el siguiente ejemplo:

    SELECT @@TRANCOUNT AS [TRANCOUNT], XACT_STATE() AS [XACT_STATE], @@SPID AS [SPID];
    GO
    DECLARE @SPID INT;
    SET @SPID = @@SPID;
    SELECT @@TRANCOUNT AS [TRANCOUNT], XACT_STATE() AS [XACT_STATE], @SPID AS [SPID];
    GO
  • Por lo tanto, el nuevo error de conexión:
    XACT_STATE () devuelve 1 cuando se usa en SELECT con algunas variables del sistema pero sin la cláusula FROM

TENGA EN CUENTA que en el "XACT_STATE () puede devolver un estado de transacción incorrecto en SQL 2012" Conecte el elemento vinculado directamente arriba, Microsoft (bueno, un representante de) declara:

@@ trancount devuelve el número de declaraciones BEGIN TRAN. Por lo tanto, no es un indicador confiable de si hay una transacción activa. XACT_STATE () también devuelve 1 si hay una transacción de confirmación automática activa y, por lo tanto, es un indicador más confiable de si hay una transacción activa.

Sin embargo, no puedo encontrar ninguna razón para no confiar @@TRANCOUNT. La siguiente prueba muestra que @@TRANCOUNTefectivamente regresa 1en una transacción de confirmación automática:

--- begin setup
GO
CREATE PROCEDURE #TransactionInfo AS
SET NOCOUNT ON;
SELECT @@TRANCOUNT AS [TranCount],
       XACT_STATE() AS [XactState];
GO
--- end setup

DECLARE @Test TABLE (TranCount INT, XactState INT);

SELECT * FROM @Test; -- no rows

EXEC #TransactionInfo; -- 0 for both fields

INSERT INTO @Test (TranCount, XactState)
    EXEC #TransactionInfo;

SELECT * FROM @Test; -- 1 row; 1 for both fields

También probé en una tabla real con un Trigger y @@TRANCOUNTdentro del Trigger hice un informe preciso 1a pesar de que no se había iniciado ninguna Transacción explícita.

Solomon Rutzky
fuente
4

La programación defensiva requiere que escriba código que maneje tantos estados conocidos como sea posible, reduciendo así la posibilidad de errores.

Comprobar XACT_STATE () para determinar si se puede ejecutar una reversión es simplemente una buena práctica. Intentar ciegamente una reversión significa que, sin darse cuenta, puede causar un error dentro de su TRY ... CATCH.

Una forma en que una reversión podría fallar dentro de TRY ... CATCH sería si no iniciara una transacción explícitamente. Copiar y pegar bloques de código podría causar esto fácilmente.

Max Vernon
fuente
Gracias por tu respuesta. Simplemente no podía pensar en un caso en el que lo simple ROLLBACKno funcionara dentro CATCHy usted dio un buen ejemplo. Supongo que también puede volverse desordenado rápidamente si TRY ... CATCH ... ROLLBACKse involucran transacciones anidadas y procedimientos almacenados anidados con los suyos .
Vladimir Baranov
Aún así, le agradecería si pudiera extender su respuesta con respecto a la segunda parte: IF (XACT_STATE()) = 1 COMMIT TRANSACTION; ¿cómo podemos terminar dentro del CATCHbloque con una transacción comprometible? No me atrevería a cometer algo de basura (posible) desde adentro CATCH. Mi razonamiento es: si estamos dentro de CATCHalgo, algo salió mal, no puedo confiar en el estado de la base de datos, así que será mejor ROLLBACKlo que tengamos.
Vladimir Baranov