В каких случаях транзакция может быть зафиксирована внутри блока CATCH, если для XACT_ABORT установлено значение ON?

13

Я читал MSDN о TRY...CATCHи XACT_STATE.

Он имеет следующий пример, который используется XACT_STATEв CATCHблоке TRY…CATCHконструкции, чтобы определить, следует ли зафиксировать или откатить транзакцию:

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

Что я не понимаю, так это то, почему я должен заботиться и проверять, что XACT_STATEвозвращается?

Обратите внимание, что флаг XACT_ABORTустановлен ONв примере.

Если внутри TRYблока будет достаточно серьезная ошибка , управление перейдет в CATCH. Итак, если я нахожусь внутри CATCH, я знаю, что у транзакции была проблема, и на самом деле единственная разумная вещь, которую нужно сделать в этом случае, это откатить ее, не так ли?

Но этот пример из MSDN подразумевает, что могут быть случаи, когда управление передается, CATCHи все же имеет смысл совершать транзакцию. Может ли кто-нибудь привести практический пример, когда это может произойти, когда это имеет смысл?

Я не вижу, в каких случаях управление может быть передано внутрь CATCHс помощью транзакции, которая может быть зафиксирована, когда XACT_ABORTзадано значениеON .

В статье MSDN SET XACT_ABORTесть пример, когда некоторые операторы внутри транзакции выполняются успешно, а некоторые - когда XACT_ABORTустановлено OFF, я понимаю это. Но SET XACT_ABORT ONкак это может произойти, что XACT_STATE()возвращает 1 внутри CATCHблока?

Первоначально я написал бы этот код следующим образом:

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

Учитывая ответ Макса Вернона, я бы написал такой код. Он показал, что имеет смысл проверить, есть ли активная транзакция, прежде чем пытаться ROLLBACK. Тем не менее, с SET XACT_ABORT ONв CATCHблоке может быть либо обречена сделка или нет сделки вообще. Так что в любом случае нечего COMMIT. Я ошибаюсь?

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
Владимир Баранов
источник

Ответы:

8

Оказывается, транзакция не может быть зафиксирована внутри CATCHблока, если XACT_ABORTона установлена ​​в ON.

Пример из MSDN несколько вводит в заблуждение, поскольку проверка подразумевает, что XACT_STATEв некоторых случаях может возвращать 1 и это может быть возможным для COMMITтранзакции.

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

Это не так, XACT_STATEникогда не вернет 1 внутри CATCHблока, если XACT_ABORTустановлено в ON.

Похоже, что пример кода MSDN предназначался, прежде всего, для иллюстрации использования XACT_STATE()функции независимо от XACT_ABORTнастроек. Пример кода выглядит достаточно универсальным для работы как с XACT_ABORTустановленным на, так ONи с OFF. Просто с XACT_ABORT = ONпроверкой это IF (XACT_STATE()) = 1становится ненужным.


Эрланд Соммарског (Erland Sommarskog ) предлагает очень хороший подробный набор статей об обработке ошибок и транзакций в SQL Server . В Части 2 - Классификация ошибок он представляет исчерпывающую таблицу, которая объединяет все классы ошибок и то, как они обрабатываются SQL Server, а также как TRY ... CATCHи как XACT_ABORTизменить поведение.

+-----------------------------+---------------------------++------------------------------+
|                             |     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    |
+-----------------------------+-------+-------+-----+-----++------------------+-----+-----+

Последний столбец в таблице отвечает на вопрос. С TRY-CATCHи с XACT_ABORT ONсделкой обречена во всех возможных случаях.

Одна заметка выходит за рамки вопроса. Как говорит Erland, эта последовательность является одной из причин , чтобы установить XACT_ABORTна ON:

Я уже дал рекомендацию, что ваши хранимые процедуры должны включать команду SET XACT_ABORT, NOCOUNT ON. Если вы посмотрите на приведенную выше таблицу, то увидите, что XACT_ABORTв действительности наблюдается более высокий уровень согласованности. Например, транзакция всегда обречена. В дальнейшем, я покажу много примеров , когда я ставлю XACT_ABORTна OFF, так что вы можете получить представление о том, почему вы должны избегать этого параметра по умолчанию.

