SQL Server w logistyce

TRY...CATCH w procedurze zapisu: po ROLLBACK trzeba jeszcze zgłosić błąd

Blok CATCH, który tylko wycofuje transakcję, ukrywa błąd przed wywołującym. Wzorzec z XACT_ABORT, ROLLBACK, zapisem do logu i THROW oraz test wywołania z cudzej transakcji.

sql.server.net.pl/sql-server/
Dwie programistki przy laptopach wśród chmur i ognistych rozbłysków na tle cyfrowego miasta
Dwie programistki przy laptopach wśród chmur i ognistych rozbłysków na tle cyfrowego miasta
W skrócie
  • Zasada: po ROLLBACK blok CATCH musi zgłosić błąd dalej przez THROW, bo sam ROLLBACK zamienia awarię w pozorny sukces.
  • Kolejność w CATCH: najpierw ROLLBACK własnej transakcji, potem zapis do tabeli błędów, na końcu THROW bez parametrów.
  • Cudza transakcja: flaga własnej transakcji sprawia, że procedura wywołana z cudzej transakcji nie robi ROLLBACK, a wywołujący sam ją wycofuje.
  • Numer błędu: obcięcie danych to 2628 przy poziomie zgodności 150 lub wyższym, a przy poziomie 130 numer 8152.

Blok TRY CATCH z ROLLBACK i THROW w procedurze składowanej decyduje, co zobaczy wywołujący po błędzie zapisu: wyjątek albo pozorny sukces. Formularz, który woła procedurę zapisu, pokaże przy samym ROLLBACK komunikat o zapisie, choć tabela została pusta. Pokazujemy wzorzec, który tego unika.

Po ROLLBACK w bloku CATCH procedura musi zgłosić błąd dalej instrukcją THROW bez parametrów. Bez niej wywołujący nie dostaje wyjątku, tabela zostaje pusta, a dla aplikacji brak wyjątku oznacza sukces. Poprawna kolejność zaczyna się od ROLLBACK własnej transakcji i kończy na THROW, a między nimi stoi zapis błędu do tabeli.

Procedura z samym ROLLBACK w CATCH wygląda jak sukces

Zacznijmy od wariantu, którego nie należy stosować. Procedura otwiera transakcję, zapisuje wiersz, a w bloku CATCH wykonuje wyłącznie ROLLBACK. Wygląda to na staranną obsługę błędu, bo transakcja jest wycofywana.

CREATE PROCEDURE dbo.rejestr_zapisz_cicho
    @email varchar(100), @opis varchar(50)
AS
BEGIN
    BEGIN TRY
        BEGIN TRANSACTION;
        INSERT dbo.rejestr (email, opis) VALUES (@email, @opis);
        COMMIT;
    END TRY
    BEGIN CATCH
        ROLLBACK;
    END CATCH
END
GO

Przykład ilustracyjny, dane wymyślone. Wariant, którego nie należy stosować: CATCH wycofuje transakcję i nie zgłasza błędu.

W teście wywołujący nie złapał wyjątku, tabela została pusta, a sqlcmd -b zakończył skrypt kodem 0. Dla aplikacji brak wyjątku oznacza sukces, więc użytkownik nie dowie się, że zapis nie powstał. Wersja z THROW; kończy ten sam skrypt kodem 1 i komunikatem, który wskazuje numer błędu oraz procedurę wraz z linią.

Blok CATCH, który wycofuje transakcję i nie zgłasza błędu, zamienia awarię w pozorny sukces.

Przełącznik -b programu sqlcmd odróżnia obie wersje w teście, a przełącznik -I opisuje artykuł o QUOTED_IDENTIFIER OFF w procedurze. Przykład sprawdziliśmy na SQL Server 2022 Express w jednorazowej bazie, przy poziomach zgodności od 130 do 160. Składnia nie używa elementów nowszych niż SQL Server 2016 SP1.

XACT_ABORT i flaga własnej transakcji porządkują początek procedury

Procedura zapisuje wiersz do rejestru i wiersz do historii, a tabela błędów zbiera wpisy z bloku CATCH. Kolumna adresu e-mail jest krótsza niż parametr procedury, żeby dało się wymusić błąd obcięcia.

CREATE TABLE dbo.rejestr (id int IDENTITY PRIMARY KEY, email varchar(30) NOT NULL, opis varchar(50) NOT NULL);
CREATE TABLE dbo.rejestr_historia (id int IDENTITY PRIMARY KEY, id_rejestr int NOT NULL, zdarzenie varchar(10) NOT NULL);
CREATE TABLE dbo.log_bledow (id int IDENTITY PRIMARY KEY, kiedy datetime2 NOT NULL DEFAULT SYSDATETIME(),
    procedura sysname NULL, numer int NULL, linia int NULL, komunikat nvarchar(2048) NULL);
