Pourquoi Sql Server continue-t-il de s'exécuter après raiserror lorsque xact_abort est activé?

87

Je viens d'être surpris par quelque chose dans TSQL. Je pensais que si xact_abort était activé, j'appelais quelque chose comme

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

arrêterait l'exécution de la procédure stockée (ou de tout lot).

Mais mon message d'erreur ADO.NET vient de prouver le contraire. J'ai reçu à la fois le message d'erreur de raiserror dans le message d'exception, ainsi que la prochaine chose qui s'est cassée après cela.

C'est ma solution de contournement (ce qui est mon habitude de toute façon), mais il ne semble pas que cela soit nécessaire:

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

Les documents disent ceci:

Lorsque SET XACT_ABORT est sur ON, si une instruction Transact-SQL génère une erreur d'exécution, la transaction entière est terminée et annulée.

Cela signifie-t-il que je dois utiliser une transaction explicite?

Eric Z Beard
la source
Je viens de tester et RAISERRORmettra en fait fin à l'exécution si la gravité est définie sur 17 ou 18, au lieu de 16.
réformé le
1
Je viens de tester SQL Server 2012 et RAISERRORne mettra en fait pas fin à l'exécution si la gravité est définie sur 17 ou 18, au lieu de 16.
Ian Boyd
La raison est clairement expliquée par Erland Sommarskog (MVP SQL Server depuis 2001) dans la partie 2 de son excellente série Error and Transaction Handling in SQL Server: "De temps en temps, j'ai le sentiment que SQL Server est intentionnellement conçu pour être déroutant que possible. Lorsqu'ils planifient une nouvelle version, ils se demandent ce que nous pouvons faire cette fois pour semer la confusion chez les utilisateurs? Parfois, ils manquent d'idées, mais quelqu'un dit alors Faisons quelque chose avec la gestion des erreurs! "
Ingénieur inversé le

Réponses:

48

Ceci est By Design TM , comme vous pouvez le voir sur Connect par la réponse de l'équipe SQL Server à une question similaire:

Merci pour votre avis. De par sa conception, l'option set XACT_ABORT n'a pas d'incidence sur le comportement de l'instruction RAISERROR. Nous prendrons en considération vos commentaires pour modifier ce comportement pour une future version de SQL Server.

Oui, c'est un peu un problème pour certains qui espéraient RAISERRORqu'avec une gravité élevée (comme 16) serait la même chose qu'une erreur d'exécution SQL - ce n'est pas le cas.

Votre solution de contournement concerne exactement ce que vous devez faire et l'utilisation d'une transaction explicite n'a aucun effet sur le comportement que vous souhaitez modifier.

Philip Rieck
la source
1
Merci Philip. Le lien que vous avez référencé ne semble pas disponible.
Eric Z Beard
2
Le lien fonctionne bien, si jamais vous avez besoin de le rechercher, titre "Have RAISERROR work with XACT_ABORT", author "jorundur", ID: 275308
JohnC
Le lien est mort, sans copie en cache archive.org. Il a été perdu pour toujours dans le sable du temps.
Ian Boyd
Cette réponse est une bonne sauvegarde - avec un lien vers les documents où ce comportement est clairement indiqué.
pcdev
25

Si vous utilisez un bloc try / catch, un numéro d'erreur de raiserror de gravité 11-19 provoquera le saut de l'exécution vers le bloc catch.

Toute gravité supérieure à 16 est une erreur système. Pour illustrer le code suivant, configure un bloc try / catch et exécute une procédure stockée dont nous supposons qu'elle échouera:

Supposons que nous ayons une table [dbo]. [Erreurs] pour contenir les erreurs Supposons que nous ayons une procédure stockée [dbo]. [AssumeThisFails] qui échouera lorsque nous l'exécuterons

-- 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
la source
22

Utilisez RETURNimmédiatement après RAISERROR()et il n'exécutera pas la procédure plus loin.

piyush
la source
8
Vous voudrez peut-être appeler rollback transactionavant d'appeler return.
Mike Christian
1
Vous devrez probablement faire quelque chose dans votre bloc catch
sqluser
14

Comme indiqué sur la documentation pour SET XACT_ABORT, l' THROWinstruction doit être utilisée à la place de RAISERROR.

Les deux se comportent légèrement différemment . Mais lorsque XACT_ABORTest défini sur ON, vous devez toujours utiliser la THROWcommande.

Möoz
la source
25
Si vous n'avez pas 2k12 (ou plus quand il sort), alors il n'y a pas d'instruction THROW à avoir.
Jeff Moden