Eu tenho lido MSDN sobre TRY...CATCH
e XACT_STATE
.
Tem o seguinte exemplo que usa XACT_STATE
no CATCH
bloco de uma TRY…CATCH
construção para determinar se deve confirmar ou reverter uma transação:
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
O que não entendo é: por que devo me importar e verificar o que XACT_STATE
retorna?
Observe que o sinalizador XACT_ABORT
está definido como ON
no exemplo.
Se houver um erro grave o suficiente dentro do TRY
bloco, o controle passará para CATCH
. Então, se estou dentro do CATCH
, sei que aquela transação teve um problema e realmente a única coisa sensata a fazer nesse caso é revertê-la, não é?
Mas, este exemplo do MSDN implica que pode haver casos em que o controle é passado CATCH
e ainda faz sentido confirmar a transação. Alguém poderia dar algum exemplo prático de quando isso pode acontecer, quando faz sentido?
Não vejo em que casos o controle pode ser passado para dentro CATCH
com uma transação que pode ser confirmada quando XACT_ABORT
está definida comoON
.
O artigo do MSDN sobre SET XACT_ABORT
tem um exemplo quando algumas instruções dentro de uma transação são executadas com êxito e algumas falham quando XACT_ABORT
é definido como OFF
, eu entendo isso. Mas, SET XACT_ABORT ON
como pode acontecer que XACT_STATE()
retorne 1 dentro do CATCH
bloco?
Inicialmente, eu teria escrito este código assim:
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
Levando em consideração uma resposta de Max Vernon, eu escreveria o código assim. Ele mostrou que faz sentido verificar se há uma transação ativa antes de tentar ROLLBACK
. Ainda assim, com SET XACT_ABORT ON
o CATCH
bloco pode haver uma transação condenada ou nenhuma transação. Portanto, em qualquer caso, não há nada a fazer COMMIT
. Estou errado?
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
It turns out that transaction can not be committed from inside the
CATCH
block ifXACT_ABORT
is set toON
.The example from MSDN is somewhat misleading, because the check implies that
XACT_STATE
can return 1 in some cases and it may be possible toCOMMIT
the transaction.It is not true,
XACT_STATE
will never return 1 insideCATCH
block ifXACT_ABORT
is set toON
.It seems that the MSDN sample code was meant to primarily illustrate the use of
XACT_STATE()
function regardless of theXACT_ABORT
setting. The sample code looks generic enough to work with bothXACT_ABORT
set toON
andOFF
. It is just that withXACT_ABORT = ON
the checkIF (XACT_STATE()) = 1
becomes unnecessary.There is a very good detailed set of articles about Error and Transaction Handling in SQL Server by Erland Sommarskog. In Part 2 - Classification of Errors he presents a comprehensive table that puts together all classes of errors and how they are handled by SQL Server and how
TRY ... CATCH
andXACT_ABORT
changes the behaviour.The last column in the table answers the question. With
TRY-CATCH
and withXACT_ABORT ON
the transaction is doomed in all possible cases.One note outside the scope of the question. As Erland says, this consistency is one of the reasons to set
XACT_ABORT
toON
:TL;DR / Resumo executivo: Com relação a esta parte da pergunta:
Eu fiz alguns testes sobre isso agora e não consigo encontrar nenhum caso em que
XACT_STATE()
retorna1
dentro de umCATCH
bloco quando@@TRANCOUNT > 0
e a propriedade de sessão deXACT_ABORT
éON
. E, de fato, de acordo com a página atual do MSDN para SET XACT_ABORT :Essa afirmação parece estar de acordo com sua especulação e minhas descobertas.
Verdadeiro, mas as instruções nesse exemplo não estão dentro de um
TRY
bloco. Essas mesmas instruções dentro de umTRY
bloco ainda impediriam a execução de quaisquer instruções após aquela que causou o erro, mas supondo queXACT_ABORT
sejaOFF
, quando o controle é passado para oCATCH
bloco, a transação ainda é fisicamente válida, pois todas as alterações anteriores ocorreram sem erro e podem ser cometidos, se esse for o desejo, ou podem ser revertidos. Por outro lado, seXACT_ABORT
forON
, todas as alterações anteriores serão revertidas automaticamente e você terá a opção de: a) emitir umROLLBACK
que é principalmente apenas uma aceitação da situação, pois a transação já foi revertida menos a redefinição@@TRANCOUNT
para0
, ou b) obter um erro. Não há muita escolha, não é?Um detalhe possivelmente importante para esse quebra-cabeça que não é aparente nessa documentação
SET XACT_ABORT
é que essa propriedade de sessão, e até mesmo esse código de exemplo, existe desde o SQL Server 2000 (a documentação é quase idêntica entre as versões), anterior àTRY...CATCH
construção que foi introduzido no SQL Server 2005. Olhando para aquela documentação novamente e olhando para o exemplo ( sem oTRY...CATCH
), usingXACT_ABORT ON
causa uma reversão imediata da Transação: não há nenhum estado de Transação de "incommittável" (observe que não há nenhuma menção em todos de um estado de Transação "não compromissável" nessaSET XACT_ABORT
documentação).Acho razoável concluir que:
TRY...CATCH
construção no SQL Server 2005 criou a necessidade de um novo estado de Transação (ou seja, "incommittable") e aXACT_STATE()
função para obter essas informações.XACT_STATE()
em umCATCH
bloco realmente só faz sentido se ambos os itens a seguir forem verdadeiros:XACT_ABORT
isOFF
(elseXACT_STATE()
deve sempre retornar-1
e@@TRANCOUNT
seria tudo que você precisa)CATCH
bloco, ou em algum lugar na cadeia se as chamadas estiverem aninhadas, que faz uma alteração (umaCOMMIT
ou mesmo qualquer instrução DML, DDL, etc) em vez de fazer umROLLBACK
. (este é um caso de uso muito atípico) ** consulte a nota na parte inferior, na seção UPDATE 3, sobre uma recomendação não oficial da Microsoft para sempre verificarXACT_STATE()
em vez de@@TRANCOUNT
, e por que o teste mostra que seu raciocínio não dá certo.TRY...CATCH
construção no SQL Server 2005 tornou, em grande parte, obsoleta aXACT_ABORT ON
propriedade da sessão, pois fornece um maior grau de controle sobre a transação (você tem pelo menos a opção deCOMMIT
, desde queXACT_STATE()
não retorne-1
).Outra maneira de ver isso é, antes do SQL Server 2005 ,
XACT_ABORT ON
fornecer uma maneira fácil e confiável de interromper o processamento quando ocorre um erro, em comparação com a verificação@@ERROR
após cada instrução.XACT_STATE()
é errôneo ou, na melhor das hipóteses, enganoso, pois mostra a verificação deXACT_STATE() = 1
quandoXACT_ABORT
éON
.A parte longa ;-)
Sim, esse código de exemplo no MSDN é um pouco confuso (veja também: @@TRANCOUNT (Rollback) vs. XACT_STATE ) ;-). E, eu acho que é enganoso porque mostra algo que não faz sentido (pelo motivo que você está perguntando: você pode ter uma transação "committable" no
CATCH
bloco quandoXACT_ABORT
isON
), ou mesmo se for possível, é ainda se concentra em uma possibilidade técnica que poucos irão querer ou precisar, e ignora a razão pela qual é mais provável que alguém precise dela.Acho que ajudaria se tivéssemos certeza de que estamos na mesma página em relação ao significado de certas palavras e conceitos:
"erro grave o suficiente": Só para ficar claro, TRY...CATCH interceptará a maioria dos erros. A lista do que não será capturado está listada na página vinculada do MSDN, na seção "Erros não afetados por uma construção TRY…CATCH".
"se estou dentro do CATCH, sei que a transação teve um problema" ( ênfase adicionada): Se por "transação" você quer dizer a unidade lógica de trabalho conforme determinado por você agrupando instruções em uma transação explícita, então Muito provavelmente sim. Acho que a maioria de nós, pessoal do banco de dados, tenderia a concordar que a reversão é "a única coisa sensata a fazer", pois provavelmente temos uma visão semelhante de como e por que usamos transações explícitas e concebemos quais etapas devem constituir uma unidade atômica de trabalho.
Mas, se você quer dizer as unidades reais de trabalho que estão sendo agrupadas na transação explícita, então não, você não sabe que a própria transação teve um problema. Você sabe apenas que uma instrução em execução dentro da transação definida explicitamente gerou um erro. Mas pode não ser uma instrução DML ou DDL. E mesmo que fosse uma instrução DML, a própria transação ainda poderia ser confirmada.
Considerando os dois pontos mencionados acima, provavelmente deveríamos fazer uma distinção entre transações que você "não pode" confirmar e aquelas que você "não deseja" confirmar.
Quando
XACT_STATE()
retorna um1
, isso significa que a Transação é "confirmável", que você pode escolher entreCOMMIT
ouROLLBACK
. Você pode não querer confirmá-lo, mas se, por algum motivo difícil de encontrar-um-exemplo, você quisesse, pelo menos você poderia, porque algumas partes da transação foram concluídas com sucesso.Mas quando
XACT_STATE()
retorna um-1
, você realmente precisaROLLBACK
porque alguma parte da transação entrou em um estado ruim. Agora, concordo que, se o controle foi passado para o bloco CATCH, faz sentido apenas verificar@@TRANCOUNT
, porque mesmo que você pudesse confirmar a transação, por que faria isso?Mas se você observar no início do exemplo, a configuração
XACT_ABORT ON
muda um pouco as coisas. Você pode ter um erro regular, depois de fazerBEGIN TRAN
isso passará o controle para o bloco CATCH quandoXACT_ABORT
forOFF
e XACT_STATE() retornará1
. MAS, se XACT_ABORT forON
, então a Transação é "abortada" (ou seja, invalidada) por qualquer erro 'ol e entãoXACT_STATE()
retornará-1
. Nesse caso, parece inútil verificarXACT_STATE()
dentro doCATCH
bloco, pois sempre parece retornar um-1
whenXACT_ABORT
isON
.Então para que serve
XACT_STATE()
? Algumas pistas são:A página do MSDN para
TRY...CATCH
, na seção "Uncommittable Transactions and XACT_STATE", diz:A página do MSDN para SET XACT_ABORT , na seção "Comentários", diz:
e:
A página do MSDN para BEGIN TRANSACTION , na seção "Comentários", diz:
O uso mais aplicável parece estar dentro do contexto das instruções DML do Linked Server. E acredito que me deparei com isso anos atrás. Não me lembro de todos os detalhes, mas tinha algo a ver com o servidor remoto não estar disponível e, por algum motivo, esse erro não foi detectado no bloco TRY e nunca foi enviado para o CATCH e assim foi um COMMIT quando não deveria. Claro, isso poderia ter sido um problema de não ter
XACT_ABORT
definido emON
vez de não ter verificadoXACT_STATE()
, ou possivelmente ambos. E eu me lembro de ter lido algo que dizia que se você usa Servidores Vinculados e/ou Transações Distribuídas, então você precisa usarXACT_ABORT ON
e/ouXACT_STATE()
, mas não consigo encontrar esse documento agora. Se eu encontrar, atualizarei com o link.Mesmo assim, já tentei várias coisas e não consigo encontrar um cenário que tenha
XACT_ABORT ON
e passe o controle para oCATCH
bloco comXACT_STATE()
reporting1
.Experimente estes exemplos para ver o efeito de
XACT_ABORT
no valor deXACT_STATE()
:ATUALIZAR
Embora não faça parte da pergunta original, com base nestes comentários sobre esta resposta:
Prior to using
XACT_ABORT ON
everywhere, I would question: what exactly is being gained here? I have not found it necessary to do and generally advocate that you should use it only when necessary. Whether or not you want toROLLBACK
can be handle easily enough by using the template shown in @Remus's answer, or the one that I have been using for years that is essentially the same thing but without the Save Point, as shown in this answer (which handles nested calls):Are we required to handle Transaction in C# Code as well as in stored procedure
UPDATE 2
I did a bit more testing, this time by creating a small .NET Console App, creating a Transaction in the app layer, prior to executing any
SqlCommand
objects (i.e. viausing (SqlTransaction _Tran = _Connection.BeginTransaction()) { ...
), as well as using a batch-aborting error instead of just a statement-aborting error, and found that:@@TRANCOUNT
is still > 0.COMMIT
as that will generate and error saying that the Transaction is "uncommittable". You also cannot ignore it / do nothing as an error will be generated when the batch finishes stating that the batch completed with a lingering, uncommittable transaction and it will be rolled back (so, um, if it will auto-roll-back anyway, why bother throwing the error?). So you must issue an explicitROLLBACK
, maybe not in the immediateCATCH
block, but before the batch ends.TRY...CATCH
construct, whenXACT_ABORT
isOFF
, errors that would terminate the Transaction automatically had they occurred outside of aTRY
block, such as batch-aborting errors, will undo the work but not terminate the Tranasction, leaving it as "uncommitable". Issuing aROLLBACK
is more of a formality needed to close out the Transaction, but the work has already been rolled-back.XACT_ABORT
isON
, most errors act as batch-aborting, and hence behave as described in the bullet point directly above (#3).XACT_STATE()
, at least in aCATCH
block, will show a-1
for batch-aborting errors if there was an active Transaction at the time of the error.XACT_STATE()
sometimes returns1
even when there is no active Transaction. If@@SPID
(among others) is in theSELECT
list along withXACT_STATE()
, thenXACT_STATE()
will return 1 when there is no active Transaction. This behavior started in SQL Server 2012, and exists on 2014, but I haven't tested on 2016.With the above points in mind:
XACT_STATE()
in theCATCH
block whenXACT_ABORT
isON
since the value returned will always be-1
.XACT_STATE()
in theCATCH
block whenXACT_ABORT
isOFF
makes more sense because the return value will at least have some variation since it will return1
for statement-aborting errors. However, if you code like most of us, then this distinction is meaningless since you will be callingROLLBACK
anyway simply for the fact that an error occurred.COMMIT
in theCATCH
block, then check the value ofXACT_STATE()
, and be sure toSET XACT_ABORT OFF;
.XACT_ABORT ON
seems to offer little to no benefit over theTRY...CATCH
construct.XACT_STATE()
provides a meaningful benefit over simply checking@@TRANCOUNT
.XACT_STATE()
returns1
in aCATCH
block whenXACT_ABORT
isON
. I think it is a documentation error.XACT_ABORT ON
, it's a moot point since an error happening in aTRY
block will automatically roll-back the changes.TRY...CATCH
construct has the benefit overXACT_ABORT ON
in not automatically cancelling the whole Transaction, and hence allowing the Transaction (as long asXACT_STATE()
returns1
) to be committed (even if this is an edge-case).Example of
XACT_STATE()
returning-1
whenXACT_ABORT
isOFF
:UPDATE 3
Related to item #6 in the UPDATE 2 section (i.e. possible incorrect value returned by
XACT_STATE()
when there is no active Transaction):The odd / erroneous behavior started in SQL Server 2012 (so far tested against 2012 SP2 and 2014 SP1)
In SQL Server versions 2005, 2008, and 2008 R2,
XACT_STATE()
did not report expected values when used in Triggers orINSERT...EXEC
scenarios: xact_state() cannot be used reliably to determine whether a transaction is doomed (archived page). However, in these 3 versions (I only tested on 2008 R2),XACT_STATE()
does not incorrectly report1
when used in aSELECT
with@@SPID
.There
iswas a Connect bug filed against the behavior mentioned here but is closed as "By Design": XACT_STATE() can return an incorrect transaction state in SQL 2012 (link no longer valid due to incompetent and/or grossly negligent site migration ?). However, the test was done when selecting from a DMV and it was concluded that doing so would naturally have a system generated transaction, at least for some DMVs. It was also stated in the final response by MS that:Those statements are incorrect given the following example:
Hence, I filed a new Feedback bug:
XACT_STATE() returns 1 when used in SELECT with some system variables but without FROM clause
PLEASE NOTE that in the "XACT_STATE() can return an incorrect transaction state in SQL 2012" Connect item linked directly above, Microsoft (well, a representative of) states:
However, I can find no reason to not trust
@@TRANCOUNT
. The following test shows that@@TRANCOUNT
does indeed return1
in an auto-commit transaction:I also tested on a real table with a Trigger and
@@TRANCOUNT
within the Trigger did accurately report1
even though no explicit Transaction had been started.I would approach this differently.
XACT_ABORT_ON
is a sledge hammer, you can use a more refined approach, see Exception handling and nested transactions:This approach will rollback, when possible, only the work performed inside the TRY block, and restore state to the state before entering the TRY block. This way you can do complex processing, like iterating a cursor, w/o loosing all the work in case of an error. The only draw back is that, by using transaction savepoints, you are restricted from using anything that is incompatible with savepoints, like distributed transactions.
A programação defensiva exige que você escreva um código que lide com tantos estados conhecidos quanto possível, reduzindo assim a possibilidade de erros.
Verificar XACT_STATE() para determinar se uma reversão pode ser executada é simplesmente uma boa prática. Tentar cegamente um rollback significa que você pode inadvertidamente causar um erro dentro do seu TRY...CATCH.
Uma maneira de um rollback falhar dentro de um TRY...CATCH seria se você não iniciasse explicitamente uma transação. Copiar e colar blocos de código pode facilmente causar isso.