GO

Przykład ilustracyjny, dane wymyślone. Trzy tabele pomocnicze z przykładu.

Początek procedury robi dwie rzeczy. SET XACT_ABORT ON sprawia, że błąd wykonania w bloku TRY ustawia XACT_STATE() na -1, czyli transakcja jest nieodwołalna. Zmienna @wlasna przyjmuje 1 tylko wtedy, gdy @@TRANCOUNT w chwili wejścia wynosi 0. Procedura otwiera wtedy własną transakcję i tylko w tym przypadku ją zatwierdza albo wycofuje.

CREATE PROCEDURE dbo.rejestr_zapisz
    @email     varchar(100),
    @opis      varchar(50),
    @zdarzenie varchar(30) = 'przyjecie'
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;
    DECLARE @wlasna bit = CASE WHEN @@TRANCOUNT = 0 THEN 1 ELSE 0 END;
    BEGIN TRY
        IF @wlasna = 1 BEGIN TRANSACTION;
        INSERT dbo.rejestr (email, opis) VALUES (@email, @opis);
        DECLARE @id int = SCOPE_IDENTITY();
        INSERT dbo.rejestr_historia (id_rejestr, zdarzenie) VALUES (@id, @zdarzenie);
        IF @wlasna = 1 COMMIT TRANSACTION;
        SELECT @id AS id;
    END TRY
    BEGIN CATCH
        IF @wlasna = 1 AND XACT_STATE() <> 0 ROLLBACK TRANSACTION;
        IF XACT_STATE() <> -1
            INSERT dbo.log_bledow (procedura, numer, linia, komunikat)
            VALUES (ERROR_PROCEDURE(), ERROR_NUMBER(), ERROR_LINE(), ERROR_MESSAGE());
        THROW;
    END CATCH
END
GO

Przykład ilustracyjny, dane wymyślone. Procedura zapisu z flagą własnej transakcji i THROW w bloku CATCH.

Identyfikator pierwszego wiersza procedura odczytuje funkcją SCOPE_IDENTITY i przekazuje do zapisu w historii. Po zatwierdzeniu zwraca go zapytaniem SELECT @id AS id.

Przepływ procedury zapisu: TRY, błąd, w CATCH ROLLBACK, zapis do logu i THROW Przepływ procedury zapisu: TRY, błąd, w CATCH ROLLBACK, zapis do logu i THROW do wywołującego. Ścieżka poprawna kończy się sukcesem, ścieżka błędu przechodzi przez trzy kroki bloku CATCH, a CATCH bez THROW daje pozorny sukces. Procedura zapisu z TRY...CATCH: ROLLBACK, zapis do logu i THROW Po błędzie blok CATCH wycofuje własną transakcję, zapisuje błąd w logu i zgłasza go wywołującemu. SET XACT_ABORT ON @@TRANCOUNT = 0? tak nie BEGIN TRANSACTION, flaga własna = 1 bez własnej transakcji, flaga = 0 INSERT rejestr INSERT historia COMMIT (tylko własna) SELECT id wywołujący: sukces CATCH 1 ROLLBACK, jeśli własna i XACT_STATE() <> 0 2 zapis do log_bledow, jeśli XACT_STATE() <> -1 3 THROW; błąd błąd wywołujący: błąd 2628, @@TRANCOUNT = 0 poziom zgodności 150+; przy 130 błąd 8152 CATCH bez THROW wywołujący: pozorny sukces
Przepływ procedury zapisu: błąd w bloku TRY prowadzi do bloku CATCH z trzema krokami i do THROW dla wywołującego. Ścieżka bez THROW kończy się pozornym sukcesem.

Blok CATCH wycofuje transakcję, a potem zgłasza błąd dalej

W bloku CATCH pierwsza instrukcja wycofuje własną transakcję, gdy @wlasna wynosi 1, a stan transakcji jest różny od zera. Ostatnia instrukcja to THROW bez parametrów. Według dokumentacji działa on tylko w bloku CATCH, a instrukcja przed nim musi kończyć się średnikiem, więc w przykładzie wszystkie poprzednie instrukcje mają średnik.

Wywołujący dostaje oryginalny błąd z tą samą procedurą i tą samą linią. Błąd obcięcia danych ma numer 2628 w bazie z poziomem zgodności 150 lub wyższym (SQL Server 2019 i nowszy). W bazie z poziomem zgodności 130 ten sam błąd ma numer 8152, także na SQL Server 2022. Wersji starszych niż 2019 nie sprawdzaliśmy. Poziom zgodności ustawia się instrukcją ALTER DATABASE. Po błędzie @@TRANCOUNT wywołującego wynosi 0.

