ГЛАВА 5


ОБРАБОТКА ОШИБОК

Нет ничего приятнее, чем когда в тебя промахнутся.
Уинстон Черчилль

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

В большинстве языков программирования ошибка времени выполнения, такая как ПЕРЕПОЛНЕНИЕ СТЕКА или ДЕЛЕНИЕ НА 0, если вы не предусмотрели ее обработку, прекращает нормальную работу и возвращает управление в операционную систему. В PL/SQL есть специальный механизм, называемый ОБРАБОТКОЙ ИСКЛЮЧЕНИЙ, который позволяет вам "защитить" вашу программу так, что она сможет продолжать выполнение даже при наличии ошибок.

Общие сведения

В PL/SQL условие ошибки называется ИСКЛЮЧЕНИЕМ. Исключения могут быть внутренне определены (исполнителем PL/SQL) или определены пользователем.

Примеры внутренне определенных исключений включают "ДЕЛЕНИЕ НА 0" и "НЕХВАТКУ ПАМЯТИ". Некоторые общие внутренне определенные исключения имеют предопределенные имена, такие как ZERO_DIVIDE и STORAGE_ERROR. Другим внутренне определенным исключениям имена могут быть присвоены.

Вы можете определить ваши собственные исключения в декларативной части либого блока, подпрограммы или пакета PL/SQL. Например, вы можете определить исключение с именем insufficient_funds для обработки банковских счетов, превысивших кредит. В отличие от внутренних исключений, пользовательские исключения ДОЛЖНЫ иметь имена.

Когда возникает ошибка, соответствующее исключение ВОЗБУЖДАЕТСЯ. Это значит, что нормальное выполнение останавливается, и управление передается на часть обработки исключений вашего блока или подпрограммы PL/SQL. Внутренние исключения возбуждаются неявно (автоматически) системой исполнения; пользовательские исключения возбуждаются явно, посредством предложений RAISE, которые могут также возбуждать предопределенные исключения.

Для обработки возбуждаемых исключений вы пишете отдельные программы, называемые ОБРАБОТЧИКАМИ ИСКЛЮЧЕНИЙ. После выполнения обработчика исключений исполнение текущего блока заканчивается, и окружающий блок продолжает свое выполнение со следующего предложения. Если окружающего блока нет (т.е. текущий блок не вложен в другой блок), то управление возвращается в хост-окружение.

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

        DECLARE
            pe_ratio  NUMBER(3,1);
        BEGIN
            ...
            SELECT price / earnings INTO pe_ratio FROM stocks
                WHERE symbol = 'XYZ';
                -- может вызвать ошибку "деление на 0"
            INSERT INTO stats (symbol, ratio) VALUES ('XYZ', pe_ratio);
            COMMIT;
        EXCEPTION  -- здесь начинаются обработчики исключений
            WHEN ZERO_DIVIDE THEN  -- обрабатывает "деление на 0"
                INSERT INTO stats (symbol, ratio) VALUES ('XYZ', NULL);
                COMMIT;
            ...
            WHEN OTHERS THEN  -- обрабатывает все прочие ошибки
                ROLLBACK;
        END;  -- здесь заканчиваются обработчики исключений и весь блок
Необязательный обработчик OTHERS перехватывает все исключения, персонально не перечисленные в блоке.

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

        INSERT INTO stats (symbol, ratio)
            SELECT symbol, DECODE(earnings, 0, NULL, price / earnings)
            FROM stocks
            WHERE symbol = 'XYZ';
При этом способе используется подзапрос, поставляющий значения предложению INSERT. Если доходы нулевые, функция DECODE возвратит пустое значение. В противном случае DECODE возвращает отношение цены к доходам.

Преимущества исключений

Использование исключений для обработки ошибок имеет несколько преимуществ. Не имея таких средств, вы должны были бы проверять на ошибки выполнения при каждом выполнении команды, как показывает следующий пример:
        BEGIN
            SELECT ...
                -- проверить на ошибку 'no data found'
            SELECT ...
                -- проверить на ошибку 'no data found'
            SELECT ...
                -- проверить на ошибку 'no data found'
        END;
Обработка ошибок при этом не является ни четко отделенной от нормальной обработки, ни концептуально осмысленной. Если вы пренебрежете проверкой в одном месте, ошибка пройдет необнаруженной и может вызвать другие, внешне не связанные ошибки.

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

        BEGIN
            SELECT ...
            SELECT ...
            SELECT ...
        EXCEPTION
            WHEN NO_DATA_FOUND THEN  -- этот обработчик перехватывает
                                     -- все ошибки 'no data found'
        END;
