Kostenlos

Язык PL/SQL

Text
Als gelesen kennzeichnen
Schriftart:Kleiner AaGrößer Aa

7 BEGIN

8

9 SELECT surname,course_works,elective_courses

10 INTO l_surname,l_course_works,l_elective_courses

11 FROM students WHERE id=l_student_id;

12

13 DBMS_OUTPUT.PUT_LINE('Студент: '||l_surname);

14

15 IF l_course_works.EXISTS(1) or l_course_works.EXISTS(2) THEN

16 DBMS_OUTPUT.PUT_LINE('Курсовые на младших курсах:');

17 ELSE

18 DBMS_OUTPUT.PUT_LINE('Курсовые на младших курсах отсутствуют')

19 END IF;

20

21 FOR i in 1..2 LOOP

22 IF l_course_works.EXISTS(i) THEN

23 DBMS_OUTPUT.PUT_LINE(' Курсовая на '||i||' курсе: ' ||

24 ' оценка '||l_course_works(i));

25 END IF;

26 END LOOP;

27

28 DBMS_OUTPUT.PUT_LINE('Курсовые на старших курсах:');

29

30 l_row_index := l_course_works.NEXT(2);

31 WHILE l_row_index IS NOT NULL LOOP

32 DBMS_OUTPUT.PUT_LINE(' Курсовая на '||l_row_index

33 ||' курсе: оценка ' ||l_course_works(l_row_index));

34 l_row_index := l_course_works.NEXT(l_row_index);

35 END LOOP;

36

37 DBMS_OUTPUT.PUT_LINE('Факультативы (всего '

38 ||l_elective_courses.COUNT()||'):');

39

40 l_row_index := l_elective_courses.FIRST();

41 WHILE l_row_index IS NOT NULL LOOP

42 DBMS_OUTPUT.PUT_LINE(' ' ||l_elective_courses(l_row_index));

43 l_row_index := l_elective_courses.NEXT(l_row_index);

44 END LOOP;

45

46 END;

47 /

Студент: Ильин

Курсовые на младших курсах:

Курсовая на 1 курсе: оценка 4

Курсовая на 2 курсе: оценка 4

Курсовые на старших курсах:

Курсовая на 3 курсе:

Курсовая на 4 курсе: оценка 5

Курсовая на 5 курсе: оценка 5

Факультативы (всего 2):

Оптимизация баз данных

Теория надежности

PL/SQL procedure successfully completed.

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

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

Распространено мнение, что только половина профессионально написанного исходного кода реализует собственно функциональность программы. Остальной код – это ведение журнала программы, сохранение отладочной информации и обработка всевозможных ошибок.

Понятие исключения

Исключением (exception) в PL/SQL называется ситуация, которая не должна возникать при нормальном выполнении программы PL/SQL.

Существует два типа исключений PL/SQL:

системные исключения (run-time system exceptions), которые автоматически инициируются виртуальной машиной PL/SQL при возникновении программных ошибок этапа выполнения;

пользовательские исключения (user-defined exceptions), объявляемые программистом в коде PL/SQL и используемые при реализации бизнес-логики.

Программной ошибкой этапа выполнения (run-time program error) называется ситуация, когда наблюдается неожиданное поведение программы, затрудняющее или делающее невозможным достижение целей пользователя. Примерами программных ошибок могут служить попытки деления на ноль, ошибки преобразования символов в числа, ошибки выполнения предложений SQL.

Пользовательские исключения инициируются в программах PL/SQL в том случае, когда на прикладном уровне возникли отклонения от стандартного процесса обработки данных. Например, при обработке поступивших данных встретился чек с отрицательной суммой покупки или не в рублях. С точки зрения правил бизнес-логики это такая же ошибка, как и деление на ноль с точки зрения правил арифметики. Для попытки деления на ноль в ходе выполнения программы системное исключение будет автоматически инициировано виртуальной машиной PL/SQL, потому что она «знает» правила арифметики. Для поступающих ошибочных платежей инициировать пользовательское исключение должен в своем коде программист PL/SQL, потому что он знает правила бизнес-логики вида «Платежи принимаются только в рублях, на положительные суммы с точностью до копеек», «Платежи принимаются только для открытой смены контрольно-кассовой машины (ККМ)» и так далее.

