Untitled Document

Стивен Фернстайн

член коллегии Oracle ACEOracle ACE

Об исключениях и правилах

( On Exceptions and Rules, by Steven Feuerstein Oracle ACE)

Источник : серия « DEVELOPER: PL/SQL Practices»

Где, когда и как лучше всего обрабатывать исключения

Вопрос: Недавно я узнал, что если исключение возникает в разделе объявлений блока, то раздел обработки исключений этого блока не обрабатывает его. Это кажется неверным. Почему PL / SQL работает именно так, и что это значит для моей практики программирования?

Ответ: Если исключение возникает в разделе объявлений блока, то оно распространяется во внешнюю среду необработанным.

PL / SQL ведет себя таким образом (или, если быть более точным, команда разработчиков PL / SQL решила реализовать обработку исключений именно таким образом) потому, что до тех пор пока локальные переменные и константы полностью не обработаны, программа не является жизнеспособной. Предположим, что исключение, возникшее в разделе объявлений, будет обрабатываться внутри этой подпрограммы. На что можно будет сослаться внутри раздела исключений? Ведь нельзя быть уверенным, что локальные переменные были проинициализированы.

Ключевой вопрос: как это поведение влияет на способ написания кода? Перед тем, как ответить на этот вопрос, давайте посмотрим, когда мы можем столкнуться с этой проблемой.

Исключения в разделе объявлений происходят, когда вы пытаетесь инициализировать переменную, объявленную в этом разделе, таким способом, который вызывает исключение. Наиболее часто возникающим исключением является, конечно, ORA -06502 или VALUE _ ERROR , которое возникает (назовем два сценария) при попытке присвоить символьной переменной значение, которое слишком велико для неё, либо когда вы пытаетесь присвоить числовой переменной нечисловое значение. Например:

DECLARE
   l_name VARCHAR2(5) := 'STEVEN';
   l_age NUMBER := '49 Years Old';
BEGIN

То же самое правило для исключений применяется при инициализации переменных, объявленных в пакете (вне любой подпрограммы). Если исключение возникает при попытке инициализировать переменную уровня пакета, то это исключение будет распространяться необработанным за пределы пакета, даже если раздел инициализации содержит раздел обработки исключений. В такой ситуации машина PL / SQL все же регистрирует пакет как инициализированный и позволяет ссылаться на подпрограммы и переменные пакета.

Чтобы понять это, рассмотрим следующую последовательность шагов и PL / SQL операторов:

  1. Я компилирую пакет valerr , который присваивает слишком длинное значение пакетной символьной переменной. Тело пакета включает раздел обработки исключений (см. листинг 1).
  2. Листинг 1: В пакете присваивается слишком длинное значение пакетной символьной переменной. 
    PACKAGE valerr
    IS
    	FUNCTION little_name RETURN VARCHAR2;
    END valerr;
    
    PACKAGE BODY valerr
    IS
    	g_name	   VARCHAR2 (1)	 :=	 'Liu';
    
    	FUNCTION little_name RETURN VARCHAR2
    	IS
    	BEGIN
    		RETURN g_name;
    	END little_name;
    BEGIN
    	DBMS_OUTPUT.put_line ('Before I show you the name... ');
    EXCEPTION
    	WHEN OTHERS
    	THEN
    		DBMS_OUTPUT.put_line (	'Trapped the error: ' ||	DBMS_UTILITY.format_error_stack ()
    										 );
    		RAISE;
    END valerr;
    
  3. Теперь я пытаюсь запустить функцию valerr . little _ name , исключение не обрабатывается
  4. [у автора: « the exception goes unhandled », по моему, как раз наоборот. А.Бачин ]:

    SQL>	 BEGIN
      2		 DBMS_OUTPUT.put_line 
    ('Name: ' || valerr.little_name);
      3	 END;
      4	 /
    BEGIN
    *
    ERROR at line 1:
    ORA-06502: PL/SQL: numeric or value
    error: character string buffer too small
    ORA-06512: at "HR.VALERR", line 3
    ORA-06512: at line 2
    

    Этого и следовало ожидать.

  5. Но когда я пытаюсь вызвать эту функцию второй раз, исключение не возбуждается:
  6. SQL>	BEGIN
      2		  DBMS_OUTPUT.put_line 
    ('Name: ' || Valerr.little_name);
      3	END;
      4	/
    
    Name:
    PL/SQL procedure successfully completed.
    

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

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