Владимир Баранов
источник
7

Я бы подошел к этому по-другому. XACT_ABORT_ONэто кувалда, вы можете использовать более изощренный подход, см. Обработка исключений и вложенные транзакции :

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

Этот подход будет откатывать, когда это возможно, только работу, выполненную внутри блока TRY, и восстанавливать состояние до состояния перед входом в блок TRY. Таким образом, вы можете выполнять сложную обработку, такую ​​как итерация курсора, без потери всей работы в случае ошибки. Единственным недостатком является то, что при использовании точек сохранения транзакций вы не можете использовать что-либо несовместимое с точками сохранения, например распределенные транзакции.

Ремус Русану
источник
Я ценю ваш ответ, но вопрос в том , на самом деле не следует ли установить XACT_ABORTна ONили OFF.
Владимир Баранов
7

TL; DR / Резюме: Относительно этой части Вопроса:

Я не вижу, в каких случаях управление может быть передано внутрь CATCHс помощью транзакции, которая может быть зафиксирована, когда XACT_ABORTзадано значениеON .

Я провел немало тестов по этому вопросу сейчас и не могу найти случаев, когда XACT_STATE()возвращается 1внутри CATCHблока когда @@TRANCOUNT > 0 и свойство сеанса XACT_ABORTis ON. И действительно, в соответствии с текущей страницей MSDN для SET XACT_ABORT :

Когда SET XACT_ABORT установлен в ON, если инструкция Transact-SQL вызывает ошибку во время выполнения, вся транзакция завершается и откатывается.

Это утверждение, похоже, согласуется с вашими предположениями и моими выводами.

В статье MSDN о SET XACT_ABORTприведен пример, когда некоторые операторы внутри транзакции выполняются успешно, а некоторые - когда XACT_ABORTустановлено значениеOFF

Верно, но операторы в этом примере не находятся внутри TRYблока. Те же заявления в пределах TRYблока равно предотвратить выполнение любых заявлений после того, что произошла ошибка, но если предположить , что XACT_ABORTэто OFF, когда управление передается в CATCHблок транзакции остается физически действительным в том , что все предыдущие изменения случилось без ошибок и могут быть совершены, если это желание, или они могут быть отменены. С другой стороны, если XACT_ABORTэто так, ONто любые предыдущие изменения автоматически отменяются, и тогда вам предоставляется выбор: а) выполнитьROLLBACKкоторая в основном просто принятие ситуации , так как сделка была уже откат минус сброс @@TRANCOUNTк 0, или B) выдается сообщение об ошибке. Не так много выбора, не так ли?

Возможно, важная деталь этой загадки, которая не очевидна в этой документации, SET XACT_ABORTзаключается в том, что это свойство сеанса и даже этот пример кода существовали начиная с SQL Server 2000 (документация почти идентична между версиями), предшествуя TRY...CATCHконструкции, которая была введенный в SQL Server 2005. глядя на этой документации снова и посмотреть на примере ( безTRY...CATCH ), используя XACT_ABORT ONвызывает немедленный откат сделки: нет состояния транзакции из «нефиксируемого» (обратите внимание , что нет никакого упоминания в все «непередаваемое» состояние транзакции в этой SET XACT_ABORTдокументации).

