
Март 2005
Профессионалу разработчику
Стивен Фернстайн
Строки с ошибками
(Tracing Lines
By Steven Feuerstein)
Источник: журнал Oracle Magazine, no.2, 2005,
http://www.oracle.com/technology/oramag/oracle/05-mar/o25plsql.html
Найдите ошибки и сообщите о них – с указанием номеров строк – в базе данных Oracle 10g.
PL/SQL предлагает мощную и гибкую архитектуру исключений. Но совершенству нет предела, и в базе данных Oracle 10g обработка исключений существенно продвинулась вперед благодаря появлению функции
DBMS_UTILITY.FORMAT_ERROR_BACKTRACE. В этой статье рассматривается проблема, которую решает эта функция, и то, как лучше ее использовать.
Кто вызвал это исключение?
Когда исключение вызвано, одним из наиболее важных фрагментов информации, которую программист хотел бы получить, является строка кода, вызвавшая исключение. До версии Oracle 10g эту информацию можно было получить, только оставив исключение необработанным.
Давайте рассмотрим сценарий обработки исключения, доступный программистам в базе данных Oracle9i. Обсудим следующую простую цепочку вызовов программ, приведенную в Листинге 1: процедура proc 3 вызывает proc2, которая в свою очередь вызывает proc1, при этом proc 1 вызывает исключение NO_DATA_FOUND. Обратите внимание, что ни в одной из процедур нет обработки ошибок; ее особенно не хватает в процедуре верхнего уровня proc3. При запуске proc3 в SQL*Plus, я получу следующие результаты:
ERROR at line 1:
ORA-01403: no data found
ORA-06512: at "SCOTT.PROC1", line 4
ORA-06512: at "SCOTT.PROC2", line 6
ORA-06512: at "SCOTT.PROC3", line 4
ORA-06512: at line 3
Код Листинга 1: Стек процедур.
CREATE OR REPLACE PROCEDURE proc1 IS
BEGIN
DBMS_OUTPUT.put_line ('running proc1');
RAISE NO_DATA_FOUND;
END;
/
CREATE OR REPLACE PROCEDURE proc2 IS
l_str VARCHAR2(30)
:= 'calling proc1';
BEGIN
DBMS_OUTPUT.put_line (l_str);
proc1;
END;
/
CREATE OR REPLACE PROCEDURE proc3 IS
BEGIN
DBMS_OUTPUT.put_line ('calling proc2');
proc2;
END;
/
Это сообщение об ошибке, инициированной необработанным исключением, и оно показывает, что ошибка была вызвана строкой 4 процедуры proc1. С одной стороны мы должны быть очень довольны таким поведением. Теперь, когда у нас есть номер строки, мы можем обратить внимание непосредственно на код, вызвавший ошибку, и отладить его. С другой стороны, мы получили эту информацию, оставив исключение необработанным. Однако во многих приложениях мы стараемся избегать необработанных исключений.
Давайте посмотрим, что произойдет, если добавить раздел обработки исключений в процедуру proc3 и затем отобразить информацию об ошибке (простейшая форма регистрации ошибок). Вот вторая версия proc3:
CREATE OR REPLACE PROCEDURE proc3
IS
BEGIN
DBMS_OUTPUT.put_line ('calling proc2');
proc2;
EXCEPTION
WHEN OTHERS THEN
my_putline (
DBMS_UTILITY.FORMAT_ERROR_STACK);
END;
/
Обратите внимание, что я вызвал DBMS_UTILITY.FORMAT_ERROR_STACK, потому что она возвращает полное сообщение об ошибке. Конечно, DBMS_OUTPUT.PUT_LINE вызовет исключение, если ей передать строку, длиннее 255 символов, поэтому я буду отображать сообщение об ошибке, используя свою собственную улучшенную версию DBMS_OUTPUT.PUT_LINE - процедуру my_putline (которую можно скачать).
Откомпилировав новую proc3, я запустил ее в SQL*Plus и увидел следующий результат:
SQL> SET SERVEROUTPUT ON
SQL> exec proc3
вызов proc2
вызов proc1
выполнение proc1
ORA-01403: no data found
Иначе говоря, DBMS_UTILITY.FORMAT_ERROR_STACK не показывает полный стек ошибок с номерами строк; SQLERRM работает аналогичным образом.
Спасительная функция
В базе данных Oracle 10g добавлена функция DBMS_UTILITY.FORMAT_ERROR_BACKTRACE, которая может и должна вызываться в обработчике исключений. Она отображает стек вызовов в точке, в которой произошло исключение, даже если эта функция вызвана в PL/SQL блоке внешнем по отношению к тому, в котором произошло исключение. Следовательно, можно вызвать DBMS_UTILITY.FORMAT_ERROR_BACKTRACE в разделе исключений процедуры верхнего уровня нашего стека и все же обнаружить, где произошла ошибка в стеке вызовов.
Перейдя на версию Oracle 10g, я могу теперь вернуться к процедуре proc3, и заменить вызов FORMAT_ERROR_STACK на FORMAT_ERROR_BACKTRACE, как показано в Листинге 2. Я буду продолжать использовать my_putline, поскольку сообщение может быть очень длинным, если стек вызовов глубокий (и названия ваших программ длинные).
Код Листинга 2: Proc3, использующая FORMAT_ERROR_BACKTRACE
CREATE OR REPLACE PROCEDURE proc3
IS
BEGIN
DBMS_OUTPUT.put_line ('calling proc2');
proc2;
EXCEPTION
WHEN OTHERS
THEN
DBMS_OUTPUT.put_line ('Стек ошибок на верхнем уровне:');
my_putline (DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
END;
/
И теперь, запустив proc3, я увижу следующий результат:
SQL> SET SERVEROUTPUT ON
SQL> exec proc3
вызов proc2
вызов proc1
выполнение proc1
Стек ошибок на верхнем уровне:
ORA-06512: at "SCOTT.PROC1", line 4
ORA-06512: at "SCOTT.PROC2", line 6
ORA-06512: at "SCOTT.PROC3", line 4
Иначе говоря, информацию, которая раньше была доступна только через необработанное исключение, теперь можно получить внутри PL/SQL кода.
Воздействие многочисленных RAISE
Исключения часто встречаются глубоко внутри стека вызовов. Если вы хотите, чтобы исключение все время распространялось на внешний PL/SQL блок, оно должно быть вновь вызвано внутри каждого обработчика исключений в стеке блоков. В Листинге3 показан пример такого рода:
Код Листинга 3: Повторный вызов исключений во внешнем блоке стека.
CREATE OR REPLACE PROCEDURE proc1 IS
BEGIN
DBMS_OUTPUT.put_line ('выполнение proc1');
RAISE NO_DATA_FOUND;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.put_line (
'Стек ошибок в блоке, в котором они произошли:');
my_putline (
DBMS_UTILITY.format_error_backtrace);
RAISE;
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 ('Стек ошибок на верхнем уровне:');
my_putline (DBMS_UTILITY.format_error_backtrace);
bt.show (DBMS_UTILITY.format_error_backtrace);
END;
/
При запуске кода, представленного в Листинге 3, я получаю следующий результат:
SQL> exec proc3
вызов proc2
вызов proc1
выполнение proc1
Стек ошибок в блоке, в котором они произошли:
ORA-06512: at "SCOTT.PROC1", line 4
Стек ошибок на верхнем уровне:
ORA-06512: at "SCOTT.PROC1", line 11
ORA-06512: at "SCOTT.PROC2", line 6
ORA-06512: at "SCOTT.PROC3", line 4
Program owner = SCOTT
Program name = PROC1
Line number = 11
Когда я вызываю функцию DBMS_UTILITY.FORMAT_ERROR_BACKTRACE внутри программы нижнего уровня, она совершенно верно указывает строку 4 процедуры proc1 в качестве строки, изначально вызвавшей ошибку. Затем я снова инициирую то же самое исключение, используя оператор RAISE. Когда исключение распространяется на внешний блок, я вновь вызываю функцию DBMS_UTILITY.FORMAT_ERROR_BACKTRACE, и на этот раз она показывает, что ошибка была вызвана строкой 11 процедуры proc1.
Из этого поведения можно сделать вывод, что DBMS_UTILITY.FORMAT_ERROR_BACKTRACE показывает трассу выполнения обратную по отношению к последнему оператору RAISE сессии. Как только вы инициируете конкретное исключение или вызываете вновь текущее исключение, стек, с которым работает функция DBMS_UTILITY.FORMAT_ERROR_BACKTRACE, начинается заново. Это означает, что если вы хотите воспользоваться преимуществами этой функции, необходимо использовать один из двух подходов:
- Вызывайте функцию DBMS_UTILITY.FORMAT_ERROR_BACKTRACE в разделе исключений блока, который вызвал ошибку. В этом случае вы получите (и сможете зарегистрировать) необходимый номер строки, даже если это исключение вновь вызывается дальше в стеке.
- Избегайте обработчиков исключений в промежуточных программах вашего стека, и вызывайте функцию DBMS_UTILITY.FORMAT_ERROR_BACKTRACE в разделе исключений внешней программы вашего стека.
Только номер строки, пожалуйста
В реальных приложениях сообщение об ошибке может быть очень длинным. Как правило, отладчики и поддерживающий персонал не хотят иметь дело с целым стеком; их гораздо больше интересует самый верхний компонент. Разработчик приложения может, при желании, показать эту важную информацию пользователям, чтобы они могли немедленно и точно сообщить о проблеме поддерживающему персоналу.
В этом случае необходимо разбирать строку, которую возвращает функция, и выбирать только самый верхний компонент. Для этого я создал утилиту, которая называется пакет BT . В этом пакете предусмотрен простой и понятный интерфейс, как показано ниже:
CREATE OR REPLACE 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 содержит отдельные поля для каждого элемента сообщения, которые я хочу выбрать (владелец программного модуля, название программного модуля и номер строки в программе). Вместо того чтобы вызывать и разбирать функцию backtrace в каждом разделе исключений, я вызываю функцию bt.info и выдаю конкретное сообщение об ошибке.
Например, с использованием функции bt.info раздел исключений proc3 выглядит теперь как код, представленный в Листинге 4.
Код Листинга 4: Исправленная proc3 вызывает bt.info
CREATE OR REPLACE PROCEDURE proc3
IS
BEGIN
DBMS_OUTPUT.put_line ('вызов proc2');
proc2;
EXCEPTION
WHEN OTHERS
THEN
DECLARE
l_trace bt.error_rt;
BEGIN
l_trace := bt.info (DBMS_UTILITY.format_error_backtrace);
raise_application_error (-20000
, 'Ошибка '
|| SQLCODE
|| ' обнаружена в строке '
|| l_trace.line_number
|| ' в программе '
|| l_trace.program_owner
|| '.'
|| l_trace.program_name
);
END;
END proc3;
/
Сообщение об ошибке теперь предлагает адресату гораздо более конкретную информацию. Реализация этой функции проста; самое главное, о чем следует помнить при написании утилиты вроде этой – это создание гибкого и хорошо структурированного кода. Основной задачей является разбор строки следующего формата:
ORA-NNNNN: at "OWNER.PROGRAM_NAME",
line NNN
Вот шаги, которые я предпринял:
1. Выяснив, что мне придется разбирать содержимое строки, основанной на различных разделителях, я объявил несколько констант для хранения этих разделителей. Таким образом, в дальнейшем я смогу избежать жесткого кодирования этих значений в своей программе (и, возможно, более одного раза).
c_name_delim CONSTANT CHAR (1)
:= '"';
c_dot_delim CONSTANT CHAR (1)
:= '.';
c_line_delim CONSTANT CHAR (4)
:= 'line';
c_eol_delim CONSTANT CHAR (1)
:= CHR (10);
2. Самым первым шагом в моей функции info является выполнение нескольких вызовов INSTR, чтобы найти в строке начальное и конечное расположение различных элементов, которые необходимо выделить и вернуть отдельно. Я поместил весь этот код в отдельную процедуру инициализации в Листинге 5.
Код Листинга 5: Процедура инициализации в bt.info
PROCEDURE initialize_values
IS
BEGIN
l_name_start_loc := INSTR (backtrace_in, c_name_delim, 1, 1);
l_dot_loc := INSTR (backtrace_in, c_dot_delim);
l_name_end_loc := INSTR (backtrace_in, c_name_delim, 1, 2);
l_line_loc := INSTR (backtrace_in, c_line_delim);
l_eol_loc := INSTR (backtrace_in, c_eol_delim);
END initialize_values;
3. Теперь, зная расположение этих элементов, можно использовать SUBSTR, чтобы извлекать необходимые части и присваивать их полям моей записи, которая будет возвращена в вызывающую программу, как показано в Листинге 6.
Код листинга 6: Исполняемый раздел функции bt.info
BEGIN
initialize_values;
retval.program_owner :=
SUBSTR (backtrace_in
, + 1
, l_dot_loc - - 1
);
retval.program_name :=
SUBSTR(backtrace_in,l_dot_loc + 1,l_name_end_loc - l_dot_loc - 1);
retval.line_number :=
SUBSTR(backtrace_in,l_line_loc + 5,l_eol_loc - l_line_loc - 5);
RETURN retval;
END info;
Заключение
Будете ли вы вызывать DBMS_UTILITY.FORMAT_ERROR_BACKTRACE напрямую или использовать утилиту вроде bt.info, чтобы разбирать эту строку, информация эта очень полезна. Обработка ошибок и их разрешение стало гораздо легче в базе данных Oracle 10g.
Стивен Фернстайн (steven@stevenfeuerstein.com) - авторитет в языке PL/SQL. Он является автором девяти книг по PL/SQL (все - издательства O'Reilly Media, Inc.), включая Oracle PL/SQL Best Practices и Oracle PL/SQL Programming. Фернстайн разработал новый активный обучающий инструментарий для разработчиков, называемый Qnxo, предлагает обучение PL/SQL, и является старшим технологическим консультантом компании Quest Software.
|