Błąd w drugim INSERT cofa także pierwszy. W osobnym teście liczba wierszy w tabeli głównej po takim wywołaniu była taka sama jak przed nim. Samo THROW niczego jednak nie cofa. Transakcję wycofuje ROLLBACK, a przy XACT_ABORT ON jest ona cofnięta albo nieodwołalna już z powodu samego błędu.

Składnię opisują strony dokumentacji o TRY...CATCH i o ROLLBACK TRANSACTION. Procedura API, która zamiast THROW zwraca błąd jako wynik w formacie JSON, wygląda inaczej: tak robi procedura dopisująca kartotekę, a błąd jako zwykły wynik opisuje także artykuł o API na procedurach SQL i JSON.

Wpis do tabeli błędów musi powstać po ROLLBACK

Wpis do tabeli błędów wykonany przed ROLLBACK znika razem z transakcją. W teście bez XACT_ABORT nie przybył żaden wpis, bo zapis został cofnięty razem z resztą. Przy XACT_ABORT ON taki zapis kończy się błędem 3930 i przesłania oryginalny błąd. Kolejność w CATCH jest więc stała: najpierw ROLLBACK, potem zapis do logu, na końcu THROW.

Drugi warunek dotyczy stanu transakcji. Przy XACT_ABORT ON błąd w bloku TRY ustawia XACT_STATE() na -1. W tym stanie zapis do logu kończy się błędem 3930 i przesłoniłby oryginalny błąd, dlatego procedura zapisuje do logu tylko wtedy, gdy stan jest różny od -1.

Komunikat błędu obcięcia na SQL Server 2022 zawiera nazwę tabeli i kolumny oraz początek wartości (w teście 30 znaków adresu e-mail). Zapis ERROR_MESSAGE() do logu zapisuje więc także fragment danych użytkownika. Przy poziomie zgodności 130 komunikat błędu 8152 nie zawiera ani nazwy tabeli i kolumny, ani wartości. Kto czyta tabelę błędów, powinien o tym wiedzieć przed ustaleniem, co wolno do niej zapisywać.

IDENTITY nie cofa się przy ROLLBACK. Po nieudanych zapisach kolejny udany zapis dostaje numer wyższy niż liczba wierszy w tabeli (w teście numer 6 przy jednym wierszu po czterech nieudanych próbach).

Wywołanie z cudzej transakcji nie może jej cofać za plecami wywołującego

Wersja naiwna, która zawsze wykonuje BEGIN TRANSACTION i ROLLBACK, wywołana z cudzej transakcji cofa transakcję wywołującego. Licznik @@TRANCOUNT spada do 0, a późniejszy COMMIT wywołującego kończy się błędem 3902. Wersja bez THROW kończy się błędem 266, czyli niezgodną liczbą instrukcji BEGIN i COMMIT.

Wersja z flagą zachowuje się inaczej. Nie wykonuje ROLLBACK, bo transakcja nie jest jej własna, a wywołujący w swoim CATCH widzi XACT_STATE() = -1 i sam wycofuje transakcję. Cały zapis, także pierwszego udanego wywołania, zostaje cofnięty. Przy XACT_ABORT ON wywołujący musi więc wycofać transakcję, a dane z niej przepadają.

Zapisu do logu w stanie -1 nie wymuszamy. Procedura go pomija, a wywołujący może zapisać błąd po własnym ROLLBACK. Ostrożność wobec transakcji w skrypcie wdrożeniowym pokazuje artykuł o zmianie zapytań z trybem próbnym i ROLLBACK.

Test z za długą wartością pokazuje różnicę po stronie wywołującego

Test wywołuje procedurę dwa razy. Pierwsze wywołanie ma adres e-mail mieszczący się w kolumnie. Drugie ma adres dłuższy niż 30 znaków, więc zapis kończy się błędem obcięcia, który blok CATCH w teście przechwytuje po stronie wywołującego. Na końcu zapytanie kontrolne liczy wiersze w trzech tabelach i pokazuje @@TRANCOUNT.

EXEC dbo.rejestr_zapisz @email = 'klient.testowy@example.test', @opis = 'przyklad';
BEGIN TRY
    EXEC dbo.rejestr_zapisz @email = 'bardzo.dlugi.adres.klienta.testowego@example.test', @opis = 'x';
END TRY
BEGIN CATCH
    SELECT ERROR_NUMBER() AS numer, ERROR_PROCEDURE() AS procedura, ERROR_LINE() AS linia;