Заметьте, как исключения улучшают читабельность, позволяя вам изолировать программы обработки ошибок. Основной алгоритм не затемняется алгоритмами восстановления от ошибок.

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

Предопределенные исключения

Внутреннее исключение возбуждается неявно всякий раз, когда ваша программа PL/SQL нарушает правило ORACLE или превышает установленный системой лимит. Каждая ошибка ORACLE имеет номер, однако исключения должны обрабатываться по их именам. Поэтому PL/SQL внутренне определяет некоторые распространенные ошибки ORACLE как исключения. Например, предопределенное исключение NO_DATA_FOUND возбуждается, когда предложение SELECT INTO не возвращает ни одной строки.

Для обработки других ошибок ORACLE вы можете использовать общий обработчик OTHERS. Функции сообщений об ошибках SQLCODE и SQLERRM особенно полезны в обработчике OTHERS, так как они возвращают код ошибки ORACLE и текст сообщения об ошибке. Альтернативно, вы можете использовать прагму EXCEPTION_INIT, чтобы ассоциировать имена исключений с номерами ошибок ORACLE. Подробнее об этом см. в разделе раздел "Использование EXCEPTION_INIT" ниже в этой главе.

PL/SQL объявляет предопределенные исключения глобально, в пакете STANDARD, который определяет окружение PL/SQL. Поэтому вы не обязаны объявлять их сами. Вы можете писать обработчики для предопределенных исключений, используя имена, приведенные в следующей таблице. Эта таблица показывает также коды ошибок ORACLE и значения возврата функции SQLCODE.
Имя исключения Ошибка ORACLE Код SQLCODE
CURSOR_ALREADY_OPEN ORA-06511 -6511
DUP_VAL_ON_INDEX ORA-00001 -1
INVALID_CURSOR ORA-01001 -1001
INVALID_NUMBER ORA-01722 -1722
LOGIN_DENIED ORA-01017 -1017
NO_DATA_FOUND ORA-01403 +100
NOT_LOGGED_ON ORA-01012 -1012
PROGRAM_ERROR ORA-06501 -6501
STORAGE_ERROR ORA-06500 -6500
TIMEOUT_ON_RESOURCE ORA-00051 -51
TOO_MANY_ROWS ORA-01422 -1422
TRANSACTION_BACKED_OUT ORA-00061 -61
VALUE_ERROR ORA-06502 -6502
ZERO_DIVIDE ORA-01476 -1476
Ниже приведены краткие описания предопределенных исключений:

CURSOR_ALREADY_OPEN

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

Курсорный цикл FOR автоматически открывает свой курсор. Поэтому вы не можете войти в такой цикл, если данный курсор уже открыт. Нельзя также явно открывать курсор внутри цикла FOR.

DUP_VAL_ON_INDEX

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

INVALID_CURSOR

Возбуждается, когда вызов PL/SQL специфицирует некорректный курсор (например, при попытке закрыть неоткрытый курсор).

INVALID_NUMBER

Возбуждается в предложении SQL, когда преобразование символьной строки в число сбивается из-за того, что строка не содержит правильного представления числа. Например, следующее предложение INSERT возбудит исключение INVALID_NUMBER, когда ORACLE попытается преобразовать 'HALL' в число:

        INSERT INTO emp (empno,ename,deptno) VALUES ('HALL', 7888, 20);
В процедурных предложениях вместо этого исключения возбуждается VALUE_ERROR.

LOGIN_DENIED

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

NO_DATA_FOUND

Возбуждается, когда предложение SELECT INTO не возвращает ни одной строки, или при обращении к неинициализированной строке таблицы PL/SQL.

Групповые функции SQL, такие как AVG или SUM, ВСЕГДА возвращают значение, даже если это значение есть NULL. Поэтому предложение SELECT INTO, вызывающее групповую функцию, никогда не возбудит исключение NO_DATA_FOUND.

Поскольку NO_DATA_FOUND возбуждается, когда предложение SELECT INTO не возвращает строк, вы можете проверять значение SQL%NOTFOUND только в обработчике исключений. Однако, значение атрибута имя_курсора%NOTFOUND можно проверять после каждой операции FETCH. От операции FETCH ожидается, что в конце концов она не сможет возвратить очередную строку, так что, когда это происходит, никакого исключения не возбуждается.

NOT_LOGGED_ON

Возбуждается, когда ваша программа PL/SQL выдает вызов ORACLE, не будучи подключена к ORACLE.

PROGRAM_ERROR

Возбуждается, когда PL/SQL встретился с внутренней проблемой.

STORAGE_ERROR

Возбуждается, когда PL/SQL исчерпал доступную память, или когда память запорчена.

TIMEOUT_ON_RESOURCE

