Dlaczego Sql Server kontynuuje wykonywanie po raiserror, gdy xact_abort jest włączony?

87

Właśnie coś mnie zaskoczyło w TSQL. Pomyślałem, że jeśli xact_abort jest włączony, dzwonię do czegoś takiego

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

zatrzymałoby wykonywanie procedury składowanej (lub dowolnej partii).

Ale mój komunikat o błędzie ADO.NET właśnie pokazał coś przeciwnego. Otrzymałem zarówno komunikat o błędzie raiserror w komunikacie o wyjątku, jak i następną rzecz, która się zepsuła.

To jest moje obejście (które i tak jest moim nawykiem), ale nie wydaje się, aby było to konieczne:

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

Doktorzy mówią tak:

Gdy SET XACT_ABORT jest ON, jeśli instrukcja Transact-SQL spowoduje błąd w czasie wykonywania, cała transakcja zostanie zakończona i wycofana.

Czy to oznacza, że ​​muszę używać jawnej transakcji?

Eric Z Beard
źródło
Właśnie przetestowano i RAISERRORfaktycznie zakończy wykonywanie, jeśli poziom ważności jest ustawiony na 17 lub 18 zamiast 16.
zreformowany
2
Właśnie przetestowałem SQL Server 2012 i RAISERRORfaktycznie nie przerywa wykonywania, jeśli ważność jest ustawiona na 17 lub 18 zamiast 16.
Ian Boyd
1
Powód został jasno wyjaśniony przez Erlanda Sommarskoga (SQL Server MVP od 2001 r.) W części 2 jego znakomitej serii Obsługa błędów i transakcji w SQL Server: „Od czasu do czasu mam wrażenie, że SQL Server jest tak jak najbardziej mylące. Kiedy planują nową wersję, pytają się nawzajem, co możemy zrobić tym razem, aby zmylić użytkowników? Czasami brakuje im pomysłów, ale potem ktoś mówi: Zróbmy coś z obsługą błędów! ”
Odwrócony inżynier
@IanBoyd, rzeczywiście, nawet po ustawieniu ważności na 17 lub 18 lub 19 wykonanie nie zostaje zatrzymane. Co ciekawsze, jeśli spojrzysz w Messageszakładkę, zobaczysz brak (X rows affected)lub PRINTkomunikaty, co powiedziałbym, że jest kompletnym kłamstwem !
Gabrielius

Odpowiedzi:

48

To jest By Design TM , jak można zobaczyć w Connect, w odpowiedzi zespołu SQL Server na podobne pytanie:

Dziękujemy za twoją opinię. Z założenia opcja zestawu XACT_ABORT nie wpływa na zachowanie instrukcji RAISERROR. Uwzględnimy Twoją opinię, aby zmodyfikować to zachowanie w przyszłej wersji programu SQL Server.

Tak, jest to trochę problem dla niektórych, którzy mieli nadzieję, że RAISERRORwysoka waga (podobna 16) będzie taka sama jak błąd wykonania SQL - tak nie jest.

Twoje obejście dotyczy tylko tego, co musisz zrobić, a użycie jawnej transakcji nie ma żadnego wpływu na zachowanie, które chcesz zmienić.

Philip Rieck
źródło
1
Dzięki Philip. Odnośnik, do którego się odwołałeś, wydaje się niedostępny.
Eric Z Beard,
2
Link działa dobrze, jeśli kiedykolwiek będziesz go potrzebować, tytuł "Have RAISERROR work with XACT_ABORT", autor "jorundur", ID: 275308
JohnC
Link nie żyje, nie ma kopii w pamięci podręcznej archive.org. Na zawsze zaginęła w piaskach czasu.
Ian Boyd
Ta odpowiedź to dobra kopia zapasowa - z linkiem do dokumentów, w których to zachowanie jest jasne.
pcdev
25

Jeśli używasz bloku try / catch, numer błędu raiserror o wadze 11-19 spowoduje, że wykonanie przeskoczy do bloku catch.

Każdy poziom istotności powyżej 16 jest błędem systemu. Aby zademonstrować następujący kod, ustaw blok try / catch i wykonuje procedurę składowaną, która, jak zakładamy, zakończy się niepowodzeniem:

załóżmy, że mamy tabelę [dbo]. [Błędy] do przechowywania błędów zakładamy, że mamy procedurę składowaną [dbo]. [AssumeThisFails], która zakończy się niepowodzeniem, gdy ją wykonamy

-- 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
źródło
22

Użyj RETURNnatychmiast po, RAISERROR()a procedura nie będzie dalej wykonywana.

pijusz
źródło
8
Możesz zadzwonić rollback transactionprzed rozmową return.
Mike Christian
1
Prawdopodobnie będziesz musiał coś zrobić w swoim bloku catch
sqluser
14

Jak wskazano w dokumentach dla SET XACT_ABORT, THROWnależy użyć oświadczenia zamiast RAISERROR.

Obaj zachowują się nieco inaczej . Ale kiedy XACT_ABORTjest ustawiony na ON, zawsze powinieneś używać tego THROWpolecenia.

Möoz
źródło
25
Jeśli nie masz 2k12 (lub więcej, gdy wyjdzie), to nie ma instrukcji THROW.
Jeff Moden