END CATCH
SELECT (SELECT COUNT(*) FROM dbo.rejestr) AS rejestr,
       (SELECT COUNT(*) FROM dbo.rejestr_historia) AS historia,
       (SELECT COUNT(*) FROM dbo.log_bledow) AS log_bledow,
       @@TRANCOUNT AS trancount;

Przykład ilustracyjny, dane wymyślone. Wywołanie testowe z wymuszonym błędem i zapytanie kontrolne.

Na SQL Server 2019 lub nowszym, w bazie z poziomem zgodności 150 lub wyższym, pierwsze wywołanie zwraca id = 1. Blok CATCH zwraca 2628 | dbo.rejestr_zapisz | 12, a linia 12 to pierwszy INSERT w procedurze. Zapytanie kontrolne zwraca 1 | 1 | 1 | 0, czyli po jednym wierszu w każdej z trzech tabel i brak otwartej transakcji. Przy poziomie zgodności 130 numer błędu to 8152.

Wynik zapytania kontrolnego po błędzie w procedurze zapisu Makieta poglądowa, dane przykładowe. Wynik zapytania kontrolnego: liczby wierszy w trzech tabelach, @@TRANCOUNT i ostatni wpis z logu błędów. Wyniki Zapytanie kontrolne Zapytanie kontrolne SELECT (SELECT COUNT(*) FROM dbo.rejestr) AS rejestr,        (SELECT COUNT(*) FROM dbo.rejestr_historia) AS historia,        (SELECT COUNT(*) FROM dbo.log_bledow) AS log_bledow,        @@TRANCOUNT AS trancount; po wywołaniu z za długim adresem e-mail (przykład, baza z poziomem zgodności 150 lub wyższym) rejestr historia log_bledow trancount 1 1 1 0 zapis z pierwszego wywołania historia pierwszego wywołania wpis z błędu brak otwartej transakcji Blok CATCH zwrócił numer procedura linia 2628 dbo.rejestr_zapisz 12 wiersz pierwszego INSERT w procedurze Makieta poglądowa, dane przykładowe
Makieta poglądowa, dane przykładowe: wynik zapytania kontrolnego po wywołaniu z za długim adresem e-mail.
Cztery warianty bloku CATCH i ich skutki dla wywołującego
Wariant bloku CATCHBłąd u wywołującegoDane po błędzieWpis w tabeli błędów
Tylko ROLLBACKbrakcofniętebrak
Zapis do logu przed ROLLBACK, bez XACT_ABORT (z THROW)błąd 2628cofniętebrak (cofnięty z transakcją; przy XACT_ABORT ON ten zapis daje błąd 3930)
Zapis do logu po ROLLBACK (z THROW)błąd 2628cofniętejest
Wersja z flagą wywołana z cudzej transakcjibłąd 2628, XACT_STATE() = -1cofnięte razem z transakcją wywołującegobrak (zapis pominięty w stanie -1)

Numery błędu dotyczą bazy z poziomem zgodności 150 lub wyższym. Przy poziomie 130 w miejscu 2628 stoi 8152.

Przykład: formularz zgłoszeń woła procedurę zapisu dla klienta Klient Testowy i numeru RMA 7C3A-1E94-B20F, ale adres e-mail jest dłuższy niż kolumna w tabeli. W wersji, w której CATCH robi tylko ROLLBACK, formularz pokazuje komunikat o zapisie, choć tabela jest pusta, a zgłoszenie 2026-10-0007 nigdy nie powstaje. Po dodaniu THROW; formularz dostaje błąd 2628 (przy poziomie zgodności 150 lub wyższym) i może poprosić o poprawienie adresu.

W tabeli błędów zostaje wpis, który wskazuje błąd i miejsce jego wystąpienia, więc programista widzi przyczynę bez przeszukiwania logów aplikacji. Gdy procedura zostanie później wywołana z większej transakcji innej procedury, cały zapis cofa się razem z nią, a wywołujący sam decyduje, co dalej. Od strony użytkownika kolejkę zgłoszeń z Internetu i pierwszy zapis pracownika opisuje strona o zgłoszeniu z Internetu, które nie jest jeszcze dokumentem.

Wzorzec nie był testowany pod kątem zakleszczeń, ponowień prób ani transakcji rozproszonych. Pokrewne wzorce to podstawianie pól w szablonie wiadomości, gdzie błędy tokenu zgłasza THROW, oraz wąska lista z zapasowym zbiorem.

Cztery pytania przed budową własnej procedury zapisu

