¿Por qué Sql Server sigue ejecutándose después de raiserror cuando xact_abort está activado?

87

Me sorprendió algo en TSQL. Pensé que si xact_abort estaba activado, llamar a algo como

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

detendría la ejecución del procedimiento almacenado (o cualquier lote).

Pero mi mensaje de error de ADO.NET demostró lo contrario. Recibí tanto el mensaje de error raiserror en el mensaje de excepción, más lo siguiente que se rompió después de eso.

Esta es mi solución alternativa (que es mi hábito de todos modos), pero no parece que deba ser necesaria:

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

Los doctores dicen esto:

Cuando SET XACT_ABORT está en ON, si una instrucción Transact-SQL genera un error en tiempo de ejecución, toda la transacción se termina y se revierte.

¿Eso significa que debo utilizar una transacción explícita?

Eric Z Barba
fuente
Se acaba de probar y RAISERROR, de hecho, finalizará la ejecución si la gravedad se establece en 17 o 18, en lugar de 16.
reformado el
2
Acabo de probar SQL Server 2012 y RAISERROR, de hecho, no finalizará la ejecución si la gravedad se establece en 17 o 18, en lugar de 16.
Ian Boyd
1
Erland Sommarskog (MVP de SQL Server desde 2001) explica claramente la razón en la parte 2 de su excelente serie Error and Transaction Handling en SQL Server: "De vez en cuando, tengo la sensación de que SQL Server está diseñado intencionalmente para ser lo lo más confuso posible. Cuando planean una nueva versión, se preguntan entre ellos qué podemos hacer esta vez para confundir a los usuarios .
Ingeniero
@IanBoyd, de hecho, incluso después de establecer la gravedad en 17, 18 o 19, la ejecución no se detiene. Lo que es más interesante, si nos fijamos en la Messagespestaña que no verá (X rows affected)o PRINTmensajes, que yo diría que es una completa mentira !
Gabrielius

Respuestas:

48

Esto es By Design TM , como puede ver en Connect por la respuesta del equipo de SQL Server a una pregunta similar:

Gracias por tus comentarios. Por diseño, la opción de conjunto XACT_ABORT no afecta el comportamiento de la declaración RAISERROR. Consideraremos sus comentarios para modificar este comportamiento para una versión futura de SQL Server.

Sí, esto es un problema para algunos que esperaban que RAISERRORcon una gravedad alta (como 16) fuera lo mismo que un error de ejecución de SQL, no lo es.

Su solución es solo lo que necesita hacer, y el uso de una transacción explícita no tiene ningún efecto en el comportamiento que desea cambiar.

Philip Rieck
fuente
1
Gracias Philip. El enlace al que hizo referencia parece no estar disponible.
Eric Z Beard
2
El enlace funciona bien, si alguna vez necesita buscarlo, título "Haga que RAISERROR funcione con XACT_ABORT", autor "jorundur", ID: 275308
JohnC
El enlace está muerto, sin una copia en caché de archive.org. Se ha perdido en las arenas del tiempo para siempre.
Ian Boyd
Esta respuesta es una buena copia de seguridad, con un enlace a los documentos donde se aclara este comportamiento.
pcdev
25

Si usa un bloque try / catch, un número de error raiserror con gravedad 11-19 hará que la ejecución salte al bloque catch.

Cualquier gravedad superior a 16 es un error del sistema. Para demostrar, el siguiente código configura un bloque try / catch y ejecuta un procedimiento almacenado que asumimos fallará:

supongamos que tenemos una tabla [dbo]. [Errores] para contener errores supongamos que tenemos un procedimiento almacenado [dbo]. [AssumeThisFails] que fallará cuando lo ejecutemos

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

Úselo RETURNinmediatamente después RAISERROR()y no ejecutará más el procedimiento.

piyush
fuente
8
Es posible que desee llamar rollback transactionantes de llamar return.
Mike Christian
1
Probablemente necesite hacer algo en su bloque de captura
sqluser
14

Como se indica en los documentos SET XACT_ABORT, la THROWdeclaración debe usarse en lugar de RAISERROR.

Los dos se comportan de manera ligeramente diferente . Pero cuando XACT_ABORTse establece en ON, siempre debe usar el THROWcomando.

Möoz
fuente
25
Si no tiene 2k12 (o más cuando sale), entonces no hay una declaración THROW.
Jeff Moden