Обратите внимание, что Oracle рассматривает это поведение как баг (номер 5658561). Если вы столкнулись с таким поведением и хотите, чтобы Oracle изменил его, предлагаю вам зайти на Oracle MetaLink и добавить к этому багу свое сообщение о том, как текущее поведение наносит ущерб вашему приложению.

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

Сделать это просто: не присваивайте переменным значения по умолчанию в разделе объявлений. Вместо этого создайте локальную процедуру инициализации и присвойте значения по умолчанию в этой процедуре. Затем вызовите процедуру инициализации в первой строке исполняемого раздела программы, тогда любое исключение можно будет обработать в этой программе. Например, вместо того, чтобы писать процедуру, подобную этой:

PROCEDURE process_data
IS
	l_name   VARCHAR2 (10) := 
		'Steven Feuerstein';
BEGIN
	DBMS_OUTPUT.put_line (l_name);
EXCEPTION
	WHEN OTHERS
	THEN
		DBMS_OUTPUT.put_line
			(  'Trapped the error: '
			 || DBMS_UTILITY.format_
			 error_stack ()
			);
		RAISE;
END process_data;
напишите следующее: 
PROCEDURE process_data
IS
	l_name   VARCHAR2 (10);
	PROCEDURE initialize
	IS
	BEGIN
		l_name := 'Steven Feuerstein';
	END initialize;

BEGIN
	initialize;
	DBMS_OUTPUT.put_line (l_name);
EXCEPTION
	WHEN OTHERS
	THEN
		DBMS_OUTPUT.put_line
			(  'Trapped the error: '
			 || DBMS_UTILITY.format_
			 error_stack ()
			);
    RAISE;
END process_data;

Теперь, когда я запущу исправленную процедуру process_data, ошибка будет перехвачена и обработана, а затем снова возбуждена:

SQL>	 BEGIN
	2		 process_data;
	3	 END;
	4	 /

Trapped the error: ORA-06502:
PL/SQL: numeric or value error:
character string buffer too small
BEGIN
*
ERROR at line 1:
ORA-06502: PL/SQL: numeric or value
error: character string buffer too small
ORA-06512: at "HR.PROCESS_DATA",
line 19
ORA-06512: at line 2

То же самое верно и для пакетов. В исправленной процедуре valerr  в Листинге 2, в разделе инициализации просто вызывается процедура инициализации. 

 Листинг 2: В разделе инициализации вызывается процедура инициализации
  1	 PACKAGE BODY valerr
 2	 IS
 3	 	g_name	VARCHAR2 (1);
 4 
 5	 	FUNCTION little_name
 6	 		RETURN VARCHAR2
 7	 	IS
 8	 	BEGIN
 9	 		RETURN g_name;
10	 	END little_name;
11
12	 	PROCEDURE initialize
13	 	IS
14	 	BEGIN
15	 		g_name := 'Lu';
16	 	END initialize;
17	 BEGIN
18		initialize;
19	 EXCEPTION
20		WHEN OTHERS
21		THEN
22			DBMS_OUTPUT.put_line ( 'Trapped the error: ' || DBMS_UTILITY.format_error_stack ()
23											 );
24         RAISE;
25	 END valerr;

Теперь я должен признаться, что у меня есть два замечания относительно данного мной совета (переместить присваивание значений по умолчанию в отдельную подпрограмму инициализации). Во-первых, этому совету нельзя следовать по отношению к константам. Значения по умолчанию должны быть присвоены им во время объявления. Во-вторых, в исправленном пакете valerr (в Листинге 2) переменная g _ name объявлена в строке 3, а значение ей присваивается только в строке 15. В большинстве обычных пакетов, переменные будут также объявлены в первых строках пакета, а код инициализации будет отстоять от них на сотни, возможно, даже тысячи строк. Лично я не люблю такие расстояния.

