Header RSS-подписка на обновления сайта eMail-подписка на обновления сайта

Блоки try-catch для программистов на T-SQL. Часть 2/3.

Уровни серьезности и ошибки компиляции.


366bef3a

Давайте, собственно, с severity level, а точнее с их возможных значений и начнем. Как покажут наши дальнейшие исследования, при работе с блоками TRY/CATCH весь диапазон допустимых серьезностей (а это, напомню, все целые числа в диапазоне 0...25 включая обе границы) разбивается на три группы:

  • до 10 включительно
  • от 11 до 19 включительно
  • от 20 до 25 включительно

Поведение TRY/CATCH будет принципиально различным для каждой из трех групп. Давайте смотреть.

Начнем с группы средней, в нем поведение изучаемой конструкции самое предсказуемое:

1
2
3
4
5
6
7
8
9
10
11
PRINT '--Start--'
BEGIN TRY
    PRINT 'TRY >> Start'
    RAISERROR ('**Exception in TRY**', 11, 1)
    PRINT 'TRY >> End'
END TRY
BEGIN CATCH
    PRINT 'CATCH >> Start >> message: '+error_message()
    PRINT 'CATCH >> End'
END CATCH
PRINT '--End--'

Результат:

--Start--
TRY >> Start
CATCH >> Start >> message: **Exception in TRY**
CATCH >> End
--End--

Комментировать особо нечего, все ожидаемо: начинается исполнение пакета → блок TRY начинает свое исполнение → ошибка → блок CATCH начинает свое исполнение и выполняет все свои инструкции → исполнение пакета (batch) продолжается первой инструкцией после метки END CATCH. Все абсолютно точно в согласии с той теорией изложением которой открылся данный труд. Единственное замечание: в показанном скрипте вы можете вместо уровня серьезности 11 выставить любой уровень вплоть до 18-ти включительно и результат будет тем же байт-в-байт. Можно выставить и уровень 19, он, как отмечалось выше, тоже относится к «средней» группе, а потому и при нем результат будет идентичным. Но вот саму строчку «поднимающую» ошибку (строка 4 показанного скрипта) придется чуть дополнить:

4
RAISERROR ('**Exception in TRY**', 19, 1) WITH LOG

Применение опции WITH LOG продиктовано простым правилом: начиная с уровня серьезности 19 и выше все ошибки без исключения могут «кидаться» только с указанной опцией. Просто потому что с 19-го уровня «игры в песочек» заканчиваются, и каждая ошибка, скорее всего, будет последней вещью которую сделает сервер перед тем как принудительно отключить от себя клиента а то и просто остановиться. А такие ошибки достойны не только внимания отдельного клиента, но и занесения в error log данного экземпляра. Ну и еще в application log на уровне OS Windows, до кучи. И опция WITH LOG, помимо стандартной отправки текста ошибки клиенту, делает еще и две указанные вещи: пишет тот же текст в один лог, и в другой лог. Однако на течение кода, повторюсь, сей факт не влияет совершенно: какие пять строк выводила нам ошибка с уровнем 11, те же пять строк выводит и ошибка с уровнем 19.

Теперь давайте переместимся к первой группе уровней и сменим серьезность на, допустим, 10, не трогая остальной код скрипта. Тогда на выходе у нас:

--Start--
TRY >> Start
**Exception in TRY**
TRY >> End
--End--

Что мы видим? Что, вроде как, и RAISERROR отработал, но и в CATCH мы не «свалились». Да, именно! А это все потому, что ошибки до 10-го уровня включительно — не ошибки. И уж тем более не исключения. А информационные сообщения. О которых нужно, собственно, информировать клиента, то есть послать ему текст такой «псевдо-ошибки» и, иногда, код и состояние ее же. А в CATCH уходить не надо — ошибки-то нет!

И, наконец, ошибки третьей группы. Исправляем уровень серьезности на 20, не забыв, разумеется, включить опцию WITH LOG и получаем:

--Start--
TRY >> Start
Msg 2745, Level 16, State 2, Line 4
Process ID 52 has raised user error 50000, severity 20. SQL Server is terminating this process.
Msg 50000, Level 20, State 1, Line 4
**Exception in TRY**
Msg 0, Level 20, State 0, Line 0
A severe error occurred on the current command.  The results, if any, should be discarded.