Таким образом, основное различие системных и пользовательских исключений заключается в том, что они инициируются по-разному. Системное исключение автоматически инициируется виртуальной машиной, происходит неожиданно и обычно его появление говорит о том, что скоро придется решать проблемы самого разного вида. С пользовательскими исключениями все гораздо спокойнее – сами исключения, их инициирование специальными командами PL/SQL в коде и штатная обработка для выправления положения заранее предусматриваются программистом при проектировании.

Правила работы с исключениями:

пользовательские исключения объявляются в разделах объявлений блоков PL/SQL и имеют имена;

системные исключения имен не имеют, они характеризуются номером ошибки;

имеется возможность объявить пользовательское исключение и с помощью директивы компилятору связать его с некоторым номером ошибки.

Несколько исключений для часто возникающих в программах PL/SQL ошибок объявлено во встроенном пакете STANDARD с привязкой к соответствующим номерам ошибок. Эти исключения называются предопределенными исключениями PL/SQL (predefined exception) и их можно использовать в любых программах PL/SQL без дополнительных объявлений.

Таблица 2. Предопределенные исключения PL/SQL.

Исключение

Описание исключения (номер ошибки)

INVALID_CURSOR

ссылка на несуществующий курсор (ORA-01001)

NO_DATA_FOUND

не найдены данные командой

SELECT INTO (ORA-01403)

DUP_VAL_ON_INDEX

попытка вставить в столбец с ограничением

на уникальность значение-дубликат (ORA-00001)

TOO_MANY_ROWS

команда SELECT INTO возвращает

более одной строки (ORA-01422)

VALUE_ERROR

арифметическая ошибка, ошибка преобразования

или усечения чисел и строк (ORA-06502)

INVALID_NUMBER

ошибка преобразования строки в число (ORA-01722)

PROGRAM_ERROR

внутренняя ошибка PL/SQL (ORA-06501)

ZERO_DIVIDE

попытка деления на ноль (ORA-01476)

Схема обработки исключений в Java

В языке программирования Java при описании работы с исключениями используется бейсбольная терминология. При возникновении исключения бросается (throws) объект-исключение. Этот объект как бейсбольный мяч пролетает через исходный код, появившись сначала в том методе, где произошло исключение. В одном или нескольких местах кода объект-исключение пытаются (try) поймать (catch) и обработать. Причем исключение можно обработать в одном месте кода полностью, а можно обработать исключение частично, выбросить его из обработчика снова, поймать в другом месте и обрабатывать дальше.

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

try{

}

catch(ArithmeticException ae){

System.out.println("From Arithm.Exc. catch: "+ae);

}

catch(ArraylndexOutOfBoundsException arre){

System.out.println("From Array.Exc.catch: "+arre);

}

}

Схема обработки исключений в PL/SQL

Работа с исключениями в PL/SQL очень похожа на то, как это делается в Java.

Для обработки исключений предназначен последний раздел блока PL/SQL – раздел обработки исключений. Этот последний раздел блока после ключевого слова EXCEPTION похож на то, что в Java указывается после ключевого слова catch. Перед обсуждением правил обработки исключений приведем небольшой пример с комментариями в коде.

DECLARE

a INTEGER;

BEGIN

a := 1;

a := 2/0; – бросается предопределенное исключение ZERO_DIVIDE

a := 3; – над этой командой пролетает, команда не выполняется

a := 4; – над этой командой тоже пролетает, команда не выполняется

– управление передается в раздел обработки исключений,

– начинаем «примерку» обработчиков

EXCEPTION

– не подходит по имени ловимого исключения к прилетевшему ZERO_DIVIDE

WHEN PROGRAM_ERROR THEN

DBMS_OUTPUT.PUT_LINE('Программная ошибка');

– оба имени ловимых исключений не подходят к прилетевшему ZERO_DIVIDE

WHEN INVALID_NUMBER OR VALUE_ERROR THEN

DBMS_OUTPUT.PUT_LINE('Ошибка работы с числами и строками');

– подходит по имени к ZERO_DIVIDE (поймали), заходим внутрь обработчика

WHEN ZERO_DIVIDE THEN

DBMS_OUTPUT.PUT_LINE('Ошибка деления на ноль');

– OTHERS ловит все, что не поймали другие до него,

– но сюда в этом случае «примерка» не дошла, раньше поймали

WHEN OTHERS THEN

DBMS_OUTPUT.PUT_LINE('При выполнении произошла ошибка '||SQLERRM);

END;