Я думаю, что разумно сделать вывод, что:

  1. введение TRY...CATCHконструкции в SQL Server 2005 создало необходимость в новом состоянии транзакции (т.е. «uncommittable») и XACT_STATE()функции для получения этой информации.
  2. проверка XACT_STATE()в CATCHблоке действительно имеет смысл, только если выполняются оба следующих условия:
    1. XACT_ABORTесть OFF(иначе XACT_STATE()должен всегда возвращаться -1и @@TRANCOUNTбудет все, что вам нужно)
    2. У вас есть логика в CATCHблоке или где-то в цепочке, если вызовы вложены, что делает изменение (a COMMITили даже любой оператор DML, DDL и т. Д.) Вместо выполнения a ROLLBACK. (это очень нетипичный вариант использования) ** см. примечание внизу, в разделе ОБНОВЛЕНИЕ 3, относительно неофициальной рекомендации Microsoft всегда проверять XACT_STATE()вместо @@TRANCOUNT, и почему тестирование показывает, что их рассуждения не оправдываются.
  3. введение TRY...CATCHконструкции в SQL Server 2005, по большей части, устарело XACT_ABORT ONсвойство сеанса, поскольку оно обеспечивает большую степень контроля над транзакцией (по крайней мере, у вас есть опция при COMMITусловии, что XACT_STATE()она не возвращается -1).
    Другой способ смотреть на это, до SQL Server 2005 , при XACT_ABORT ONусловии , простой и надежный способ обработки стоп , когда произошла ошибка, по сравнению с проверкой @@ERRORпосле каждого оператора.
  4. Документации пример кода для XACT_STATE()ошибочна, или в лучшем случае вводит в заблуждение, в том , что он показывает проверка , XACT_STATE() = 1если XACT_ABORTесть ON.

Длинная часть ;-)

Да, этот пример кода в MSDN немного сбивает с толку (см. Также: @@ TRANCOUNT (откат) против XACT_STATE ) ;-). И я чувствую, что это вводит в заблуждение, потому что он либо показывает что-то, что не имеет смысла (по той причине, о которой вы спрашиваете: можете ли вы иметь транзакцию committable в CATCHблоке, когда XACT_ABORTесть ON), или даже если это возможно, это все еще сосредотачивается на технической возможности, которая когда-либо будет хотеться или нуждаться в немногих, и игнорирует причину, в которой это более вероятно потребуется

Если внутри блока TRY имеется достаточно серьезная ошибка, управление переходит в CATCH. Итак, если я нахожусь внутри CATCH, я знаю, что транзакция имела проблему, и в действительности единственное разумное, что нужно сделать в этом случае, это откатить ее, не так ли?

Я думаю, что было бы полезно, если бы мы убедились, что находимся на одной странице относительно того, что подразумевается под определенными словами и понятиями:

  • «Достаточно серьезная ошибка»: просто чтобы понять, TRY ... CATCH перехватит большинство ошибок. Список того, что не будет перехвачено, приведен на этой связанной странице MSDN в разделе «Ошибки, не затронутые конструкцией TRY… CATCH».

  • «если я нахожусь внутри CATCH, я знаю, что у транзакции возникла проблема» ( добавляется em phas ): если под «транзакцией» вы подразумеваете логическую единицу работы, определенную вами, сгруппировав операторы в явную транзакцию, то скорее всего да. Я думаю, что большинство из нас, работающих с БД, склонны согласиться с тем, что откат - это «единственно разумная вещь», поскольку мы, вероятно, придерживаемся аналогичного представления о том, как и почему мы используем явные транзакции, и понимаем, какие шаги должны составлять атомарную единицу. работы.

    Но если вы имеете в виду фактические единицы работы, которые группируются в явную транзакцию, то нет, вы не знаете, что в самой транзакции возникла проблема. Вы только знаете , что оператор выполняет в явном виде определенной сделки появлялось сообщение об ошибке. Но это может быть не оператор DML или DDL. И даже если это был оператор DML, сама Транзакция все еще может быть коммитируемой.

Учитывая два вышеизложенных момента, мы, вероятно, должны провести различие между транзакциями, которые вы «не можете» совершить, и транзакциями, которые вы «не хотите» совершать.

Когда XACT_STATE()возвращается a 1, это означает, что транзакция является "коммитируемой", что у вас есть выбор между COMMITили ROLLBACK. Возможно, вы не захотите его зафиксировать, но если по какой-то причине, которую трудно было даже придумать, по причине, которую вы хотели, по крайней мере, вы могли бы это сделать, потому что некоторые части транзакции успешно завершились.

Но когда XACT_STATE()возвращается a -1, тогда вам действительно нужно, ROLLBACKпотому что какая-то часть транзакции перешла в плохое состояние. Теперь я согласен с тем, что если управление было передано блоку CATCH, то имеет смысл просто проверить @@TRANCOUNT, потому что, даже если вы можете зафиксировать транзакцию, зачем вам это нужно?

