Sql текст ошибки

Improve Article

Save Article

Like Article

  • Read
  • Discuss
  • Improve Article

    Save Article

    Like Article

    Prerequisite – SQL Injection While checking for SQL injection we all discover various error messages. Let us figure out the basic cause behind each error and how it appears in MySQL. Below are various error and their explanation. Error-1: You have an error in your SQL syntax. Check the manual that corresponds to your MySQL server version for the right syntax to use near ” foo ‘ at line X. Explanation – If you entered a single quote and it altered the syntax of the database query, this is the expected error message. For MySQL, SQL injection may be present, but the same error message can appear in other contexts. 
    Error-2: N/A Explanation – You have commented out or removed a variable that normally would be supplied to the database. 
    Error-3: The used SELECT statements have different number of columns. Explanation – You will see this when you are attempting a UNION SELECT attack, and you specified different number of columns to the number in the original SELECT statement. 
    Error-4: You have an error in your SQL syntax. Check the manual that corresponds to your MySQL server version for the right syntax to use near ‘ XXX, YYY from SOME_TABLE’ at line 1 Explanation – You commonly see this error message when your injection point occurs before the FROM keyword (Example, you have injected into the columns to be returned) and you have used the comment character to remove required SQL keywords. Try completing the SQL statement yourself while using your comment character. MySQL should helpfully reveal the column names XXX, YYY when this condition is encountered. 
    Error-5: Table ‘DBNAME.SOMETABLE’ doesn’t exist. Explanation – Either you are trying to access a table or view that does not exist. Test your query against a table you know you have access to. MySQL should helpfully reveal the current database schema DBNAME when this condition is encountered. 
    Error-6: You have an error in your SQL syntax. Check the manual that corresponds to your MySQL server version for the right syntax to use near ‘ ‘ at line 1. Explanation – You were probably altering something in a WHERE clause, and your SQL injection attempt has disrupted the grammar. 
    Error-7: You have an error in your SQL syntax. Check the manual that corresponds to your MySQL server version for the right syntax to use near ‘ ‘ line 1. Explanation – Your SQL injection attempt has worked, but the injection point was inside parentheses. You probably commented out the closing parentheses with injected comment characters (–). 
    Error-8: You have an error in your SQL syntax. Check the manual that corresponds to your MySQL server version for the right syntax to use near XXXXX. Explanation – A general error message. The error messages listed previously all take precedence, so something else went wrong. It is likely that you can try alternative input and get a more meaningful message. 
     

    Last Updated :
    14 Jul, 2022

    Like Article

    Save Article

    Привет, Хабр! Представляю вашему вниманию перевод статьи «Error and Transaction Handling in SQL Server. Part One – Jumpstart Error Handling» автора Erland Sommarskog.

    1. Введение

    Эта статья – первая в серии из трёх статей, посвященных обработке ошибок и транзакций в SQL Server. Её цель – дать вам быстрый старт в теме обработки ошибок, показав базовый пример, который подходит для большей части вашего кода. Эта часть написана в расчете на неопытного читателя, и по этой причине я намеренно умалчиваю о многих деталях. В данный момент задача состоит в том, чтобы рассказать как без упора на почему. Если вы принимаете мои слова на веру, вы можете прочесть только эту часть и отложить остальные две для дальнейших этапов в вашей карьере.

    С другой стороны, если вы ставите под сомнение мои рекомендации, вам определенно необходимо прочитать две остальные части, где я погружаюсь в детали намного более глубоко, исследуя очень запутанный мир обработки ошибок и транзакций в SQL Server. Вторая и третья части, так же, как и три приложения, предназначены для читателей с более глубоким опытом. Первая статья — короткая, вторая и третья значительно длиннее.

    Все статьи описывают обработку ошибок и транзакций в SQL Server для версии 2005 и более поздних версий.

    1.1 Зачем нужна обработка ошибок?

    Почему мы обрабатываем ошибки в нашем коде? На это есть много причин. Например, на формах в приложении мы проверяем введенные данные и информируем пользователей о допущенных при вводе ошибках. Ошибки пользователя – это предвиденные ошибки. Но нам также нужно обрабатывать непредвиденные ошибки. То есть, ошибки могут возникнуть из-за того, что мы что-то упустили при написании кода. Простой подход – это прервать выполнение или хотя бы вернуться на этап, в котором мы имеем полный контроль над происходящим. Недостаточно будет просто подчеркнуть, что совершенно непозволительно игнорировать непредвиденные ошибки. Это недостаток, который может вызвать губительные последствия: например, стать причиной того, что приложение будет предоставлять некорректную информацию пользователю или, что еще хуже, сохранять некорректные данные в базе. Также важно сообщать о возникновении ошибки с той целью, чтобы пользователь не думал о том, что операция прошла успешно, в то время как ваш код на самом деле ничего не выполнил.

    Мы часто хотим, чтобы в базе данных изменения были атомарными. Например, задача по переводу денег с одного счета на другой. С этой целью мы должны изменить две записи в таблице CashHoldings и добавить две записи в таблицу Transactions. Абсолютно недопустимо, чтобы ошибки или сбой привели к тому, что деньги будут переведены на счет получателя, а со счета отправителя они не будут списаны. По этой причине обработка ошибок также касается и обработки транзакций. В приведенном примере нам нужно обернуть операцию в BEGIN TRANSACTION и COMMIT TRANSACTION, но не только это: в случае ошибки мы должны убедиться, что транзакция откачена.

    2. Основные команды

    Мы начнем с обзора наиболее важных команд, которые необходимы для обработки ошибок. Во второй части я опишу все команды, относящиеся к обработке ошибок и транзакций.

    2.1 TRY-CATCH

    Основным механизмом обработки ошибок является конструкция TRY-CATCH, очень напоминающая подобные конструкции в других языках. Структура такова:

    BEGIN TRY
       <обычный код>
    END TRY
    BEGIN CATCH
       <обработка ошибок>
    END CATCH

    Если какая-либо ошибка появится в <обычный код>, выполнение будет переведено в блок CATCH, и будет выполнен код обработки ошибок.

    Как правило, в CATCH откатывают любую открытую транзакцию и повторно вызывают ошибку. Таким образом, вызывающая клиентская программа понимает, что что-то пошло не так. Повторный вызов ошибки мы обсудим позже в этой статье.

    Вот очень быстрый пример:

    BEGIN TRY
       DECLARE @x int
       SELECT @x = 1/0
       PRINT 'Not reached'
    END TRY
    BEGIN CATCH 
       PRINT 'This is the error: ' + error_message()
    END CATCH

    Результат выполнения: This is the error: Divide by zero error encountered.

    Мы вернемся к функции error_message() позднее. Стоит отметить, что использование PRINT в обработчике CATCH приводится только в рамках экспериментов и не следует делать так в коде реального приложения.

    Если <обычный код> вызывает хранимую процедуру или запускает триггеры, то любая ошибка, которая в них возникнет, передаст выполнение в блок CATCH. Если более точно, то, когда возникает ошибка, SQL Server раскручивает стек до тех пор, пока не найдёт обработчик CATCH. И если такого обработчика нет, SQL Server отправляет сообщение об ошибке напрямую клиенту.

    Есть одно очень важное ограничение у конструкции TRY-CATCH, которое нужно знать: она не ловит ошибки компиляции, которые возникают в той же области видимости. Рассмотрим пример:

    CREATE PROCEDURE inner_sp AS
       BEGIN TRY
          PRINT 'This prints'
          SELECT * FROM NoSuchTable
          PRINT 'This does not print'
       END TRY
       BEGIN CATCH
          PRINT 'And nor does this print'
       END CATCH
    go
    EXEC inner_sp

    Выходные данные:

    This prints
    Msg 208, Level 16, State 1, Procedure inner_sp, Line 4
    Invalid object name 'NoSuchTable'

    Как можно видеть, блок TRY присутствует, но при возникновении ошибки выполнение не передается блоку CATCH, как это ожидалось. Это применимо ко всем ошибкам компиляции, таким как пропуск колонок, некорректные псевдонимы и тому подобное, которые возникают во время выполнения. (Ошибки компиляции могут возникнуть в SQL Server во время выполнения из-за отложенного разрешения имен – особенность, благодаря которой SQL Server позволяет создать процедуру, которая обращается к несуществующим таблицам.)

    Эти ошибки не являются полностью неуловимыми; вы не можете поймать их в области, в которой они возникают, но вы можете поймать их во внешней области. Добавим такой код к предыдущему примеру:

    CREATE PROCEDURE outer_sp AS
       BEGIN TRY
          EXEC inner_sp
       END TRY
       BEGIN CATCH
          PRINT 'The error message is: ' + error_message()
       END CATCH
    go
    EXEC outer_sp

    Теперь мы получим на выходе это:

    This prints
    The error message is: Invalid object name 'NoSuchTable'.

    На этот раз ошибка была перехвачена, потому что сработал внешний обработчик CATCH.

    2.2 SET XACT_ABORT ON

    В начало ваших хранимых процедур следует всегда добавлять это выражение:

    SET XACT_ABORT, NOCOUNT ON

    Оно активирует два параметра сессии, которые выключены по умолчанию в целях совместимости с предыдущими версиями, но опыт доказывает, что лучший подход – это иметь эти параметры всегда включенными. Поведение SQL Server по умолчанию в той ситуации, когда не используется TRY-CATCH, заключается в том, что некоторые ошибки прерывают выполнение и откатывают любые открытые транзакции, в то время как с другими ошибками выполнение последующих инструкций продолжается. Когда вы включаете XACT_ABORT ON, почти все ошибки начинают вызывать одинаковый эффект: любая открытая транзакция откатывается, и выполнение кода прерывается. Есть несколько исключений, среди которых наиболее заметным является выражение RAISERROR.

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

    Для надежной обработки ошибок в SQL Server вам необходимы как TRY-CATCH, так и SET XACT_ABORT ON. Среди них инструкция SET XACT_ABORT ON наиболее важна. Если для кода на промышленной среде только на нее полагаться не стоит, то для быстрых и простых решений она вполне подходит.

    Параметр NOCOUNT не имеет к обработке ошибок никакого отношения, но включение его в код является хорошей практикой. NOCOUNT подавляет сообщения вида (1 row(s) affected), которые вы можете видеть в панели Message в SQL Server Management Studio. В то время как эти сообщения могут быть полезны при работе c SSMS, они могут негативно повлиять на производительность в приложении, так как увеличивают сетевой трафик. Сообщение о количестве строк также может привести к ошибке в плохо написанных клиентских приложениях, которые могут подумать, что это данные, которые вернул запрос.

    Выше я использовал синтаксис, который немного необычен. Большинство людей написали бы два отдельных выражения:

    SET NOCOUNT ON
    SET XACT_ABORT ON

    Между ними нет никакого отличия. Я предпочитаю версию с SET и запятой, т.к. это снижает уровень шума в коде. Поскольку эти выражения должны появляться во всех ваших хранимых процедурах, они должны занимать как можно меньше места.

    3. Основной пример обработки ошибок

    После того, как мы посмотрели на TRY-CATCH и SET XACT_ABORT ON, давайте соединим их вместе в примере, который мы можем использовать во всех наших хранимых процедурах. Для начала я покажу пример, в котором ошибка генерируется в простой форме, а в следующем разделе я рассмотрю решения получше.

    Для примера я буду использовать эту простую таблицу.

    CREATE TABLE sometable(a int NOT NULL,
                           b int NOT NULL,
                           CONSTRAINT pk_sometable PRIMARY KEY(a, b))

    Вот хранимая процедура, которая демонстрирует, как вы должны работать с ошибками и транзакциями.

    CREATE PROCEDURE insert_data @a int, @b int AS 
       SET XACT_ABORT, NOCOUNT ON
       BEGIN TRY
          BEGIN TRANSACTION
          INSERT sometable(a, b) VALUES (@a, @b)
          INSERT sometable(a, b) VALUES (@b, @a)
          COMMIT TRANSACTION
       END TRY
       BEGIN CATCH
          IF @@trancount > 0 ROLLBACK TRANSACTION
          DECLARE @msg nvarchar(2048) = error_message()  
          RAISERROR (@msg, 16, 1)
          RETURN 55555
       END CATCH

    Первая строка в процедуре включает XACT_ABORT и NOCOUNT в одном выражении, как я показывал выше. Эта строка – единственная перед BEGIN TRY. Все остальное в процедуре должно располагаться после BEGIN TRY: объявление переменных, создание временных таблиц, табличных переменных, всё. Даже если у вас есть другие SET-команды в процедуре (хотя причины для этого встречаются редко), они должны идти после BEGIN TRY.

    Причина, по которой я предпочитаю указывать SET XACT_ABORT и NOCOUNT перед BEGIN TRY, заключается в том, что я рассматриваю это как одну строку шума: она всегда должна быть там, но я не хочу, чтобы это мешало взгляду. Конечно же, это дело вкуса, и если вы предпочитаете ставить SET-команды после BEGIN TRY, ничего страшного. Важно то, что вам не следует ставить что-либо другое перед BEGIN TRY.

    Часть между BEGIN TRY и END TRY является основной составляющей процедуры. Поскольку я хотел использовать транзакцию, определенную пользователем, я ввел довольно надуманное бизнес-правило, в котором говорится, что если вы вставляете пару, то обратная пара также должна быть вставлена. Два выражения INSERT находятся внутри BEGIN и COMMIT TRANSACTION. Во многих случаях у вас будет много строк кода между BEGIN TRY и BEGIN TRANSACTION. Иногда у вас также будет код между COMMIT TRANSACTION и END TRY, хотя обычно это только финальный SELECT, возвращающий данные или присваивающий значения выходным параметрам. Если ваша процедура не выполняет каких-либо изменений или имеет только одно выражение INSERT/UPDATE/DELETE/MERGE, то обычно вам вообще не нужно явно указывать транзакцию.

    В то время как блок TRY будет выглядеть по-разному от процедуры к процедуре, блок CATCH должен быть более или менее результатом копирования и вставки. То есть вы делаете что-то короткое и простое и затем используете повсюду, не особо задумываясь. Обработчик CATCH, приведенный выше, выполняет три действия:

    1. Откатывает любые открытые транзакции.
    2. Повторно вызывает ошибку.
    3. Убеждается, что возвращаемое процедурой значение отлично от нуля.

    Эти три действия должны всегда быть там. Мы можете возразить, что строка

    IF @@trancount > 0 ROLLBACK TRANSACTION

    не нужна, если нет явной транзакции в процедуре, но это абсолютно неверно. Возможно, вы вызываете хранимую процедуру, которая открывает транзакцию, но которая не может ее откатить из-за ограничений TRY-CATCH. Возможно, вы или кто-то другой добавите явную транзакцию через два года. Вспомните ли вы тогда о том, что нужно добавить строку с откатом? Не рассчитывайте на это. Я также слышу читателей, которые возражают, что если тот, кто вызывает процедуру, открыл транзакцию, мы не должны ее откатывать… Нет, мы должны, и если вы хотите знать почему, вам нужно прочитать вторую и третью части. Откат транзакции в обработчике CATCH – это категорический императив, у которого нет исключений.

    Код повторной генерации ошибки включает такую строку:

    DECLARE @msg nvarchar(2048) = error_message()

    Встроенная функция error_message() возвращает текст возникшей ошибки. В следующей строке ошибка повторно вызывается с помощью выражения RAISERROR. Это не самый простой способ вызова ошибки, но он работает. Другие способы мы рассмотрим в следующей главе.

    Замечание: синтаксис для присвоения начального значения переменной в DECLARE был внедрен в SQL Server 2008. Если у вас SQL Server 2005, вам нужно разбить строку на DECLARE и выражение SELECT.

    Финальное выражение RETURN – это страховка. RAISERROR никогда не прерывает выполнение, поэтому выполнение следующего выражения будет продолжено. Пока все процедуры используют TRY-CATCH, а также весь клиентский код обрабатывает исключения, нет повода для беспокойства. Но ваша процедура может быть вызвана из старого кода, написанного до SQL Server 2005 и до внедрения TRY-CATCH. В те времена лучшее, что мы могли делать, это смотреть на возвращаемые значения. То, что вы возвращаете с помощью RETURN, не имеет особого значения, если это не нулевое значение (ноль обычно обозначает успешное завершение работы).

    Последнее выражение в процедуре – это END CATCH. Никогда не следует помещать какой-либо код после END CATCH. Кто-нибудь, читающий процедуру, может не увидеть этот кусок кода.

    После прочтения теории давайте попробуем тестовый пример:

    EXEC insert_data 9, NULL

    Результат выполнения:

    Msg 50000, Level 16, State 1, Procedure insert_data, Line 12
    Cannot insert the value NULL into column 'b', table 'tempdb.dbo.sometable'; column does not allow nulls. INSERT fails.

    Давайте добавим внешнюю процедуру для того, чтобы увидеть, что происходит при повторном вызове ошибки:

    CREATE PROCEDURE outer_sp @a int, @b int AS
       SET XACT_ABORT, NOCOUNT ON
       BEGIN TRY
          EXEC insert_data @a, @b
       END TRY
       BEGIN CATCH
          IF @@trancount > 0 ROLLBACK TRANSACTION
          DECLARE @msg nvarchar(2048) = error_message()
          RAISERROR (@msg, 16, 1)
          RETURN 55555
       END CATCH
    go
    EXEC outer_sp 8, 8

    Результат работы:

    Msg 50000, Level 16, State 1, Procedure outer_sp, Line 9
    Violation of PRIMARY KEY constraint 'pk_sometable'. Cannot insert duplicate key in object 'dbo.sometable'. The duplicate key value is (8, 8).

    Мы получили корректное сообщение об ошибке, но если вы посмотрите на заголовки этого сообщения и на предыдущее поближе, то можете заметить проблему:

    Msg 50000, Level 16, State 1, Procedure insert_data, Line 12
    Msg 50000, Level 16, State 1, Procedure outer_sp, Line 9

    Сообщение об ошибке выводит информацию о расположении конечного выражения RAISERROR. В первом случае некорректен только номер строки. Во втором случае некорректно также имя процедуры. Для простых процедур, таких как наш тестовый пример, это не является большой проблемой. Но если у вас есть несколько уровней вложенных сложных процедур, то наличие сообщения об ошибке с отсутствием указания на место её возникновения сделает поиск и устранение ошибки намного более сложным делом. По этой причине желательно генерировать ошибку таким образом, чтобы можно было определить нахождение ошибочного фрагмента кода быстро, и это то, что мы рассмотрим в следующей главе.

    4. Три способа генерации ошибки

    4.1 Использование error_handler_sp

    Мы рассмотрели функцию error_message(), которая возвращает текст сообщения об ошибке. Сообщение об ошибке состоит из нескольких компонентов, и существует своя функция error_xxx() для каждого из них. Мы можем использовать их для повторной генерации полного сообщения, которое содержит оригинальную информацию, хотя и в другом формате. Если делать это в каждом обработчике CATCH, это будет большой недостаток — дублирование кода. Вам не обязательно находиться в блоке CATCH для вызова error_message() и других подобных функций, и они вернут ту же самую информацию, если будут вызваны из хранимой процедуры, которую выполнит блок CATCH.

    Позвольте представить вам error_handler_sp:

    CREATE PROCEDURE error_handler_sp AS
     
       DECLARE @errmsg   nvarchar(2048),
               @severity tinyint,
               @state    tinyint,
               @errno    int,
               @proc     sysname,
               @lineno   int
               
       SELECT @errmsg = error_message(), @severity = error_severity(),
              @state  = error_state(), @errno = error_number(),
              @proc   = error_procedure(), @lineno = error_line()
           
       IF @errmsg NOT LIKE '***%'
       BEGIN
          SELECT @errmsg = '*** ' + coalesce(quotename(@proc), '<dynamic SQL>') + 
                           ', Line ' + ltrim(str(@lineno)) + '. Errno ' + 
                           ltrim(str(@errno)) + ': ' + @errmsg
       END
       RAISERROR('%s', @severity, @state, @errmsg)

    Первое из того, что делает error_handler_sp – это сохраняет значение всех error_xxx() функций в локальные переменные. Я вернусь к выражению IF через секунду. Вместо него давайте посмотрим на выражение SELECT внутри IF:

    SELECT @errmsg = '*** ' + coalesce(quotename(@proc), '<dynamic SQL>') + 
                     ', Line ' + ltrim(str(@lineno)) + '. Errno ' + 
                     ltrim(str(@errno)) + ': ' + @errmsg

    Цель этого SELECT заключается в форматировании сообщения об ошибке, которое передается в RAISERROR. Оно включает в себя всю информацию из оригинального сообщения об ошибке, которое мы не можем вставить напрямую в RAISERROR. Мы должны обработать имя процедуры, которое может быть NULL для ошибок в обычных скриптах или в динамическом SQL. Поэтому используется функция COALESCE. (Если вы не понимаете форму выражения RAISERROR, я рассказываю о нем более детально во второй части.)

    Отформатированное сообщение об ошибке начинается с трех звездочек. Этим достигаются две цели: 1) Мы можем сразу видеть, что это сообщение вызвано из обработчика CATCH. 2) Это дает возможность для error_handler_sp отфильтровать ошибки, которые уже были сгенерированы один или более раз, с помощью условия NOT LIKE ‘***%’ для того, чтобы избежать изменения сообщения во второй раз.

    Вот как обработчик CATCH должен выглядеть, когда вы используете error_handler_sp:

    BEGIN CATCH
       IF @@trancount > 0 ROLLBACK TRANSACTION
       EXEC error_handler_sp
       RETURN 55555
    END CATCH

    Давайте попробуем несколько тестовых сценариев.

    EXEC insert_data 8, NULL
    EXEC outer_sp 8, 8

    Результат выполнения:

    Msg 50000, Level 16, State 2, Procedure error_handler_sp, Line 20
    *** [insert_data], Line 5. Errno 515: Cannot insert the value NULL into column 'b', table 'tempdb.dbo.sometable'; column does not allow nulls. INSERT fails.
    Msg 50000, Level 14, State 1, Procedure error_handler_sp, Line 20
    *** [insert_data], Line 6. Errno 2627: Violation of PRIMARY KEY constraint 'pk_sometable'. Cannot insert duplicate key in object 'dbo.sometable'. The duplicate key value is (8, 8).

    Заголовки сообщений говорят о том, что ошибка возникла в процедуре error_handler_sp, но текст сообщений об ошибках дает нам настоящее местонахождение ошибки – как название процедуры, так и номер строки.

    Я покажу еще два метода вызова ошибок. Однако error_handler_sp является моей главной рекомендацией для читателей, которые читают эту часть. Это — простой вариант, который работает на всех версиях SQL Server начиная с 2005. Существует только один недостаток: в некоторых случаях SQL Server генерирует два сообщения об ошибках, но функции error_xxx() возвращают только одну из них, и поэтому одно из сообщений теряется. Это может быть неудобно при работе с административными командами наподобие BACKUPRESTORE, но проблема редко возникает в коде, предназначенном чисто для приложений.

    4.2. Использование ;THROW

    В SQL Server 2012 Microsoft представил выражение ;THROW для более легкой обработки ошибок. К сожалению, Microsoft сделал серьезную ошибку при проектировании этой команды и создал опасную ловушку.

    С выражением ;THROW вам не нужно никаких хранимых процедур. Ваш обработчик CATCH становится таким же простым, как этот:

    BEGIN CATCH
       IF @@trancount > 0 ROLLBACK TRANSACTION
       ;THROW
       RETURN 55555
    END CATCH

    Достоинство ;THROW в том, что сообщение об ошибке генерируется точно таким же, как и оригинальное сообщение. Если изначально было два сообщения об ошибках, оба сообщения воспроизводятся, что делает это выражение еще привлекательнее. Как и со всеми другими сообщениями об ошибках, ошибки, сгенерированные ;THROW, могут быть перехвачены внешним обработчиком CATCH и воспроизведены. Если обработчика CATCH нет, выполнение прерывается, поэтому оператор RETURN в данном случае оказывается не нужным. (Я все еще рекомендую оставлять его, на случай, если вы измените свое отношение к ;THROW позже).

    Если у вас SQL Server 2012 или более поздняя версия, измените определение insert_data и outer_sp и попробуйте выполнить тесты еще раз. Результат в этот раз будет такой:

    Msg 515, Level 16, State 2, Procedure insert_data, Line 5
    Cannot insert the value NULL into column 'b', table 'tempdb.dbo.sometable'; column does not allow nulls. INSERT fails.
    Msg 2627, Level 14, State 1, Procedure insert_data, Line 6
    Violation of PRIMARY KEY constraint 'pk_sometable'. Cannot insert duplicate key in object 'dbo.sometable'. The duplicate key value is (8, 8).

    Имя процедуры и номер строки верны и нет никакого другого имени процедуры, которое может нас запутать. Также сохранены оригинальные номера ошибок.

    В этом месте вы можете сказать себе: действительно ли Microsoft назвал команду ;THROW? Разве это не просто THROW? На самом деле, если вы посмотрите в Books Online, там не будет точки с запятой. Но точка с запятой должны быть. Официально они отделяют предыдущее выражение, но это опционально, и далеко не все используют точку с запятой в выражениях T-SQL. Более важно, что если вы пропустите точку с запятой перед THROW, то не будет никакой синтаксической ошибки. Но это повлияет на поведение при выполнении выражения, и это поведение будет непостижимым для непосвященных. При наличии активной транзакции вы получите сообщение об ошибке, которое будет полностью отличаться от оригинального. И еще хуже, что при отсутствии активной транзакции ошибка будет тихо выведена без обработки. Такая вещь, как пропуск точки с запятой, не должно иметь таких абсурдных последствий. Для уменьшения риска такого поведения, всегда думайте о команде как о ;THROW (с точкой с запятой).

    Нельзя отрицать того, что ;THROW имеет свои преимущества, но точка с запятой не единственная ловушка этой команды. Если вы хотите использовать ее, я призываю вас прочитать по крайней мере вторую часть этой серии, где я раскрываю больше деталей о команде ;THROW. До этого момента, используйте error_handler_sp.

    4.3. Использование SqlEventLog

    Третий способ обработки ошибок – это использование SqlEventLog, который я описываю очень детально в третьей части. Здесь я лишь сделаю короткий обзор.

    SqlEventLog предоставляет хранимую процедуру slog.catchhandler_sp, которая работает так же, как и error_handler_sp: она использует функции error_xxx() для сбора информации и выводит сообщение об ошибке, сохраняя всю информацию о ней. Вдобавок к этому, она логирует ошибку в таблицу splog.sqleventlog. В зависимости от типа приложения, которое у вас есть, эта таблица может быть очень ценным объектом.

    Для использования SqlEventLog, ваш обработчик CATCH должен быть таким:

    BEGIN CATCH
       IF @@trancount > 0 ROLLBACK TRANSACTION
       EXEC slog.catchhandler_sp @@procid
       RETURN 55555
    END CATCH

    @@procid возвращает идентификатор объекта текущей хранимой процедуры. Это то, что SqlEventLog использует для логирования информации в таблицу. Используя те же тестовые сценарии, получим результат их работы с использованием catchhandler_sp:

    Msg 50000, Level 16, State 2, Procedure catchhandler_sp, Line 125
    {515} Procedure insert_data, Line 5
    Cannot insert the value NULL into column 'b', table 'tempdb.dbo.sometable'; column does not allow nulls. INSERT fails.
    Msg 50000, Level 14, State 1, Procedure catchhandler_sp, Line 125
    {2627} Procedure insert_data, Line 6
    Violation of PRIMARY KEY constraint 'pk_sometable'. Cannot insert duplicate key in object 'dbo.sometable'. The duplicate key value is (8, 8).

    Как вы видите, сообщение об ошибке отформатировано немного не так, как это делает error_handler_sp, но основная идея такая же. Вот образец того, что было записано в таблицу slog.sqleventlog:

    logid logdate errno severity logproc linenum msgtext
    1 2015-01-25 22:40:24.393 515 16 insert_data 5 Cannot insert …
    2 2015-01-25 22:40:24.395 2627 14 insert_data 6 Violation of …

    Если вы хотите попробовать SqlEventLog, вы можете загрузить файл sqleventlog.zip. Инструкция по установке находится в третьей части, раздел Установка SqlEventLog.

    5. Финальные замечания

    Вы изучили основной образец для обработки ошибок и транзакций в хранимых процедурах. Он не идеален, но он должен работать в 90-95% вашего кода. Есть несколько ограничений, на которые стоит обратить внимание:

    1. Как мы видели, ошибки компиляции не могут быть перехвачены в той же процедуре, в которой они возникли, а только во внешней процедуре.
    2. Пример не работает с пользовательскими функциями, так как ни TRY-CATCH, ни RAISERROR нельзя в них использовать.
    3. Когда хранимая процедура на Linked Server вызывает ошибку, эта ошибка может миновать обработчик в хранимой процедуре на локальном сервере и отправиться напрямую клиенту.
    4. Когда процедура вызвана как INSERT-EXEC, вы получите неприятную ошибку, потому что ROLLBACK TRANSACTION не допускается в данном случае.
    5. Как упомянуто выше, если вы используете error_handler_sp или SqlEventLog, мы потеряете одно сообщение, когда SQL Server выдаст два сообщения для одной ошибки. При использовании ;THROW такой проблемы нет.

    Я рассказываю об этих ситуациях более подробно в других статьях этой серии.

    Перед тем как закончить, я хочу кратко коснуться триггеров и клиентского кода.

    Триггеры

    Пример для обработки ошибок в триггерах не сильно отличается от того, что используется в хранимых процедурах, за исключением одной маленькой детали: вы не должны использовать выражение RETURN (потому что RETURN не допускается использовать в триггерах).

    С триггерами важно понимать, что они являются частью команды, которая запустила триггер, и в триггере вы находитесь внутри транзакции, даже если не используете BEGIN TRANSACTION.
    Иногда я вижу на форумах людей, которые спрашивают, могут ли они написать триггер, который не откатывает в случае падения запустившую его команду. Ответ таков: нет способа сделать это надежно, поэтому не стоит даже пытаться. Если в этом есть необходимость, по возможности не следует использовать триггер вообще, а найти другое решение. Во второй и третьей частях я рассматриваю обработку ошибок в триггерах более подробно.

    Клиентский код

    У вас должна быть обработка ошибок в коде клиента, если он имеет доступ к базе. То есть вы должны всегда предполагать, что при любом вызове что-то может пойти не так. Как именно внедрить обработку ошибок, зависит от конкретной среды.

    Здесь я только обращу внимание на важную вещь: реакцией на ошибку, возвращенную SQL Server, должно быть завершение запроса во избежание открытых бесхозных транзакций:

    IF @@trancount > 0 ROLLBACK TRANSACTION

    Это также применимо к знаменитому сообщению Timeout expired (которое является не сообщением от SQL Server, а от API).

    6. Конец первой части

    Это конец первой из трех частей серии. Если вы хотели изучить вопрос обработки ошибок быстро, вы можете закончить чтение здесь. Если вы настроены идти дальше, вам следует прочитать вторую часть, где наше путешествие по запутанным джунглям обработки ошибок и транзакций в SQL Server начинается по-настоящему.

    … и не забывайте добавлять эту строку в начало ваших хранимых процедур:

    SET XACT_ABORT, NOCOUNT ON

    message_text — сообщение, которое вы хотите показать при ошибке. Замечание: вы можете добавлять пользовательские сообщения для вывода информации об ошибке. Смотрите следующий раздел статьи.
    message_id — id сообщения об ошибке. Если вы хотите вывести пользовательское сообщение, вы можете определить этот идентификатор. Посмотрите список идентификаторов сообщений в sys.messages DMV.
    Запрос:

    select * from sys.messages

    Вывод:

    severity — серьезность ошибки. Тип данных переменной severity — smallint, значения находятся в диапазоне от 0 до 25. Допустимыми значениями серьезности ошибки являются:

    • 0-10
    • — информационные сообщения

    • 11-18
    • — ошибки

    • 19-25
    • — фатальные ошибки

    Замечание: Если вы создаете пользовательское сообщение, сложность, указанная в этом сообщении, будет перебиваться сложностью, заданной в операторе RAISERROR.
    state — уникальное идентификационное число, которое может использоваться для раздела кода, вызывающего ошибку. Тип данных параметра state — smallint, и допустимые значения между 0 и 255.

    Теперь давайте перейдем к практическим примерам.

    Пример 1: использование оператора SQL Server RAISERROR для вывода сообщения

    В этом примере вы можете увидеть, как можно отобразить ошибку или информационное сообщение с помощью оператора RAISERROR.

    Предположим, что вы хотите отобразить сообщение после вставки записей в таблицу. Мы можем использовать операторы PRINT или RAISERROR. Ниже — код:

    SET nocount ON 
    INSERT INTO tblpatients
    (patient_id,
    patient_name,
    address,
    city)
    VALUES ('OPD00006',
    'Nimesh Upadhyay',
    'AB-14, Ratnedeep Flats',
    'Mehsana')
    RAISERROR ( 'Patient detail added successfully',1,1)

    Вывод:

    Как видно на рисунке выше, ID сообщения равно 50000, поскольку это пользовательское сообщение.

    Пример 2: оператор SQL RAISERROR с текстом динамического сообщения

    Теперь посмотрите, как мы можем создать текст динамического сообщения для оператора SQL RAISERROR.

    Предположим, что мы хотим напечатать в сообщении ID пациента. Я описал локальную переменную с именем @PatientID, которая содержит patient_id. Чтобы отобразить значение переменной @PatientID в тексте сообщения, мы можем использовать следующий код:

    DECLARE @PatientID VARCHAR(15) 
    DECLARE @message NVARCHAR(max)
    SET @PatientID='OPD00007'
    SET @message ='Patient detail added successfully. The OPDID is %s'
    INSERT INTO tblpatients
    (patient_id,
    patient_name,
    address,
    city)
    VALUES ('' + @PatientID + '',
    'Nimesh Upadhyay',
    'AB-14, Ratnedeep Flats',
    'Mehsana')
    RAISERROR ( @message,1,1,@patientID)

    Вывод:

    Для отображения строки в операторе RAISERROR, мы должны использовать операторы print в стиле языка Си.

    Как видно на изображении выше, для вывода параметра в тексте сообщения я использую опцию %s, которая отображает строковое значение параметра. Если вы хотите вывести целочисленный параметр, вы можете использовать опцию %d.

    Использование SQL RAISERROR в блоке TRY..CATCH

    В этом примере мы добавляем SQL RAISERROR в блок TRY. При запуске этого кода он выполняет связанный блок CATCH. В блоке CATCH мы будем выводить подробную информацию о возникшей ошибке.

    BEGIN try 
    RAISERROR ('Error invoked in the TRY code block.',16,1 );
    END try
    BEGIN catch
    DECLARE @ErrorMsg NVARCHAR(4000);
    DECLARE @ErrSeverity INT;
    DECLARE @ErrState INT;
    SELECT @ErrorMsg = Error_message(),
    @ErrSeverity = Error_severity(),
    @ErrState = Error_state();
    RAISERROR (@ErrorMsg,
    @ErrSeverity,
    @ErrState
    );
    END catch;

    Так мы добавили оператор RAISERROR с ВАЖНОСТЬЮ МЕЖДУ 11 И 19. Это вызывает выполнение блока CATCH.

    В блоке CATCH мы показываем информацию об исходной ошибке, используя оператор RAISERROR.
    Вывод:

    Как вы можете увидеть, код вернул информацию об исходной ошибке.

    Теперь давайте разберемся, как добавить пользовательское сообщение, используя хранимую процедуру sp_addmessage.

    Хранимая процедура sp_addmessage

    Мы можем добавить пользовательское сообщение, выполнив хранимую процедуру sp_addmessages. Синтаксис процедуры:

    EXEC Sp_addmessage 
    @msgnum= 70001,
    @severity=16,
    @msgtext='Please enter the numeric value',
    @lang=NULL,
    @with_log='TRUE',
    @replace='Replace';

    @msgnum: задает номер сообщения. Тип данных параметра — integer. Это ID пользовательского сообщения.
    @severity: указывает уровень серьезности ошибки. Допустимые значения от 1 до 25. Тип данных параметра — smallint.
    @messagetext: задает текст сообщения, который вы хотите выводить. Тип данных параметра nvarchar(255), значение по умолчанию NULL.
    @lang: задает язык, который вы хотите использовать для вывода сообщения об ошибке. Значение по умолчанию NULL.
    @with_log: этот параметр используется для записи сообщения в просмотрщик событий. Допустимые значения TRUE и FALSE. Если вы задаете TRUE, сообщение об ошибке будет записано в просмотрщик событий Windows. Если выбрать FALSE, ошибка не будет записана в журнал ошибок Windows.
    @replace: если вы хотите заменить существующее сообщение об ошибке на пользовательское сообщение и уровень серьезности, вы можете указать это в хранимой процедуре.

    Предположим, что вы хотите создать сообщение об ошибке, которое возвращает ошибку о недопустимом качестве (invalid quality). В операторе INSERT значение invalid_quality находится в диапазоне между 20 и 100. Сообщение следует рассматривать как ошибку с уровнем серьезности 16.

    Чтобы создать такое сообщение, выполните следующий запрос:

    USE master;
    go
    EXEC Sp_addmessage
    70001,
    16,
    N'Product Quantity must be between 20 and 100.';
    go

    После добавления сообщения выполните запрос ниже, чтобы увидеть его:

    USE master 
    go
    SELECT * FROM sys.messages WHERE message_id = 70001

    Вывод:

    Как использовать пользовательские сообщения об ошибках

    Как упоминалось выше, мы должны использовать message_id в операторе RAISERROR для пользовательских сообщений.

    Мы создали сообщение с ID = 70001. Оператор RAISERROR должен быть таким:

    USE master 
    go
    RAISERROR (70001,16,1 );
    go

    Вывод:

    Оператор RAISERROR вернул пользовательское сообщение.

    Хранимая процедура sp_dropmessage

    Хранимая процедура sp_dropmessage используется для удаления пользовательских сообщений. Синтаксис оператора:

    EXEC Sp_dropmessage @msgnum

    Здесь @msgnum задает ID сообщения, которое вы хотите удалить.

    Теперь мы хотим удалить сообщение, с ID = 70001. Запрос:

    EXEC Sp_dropmessage 70001

    Выполним следующий запрос для просмотра сообщения после его удаления:

    USE master 
    go
    SELECT * FROM sys.messages WHERE message_id = 70001

    Вывод:

    Как видно, сообщение было удалено.

    Error handling overview

    Error handling in SQL Server gives us control over the Transact-SQL code. For example, when things go wrong, we get a chance to do something about it and possibly make it right again. SQL Server error handling can be as simple as just logging that something happened, or it could be us trying to fix an error. It can even be translating the error in SQL language because we all know how technical SQL Server error messages could get making no sense and hard to understand. Luckily, we have a chance to translate those messages into something more meaningful to pass on to the users, developers, etc.

    In this article, we’ll take a closer look at the TRY… CATCH statement: the syntax, how it looks, how it works and what can be done when an error occurs. Furthermore, the method will be explained in a SQL Server case using a group of T-SQL statements/blocks, which is basically SQL Server way of handling errors. This is a very simple yet structured way of doing it and once you get the hang of it, it can be quite helpful in many cases.

    On top of that, there is a RAISERROR function that can be used to generate our own custom error messages which is a great way to translate confusing error messages into something a little bit more meaningful that people would understand.

    Handling errors using TRY…CATCH

    Here’s how the syntax looks like. It’s pretty simple to get the hang of. We have two blocks of code:

    BEGIN TRY  

         —code to try

    END TRY  

    BEGIN CATCH  

         —code to run if an error occurs

    —is generated in try

    END CATCH

    Anything between the BEGIN TRY and END TRY is the code that we want to monitor for an error. So, if an error would have happened inside this TRY statement, the control would have immediately get transferred to the CATCH statement and then it would have started executing code line by line.

    Now, inside the CATCH statement, we can try to fix the error, report the error or even log the error, so we know when it happened, who did it by logging the username, all the useful stuff. We even have access to some special data only available inside the CATCH statement:

    • ERROR_NUMBER – Returns the internal number of the error
    • ERROR_STATE – Returns the information about the source
    • ERROR_SEVERITY – Returns the information about anything from informational errors to errors user of DBA can fix, etc.
    • ERROR_LINE – Returns the line number at which an error happened on
    • ERROR_PROCEDURE – Returns the name of the stored procedure or function
    • ERROR_MESSAGE – Returns the most essential information and that is the message text of the error

    That’s all that is needed when it comes to SQL Server error handling. Everything can be done with a simple TRY and CATCH statement and the only part when it can be tricky is when we’re dealing with transactions. Why? Because if there’s a BEGIN TRANSACTION, it always must end with a COMMIT or ROLLBACK transaction. The problem is if an error occurs after we begin but before we commit or rollback. In this particular case, there is a special function that can be used in the CATCH statement that allows checking whether a transaction is in a committable state or not, which then allows us to make a decision to rollback or to commit it.

    Let’s head over to SQL Server Management Studio (SSMS) and start with basics of how to handle SQL Server errors. The AdventureWorks 2014 sample database is used throughout the article. The script below is as simple as it gets:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    USE AdventureWorks2014

    GO

    — Basic example of TRY…CATCH

    BEGIN TRY

    — Generate a divide-by-zero error  

      SELECT

        1 / 0 AS Error;

    END TRY

    BEGIN CATCH

      SELECT

        ERROR_NUMBER() AS ErrorNumber,

        ERROR_STATE() AS ErrorState,

        ERROR_SEVERITY() AS ErrorSeverity,

        ERROR_PROCEDURE() AS ErrorProcedure,

        ERROR_LINE() AS ErrorLine,

        ERROR_MESSAGE() AS ErrorMessage;

    END CATCH;

    GO

    This is an example of how it looks and how it works. The only thing we’re doing in the BEGIN TRY is dividing 1 by 0, which, of course, will cause an error. So, as soon as that block of code is hit, it’s going to transfer control into the CATCH block and then it’s going to select all of the properties using the built-in functions that we mentioned earlier. If we execute the script from above, this is what we get:

    Basic SQL Server try catch script executed in Management Studio that returns an error

    We got two result grids because of two SELECT statements: the first one is 1 divided by 0, which causes the error and the second one is the transferred control that actually gave us some results. From left to right, we got ErrorNumber, ErrorState, ErrorSeverity; there is no procedure in this case (NULL), ErrorLine, and ErrorMessage.

    Now, let’s do something a little more meaningful. It’s a clever idea to track these errors. Things that are error-prone should be captured anyway and, at the very least, logged. You can also put triggers on these logged tables and even set up an email account and get a bit creative in the way of notifying people when an error occurs.

    If you’re unfamiliar with database email, check out this article for more information on the emailing system: How to configure database mail in SQL Server

    The script below creates a table called DB_Errors, which can be used to store tracking data:

    — Table to record errors

    CREATE TABLE DB_Errors

             (ErrorID        INT IDENTITY(1, 1),

              UserName       VARCHAR(100),

              ErrorNumber    INT,

              ErrorState     INT,

              ErrorSeverity  INT,

              ErrorLine      INT,

              ErrorProcedure VARCHAR(MAX),

              ErrorMessage   VARCHAR(MAX),

              ErrorDateTime  DATETIME)

    GO

    Here we have a simple identity column, followed by username, so we know who generated the error and the rest is simply the exact information from the built-in functions we listed earlier.

    Now, let’s modify a custom stored procedure from the database and put an error handler in there:

    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

    ALTER PROCEDURE dbo.AddSale @employeeid INT,

                       @productid  INT,

                       @quantity   SMALLINT,

                       @saleid     UNIQUEIDENTIFIER OUTPUT

    AS

    SET @saleid = NEWID()

      BEGIN TRY

        INSERT INTO Sales.Sales

             SELECT

               @saleid,

               @productid,

               @employeeid,

               @quantity

      END TRY

      BEGIN CATCH

        INSERT INTO dbo.DB_Errors

        VALUES

      (SUSER_SNAME(),

       ERROR_NUMBER(),

       ERROR_STATE(),

       ERROR_SEVERITY(),

       ERROR_LINE(),

       ERROR_PROCEDURE(),

       ERROR_MESSAGE(),

       GETDATE());

      END CATCH

    GO

    Altering this stored procedure simply wraps error handling in this case around the only statement inside the stored procedure. If we call this stored procedure and pass some valid data, here’s what happens:

    Script for inserting valid data through a stored procedure into Sales table

    A quick Select statement indicates that the record has been successfully inserted:

    Script for validating if data is inserted successfully into the table

    However, if we call the above-stored procedure one more time, passing the same parameters, the results grid will be populated differently:

    Script for inserting invalid data that would cause raise error SQL state

    This time, we got two indicators in the results grid:

    0 rows affected – this line indicated that nothing actually went into the Sales table

    1 row affected – this line indicates that something went into our newly created logging table

    So, what we can do here is look at the errors table and see what happened. A simple Select statement will do the job:

    Script for retrieving data from the errors table

    Here we have all the information we set previously to be logged, only this time we also got the procedure field filled out and of course the SQL Server “friendly” technical message that we have a violation:

    Violation of PRIMARY KEY constraint ‘PK_Sales_1′. Cannot insert duplicate key in object’ Sales.Sales’. The duplicate key value is (20).

    How this was a very artificial example, but the point is that in the real world, passing an invalid date is very common. For example, passing an employee ID that doesn’t exist in a case when we have a foreign key set up between the Sales table and the Employee table, meaning the Employee must exist in order to create a new record in the Sales table. This use case will cause a foreign key constraint violation.

    The general idea behind this is not to get the error fizzled out. We at least want to report to an individual that something went wrong and then also log it under the hood. In the real world, if there was an application relying on a stored procedure, developers would probably have SQL Server error handling coded somewhere as well because they would have known when an error occurred. This is also where it would be a clever idea to raise an error back to the user/application. This can be done by adding the RAISERROR function so we can throw our own version of the error.

    For example, if we know that entering an employee ID that doesn’t exist is more likely to occur, then we can do a lookup. This lookup can check if the employee ID exists and if it doesn’t, then throw the exact error that occurred. Or in the worst-case scenario, if we had an unexpected error that we had no idea what it was, then we can just pass back what it was.

    Advanced SQL error handling

    We only briefly mentioned tricky part with transactions, so here’s a simple example of how to deal with them. We can use the same procedure as before, only this time let’s wrap a transaction around the Insert statement:

    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

    ALTER PROCEDURE dbo.AddSale @employeeid INT,

                       @productid  INT,

                       @quantity   SMALLINT,

                       @saleid     UNIQUEIDENTIFIER OUTPUT

    AS

    SET @saleid = NEWID()

      BEGIN TRY

        BEGIN TRANSACTION

        INSERT INTO Sales.Sales

             SELECT

               @saleid,

               @productid,

               @employeeid,

               @quantity

        COMMIT TRANSACTION

      END TRY

      BEGIN CATCH

        INSERT INTO dbo.DB_Errors

        VALUES

      (SUSER_SNAME(),

       ERROR_NUMBER(),

       ERROR_STATE(),

       ERROR_SEVERITY(),

       ERROR_LINE(),

       ERROR_PROCEDURE(),

       ERROR_MESSAGE(),

       GETDATE());

    — Transaction uncommittable

        IF (XACT_STATE()) = 1

          ROLLBACK TRANSACTION

    — Transaction committable

        IF (XACT_STATE()) = 1

          COMMIT TRANSACTION

      END CATCH

    GO

    So, if everything executes successfully inside the Begin transaction, it will insert a record into Sales, and then it will commit it. But if something goes wrong before the commit takes place and it transfers control down to our Catch – the question is: How do we know if we commit or rollback the whole thing?

    If the error isn’t serious, and it is in the committable state, we can still commit the transaction. But if something went wrong and is in an uncommittable state, then we can roll back the transaction. This can be done by simply running and analyzing the XACT_STATE function that reports transaction state.

    This function returns one of the following three values:

      1 – the transaction is committable

    -1 – the transaction is uncommittable and should be rolled back

      0 – there are no pending transactions

    The only catch here is to remember to actually do this inside the catch statement because you don’t want to start transactions and then not commit or roll them back:

    Script for modifying the stored procedure for inserting sales data to either rollback or commit transaction

    How, if we execute the same stored procedure providing e.g. invalid EmployeeID we’ll get the same errors as before generated inside out table:

    T-SQL code for inserting invalid data that would cause raise error SQL state

    The way we can tell that this wasn’t inserted is by executing a simple Select query, selecting everything from the Sales table where EmployeeID is 20:

    A Select statement that proves nothing was inserted into Sales table with the employee ID of 20

    Generating custom raise error SQL message

    Let’s wrap things up by looking at how we can create our own custom error messages. These are good when we know that there’s a possible situation that might occur. As we mentioned earlier, it’s possible that someone will pass an invalid employee ID. In this particular case, we can do a check before then and sure enough, when this happens, we can raise our own custom message like saying employee ID does not exist. This can be easily done by altering our stored procedure one more time and adding the lookup in our TRY block:

    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

    ALTER PROCEDURE dbo.AddSale @employeeid INT,

                       @productid  INT,

                       @quantity   SMALLINT,

                       @saleid     UNIQUEIDENTIFIER OUTPUT

    AS

    SET @saleid = NEWID()

      BEGIN TRY

      IF (SELECT COUNT(*) FROM HumanResources.Employee e WHERE employeeid = @employeeid) = 0

          RAISEERROR (‘EmployeeID does not exist.’, 11, 1)

        INSERT INTO Sales.Sales

             SELECT

               @saleid,

               @productid,

               @employeeid,

               @quantity

      END TRY

      BEGIN CATCH

        INSERT INTO dbo.DB_Errors

        VALUES

      (SUSER_SNAME(),

       ERROR_NUMBER(),

       ERROR_STATE(),

       ERROR_SEVERITY(),

       ERROR_LINE(),

       ERROR_PROCEDURE(),

       ERROR_MESSAGE(),

       GETDATE());

       DECLARE @Message varchar(MAX) = ERROR_MESSAGE(),

            @Severity int = ERROR_SEVERITY(),

            @State smallint = ERROR_STATE()

       RAISEERROR (@Message, @Severity, @State)

      END CATCH

    GO

    If this count comes back as zero, that means the employee with that ID doesn’t exist. Then we can call the RAISERROR where we define a user-defined message, and furthermore our custom severity and state. So, that would be a lot easier for someone using this stored procedure to understand what the problem is rather than seeing the very technical error message that SQL throws, in this case, about the foreign key validation.

    With the last changes in our store procedure, there also another RAISERROR in the Catch block. If another error occurred, rather than having it slip under, we can again call the RAISERROR and pass back exactly what happened. That’s why we have declared all the variables and the results of all the functions. This way, it will not only get logged but also report back to the application or user.

    And now if we execute the same code from before, it will both get logged and it will also indicate that the employee ID does not exist:

    Custom raise error SQL Server message returned by executing the script and inserting invalid data through a stored procedure

    Another thing worth mentioning is that we can actually predefine this error message code, severity, and state. There is a stored procedure called sp_addmessage that is used to add our own error messages. This is useful when we need to call the message on multiple places; we can just use RAISERROR and pass the message number rather than retyping the stuff all over again. By executing the selected code from below, we then added this error into SQL Server:

    Script for storing message code, severity, and state in an instance of the SQL Server Database Engine used to add our own raise error SQL message

    This means that now rather than doing it the way we did previously, we can just call the RAISERROR and pass in the error number and here’s what it looks like:

    The custom raise error SQL Server message with code, severity, and state in results grid of Management Studio

    The sp_dropmessage is, of course, used to drop a specified user-defined error message. We can also view all the messages in SQL Server by executing the query from below:

    SELECT * FROM master.dbo.sysmessages

    List of all SQL Server error messages showed in results grid of Management Studio

    There’s a lot of them and you can see our custom raise error SQL message at the very top.

    I hope this article has been informative for you and I thank you for reading.

    References

    • TRY…CATCH (Transact-SQL)
    • RAISERROR (Transact-SQL)
    • System Functions (Transact-SQL)
    • Author
    • Recent Posts

    Bojan Petrovic

    Bojan aka “Boksi”, an AP graduate in IT Technology focused on Networks and electronic technology from the Copenhagen School of Design and Technology, is a software analyst with experience in quality assurance, software support, product evangelism, and user engagement.

    He has written extensively on both the SQL Shack and the ApexSQL Solution Center, on topics ranging from client technologies like 4K resolution and theming, error handling to index strategies, and performance monitoring.

    Bojan works at ApexSQL in Nis, Serbia as an integral part of the team focusing on designing, developing, and testing the next generation of database tools including MySQL and SQL Server, and both stand-alone tools and integrations into Visual Studio, SSMS, and VSCode.

    See more about Bojan at LinkedIn

    View all posts by Bojan Petrovic

    Bojan Petrovic

    Обработка исключений

    Подключение к базе данных представляет собой одно из слабых мест в работе программы. В силу самых разных причин клиент может не получить доступ к базе данных. Поэтому при создании приложения следует обязательно включать обработку исключений и возможность предоставления пользователю информации о них.

    Для получения специализированных сообщений при возникновении ошибок подключения к базе данных Microsoft SQL Server используются классы SqlException и SqlErro r. Объекты этих классов можно применять для перехвата номеров ошибок, возвращаемых базой данных (таблица 4.2):

    Таблица
    4.2.
    Ошибки SQL Server

    Номер ошибки Описание
    17 Неверное имя сервера
    4060 Неверное название базы данных
    18456 Неверное имя пользователя или пароль

    Дополнительно вводятся уровни ошибок SQL Server, позволяющие охарактеризовать причину проблемы и ее сложность (таблица 4.3):

    Таблица
    4.3.
    Уровни ошибок SQL Server

    Интервал возвращаемых значений Описание Действие
    11-16 Ошибка, созданная пользователем Пользователь должен повторно ввести верные данные
    17-19 Ошибки программного обеспечения или оборудования Пользователь может продолжать работу, но некоторые запросы будут недоступны. Соединение остается открытым
    20-25 Ошибки программного обеспечения или оборудования Сервер закрывает соединение. Пользователь должен открыть его снова

    Создайте новое Windows-приложение и назовите его «ExceptionsSQL». Свойству Size формы устанавливаем значение «600;380». Добавляем на форму элемент управления DataGrid, его свойству Dock устанавливаем значение «Fill». Перетаскиваем элемент Panel, определяем следующие его свойства:

    panel1, свойство Значение
    Dock Right
    Location 392; 0
    Size 200; 346

    На панели размещаем четыре текстовых поля, надпись и кнопку:

    textBox1, свойство Значение
    Name txtDataSource
    Location 8; 8
    Size 184; 20
    Text Введите название сервера
    textBox2, свойство Значение
    Name txtInitialCatalog
    Location 8; 40
    Size 184; 20
    Text Введите название базы данных
    textBox3, свойство Значение
    Name txtUserID
    Location 8; 72
    Size 184; 20
    Text Введите имя пользователя
    textBox4, свойство Значение
    Name txtPassword
    Location 8; 104
    Size 184; 20
    Text Введите пароль1Для скрывания пароля при вводе можно в свойстве «PasswordChar» текстового поля ввести заменяющий символ, например, звездочку («*»).
    label1, свойство Значение
    Location 16; 136
    Size 176; 160
    Text
    button1, свойство Значение
    Name btnConnect
    Location 56; 312
    Size 96; 23
    Text Соединение

    Интерфейс приложения готов. Подключаем пространство имен для работы с базой данных:

    using System.Data.SqlClient;

    Объекты ADO .NET и весь блок обработки исключений помещаем в обработчик кнопки «Соединение»:

    private void btnConnect_Click(object sender, System.EventArgs e)
    {
    	SqlConnection conn = new SqlConnection();
    	label1.Text = "";
    	try
    	{
    //conn.ConnectionString = "workstation id=9E0D682EA8AE448;data source="(local)
    //";" + "persist security info=True;initial catalog=Northwind;
    //user id=sa;password=12345";
    
    //Строка ConnectionString в качестве параметров 
    //будет передавать значения, введенные в текстовые поля:
    		conn.ConnectionString = 
    		"initial catalog=" + txtInitialCatalog.Text + ";" +
    		"user id=" + txtUserID.Text + ";" +
    		"password=" + txtPassword.Text + ";" +
    		"data source=" + txtDataSource.Text + ";" +
    		"workstation id=9E0D682EA8AE448;persist security info=True;";
    		SqlDataAdapter dataAdapter = new SqlDataAdapter("SELECT * FROM 
    		 Customers", conn);
    		DataSet ds = new DataSet();
    		conn.Open();
    		dataAdapter.Fill(ds);
    		dataGrid1.DataSource = ds.Tables[0].DefaultView;
    	}
    	catch (SqlException OshibkiSQL)
    	{
    		foreach (SqlError oshibka in OshibkiSQL.Errors)
    		{
    			//Свойство Number объекта oshibka возвращает 
    			//номер ошибки SQL Server
    			switch (oshibka.Number)
    			{
    			case 17:
    			label1.Text += "nНеверное имя сервера!";
    			break;
    			case 4060:
    			label1.Text += "nНеверное имя базы данных!";
    			break;
    			case 18456:
    			label1.Text += "nНеверное имя пользователя или пароль!";
    			break;
    			}
    			//Свойство Class объекта oshibka возвращает 
    			//уровень ошибки SQL Server,
    			//а свойство Message - уведомляющее сообщение
    			label1.Text +="n"+oshibka.Message + "
    			 Уровень ошибки SQL Server: " + oshibka.Class;			}
    	}
    	//Отлавливаем прочие возможные ошибки:
    	catch (Exception ex)
    	{
    		label1.Text += "nОшибка подключения: " + ex.Message;
    	}
    	finally
    	{
    		conn.Dispose();
    	}
    }

    Закомментированная строка подключения содержит обычное перечисление параметров. При отладке приложения будет легче сначала добиться наличия подключения, а затем осуществлять привязку параметров, вводимых в текстовые поля. Запускаем приложение. При вводе неверных параметров в надпись выводятся соответствующие сообщения, а при правильных параметрах элемент DataGrid отображает данные (рис. 4.11):

    В программном обеспечении к курсу вы найдете приложение Exceptions SQL (CodeGlava2 ExceptionsSQL).

    Скопируйте папку приложения ExceptionsSQL и назовите ее «ExceptionsMDB». Удаляем с панели на форме имеющиеся текстовые поля и добавляем три новых:

    textBox1, свойство Значение
    Name txtDataBasePassword
    Location 8; 16
    Size 184; 20
    Text Введите пароль базы данных
    textBox2, свойство Значение
    Name txtUserID
    Location 8; 48
    Size 184; 20
    Text Введите имя пользователя
    textBox3, свойство Значение
    Name TxtPassword
    Location 8; 80
    Size 184; 20
    Text Введите пароль пользователя

    Изменяем пространство имен для работы с базой данных:

    Обработчик кнопки «Соединение» будет выглядеть так:

    private void btnConnect_Click(object sender, System.EventArgs e)
    {
    	OleDbConnection conn = new OleDbConnection();
    	label1.Text = "";
    	try
    	{
    // conn.ConnectionString = @"Provider=""Microsoft.Jet.OLEDB.4.0"";
    //Data Source=""D:UchebnikCodeGlava2BDwithUsersP.mdb"";
    Jet OLEDB:System database=""D:UchebnikCodeGlava2BDWorkFile.mdw"";
    User ID=Adonetuser;Password=12345;Jet OLEDB:Database Password=98765;";
    				
    //Строка ConnectionString в качестве параметров 
    //будет передавать значения, введенные в текстовые поля:
    		conn.ConnectionString = 
    		"Jet OLEDB:Database Password=" + txtDataBasePassword.Text 
    		 + ";" + "User ID=" + txtUserID.Text + ";" +
    		 "password=" + txtPassword.Text + ";" +
    
    				@"Provider=""Microsoft.Jet.OLEDB.4.0"";Data 		
    	Source=""D:UchebnikCodeGlava2BDwithUsersP.mdb"";
    Jet OLEDB:System database=""D:UchebnikCodeGlava
    BDWorkFile.mdw"";";
    
    		OleDbDataAdapter dataAdapter = 
    		 new OleDbDataAdapter("SELECT * FROM Туристы", conn);
    		DataSet ds = new DataSet();
    		conn.Open();
    		dataAdapter.Fill(ds);
    		dataGrid1.DataSource = ds.Tables[0].DefaultView;
    	}
    	catch (OleDbException oshibka)
    	{
    		//Пробегаем по всем ошибкам
    		for (int i=0; i < oshibka.Errors.Count; i++)
    		{
    			label1.Text+= "Номер ошибки " + i 
    			 + "n" + "Сообщение: " + 
    			 oshibka.Errors[i].Message + "n" +
    			 "Номер ошибки NativeError: " + 
    			 oshibka.Errors[i].NativeError + "n" +
    			 "Источник: " + oshibka.Errors[i].Source + 
    			 "n" + "Номер SQLState: " + 
    			 oshibka.Errors[i].SQLState + "n";
    			}
    		}
    		//Отлавливаем прочие возможные ошибки:
    		catch (Exception ex)
    		{
    			label1.Text += "nОшибка подключения: " + 
    			 ex.Message;
    		}
    		finally
    		{
    			conn.Dispose();
    		}
    }

    Запускаем приложение (рис. 4.12). Свойство Message возвращает причину ошибки на русском языке, поскольку установлена русская версия Microsoft Office 2003. Свойство NativeError (внутренняя ошибка) возвращает номер исключения, генерируемый самим источником данных. Вместе или по отдельности со свойством SQL State их можно использовать для создания переключателя, предоставляющего пользователю расширенную информацию (мы это делали в приложении ExceptionsSQL) .

     Готовое приложение ExceptionsMDB

    Рис.
    4.12.
    Готовое приложение ExceptionsMDB

    Кроме обработки исключений, здесь следует обратить внимание на интерфейс, предоставляющий возможность пользователю вводить сведения своей учетной записи. Теперь в целях безопасности приложения не нужно включать пароль в код при использовании мастеров, поскольку строка соединения будет генерироваться динамически. Разумеется, ваше приложение может иметь другой интерфейс — например, окно соединения будет появляться в дочерней форме.

    В программном обеспечении к курсу вы найдете приложение Exceptions MDB (CodeGlava2 ExceptionsMDB).

    Понравилась статья? Поделить с друзьями:
  • Sql state 42p01 ошибка
  • Sql синтаксические ошибки
  • Sql server эта страница содержит ошибки проверки
  • Sql сервер ошибка 233
  • Sql server ошибка 701