Возбуждается при возникновении таймаута, когда ORACLE ожидает ресурса.

TOO_MANY_ROWS

Возбуждается, когда предложение SELECT INTO возвращает больше одной строки.

TRANSACTION_BACKED_OUT

Обычно возбуждается, если удаленная часть транзакции была подвергнута неявному или явному откату. Причиной может быть несогласованность данных ORACLE в каких-нибудь узлах. В таких случаях выдайте ROLLBACK, а затем повторите транзакцию. Смотрите раздел "Повторение транзакции" ниже в этой главе.

VALUE_ERROR

Возбуждается при возникновении арифметической ошибки, ошибки преобразования, ошибки усечения или ошибки ограничения. Например, VALUE_ERROR возбуждается при усечении строкового значения, присваиваемого переменной PL/SQL. (Однако при усечении строкового значения, присваиваемого хост-переменной, никакого исключения не возбуждается.)

В процедурных предложениях VALUE_ERROR возбуждается при ошибке преобразования символьной строки в число. Например, следующее предложение присваивания вызовет RAISE_ERROR, когда PL/SQL попытается преобразовать 'HALL' в числовое значение:

        DECLARE
            my_empno  NUMBER(4);
            my_ename  CHAR(10);
        BEGIN
            my_empno := 'HALL';
            ...
В предложениях SQL в таких случаях возбуждается INVALID_NUMBER.

ZERO_DIVIDE

Возбуждается при попытке деления числа на 0.

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

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

Объявление исключений

Вы можете объявлять исключения только в декларативной части блока, подпрограммы или пакета PL/SQL. Вы объявляете исключение, вводя его имя, за которым следует ключевое слово EXCEPTION. В следующем примере объявляется исключение с именем past_due:

        DECLARE
            past_due  EXCEPTION;
            acct_num  NUMBER(5);
        BEGIN
            ...
Объявления исключений выглядят так же, как и объявления переменных. Однако не забывайте, что исключение - это условие ошибки, но не объект. В отличие от переменных, исключениям нельзя присваивать значений, и они не могут использоваться в предложениях SQL. Однако к исключениям применимы те же правила сферы, что и к переменным.

Правила сферы

Нельзя объявлять исключение дважды в одном и том же блоке. Однако вы можете объявить то же самое исключение в двух различных блоках.

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

Вы можете переобъявить глобальное исключение в подблоке. В таком случае локальное объявление имеет преимущество, и подблок не может обращаться к глобальному исключению (если только оно не было объявлено в помеченном блоке; в этом случае можно использовать синтаксис метка_блока.имя_исключения).

Следующий пример иллюстрирует правила сферы для исключений:

        DECLARE
            past_due  EXCEPTION;
            acct_num  NUMBER;
        BEGIN
            ...
            ---------------- начало подблока -------------------------
            DECLARE
                past_due  EXCEPTION;  -- имеет преимущество в подблоке
                acct_num  NUMBER;
            BEGIN
                ...
                IF ... THEN
                    RAISE past_due;  -- это не обрабатывается
                END IF;
                ...
            END;
            ---------------- конец подблока --------------------------
            ...
        EXCEPTION
            WHEN past_due THEN  -- это не относится к исключению,
                ...             -- возбуждаемому в подблоке
        END;
Здесь окружающий блок не обрабатывает исключение, возбуждаемое в подблоке, потому что объявление past_due в подблоке имеет преимущество. Два исключения past_due, хотя их имена одинаковы, являются разными, точно так же, как различны две объявленные в этом примере переменные acct_num. Поэтому предложение RAISE в подблоке и фраза WHEN в окружающем блоке относятся к РАЗНЫМ исключениям.

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

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

Для обработки непоименованных внутренних исключений вы должны использовать обработчик OTHER либо прагму EXCEPTION_INIT. ПРАГМА - это директива (указание) компилятору. Прагмы (называемые также ПСЕВДОИНСТРУКЦИЯМИ) обрабатываются во время компиляции, а не во время выполнения. Они не изменяют смысла программы, а лишь поставляют информацию компилятору. Например, в языке Ada следующая предопределенная прагма указывает компилятору, что необходимо оптимизировать использование памяти:

        pragma OPTIMIZE(space);
В PL/SQL, предопределенная прагма EXCEPTION_INIT сообщает компилятору имя исключения, которое вы ассоциируете с конкретным кодом ошибки ORACLE. Это позволяет вам обращаться к любому внутреннему исключению по имени, написав для него специальный обработчик.

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

        PRAGMA EXCEPTION_INIT(имя_исключения, код_ошибки_ORACLE);
