Por que o Sql Server continua executando após raiserror quando xact_abort está ligado?

87

Acabei de me surpreender com algo em TSQL. Eu pensei que se xact_abort estivesse ligado, chamando algo como

raiserror('Something bad happened', 16, 1);

interromperia a execução do procedimento armazenado (ou qualquer lote).

Mas minha mensagem de erro do ADO.NET provou o contrário. Recebi a mensagem de erro raiserror na mensagem de exceção, além da próxima coisa que quebrou depois disso.

Esta é a minha solução alternativa (que é meu hábito de qualquer maneira), mas não parece que seja necessário:

if @somethingBadHappened
    begin;
        raiserror('Something bad happened', 16, 1);
        return;
    end;

Os documentos dizem o seguinte:

Quando SET XACT_ABORT está ON, se uma instrução Transact-SQL gerar um erro em tempo de execução, a transação inteira será encerrada e revertida.

Isso significa que devo usar uma transação explícita?

Eric Z Beard
fonte
Acabei de testar e RAISERROR, de fato, encerrará a execução se a gravidade for definida como 17 ou 18, em vez de 16.
reformado em
1
Acabei de testar o SQL Server 2012 e RAISERROR, de fato, não encerrará a execução se a gravidade for definida como 17 ou 18, em vez de 16.
Ian Boyd
O motivo é claramente explicado por Erland Sommarskog (SQL Server MVP desde 2001) na parte 2 de sua excelente série Error and Transaction Handling in SQL Server: "De vez em quando, tenho a sensação de que o SQL Server foi intencionalmente projetado para ser tão o mais confuso possível. Quando eles planejam um novo lançamento, eles se perguntam o que podemos fazer desta vez para confundir os usuários? Às vezes, eles ficam um pouco sem ideias, mas então alguém diz Vamos fazer algo com tratamento de erros! "
Engenheiro

Respostas:

48

Este é o By Design TM , como você pode ver no Connect pela resposta da equipe do SQL Server a uma pergunta semelhante:

Obrigado pelo seu feedback. Por design, a opção de conjunto XACT_ABORT não afeta o comportamento da instrução RAISERROR. Vamos considerar seus comentários para modificar esse comportamento em uma versão futura do SQL Server.

Sim, isso é um pouco problemático para alguns que esperavam que RAISERRORcom uma gravidade alta (como 16) fosse o mesmo que um erro de execução SQL - não é.

Sua solução alternativa é exatamente o que você precisa fazer, e usar uma transação explícita não tem nenhum efeito no comportamento que você deseja alterar.

Philip Rieck
fonte
1
Obrigado Philip. O link que você referenciou parece estar indisponível.
Eric Z Beard,
2
O link está funcionando bem, se você precisar pesquisá-lo, título "Faça RAISERROR trabalhar com XACT_ABORT", autor "jorundur", ID: 275308
JohnC
O link está morto, sem cópia em cache do archive.org. Foi perdido nas areias do tempo para sempre.
Ian Boyd
Esta resposta é um bom backup - com um link para os documentos onde esse comportamento é esclarecido.
pcdev
25

Se você usar um bloco try / catch, um número de erro raiserror com gravidade 11-19 fará com que a execução pule para o bloco catch.

Qualquer gravidade acima de 16 é um erro do sistema. Para demonstrar o código a seguir, configura um bloco try / catch e executa um procedimento armazenado que presumimos que falhará:

suponha que temos uma tabela [dbo]. [Erros] para manter os erros pressupõe que temos um procedimento armazenado [dbo]. [AssumeThisFails] que falhará quando for executado

-- first lets build a temporary table to hold errors
if (object_id('tempdb..#RAISERRORS') is null)
 create table #RAISERRORS (ErrorNumber int, ErrorMessage varchar(400), ErrorSeverity int, ErrorState int, ErrorLine int, ErrorProcedure varchar(128));