И мало того, если посмотреть на ярлык той закладки редактора в которой мы только что исполняли наши скрипты то мы увидим, что она not connected. И вновь такое поведение полностью запланировано, ведь ошибки уровня 20-го и выше считаются фатальными (fatal) и сервер безусловно рвет соединение с тем клиентом, в чьей сессии произошла подобная фатальная ошибка. Причем рвется соединение тут же, по обнаружении ошибки. Никакого шанса на ее корректировку сервер не оставляет.

Итого, как это ни странно, но ошибки вроде бы находящиеся на разных полюсах серьезности (до 10-го уровня и от 20-го) обрабатываются блоком TRY/CATCH идентично, а именно — никак не обрабатываются. Только причины тому совершенно разные. В одном случае это недостаточная серьезность что бы прерывать течение основного алгоритма, в другом — серьезность избыточная, в силу которой исполнение прерывается слишком рано и слишком грубо. И, таким образом, только ошибки из диапазона серьезности 11-19 могут извлечь какой-то «профит» из обсуждаемой конструкции. Справедливости ради заметим, что большинство «ожидаемых» ошибок (обозначим их так) вполне укладывается в этот диапазон. Да, собственно говоря, там по большому счету только уровень 16 играет решающую роль, прочие уровни серьезности погоды не делают. Допустим всего сервер версии 2008R2 нам может вернуть 76 различных ошибок уровня 11. Или 69 ошибок уровня 14. Или 288 уровня 15. А ошибок уровней 12 и 13 так и вовсе по одной (!) на каждый уровень. А вот вот ошибок уровня 16 — 6400, круглым счетом. Отсюда понятно, почему новая инструкция THROW упомянутая чуть ранее решила более никакой серьезностью не морочиться, а все пользовательские ошибки складывать в этот же «мейнстрим». Тем не менее, для нас важен лишь вот такой промежуточный итог:

Конструкция TRY/CATCH языка T-SQL способна обработать только ошибки уровня серьезности от 11 до 19 включительно.

Теперь давайте вот такой скрипт разберем:

1
2
3
4
5
6
7
8
9
10
11
PRINT '--Start--'
BEGIN TRY
    PRINT 'TRY >> Start'
    SELLECT * TableThatNotExist
    PRINT 'TRY >> End'
END TRY
BEGIN CATCH
    PRINT 'CATCH >> Start >> message: '+error_message()
    PRINT 'CATCH >> End'
END CATCH
PRINT '--End--'

В нем, как легко заметить, две проблемы: таблицы TableThatNotExist, как можно предположить из названия, не существует, и слово SELLECT может быть чем угодно, но только не инструкцией языка T-SQL. Там, конечно, еще ключевое слово FROM пропущено, но это уж сущие мелочи. Пробуем исполнить:

1
2
Msg 102, Level 15, State 1, Line 4
Incorrect syntax near 'SELLECT'.

Очевидно что не только не срабатывает TRY/CATCH, но и пакет (batch) как таковой не начинает исполняться. Тут все дело в том, что показанная ошибка — не ошибка времени исполнения, она — времени компиляции пакета. Убедиться в том очень легко — достаточно в студии нажать Ctrl+F5, а не просто F5, или, что эквивалентно, нажать на панели инструментов кнопку Parse. Сообщение будет тем же самым, хотя выполнение в таком случае и начаться не могло. Так вот что бы пакет как целое в принципе мог стартовать, абсолютно все входящие в его состав операторы должны быть валидны, или, если вы предпочитаете практические умозаключения, нажатие Ctrl+F5 должно возвращать:

Command(s) completed successfully.

В этом, и только в этом случае исполнение начнется и у TRY/CATCH появится хотя бы потенциальная возможность проявить себя. Ну а поскольку в показанном примере наш batch даже не стартует — чего же мы ждем? В то же время не надо думать, что ошибка компиляции это некая грандиозная ошибка с уровнем серьезности 5 тысяч. Да нет, как показывает сообщение чуть выше серьезность у нее самая обычная, 15-я. «Видали и покруче», как говорится. И поэтому TRY/CATCH перехватить такую ошибку вполне может (тем паче, что она вполне укладывается в требуемый диапазон 11-19), важно лишь что бы этот блок уже работал, в то время как будет происходить эта ошибка. Такая диспозиция возможна лишь в одном случае: блок TRY/CATCH находится на более высоком уровне исполнения, нежели пакет содержащий синтаксическую ошибку. Непонятно? Вот вам пример:

1
2
3
4
5
6
7
8
9
10
11
12
13
PRINT '--Start--'
BEGIN TRY
    PRINT 'TRY >> Start'
    DECLARE @n nvarchar(100)
    SET @n='SELLECT * TableThatNotExist'
    EXEC (@n)
    PRINT 'TRY >> End'
END TRY
BEGIN CATCH
    PRINT 'CATCH >> Start >> message: '+error_message()
    PRINT 'CATCH >> End'
END CATCH
PRINT '--End--'

Результат:

--Start--
TRY >> Start
CATCH >> Start >> message: Incorrect syntax near '*'.
CATCH >> End
--End--

В данном случае компиляция исходного пакета проблем не вызывает, переменная @n есть просто строка могущая содержать совершенно любую ересь, компилятор ее содержимое не анализирует. Так что пакет компилируется, блок TRY/CATCH включается в работу, и лишь после этого выясняется что указанная переменная должна содержать вовсе не ересь, а валидный T-SQL код. Ключевое слово тут — после этого. Блок уже работает, и лишь тут случается компиляция операторов уровнем ниже, например потому что эти операторы оформлены в виде динамического SQL. Ошибка компиляции прекрасно перехватывается и мы можем делать с нею все что захотим.

Хорошо, давайте исправим весь синтаксис, но несуществующую таблицу оставим:

1
2
3
4
5
6
7
8
9
10
11
PRINT '--Start--'
BEGIN TRY
    PRINT 'TRY >> Start'
    SELECT * FROM TableThatNotExist
    PRINT 'TRY >> End'
END TRY
BEGIN CATCH
    PRINT 'CATCH >> Start >> message: '+error_message()
    PRINT 'CATCH >> End'
END CATCH
PRINT '--End--'

Результат:

--Start--
TRY >> Start
Msg 208, Level 16, State 1, Line 4
Invalid object name 'TableThatNotExist'.

Хм. Вроде как и блок TRY начал исполняться, но все же ошибка не наша, не из CATCH-а. Обычная, системная ошибка. Которая, кстати, прервала исполнение всего пакета, а если бы мы «свалились» бы в CATCH, то был бы напечатан по крайней мере финальный --End--. Да, здесь уже проблема упирается не в компиляцию, Ctrl+F5 подтверждает нам что с нею-то все в порядке. Тут все крутится вокруг иного механизма — отложенного разрешения имен (deferred name resolution), который в целом является «тем еще» механизмом. Дело в том, что движок занимаясь парсингом текста в пакете и натыкаясь на несуществующий объект (как раз наш случай) не засчитывает автоматически сложившуюся ситуацию как ошибочную. И никаких предупреждений на этапе компиляции вы не получаете. Движок справедливо (по крайней мере с точки зрения команды разработчиков SQL Server это — справедливо, хотя автор строк читаемых вами в настоящий момент готов с ними поспорить) полагает, что течение кода в пакете может быть построено таким хитрым образом, что когда дело дойдет до реального обращения к объекту последний существовать уже будет! Допустим одна из строк пакета может вызвать хранимую процедуру, которая и создаст требуемый объект. Так что парсер оставляет нашу TableThatNotExist в покое на некоторое время, «допарсивает» остальные строки пакета, и, поскольку уж с ними-то вообще полный OK, дает всему пакету «отмашку» на исполнение. Начинает исполняться пакет → начинает исполняться блок TRY → и тут — SELECT. Со все еще несуществующей таблицей. Вот тут-то случается финальное разрешение имен которое никакой однозначности не допускает. И вновь по «условиям игры», TRY/CATCH не способен перехватить ошибки такого финального разрешения имен если оно происходит на одном уровне исполнения с указанным блоком. У нас TRY/CATCH исполняется в пакете и в нем же происходит разрешение имен — полный провал, блок бесполезен. Однако, как вновь не сложно заметить, ошибка разрешения имен самая обычная, 16-го уровня серьезности, так что... шансы есть. Попробуем?

1
2
3
4
5
6
7
8
9
10
11
12
13
PRINT '--Start--'
BEGIN TRY
    PRINT 'TRY >> Start'
    DECLARE @n nvarchar(100)
    SET @n='SELECT * FROM TableThatNotExist'
    EXEC (@n)
    PRINT 'TRY >> End'
END TRY
BEGIN CATCH
    PRINT 'CATCH >> Start >> message: '+error_message()
    PRINT 'CATCH >> End'
END CATCH
PRINT '--End--'

И в этот раз:

--Start--
TRY >> Start
CATCH >> Start >> message: Invalid object name 'TableThatNotExist'.
CATCH >> End
--End--

А все потому, что динамический SQL определяет свой, отдельный пакет в котором все свое: своя компиляция, свое отложенное разрешение имен, свое финальное разрешение имен и т.д. И блок TRY/CATCH оказывается «сверху» всех этих «своих». Отсюда и результат, которого, кстати, можно добиться и вот как еще:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
USE tempdb
go
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.ROUTINES  WHERE SPECIFIC_NAME = 'A') DROP PROCEDURE A
go

CREATE PROCEDURE A
AS
    SELECT * FROM TableThatNotExist
GO

PRINT '--Start--'
BEGIN TRY
    PRINT 'TRY >> Start'
    EXEC A
    PRINT 'TRY >> End'
END TRY
BEGIN CATCH
    PRINT 'CATCH >> Start >> message: '+error_message()
    PRINT 'CATCH >> End'
END CATCH
PRINT '--End--'

Совершенно аналогично, финальное разрешение имен происходит на уровень ниже, чем тот на котором работает TRY/CATCH. Последний прекрасно себя чувствует и оказывается очень даже «при делах». Хранимая процедура представляет собой набор инструкций исполняемых в отдельном пакете, а TRY/CATCH работают в пакете уровнем выше. В целом же, автор блога согласен с резкой критикой отечественных и зарубежных SQL-профессионалов в адрес отложенного разрешения имен, позволяющего указывать несуществующие таблицы. Нельзя сказать, что у данного подхода 100% минусов и ни единого плюса. Есть и они, конечно. Но недостатки значительно перевешивают достоинства, на что было указано неоднократно. Для борьбы с ними, как минимум, можно было бы начать с опции с помощью которой программист на языке T-SQL мог бы сам себе перекрывать доступ к обсуждаемой «фиче» сомнительной ценности, и требовать от себя же безусловного наличия всех объектов на которые он ссылается в момент парсинга таких ссылок. Но пока — увы, увы — «фича» неотключаема, «радует» нас всех, вне зависимости рады вы ей или нет, и, похоже, вообще не собирается уходить на заслуженный (?) отдых. Так что сейчас и в обозримом будущем нет иного выхода кроме как знать про нее и про кучу совсем неочевидных нюансов вытекающих из факта ее существования. Пару пунктов из этой кучи неочевидностей мы рассмотрели только что в контексте нашего разговора, хотя в целом это тема отдельной заметки если не статьи.

Вложенные блоки TRY/CATCH и блоки TRY/CATCH различных областей.

Вполне ожидаемо, обсуждаемая нами конструкция может быть вложена. Иными словами, любая инструкция в любом из двух блоков может быть, на самом деле, составной инструкцией выраженной через внутренний блок TRY/CATCH. А у этого внутреннего блока может быть свой, еще более вложенный блок, и т.д. Еще раз подчеркиваю (почему-то многих SQL-программистов на этом месте начинают терзать сомнения) — любая инструкция в любом из блоков (сомнения наших программистов связаны, разумеется, с CATCH блоком, так вот и в нем тоже можно!) может быть отдельной парой TRY/CATCH. О вложенности в блок CATCH мы поговорим в следующем разделе, а этот посвятим полностью вложениям в блок TRY. Начнем с примера:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
PRINT '--Start--'
BEGIN TRY --Lvl 1
    PRINT 'TRY_1 >> Start'
    BEGIN TRY --Lvl 2
        PRINT 'TRY_2 >> Start'
        BEGIN TRY --Lvl 3
            PRINT 'TRY_3 >> Start'
            RAISERROR ('**Exception in TRY_3**', 11, 1)
            PRINT 'TRY_3 >> End'
        END TRY
        BEGIN CATCH
            PRINT 'CATCH_3 >> Start >> message: '+error_message()
            PRINT 'CATCH_3 >> End'
        END CATCH --Lvl 3
        RAISERROR ('**Exception in TRY_2**', 11, 1)
        PRINT 'TRY_2 >> End'
    END TRY
    BEGIN CATCH
        PRINT 'CATCH_2 >> Start >> message: '+error_message()
        PRINT 'CATCH_2 >> End'
    END CATCH --Lvl 2
    RAISERROR ('**Exception in TRY_1**', 11, 1)
    PRINT 'TRY_1 >> End'
END TRY
BEGIN CATCH
    PRINT 'CATCH_1 >> Start >> message: '+error_message()
    PRINT 'CATCH_1 >> End'