Здесь имя_исключения - это имя исключения, ранее уже объявленного в этом блоке. Прагма должна появиться в той же декларативной части, что и соответствующее исключение, как показано в следующем примере:
        DECLARE
            insufficient_privileges  EXCEPTION;
            PRAGMA EXCEPTION_INIT(insufficient_privileges, -1031);
                --------------------------------------------------------
                -- ORACLE возвращает код ошибки -1031, если, например,
                -- вы пытаетесь обновить таблицу, для которой имеете
                -- лишь полномочия SELECT.
                --------------------------------------------------------
        BEGIN
            ...
        EXCEPTION
            WHEN insufficient_privileges THEN
                -- обработать ошибку
            ...
        END;

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

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

Вызывающее приложение получает при этом исключение PL/SQL, которое оно может обработать с помощью функций сообщений об ошибках SQLCODE и SQLERRM. Более того, оно может использовать прагму EXCEPTION_INIT, чтобы сопоставить специфические номера ошибок, которые возвращает raise_application_error, своим собственным исключениям. Для более подробной информации о процедуре raise_application_error обратитесь к разделу "Пакеты, специфичные для продуктов" в главе 7.

Переобъявление предопределенных исключений

Вспомним, что PL/SQL объявляет свои предопределенные исключения глобально, в пакете STANDARD, так что вам не требуется объявлять их самим. Однако переобъявление предопределенного исключения не приведет к ошибке, так как ваше локальное объявление перекрывает глобальное внутреннее объявление. Например, если вы объявите исключение с именем invalid_number, а затем PL/SQL внутренне возбудит предопределенное исключение INVALID_NUMBER, то обработчик, написанный вами для вашего invalid_number, не увидит внутреннего исключения. В таком случае вы должны обращаться к предопределенному исключению с помощью квалифицированной ссылки, как показывает следующий пример:

            ...
        EXCEPTION
            WHEN invalid_number OR STANDARD.INVALID_NUMBER THEN
                -- обработать ошибку
            ...
        END;

Как возбуждаются исключения

Внутренние исключения возбуждаются неявно исполнительной системой, как и те пользовательские исключения, которые вы ассоциировали с кодами ошибок ORACLE с помощью прагмы EXCEPTION_INIT. Однако остальные пользовательские исключения должны возбуждаться явно, посредством предложений RAISE.

Использование предложения RAISE

Блоки и подпрограммы PL/SQL должны явно возбуждать исключение лишь в том случае, когда ошибка делает невозможным или нежелательным продолжение обработки. Вы можете закодировать предложение RAISE для данного исключения в любом месте сферы этого исключения. В следующем примере вы возбуждаете в вашем блоке PL/SQL пользовательское исключение с именем out_of_stock:

        DECLARE
            out_of_stock    EXCEPTION;
            number_on_hand  NUMBER(4);
        BEGIN
            ...
            IF number_on_hand < 1 THEN
                RAISE out_of_stock;
            END IF;
            ...
        EXCEPTION
            WHEN out_of_stock THEN
                -- обработать ошибку
        END;
Вы можете также явно возбуждать предопределенное исключение, как показывает следующий пример:
        RAISE INVALID_NUMBER;
Это позволяет вам использовать обработчики, созданные для предопределенных исключений, для обработки других ошибок, как показывает следующий пример:
        DECLARE
            acct_type  NUMBER;
            ...
        BEGIN
            ...
            IF acct_type NOT IN (1, 2, 3) THEN
                RAISE INVALID_NUMBER;
            END IF;
            ...
        EXCEPTION
            WHEN INVALID_NUMBER THEN
                ROLLBACK;
            ...
        END;

Как распространяются исключения

Что происходит, если в текущем блоке нет обработчика исключений для возбужденного исключения? В этом случае блок завершается, а исключение ПРОДВИГАЕТСЯ в окружающий блок. Теперь окружающий блок становится текущим, а исключение воспроизводит себя в нем. Если обработчик исключения не находится и в этом блоке, процесс поиска повторяется. Если текущий блок не имеет окружающего блока, PL/SQL возвращает ошибку "необрабатываемое исключение" в хост-окружение.

Рисунки 5-1, 5-2 и 5-3 иллюстрируют правила распространения исключений.