-- this will determine if the transaction level of the query to programatically determine if we need to begin a new transaction or create a save point to rollback to
declare @tc as int;
set @tc = @@trancount;
if (@tc = 0)
 begin transaction;
else
 save transaction myTransaction;

-- the code in the try block will be executed
begin try
 declare @return_value = '0';
 set @return_value = '0';
 declare
  @ErrorNumber as int,
  @ErrorMessage as varchar(400),
  @ErrorSeverity as int,
  @ErrorState as int,
  @ErrorLine as int,
  @ErrorProcedure as varchar(128);


 -- assume that this procedure fails...
 exec @return_value = [dbo].[AssumeThisFails]
 if (@return_value <> 0)
  raiserror('This is my error message', 17, 1);

 -- the error severity of 17 will be considered a system error execution of this query will skip the following statements and resume at the begin catch block
 if (@tc = 0)
  commit transaction;
 return(0);
end try


-- the code in the catch block will be executed on raiserror("message", 17, 1)
begin catch
  select
   @ErrorNumber = ERROR_NUMBER(),
   @ErrorMessage = ERROR_MESSAGE(),
   @ErrorSeverity = ERROR_SEVERITY(),
   @ErrorState = ERROR_STATE(),
   @ErrorLine = ERROR_LINE(),
   @ErrorProcedure = ERROR_PROCEDURE();

  insert #RAISERRORS (ErrorNumber, ErrorMessage, ErrorSeverity, ErrorState, ErrorLine, ErrorProcedure)
   values (@ErrorNumber, @ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorLine, @ErrorProcedure);

  -- if i started the transaction
  if (@tc = 0)
  begin
   if (XACT_STATE() <> 0)
   begin
     select * from #RAISERRORS;
    rollback transaction;
    insert into [dbo].[Errors] (ErrorNumber, ErrorMessage, ErrorSeverity, ErrorState, ErrorLine, ErrorProcedure)
     select * from #RAISERRORS;
    insert [dbo].[Errors] (ErrorNumber, ErrorMessage, ErrorSeverity, ErrorState, ErrorLine, ErrorProcedure)
     values (@ErrorNumber, @ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorLine, @ErrorProcedure);
    return(1);
   end
  end
  -- if i didn't start the transaction
  if (XACT_STATE() = 1)
  begin
   rollback transaction myTransaction;
   if (object_id('tempdb..#RAISERRORS') is not null)
    insert #RAISERRORS (ErrorNumber, ErrorMessage, ErrorSeverity, ErrorState, ErrorLine, ErrorProcedure)
     values (@ErrorNumber, @ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorLine, @ErrorProcedure);
   else
    raiserror(@ErrorMessage, @ErrorSeverity, @ErrorState);
   return(2); 
  end
  else if (XACT_STATE() = -1)
  begin
   rollback transaction;
   if (object_id('tempdb..#RAISERRORS') is not null)
    insert #RAISERRORS (ErrorNumber, ErrorMessage, ErrorSeverity, ErrorState, ErrorLine, ErrorProcedure)
     values (@ErrorNumber, @ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorLine, @ErrorProcedure);
   else
    raiserror(@ErrorMessage, @ErrorSeverity, @ErrorState);
   return(3);
  end
 end catch
end
ninegrid
fonte
22

Use RETURNimediatamente depois RAISERROR()e ele não executará o procedimento posteriormente.

piyush
fonte
8
Você pode querer ligar rollback transactionantes de ligar return.
Mike Christian
1
Provavelmente você precisa fazer algo em seu bloco catch
sqluser
14

Conforme indicado nos documentos para SET XACT_ABORT, a THROWinstrução deve ser usada em vez de RAISERROR.

Os dois se comportam de maneira um pouco diferente . Mas quando XACT_ABORTestá definido como ON, você deve sempre usar o THROWcomando.

Möoz
fonte
25
Se você não tiver 2k12 (ou superior quando for lançado), não há nenhuma instrução THROW disponível.
Jeff Moden