Но если вы заметите в верхней части примера, настройка XACT_ABORT ONнемного изменится. У вас может быть обычная ошибка, после BEGIN TRANэтого она передаст управление блоку CATCH, когда XACT_ABORTбудет OFFи XACT_STATE () вернется 1. НО, если XACT_ABORT равен ON, то транзакция «прерывается» (то есть становится недействительной) для любой ошибки и затем XACT_STATE()возвращается -1. В этом случае, кажется, бесполезно проверять XACT_STATE()внутри CATCHблока, так как всегда кажется, что возвращает -1когда XACT_ABORTесть ON.

Так тогда для чего XACT_STATE()? Некоторые подсказки:

  • Страница MSDN для TRY...CATCHраздела «Uncommittable Transactions and XACT_STATE» гласит:

    Ошибка, которая обычно завершает транзакцию вне блока TRY, приводит к тому, что транзакция переходит в состояние uncommittable, когда ошибка происходит внутри блока TRY.

  • Страница MSDN для SET XACT_ABORT в разделе «Замечания» гласит:

    Когда SET XACT_ABORT имеет значение OFF, в некоторых случаях выполняется только откат оператора Transact-SQL, вызвавший ошибку, и транзакция продолжает обрабатываться.

    и:

    XACT_ABORT должен быть включен для операторов модификации данных в неявной или явной транзакции против большинства поставщиков OLE DB, включая SQL Server.

  • Страница MSDN для BAGIN TRANSACTION в разделе «Замечания» гласит:

    Локальная транзакция, запущенная оператором BEGIN TRANSACTION, преобразуется в распределенную транзакцию, если перед фиксацией или откатом инструкции выполняются следующие действия:

    • Выполняется оператор INSERT, DELETE или UPDATE, который ссылается на удаленную таблицу на связанном сервере. Оператор INSERT, UPDATE или DELETE не выполняется, если поставщик OLE DB, используемый для доступа к связанному серверу, не поддерживает интерфейс ITransactionJoin.

Наиболее применимое использование, кажется, находится в контексте операторов DML Linked Server. И я верю, что столкнулся с этим сам много лет назад. Я не помню всех деталей, но это было связано с тем, что удаленный сервер был недоступен, и по какой-то причине эта ошибка не попала в блок TRY и никогда не отправлялась в CATCH, и поэтому КОМИТ, когда не должно быть. Конечно, это могло быть связано с тем, что вы не XACT_ABORTустановили, ONа не проверили XACT_STATE(), или, возможно, и то, и другое. И я вспоминаю, что читал что-то, в котором говорилось, что если вы используете связанные серверы и / или распределенные транзакции, тогда вам нужно было использовать XACT_ABORT ONи / или XACT_STATE(), но я не могу сейчас найти этот документ. Если я найду его, я обновлю его ссылкой.

Тем не менее, я попробовал несколько вещей и не могу найти сценарий, который имеет XACT_ABORT ONи передает управление CATCHблоку с XACT_STATE()отчетностью 1.

Попробуйте эти примеры, чтобы увидеть влияние XACT_ABORTна значение 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;

ОБНОВИТЬ

Хотя это и не является частью исходного Вопроса, основываясь на этих комментариях к этому Ответу:

Я читал статьи Эрланда по обработке ошибок и транзакций, где он говорит, что XACT_ABORTэто OFFпо умолчанию по старым причинам, и обычно мы должны установить его ON.
...
"... если вы последуете рекомендации и запустите SET XACT_ABORT ON, транзакция всегда будет обречена."

Прежде чем использовать XACT_ABORT ONвезде, я хотел бы спросить: что именно здесь получается? Я не нашел в этом необходимости и вообще рекомендую использовать его только в случае необходимости. Независимо от того, хотите ли вы, ROLLBACKможете ли вы обрабатывать его достаточно легко, используя шаблон, показанный в ответе @ Remus , или шаблон, который я использовал годами, по сути одно и то же, но без точки сохранения, как показано в этом ответе (который обрабатывает вложенные вызовы):

Должны ли мы обрабатывать транзакции в коде C #, а также в хранимых процедурах


ОБНОВЛЕНИЕ 2