Присваивание значений по умолчанию в той же строке, что и объявление переменной, кажется более простым для понимания. Вся информация в одном месте. Должен ли я поступиться этим удобством для чтения, чтобы улучшить обработку ошибок? Это обоснованная уступка, но здесь разумно использовать компромисс.

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

Этот подход улучшит читабельность кода, уменьшив риск возникновения необработанных исключений. Риск будет минимальным, поскольку мы обращаем достаточно внимания на свой код, когда пишем его, чтобы увидеть, что мы присваиваем значение неправильного типа или неверного размера. Например, если все переменные инициализируются литералами, то подпрограмма инициализации не понадобится (см. листинг 3).

 Листинг 3: Переменные инициализируются литеральными значениями. 
PROCEDURE process_data
IS
	l_name				VARCHAR2 (100) := 'Steven Feuerstein';
	l_books_sold		PLS_INTEGER;

	PROCEDURE initialize
	IS
	BEGIN
		l_books_sold := book_counter.in_circulation ('Oracle PL/SQL Programming');
	END initialize;
BEGIN
	initialize;
	DBMS_OUTPUT.put_line (
			 l_name 
	  ||	 ' sold ' 
	  ||	 l_books_sold 
	  ||	 ' books.');
EXCEPTION
	WHEN OTHERS
	THEN
		q$error_manager.raise_unanticipated;
		RAISE;
END process_data;

А чтобы перехватить исключения, связанные с объявлением, можно использовать оператор блока. Оператор блока – это раздел DECLARE - BEGIN - END , который размещается внутри раздела исключений. Поскольку этот блок может иметь свой собственный раздел обработки исключений, можно немедленно перехватить исключение, и либо обработать ошибку, либо зарегистрировать ее, и снова возбудить исключение.

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

Предположим, например, что в процедуре process _ data я не работаю с переменной 1_ books _ sold до строки 245 этой процедуры. Вместо того, чтобы объявлять эту переменную рядом с l _ name , которая используется в процедуре немедленно, я подожду до того момента, когда она потребуется в программе, и использую оператор блока. Тогда я смогу перехватить исключение, которое может возникнуть в разделе объявлений. Листинг 4 содержит исправленную процедуру process _ data , иллюстрирующую использование оператора блока.

Листинг 4: исправленная процедура PROCESS_DATA с оператором блока 
PROCEDURE process_data
IS
	l_name	VARCHAR2 (100) := 'Steven Feuerstein';
BEGIN
	/*
	Немедленное использование l_name
	*/
	IF l_name IS NOT NULL
	THEN
... много кода здесь ... 
	END IF;

	/*

еще больше кода ...

Затем я использую оператор блока, чтобы объявить l _ books _ sold

прямо в той области программы, в которой она требуется.

*/
	<>
	DECLARE
		l_books_sold		PLS_INTEGER := book_counter.in_circulation ('Oracle PL/SQL Programming');

	BEGIN
		IF l_books_sold > 1000
		THEN
			... много кода здесь ...
		END IF;
	EXCEPTION
		WHEN VALUE_ERROR
		THEN
			q$error_manager.raise_unanticipated
				  ('Problem initializing l_books_sold!');
			RAISE;
	END check_books_sold;
	
	... и много кода здесь ...
END process_data;

Один заключительный момент: начиная с Oracle Database 10 g Release 1, компилятор PL / SQL может выдавать рекомендации относительно качества кода. Например, он будет предупреждать нас, что некоторые строки кода в подпрограмме никогда не будут выполнены или являются «недостижимыми» ( PLW -6002). Было бы замечательно, если бы Oracle добавил рекомендацию для кода подобного этому:

DECLARE
	l_name VARCHAR2(5) := 'STEVEN';
	l_age NUMBER := '49 Years Old';
BEGIN

Тогда не придется ждать до запуска программы, чтобы обнаружить эту проблему. Если вы согласны, призываю вас посетить www . ILovePLSQLAnd . net и отправить сообщение Брюну Ллевеллину ( Bryn Llewellyn ), менеджеру по продуктам Oracle PL / SQL , выразив свою заинтересованность в этом улучшении.

Исходный код приложения в обработчике исключения.