Рис.5-1
Правила распространения: Пример 1

           ╡°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°≈
           ▀ BEGIN                           ▀
           ▀     ...                         ▀
           ▀    ╡°°°°°°°°°°°°°°°°°°°°°°°°°°≈ ▀
           ▀    ▀ BEGIN                    ▀ ▀
           ▀    ▀     IF X = 1 THEN        ▀ ▀
           ▀    ▀         RAISE A; °°°°°≈  ▀ ▀
           ▀    ▀     ELSIF X = 2 THEN  ▀  ▀ ▀
           ▀    ▀         RAISE B;      ▀  ▀ ▀
           ▀    ▀     ELSE              ▀  ▀ ▀
           ▀    ▀         RAISE C;      ▀  ▀ ▀
           ▀    ▀     END IF;           ▀  ▀ ▀
           ▀    ▀     ...               ▀  ▀ ▀
           ▀    ▀                       ▀ ╡≥°≥°°°°°°°°°°°°°°°°°°°°°°°°°≈
           ▀    ▀ EXCEPTION             ▀ ▀ Исключение A обрабатывается▀
           ▀    ▀     WHEN A THEN   °°°╠ ▀ локально, затем выполнение ▀
           ▀    ▀     ...            °°°≈ ▀ продолжается в окружающем  ▀
           ▀    ▀ END;                  ▀ ▀ блоке                      ▀
           ▀    ▀                       ▀ ≤ ° °°°°°°°°°°°°°°°°°°°°°°°°°╠
           ▀    ▀                       ▀  ▀ ▀
           ▀    ≤°°°°°°°°°°°°°°°°°°°°°°°²°°╠ ▀
           ▀     ...              °°°°°╠    ▀
           ▀ EXCEPTION                       ▀
           ▀     WHEN B THEN                 ▀
           ▀     ...                         ▀
           ▀ END;                            ▀
           ≤°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°╠

Рис.5-2
Правила распространения: Пример 2

           ╡°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°≈
           ▀ BEGIN                           ▀
           ▀     ...                         ▀
           ▀    ╡°°°°°°°°°°°°°°°°°°°°°°°°°°≈ ▀
           ▀    ▀ BEGIN                    ▀ ▀
           ▀    ▀     IF X = 1 THEN        ▀ ▀
           ▀    ▀         RAISE A;         ▀ ▀
           ▀    ▀     ELSIF X = 2 THEN     ▀ ▀
           ▀    ▀         RAISE B; °°°°°≈  ▀ ▀
           ▀    ▀     ELSE              ▀  ▀ ▀
           ▀    ▀         RAISE C;      ▀  ▀ ▀
           ▀    ▀     END IF;           ▀  ▀ ▀
           ▀    ▀     ...               ▀  ▀ ▀
           ▀    ▀                       ▀ ╡≥°≥°°°°°°°°°°°°°°°°°°°°°°°°°≈
           ▀    ▀ EXCEPTION             ▀ ▀ Исключение B продвигается  ▀
           ▀    ▀     WHEN A THEN   °°°╠ ▀ до первого окружающего     ▀
           ▀    ▀     ...            °°°≈ ▀ блока, имеющего подходящий ▀
           ▀    ▀ END;                  ▀ ▀ обработчик                 ▀
           ▀    ▀                       ▀ ≤ ° °°°°°°°°°°°°°°°°°°°°°°°°°╠
           ▀    ≤°°°°°°°°°°°°°°°°°°°°°°°²°°╠ ▀
           ▀     ...                    ▀ ╡°°≥°°°°°°°°°°°°°°°°°°°°°°°°°≈
           ▀ EXCEPTION                  ▀ ▀ Исключение B обрабатывается▀
           ▀     WHEN B THEN      °°°°°╠ ▀ затем управление передается▀
           ▀     ...              °°°°°°≈ ▀ назад в хост-окружение     ▀
           ▀ END;                       ▀ ≤°° °°°°°°°°°°°°°°°°°°°°°°°°°╠
           ≤°°°°°°°°°°°°°°°°°°°°°°°°°°°°²°°°°╠
                                  °°°°°╠

Рис.5-3
Правила распространения: Пример 3

           ╡°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°≈
           ▀ BEGIN                           ▀
           ▀     ...                         ▀
           ▀    ╡°°°°°°°°°°°°°°°°°°°°°°°°°°≈ ▀
           ▀    ▀ BEGIN                    ▀ ▀
           ▀    ▀     IF X = 1 THEN        ▀ ▀
           ▀    ▀         RAISE A;         ▀ ▀
           ▀    ▀     ELSIF X = 2 THEN     ▀ ▀
           ▀    ▀         RAISE B;         ▀ ▀
           ▀    ▀     ELSE                 ▀ ▀
           ▀    ▀         RAISE C; °°°°°≈  ▀ ▀
           ▀    ▀     END IF;           ▀  ▀ ▀
           ▀    ▀     ...               ▀  ▀ ▀
           ▀    ▀                       ▀  ▀ ▀
           ▀    ▀ EXCEPTION             ▀  ▀ ▀
           ▀    ▀     WHEN A THEN   °°°╠  ▀ ▀
           ▀    ▀     ...            °°°≈  ▀ ▀
           ▀    ▀ END;                  ▀  ▀ ▀
           ▀    ≤°°°°°°°°°°°°°°°°°°°°°°°²°°╠ ▀
           ▀     ...                    ▀ ╡°°≥°°°°°°°°°°°°°°°°°°°°°°°°°≈
           ▀ EXCEPTION                  ▀ ▀ Исключение C не имеет      ▀
           ▀     WHEN B THEN       °°°°╠ ▀ обработчика, поэтому ошибка▀
           ▀     ...               °°°°°≈ ▀ возвращается в хост-среду  ▀
           ▀ END;                       ▀ ≤°° °°°°°°°°°°°°°°°°°°°°°°°°°╠
           ≤°°°°°°°°°°°°°°°°°°°°°°°°°°°°²°°°°╠
                                   °°°°╠