END CATCH --Lvl 1
PRINT '--End--'

Результат вполне ожидаем:

--Start--
TRY_1 >> Start
TRY_2 >> Start
TRY_3 >> Start
CATCH_3 >> Start >> message: **Exception in TRY_3**
CATCH_3 >> End
CATCH_2 >> Start >> message: **Exception in TRY_2**
CATCH_2 >> End
CATCH_1 >> Start >> message: **Exception in TRY_1**
CATCH_1 >> End
--End--

Иными словами, как мы того и ждем интуитивно, исключение произошедшее в блоке TRY ловится тем блоком CATCH, что составляет пару к предыдущему. Вообще-то, все происходит настолько очевидно, что можно было бы и не тратить время на пример если бы он не был переходным звеном к теме близкой к вложенным блокам, но все-таки теме отдельной — TRY/CATCH блоки различных областей. Вот она безусловно заслуживает отдельного разбирательства.

Разумеется, сначала выясним доподлинно — а что такое область? Это, по счастью, совсем просто: область, она же scope, есть отдельный программный модуль, то есть или хранимая процедура, или триггер, или функция или пакет (batch). Если области никак друг с другом не связаны и используют в своем коде блоки TRY/CATCH то ничего нового помимо уже сказанного добавить мы не можем: TRY в каждом таком модуле будет (возможно) «бросаться» исключениями, кои и будут успешно перехватываться парным ему блоком CATCH. Самые «интересности» начинаются когда один модуль в своем коде вызывает другой, а тот — третий и при этом часть модулей написано с привлечением технологии обработки ошибок, а часть — без нее. Снова смотрим:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
USE tempdb
go
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.ROUTINES  WHERE SPECIFIC_NAME = 'A') DROP PROCEDURE A
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.ROUTINES  WHERE SPECIFIC_NAME = 'B') DROP PROCEDURE B
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.ROUTINES  WHERE SPECIFIC_NAME = 'C') DROP PROCEDURE C
GO

CREATE PROCEDURE C
AS
    PRINT 'C >> --Start--'
    BEGIN TRY
        PRINT 'C >> TRY >> Start'
        RAISERROR ('**Exception in C**', 11, 1)
        PRINT 'C >> TRY >> End'
    END TRY
    BEGIN CATCH
        PRINT 'C >> CATCH >> Start >> message: '+error_message()
        PRINT 'C >> CATCH >> End'
    END CATCH
    PRINT 'C >> --End--'
GO
CREATE PROCEDURE B
AS
    PRINT 'B >> --Start--'
    BEGIN TRY
        PRINT 'B >> TRY >> Start'
        EXECUTE C
        PRINT 'B >> TRY >> End'
    END TRY
    BEGIN CATCH
        PRINT 'B >> CATCH >> Start >> message: '+error_message()
        PRINT 'B >> CATCH >> End'
    END CATCH
    PRINT 'B >> --End--'
GO
CREATE PROCEDURE A
AS
    PRINT 'A >> --Start--'
    BEGIN TRY
        PRINT 'A >> TRY >> Start'
        EXECUTE B
        PRINT 'A >> TRY >> End'
    END TRY
    BEGIN CATCH
        PRINT 'A >> CATCH >> Start >> message: '+error_message()
        PRINT 'A >> CATCH >> End'
    END CATCH
    PRINT 'A >> --End--'
GO

EXECUTE A
GO

Итог:

A >> --Start--
A >> TRY >> Start
B >> --Start--
B >> TRY >> Start
C >> --Start--
C >> TRY >> Start
C >> CATCH >> Start >> message: **Exception in C**
C >> CATCH >> End
C >> --End--
B >> TRY >> End
B >> --End--
A >> TRY >> End
A >> --End--

Анализируем: у нас три модуля (иными словами — три области) — хранимые процедуры A,B,C, причем A вызывает B, а та, в свою очередь, C. Каждая процедура берет на вооружение перехват ошибок и вызов указанных процедур случается только внутри блоков TRY. Проблемным является код самой «вложенной» процедуры — C, именно он «кидается» исключением. Однако поскольку блоки TRY/CATCH той же самой процедуры C ошибку обрабатывают и подавляют — никто из «вызывателей» никогда не узнает что проблемы имели место быть. Процедуры A и B полагают что у нас «полный OK» и спокойно исполняют все свои инструкции, как внутри блоков TRY так и за их пределами. Теперь изменим код процедуры C отказавшись от обработки ошибок в ее пределах:

1
2
3
4
5
6
7
8
...
CREATE PROCEDURE C
AS
    PRINT 'C >> --Start--'
    RAISERROR ('**Exception in C**', 11, 1)
    PRINT 'C >> --End--'
GO
...

Тогда:

A >> --Start--
A >> TRY >> Start
B >> --Start--
B >> TRY >> Start
C >> --Start--
B >> CATCH >> Start >> message: **Exception in C**
B >> CATCH >> End
B >> --End--
A >> TRY >> End
A >> --End--

Обратите внимание — исключение все-равно перехвачено! Только озаботилась этим процедура «вышестоящая», B. Однако в отличии от предыдущего варианта это не прошло бесследно для нее — блок TRY процедуры B не смог полностью завершиться. Иными словами, B узнала о проблемах в C и, образно говоря, это стали «ее проблемы». Строка EXECUTE C стала последней выполненной в блоке TRY процедуры B. Однако процедура A по-прежнему считает что у нас «полный OK». Попробуем оставив процедуру C в ее текущей редакции произвести аналогичные изменения в процедуре B:

1
2
3
4
5
6
7
8
...
CREATE PROCEDURE B
AS
    PRINT 'B >> --Start--'
    EXECUTE C
    PRINT 'B >> --End--'
GO
...

Новый тест:

A >> --Start--
A >> TRY >> Start
B >> --Start--
C >> --Start--
A >> CATCH >> Start >> message: **Exception in C**
A >> CATCH >> End
A >> --End--

Исключение все еще перехватывается но уже на самом верхнем уровне (читай — области) — в процедуре A. Теперь уже ни одна из процедур не может говорить про OK, все три в той или иной степени «пострадали» от проблем процедуры C. Наконец — уберем обработку ошибок и в A тоже:

1
2
3
4
5
6
7
8
...
CREATE PROCEDURE A
AS
    PRINT 'A >> --Start--'
    EXECUTE B
    PRINT 'A >> --End--'
GO
...

Финальный тест:

A >> --Start--
B >> --Start--
C >> --Start--
Msg 50000, Level 11, State 1, Procedure C, Line 5
**Exception in C**
C >> --End--
B >> --End--
A >> --End--

Теперь об ошибке узнали «на самом верху» — в клиенте (в нашем случае — в студии). Поскольку блоки TRY не используются и ошибка не является фатальной (уровень серьезности меньше 20) сами хранимые процедуры выполняются до конца. Если бы ошибка была бы не эмулируема с помощью RAISERROR, а была бы настоящей системной ошибкой вроде ошибки конвертации типов, исполнение всего скрипта прервалось на этой самой проблемной строке кода. Однако видя «нестрашную» пользовательскую ошибку с кодом 50000 движок разрешает коду продолжать свое исполнение. Отметим, что блок TRY/CATCH совладал бы и с более серьезной системной ошибкой вроде упомянутой ошибки конвертации, но при условии, разумеется, если серьезность такой системной ошибки будет меньше 20.

Какое же умозаключение можно вывести из серии последних опытов? Умозаключение такое: движок сервера пытается найти обработчик в каждой из областей, двигаясь от области самой «внутренней» (где, собственно, ошибка и была замечена), к области самой «внешней». Первый же найденный обработчик будет «назначен» ответственным за перехват исключения. Если ни одна из областей, включая самую «внешнюю», с которой началось исполнение кода, не смогла предоставить обработчик, то ошибка безусловно отправляется клиенту, а код или продолжает свое исполнение или прекращает исполнение немедленно, в зависимости от типа и серьезности каждой конкретной ошибки. Для программистов языков высокого уровня все предыдущие предложения текущего абзаца можно было заменить фразой «стек вызовов раскручивается (unwind the call stack) до обнаружения первого блока CATCH», и они бы все поняли, но мы-то пишем материал для программистов на T-SQL, верно? ;) А поэтому — не будем экономить на пояснениях, с задачей подобной экономии уже прекрасно справилось множество авторов до нас.

Итак, мы убедились, что герой нашего повествования весьма гибок и эластичен: его можно «запихивать» внутрь себя же как непосредственно, в границах одной области исполнения, так и делать тоже самое косвенно, если мы имеем дело с несколькими областями. И в том, и в другом случае вложенные TRY/CATCH ведут себя похоже, однако, как говорится, «есть нюансы».

