К сожалению, многие программисты не склонны тратить время на то, чтобы застраховать свой код PL/SQL от всех возможных неожиданностей. У большинства из нас хватает проблем с написанием кода, реализующего положительные аспекты приложения: управление данными клиентов, построение счетов и т. д.; вдобавок это увеличивает объем работы. Всегда бывает дьявольски сложно — как с психологической точки зрения, так и в отношении расходования ресурсов — сосредоточиться на негативных аспектах работы системы: что, если пользователь нажмет не ту клавишу? А что делать, если база данных Oracle недоступна?
В результате мы пишем приложения PL/SQL, предназначенные для работы в «идеальном мире», где в программах не бывает ошибок, пользователи вводят лишь правильные данные, а все системы — и аппаратные и программные — всегда в полном порядке.
Конечно, жестокая реальность устанавливает свои правила: как бы вы ни старались, в приложении все равно отыщется еще одна ошибка. А ваши пользователи всегда постараются отыскать последовательность нажатий клавиш, от которых форма перестанет работать. Проблема проста: либо вы выделяете время на отладку и защиту своих программ, либо вам придется вести бесконечные бои в отступлении, принимая отчаянные звонки от пользователей и пытаясь потушить разгорающееся пламя.
К счастью, PL/SQL предоставляет достаточно мощный и гибкий механизм перехвата и обработки ошибок. И вполне возможно написать на языке PL/SQL такое приложение, которое полностью защитит от ошибок и всех пользователей, и базу данных Oracle.
Основные концепции и терминология обработки исключений
В языке PL/SQL ошибки всех видов интерпретируются как исключения — ситуации, которые не должны возникать при нормальном выполнении программы.
К числу исключений относятся:
- ошибки, генерируемые системой (например, нехватка памяти или повторяющееся значение индекса);
- ошибки, вызванные действиями пользователя;
- предупреждения, выдаваемые приложением пользователю.
PL/SQL перехватывает ошибки и реагирует на них при помощи так называемых обработчиков исключений. Механизм обработчиков исключений позволяет четко отделить код обработки ошибок от основной логики программы, а также дает возможность реализовать обработку ошибок, управляемую событиями (в отличие от старой линейной модели). Независимо от того, как и по какой причине возникло конкретное исключение, оно всегда обрабатывается одним и тем же обработчиком в разделе исключений.
При возникновении ошибки — как системной, так и ошибки в приложении — в PL/SQL инициируется исключение. В результате выполнение блока прерывается, и управление передается для обработки в раздел исключений текущего блока, если он имеется. После обработки исключения возврат в тот блок, где исключение было инициировано, невозможен, поэтому управление передается во внешний блок.
Схема передачи управления при возникновении исключения показана на рис. 1.
Рис. 1. Архитектура обработки исключений
Существует два типа исключений:
- Системное исключение определяется в Oracle и обычно инициируется исполняемым ядром PL/SQL, обнаружившим ошибку. Одним системным исключениям присваиваются имена (например,
NO_DATA_FOUND
), другие ограничиваются номерами и описаниями. - Исключение, определяемое программистом, актуально только для конкретного приложения. Имя исключения можно связать с конкретной ошибкой Oracle с помощью директивы компилятора
EXCEPTION_INIT
или же назначить ошибке номер и описание процедуройRAISE_APPLICATION_ERROR
.
В этом блоге будут использоваться следующие термины:
- Раздел исключений — необязательный раздел блока PL/SQL (анонимного блока, процедуры, функции, триггера или инициализационного раздела пакета), содержащий один или несколько обработчиков исключений. Структура раздела исключений очень похожа на структуру команды
CASE
, о которой рассказывалось в этом блоге. - Инициировать исключение — значит остановить выполнение текущего блока PL/SQL, оповещая исполняемое ядро об ошибке. Исключение может инициировать либо Oracle, либо ваш собственный программный код при помощи команды
RAISE
или процедурыRAISE_APPLICATION_ERROR
. - Обработать исключение — значит перехватить ошибку, передав управление обработчику исключения. Написанный программистом обработчик может содержать код, который в ответ на исключение выполняет определенные действия (например, записывает информацию об ошибке в журнал, выводит сообщение для пользователя или передает исключение во внешний блок).
- Область действия — часть кода (конкретный блок или весь раздел), в котором может инициироваться исключение, а также часть кода, инициируемые исключения которого могут перехватываться и обрабатываться соответствующим разделом исключений.
- Передача исключения — процесс передачи исключения во внешний блок, если в текущем блоке это исключение не обработано.
- Необработанное исключение — исключение, которое передается без обработки из «самого внешнего» блока PL/SQL. После этого управление передается исполнительной среде, которая уже сама определяет, как отреагировать на исключение (выполнить откат транзакции, вывести сообщение об ошибке, проигнорировать ее и т. д.).
- Анонимное исключение — исключение, с которым связан код ошибки и описание. Такое исключение не имеет имени, которое можно было бы использовать в команде
RAISE
или секцииWHEN
обработчика исключений. - Именованное исключение — исключение, которому имя присвоено либо Oracle (в одном из встроенных пакетов), либо разработчиком. В частности, для этой цели можно использовать директиву компилятора
EXCEPTION_INIT
(в таком случае имя можно будет применять и для инициирования, и для обработки исключения).
Определение исключений
Прежде чем исключение можно будет инициировать и обрабатывать, его необходимо определить. В Oracle заранее определены тысячи исключений, большинство из которых имеют только номера и пояснительные сообщения. Имена присваиваются только самым распространенным исключениям.
Имена присваиваются в пакете STANDARD
(одном из двух пакетов по умолчанию PL/SQL; другой пакет — DBMS_STANDARD
), а также в других встроенных пакетах, таких как UTL_FILE
и DBMS_SQL
. Код, используемый Oracle для определения исключений (таких, как NO_DATA_FOUND
), не отличается от кода, который вы будете использовать для определения или объявления ваших собственных исключений.
Это можно сделать двумя способами, описанными ниже.
Объявление именованных исключений
Исключения PL/SQL, объявленные в пакете STANDARD
и в других встроенных пакетах, представляют внутренние (то есть системные) ошибки. Однако многие проблемы, с которыми будет сталкиваться пользователь приложения, актуальны только в этом конкретном приложении. Возможно, вашей программе придется перехватывать и обрабатывать такие ошибки, как «отрицательный баланс счета» или «дата обращения не может быть меньше текущей даты». Хотя эти ошибки имеют иную природу, нежели, скажем, ошибки «деления на нуль», они также относятся к разряду исключений, связанных с нормальной работой программы, и должны обрабатываться этой программой.
Одной из самых полезных особенностей обработки исключений PL/SQL является отсутствие структурных различий между внутренними ошибками и ошибками конкретных приложений. Любое исключение может и должно обрабатываться в разделе исключений независимо от типа ошибки.
Конечно, для обработки исключения необходимо знать его имя. Поскольку в PL/SQL имена пользовательским исключениям автоматически не назначаются, вы должны делать это самостоятельно, определяя исключения в разделе объявлений блока PL/SQL. При этом задается имя исключения, за которым следует ключевое слово EXCEPTION
:
имя_исключения EXCEPTION;
Следующий раздел объявлений процедуры calc_annual_sales
содержит два объявления исключений, определяемых программистом:
PROCEDURE calc_annual_sales(company_id_in IN company.company_id%TYPE)
IS
invalid_company_id EXCEPTION;
negative_balance EXCEPTION;
duplicate_company BOOLEAN;
BEGIN
... исполняемые команды ...
EXCEPTION
WHEN NO_DATA_FOUND -- системное исключение
THEN
...
WHEN invalid_company_id
THEN
WHEN negative_balance
THEN
...
END;
По своему формату имена исключений схожи с именами других переменных, но ссылаться на них можно только двумя способами:
- В команде RAISE, находящейся в исполняемом разделе программы (для инициирования исключения):
RAISE invalid_company_id;
- В секции WHEN раздела исключений (для обработки инициированного исключения):
WHEN invalid_company_id THEN
Связывание имени исключения с кодом ошибки
В Oracle, как уже было сказано, имена определены лишь для самых распространенных исключений. Тысячи других ошибок в СУБД имеют лишь номера и снабжены пояснительными сообщениями. Вдобавок инициировать исключение с номером ошибки (в диапазоне от –20 999 до –20 000) может и разработчик приложения, воспользовавшись для этой цели процедурой RAISE_APPLICATION_ERROR
(см. далее раздел «Инициирование исключений»).
Наличие в программном коде исключений без имен вполне допустимо, но такой код малопонятен и его трудно сопровождать. Допустим, вы написали программу, при выполнении которой Oracle выдает ошибку, связанную с данными, например ORA-01843: not a valid month
. Для перехвата этой ошибки в программу включается обработчик следующего вида:
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE = -1843 THEN
Но код получается совершенно непонятным. Чтобы сделать смысл этого кода более очевидным, следует воспользоваться директивой EXCEPTION_INIT
.
Встроенная функция SQLCODE возвращает номер последней сгенерированной ошибки. Она будет рассмотрена далее в разделе «Обработка исключений» этой статьи.
Директива EXCEPTION_INIT
Директива компилятора EXCEPTION_INIT
(команда, выполняемая во время компиляции) связывает идентификатор, объявленный с ключевым словом EXCEPTION
, с внутренним кодом ошибки. Установив такую связь, можно инициировать исключение по имени и указать это имя в условии WHEN
обработчика ошибок.
С директивой EXCEPTION_INIT
условие WHEN
, использованное в предыдущем примере, приводится к следующему виду:
PROCEDURE my_procedure
IS
invalid_month EXCEPTION;
PRAGMA EXCEPTION_INIT (invalid_month, −1843);
BEGIN
...
EXCEPTION
WHEN invalid_month THEN
Жесткое кодирование номера ошибки становится излишним; имя ошибки говорит само за себя.
Директива EXCEPTION_INIT
должна располагаться в разделе объявлений блока. Указанное в ней исключение должно быть объявлено либо в том же блоке, либо во внешнем, либо в спецификации пакета. Синтаксис директивы в анонимном блоке:
DECLARE
имя_исключения EXCEPTION;
PRAGMA EXCEPTION_INIT (имя_исключения, целое_число);
Здесь имя_исключения
— имя исключения, объявляемого программистом, а целое_число
— номер ошибки Oracle, которую следует связать с данным исключением. Номером ошибки может служить любое число со следующими ограничениями:
- Номер ошибки не может быть равен –1403 (один из двух кодов ошибок
NO_DATA_FOUND
). Если вы по какой-либо причине захотите связать свое именованное исключение с этой ошибкой, передайте директивеEXCEPTION_INIT
значение 100. - Номер ошибки не может быть равен 0 или любому положительному числу, кроме 100.
- Номер ошибки не может быть отрицательным числом, меньшим –1 000 000.
Рассмотрим пример возможного объявления исключения. В приведенном ниже программном коде я объявляю и связываю исключение со следующим номером:
ORA-2292 integrity constraint (OWNER.CONSTRAINT) violated -
child record found.
Ошибка происходит при попытке удаления родительской записи, у которой в таблице имеются дочерние записи (то есть записи с внешним ключом, ссылающимся на родительскую запись):
PROCEDURE delete_company (company_id_in IN NUMBER)
IS
/* Объявление исключения. */
still_have_employees EXCEPTION;
/* Имя исключения связывается с номером ошибки. */
PRAGMA EXCEPTION_INIT (still_have_employees, 2292);
BEGIN
/* Попытка удаления информации о компании. */
DELETE FROM company
WHERE company_id = company_id_in;
EXCEPTION
/* При обнаружении дочерних записей инициируется это исключение! */
WHEN still_have_employees
THEN
DBMS_OUTPUT.PUT_LINE
('Пожалуйста, сначала удалите данные о служащих компании.');
END;
Рекомендации по использованию EXCEPTION_INIT
Директиву EXCEPTION_INIT
целесообразно использовать в двух ситуациях:
- при необходимости присвоить имя безымянному системному исключению, задействованному в программе (следовательно, если в Oracle не определено имя для некоторой ошибки, это еще не означает, что с ней можно работать только по номеру);
- когда нужно присвоить имя специфическому для приложения исключению, инициируемому процедурой
RAISE_APPLICATION_ERROR
(см. далее раздел «Инициирование исключений»). Это позволяет обрабатывать данное исключение по имени, а не по номеру.
В обоих случаях все директивы EXCEPTION_INIT
желательно объединить в пакет, чтобы определения исключений не были разбросаны по всему коду приложения. Допустим, вы интенсивно используете динамический SQL, и при выполнении запросов часто возникает ошибка «invalid column name» (неверное имя столбца). Запоминать код ошибки не хочется, но и определять директивы имя для исключения в 20 разных программах тоже неразумно. Поэтому имеет смысл определить собственные «системные исключения» в отдельном пакете для работы с динамическим SQL:
CREATE OR REPLACE PACKAGE dynsql
IS
invalid_table_name EXCEPTION;
PRAGMA EXCEPTION_INIT (invalid_table_name, -903);
invalid_identifier EXCEPTION;
PRAGMA EXCEPTION_INIT (invalid_identifier, -904);
Теперь перехват этих ошибок в программе может производиться следующим образом:
WHEN dynsql.invalid identifier THEN ...
Аналогичный подход рекомендуется использовать при работе с кодами ошибок –20NNN, передаваемыми процедуре RAISE_APPLICATION_ERROR
(см. далее в этой заметке моего блога). Создайте пакет, в котором этим кодам будут присваиваться имена. Он может выглядеть примерно так:
PACKAGE errnums
IS
en_too_young CONSTANT NUMBER := -20001;
exc_too_young EXCEPTION;
PRAGMA EXCEPTION_INIT (exc_too_young, -20001);
en_sal_too_low CONSTANT NUMBER := -20002;
exc_sal_too_low EXCEPTION;
PRAGMA EXCEPTION_INIT (exc_sal_too_low , -20002);
END errnums;
При наличии такого пакета можно использовать код следующего вида, не указывая номер ошибки в коде:
PROCEDURE validate_emp (birthdate_in IN DATE)
IS
min_years CONSTANT PLS_INTEGER := 18;
BEGIN
IF ADD_MONTHS (SYSDATE, min_years * 12 * -1) < birthdate_in
THEN
RAISE_APPLICATION_ERROR
(errnums.en_too_young,
'Возраст работника должен быть не менее ' || min_years || ' лет.');
END IF;
END;
Именованные системные исключения
В Oracle для относительно небольшого количества исключений определены стандартные имена, задаваемые директивой компилятора EXCEPTION_INIT
во встроенных пакетах. Самые важные и часто применяемые из них определены в пакете STANDARD
. Так как это один из двух используемых по умолчанию пакетов PL/SQL, на определенные в нем исключения можно ссылаться без префикса с именем пакета. Например, если потребуется инициировать в программе исключение NO_DATA_FOUND
, это можно сделать любой из следующих команд:
WHEN NO_DATA_FOUND THEN
WHEN STANDARD.NO_DATA_FOUND THEN
Определения стандартных именованных исключений встречаются и в других встроенных пакетах — например, в пакете DBMS_LOB
, предназначенном для работы с большими объектами. Пример одного такого определения из указанного пакета:
invalid_argval EXCEPTION;
PRAGMA EXCEPTION_INIT(invalid_argval, -21560);
Поскольку пакет DBMS_LOB
не используется по умолчанию, перед ссылкой на это исключение необходимо указать имя пакета:
WHEN DBMS_LOB.invalid_argval THEN...
Многие исключения, определенные в пакете STANDARD
, перечислены в табл. 1. Для каждого из них приводится номер ошибки Oracle, значение, возвращаемое при вызове SQLCODE
(встроенная функция SQLCODE
, которая возвращает текущий код ошибки — см. раздел «Встроенные функции ошибок»), и краткое описание. Значение, возвращаемое SQLCODE
, совпадает с кодом ошибки Oracle, с одним исключением: определяемый стандартом ANSI код ошибки NO_DATA_FOUND
равен 100.
Имя исключения/Ошибка Oracle/SQLCODE | Описание |
CURSOR_ALREADY_OPEN ORA-6511 SQLCODE = –6511 | Попытка открытия курсора, который был открыт ранее. Перед повторным открытием курсор необходимо сначала закрыть |
DUP_VAL_ON_INDEX ORA-00001 SQLCODE = −1 | Команда INSERT или UPDATE пытается сохранить повторяющиеся значения в столбцах, объявленных с ограничением UNIQUE |
INVALID_CURSOR ORA-01001 SQLCODE = −1001 | Ссылка на несуществующий курсор. Обычно ошибка встречается при попытке выборки данных из неоткрытого курсора или закрытия курсора до его открытия |
INVALID_NUMBER ORA-01722 SQLCODE = −1722 | Выполняемая SQL-команда не может преобразовать символьную строку в число. Это исключение отличается от VALUE_ERROR тем, что оно инициируется только из SQL-команд |
LOGIN_DENIED ORA-01017 SQLCODE = −1017 | Попытка программы подключиться к СУБД Oracle с неверным именем пользователя или паролем. Исключение обычно встречается при внедрении кода PL/SQL в язык 3GL |
NO_DATA_FOUND ORA-01403 SQLCODE = +100 | Исключение инициируется в трех случаях: (1) при выполнении инструкции SELECT INTO (неявный курсор), которая не возвращает ни одной записи; (2) при ссылке на неинициализированную запись локальной таблицы PL/SQL; (3) при попытке выполнить операцию чтения после достижения конца файла при использовании пакета UTL_FILE |
NOT_LOGGED ON ORA-01012 SQLCODE = −1012 | Программа пытается обратиться к базе данных (обычно из инструкции DML) до подключения к СУБД Oracle |
PROGRAM_ERROR ORA-06501 SQLCODE = −6501 | Внутренняя программная ошибка PL/SQL. В сообщении об ошибке обычно предлагается обратиться в службу поддержки Oracle |
STORAGE_ERROR ORA-06500 SQLCODE = −6500 | Программе PL/SQL не хватает памяти или память по какой-то причине повреждена |
TIMEOUT_ON_RESOURCE ORA-00051 SQLCODE = −51 | Тайм-аут СУБД при ожидании ресурса |
TOO_MANY_ROWS ORA-01422 SQLCODE = −1422 | Команда SELECT INTO возвращает несколько записей, хотя должна возвращать лишь одну (в таких случаях инструкция SELECT включается в явное определение курсора, а записи выбираются по одной) |
TRANSACTION_BACKED_OUT ORA-00061 SQLCODE = −61 | Удаленная часть транзакции отменена либо при помощи явной инструкции ROLLBACK , либо в результате какого-то другого действия (например, неудачного выполнения команды SQL или DML в удаленной базе данных) |
VALUE_ERROR ORA-06502 SQLCODE = −6502 | Ошибка связана с преобразованием, усечением или проверкой ограничений числовых или символьных данных. Это общее и очень распространенное исключение. Если подобная ошибка содержится в инструкции SQL или DML, то в блоке PL/SQL инициируется исключение INVALID_NUMBER |
ZERO_DIVIDE ORA-01476 SQLCODE = −1476 | Попытка деления на ноль |
Рассмотрим пример использования этой таблицы исключений. Предположим, ваша программа инициирует необрабатываемое исключение для ошибки ORA-6511. Заглянув в таблицу, вы видите, что она связана с исключением CURSOR_ALREADY_OPEN
. Найдите блок PL/SQL, в котором произошла ошибка, и добавьте в него обработчик исключения
CURSOR_ALREADY_OPEN:
EXCEPTION
WHEN CURSOR_ALREADY_OPEN
THEN
CLOSE my_cursor;
END;
Конечно, еще лучше было бы проанализировать весь программный код и заранее определить, какие из стандартных исключений в нем могут инициироваться. В таком случае вы сможете решить, какие исключения следует обрабатывать конкретно, какие следует включить в конструкцию WHEN OTHERS
(см. далее), а какие оставить необработанными.
Область действия исключения
Областью действия исключения называется та часть программного кода, к которой оно относится, то есть блок, где данное исключение может быть инициировано. В следующей таблице указаны области действия исключений четырех разных типов.
Тип исключения | Область действия |
Именованное системное исключение | Исключение является глобальным, то есть не ограничивается каким-то конкретным блоком кода. Системные исключения могут инициироваться и обрабатываться в любом блоке |
Именованное исключение, определяемое программистом | Исключение может инициироваться и обрабатываться только в исполнительном разделе и разделе исключений, входящих в состав блока, где объявлено данное исключение (или в состав любого из вложенных в него блоков). Если исключение определено в спецификации пакета, то его областью действия являются все те программы, владельцы которых обладают для этого пакета привилегией EXECUTE |
Анонимное системное исключение | Исключение может обрабатываться в секции WHEN OTHERS любого раздела исключений PL/SQL. Если присвоить ему имя, то его область действия будет такой же, как у именованного исключения, определяемого программистом |
Анонимное исключение, определяемое программистом | Исключение определяется в вызове процедуры RAISE_APPLICATION_ERROR , а затем передается в вызывающую программу |
Рассмотрим пример исключения overdue_balance
, объявленного в процедуре check_account
(таким образом, область его действия ограничивается указанной процедурой):
PROCEDURE check_account (company_id_in IN NUMBER)
IS
overdue_balance EXCEPTION;
BEGIN
... исполняемые команды ...
LOOP
...
IF ... THEN
RAISE overdue_balance;
END IF;
END LOOP;
EXCEPTION
WHEN overdue_balance THEN ...
END;
С помощью команды RAISE
исключение overdue_balance
можно инициировать в процедуре check_account
, но не в программе, которая ее вызывает. Например, для следующего анонимного блока компилятор выдает ошибку:
DECLARE
company_id NUMBER := 100;
BEGIN
check_account (100);
EXCEPTION
WHEN overdue_balance /* В PL/SQL такая ссылка недопустима. */
THEN ...
END;
PLS-00201: identifier "OVERDUE_BALANCE" must be declared
Для приведенного выше анонимного блока процедура check_account
является «черным ящиком». Все объявленные в ней идентификаторы, в том числе идентификаторы исключения, не видны для внешнего программного кода.
Инициирование исключений
Исключение может быть инициировано приложением в трех случаях:
- Oracle инициирует исключение при обнаружении ошибки;
- приложение инициирует исключение командой
RAISE
; - исключение инициируется встроенной процедурой
RAISE_APPLICATION_ERROR
.
Как Oracle инициирует исключения, вы уже знаете. Теперь давайте посмотрим, как это может сделать программист.
Команда RAISE
Чтобы программист имел возможность самостоятельно инициировать именованные исключения, в Oracle поддерживается команда RAISE
. С ее помощью можно инициировать как собственные, так и системные исключения. Команда имеет три формы:
RAISE имя_исключения;
RAISE имя_пакета.имя_исключения;
RAISE;
Первая форма (без имени пакета) может инициировать исключения, определенные в текущем блоке (или в содержащем его блоке), а также системные исключения, объявленные в пакете STANDARD
. Далее приводятся два примера, в первом из которых инициируется исключение, определенное программистом:
DECLARE
invalid_id EXCEPTION; -- Все идентификаторы должны начинаться с буквы 'X'.
id_value VARCHAR2(30);
BEGIN
id_value := id_for ('SMITH');
IF SUBSTR (id_value, 1, 1) != 'X'
THEN
RAISE invalid_id;
END IF;
...
END;
При необходимости вы всегда можете инициировать системное исключение:
BEGIN
IF total_sales = 0
THEN
RAISE ZERO_DIVIDE; -- Определено в пакете STANDARD
ELSE
RETURN (sales_percentage_calculation (my_sales, total_sales));
END IF;
END;
Если исключение объявлено в пакете (но не в STANDARD
) и инициируется извне, имя исключения необходимо уточнить именем пакета:
IF days_overdue (isbn_in, borrower_in) > 365
THEN
RAISE overdue_pkg.book_is_lost;
END IF;
Третья форма RAISE
не требует указывать имя исключения, но используется только в условии WHEN
раздела исключений. Ее синтаксис предельно прост:
RAISE;
Используйте эту форму для повторного инициирования (передачи) перехваченного исключения:
EXCEPTION
WHEN NO_DATA_FOUND
THEN
-- Используем общий пакет для сохранений всей контекстной
-- информации: код ошибки, имя программы и т. д.
errlog.putline (company_id_in);
-- А теперь исключение NO_DATA_FOUND передается
-- в родительский блок без обработки.
RAISE;
Эта возможность особенно полезна в тех случаях, когда информацию об ошибке нужно записать в журнал, а сам процесс обработки возложить на родительский блок. Таким образом выполнение родительских блоков завершается без потери информации об ошибке.
Процедура RAISE_APPLICATION_ERROR
Для инициирования исключений, специфических для приложения, Oracle предоставляет процедуру RAISE_APPLICATION_ERROR
(определенную в используемом по умолчанию пакете DBMS_STANDARD
). Ее преимущество перед командой RAISE (которая тоже может инициировать специфические для приложения явно объявленные исключения) заключается в том, что она позволяет связать с исключением сообщение об ошибке.
При вызове этой процедуры выполнение текущего блока PL/SQL прекращается, а любые изменения аргументов OUT
и IN OUT
(если таковые имеются) отменяются. Изменения, внесенные в глобальные структуры данных (с помощью команды INSERT, UPDATE, MERGE
или DELETE
), такие как переменные пакетов и объекты баз данных, не отменяются. Для отката DML-команд необходимо явно указать в разделе обработки исключений команду ROLLBACK
.
Заголовок этой процедуры (определяемый в пакете DBMS_STANDARD
) выглядит так:
PROCEDURE RAISE_APPLICATION_ERROR (
num binary_integer,
msg varchar2,
keeperrorstack boolean default FALSE);
Здесь num
— номер ошибки из диапазона от –20 999 до –20 000 (только представьте: все остальные отрицательные числа Oracle резервирует для собственных исключений!); msg
— сообщение об ошибке, длина которого не должна превышать 2048 символов (символы, выходящие за эту границу, игнорируются); аргумент keeperrorstack
указывает, хотите ли вы добавить ошибку к уже имеющимся в стеке (TRUE
), или заменить существующую ошибку (значение по умолчанию — FALSE
).
Oracle выделяет диапазон номеров от –20 999 до –20 000 для пользовательских ошибок, но учтите, что в некоторых встроенных пакетах, в том числе в DBMS_OUTPUT
и DBMS_DESCRIBE
, номера от –20 005 до –20 000 все равно присваиваются системным ошибкам. За дополнительной информацией обращайтесь к документации пакетов.
Рассмотрим пример полезного применения этой встроенной процедуры. Допустим, мы хотим, чтобы сообщения об ошибках выдавались пользователям на разных языках. Создадим для них таблицу error_table
и определим в ней язык каждого сообщения значением столбца string_language
. Затем создается процедура, которая генерирует заданную ошибку, загружая соответствующее сообщение из таблицы с учетом языка текущего сеанса:
PROCEDURE raise_by_language (code_in IN PLS_INTEGER)
IS
l_message error_table.error_string%TYPE;
BEGIN
SELECT error_string
INTO l_message
FROM error_table
WHERE error_number = code_in
AND string_language = USERENV ('LANG');
RAISE_APPLICATION_ERROR (code_in, l_message);
END;
Обработка исключений
Как только в программе возникает исключение, нормальное выполнение блока PL/SQL останавливается, и управление передается в раздел исключений. Затем исключение либо обрабатывается обработчиком исключений в текущем блоке PL/SQL, либо передается в родительский блок.
Чтобы обработать или перехватить исключение, нужно написать для него обработчик. Обработчики исключений располагаются после всех исполняемых команд блока, но перед завершающим ключевым словом END
. Начало раздела исключений отмечает ключевое слово EXCEPTION
:
DECLARE
... объявления ...
BEGIN
... исполняемые команды ...
[ EXCEPTION
... обработчики исключений ... ]
END;
Синтаксис обработчика исключений может быть таким:
WHEN имя_исключения [ OR имя_исключения ... ]
THEN
исполняемые команды
или таким:
WHEN OTHERS
THEN
исполняемые команды
В одном разделе исключений может быть несколько их обработчиков. Структура обработчиков напоминает структуру условной команды CASE
.
Свойство | Описание |
EXCEPTION WHEN NO_DATA_FOUND THEN исполняемые_команды1; | Если инициировано исключение NO_DATA_FOUND , выполнить первый набор команд |
WHEN payment_overdue THEN исполняемые_команды2; | Если просрочена оплата, выполнить второй набор команд |
WHEN OTHERS THEN исполняемые_команды3; END; | Если инициировано иное исключение, выполнить третий набор команд |
Если имя, заданное в условии WHEN
, совпадает с инициированным исключением, то это исключение обрабатывается соответствующим набором команд. Обратите внимание: исключения перехватываются по именам, а не по кодам ошибок. Но если инициированное исключение не имеет имени или его имя не соответствует ни одному из имен, указанных в условиях WHEN
, тогда оно обрабатывается командами, заданными в секции WHEN OTHERS
(если она имеется). Любая ошибка может быть перехвачена только одним обработчиком исключений. После выполнения команд обработчика управление сразу же передается из текущего блока в родительский или вызывающий блок.
Секция WHEN OTHERS
не является обязательной. Когда она отсутствует, все необработанные исключения немедленно передаются в родительский блок, если таковой имеется. Секция WHEN OTHERS
должна быть последним обработчиком исключений в блоке. Если разместить после нее еще одну секцию WHEN
, компилятор выдаст сообщение об ошибке.
Встроенные функции ошибок
Прежде чем переходить к изучению тонкостей обработки ошибок, мы сначала вкратце познакомимся со встроенными функциями Oracle, предназначенными для идентификации, анализа и реагирования на ошибки, возникающие в приложениях PL/SQL.
SQLCODE
Функция SQLCODE
возвращает код ошибки последнего исключения, инициированного в блоке. При отсутствии ошибок SQLCODE
возвращает 0. Кроме того, SQLCODE
возвращает 0 при вызове за пределами обработчика исключений.
База данных Oracle поддерживает стек значений SQLCODE
. Допустим, к примеру, что функция FUNC инициирует исключение VALUE_ERROR
(–6502). В разделе исключений FUNC
вызывается процедура PROC
, которая инициирует исключение DUP_VAL_ON_INDEX
(–1). В разделе исключений PROC
функция SQLCODE
возвращает значение –1. Но когда управление передается в раздел исключений FUNC
, SQLCODE
будет возвращать –6502.
SQLERRM
Функция SQLERRM
возвращает сообщение об ошибке для заданного кода ошибки. Если вызвать SQLERRM
без указания кода ошибки, функция вернет сообщение, связанное со значением, возвращаемым SQLCODE
. Например, если SQLCODE
возвращает 0, функция SQLERRM
вернет следующую строку:
ORA-0000: normal, successful completion
Если же SQLCODE
возвращает 1 (обобщенный код ошибки для исключения, определяемого пользователем), SQLERRM
вернет строку:
User-Defined Exception
Пример вызова SQLERRM
для получения сообщения об ошибке для конкретного кода:
1 BEGIN
2 DBMS_OUTPUT.put_line (SQLERRM (-1403));
3* END;
SQL> /
ORA-01403: no data found
Максимальная длина строки, возвращаемой SQLERRM
, составляет 512 байт (в некоторых ранних версиях Oracle — 255 байт). Из-за этого ограничения Oracle Corporation рекомендует вызывать функцию DBMS_UTILITY
.FORMAT_ERROR_STACK
, чтобы гарантировать вывод полной строки (эта встроенная функция не усекает текст до 2000 байт).
DBMS_UTILITY.FORMAT_ERROR_STACK
Эта встроенная функция, как и SQLERRM
, возвращает сообщение, связанное с текущей ошибкой (то есть значение, возвращаемое SQLCODE
). Ее отличия от SQLERRM
:
- Она возвращает до 1899 символов сообщения, что позволяет избежать проблем с усечением.
- Этой функции не может передаваться код ошибки; соответственно, она не может использоваться для получения сообщения, соответствующего произвольному коду.
Как правило, эта функция вызывается в логике обработчика исключения для получения полного сообщения об ошибке.
Хотя в имя функции входит слово «stack», она не возвращает информацию о стеке ошибок, приведшем к строке, в которой изначально была инициирована ошибка. Эту задачу решает DBMS_UTILITY
.FORMAT_ERROR_BACKTRACE
.
DBMS_UTILITY.FORMAT_ERROR_BACKTRACE
Эта функция, появившаяся в Oracle10g, возвращает отформатированную строку с содержимым стека программ и номеров строк. Ее выходные данные позволяют отследить строку, в которой изначально была инициирована ошибка.
Тем самым заполняется весьма существенный пробел в функциональности PL/SQL. В Oracle9i и предшествующих версиях после обработки исключения в блоке PL/ SQL было невозможно определить строку, в которой произошла ошибка (возможно, самая важная информация для разработчика). Если программист хотел получить эту информацию, он должен был разрешить прохождение необработанного исключения, чтобы полная трассировочная информация ошибки была выведена на экран. Ситуация более подробно описана в следующем разделе.
DBMS_UTILITY.FORMAT_CALL_STACK
Функция возвращает отформатированную строку со стеком вызовов в приложении PL/SQL. Практическая полезность функции не ограничивается обработкой ошибок; она также пригодится для трассировки выполнения вашего кода.
В Oracle Database 12c появился пакет UTL_CALL_STACK
, который также предоставляет доступ к стеку вызовов, стеку ошибок и информации обратной трассировки.
Подробнее о DBMS_UTILITY.FORMAT_ERROR_BACKTRACE
Функцию DBMS_UTILITY
.FORMAT_ERROR_BACKTRACE
следует вызывать в обработчике исключения. Она выводит содержимое стека выполнения в точке инициирования исключения. Таким образом, вызов DBMS_UTILITY
.FORMAT_ERROR_BACKTRACE
в разделе исключений на верхнем уровне стека позволит узнать, где именно в стеке вызовов произошла ошибка. Рассмотрим следующий сценарий: мы определяем процедуру proc3
, которая вызывает процедуру proc2
, а последняя, в свою очередь, вызывает proc1
. Процедура proc1 инициирует исключение:
CREATE OR REPLACE PROCEDURE proc1 IS
BEGIN
DBMS_OUTPUT.put_line ('выполнение proc1');
RAISE NO_DATA_FOUND;
END;
/
CREATE OR REPLACE PROCEDURE proc2 IS
l_str VARCHAR2 (30) := 'вызов proc1';
BEGIN
DBMS_OUTPUT.put_line (l_str);
proc1;
END;
/
CREATE OR REPLACE PROCEDURE proc3 IS
BEGIN
DBMS_OUTPUT.put_line ('вызов proc2');
proc2;
EXCEPTION
WHEN OTHERS
THEN
DBMS_OUTPUT.put_line ('Стек ошибок верхнего уровня:');
DBMS_OUTPUT.put_line (DBMS_UTILITY.format_error_backtrace);
END;
/
Единственной программой с обработчиком ошибок является внешняя процедура proc3
. Вызов функции трассировки включен в обработчик WHEN OTHERS
процедуры proc3
. При выполнении этой процедуры будет получен следующий результат:
SQL> SET SERVEROUTPUT ON
SQL> BEGIN
2 DBMS_OUTPUT.put_line ('Proc3 -> Proc2 -> Proc1 backtrace');
3 proc3;
4 END;
5 /
Proc3 -> Proc2 -> Proc1 backtrace
вызов proc2
вызов proc1
выполнение proc1
Error stack at top level:
ORA-06512: at "SCOTT.PROC1", line 4
ORA-06512: at "SCOTT.PROC2", line 5
ORA-06512: at "SCOTT.PROC3", line 4
Как видите, функция трассировки выводит в начале стека номер строки proc1
, в которой произошла исходная ошибка.
Часто исключение происходит где-то в глубине стека вызовов. Если вы хотите, чтобы оно было передано во внешний блок PL/SQL, вероятно, вам придется заново инициировать его в каждом обработчике стека блоков. Функция DBMS_UTILITY
.FORMAT_ERROR_BACKTRACE
выдает трассировку исполнения вплоть до последней команды RAISE
в сеансе пользователя. Учтите, что вызов RAISE
для конкретного исключения или повторное инициирование текущего исключения приводит к инициализации стека, выдаваемого DBMS_UTILITY
.FORMAT_ERROR_BACKTRACE
. Таким образом, если вы хотите использовать эту функцию, возможны два пути:
- Вызовите функцию в разделе исключений блока, в котором была инициирована ошибка. Это позволит вам получить (и сохранить в журнале) номер ошибки, даже если исключение было заново инициировано в дальнейшей позиции стека.
- Обойдите обработчики исключений в промежуточных программах вашего стека и вызовите функцию в разделе исключений внешней программы в стеке.
Только номер строки, пожалуйста
В реальном приложении трассировка ошибок может быть очень длинной. Как правило, специалиста, занимающегося отладкой или поддержкой, не интересует весь стек — достаточно только последнего элемента. Возможно, разработчику приложения стоит вывести эту важную информацию, чтобы пользователь мог немедленно и точно описать суть проблемы группе поддержки.
В такой ситуации необходимо разобрать строку с данными трассировки и извлечь из нее последний элемент. Я написал для этого специальную программу и оформил ее в пакет BT
. В этом пакете реализован простой, понятный интерфейс:
PACKAGE bt
IS
TYPE error_rt IS RECORD (
program_owner all_objects.owner%TYPE
, program_name all_objects.object_name%TYPE
, line_number PLS_INTEGER
);
FUNCTION info (backtrace_in IN VARCHAR2)
RETURN error_rt;
PROCEDURE show_info (backtrace_in IN VARCHAR2);
END bt;
Тип записи error_rt
содержит отдельное поле для каждого возвращаемого элемента трассировки (владелец программного модуля, имя программного модуля и номер строки в программе). Затем вместо того, чтобы вызывать функцию трассировки в каждом разделе исключения и разбирать ее результаты, я вызываю функцию bt.info
и вывожу конкретную информацию об ошибке.
Полезные применения SQLERRM
Вы можете использовать DBMS_UTILITY
.FORMAT_ERROR_STACK
вместо SQLERRM
, но это не означает, что функция SQLERRM
совершенно неактуальна. В частности, она поможет вам получить ответ на следующие вопросы:
- Является ли заданное число действительным кодом ошибки Oracle?
- Какое сообщение соответствует коду ошибки?
Как упоминалось ранее в нашей статье, функция SQLERRM
возвращает сообщение об ошибке для заданного кода. Но если передать SQLERRM
недействительный код, исключение не инициируется. Вместо этого возвращается строка в одном из двух форматов:
- Если число отрицательно:
ORA-NNNNN: Message NNNNN not found; product=RDBMS; facility=ORA
- Если число положительно или меньше −65535:
-N: non-ORACLE exception
Этим обстоятельством можно воспользоваться для построения функций, возвращающих точную информацию о том коде, с которым вы работаете в настоящее время. Ниже приведена спецификация пакета с этими программами:
PACKAGE oracle_error_info
IS
FUNCTION is_app_error (code_in IN INTEGER)
RETURN BOOLEAN;
FUNCTION is_valid_oracle_error (
code_in IN INTEGER
, app_errors_ok_in IN BOOLEAN DEFAULT TRUE
, user_error_ok_in IN BOOLEAN DEFAULT TRUE
)
RETURN BOOLEAN;
PROCEDURE validate_oracle_error (
code_in IN INTEGER
, message_out OUT VARCHAR2
, is_valid_out OUT BOOLEAN
, app_errors_ok_in IN BOOLEAN DEFAULT TRUE
, user_error_ok_in IN BOOLEAN DEFAULT TRUE
);
END oracle_error_info;
Объединение нескольких исключений в одном обработчике
В одном условии WHEN
можно оператором OR
объединить несколько исключений — подобно тому, как этим оператором объединяются логические выражения:
WHEN invalid_company_id OR negative_balance
THEN
В одном обработчике также можно комбинировать имена пользовательских и системных исключений:
WHEN balance_too_low OR ZERO_DIVIDE OR DBMS_LDAP.INVALID_SESSION
THEN
Впрочем, применять оператор AND
в такой комбинации нельзя, потому что в любой момент времени может быть инициировано только одно исключение.
Необработанные исключения
Исключение, инициированное в программе, но не обработанное в соответствующем разделе текущего или родительского блока PL/SQL, называется необработанным. PL/ SQL возвращает сообщение об ошибке, вызвавшей необработанное исключение, в ту среду, где была запущена данная программа. Эта среда (ею может быть SQL*Plus. Oracle Forms, программа на языке Java и т. д.) действует по ситуации. В частности, SQL*Plus осуществляет откат всех DML-инструкций, выполненных в родительском блоке.
Одним из важнейших моментов, связанных с проектированием архитектуры приложения, является вопрос о том, разрешается ли в нем использовать необработанные исключения. Такие исключения разными средами обрабатываются по-разному, и не всегда это делается корректно. Если ваша программа PL/SQL вызывается не из PL/SQL-среды, в ее «самом внешнем» блоке можно запрограммировать следующие действия:
- перехват всех исключений, которые могли быть переданы до текущей точки;
- запись информации об ошибке в журнал, с тем чтобы впоследствии ее мог проанализировать разработчик;
- возврат кода состояния, описания и другой информации, необходимой управляющей среде для выбора оптимального варианта действий.
Передача необработанного исключения
Блок, в котором может быть инициировано исключение, определяется правилами области действия исключений. В программе инициированное исключение распространяется в соответствии с определенными правилами.
Сначала PL/SQL ищет обработчик исключения в текущем блоке (анонимном блоке, процедуре или функции). Если такового нет, исключение передается в родительский блок. Затем PL/SQL пытается обработать исключение, инициировав его еще раз в родительском блоке. И так происходит в каждом внешнем по отношению к другому блоке до тех пор, пока все они не будут исчерпаны (рис. 2). После этого PL/SQL возвращает необработанное исключение в среду приложения, выполнившего «самый внешний» блок PL/SQL. И только теперь исключение может прервать выполнение основной программы.
Рис. 2. Передача исключений во вложенных блоках PL/SQL
Потеря информации об исключении
Структура процесса обработки локальных, определяемых программистом исключений в PL/SQL такова, что можно легко потерять информацию об исключении (то есть о том, какая именно произошла ошибка). Пример:
BEGIN
<<local_block>>
DECLARE
case_is_not_made EXCEPTION;
BEGIN
...
END local_block;
Допустим, мы забыли включить в этот блок раздел исключений. Область действия исключения case_is_not_made
ограничена блоком local_block
. Если исключение не обрабатывается в данном блоке, оно передается в родительский, где нет никакой информации о нем. Известно только то, что произошла ошибка, а какая именно — неизвестно. Ведь все пользовательские исключения имеют один и тот же номер ошибки 1 и одно и то же сообщение «User Defined Exception» — если только вы не воспользуетесь директивой EXCEPTION_INIT
, чтобы связать с объявленным исключением другой номер, и не присвоите ему другое сообщение об ошибке при вызове RAISE_APPLICATION_ERROR
.
Таким образом, локально объявленные (и инициированные) исключения всегда следует обрабатывать по имени.
Примеры передачи исключения
Рассмотрим несколько примеров передачи исключений через внешние блоки. На рис. 3 показано, как исключение too_many_faults
, инициированное во внутреннем блоке, обрабатывается в следующем — внешнем — блоке. Внутренний блок содержит раздел исключений, так что PL/SQL сначала проверяет, обрабатывается ли в этом разделе инициированное исключение too_many_faults
.
Рис. 3. Передача исключений во вложенных блоках PL/SQL
А поскольку оно не обрабатывается, PL/SQL закрывает этот блок и инициирует исключение too_many_faults
во внешнем блоке, обозначенном на рисунке как вложенный блок 1. (Используемые команды, расположенные после вложенного блока 2, не выполняются.) Затем просматривается раздел исключений этого блока с целью поиска обработчика исключения too_many_faults
, который обрабатывает его и передает управление процедуре list_my_faults
.
Обратите внимание: если исключение NO_DATA_FOUND
будет инициировано в «самом внутреннем» блоке, то оно будет обработано в разделе исключений этого же блока. Затем управление передается во вложенный блок 1 и будут выполнены исполняемые команды, расположенные после вложенного блока 2.
На рис. 4 представлен пример обработки в «самом внешнем» блоке исключения, инициированного во внутреннем блоке. В изображенной ситуации раздел исключений присутствует только во внешнем блоке, поэтому когда во вложенном блоке 2 инициируется исключение too_many_faults
, PL/SQL прекращает выполнение этого блока и инициирует данное исключение в его родительском блоке, то есть вложенном блоке 1. Но поскольку и у него нет раздела исключений, управление передается «самому внешнему» блоку, процедуре list_my_faults
. В этой процедуре имеется раздел исключений, поэтому PL/ SQL проверяет его, находит обработчик исключения too_many_faults
, выполняет имеющийся там код и передает управление программе, вызвавшей процедуру list_my_faults
.
Рис. 4. Исключение, инициированное во вложенном блоке,
обрабатывается в «самом внешнем» блоке
Продолжение выполнения после исключений
Когда в блоке PL/SQL инициируется исключение, нормальная последовательность выполнения программы прерывается, а управление передается в раздел исключений. Вернуться к исполняемому разделу блока после возникновения в нем исключения уже не удастся. Впрочем, в некоторых ситуациях требуется именно это — продолжить выполнение программы после обработки исключения.
Рассмотрим следующий сценарий: требуется написать процедуру, которая применяет серию операций DML к разным таблицам (удаление из одной таблицы, обновление другой, вставка в последнюю таблицу). На первый взгляд код мог бы выглядеть примерно так:
PROCEDURE change_data IS
BEGIN
DELETE FROM employees WHERE ... ;
UPDATE company SET ... ;
INSERT INTO company_history SELECT * FROM company WHERE ... ;
END;
Безусловно, процедура содержит все необходимые команды DML. Однако одно из требований к программе заключается в том, что при последовательном выполнении этих команд они должны быть логически независимы друг от друга. Другими словами, даже если при выполнении DELETE
произойдет сбой, программа должна выполнить UPDATE
и INSERT
.
В текущей версии change_data
ничто не гарантирует, что программа хотя бы попытается выполнить все три операции DML. Если при выполнении DELETE
произойдет исключение, например, то выполнение всей программы прервется, а управление будет передано в раздел исключений (если он имеется). Остальные команды SQL при этом выполняться не будут.
Как обеспечить обработку исключения без прерывания программы? Для этого DELETE
следует поместить в собственный блок PL/SQL. Рассмотрим следующую версию программы change_data
:
PROCEDURE change_data
IS
BEGIN
BEGIN
DELETE FROM employees WHERE ... ;
EXCEPTION
WHEN OTHERS THEN log_error;
END;
BEGIN
UPDATE company SET ... ;
EXCEPTION
WHEN OTHERS THEN log_error;
END;
BEGIN
INSERT INTO company_history SELECT * FROM company WHERE ... ;
EXCEPTION
WHEN OTHERS THEN log_error;
END;
END;
В новом варианте программы, если при выполнении DELETE
произойдет исключение, управление немедленно передается в раздел исключений. Но поскольку команда DELETE
теперь находится в собственном блоке, она может иметь собственный раздел исключений. Условие WHEN OTHERS
этого раздела обрабатывает ошибку без повторного инициирования этой или другой ошибки, после чего управление возвращается за пределы блока DELETE
внешней процедуре change_data
. Так как «активное» исключение отсутствует, выполнение продолжается во внешнем блоке со следующей команды процедуры. Программа входит в новый анонимный блок для команды UPDATE
. Если при выполнении UPDATE
произойдет ошибка, она будет перехвачена условием WHEN OTHERS
раздела исключений UPDATE
. Далее управление будет возвращено процедуре change_data
, которая перейдет к выполнению команды INSERT
(также содержащейся в собственном блоке).
На рис. 5 показано, как выполняется этот процесс для двух последовательно выполняемых команд DELETE
.
Рис. 5. Последовательное выполнение DELETE с разными областями действия
Подведем итог: исключение, инициированное в исполняемом разделе, всегда обрабатывается в текущем блоке (при наличии подходящего обработчика). Любую команду можно заключить в «виртуальный блок», заключив ее между ключевыми словами BEGIN
и END
с определением раздела EXCEPTION
. Это позволяет ограничить область действия сбоев в программе посредством определения «буферных» анонимных блоков.
Эту стратегию можно развить с выделением изолируемого кода в отдельные процедуры и функции. Конечно, именованные блоки PL/SQL тоже могут иметь собственные разделы исключений и предоставлять ту же защиту от общих сбоев. Важнейшее преимущество процедур и функций заключается в том, что они скрывают все команды BEGIN-EXCEPTION-END
от основной программы. Программа лучше читается, код проще сопровождать и повторно использовать в других контекстах.
Существуют и другие способы продолжить выполнение после исключения DML — например, можно использовать конструкцию SAVE EXCEPTIONS
с FORALL
и LOG ERRORS
в сочетании с DBMS_ERRORLOG
.
Написание раздела WHEN OTHERS
Условие WHEN OTHERS
включается в раздел исключений для перехвата всех исключений, не обработанных предшествующими обработчиками. Так как конкретный тип исключения изначально неизвестен, в WHEN OTHERS
очень часто используются встроенные функции для получения информации о возникшей ошибке (такие, как SQLCODE
и DBMS_UTILITY
. FORMAT_ERROR_STACK
).
В сочетании с WHEN OTHERS
функция SQLCODE
представляет средства для обработки разных видов исключений без применения директивы EXCEPTION_INIT
. В следующем примере перехватываются два исключения категории «родитель/потомок», −1 и −2292, и для каждой ситуации выполняется подходящее действие:
PROCEDURE add_company (
id_in IN company.ID%TYPE
, name_in IN company.name%TYPE
, type_id_in IN company.type_id%TYPE
)
IS
BEGIN
INSERT INTO company (ID, name, type_id)
VALUES (id_in, name_in, type_id_in);
EXCEPTION
WHEN OTHERS
THEN
/*
|| Анонимный блок в обработчике исключения позволяет объявить
|| локальные переменные для хранения информации о кодах ошибок.
*/
DECLARE
l_errcode PLS_INTEGER := SQLCODE;
BEGIN
CASE l_errcode
WHEN −1 THEN
-- Дублирующееся значение уникального индекса. Повторяется либо
-- первичный ключ, либо имя. Сообщить о проблеме
-- и инициировать исключение заново.
DBMS_OUTPUT.put_line
( 'идентификатор или имя компании уже используется. ID = '
|| TO_CHAR (id_in)
|| ' name = '
|| name_in
);
RAISE;
WHEN −2291 THEN
-- Родительский ключ не найден. Сообщить о проблеме
-- и инициировать исключение заново.
DBMS_OUTPUT.put_line (
'Недопустимый идентификатор типа компании: ' || TO_CHAR (type_id_in));
RAISE;
ELSE
RAISE;
END CASE;
END; -- Конец анонимного блока.
END add_company;
Будьте осторожны при использовании WHEN OTHERS
— этот раздел способен «поглощать» ошибки, скрывая их от внешних блоков и пользователя. А точнее, обращайте внимание на обработчики WHEN OTHERS
, которые не инициируют текущее исключение заново и не заменяют его другим исключением. Если WHEN OTHERS
не передает исключение наружу, внешние блоки вашей программы не узнают о возникшей ошибке.
В Oracle Database 11g появилось новое предупреждение, которое помогает выявлять программы, игнорирующие ошибки или поглощающие их:
PLW-06009: procedure "string" OTHERS handler does not end in RAISE or RAISE_
APPLICATION_ERROR
Пример использования этого предупреждения:
SQL> ALTER SESSION SET plsql_warnings = 'enable:all'
2 /
SQL> CREATE OR REPLACE PROCEDURE plw6009_demo
2 AS
3 BEGIN
4 DBMS_OUTPUT.put_line ('I am here!');
5 RAISE NO_DATA_FOUND;
6 EXCEPTION
7 WHEN OTHERS
8 THEN
9 NULL;
10 END plw6009_demo;
11 /
SP2-0804: Procedure created with compilation warnings
SQL> SHOW ERRORS
Errors for PROCEDURE PLW6009_DEMO:
LINE/COL ERROR
-------- -----------------------------------------------------------------
7/9 PLW-06009: procedure "PLW6009_DEMO" OTHERS handler does not end
in RAISE or RAISE_APPLICATION_ERROR
Построение эффективной архитектуры управления ошибками
Механизм инициирования и обработки ошибок в PL/SQL отличается мощью и гибкостью, но он не лишен недостатков, которые могут создать проблемы для групп разработки, желающих реализовать надежную, последовательную, содержательную архитектуру управления ошибками. В частности, вы столкнетесь со следующими проблемами:
EXCEPTION
— особая разновидность структуры данных PL/SQL. Переменные, объявленные с типомEXCEPTION
, можно только инициировать и обрабатывать. Исключение нельзя передать в аргументе программы, с ним нельзя связать дополнительные атрибуты.- Повторное использование кода обработки исключений сильно затруднено. Из предыдущего пункта непосредственно следует другой факт: раз исключение нельзя передать в аргументе, разработчику приходится копировать код обработчика — конечно, такой способ написания кода никак не назовешь оптимальным.
- Не существует формализованного способа объявления исключений, которые могут инициироваться программой. Например, в Java эта информация становится частью спецификации программы. Как следствие, разработчику приходится обращаться к коду реализации и искать в нем информацию о потенциальных исключениях — или же надеяться на лучшее.
- Oracle не предоставляет средств организации и классификации исключений, относящихся к конкретному приложению, а просто резервирует (в основном) 1000 кодов в диапазоне от −20 999 до −20 000. Управлять этими значениями должен сам разработчик.
Давайте посмотрим, как преодолеть большинство из перечисленных трудностей.
Определение стратегии управления ошибками
Очень важно, чтобы еще до написания кода была выработана последовательная стратегия и архитектура обработки ошибок в приложении. Вот лишь некоторые вопросы, на которые необходимо ответить для этого:
- Как и когда сохранять информацию об ошибках для последующего просмотра и исправления? Куда выводить информацию — в файл, в таблицу базы данных? выводить на экран?
- Как и где сообщать об ошибках пользователю? Какую информацию должен получать пользователь? Как «перевести» часто невразумительные сообщения об ошибках, выдаваемые базой данных, на язык, понятный пользователям?
С этими общими вопросами тесно связаны более конкретные проблемы:
- Следует ли включать раздел обработки исключений в каждый блок PL/SQL?
- Следует ли включать раздел обработки исключений только в блок верхнего уровня или внешние блоки?
- Как организовать управление транзакциями при возникновении ошибок? Сложность обработки исключений отчасти связана с тем, что на все эти вопросы не существует единственно правильного ответа. Все зависит (по крайней мере частично) от архитектуры приложения и режима его использования (например, пакетное выполнение или транзакции, управляемые пользователем). Но если вы сможете ответить на эти вопросы для своего приложения, я рекомендую «запрограммировать» стратегию и правила обработки ошибок в стандартном пакете (см. далее «Стандартизация обработки ошибок»).
Некоторые общие принципы, которые стоит принять во внимание:
- Когда в коде происходит ошибка, получите как можно больше информации о контексте ее возникновения. Избыток информации — лучше, чем ее нехватка. Далее исключение можно передавать во внешние блоки, собирая дополнительную информацию по мере продвижения.
- Избегайте применения обработчиков вида
WHEN
ошибкаTHEN NULL
; (или еще хуже,WHEN OTHERS THEN NULL;
). Возможно, для написания такого хода у вас имеются веские причины, но вы должны твердо понимать, что это именно то, что вам нужно, и документировать такое использование, чтобы о нем знали другие. - Там, где это возможно, используйте механизмы обработки ошибок PL/SQL по умолчанию. Избегайте написания программ, возвращающих коды состояния управляющей среде или вызывающим блокам. Применять коды состояния следует только в одной ситуации: если управляющая среда не способна корректно обрабатывать ошибки Oracle (в таком случае стоит подумать о смене управляющей среды!).
Стандартизация обработки разных типов исключений
Исключение всегда свидетельствует о критической ситуации? Вовсе нет. Некоторые исключения (например, ORA-00600) сообщают о том, что в базе данных возникли очень серьезные низкоуровневые проблемы. Другие исключения, такие как NO_DATA_FOUND
, встречаются так часто, что мы воспринимаем их не как ошибки, а как условную логическую конструкцию («Если строка не существует, то выполнить следующие действия…»). Нужно ли различать эти категории исключений?
Коллеги-программисты научил меня очень полезной системе классификации исключений.
- Преднамеренные исключения. Архитектура кода сознательно использует особенности работы исключения. Это означает, что разработчик должен предвидеть исключение и запрограммировать его обработку. Пример —
UTL_FILE
.GET_LINE
. - Нежелательные исключения. Происходит ошибка, но ее возможность была предусмотрена заранее. Возможно, исключение даже не свидетельствует о возникновении проблемы. Пример команда
SELECT INTO
, инициирующая исключениеNO_DATA_FOUND
. - Непредвиденные исключения. Серьезные ошибки, указывающие на возникновение проблемы в приложении. Пример — команда
SELECT INTO
, которая должна вернуть строку для заданного первичного ключа, но вместо этого инициирует исключениеTOO_MANY
ROWS
.
Давайте поближе познакомимся с примерами всех категорий, а затем поговорим о том, какую пользу вы можете извлечь из знания этих категорий.
Преднамеренные исключения
Разработчики PL/SQL могут использовать процедуру UTL_FILE
.GET_LINE
для чтения содержимого файла по строкам. Когда GET_LINE
выходит за границу файла, инициируется исключение NO_DATA_FOUND
. Так работает эта процедура. Итак, если я хочу прочитать все содержимое файла и сделать «что-то полезное», программа может выглядеть так:
PROCEDURE read_file_and_do_stuff (
dir_in IN VARCHAR2, file_in IN VARCHAR2
)
IS
l_file UTL_FILE.file_type;
l_line VARCHAR2 (32767);
BEGIN
l_file := UTL_FILE.fopen (dir_in, file_in, 'R', max_linesize => 32767);
LOOP
UTL_FILE.get_line (l_file, l_line);
do_stuff;
END LOOP;
EXCEPTION
WHEN NO_DATA_FOUND
THEN
UTL_FILE.fclose (l_file);
more_stuff_here;
END;
У этого цикла есть одна особенность: он не содержит команды EXIT
. Кроме того, в разделе исключений выполняется дополнительная логика приложения (more_stuff_here
). Цикл можно переписать в следующем виде:
LOOP
BEGIN
UTL_FILE.get_line (l_file, l_line);
do_stuff;
EXCEPTION
WHEN NO_DATA_FOUND
THEN
EXIT;
END;
END LOOP;
UTL_FILE.flcose (l_file);
more_stuff_here;
Теперь цикл содержит команду EXIT
, но код стал более громоздким.
Подобные конструкции приходится использовать при работе с кодом, намеренно инициирующем исключения в своей архитектуре. Дополнительная информация о том, как следует поступать в подобных случаях, приводится в следующих разделах.
Нежелательные и непредвиденные исключения
Я рассматриваю эти две категории вместе, потому что приводимые примеры (NO_DATA_FOUND
и TOO_MANY_ROWS
) тесно связаны между собой. Предположим, я хочу написать функцию, возвращающую полное имя работника (в формате фамилия запятая имя) для заданного значения первичного ключа. Проще всего это сделать так:
FUNCTION fullname (
employee_id_in IN employees.employee_id%TYPE
)
RETURN VARCHAR2
IS
retval VARCHAR2 (32767);
BEGIN
SELECT last_name || ',' || first_name
INTO retval
FROM employees
WHERE employee_id = employee_id_in;
RETURN retval;
END fullname;
Если вызвать эту программу с кодом работника, отсутствующим в таблице, база данных инициирует исключение NO_DATA_FOUND
. Если же вызвать ее с кодом работника, встречающимся в нескольких строках таблицы, будет инициировано исключение TOO_MANY_ROWS
. Один запрос, два разных исключения — нужно ли рассматривать их одинаково? Вероятно, нет. Описывают ли эти два исключения похожие группы проблем? Давайте посмотрим:
NO_DATA_FOUND
— совпадение не найдено. Исключение может указывать на наличие серьезной проблемы, но не обязательно. Возможно, в большинстве обращений к базе данных совпадение не будет обнаруживаться, и я буду вставлять в базу данные нового работника. В общем, исключение нежелательно, но в данном случае оно даже не указывает на возникновение ошибки.TOO_MANY_ROWS
— в базе данных возникла серьезная проблема с ограничением первичного ключа. Трудно представить себе ситуацию, в которой это было бы нормально или просто «нежелательно». Нет, нужно прервать работу программы и привлечь внимание пользователя к совершенно непредвиденной, критической ошибке.
Как извлечь пользу из этой классификации
Надеюсь, вы согласитесь, что такая классификация полезна. Приступая к построению нового приложения, постарайтесь по возможности определиться со стандартным подходом, который будет применяться вами (и всеми остальными участниками группы) для каждого типа исключений. Затем для каждого исключения (которое необходимо обработать или хотя бы учитывать заранее при написании кода) решите, к какой категории относится, и примените уже согласованный подход. Все это поможет сделать ваш код более последовательным, и повысит эффективность вашей работы. Приведу несколько рекомендаций для трех типов исключений.
- Преднамеренные исключения. Пишите код, учитывающий возможность возникновения таких исключений. Прежде всего постарайтесь избежать размещения логики приложения в разделе исключений. Раздел исключений должен содержать только код, относящийся к обработке ошибки: сохранение информации об ошибке в журнале, повторное инициирование исключения и т. д. Программисты не ожидают увидеть логику приложения в разделе исключений, поэтому им будет намного труднее разобраться в таком коде и обеспечить его сопровождение.
- Нежелательные исключения. Если в каких-то обстоятельствах пользователь кода, инициировавшего исключения, не будет интерпретировать ситуацию как ошибку, не передавайте исключения наружу без обработки. Вместо этого верните значение или флаг состояния, показывающий, что исключение было обработано. Далее пользователь программы может сам решить, должна ли программа завершиться с ошибкой. А еще лучше — почему бы не разрешить стороне, вызывающей вашу программу, решить, нужно ли инициировать исключение, и если не нужно — какое значение должно передаваться для обозначения возникшего исключения?
- Непредвиденные исключения. А теперь начинается самое неприятное. Все непредвиденные ошибки должны быть сохранены в журнале с максимумом возможной контекстной информации, которая поможет понять причины возникновения ошибки. Затем программа должна завершиться с необработанным исключением (обычно тем же), инициированным из программы; для этого можно воспользоваться командой
RAISE
. Исключение заставит вызвавшую программу прервать работу и обработать ошибку.
Коды ошибок, связанные с конкретным приложением
Используя команду RAISE_APPLICATION_ERROR
для инициирования ошибок, относящихся к конкретному приложению, вы несете полную ответственность за управление кодами ошибок и сообщениями. Это быстро становится хлопотным и непростым делом («Так, какой бы код мне выбрать? Пожалуй, –20 774 — вроде бы такого еще не было?»).
Чтобы упростить управление кодами ошибок и предоставить последовательный интерфейс, через который разработчики смогут обрабатывать серверные ошибки, постройте таблицу со всеми используемыми кодами ошибок −20 NNN, сопутствующими именами исключений и сообщениями об ошибках.
Разработчик может просмотреть уже определенные ошибки на экране и выбрать ту из них, которая лучше всего подходит для конкретной ситуации.
Также можно попытаться полностью избегать диапазон −20 NNN для ошибок приложений. Почему бы не воспользоваться положительными числами? Из положительного цело-численного поддиапазона Oracle использует только 1 и 100. Теоретически возможно, что когда-нибудь Oracle будет использовать и другие положительные числа, но это весьма маловероятно. В распоряжении разработчиков остается великое множество кодов ошибок.
В частности, я пошел по этому пути при проектировании Quest Error Manager (QEM) — бесплатной программы управления ошибками. В Quest Error Manager вы можете определять свои ошибки в специальной таблице. Ошибка определяется именем и/ или кодом. Коды ошибок могут быть положительными или отрицательными. Если код ошибки положителен, при инициировании исключения QEM использует команду RAISE_APPLICATION_ERROR
для инициирования обобщенного исключения (обычно −20 000). Информация о текущем коде ошибки приложения встраивается в сообщение об ошибке, которое может быть расшифровано программой-получателем.
Упрощенная реализация этого подхода представлена в пакете обработки ошибок errpkg. pkg
, описанном в следующем разделе блога.
Стандартизация обработки ошибок
Обязательным элементом любого профессионально написанного приложения является надежная и согласованная схема обработки ошибок. Согласованность в этом вопросе важна как для пользователя, так и для разработчика. Если при возникновении ошибки пользователю предоставляется понятная, хорошо структурированная информация, он сможет более подробно рассказать об ошибке службе поддержки и будет более уверенно чувствовать себя при работе с приложением. Если приложение всегда обрабатывает и протоколирует ошибки определенным образом, программистам, занимающимся его поддержкой и сопровождением, будет легче их найти и устранить.
Все кажется вполне очевидным, не так ли? К сожалению, на практике (и особенно в больших группах разработчиков) все происходит несколько иначе. Очень часто каждый разработчик идет своим путем, следуя личным принципам и приемам, сохраняя информацию в произвольно выбранном формате и т. д. Одним словом, без стандартизации отладка и сопровождение приложений оборачиваются сущим кошмаром. Рассмотрим типичный пример:
EXCEPTION
WHEN NO_DATA_FOUND
THEN
v_msg := 'Нет компании с идентификатором '||TO_CHAR (v_id);
v_err := SQLCODE;
v_prog := 'fixdebt';
INSERT INTO errlog VALUES
(v_err,v_msg,v_prog,SYSDATE,USER);
WHEN OTHERS
THEN
v_err := SQLCODE;
v_msg := SQLERRM;
v_prog := 'fixdebt';
INSERT INTO errlog VALUES
(v_err,v_msg,v_prog,SYSDATE,USER);
RAISE;
На первый взгляд код выглядит вполне разумно. Если компания с заданным идентификатором не найдена, мы получаем значение SQLCODE
, задаем имя программы и сообщение и записываем строку с информацией об ошибке в таблицу ошибок. Выполнение родительского блока продолжается, поскольку ошибка не критична. Если происходит любая другая ошибка, получаем ее код и соответствующее сообщение, задаем имя программы и записываем строку с информацией об ошибке в таблицу ошибок, а затем передаем исключение в родительский блок, чтобы остановить его выполнение (поскольку неизвестно, насколько критична эта ошибка).
Что же здесь не так? Чтобы подробно объяснить суть проблемы, достаточно взглянуть на код. В нем жестко закодированы все действия по обработке ошибок. В результате (1) код получается слишком объемистым, (2) его придется полностью переписывать при изменении схемы обработки ошибок. Обратите внимание еще и на тот факт, что информация об ошибке записывается в таблицу базы данных. Это означает, что запись в журнале становится частью логической транзакции. И если потребуется выполнить откат транзакции, записи в журнале ошибок будут утеряны.
Существует несколько способов избежать потери информации: можно записывать данные в файл или использовать автономные транзакции для сохранения журнала вне основной транзакции. Но как бы то ни было, код в случае его изменения придется исправлять в сотнях разных мест.
А теперь посмотрите, как этот же раздел исключений оформляется при использовании стандартизированного пакета:
EXCEPTION
WHEN NO_DATA_FOUND
THEN
errpkg.record_and_continue (
SQLCODE, 'Нет компании с идентификатором ' || TO_CHAR (v_id));
WHEN OTHERS
THEN
errpkg.record_and_stop;
END;
Такой пакет обработки ошибок скрывает все подробности реализации; вы просто решаете, какая из процедур-обработчиков должна использоваться в конкретном случае, просматривая спецификацию пакета. Если требуется сохранить информацию об ошибке и продолжить работу, вызывается программа record_and_continue
. Если же нужно сохранить информацию об ошибке и прервать выполнение родительского блока, вызывается программа record_and_stop
. Мы не знаем, как эти программы сохраняют информацию об ошибке, как они останавливают работу родительского блока, то есть передают исключение, но для нас это и не важно. Главное, что все происходит так, как определено стандартами приложения.
Это дает вам возможность уделить больше времени разработке более интересных элементов приложения и не заниматься административной рутиной.
Имеется файл errpkg.pkg
с прототипом стандартизированного пакета обработки ошибок. Правда, прежде чем использовать его в приложениях, вам необходимо будет завершить его реализацию; это поможет составить ясное представление о том, как конструируются подобные утилиты.
Вы также можете воспользоваться намного более мощным (и тоже бесплатным) средством обработки ошибок Quest Error Manager. Важнейшая концепция, заложенная в основу QEM, заключается в возможности перехвата и протоколирования экземпляров ошибок, не только ошибок Oracle. QEM состоит из пакета PL/SQL и четырех таблиц для хранения информации об ошибках, возникающих в приложениях.
Работа с «объектами» исключений
Реализация типа данных EXCEPTION
в Oracle имеет свои ограничения, о которых было рассказано выше. Исключение состоит из идентификатора (имени), с которым связывается числовой код и сообщение. Исключение можно инициировать, его можно обработать… и все. Теперь представьте, как та же ситуация выглядит в Java: все ошибки являются производными от единого класса Exception
. Этот класс можно расширить, дополняя его новыми характеристиками, которые вы хотите отслеживать (стек ошибок, контекстные данные и т. д.). Объект, созданный на основе класса Exception, ничем не отличается от любых других объектов Java. Разумеется, он может передаваться в аргументах методов.
PL/SQL не позволяет делать ничего подобного со своими исключениями. Впрочем, этот факт не мешает вам реализовать свой «объект» исключения. Для этого можно воспользоваться объектными типами Oracle или реляционной таблицей, содержащей информацию об ошибке. Независимо от выбранной реализации очень важно различать определение ошибки (код ошибки –1403, имя «данные не найдены», причина — «неявный курсор не нашел ни одной записи») и ее конкретный экземпляр (я попытался найти компанию с указанным именем, ни одной строки не найдено). Иначе говоря, существует всего одно определение исключения NO_DATA_FOUND
, которое может существовать во множестве экземпляров. Oracle не различает эти два представления ошибки, но для нас это безусловно необходимо.
Пример простой иерархии объектов исключений продемонстрирует этот момент. Начнем с базового объектного типа всех исключений:
CREATE TYPE exception_t AS OBJECT (
name VARCHAR2(100),
code INTEGER,
description VARCHAR2(4000),
help_text VARCHAR2(4000),
recommendation VARCHAR2(4000),
error_stack CLOB,
call_stack CLOB,
created_on DATE,
created_by VARCHAR2(100)
)
NOT FINAL;
/
Затем базовый тип исключения расширяется для ошибок динамического SQL посредством добавления атрибута sql_string. При обработке ошибок динамического SQL очень важно сохранить строку, создавшую проблемы, для анализа в будущем:
CREATE TYPE dynsql_exception_t UNDER exception_t (
sql_string CLOB )
NOT FINAL;
/
А вот другой подтип exception_t
, на этот раз относящийся к конкретной сущности приложения — работнику. Исключение, инициируемое для ошибок, относящихся к работникам, будет включать идентификатор работника и внешний ключ нарушенного правила:
CREATE TYPE employee_exception_t UNDER exception_t (
employee_id INTEGER,
rule_id INTEGER );
/
Полная спецификация иерархии объектов ошибок включает методы супертипа исключения, предназначенные для вывода информации об ошибках или ее записи в репозиторий. Вы можете самостоятельно завершить иерархию, определенную в файле exception.ot
.
Если вы не хотите работать с объектными типами, попробуйте использовать подход, использованный мной в QEM: я определяю таблицу определений ошибок (Q$ERROR
) и другую таблицу экземпляров ошибок (Q$ERROR_INSTANCE
), которая содержит информацию о конкретных экземплярах ошибок. Все контекстные данные экземпляра ошибки сохраняются в таблице Q$ERROR_CONTEXT
.
Пример кода, который мог бы быть написан для QEM API:
WHEN DUP_VAL_ON_INDEX
THEN
q$error_manager.register_error (
error_name_in => 'DUPLICATE-VALUE'
,err_instance_id_out => l_err_instance_id
);
q$error_manager.add_context (
err_instance_id_in => l_err_instance_id
,name_in => 'TABLE_NAME', value_in => 'EMPLOYEES'
);
q$error_manager.add_context (
err_instance_id_in => l_err_instance_id
,name_in => 'KEY_VALUE', value_in => l_employee_id
);
q$error_manager.raise_error_instance (
err_instance_id_in => l_err_instance_id);
END;
Если ошибка повторяющегося значения была вызвана ограничением уникального имени, я получаю идентификатор экземпляра ошибки DUPLICATE-VALUE
. (Да, все верно: я использую имена ошибок, полностью обходя все проблемы, связанные с номерами ошибок.) Затем я добавляю контекстную информацию экземпляра (имя таблицы и значение первичного ключа, вызвавшее проблему). В завершение инициируется экземпляр ошибки, в результате чего исключение передается в следующий наружный блок.
По аналогии с передачей данных из приложения в репозиторий ошибок через API, вы также можете получить информацию об ошибке при помощи процедуры get_error_info
.
Пример:
BEGIN
run_my_application_code;
EXCEPTION
WHEN OTHERS
THEN
DECLARE
l_error q$error_manager.error_info_rt;
BEGIN
q$error_manager.get_error_info (l_error);
DBMS_OUTPUT.put_line ('');
DBMS_OUTPUT.put_line ('Error in DEPT_SAL Procedure:');
DBMS_OUTPUT.put_line ('Code = ' || l_error.code);
DBMS_OUTPUT.put_line ('Name = ' || l_error.NAME);
DBMS_OUTPUT.put_line ('Text = ' || l_error.text);
DBMS_OUTPUT.put_line ('Error Stack = ' || l_error.error_stack);
END;
END;
Это лишь два из многих способов преодоления ограничений типа EXCEPTION
в PL/SQL. Мораль: ничто не заставляет вас мириться с ситуацией по умолчанию, при которой с экземпляром ошибки связывается только код и сообщение.
Создание стандартного шаблона для обобщенной обработки ошибок
Невозможность передачи исключений программе сильно усложняет совместное использование разделов обработки ошибок в разных блоках PL/SQL. Одну и ту же логику обработчика нередко приходится записывать снова и снова, особенно при работе с конкретными функциональными областями — скажем, файловым вводом/ выводом с UTL_FILE
. В таких ситуациях стоит выделить время на создание шаблонов обработчиков.
Давайте поближе познакомимся с UTL_FILE
. До выхода Oracle9i Database Release 2 в спецификации пакета UTL_FILE
определялся набор исключений. Однако компания Oracle не стала предоставлять коды этих исключений через директиву EXCEPTION_INIT
. А без обработки исключений UTL_FILE
по имени SQLCODE
не сможет разобраться, что пошло не так. Вероятно, в такой ситуации для программ UTL_FILE
можно создать шаблон, часть которого выглядит так:
DECLARE
l_file_id UTL_FILE.file_type;
PROCEDURE cleanup (file_in IN OUT UTL_FILE.file_type
,err_in IN VARCHAR2 := NULL)
IS
BEGIN
UTL_FILE.fclose (file_in);
IF err_in IS NOT NULL
THEN
DBMS_OUTPUT.put_line ('Обнаружена ошибка UTL_FILE:');
DBMS_OUTPUT.put_line (err_in);
END IF;
END cleanup;
BEGIN
-- Здесь размещается тело программы.
-- Перед выходом необходимо прибрать за собой ...
cleanup (l_file_id);
EXCEPTION
WHEN UTL_FILE.invalid_path
THEN
cleanup (l_file_id, 'invalid_path');
RAISE;
WHEN UTL_FILE.invalid_mode
THEN
cleanup (l_file_id, 'invalid_mode');
RAISE;
END;
Основные элементы шаблона:
- Программа выполнения завершающих действий, пригодная для повторного использования; гарантирует, что текущий файл будет закрыт до потери дескриптора файла.
- Преобразование именованного исключения в строку, которую можно сохранить в журнале или вывести на экран, чтобы пользователь точно знал, какая ошибка была инициирована.
Рассмотрим еще один пример шаблона, который удобно использовать при работе с UTL_FILE
. В Oracle9i Database Release 2 появилась программа FREMOVE
для удаления файлов. Пакет UTL_FILE
предоставляет исключение DELETE_FAILED
, инициируемое тогда, когда FREMOVE
не удается удалить файл. После тестирования программы я обнаружил, что FREMOVE
может инициировать несколько возможных исключений, в числе которых:
UTL_FILE
.INVALID_OPERATION
— удаляемый файл не существует.UTL_FILE
.DELETE_FAILED
— у вас (или у процесса Oracle) недостаточно привилегий для удаления файла, или попытка завершилась неудачей по другой причине.
Начиная с Oracle9i Database Release 2, UTL_FILE
назначает коды ошибок всем своим исключениям, но вы все равно должны проследить за тем, чтобы при возникновении ошибки файлы были закрыты, и организовать последовательную обработку ошибок.
Итак, при использовании UTL_FILE
.FREMOVE
следует включать раздел обработчика исключения, который различает эти две ошибки:
BEGIN
UTL_FILE.fremove (dir, filename);
EXCEPTION
WHEN UTL_FILE.delete_failed
THEN
DBMS_OUTPUT.put_line (
'Ошибка при попытке удаления: ' || filename || ' в ' || dir);
-- Выполнение соответствующих действий...
WHEN UTL_FILE.invalid_operation
THEN
DBMS_OUTPUT.put_line (
'Не удалось найти и удалить: ' || filename || ' в ' || dir);
-- Выполнение соответствующих действий...
END;
Оптимальная организация обработки ошибок в PL/SQL
Без унифицированной качественной методологии обработки ошибок очень трудно написать приложение, которое было бы удобным в использовании и одновременно простым в отладке.
Архитектура обработки ошибок в Oracle PL/SQL предоставляет очень гибкие средства для определения, инициирования и обработки ошибок. Однако у нее имеются свои ограничения, вследствие чего встроенную функциональность обычно приходится дополнять таблицами и кодами ошибок, специфическими для конкретного приложения.
Для решения проблемы обработки ошибок рекомендуется предпринять следующие действия:
- Тщательно разберитесь в системе инициирования и обработки ошибок в PL/SQL. Далеко не во ее аспекты интуитивно понятны. Простейший пример: исключение, инициированное в секции объявлений, не будет обрабатываться секцией исключений текущего блока.
- Выберите общую схему обработки ошибок в вашем приложении. Где и как будут обрабатываться ошибки? Какая информация об ошибке будет сохраняться и как это будет сделано? Как исключения будут передаваться в управляющую среду? Как будут обрабатываться намеренные и непредвиденные ошибки?
- Постройте стандартную инфраструктуру, которая будет использоваться всеми разработчиками проекта. Инфраструктура должна включать таблицы, пакеты и, возможно, объектные типы, а также четко определенный процесс использования всех перечисленных элементов. Не останавливайтесь на ограничениях PL/ SQL. Найдите обходные пути, расширяя модель обработки ошибок.
- Создайте шаблоны, которые могут использоваться всеми участниками вашей группы. Всегда проще следовать готовому стандарту, чем самостоятельно писать код обработки ошибок.
Жду отклика на статью. Что понравилось? Что нет?
Вас заинтересует / Intresting for you:
По умолчанию ошибки нигде не логируются. Это задача резработчика процедуры обеспечить запись ошибок и других событий в таблицы или файлы логирования. Не отловленые исключения возвращаются туда, где процедура была вызвана.
В самом простейшем случае, логирование выглядит так:
create table log (id int generated always as identity, text varchar2 (4000), ts timestamp)
/
create or replace procedure logger (text varchar2) is
pragma autonomous_transaction;
begin
insert into log (text, ts) values (text, systimestamp);
commit;
end;
/
create or replace procedure proc (par varchar2) is
val int;
begin
select 1 into val
from dual
where par='xxx';
exception when others then
logger (sqlerrm||chr(10)||sys.dbms_utility.format_error_backtrace());
raise;
end;
/
Запуск и результат:
exec proc ('aaa')
select * from log order by ts desc
/
ID TEXT TS
---------- -------------------------------- -----------------------------
1 ORA-01403: no data found 2020-09-07 23:16:04,055966000
ORA-06512: at "ME.PROC", line 4
Как отлавливать ошибки в Oracle (PLSQL) EXCEPTION,SQLERRM,SQLCODE
Осваиваем Oracle и PL/SQL
Маленькое руководство по отлавливанию ошибок в Oracle PLSQL.
Описание как использовать в Oracle (PLSQL) функции SQLERRM и SQLCODE для отлова ошибок EXCEPTION, с описанием синтаксиса и примером.
Функция SQLERRM возвращает сообщение об ошибке связанное с последним возникшим исключением (ошибкой).
Функция SQLERRM — не имеет параметров.
Функция SQLCODE возвращает код ошибки связанный с последним возникшим исключением (ошибкой)
Функция SQLERRM — не имеет параметров.
Обычно обработка исключений EXCEPTION выглядит следующим образом:
EXCEPTION
WHEN наименование_ошибки_1 THEN
[statements]
WHEN наименование_ошибки_2 THEN
[statements]
WHEN наименование_ошибки_N THEN
[statements]
WHEN OTHERS THEN
[statements]
END [наименование_процедуры];
Вы можете использовать функции SQLERRM и SQLCODE для вызова сообщения об ошибке например таким образом:
EXCEPTION
WHEN OTHERS THEN
raise_application_error(-20001,'Произошла ошибка - '||SQLCODE||' -ERROR- '||SQLERRM);
END;
-- В данном случае появится всплывающее сообщение.
Или вы можете сохранить сообщение об ошибке в таблицу таким образом:
EXCEPTION
WHEN OTHERS THEN
err_code := SQLCODE;
err_msg := SUBSTR(SQLERRM, 1, 200);
INSERT INTO error_log_table (error_time, error_number, error_message)
VALUES (sysdate, err_code, err_msg);
END;
--В данном случае ошибка будет помещена в таблицу error_log_table
Варианты основных возможных ошибок:
DUP_VAL_ON_INDEX
ORA-00001
При попытке произвести вставку INSERT или изменение UPDATE данных которое создает дублирующую запись нарушающую уникальный индекс (unique index).
TIMEOUT_ON_RESOURCE
ORA-00051
Происходит когда ресурс над которым производится операция заблокирован и произошел сброс по таймауту.
TRANSACTION_BACKED_OUT
ORA-00061
Произошёл частичный откат транзакции.
INVALID_CURSOR
ORA-01001
Попытка использовать курсор которого не существует. Может происходить если вы пытаетесь использовать FETCH курсор или закрыть CLOSE курсор до того как вы этот курсор открыли OPEN.
NOT_LOGGED_ON
ORA-01012
Попытка произвести действия не залогинившись.
LOGIN_DENIED
ORA-01017
Неудачная попытка залогиниться, в доступе отказано, не верный пользователь или пароль.
NO_DATA_FOUND
ORA-01403
Что то из перечисленного: Попытка произвести вставку SELECT INTO несуществующего набора значений (select — ничего не возвращает). Попытка доступа к неинициализированной строке/записи в таблице. Попытка чтения записи после окончания файла при помощи пакета UTL_FILE.
TOO_MANY_ROWS
ORA-01422
Попытка вставить значение в переменную при помощи SELECT INTO и select вернул более чем одно значение.
ZERO_DIVIDE
ORA-01476
Попытка деления на ноль.
INVALID_NUMBER
ORA-01722
Попытка выполнить SQL запрос который производит конвертацию строки (STRING) в число (NUMBER) при этом такое преобразование невозможно.
STORAGE_ERROR
ORA-06500
Либо нехватка памяти, либо ошибка в памяти.
PROGRAM_ERROR
ORA-06501
Внутренняя программная ошибка рекомендуется с такой ошибкой обращаться в службу поддержки Oracle.
VALUE_ERROR
ORA-06502
Попытка выполнить операцию конвертации данных которая закончилась с ошибкой (например: округление, преобразование типов, и т.п.).
CURSOR_ALREADY_OPEN
ORA-06511
Вы пытаетесь открыть курсор который уже открыт.
Вот пожалуй и всё.
Oracle
PLSQL
SQL
EXCEPTION
SQLERRM
SQLCODE
ошибки
программирование
В этом учебном материале вы узнаете, как использовать функцию SQLCODE в Oracle/PLSQL c синтаксисом и примерами.
Описание
Функция SQLCODE возвращает номер ошибки, связанной с исключительной ситуацией. Эта функция может быть использована только в разделе обработки исключений вашего кода.
Синтаксис
SQLCODE
Параметры или аргументы
Функция SQLCODE не имеет параметров или аргументов.
Примечание
Смотрите также: SQLERRM функцию.
Пример
Так обработка исключений обычно записывается со следующим синтаксисом:
EXCEPTION
WHEN exception_name1 THEN
[statements]WHEN exception_name2 THEN
[statements]
WHEN exception_name_n THEN
[statements]
WHEN OTHERS THEN
[statements]
END [procedure_name];
Вы можете использовать функцию SQLCODE, чтобы вызвать ошибку следующим образом:
EXCEPTION
WHEN OTHERS THEN
raise_application_error(-20001,’An error was encountered — ‘||SQLCODE||’ -ERROR- ‘||SQLERRM);
END;
Так же вы могли бы сохранить запись об ошибке в таблицу с помощью функции SQLCODE следующим образом:
EXCEPTION
WHEN OTHERS THEN
err_code := SQLCODE;
err_msg := SUBSTR(SQLERRM, 1, 200);INSERT INTO audit_table (error_number, error_message)
VALUES (err_code, err_msg);
END;
Время на прочтение
17 мин
Количество просмотров 4.9K
Предисловие
Убедительная просьба, рассматривать данный текст только как продолжение к статье о «Событийной модели логирования». Эта статья будет полезна тем, у кого уже реализовано логирование событий в БД и кто хотел бы осуществлять сбор статистики и начать проводить аналитику этих событий. Только представьте, что ваша БД сможет информировать вас о критичных сбоях системы, накапливать информацию о событиях в БД (кол-во повторений, период повторений и т.д.). И всё это без использования стороннего ПО силами одного PL/SQL.
Введение
В этом цикле статей будет показано как реализованный функционал «логирования» позволяет фиксировать факт возникновения не только ошибок (сообщение с типом «Error»), но и сообщений с предупреждением (тип «Warning»), информативных сообщений (с типом «Info») и т.д., поэтому, в рамках данных статей введём термин — «Модель логирования событий» (далее по тексту — «модель») или коротко «Логирование событий», где под «событием» подразумевается некоторое ожидаемое действие, возникшее в ходе штатной/внештатной работы алгоритма.
Модель логирования позволяет реализовать:
-
Единый подход в обработке и хранении событий (статья)
-
Собственную нумерацию и идентификацию событий происходящих в БД
-
Единый мониторинг событий (статья в разработке)
-
Анализ событий происходящих в БД (статья в разработке)
Описанные выше характеристики указаны в порядке нумерации и каждый следующий пункт (шаг) есть улучшение и усложнение существующей модели. Описание этой модели будет сложно выполнить в рамках одной статьи, поэтому опишем их последовательно. В этой (второй) статье создадим собственную нумерацию кодов для событий, а также создадим функционал идентификации событий происходящих в БД.
Для чего это нужно?
Для начала давайте рассмотрим пример. Вы реализовали логирование ошибок в вашей БД. С течением времени в ваш лог «прилетают» самые разнообразные ошибки. Предположим, имеются две ошибки вида «no_data_found» возникшие в двух разных процедурах при двух разных запросах (select). Первая ошибка возникла при попытке найти «email» клиента, что в принципе не является критичной ошибкой. Вторая ошибка возникла при попытке найти номер лицевого счета клиента, что вполне может являться критичной ошибкой. При этом если мы посмотрим в таблицу лога (из статьи), то увидим, что указанные ошибки будут храниться с одинаковым кодом 1403 (ORA-01403) в столбце msgcode. Более того, текст указанных ошибок будет практически аналогичным (текст полученный с помощью функции SQLERRM) за исключением имен объектов, на которых произошла ошибка. Для того чтобы понять является ли критичной конкретная ошибка, разработчику необходимо вникать в текст ошибки, смотреть в каком объекте возникла ошибка и на основе этой информации сделать вывод о срочности исправления. При этом, если мы сможем задать более четкое описание ошибки отличное от текста Oracle (SQLERRM), то это позволит упростить понимание причин возникновения и способов решения ошибки.
Ошибка |
Как сейчас |
Как должно быть (в идеале) |
Не найдена запись в таблице содержащей адреса электронной почты клиентов |
ORA-01403: данные не найдены |
USR0001: Не найден адрес электронной почты клиента id = *** (идентификатор клиента) |
Не найдена запись в таблице содержащей лицевые счета клиентов |
ORA-01403: данные не найдены |
USR0002: Не найден лицевой счет клиента id = *** (идентификатор клиента) |
Из этого примера видно, что одна и та же ошибка «no_data_found» (ORA-01403: данные не найдены) может иметь совершенно разное значение с точки зрения бизнес логики, а значит нам необходимо разработать механизм, который позволит идентифицировать каждое событие происходящее в БД как отдельное событие с нашим внутренним уникальным кодом и текстом события (отличную от Oracle). Таким образом мы решаем две проблемы:
1) В месте возникновения ошибки мы устанавливаем уникальный код ошибки. В будущем это позволяет достаточно быстро найти место возникновения ошибки. Также, наличие уникальных кодов позволяет нам произвести точечный подсчет повторений и на основании этой информации принять решение об устранении данной ошибки.
2) Дополнительный «читаемый» текст позволяет сильно упростить понимание ошибки. В таблице выше показано, как одна и та же ошибка может запутать или разъяснить пользователю сведения об ошибке.
Надеюсь мне удалось объяснить зачем необходимо кодировать события в таблице логов. Далее по тексту, будут введены термины «Архитектурный лог» и «Пользовательский лог». На примере процедуры поиска активного номера телефона клиента будет показано как и зачем создано разделение на архитектурный и пользовательский лог.
Архитектурное логирование событий
Давайте рассмотрим пример, имеется процедура поиска активного номера телефона принадлежащего конкретному клиенту (для примера его id = 43). Предположим, что при постановке задачи для разработчика не было описания каких-либо особых условий т.е. по условиям задачи предполагалось, что для конкретного пользователя (id = 43, идентификатор передается в качестве параметра) в таблице client_telnumbers всегда будет хотя бы одна запись с номером телефона клиента и признаком «активный» (значение поля enddate равно дате 31.12.5999 23:59:59, что означает что номер используется клиентом. В случае, любой другой даты в указанном поле означает, что номер перестал быть активным и более не используется), поэтому наша процедура будет выглядеть примерно так:
Исходный код демонстрационной процедуры
procedure p_get_telnumber(p_userid in number,
p_telnumber out number,
p_errcode out number,
p_errtext out varchar2)
is
v_objname varchar2(60) := utl_call_stack.concatenate_subprogram(utl_call_stack.subprogram(1));
v_telnumber client_telnumbers.telnumber%type;
begin
select telnumber
into v_telnumber
from client_telnumbers
where id = p_userid
and enddate = to_date('31.12.5999 23:59:59', 'dd.mm.yyyy hh24:mi:ss');
p_telnumber := v_telnumber;
p_errcode := 1;
exception
when others then
pkg_msglog.p_log_err(p_objname => v_objname,
p_msgcode => SQLCODE,
p_msgtext => SQLERRM,
p_paramvalue => 'p_userid = '||p_userid,
p_backtrace => dbms_utility.format_error_backtrace);
raise;
end p_get_telnumber;
Важно! Представленный код является примерным (примитивным) и служит только для демонстрации логирования в рамках данной статьи. В своих статьях я не выкладываю текст кода из реально действующих БД. Надеюсь, вы понимаете, что в реальности указанная процедура написана гораздо сложнее.
*Исходный код других используемых объектов смотрите в Git
Если мы будем использовать логирование ошибок как показано в предыдущей статье, то с течением времени обнаружим, что идентифицировать ошибки из данной процедуры будет сложно. Поэтому для всех ошибок попадающих в обработку исключения «WHEN OTHERS» реализована процедура pkg_msglog.p_log_archerr, которая при первом возникновении ошибки автоматически присваивает ей уникальный код и сохраняет ошибку в таблице лога. В дальнейшем, при повторении данной ошибки процедура найдет ранее созданный код и использует его при логировании в таблице лога.
В итоге, после добавления блока «архитектурного» логирования (строки с 18 по 24), наша процедура будет выглядеть следующим образом:
Исходный код демонстрационной процедуры
procedure p_get_telnumber(p_userid in number,
p_telnumber out number,
p_errcode out number,
p_errtext out varchar2)
is
v_objname varchar2(60) := utl_call_stack.concatenate_subprogram(utl_call_stack.subprogram(1));
v_telnumber client_telnumbers.telnumber%type;
begin
select telnumber
into v_telnumber
from client_telnumbers
where id = p_userid
and enddate = to_date('31.12.5999 23:59:59', 'dd.mm.yyyy hh24:mi:ss');
p_telnumber := v_telnumber;
p_errcode := 1;
exception
-- Архитектурное логирование
when others then
pkg_msglog.p_log_archerr(p_objname => v_objname,
p_msgcode => SQLCODE,
p_msgtext => SQLERRM,
p_paramvalue => 'p_userid = '||to_char(p_userid),
p_backtrace => dbms_utility.format_error_backtrace);
raise;
end p_get_telnumber;
*Исходный код других используемых объектов смотрите в Git
На этапе написания текста процедуры разработчик не всегда может предугадать возникновение той или иной ошибки (если честно, не всегда есть на это время), поэтому на начальном этапе ему достаточно «отлавливать» абсолютно все ошибки возникающие в данной процедуре с помощью оператора «WHEN OTHERS». Таким образом мы можем ввести новый термин (в рамках данного цикла статей), «Архитектурные логирование» — это логирование всех ошибок, возникновение которых не предполагается при штатной работе алгоритма. Для функционала «Архитектурных ошибок» были созданы объекты: отдельный справочник ошибок messagecodes_arch и процедура pkg_msglog.p_log_archerr создания записи в таблице лога для указанного типа ошибок.
Исходный код таблицы
create table messagecodes_arch(objectname varchar2(120) not null,
sqlerrcode number not null,
msgcode varchar2(10) not null,
insertdate date default sysdate,
constraint pk_msgcode_arch_id primary key (objectname,sqlerrcode));
Ограничение в таблице на комбинацию (Имя объекта, код ошибки SQLCODE). При первом появлении ошибки создается запись в таблице и генерируется код ошибки «SYS0000» + счетчик ошибок. При повторном появлении указанной ошибки будет взят уже сгенерированный ранее код ошибки.
*Исходный код других используемых объектов смотрите в Git
Обратите внимание, что при использовании описанной модели «архитектурного» логирования у вас появляется функционал позволяющий максимально быстро реагировать на первое появление ошибки (в конкретной функции/процедуре). Для этого необходимо реализовать отдельный мониторинг архитектурных ошибок, который постараюсь продемонстрировать в следующей (третьей) статье. Использование процедуры pkg_msglog.p_log_archerr не требует каких-либо действий кроме описания входных параметров.
Таким образом мы можем создать базовый шаблон процедуры (функции), использование которого позволит вам гарантированно отлавливать все архитектурные ошибки в вашем коде.
Шаблон процедуры/функции с архитектурным логированием
-- Шаблон процедуры/функции для построения Событийной модели логирования
-- Обратите внимание, что каждый блок "begin ... end" содержит исключение вида "when others",
-- который осуществляет логирование Архитектурных ошибок.
-- Все остальные исключения вида "no_data_found", "too_many_rows" и др.
-- будут осуществлять логирование Пользовательских ошибок
procedure p_procedure_name(p_param1 in number,
p_param2 in varchar2,
p_param3 in date,
/* others param */
p_errcode out number,
p_errtext out varchar2)
is
v_objname varchar2(60) := utl_call_stack.concatenate_subprogram(utl_call_stack.subprogram(1));
/* variable */
v_msgcode messagelog.msgcode%type;
v_msgtext messagelog.msgtext%type;
begin
/* code */
begin
/* code */
exception
-- Пользовательское логирование
when no_data_found then
v_msgcode := 'USR0000'; -- внутренний код
v_msgtext := pkg_msglog.f_get_errcode(v_msgcode);
pkg_msglog.p_log_wrn(p_objname => v_objname,
p_msgcode => v_msgcode,
p_msgtext => v_msgtext,
p_paramvalue => 'p_param1 = '||to_char(p_param1)
||', p_param2 = '||p_param2
||', p_param3 = '||to_char(p_param3,'dd.mm.yyyy hh24:mi:ss'));
p_errcode := -1;
p_errtext := v_msgtext;
end;
/* code */
/* result */
p_errcode := 1;
exception
-- Пользовательское логирование
when no_data_found or too_many_rows then
v_msgcode := 'USR0000'; -- внутренний код
v_msgtext := pkg_msglog.f_get_errcode(v_msgcode);
pkg_msglog.p_log_wrn(p_objname => v_objname,
p_msgcode => v_msgcode,
p_msgtext => v_msgtext,
p_paramvalue => 'p_param1 = '||to_char(p_param1)
||', p_param2 = '||p_param2
||', p_param3 = '||to_char(p_param3,'dd.mm.yyyy hh24:mi:ss'));
p_errcode := -1;
p_errtext := v_msgtext;
-- Архитектурное логирование
when others then
pkg_msglog.p_log_archerr(p_objname => v_objname,
p_msgcode => SQLCODE,
p_msgtext => SQLERRM,
p_paramvalue => 'p_param1 = '||to_char(p_param1)
||', p_param2 = '||p_param2
||', p_param3 = '||to_char(p_param3,'dd.mm.yyyy hh24:mi:ss'),
p_backtrace => dbms_utility.format_error_backtrace);
raise;
end p_procedure_name;
Рекомендую использовать данный шаблон для построения «Событийной модели логирования».
*Исходный код других используемых объектов смотрите в Git
В рамках событийной модели логирования, предполагается, что все архитектурные ошибки будут исправляться отдельной задачей т.е. основная цель это устранить повторное появление ошибок с кодом «SYS****» в таблице лога. В указанной задаче вам необходимо либо устранить причины возникновения данной ошибки, либо добавить отдельную обработку ошибки отличную от «when others», которую в дальнейшем будем назвать «пользовательское» логирование (в рамках данного цикла статей).
Пользовательское логирование событий
Предположим, что однажды в нашей процедуре get_telnumber произошла «архитектурная ошибка». В частности, для конкретного пользователя в таблице client_telnumbers хранится два номера телефона с признаком «активный». В таком случае, процедура «упадёт» с ошибкой «ORA-01422: too_many_rows». При этом, наш функционал архитектурного логирования сгенерировал новый код ошибки «SYS0061» и создал запись в таблице лога.
Самое важно в такой ситуации это не откладывать «на потом» исправление архитектурных ошибок. В идеале, необходимо создать отдельную задачу (баг) и в рамках неё устранить ошибку.
Предположим ,что была создана отдельная задача для устранения ошибки и назначена разработчику. В рамках этой задачи, разработчик совместно с технологом, аналитиком и др. коллегами пришел к выводу, что указанная ошибка носит систематический характер, является некорректной работой системы и требует исправления. В качестве мер исправления было решено добавить обработку события «too_many_rows» с последующим логированием события в таблице лога и выводом текста ошибки для пользователя.
Для этого в процедуре get_telnumber добавлено исключение (exception) «too_many_rows» пользовательского логирования. Также, был создан справочник пользовательских ошибок отличный от архитектурного справочника, тем что в него все записи добавляются разработчиком «вручную». Наверное это самое слабое место во всей архитектуре логирования. Предполагается, что разработчик должен описать исключение (exception) и создать для него уникальный код ошибки. Также, желательно к указанной ошибке сформулировать читаемый текст ошибки (для своих коллег, пользователя, техподдержки и т.д.), что бывает иногда очень сложным (из личного опыта).
Таблица пользовательских ошибок и процедура их «регистрации» будет выглядеть следующим образом:
Исходный код таблицы пользовательских ошибок и процедуры регистрации
create table messagecodes(msgcode varchar2(10) not null,
rustext varchar2(500) not null,
msgpriority number(1) default 1,
insertdate date default sysdate,
lastupdate date default null,
constraint pk_messagecodes_id primary key (msgcode));
Регистрация пользовательских ошибок производится процедурой p_insert_msgcode. На вход подается код и текст ошибки. В случае, если по указанному коду нет записей в справочнике messagecodes, то создается новая запись (производится регистрация). В случае, если по коду ошибки найдена запись, то производится сравнение текстов ошибки, в случае расхождений производится обновление текста, иначе работа процедуры завершается без изменений. Таким образом мы всегда можем корректировать текст ошибок.
-- Пример регистации пользовательских ошибок
begin
pkg_msglog.p_insert_msgcode('USR0001','Не найден адрес электронной почты клиента id = $1!',1);
pkg_msglog.p_insert_msgcode('USR0002','Не найден лицевой счет клиента id = $1!',5);
pkg_msglog.p_insert_msgcode('USR0003','Для клиента id = $1 найдено два и более активных номеров телефона!',1);
pkg_msglog.p_insert_msgcode('USR0004','Номер мобильного телефона не соответствует маске (clientid = $2)',1);
commit;
end;
Обратите внимание, что текст ошибок имеет параметризацию т.е. для ошибки в тексте имеются специальные символы $1, $2, $3 и т.д. Например, рассмотрим ошибку «USR0003» с текстом «Для клиента id = $1 найдено два и более активных номеров телефона!» при вызове функции f_get_errcode на вход подаётся код ошибки и параметры ошибки. Далее, функция по коду ошибки найдет строку, в тексте ошибки заменит подстроку «$1» на значение параметра to_char(p_userid) т.е. подставит значение to_char(p_userid).
v_msgcode := 'USR0003'; -- внутренний код
v_msgtext := pkg_msglog.f_get_errcode(p_msgcode => v_msgcode,
p_msgparam => to_char(p_userid));
В случае если в тексте ошибки будут два и более спецсимвола $1, $2, $3 и т.д., то параметры передаются с использованием символа-разделителя «;».
Итого, содержимое справочника пользовательских ошибок будет выглядеть следующим образом:
*Исходный код других используемых объектов смотрите в Git
После того, как мы «зарегистрировали» пользовательскую ошибку «USR0003» и добавив отдельную обработку пользовательского логирования (строки с 19 по 28), наша процедура get_telnumber будет выглядеть следующим образом:
Исходный код демонстрационной процедуры
procedure p_get_telnumber(p_userid in number,
p_telnumber out number,
p_errcode out number,
p_errtext out varchar2)
is
v_objname varchar2(60) := utl_call_stack.concatenate_subprogram(utl_call_stack.subprogram(1));
v_telnumber client_telnumbers.telnumber%type;
v_msgcode messagelog.msgcode%type;
v_msgtext messagelog.msgtext%type;
begin
select telnumber
into v_telnumber
from client_telnumbers
where id = p_userid
and enddate = to_date('31.12.5999 23:59:59', 'dd.mm.yyyy hh24:mi:ss');
p_telnumber := v_telnumber;
p_errcode := 1;
exception
-- Пользовательское логирование
when too_many_rows then
v_msgcode := 'USR0003'; -- внутренний код
v_msgtext := pkg_msglog.f_get_errcode(v_msgcode,to_char(p_userid));
pkg_msglog.p_log_wrn(p_objname => v_objname,
p_msgcode => v_msgcode,
p_msgtext => v_msgtext,
p_paramvalue => 'p_userid = '||to_char(p_userid));
p_errcode := -1;
p_errtext := v_msgtext;
-- Архитектурное логирование
when others then
pkg_msglog.p_log_archerr(p_objname => v_objname,
p_msgcode => SQLCODE,
p_msgtext => SQLERRM,
p_paramvalue => 'p_userid = '||to_char(p_userid),
p_backtrace => dbms_utility.format_error_backtrace);
raise;
end p_get_telnumber;
*Исходный код других используемых объектов смотрите в Git
При повторном возникновении ошибки «too_many_rows» обработка события пройдет по нашему сценарию «пользовательского» логирования. Таким образом мы можем ввести второй термин (в рамках данного цикла статей), «Пользовательские логирование» — это логирование всех ошибок, возникновение которых предполагается и ожидается при нештатной работе алгоритма. В итоге, пользователь получит читаемый текст ошибки с кодом «USR0003», также, мы же всегда сможем подсчитать количество ошибок с указанным кодом. В случае большого количества ошибок у нас на руках будет «живая» статистика частоты возникновения ошибки и их количества, что позволит нам выйти на руководство с предложением по доработке/оптимизации процесса.
Давайте рассмотрим еще один пример (кейс из реального случая), в момент когда процедура get_telnumber по id клиента находит один «активный» номер телефона иногда возникает ситуация, что номер телефона не принадлежит мобильному оператору. Ситуации бывают разные иногда указанный номер мог быть номером городской телефонной сети, иногда номером международного оператора, а иногда вообще набор из нескольких цифр и т.д. Основным требованием от бизнес-заказчика было использование номера телефона российских операторов мобильной связи. Поэтому было решено добавить проверку соответствия найденного номера некому «корректному» шаблону (строки с 18 по 29). В случае обнаружения некорректного номера, логировать данное событие отдельным кодом «USR0004» и типом «WRN». Добавим функцию проверки корректности номера телефона, если номер соответствует шаблону (требованиям), то вернем номер телефона, иначе пустое значение.
Исходный код демонстрационной процедуры
procedure p_get_telnumber(p_userid in number,
p_telnumber out number,
p_errcode out number,
p_errtext out varchar2)
is
v_objname varchar2(60) := utl_call_stack.concatenate_subprogram(utl_call_stack.subprogram(1));
v_telnumber client_telnumbers.telnumber%type;
v_newtelnumber client_telnumbers.telnumber%type;
v_msgcode messagelog.msgcode%type;
v_msgtext messagelog.msgtext%type;
begin
select telnumber
into v_telnumber
from client_telnumbers
where id = p_userid
and enddate = to_date('31.12.5999 23:59:59', 'dd.mm.yyyy hh24:mi:ss');
v_newtelnumber := f_check_telnumber(v_telnumber);
if v_newtelnumber is not null then
p_telnumber := v_telnumber;
p_errcode := 1;
else
-- Пользовательское логирование
v_msgcode := 'USR0004'; -- внутренний код
v_msgtext := pkg_msglog.f_get_errcode(v_msgcode,to_char(p_userid));
pkg_msglog.p_log_wrn(p_objname => v_objname,
p_msgcode => v_msgcode,
p_msgtext => v_msgtext,
p_paramvalue => 'p_userid = '||to_char(p_userid));
p_errcode := -1;
p_errtext := v_msgtext;
end if;
exception
-- Пользовательское логирование
when too_many_rows then
v_msgcode := 'USR0003'; -- внутренний код
v_msgtext := pkg_msglog.f_get_errcode(v_msgcode,to_char(p_userid));
pkg_msglog.p_log_wrn(p_objname => v_objname,
p_msgcode => v_msgcode,
p_msgtext => v_msgtext,
p_paramvalue => 'p_userid = '||to_char(p_userid));
p_errcode := -1;
p_errtext := v_msgtext;
-- Архитектурное логирование
when others then
pkg_msglog.p_log_archerr(p_objname => v_objname,
p_msgcode => SQLCODE,
p_msgtext => SQLERRM,
p_paramvalue => 'p_userid = '||to_char(p_userid),
p_backtrace => dbms_utility.format_error_backtrace);
raise;
end p_get_telnumber;
*Исходный код других используемых объектов смотрите в Git
После сбора статистических данных по конкретной ошибке с кодом «USR0004», руководству стало понятно, что ошибка актуальна и количество ошибок с течением времени не только не уменьшается, а наоборот растет с линейной прогрессией. В дальнейшем, были выявлены источники «кривых» данных и были установлены внутренние требования по первичной обработке номера телефона клиентов. В итоге, со временем количество ошибок уменьшилось до нуля. И этого нельзя было добиться до тех пор, пока у всех участвующих лиц не возникло понимание о масштабе проблемы.
Выполняя самый банальный запрос в таблицу лога с группировкой по типу сообщения (msgtype), имени объекта (objname) и вашему внутреннему коду ошибки (msgcode) за отдельный квартал, вы сможете увидеть реальную картинку частоты возникновения той или иной ошибки. Как только в вашей БД появляется ошибка с большим количеством повторений вы всегда сможете выявить это событие и принять решение об устранении.
Исходный код запроса
select msgtype,
objname,
msgcode,
count(*)
from messagelog
where insertdate between to_date('01.01.2021', 'dd.mm.yyyy') and to_date('31.03.2021', 'dd.mm.yyyy')
group by msgtype, objname, msgcode
order by 4 desc
Результат запроса:
*Исходный код других используемых объектов смотрите в Git
Заключение
В заключении наверное скажу банальную вещь, о том что ваша БД является сложным механизмом ежесекундно выполняющая рутинные операции. Прямо сейчас в БД могут происходить различные ошибки. Критичные, которые вы исправляете практически сразу или некритичные, о которых вы можете вообще не знать. И если у вас нет информации о подобных ошибках, то возникает вопрос: «Нужно ли их вообще исправлять? Или можно подождать до тех пор, пока проблема не всплывёт?». Вопрос наверное «риторический».
Я же данной статьёй хотел показать один из способов ведения логирования с кодированием отдельных событий. Данный метод требует некоторых «обязательств» от разработчика и в нынешнее время этого тяжело добиться. В следующей статье постараюсь показать один из способов мониторинга ошибок основанный напрямую по кодам ошибок созданных в текущей статье.
Спасибо за внимание.