Исключение может распространяться за границы своей сферы, то есть за границы блока, в котором оно было объявлено. Рассмотрим следующий пример:
        DECLARE
            ...
        BEGIN
            ...
            ---------------- начало подблока -------------------------
            DECLARE
                past_due  EXCEPTION;
            BEGIN
                ...
                IF ... THEN
                    RAISE past_due;
                END IF;
                ...
            END;
            ---------------- конец подблока --------------------------
            ...
        EXCEPTION
            ...
            WHEN OTHERS THEN
                ROLLBACK;
        END;
Поскольку блок, в котором объявлено исключение past_due, не имеет обработчика для этого исключения, оно продвигается в окружающий блок. Однако, согласно правилам сферы, окружающие блоки не могут обращаться к исключениям, объявленным в подблоке. Поэтому в окружающем блоке только обработчик OTHERS может перехватить данное исключение.

Повторное возбуждение исключения

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

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

        DECLARE
            out_of_balance  EXCEPTION;
        BEGIN
            ...
            ---------------- начало подблока -------------------------
            BEGIN
                ...
                IF ... THEN
                    RAISE out_of_balance;  -- возбудить исключение
                END IF;
                ...
            EXCEPTION
                WHEN out_of_balance THEN
                    -- обработать ошибку
                    RAISE;  -- повторно возбудить текущее исключение
                ...
            END;
            ---------------- конец подблока --------------------------
        EXCEPTION
            WHEN out_of_balance THEN
                -- обработать ошибку иным способом
            ...
        END;
Если опустить имя исключения в предложении RAISE (что допускается только в обработчике исключений), то подразумевается текущее исключение.

Обработка возбуждаемых исключений

Когда возбуждается исключение, нормальное исполнение вашего блока PL/SQL или подпрограммы останавливается, и управление передается на обработчик исключений этого блока или подпрограммы, что оформляется следующим образом:
        ...
        EXCEPTION
            WHEN имя_исключения1 THEN  -- обработчик
                ряд_предложений1
            WHEN имя_исключения2 THEN  -- другой обработчик
                ряд_предложений2
            ...
            WHEN OTHERS THEN           -- необязательный обработчик
                ряд_предложений3
        END;
Чтобы перехватывать возбуждаемые исключения, вы должны написать обработчики исключений. Каждый обработчик состоит, во-первых, из фразы WHEN, которая специфицирует имя исключения, и, во-вторых, из последовательности предложений, которые будут выполняться при возбуждении этого исключения. Эти предложения завершат исполнение блока или подпрограммы при исключении - управление больше не вернется в точку, где возникло исключение. Иными словами, вы не сможете возобновить работу с того места, где возникло исключение.

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

        ...
        EXCEPTION
            WHEN ... THEN
                -- обработать ошибку
            WHEN ... THEN
                -- обработать ошибку
            WHEN ... THEN
                -- обработать ошибку
            WHEN OTHERS THEN
                -- обработать все прочие ошибки
        END;
Использование обработчика OTHERS гарантирует, что ни одно исключение не пройдет необработанным.

Если вы хотите выполнять одну и ту же последовательность предложений для двух или более исключений, перечислите имена этих исключений в фразе WHEN, разделяя их ключевым словом OR, как показано ниже:

        ...
        EXCEPTION
            WHEN over_limit OR under_limit OR VALUE_ERROR THEN
                -- обработать ошибку
            ...
        END;
Если будет возбуждено любое из перечисленных в фразе WHEN исключений, соответствующий обработчик получит управление. Включение ключевого слова OTHERS в список имен исключений фразы WHEN НЕ ДОПУСКАЕТСЯ. Слово OTHERS может появиться только само по себе. Вы можете иметь сколько угодно обработчиков исключений, а каждый обработчик может ассоциировать последовательность предложений с любым списком исключений. Однако любое имя исключения может появиться лишь один раз в части обработки исключений блока или подпрограммы PL/SQL.