Вложение блоков TRY/CATCH в блок CATCH предыдущего уровня.

Как было обещано в предыдущем разделе, теперь мы отдельно разберем специфику блоков вложенных в блок CATCH. Вы уже знаете, что такое вполне возможно технически и синтаксически, но — зачем? Почему мы вообще можем захотеть городить подобные «многоэтажные» конструкции во втором из пары блоков? Дело тут в следующем.

Строго говоря, блок CATCH предназначен исключительно для обработки ошибок. То есть провели логирование произошедшего, возможно сформировали для клиента «подменное» сообщение, отправили его «наверх», и — все, END CATCH. И говоря не менее строго все операторы обсуждаемого блока не должны допускать возникновения ошибок даже теоретически. Можно и так сказать: идеальный блок CATCH содержит операторы PRINT и никаких других. И тогда блок CATCH будет при любых обстоятельствах 100% работоспособен. А почему нам так важно исключить свои ошибки в данном блоке? А потому, что раз мы в нем оказались, то у нас уже есть «ошибка 1» из блока TRY. Предлагаете «лакировать» ее сверху «ошибкой 2» из блока CATCH? Ну да, наш условный T-SQL программист обожает «шарады с подтекстом», ага. Поэтому если у CATCH нет исходного, преднамеренного умысла скрыть «ошибку 1» код этого блока не должен «путаться под ногами» и мешать нам «ребусы разгадывать». Возможно ли было достижение такой цели включением в BOL строки: «тело блока CATCH допускает единственный оператор — PRINT»? Разумеется. А было бы это хорошо? Ну... не очень, прямо скажем. Если нам нужно внести информацию о случившемся в предусмотренную для этих целей лог-таблицу, то что — никак? А ведь такой функционал более чем востребован. А вручную «прирезать» место к журналу транзакций и вновь повторить операцию вставки строк — снова не? Гм... а вообще обсуждаемая конструкция нам тогда точно нужна? Вот именно. При обсуждаемом возможном подходе идея обработки ошибок получается крайне надежной, но и столь же крайне негибкой. А поэтому нам как бы сказали: творите в CATCH чего вам заблагорассудиться, но только, по причинам изложенным выше, «оборачивайте» «опасные операторы» этого блока в свои блоки TRY/CATCH. Хороший пример, близкий к воплощению обсуждаемой концепции «безопасный многоэтажный CATCH» в реальном коде, можно найти в системе обработки ошибок учебной базы AdventureWorks2008R2 (если у вас ее нет, то ссылку на соответствующий инсталляционный пакет вам любезно предоставит страница Скачать данного блога). Высокоуровневый подход реализованного там решения такой:

  • есть таблица ErrorLog выделенная исключительно под лог случившихся ошибок;
  • есть хранимая процедура uspLogError вставляющая всю необходимую информацию в указанную таблицу;
  • программист создающий код для данной учебной базы и желающий воспользоваться готовым функционалом просто вызывает EXECUTE uspLogError. Обо всем остальном позаботится эта самая процедура. Однако указанный вызов обязан производится программистом исключительно в своем блоке CATCH и нигде более. Так же он должен позаботится о корректном решении вопроса открытой транзакции (если он таковую открывал, конечно), однако поскольку тема «TRY/CATCH и транзакции» у нас впереди, то на текущий момент можно упростить ситуацию и сказать, что нашему программисту требуется в любой строчке внутри блока CATCH выполнить указанный EXECUTE. С большой степенью вероятности этот EXECUTE будет вообще единственной строкой его блока CATCH, если только ему не нужно дополнительно «чего-то этакого»;
  • процедура uspLogError уже гарантировано находясь в блоке CATCH все же открывает свой внутренний TRY/CATCH. Зачем? Затем, что она пытается вставить строчку в упомянутую таблицу, а любой INSERT — «потенциальная опасность»;
  • если все вставилось — отлично, CATCH внутри uspLogError игнорируется, управление возвращается в CATCH на более высоком уровне и т.д.;
  • если же INSERT исполнить не удалось (причины этой неудачи не столь важны) — срабатывает «CATCH вложенный в CATCH». И вот он уже содержит PRINT-ы и только их (технически говоря этот «CATCH в CATCH» «дергает» еще одну хранимую процедуру, uspPrintError, но вот эта последняя состоит ровно из двух PRINT-ов и иных инструкций не содержит). Просто потому, что хватит уже ошибки множить.

То есть, в несколько упрощенно-графическом виде мы имеем вот какую конструкцию:

