TL; DR / Резюме: Относительно этой части Вопроса:
Я не вижу, в каких случаях управление может быть передано внутрь CATCH
с помощью транзакции, которая может быть зафиксирована, когда XACT_ABORT
задано значениеON
.
Я провел немало тестов по этому вопросу сейчас и не могу найти случаев, когда XACT_STATE()
возвращается 1
внутри CATCH
блока когда @@TRANCOUNT > 0
и свойство сеанса XACT_ABORT
is 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
документации).
Я думаю, что разумно сделать вывод, что:
- введение
TRY...CATCH
конструкции в SQL Server 2005 создало необходимость в новом состоянии транзакции (т.е. «uncommittable») и XACT_STATE()
функции для получения этой информации.
- проверка
XACT_STATE()
в CATCH
блоке действительно имеет смысл, только если выполняются оба следующих условия:
XACT_ABORT
есть OFF
(иначе XACT_STATE()
должен всегда возвращаться -1
и @@TRANCOUNT
будет все, что вам нужно)
- У вас есть логика в
CATCH
блоке или где-то в цепочке, если вызовы вложены, что делает изменение (a COMMIT
или даже любой оператор DML, DDL и т. Д.) Вместо выполнения a ROLLBACK
. (это очень нетипичный вариант использования) ** см. примечание внизу, в разделе ОБНОВЛЕНИЕ 3, относительно неофициальной рекомендации Microsoft всегда проверять XACT_STATE()
вместо @@TRANCOUNT
, и почему тестирование показывает, что их рассуждения не оправдываются.
- введение
TRY...CATCH
конструкции в SQL Server 2005, по большей части, устарело XACT_ABORT ON
свойство сеанса, поскольку оно обеспечивает большую степень контроля над транзакцией (по крайней мере, у вас есть опция при COMMIT
условии, что XACT_STATE()
она не возвращается -1
).
Другой способ смотреть на это, до SQL Server 2005 , при XACT_ABORT ON
условии , простой и надежный способ обработки стоп , когда произошла ошибка, по сравнению с проверкой @@ERROR
после каждого оператора.
- Документации пример кода для
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()) { ...
), а также с использованием ошибки прерывания пакета вместо простого выражения. - ошибка, и обнаружил, что:
- Транзакция «uncommitable» - это та, которая по большей части уже откатана (изменения отменены), но
@@TRANCOUNT
все еще> 0.
- Если у вас есть «uncommitable» Транзакция, вы не можете выдать,
COMMIT
поскольку это сгенерирует ошибку, сообщающую, что Транзакция «uncommittable». Вы также не можете игнорировать это / ничего не делать, так как по окончании пакета будет сгенерирована ошибка, в которой будет указано, что пакет завершен с длительной транзакцией, не подлежащей завершению, и он будет откатан (так что, если он все равно будет автоматически откатываться, зачем выкидывать ошибку?). Таким образом, вы должны выдать явный ROLLBACK
, возможно, не в непосредственном CATCH
блоке, а до окончания пакета.
- В
TRY...CATCH
конструкции, когда XACT_ABORT
есть OFF
, ошибки, которые автоматически TRY
прервали бы Транзакцию, если бы они произошли вне блока, такие как ошибки пакетного прерывания, отменят работу, но не прервут Tranasction, оставив его как «uncommitable». Выдача a ROLLBACK
- это скорее формальность, необходимая для закрытия транзакции, но работа уже была отменена.
- Когда
XACT_ABORT
это так ON
, большинство ошибок действуют как прерывание партии и, следовательно, ведут себя так, как описано в пуле выше (# 3).
XACT_STATE()
По крайней мере в CATCH
блоке будет отображаться -1
ошибка для прерывания пакета, если во время ошибки была активная заявка.
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
хотя явной транзакции не было запущено.
XACT_ABORT
наON
илиOFF
.