При инициировании исключения в блоке PL/SQL выполнение потока команд блока прекращается, и управление передается в раздел обработки исключений этого блока, если такой раздел есть, или в родительский блок, если раздела обработки исключений у блока нет.

Сразу после инициирования исключение получает статус «не обработано», и можно сказать, что с этим статусом исключение бросается и летит над кодом программы. Летит оно именно над кодом, пропуская все команды исполняемых разделов, задерживаясь только в разделах обработки исключений вложенных блоков. Полет исключения прекращается в том блоке, в разделе обработки исключений которого исключение смогли поймать и обработать. Сразу после этого управление будет передано блоку, родительскому по отношению к тому блоку, где эта обработка произошла.

Действия в разделе обработки исключений

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

 

определение, какой обработчик в разделе ловит прилетевшее исключение («примерка» обработчиков);

обработка исключения подходящим обработчиком.

«Примерка» обработчиков осуществляется по именам исключений – перед каждым обработчиком указывается список имен исключений, которые он ловит.

Если исключение не имеет имени или его имя не соответствует ни одному из имен исключений, указанных в разделе обработки исключений, то оно обрабатывается OTHERS-обработчиком, если он имеется. OTHERS-обработчик в разделе обработки исключений указывается последним и на него возлагается задача поймать все то, что не поймали другие обработчики перед ним – и системные исключения и пользовательские исключения с любыми именами.

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

если никакой обработчик исключению не подошел, то исключение со статусом «не обработано» бросается в дальнейший полет уже в родительском блоке (блоке, предыдущим по вложенности) с того места кода, где заканчивается вложенный блок;

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

Работа обработчика свою очередь может завершиться тремя исходами:

команды обработчика успешно выполнились, исключение получает статус «обработано» и управление передается родительскому блоку в то место кода, где заканчивается вложенный блок;

в процессе работы обработчика принято решение, что обрабатывать исключение надо не в этом обработчике, тогда исключение здесь же в обработчике инициируется повторно вызовом команды RAISE без параметров;

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

Два последних исхода работы обработчика предполагают, что из блока даже с подходящим обработчиком исключение бросится дальше – либо то же самое (после вызова команды RAISE), либо уже другое. Могло прилететь пользовательское исключение, обработаться со своей ошибкой, поэтому из обработчика бросится и в родительском блоке полетит дальше уже системное исключение, как в примере с ошибкой добавления строки в специальную таблицу журнала ошибок.

Примеры обработки исключений

Рассмотрим примеры полетов исключений в программе из трех вложенных блоков:

BEGIN – начало блока1

команда1_блока1;

команда2_блока1;

команда3_блока1;

BEGIN – начало блока2

команда1_блока2;

команда2_блока2 l_int := 1/TO_NUMBER(l_var) (l_var='1' или '0' или 'a')

команда3_блока2;

EXCEPTION

WHEN ZERO_DIVIDE THEN

команда1_zero_блока2;

команда2_zero_блока2;

END; – конец блока2

команда4_блока1;

команда5_блока1;

EXCEPTION

WHEN ZERO_DIVIDE THEN

команда1_zero_блока1;

команда2_zero_блока1;

WHEN OTHERS THEN

команда1_others_блока1;

команда2_others_блока1;

END; – конец блока 1

Пусть команда2_блока2 имеет вид l_int:=1/TO_NUMBER(l_var); где l_int – целочисленная переменная, l_var – символьная.

l_var='1' (без исключений)

l_var='0' (zero divide)

l_var='a' (conversion error)

команда1_блока1;

команда2_блока1;

команда3_блока1;

команда1_блока2;

команда2_блока2;

команда3_блока2;

команда4_блока1;

команда5_блока1;

команда1_блока1;

команда2_блока1;

команда3_блока1;

команда1_блока2;

команда2_блока2(error);

– в блоке 2

– ловится ZERO_DIVIDE:

команда1_zero_блока2;

команда2_zero_блока2;

–продолжение блока 1:

команда4_блока1;

команда5_блока1;

команда1_блока1;

команда2_блока1;

команда3_блока1;

команда1_блока2;

команда2_блока2(error);

–в блоке 2 ошибка

–преобразования

–не ловится, т.к.

–там только ZERO_DIVIDE

– в блоке 1

– ZERO_DIVIDE

– второй раз не ловит,

– а ловит OTHERS

– (он же все ловит):

команда1_others_блока1;

команда2_others_блока1;

Рассмотрим три случая в зависимости от значения, которое принимает переменная l_var ('1', или '0', или 'a').