Я убедился, что размещать исходный код приложения внутри обработчика исключения – плохой стиль программирования. Мы должны иметь возможность удалить все разделы обработки исключения, и, при отсутствии ошибок, наш код должен работать также. Но бывают ситуации, когда нужно выполнить SELECT INTO (явный однострочный запрос), ожидая, что он не вернет ни одной строки (иначе говоря, это правильный результат). Однако база данных Oracle в этом случае возвращает исключение NO _ DATA _ FOUND , и потом приходиться писать логику приложения в разделе исключений. Что же теперь никогда не использовать явный оператор SELECT INTO в своем коде?!

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

Поэтому давайте согласимся, что размещать код приложения в предложении WHEN можно только для того, чтобы обработать ошибку (обычно такой код включает регистрацию и повторный вызов исключения). Справедливо заметить, что при использовании оператора SELECT INTO в исполняемом разделе, реализация этого подхода является загадкой. Значит ли это, что никогда нельзя писать SELECT INTO в своих программах? Давайте рассмотрим этот вопрос.

Явный оператор SELECT INTO вызывает NO _ DATA _ FOUND , если ни одна строка не выбрана и TOO _ MANY _ ROWS , если найдено более одной строки. Эти два исключения нуждаются в различной обработке. Ллевеллин предлагает разделить все исключения на три группы:

  • намеренные,
  • неуместные и
  • неожиданные.

Намеренными являются исключения, которые намеренно возбуждаются в программе, как часть нормального поведения. Прекрасным примером программы, которая вызывает намеренное исключение, является UTL _ FILE . GET _ LINE , которая вызывает NO _ DATA _ FOUND , когда чтение достигло конца файла.

Неуместными называются исключения, которые, возникая, не являются ошибкой в логике приложения. Это могут быть, например, разные условия, связанные с данными. Исключение NO _ DATA _ FOUND , вызванное SELECT INTO , является неуместным.

Возникшие «тяжелые ошибки», которых вы не ожидали и которые могут указывать на серьёзную проблему в приложении, являются неожиданными . TOO _ MANY _ ROWS – это классическая неожиданная ошибка; она означает, что для первичного или уникального ключа существует дублирующее значение.

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

A. Намеренные. Измените свою программу так, чтобы избежать размещения логики приложения в разделе исключений. Например, один из способов применить это правило к UTL _ FILE . GET _ LINE показан в процедуре process _ file на Листинге 5, которая читает содержимое файла и затем обрабатывает каждую прочитанную строку. Обратите внимание на цикл в строках с 16 по 18: этот цикл выглядит как бесконечный (он не содержит оператора EXIT ), но на самом деле он останавливается, когда UTL _ FILE вызывает NO _ DATA _ FOUND .

Листинг 5: Процедура PROCESS_FILE вызывает UTL_FILE.GET_LINE напрямую. 
 1	 PROCEDURE process_file (dir_in IN VARCHAR2, file_in IN VARCHAR2)
 2	 IS
 3	 	TYPE line_t IS TABLE OF VARCHAR2 (32767)
 4	 		INDEX BY PLS_INTEGER;
 5
 6	 	l_file		    UTL_FILE.file_type;
 7	 	l_lines		line_t;
 8	 BEGIN
 9	 	l_file :=
10	 		UTL_FILE.fopen	(LOCATION 	=> dir_in
11			   , filename				=> file_in
12			   , open_mode			=> 'R'
13			   , max_linesize			=> 32767
14					     			    ) ;
15
16	 	LOOP
17		 UTL_FILE.get_line (l_file, l_lines (l_lines.COUNT + 1));
18		END LOOP;
19	 EXCEPTION
20	 	WHEN NO_DATA_FOUND
21	 	THEN
22	 		/* Обработка каждой строки */
23			FOR indx IN 1 .. l_lines.COUNT
24			LOOP
25				do_stuff_with_line (l_lines (indx));
26			END LOOP;
27
28			UTL_FILE.fclose (l_file);
29  END process_file;

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