Я провел немного больше тестирования, на этот раз путем создания небольшого консольного приложения .NET, создания транзакции на уровне приложения перед выполнением каких-либо SqlCommandобъектов (т. Е. Через using (SqlTransaction _Tran = _Connection.BeginTransaction()) { ...), а также с использованием ошибки прерывания пакета вместо простого выражения. - ошибка, и обнаружил, что:

  1. Транзакция «uncommitable» - это та, которая по большей части уже откатана (изменения отменены), но @@TRANCOUNTвсе еще> 0.
  2. Если у вас есть «uncommitable» Транзакция, вы не можете выдать, COMMITпоскольку это сгенерирует ошибку, сообщающую, что Транзакция «uncommittable». Вы также не можете игнорировать это / ничего не делать, так как по окончании пакета будет сгенерирована ошибка, в которой будет указано, что пакет завершен с длительной транзакцией, не подлежащей завершению, и он будет откатан (так что, если он все равно будет автоматически откатываться, зачем выкидывать ошибку?). Таким образом, вы должны выдать явный ROLLBACK, возможно, не в непосредственном CATCHблоке, а до окончания пакета.
  3. В TRY...CATCHконструкции, когда XACT_ABORTесть OFF, ошибки, которые автоматически TRYпрервали бы Транзакцию, если бы они произошли вне блока, такие как ошибки пакетного прерывания, отменят работу, но не прервут Tranasction, оставив его как «uncommitable». Выдача a ROLLBACK- это скорее формальность, необходимая для закрытия транзакции, но работа уже была отменена.
  4. Когда XACT_ABORTэто так ON, большинство ошибок действуют как прерывание партии и, следовательно, ведут себя так, как описано в пуле выше (# 3).
  5. XACT_STATE()По крайней мере в CATCHблоке будет отображаться -1ошибка для прерывания пакета, если во время ошибки была активная заявка.
  6. XACT_STATE()иногда возвращается, 1даже если нет активной транзакции. Если @@SPID(среди прочего) в SELECTсписке наряду с XACT_STATE(), то XACT_STATE()вернет 1, когда нет активной транзакции. Такое поведение началось в SQL Server 2012 и существует в 2014 году, но я не тестировал в 2016 году.

С учетом вышесказанного:

  • Учитывая пункты # 4 и # 5, так как большинство (или все?) Ошибок сделают транзакцию «несокрушимой», кажется совершенно бессмысленным проверять XACT_STATE()в CATCHблоке значение, которое XACT_ABORTесть, ONпоскольку возвращаемое значение всегда будет -1.
  • Проверка XACT_STATE()в CATCHблоке когда XACT_ABORTесть, OFFимеет больше смысла, потому что возвращаемое значение будет, по крайней мере, иметь некоторые вариации, поскольку оно будет возвращаться 1для ошибок, прерывающих оператор. Однако, если вы пишете код, как большинство из нас, то это различие не имеет смысла, так как вы все ROLLBACKравно будете звонить просто для того, чтобы произошла ошибка.
  • Если вы найдете ситуацию, которая требует выдачи COMMITв CATCHблоке, то проверьте значение XACT_STATE()и обязательно SET XACT_ABORT OFF;.
  • XACT_ABORT ONкажется, предлагает мало или нет никакой выгоды по сравнению с TRY...CATCHконструкцией.
  • Я не могу найти сценарий, где проверка XACT_STATE()дает значительную выгоду по сравнению с простой проверкой @@TRANCOUNT.
  • Я также не могу найти сценарий, где XACT_STATE()возвращается 1в CATCHблоке, когда XACT_ABORTесть ON. Я думаю, что это ошибка документации.
  • Да, вы можете откатить транзакцию, которую вы явно не начали. И в контексте использования XACT_ABORT ONэто спорный вопрос, поскольку ошибка, произошедшая в TRYблоке, автоматически откатит изменения.
  • Преимущество TRY...CATCHконструкции заключается XACT_ABORT ONв том, что она не отменяет автоматически всю Транзакцию и, следовательно, позволяет зафиксировать Транзакцию (до тех пор, пока она XACT_STATE()возвращается 1) (даже если это крайний случай).

Пример XACT_STATE()возврата, -1когда XACT_ABORTесть 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;

ОБНОВЛЕНИЕ 3

Относится к пункту № 6 в разделе ОБНОВЛЕНИЕ 2 (т. Е. Возможное неверное значение, возвращаемое XACT_STATE()при отсутствии активной транзакции):

  • Странное / ошибочное поведение началось в SQL Server 2012 (до сих пор проверено на 2012 SP2 и 2014 SP1)
  • В версиях SQL Server 2005, 2008 и 2008 R2 XACT_STATE()не сообщалось об ожидаемых значениях при использовании в триггерах или INSERT...EXECсценариях: xact_state () нельзя надежно использовать, чтобы определить, обречена ли транзакция . Однако в этих 3 -х вариантах (я тестировал только на 2008 R2), XACT_STATE()это не ошибочно сообщают , 1когда используется в SELECTс @@SPID.
  • Существует ошибка Connect, поданная в связи с упомянутым здесь поведением, но она закрыта как «By Design»: XACT_STATE () может возвращать неверное состояние транзакции в SQL 2012 . Тем не менее, тест был выполнен при выборе из DMV, и был сделан вывод, что для этого, естественно, потребуется сгенерированная системой транзакция, по крайней мере, для некоторых DMV. В последнем ответе MS также было указано, что:

    Обратите внимание, что оператор IF, а также оператор SELECT без FROM не запускают транзакцию.
    например, выполнение SELECT XACT_STATE (), если у вас нет ранее существующей транзакции, вернет 0.

    Эти утверждения неверны, учитывая следующий пример:

    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
  • Следовательно, новая ошибка Connect:
    XACT_STATE () возвращает 1 при использовании в SELECT с некоторыми системными переменными, но без предложения FROM

ПОЖАЛУЙСТА, ОБРАТИТЕ ВНИМАНИЕ, что в «XACT_STATE () может возвращать неверное состояние транзакции в SQL 2012» элемент Connect, связанный непосредственно выше, Microsoft (ну, представитель) утверждает:

@@ trancount возвращает количество операторов BEGIN TRAN. Таким образом, он не является надежным индикатором наличия активной транзакции. XACT_STATE () также возвращает 1, если есть активная транзакция автоматического подтверждения, и, таким образом, является более надежным индикатором того, существует ли активная транзакция.

Тем не менее, я не могу найти причин не доверять @@TRANCOUNT. Следующий тест показывает, что @@TRANCOUNTдействительно возвращается 1в транзакции автоматической фиксации:

--- 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

Я также проверил на реальной таблице с триггером и @@TRANCOUNTв триггере точно отчитался, 1хотя явной транзакции не было запущено.

Соломон Руцкий
источник
4

Защитное программирование требует от вас написания кода, который обрабатывает как можно больше известных состояний, тем самым уменьшая вероятность ошибок.

Проверка XACT_STATE () для определения возможности выполнения отката - просто хорошая практика. Слепая попытка отката означает, что вы можете непреднамеренно вызвать ошибку в вашем TRY ... CATCH.

Один из способов отката может быть неудачным внутри TRY ... CATCH, если вы явно не начали транзакцию. Копирование и вставка блоков кода могут легко вызвать это.

Макс Вернон
источник
Спасибо за ответ. Я просто не мог придумать случай, когда просто ROLLBACKне сработало бы внутри, CATCHи вы привели хороший пример. Я предполагаю, что это может также быстро стать грязным, если вложенные транзакции и вложенные хранимые процедуры с их собственными TRY ... CATCH ... ROLLBACKучаствуют.
Владимир Баранов
Тем не менее, я был бы признателен, если бы вы могли расширить свой ответ относительно второй части - IF (XACT_STATE()) = 1 COMMIT TRANSACTION; Как мы можем оказаться внутри CATCHблока с коммитируемой транзакцией? Я бы не посмел совершить какой-то (возможный) мусор изнутри CATCH. Я рассуждаю так: если мы внутри, что- CATCHто пошло не так, я не могу доверять состоянию базы данных, поэтому я бы улучшил ROLLBACKвсе, что у нас есть.
Владимир Баранов