В обработчике исключений действуют обычные правила сферы видимости идентификаторов, определенные для переменных PL/SQL, так что обработчик может обращаться лишь к локальным и глобальным переменным. Однако, когда исключение возбуждается внутри курсорного цикла FOR, соответствующий курсор неявно закрывается перед вызовом обработчика. Поэтому значения явных атрибутов курсора НЕДОСТУПНЫ в обработчике.

Исключения, возбужденные в объявлениях

Исключения могут также возбуждаться некорректными выражениями инициализации в объявлениях. Например, следующее объявление неявно возбуждает исключение VALUE_ERROR, потому что limit не может хранить числа, большие 999:

        DECLARE
            limit  CONSTANT  NUMBER(3) := 5000;
            -- возбуждает исключение VALUE_ERROR
        BEGIN
            ...
        EXCEPTION
            WHEN VALUE_ERROR THEN  -- не перехватит это исключение
                ...
        END;
Обработчики в текущем блоке не могут перехватывать исключений, возбужденных в объявлениях, потому что такое исключение НЕМЕДЛЕННО продвигается в окружающий блок.

Исключения, возбужденные в обработчиках

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

Переходы в обработчик и из него

Предложение GOTO нельзя использовать для перехода в обработчик исключений либо для перехода из обработчика исключений в текущий блок. Например, следующее предложение GOTO НЕЗАКОННО:

        DECLARE
            pe_ratio  NUMBER(3,1);
        BEGIN
            DELETE FROM stats WHERE symbol = 'XYZ';
            SELECT price / NVL(earnings, 0) INTO pe_ratio FROM stocks
                WHERE symbol = 'XYZ';
            <>
            INSERT INTO stats (symbol, ratio) VALUES ('XYZ', pe_ratio);
        EXCEPTION
            WHEN ZERO_DIVIDE THEN
                pe_ratio := 0;
                GOTO my_label;  -- незаконный переход в текущий блок
        END;
Однако предложение GOTO можно использовать для перехода из обработчика исключения в окружающий блок.

Использование SQLCODE и SQLERRM

В обработчике исключений можно использовать функции SQLCODE и SQLERRM, чтобы узнать, какая ошибка произошла и получить сообщение об ошибке.

Для внутренне определенных исключений, SQLCODE возвращает номер ошибки ORACLE, передавшей управление обработчику. Этот номер отрицателен, исключая случай ошибки ORACLE "no data found", когда SQLCODE возвращает +100. SQLERRM возвращает сообщение, ассоциированное с возникшей ошибкой ORACLE. Это сообщение начинается с кода ошибки ORACLE.

Для пользовательских исключений, SQLCODE возвращает +1, а SQLERRM возвращает сообщение

        User-Defined Exception
если вы не использовали прагму EXCEPTION_INIT, чтобы ассоциировать ваше исключение с номером ошибки ORACLE; в этом случае SQLCODE возвращает этот номер ошибки, а SQLERRM возвращает соответствующее сообщение об ошибке. Заметим, что максимальная длина сообщения ORACLE об ошибке составляет 512 символов, включая код ошибки, вложенные сообщения и подстановки, такие как имена таблиц и столбцов.

Если не возбуждено никакое исключение, то SQLCODE возвращает 0, а SQLERRM возвращает сообщение

        ORA-0000: normal, successful completion
Вы можете передать функции SQLERRM номер ошибки; в этом случае SQLERRM возвратит сообщение, ассоциированное с этим номером ошибки. Номер ошибки, передаваемый SQLERRM, должен быть отрицателен. Нулевой код, передаваемый SQLERRM, всегда возвращает сообщение
        ORA-0000: normal, successful completion
Передача SQLERRM положительного номера ошибки (за исключением +100) всегда возвратит сообщение
        User-Defined Exception
а передача SQLERRM кода +100 возвратит сообщение
        ORA-01403: no data found
В следующем примере, SQLERRM возвратит не то, что ожидалось, потому что передается положительное значение вместо отрицательного:
        DECLARE
            msg  CHAR(100);
        BEGIN
            FOR num IN 1..9999 LOOP
                msg := SQLERRM(num);  -- надо задавать SQLERRM(-num)
                INSERT INTO errors VALUES (msg);
            END LOOP;
        END;
Вы не можете использовать функции SQLCODE и SQLERRM непосредственно в предложениях SQL. Например, следующее предложение незаконно:
        INSERT INTO errors VALUES (SQLCODE, SQLERRM);