Никогда не вызывать UTL _ FILE . GET _ LINE напрямую! Листинг 6 показывает, как переписать процедуру, чтобы решить эту проблему. Я создаю локальный модуль get _ next _ line , который вызывает UTL _ FILE . GET _ LINE . Он перехватывает NO _ DATA _ FOUND и возвращает TRUE в выходном ( OUT ) булевском аргументе, если достигнут конец файла.

Листинг 6: исправленная процедура PROCESS_FILE вызывает локальный модуль 
 1	 PROCEDURE process_file (dir_in IN VARCHAR2, file_in IN VARCHAR2)
 2	 IS
 3	 	TYPE line_t IS TABLE OF VARCHAR2 (32767)
 4	 		INDEX BY PLS_INTEGER;
 5
 6	 	l_file			UTL_FILE.file_type;
 7	 	l_lines		line_t;
 8	 	l_eof			BOOLEAN				:= FALSE;
 9
10	 	PROCEDURE get_next_line (line_out OUT VARCHAR2, eof_out OUT BOOLEAN)
11	 	IS
12	 	BEGIN
13	 		UTL_FILE.get_line (l_file, line_out);
14	 		eof_out := FALSE;
15	 	EXCEPTION
16	 		WHEN NO_DATA_FOUND
17	 		THEN
18	 			line_out := NULL;
19	 			eof_out  := TRUE;
20	 	END get_next_line;
21	 BEGIN
22	 	l_file :=
23	 		UTL_FILE.fopen (LOCATION			=> dir_in
24	 			 , filename				=> file_in
25	 			 , open_mode			=> 'R'
26	 			 , max_linesize		=> 32767
27	 								);
28
29	 	WHILE (NOT l_eof)
30	 	LOOP
31	 		get_next_line (l_lines (l_lines.COUNT + 1), l_eof);
32	 		EXIT WHEN l_eof;
33	 	END LOOP;
34
35	 	/* Обработка каждой строки */
36	 	FOR indx IN 1 .. l_lines.COUNT
37	 	LOOP
38	 		do_stuff_with_line (l_lines (indx));
39	 	END LOOP;
40
41	 	UTL_FILE.fclose (l_file);
42 END process_file;

Затем я пишу цикл WHILE , который показывает, при каких обстоятельствах цикл будет завершаться. Сразу после цикла я завершаю прочую логику приложения и закрываю файл.

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

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

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

C. Неуместные . Теперь давайте обсудим, что делать с неуместными исключениями, такими как NO _ DATA _ FOUND . Как и в случае с намеренными исключениями, основное правило – избегать размещения логики приложения в разделе исключений. Реализовать это при работе с неуместными исключениями можно, дав программисту возможность выбрать: должно ли исключение возбуждаться или нет.

Чтобы продемонстрировать этот подход в случае с NO _ DATA _ FOUND , допустим, что Сэм написал программу, которая возвращает ID для отдела с заданным именем:

FUNCTION id_for_name (
	department_name_in IN departments
		.department_name%TYPE
)
	RETURN departments.department_id%TYPE
IS
	l_return departments
    .department_id%TYPE;
BEGIN
	SELECT department_id
		INTO l_return
		FROM departments
	WHERE department_name =
		department_name_in;
	RETURN l_return;
END id_for_name;

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

PROCEDURE load_from_staging_table
IS
	l_id departments.department_id%TYPE;
BEGIN
	FOR dept_rec IN (SELECT *
		FROM dept_staging_table)
	LOOP
		BEGIN
			l_id := id_for_name
				(dept_rec.department_name);
			submit_update_request (dept_rec);
		EXCEPTION
			WHEN NO_DATA_FOUND
			THEN
				submit_add_request (dept_rec);
		END;
	END LOOP;
END load_from_staging_table;

Если название отдела не найдено в таблице, функция id _ for _ name вызывает NO _ DATA _ FOUND . Поэтому Сандра создает анонимный блок внутри цикла, перехватывая исключение, помещая логику «запроса на добавление нового отдела» ( submit _ add _ request ) в разделе исключений, и продолжает работать.

Однако это именно то, чего мы хотели избежать: логика приложения в разделе обработки исключений. И вновь, что делать программисту?