Przed napisaniem własnej procedury zapisu odpowiedz na cztery pytania.

  • Aplikacja: czy wywołująca procedurę aplikacja sprawdza błąd, czy zakłada, że brak wyjątku oznacza sukces.
  • Transakcja wywołującego: czy procedura bywa wywoływana z transakcji innej procedury lub aplikacji.
  • Zawartość logu: co wolno zapisywać do tabeli błędów, skoro komunikat może zawierać fragment danych użytkownika.
  • Czytelnik logu: kto czyta tabelę błędów i po jakim czasie dowiaduje się o nowym wpisie.
FAQ

Najczęstsze pytania o TRY CATCH i THROW w procedurze zapisu

01

Czy po ROLLBACK w bloku CATCH trzeba jeszcze zgłosić błąd?

Tak. Blok CATCH, który wykonuje tylko ROLLBACK, nie zgłasza wywołującemu żadnego błędu: w teście tabela została pusta, a sqlcmd -b zakończył skrypt kodem 0. Wersja z THROW; kończy skrypt kodem 1 i komunikatem, który wskazuje numer błędu oraz procedurę wraz z linią.

02

Dlaczego zapis do tabeli błędów musi stać po ROLLBACK?

Wpis wykonany przed ROLLBACK znika razem z transakcją. Przy XACT_ABORT ON taki zapis kończy się błędem 3930 i przesłania oryginalny błąd. Dlatego kolejność jest stała: najpierw ROLLBACK, potem zapis do logu, na końcu THROW.

03

Co oznacza XACT_STATE() równe -1?

Transakcja jest nieodwołalna: przy XACT_ABORT ON błąd w bloku TRY ustawia ten stan. Zapis do logu kończy się w nim błędem 3930 i przesłoniłby oryginalny błąd, dlatego procedura zapisuje do logu tylko wtedy, gdy stan jest różny od -1.

04

Co się stanie, gdy procedura z ROLLBACK zostanie wywołana z cudzej transakcji?

Wersja naiwna cofa transakcję wywołującego, więc @@TRANCOUNT spada do 0, a późniejszy COMMIT kończy się błędem 3902. Wersja z flagą nie wykonuje ROLLBACK, a wywołujący widzi XACT_STATE() = -1 i sam wycofuje transakcję razem z całym zapisem.

05

Jak sprawdzić, że procedura zgłasza błąd, a nie ukrywa go?

Wywołać ją z wartością, która wymusza błąd, i sprawdzić trzy rzeczy: czy wywołujący dostał wyjątek, jakie liczby wierszy zwraca zapytanie kontrolne i jaki kod wyjścia ma sqlcmd -b. Kod 0 przy pustej tabeli oznacza ukryty błąd.

Słownik pojęć

Słownik pojęć

Pojęcia z kodu i przykładu opisanych w artykule.

TTHROW
Instrukcja T-SQL zgłaszająca błąd. Bez parametrów w bloku CATCH zgłasza ponownie oryginalny błąd, z tą samą procedurą i linią; sama niczego nie cofa.
XXACT_ABORT
Opcja sesji, przy której błąd wykonania przerywa transakcję. W procedurze zapisu z przykładu ustawia stan transakcji na -1 po błędzie w bloku TRY.
XXACT_STATE
Funkcja zwracająca stan transakcji: 0 oznacza brak transakcji, a -1 transakcję nieodwołalną. Blok CATCH w przykładzie sprawdza ją przed ROLLBACK i przed zapisem do logu.
@@@TRANCOUNT
Zmienna zwracająca liczbę otwartych transakcji. Procedura z flagą otwiera własną transakcję tylko wtedy, gdy w chwili wejścia wynosi 0.
TTransakcja nieodwołalna
Transakcja w stanie XACT_STATE() = -1, której nie można zatwierdzić, a tylko wycofać. Zapis do logu w tym stanie kończy się błędem 3930.
TTabela błędów
Tabela, do której blok CATCH zapisuje numer błędu, procedurę, linię i komunikat. Wpis musi powstać po ROLLBACK, bo wcześniejszy ginie z transakcją.
PPoziom zgodności bazy
Ustawienie bazy wpływające na zachowanie silnika. Numer błędu obcięcia danych to 2628 przy poziomie 150 lub wyższym, a przy 130 numer 8152.
IIDENTITY
Właściwość kolumny nadająca kolejne numery. Nie cofa się przy ROLLBACK, więc po nieudanych zapisach numer jest wyższy niż liczba wierszy w tabeli.

Zobacz to w praktyce

Sprawdź, jak systemy SoftwareStudio oparte na MS SQL Server wspierają ten obszar Twojej firmy.