Вместо этого вы должны присвоить значения этих функций локальным переменным, а затем использовать эти переменные в ваших предложениях SQL, как показывает следующий пример:
        DECLARE
            err_num  NUMBER;
            err_msg  CHAR(100);
        BEGIN
            ...
        EXCEPTION
            ...
            WHEN OTHERS THEN
                err_num := SQLCODE;
                err_msg := SUBSTR(SQLERRM, 1, 100);
                INSERT INTO errors VALUES (err_num, err_msg);
        END;
Строковая функция SUBSTR() гарантирует, что возможное усечение при присваивании переменной err_msg не возбудит исключения VALUE_ERROR. Функции SQLCODE и SQLERRM особенно полезны в исключении OTHERS, потому что они позволяют установить, какое внутреннее исключение было возбуждено.

Необработанные исключения

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

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

Необработанные исключения могут также влиять на подпрограммы. При успешном выходе из подпрограммы PL/SQL присваивает значения параметрам OUT. Однако, если вы выходите в результате необработанного исключения, значения параметрам OUT НЕ присваиваются. Кроме того, как уже сказано, если подпрограмма сбивается в результате необработанного исключения, ORACLE неявно отменяет ее работу. Однако, если подпрограмма выдала COMMIT до возбуждения необрабатываемого исключения, отменена будет лишь неподтвержденная часть работы.

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

Полезные приемы

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

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

Обработчик исключений позволяет вам исправить ошибку, которая иначе могла бы стать фатальной, до выхода из блока. Однако если завершить обработчик, то блок больше не получит управления. Вы не можете вернуться в текущий блок из обработчика исключений. Так, в следующем примере, если предложение SELECT INTO возбуждает исключение ZERO_DIVIDE (деление на 0), то вы не можете вернуть управление на INSERT:

        DECLARE
            pe_ratio  NUMBER(3,1);
        BEGIN
            DELETE FROM stats WHERE symbol = 'XYZ';
            SELECT price / NVL(earnings, 0) INTO pe_ratio FROM stocks
                WHERE symbol = 'XYZ';
            INSERT INTO stats (symbol, ratio) VALUES ('XYZ', pe_ratio);
        EXCEPTION
            WHEN ZERO_DIVIDE THEN
                ...
        END;
Тем не менее, существует способ обработать исключение для предложения, а затем продолжить работу со следующего предложения. Для этого просто поместите предложение в его собственный подблок вместе с его собственными обработчиками исключений, как показано в следующем примере:
        DECLARE
            pe_ratio  NUMBER(3,1);
        BEGIN
            DELETE FROM stats WHERE symbol = 'XYZ';
            ---------------- начало подблока ------------------
            BEGIN
                SELECT price / NVL(earnings,0) INTO pe_ratio FROM stocks
                    WHERE symbol = 'XYZ';
            EXCEPTION
                WHEN ZERO_DIVIDE THEN
                    pe_ratio := 0;
            END;
            ---------------- конец подблока -------------------
            INSERT INTO stats (symbol, ratio) VALUES ('XYZ', pe_ratio);
        EXCEPTION
            ...
        END;
Если в подблоке произойдет ошибка, локальный обработчик сможет обработать это исключение. После завершения подблока окружающий блок продолжит выполнение с той точки, где завершился подблок.

В последнем примере, если предложение SELECT INTO вызовет исключение ZERO_DIVIDE, локальный обработчик перехватит его и установит нулевое значение переменной pe_ratio. После завершения обработчика подблок завершится, и выполнение продолжится с предложения INSERT.

Повторение транзакции

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

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

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

        DECLARE
            name    CHAR(20);
            ans1    CHAR(3);
            ans2    CHAR(3);
            ans3    CHAR(3);
            suffix  NUMBER := 1;
        BEGIN
            ...
            LOOP  -- можно написать "FOR i IN 1..10 LOOP", чтобы
                  -- ограничиться максимально десятью попытками
                ------------------- начало подблока -------------------
                BEGIN
                    ...
                    SAVEPOINT start_transaction; -- точка сохранения
                        /* Удалить результаты опроса. */
                    DELETE FROM results WHERE answer1 = 'NO';
                        /* Добавить имя и ответы респондента. */
                    INSERT INTO results VALUES (name, ans1, ans2, ans3);
                        /* Это может дать исключение DUP_VAL_ON_INDEX, *
                         * если два респондента имеют одинаковые имена,*
                         * так как индекс по столбцу name уникальный. */
                    COMMIT;
                    EXIT;
                EXCEPTION
                    WHEN DUP_VAL_ON_INDEX THEN
                        ROLLBACK TO start_transaction;  -- откат
                        suffix := suffix + 1; -- попробуем исправить имя
                        name := name || TO_CHAR(suffix);
                    ...
                END;
                ------------------- конец подблока --------------------
            END LOOP;
        END;