Чтобы устранить этот недостаток, перепишите функцию id _ for _ name и, заодно, все однострочные запросы и функции поиска (смотри листинг 7). Этот подход имеет несколько основных черт. Первое , новый параметр propagate _ if _ ndf _ in указывает, должно ли исключение NO _ DATA _ FOUND (когда оно вызвано оператором SELECT INTO ) распространяться за пределы функции.

Листинг 7: Исправленная функция ID_FOR_NAME  
 1	 FUNCTION id_for_name (
 2	 	department_name_in	IN		departments.department_name%TYPE
 3	 ,	propagate_if_ndf_in	IN		BOOLEAN := FALSE
 4	 ,	ndf_value_in				IN		departments.department_id%TYPE := NULL
 5	 )
 6	 	RETURN departments.department_id%TYPE
 7	 IS
 8	 	l_return	departments.department_id%TYPE;
 9	 BEGIN
10	 	SELECT department_id
11	 	   INTO l_return
12	 	  FROM departments
13	 	WHERE department_name = department_name_in;
14
15	 	RETURN l_return;
16	 EXCEPTION
17	 	WHEN NO_DATA_FOUND
18	 	THEN
19	 		IF propagate_if_ndf_in
20	 		THEN
21	 			RAISE;
22	 		ELSE
23	 			RETURN ndf_value_in;
24	 		END IF;
25	 	WHEN TOO_MANY_ROWS
26	 	THEN
27	 		q$error_manager.raise_unanticipated
28	 		    (text_in   =>  'Multiple rows found for department name'
29	 		,    name1_in  =>  'DEPARTMENT_NAME'
30	 		,    value1_in =>  department_name_in
31	 	 	);
32  END id_for_name;

Второе , новый параметр ndf _ value _ in предоставляет значение, которое будет использоваться, чтобы указать, что данные не найдены, если исключение не распространяется. Возможно, вы захотите просто возвращать NULL , чтобы указать "данные не найдены" (" no data found "), но это значение (или, точнее, отсутствие значения) иногда может быть допустимым значением столбца. Зачем же жестко кодировать его?

Третье , если исключение NO _ DATA _ FOUND возникло, то оно распространяется за пределы функции путем повторного вызова ( RAISE ; в строке 21), только если пользователь запросил такое поведение. Иначе функция возвращает значение индикатора "данные не найдены" (" no data found ").

И последнее, если возникло TOO _ MANY _ ROWS , утилита управления ошибками регистрирует ошибку, включая ID отдела, который вызвал проблему и распространяет исключение за пределы функции необработанным.

Теперь, используя новую версию функции id _ for _ name , Сандра может переписать свою программу загрузки (смотри Листинг 8). Она решила использовать –1 для указания, что отдел не найден. Она также «спрятала» -1 в константу, чтобы код был более понятным. Вся логика приложения размещается в исполняемом разделе, а код стал более ясным и простым для понимания и управления.

Листинг 8: Вызов измененной функции ID_FOR_NAME 
PROCEDURE load_from_staging_table
IS
	c_no_such_dept	CONSTANT PLS_INTEGER := -1;
	l_id departments.department_id%TYPE;
BEGIN
	FOR dept_rec IN (SELECT * FROM dept_staging_table)
	LOOP
		BEGIN
			l_id :=
				id_for_name (dept_rec.department_name
								 , raise_if_ndf_in => FALSE
								 , ndf_value_in	=> c_no_such_dept
								 );

			IF l_id = c_no_such_dept
			THEN
				submit_update_request (dept_rec);
			ELSE
				submit_add_request (dept_rec);
			END IF;
		END;
	END LOOP;
END load_from_staging_table;

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

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

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

Стивен Ферстайн ( steven.feuerstein@quest.com) – проповедник ( evangelist – евангелист) PL / SQL из Quest Software . Он опубликовал 10 книг по этому языку программирования Oracle , включая «Программирование на Oracle PL / SQL » ( Oracle PL / SQL Programming ) и «Наилучшая практика Oracle PL / SQL » ( Oracle PL / SQL Best Practices ) (издательство O ' Reilly ). В настоящее время миссия Ферстайна в жизни, как он сам ее определяет, - улучшать качество и количество тестирования кода PL / SQL .

E-mail this page