ErrorHandling

Понятно, что все будет работать, если вы коды процедур uspLogError/uspPrintError «развернете» и просто вставите в свой собственный CATCH блок. Просто в результате у вас будет многоэтажная, плохочитаемая конструкция исключающая ее повторное применение в других CATCH блоках. Но чисто технически — нет проблем, хотите — «разворачивайте». В целом же систему обработки ошибок базы AdventureWorks2008R2 можно брать если не как готовое решение для промышленного кода, то уж как шаблон такого решения несомненно. Ну а общий вывод такой: блок CATCH почти всегда будет содержать внутренний TRY/CATCH потому, что мы весьма редко удовлетворимся банальным PRINT-ом в коде этого блока, а почти наверняка захотим проводить там операции далеко не столь «безобидные». А неконтролируемая «эскалация исключений» последнее чего мы добиваемся в нашей профессиональной карьере.

Проблема «двойной ошибки».

Большинство ошибочных ситуаций в SQL Server приводит к получению нами одного конкретного сообщения. Захотели разделить на ноль, а нам: Divide by zero error encountered. Захотели привести букву 'A' к типу int, а нам: Conversion failed when converting... . И так далее. Это «нормальные» сообщения о «нормальных» ошибках.

В тоже время, ряд ошибок (по счастью их число очень незначительно) приводит к получению сразу двух сообщений. Со своими текстами, номерами, а изредка и со своими уровнями серьезности. Вот вам пример такой «нестандартной» ошибки:

1
2
3
4
5
6
7
USE tempdb
GO
CREATE TABLE T2 (id int, n int)
go
ALTER TABLE T2 NOCHECK CONSTRAINT C2
go
DROP TABLE T2

Результат:

Msg 4917, Level 16, State 0, Line 1
Constraint 'C2' does not exist.
Msg 4916, Level 16, State 0, Line 1
Could not enable or disable the constraint. See previous errors.

Назначение сообщения 4916 решительно непонятно. Нельзя включить/выключить ограничение? Ну так если 4917-ая ошибка нам уже сказала, что ограничение отсутствует и даже указала какое именно по его имени, то, как бы все ясно, нет? Ну да в показанном примере это беда не столь «большой руки», на 4916 можно просто закрыть глаза и пользоваться информацией полезной.

Все приобретает куда более суровые формы, при «оборачивании» таких «продвинутых» ошибок в блок TRY/CATCH:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
USE tempdb
GO
CREATE TABLE T2 (id int, n int)
go
PRINT '--Start--'
BEGIN TRY
    PRINT 'TRY >> Start'
    ALTER TABLE T2 NOCHECK CONSTRAINT C2
    PRINT 'TRY >> End'
END TRY
BEGIN CATCH
    PRINT 'CATCH >> Start >> message: '+error_message()
    PRINT 'CATCH >> End'
END CATCH
PRINT '--End--'

На выходе:

--Start--
TRY >> Start
CATCH >> Start >> message: Could not enable or disable the constraint. See previous errors.
CATCH >> End
--End--

Нда, как говорится — «удружили», спасибо. «Смотрите предыдущее сообщение», только мы вам его не покажем, ага. А это все потому, что функции извлечения информации об ошибке приведший код в блок CATCH работают все строго с последней из зафиксированных ошибок. Ровно как поступала в свое время приснопамятная функция @@ERROR. А последняя в данной ситуации именно та самая бесполезная 4916. Если бы пара ошибок поступала от сервера «на выход» в обратном порядке было бы отлично, но... что есть — то есть. И вот в такой ситуации автор посоветовать вам, своим читателям, решительно ничего не может. Да и думаю не только автор. А что тут скажешь? Отбрасываются «предыдущие» ошибки, и хоть ты что делай. Насколько помнится автору, на Microsoft Connect давненько уже висит «реквест» что бы в блоке CATCH было позволено обходить «стек ошибок», а не только выбирать его верхний элемент. Но... воз и ныне там. Поэтому на текущий момент ситуация несколько бредовая: в редких случаях (и давайте порадуемся хотя бы тому, что случаи эти действительно редки, и даже очень) для получения информации об ошибке нужно снять ту «обертку», которая по замыслу ее создателей должна нас снабжать самой полной информацией о ней, об ошибке. То есть проще говоря привести последний скрипт к предпоследнему и лишь тогда увидеть «корень» проблемы. Ну а кто обещал что легко будет?