Когда l_var=1 (первый столбец таблицы) исключения не инициируются выполняются все команды из разделов выполнения в той последовательности, как они записаны в коде.

В случае ошибки деления на ноль (второй столбец таблицы, l_var='0') в команде2_блока2 выполнение блока 2 прекращается, все остальные команды в блоке 2 после нее не выполняются, управление передается в раздел EXCEPTION блока 2, где пытаются поймать исключение деления на ноль (ZERO_DIVIDE). Подходящий обработчик в разделе обработки исключений блока 2 есть, поэтому исключение ловится в блоке 2, в котором успешно выполняются команды обработчика. После успешной обработки продолжается выполнение команд блока 1, родительского для блока 2, в котором произошла обработка исключения.

В случае ошибки преобразования символа к числу (третий столбец таблицы, l_var='a') исключение ошибки преобразования не ловится в разделе EXCEPTION блока 2 и PL/SQL передает управление в родительский блок 1, сразу после END блока 2 и исключение пытаются поймать в разделе EXCEPTION блока 1. В разделе обработки исключений блока 1 есть два обработчика (ZERO_DIVIDE и OTHERS). «Примерка» обработчиков к прилетевшему исключению начинается в той последовательности, как они записаны в коде (сверху вниз). ZERO_DIVIDE для этого исключения не подходит при «примерке» уже второй раз, а OTHERS-обработчик ловит все исключения, поэтому управление передается ему и выполняются две его команды. После успешного выполнения команд обработчика исключение получает статус «обработано».

Передача исключений в вызывающую среду

При разработке клиентских приложений при любом обращении к базе данных нужно предусмотреть обработку ошибок, которые могут произойти как при вызове хранимых программ PL/SQL, так и при выполнении предложений SQL. В коде клиентских программ для этого следует использовать конструкции try/catch, имеющиеся в Java и C, или try/except – в Python.

Если в ходе работы программы PL/SQL произошло так никем и не обработанное исключение, то оно вылетит «наружу», то есть будет передано вызывавшей среде. Например, в SQL*Plus или в прикладное клиентское приложение. В SQL*Plus это выглядит вот так:

Без вылета исключения «наружу»

С вылетом исключения «наружу»

SQL> DECLARE

2 i int;

3 BEGIN

4 i := 1/0;

5 EXCEPTION

6 WHEN OTHERS THEN

7 DBMS_OUTPUT.PUT_LINE('/0');

8 END;

9 /

/0

PL/SQL procedure successfully

completed.

SQL> DECLARE

2 i int;

3 BEGIN

4 i := 1/0;

5 END;

6 /

DECLARE

*

ERROR at line 1:

ORA-01476: divisor is equal to zero

ORA-06512: at line 4

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

En error was encountered performing the requested operation

ORA-00604: error occurred at recursive SQL level 1

ORA-06502: PL/SQL: numeric or value error: character string buffer too small

ORA-06512: at line 7

View program sources of error stack?

Как-то раз один из авторов книги был свидетелем того, как характерная «оракловая» ошибка вида ORA-123456 выскочила в сообщении приложения банковской информационной системы, когда операционистка оформляла вклад. Девушка сначала некоторое время пыталась понять, что же ей программа хочет сказать на английском языке, потом подозвала более опытного коллегу, который со словами «Это нормально, бывает» закрыл сообщение, и оформление продолжилось.

Это не нормально. Все возможные исключения в программах PL/SQL должны обрабатываться, причем продуманным унифицированным способом. В книге Стивена Фейерштейна «PL/SQL для профессионалов» теме обработки исключений посвящена отдельная глава объемом 34 страницы, что больше, чем написано в этой книге про условные команды и циклы вместе взятые.

Диагностические функции

В PL/SQL имеется несколько диагностических функций для получения информации об исключениях:

SQLCODE – возвращает код ошибки последнего исключения, инициированного в блоке PL/SQL;

SQLERRM – возвращает сообщение об ошибке последнего исключения, инициированного в блоке PL/SQL;

DBMS_UTILITY.FORMAT_ERROR_BACKTRACE – возвращает отформатированную строку с содержимым стека программ и номеров строк кода.

Максимальная длина строки, возвращаемой функцией SQLERRM, составляет 512 байт. Из-за этого ограничения рекомендуется использовать вместо SQLERRM функцию встроенного пакета DBMS_UTILITY.FORMAT_ERROR_STACK, которая выводит строку с отформатированным стеком сообщений. Приведем несколько примеров использования диагностических функций:

SQL> CREATE OR REPLACE PROCEDURE error_proc IS

2 i INTEGER;

3 BEGIN

4 i := 1/0;

5 i := 15;

6 END;

7 /

Procedure created.

SQL> CREATE OR REPLACE PROCEDURE parent_proc IS

2 BEGIN

3 error_proc;

4 END;

5 /

Procedure created.

SQL> BEGIN

2 parent_proc;

3 EXCEPTION

4 WHEN OTHERS THEN

5 DBMS_OUTPUT.PUT_LINE('SQLCODE: '||SQLCODE);

6 DBMS_OUTPUT.PUT_LINE('SQLERRM: '||SQLERRM);

7 DBMS_OUTPUT.PUT_LINE('DBMS_UTILITY.FORMAT_ERROR_BACKTRACE:');

8 DBMS_OUTPUT.PUT_LINE(DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);

9 END;

10 /

SQLCODE: -1476

SQLERRM: ORA-01476: divisor is equal to zero

DBMS_UTILITY.FORMAT_ERROR_BACKTRACE:

ORA-06512: at "U1.ERROR_PROC", line 4

ORA-06512: at "U2.PARENT_PROC", line 3

ORA-06512: at line 2

PL/SQL procedure successfully completed.

По строке, которую вернула FORMAT_ERROR_BACKTRACE, видно, как пролетало системное исключение по строкам кода: сначала она возникла в процедуре error_proc на четвертой строке, управление из error_proc сразу вернулось в родительский для error_proc блок – процедуру parent_proc (на третью строку, где вызывалась error_proc). Далее выводятся сведения о второй строке анонимного блока, в котором вызывалась parent_proc. При этом в стеке появились три ошибки ORA-06512.

Также видно, что функция DBMS_UTILITY.FORMAT_ERROR_BACKTRACE не выдает сообщение о самой исходной ошибке, поэтому совместно с этой функцией следует использовать функции SQLERRM или DBMS_UTILITY.FORMAT_ERROR_STACK.

Пользовательские исключения

Пользовательские исключения объявляются следующим образом:

имя исключения EXCEPTION;

Пользовательские исключения инициируются командой RAISE, у которой есть две формы:

RAISE имя исключения (исключение должно быть предопределенным в пакете STANDARD или объявленным в области видимости);

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

Приведем пример работы с пользовательскими исключениями:

DECLARE

l_amount INTEGER := -100;

l_crncy VARCHAR2(3) := 'RUR';

ex_negative_payment_amount EXCEPTION;

ex_non_rur_payment EXCEPTION;

BEGIN

IF l_amount < 0 THEN

RAISE ex_negative_payment_amount;

END IF;

IF l_crncy <> 'RUR' THEN

RAISE ex_non_rur_payment;

END IF;

… все проверки пройдены, обрабатываем платеж

EXCEPTION

WHEN ex_negative_payment_amount THEN

DBMS_OUTPUT.PUT_LINE('Ошибка: сумма отрицательная: '||l_amount);

WHEN ex_non_rur_payment THEN

DBMS_OUTPUT.PUT_LINE('Ошибка: платеж не в рублях');

END;

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

DECLARE

l_amount INTEGER := -100;

l_crncy VARCHAR2(3) := 'RUR';

ex_negative_payment_amount EXCEPTION;

ex_non_rur_payment EXCEPTION;

BEGIN

IF l_amount < 0 THEN

DBMS_OUTPUT.PUT_LINE('Ошибка: сумма отрицательная: '||l_amount);

 

ELSE

– второй уровень вложенности IF

IF l_crncy <> 'RUR' THEN

DBMS_OUTPUT.PUT_LINE('Ошибка: платеж не в рублях');

ELSE

– потом будет третий уровень вложенности IF

… наконец все проверки пройдены, обрабатываем платеж

END IF;

END IF;

END;

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

Посмотрим на поведение диагностических функций при инициировании пользовательских исключений:

SQL> DECLARE

2 exception1 EXCEPTION;

3 BEGIN

4 RAISE exception1;

5 EXCEPTION

6 WHEN OTHERS THEN

7 DBMS_OUTPUT.PUT_LINE('SQLCODE print: '||SQLCODE);

8 DBMS_OUTPUT.PUT_LINE('SQLERRM print: '||SQLERRM);