среда, 9 октября 2019 г.

EXCEPTION (Обработка ошибок)


PL/SQL блок:

DECLARE
...  -- объявляющая секция
BEGIN
...  -- выполняющая секция
EXCEPTION
...  -- секция обработки исключительных ситуаций
END;
/


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

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

Исключения бывают:

- стандартные
- определенные пользователем

Стандартные исключительные ситуации инициируются автоматически при возникновении
соответствующей ошибки Oracle.

Исключительные ситуации, определяемые пользователем,
устанавливаются явно при помощи оператора RAISE.


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

EXCEPTION
    WHEN имя_ex1 THEN
        ...; -- обработать
    WHEN имя_ex2 THEN
        ...; -- обработать
    WHEN OTHERS THEN
        ...; -- обработать
END;
/


Имена исключений не должны повторяться т.е. каждое исключение может
обрабатываться максимум только одним обработчиком в секции EXCEPTION

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

EXCEPTION
    WHEN NO_DATA_FOUND OR TOO_MANY_ROWS THEN
        INSERT INTO log_table(info) VALUES ('A select error occurred.');
END;
/

Два исключения одновременно один обработчик обработать не может:

WHEN имя_ex1 AND имя_ex2  - > ERR



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

DECLARE
    e_my_ex EXCEPTION;
    ...
BEGIN
    IF (...) THEN
        RAISE e_my_ex;
    END IF;
    ...

EXCEPTION
    WHEN e_my_ex THEN
    ...
END;
/



После перехвата более специализированных исключений:

WHEN ... THEN
...
WHEN ... THEN

мы можем перехватить все остальные исключения с помощью:

WHEN OTHERS THEN
...


Обработчик OTHERS рекомендуется помещать на самом высоком уровне программы:
(В самом высшем блоке)
для обеспечения распознавания всех возможных ошибок.

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

Не используйте в промышленном коде такое:

WHEN OTHERS THEN NULL;

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

Обработчик OTHERS должен регистрировать ошибку и возможно предоставлять
дополнительную информацию для дальнейшего исследования.

WHEN OTHERS THEN
    INSERT INTO log_table(info) VALUES ('Another error occurred.');
END;
/

Информацию об ошибках можно получить при помощи двух встроенных функций:

- SQLCODE
- SQLERRM

первая возвращает код текущей ошибки а вторая текст сообщения об ошибке

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

SQLCODE возвращает 1
а
SQLERRM "User-defined Exception"



WHEN OTHERS THEN
    v_ErrorCode := SQLCODE;
    v_ErrorText := SUBSTR(SQLERRM, 1, 200);
    INSERT INTO log_tab(code, message, info) VALUES (v_ErrorCode, v_ErrorText, 'Oracle error.');
END;
/

В таблице log_tab поле message ограничено 200 символами
и чтобы не произошло ошибки при вставке, мы урезаем длину
сообщения до 200 символов с помощью SUBSTR
А то максимальная длина сообщения может достигать 512 символов.

Функция SQLERRM может принимать один числовой аргумент.
При этом она возвратит текст сообщения об ошибке, код которой равен заданному числу.

Аргумент должен быть всегда отрицательным числом.
Если аргумент равен 0, то будет возвращено сообщение:
ORA-0000: normal, succesful completion

При положительном аргументе не равном 100 будет возвращено сообщение:
non-ORACLE Exception

А при

SQLERRM(100) - > ORA-1403: no data found

Это исключение ANSI

Остальные коды ошибок Oracle все отрицательные.

Для получения информации об ошибке можно также использовать функцию
FORMAT_ERROR_STACK из пакета DBMS_UTILITY

Её можно непосредственно использовать в операторах SQL:

WHEN OTHERS THEN
    INSERT INTO log_tab(code, message, info) VALUES (NULL,
                                                     SUBSTR(DBMS_UTILITY.FORMAT_ERROR_STACK, 1, 200),
                                                    'Oracle error occurred.');
END;
/


Ещё одна функция.

DBMS_UTILITY.FORMAT_ERROR_BACKTRACE

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

Любое именованное исключение можно связать с конкретной ошибкой ORACLE.

Например, в ORACLE есть стандартная ошибка ORA-1400, которая возникает при пропуске значения
или вставке значения NULL в столбец с ограничением NOT NULL.

ORA-1400: mandatory NOT NULL column missing or NULL during insert.

Мы хотим создать свое пользовательское именованное исключение и связать его с этой стандартной ошибкой ORA-1400

DECLARE
e_my_ex EXCEPTION;
PRAGMA EXCEPTION_INIT(e_my_ex, -1400);

BEGIN
WHEN e_my_ex THEN
    INSERT INTO log_tab(info) VALUES ('ORA-1400 occurred.');
END;
/

Теперь мы перехватываем её по имени с помощъю WHEN или THEN

Все стандартные исключительные ситуации также ассоциируются с соответствующими им ошибками Oracle
при помощи прагмы EXCEPTION_INIT в пакете STANDARD


VALUE_ERROR  - > ORA-6501
TO_MANY_ROWS - > ORA-1422
ZERO_DIVIDE  - > ORA-1476
..........
и т.д.


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


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


RAISE_APPLICATION_ERROR(номер, текст, [флаг]);

TRUE - пополнить список ранее произошедших ошибок
FALSE - новая ошибка заместит текущий список ошибок (по умолчанию)


set serveroutput on

variable a NUMBER;
variable b NUMBER;

exec :a := 0;
exec :b := 10;

DECLARE
    l_a NUMBER := :a;
    l_b NUMBER := :b;
    l_c NUMBER;

BEGIN
    IF l_a = 0 THEN
        raise_application_error(-20005, 'Divizor is 0');
    END IF;
    l_c := l_b / l_a;
    dbms_output.put_line('The result: '||l_c);
EXCEPTION
    WHEN OTHERS THEN
        dbms_output.put_line(SQLERRM);
END;
/


Поскольку у исключения нет имени, то его может обработать только обработчик OTHERS

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

DECLARE

    my_ex EXCEPTION;
    .....
    .....
    PRAGMA EXCEPTION_INIT(my_ex, -20005);

BEGIN
    IF (...) THEN
        raise_application_error(-20005, 'Divizor is 0');
    .....
    .....

EXCEPTION
    WHEN my_ex THEN
        dbms_output.put_line(SQLERRM);
END;
/


Теперь это исключение можно обработать по имени с помощью:

WHEN my_ex THEN


EXCEPTION PROPAGATION


enclosing block  - обьемлющий блок

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

Если обработчик отсутствует, исключительная ситуация передается в обьемлющий блок и инициируется там.
Если обьемлющего блока не существует, то исключение будет передано вызывающей среды (например SQL*Plus).


При вызове процедуры также может создаваться обьемлющий блок:

BEGIN

    p(...); -- вызов процедуры
EXCEPTION
    WHEN OTHERS THEN
        -- исключение инициированное p()
        -- будет обработано здесь
END;
/




Исключения инициируемые в секции обьявлений (DECLARE) не обрабатываются секцией EXCEPTION
текущего блока, а передаются в EXCEPTION обьемлющего блока.

Тоже самое, если исключение инициируется в секции EXCEPTION,
то обработка данного исключения передается в обьемлющий блок.

Исключительную ситуацию можно обработать в текущем блоке и сразу снова установить
то же самое исключение, которое будет передано в обьемлющую область:


DECLARE

    A EXCEPTION;
BEGIN

    RAISE A;
EXCEPTION
    WHEN A THEN
        INSERT INTO log_tab(info) VALUES ('Exception A occurred.');
        COMMIT;
        RAISE;
END;
/


Тут commit гарантирует, что результаты insert будут зафиксированы
в базе данных в случае отката транзакции.

С помощью пакета UTL_FILE можно избежать необходимости commit
или используйте автономные транзакции.


Область действия исключительной ситуации


BEGIN
    DECLARE
        e_ex EXCEPTION;  -- видно по имени только внутри блока
    BEGIN
        RAISE e_ex;
    END;
EXCEPTION
    -- тут исключение не видно по имени e_ex
    -- и его можно обработать с помощью обработчика OTHERS

    WHEN OTHERS THEN
        -- инициируем это исключение повторно
        RAISE;  -- Теперь это исключение передается вызывающей среде
END;
/

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

Как описать исключение, которое будет видно вне блока?

Нужно создать пакет Globals и описать в нем пользовательское исключение.
Такая исключительная ситуация будет видима и во внешнем блоке.

CREATE OR REPLACE PACKAGE Globals AS
    e_ex EXCEPTION;
END Globals;

BEGIN
    BEGIN
        RAISE Globals.e_ex;
    END;

EXCEPTION
    WHEN Globals.e_ex THEN
        -- инициируем повторно
        -- для передачи в вызывающую среду
        RAISE;
END;
/
 

В пакете Globals можно также объявлять:

- таблицы
- переменные
- типы


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

Используйте обработчик OTHERS на самом верхнем уровне программы.
И пусть он регистрирует факт и время возникновения ошибки.
И ни одна ошибка не останется без внимания.

DECLARE

    v_ErrorNumber NUMBER;
    v_ErrorText   VARCHAR2(200);
BEGIN
    ...
    ...

EXCEPTION
    WHEN OTHERS THEN
        ...

        v_ErrorNumber := SQLCODE;
        v_ErrorText   := SUBSTR(SQLERRM, 1, 200);

        INSERT INTO log_tab(code, message, info)
        VALUES (v_ErrorNumber, v_ErrorText,
                'Oracle error ...at ' || to_char(sysdate, 'DD-MON-YYHH24:MI:SS'));
END;
/


Можно использовать и утилиту  DBMS_UTILITY.FORMAT_ERROR_BACKTRACE
она регистрирует первоначальное местовозникновения исключения.

Как определить, где произошла ошибка?

BEGIN

    SELECT ...
    SELECT ...
    SELECT ...
EXCEPTION
    WHEN NO_DATA_FOUND THEN
    -- какой select инициировал ошибку?
END;
/

Можно создать счетчик, указывающий на sql - оператор:


DECLARE

    v_sel_count NUMBER := 1;
BEGIN

    SELECT ...
    v_sel_count := 2;
    SELECT ...
    v_sel_count := 3;
    SELECT ...

EXCEPTION
    WHEN NO_DATA_FOUND THEN
        INSERT INTO log_tab(info)
        VALUES ('no data found in select '||v_sel_count);
END;
/


Можно разместить каждый select в собственном врутреннем блоке

BEGIN

    BEGIN
        SELECT ...
    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            INSERT INTO log_tab(info)
            VALUES ('no data found in select 1');
    END;


    BEGIN
        SELECT ...
    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            INSERT INTO log_tab(info)
            VALUES ('no data found in select 2');
    END;


    BEGIN
        SELECT ...
    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            INSERT INTO log_tab(info)
            VALUES ('no data found in select 3');
    END;

END;
/


Или использовать : DBMS_UTILITY.FORMAT_ERROR_BACKTRACE
и потом анализировать файл трассировки.




Пусть в нашей программе Oracle выдает ошибку ORA-01844: not f valid month

перехватить его можно так:

EXCEPTION
    WHEN OTHERS THEN
        IF SQLCODE = -1843 THEN


Да, код плохо читаем.
Сделаем его более лучшим:

PROCEDURE my_procedure
IS
    invalid_month EXCEPTION;
    PRAGMA EXCEPTION_INIT(invalid_month, -1843);

BEGIN
    ....
EXCEPTION
    WHEN invalid_month THEN

так уже более понятней.





вторник, 24 сентября 2019 г.

Ассоциативные массивы

Ассоциативные массивы

Ассоциативные массивы могут быть определены только в рамках программы PL/SQL
Ни структура массива, ни данные не могут сохраняться в БД.

Каждый элемент массива отличается индексом:

Индексирование с помощью binary_integer:

1 | ...........
2 | ...........
3 | ...........

...............
10 | ...........


Индексирование с помощью varchar2:

a | ...........
b | ...........
c | ...........

...............
z | ...........

Они впервые появились в Oracle 7 и назывались по разному:

Oracle 7  - таблицы PL/SQL
Oracle 8  - индекс таблицы
Oracle 10 - ассоциативные массивы

Объявляются они так:

type name is table of type [not null]
index by type;

Тип индекса может быть:

- binary_integer
- pls_integer
- positive
- natural
- signtype
- varchar2

Тип данных элемента может быть:
- number и его подтипы
- varchar2 и его подтипы
- date
- blob
- clob
- boolean

А также:

- типы данных наследуемые из столбца таблицы
- курсорного выражения
- предопределённой переменной модуля
- определённый пользователем тип объекта или коллекции

Пример:

type clob_t is table of clob
index by pls_integer;

type empno_t is table of employees.empno%type not null
index by employees.ename%type;


Пример ассоциативного массива:

declare

type ln is table pers.f_name%TYPE
index by pls_integer;

begin

fam(20) := 'Tom';
fam(15) := 'Larry';
fam(-10) := 'Scott';

l_row := fam.FIRST;
while (l_row is not null)  loop
    dbms_output.put_line(fam(l_row));
    l_row := fam.NEXT(l_row);
end loop;

end;
/


Ещё пример:

set serveroutput on

declare

type str_t is table of number
index by varchar2(10);

l_str str_t;
l_idx varchar2(50);

begin

l_str('JAN-MAR') := 90;
l_str('APR-JUN') := 91;
l_str('JUL-SEP') := 92;
l_str('OCT-DEC') := 93;

l_idx := l_str.FIRST;
while (l_idx is not null) loop
    dbms_output.put_line('Value at index ' || l_idx || 'is' || l_str(l_idx));
    l_idx := l_str.NEXT(l_idx);
end loop;
end;
/


Следующий PL/SQL блок объявляет ассоциативный массив для хранения ASCII значений чисел от 1 до 100:

set serveroutput on

declare

type ascii_t is table of varchar2(12)
index by pls_integer;

l_get_ascii ascii_t;

begin

for i in 1 .. 100 loop
    l_get_ascii(i) := ascii(i);
end loop;

dbms_output.put_line(l_get_ascii(5));
end;
/

53



Ассоциативные массивы :

- не могут быть использованы в таблицах и применяются только как программные структуры
- не требуют инициализации (нет метода EXTEND)
- индексы: только строки и числа
  (положительные, отрицательные и нуль)

- неявно конвертируются из:
 %ROWTYPE
 типов записи
 объектных типов
- не могут применяться в транзакциях DML (как непостоянные коллекции)
- могут передаваться как аргумент другим локальным подпрограммам в рамках одного и того-же блока.
- ассоциативный массив, объявленный в спецификации модуля, ведет себя как массив, постоянный внутри сессии.
в структуры ассоциативных массивов

- являются ключом к использованию оператора FORALL
  или фразы BULK COLLECT
которые разрешают групповую пересылку данных из БД в программный модуль.

Если в качестве индекса используется индексная строка, то порядок их обработки
зависит от параметров БД:
NLS_SORT
NLS_COMP

Пример:

DECLARE

type card is table of varchar2(5 char)
index by binary_integer;

-- конструктор использовать нельзя
cards card := card('A', 'B', 'C');
-- error 00222

-- объявим переменную
cards card;

BEGIN

-- нельзя перемещаться по ассоциативному массиву,
-- пока в нём отсутствуют элементы
dbms_output.put_line(cards(1));
-- error no data found

END;
/


Чтобы избежать подобной ошибки делайте так:

DECLARE

type card is table of varchar2(5 char)
index by binary_integer;

cards card;

BEGIN

if cards.COUNT < > 0 then
    dbms_output.put_line(cards(1));
else
    dbms_output.put_line('cards coll is empty.');
end if;

END;
/


Метод COUNT возвращает нулевое значение только:
- когда у ассоциативного массива отсутствуют элементы с присиоенными значениями.
- когда varray или nested table проинициализпрованы и их элементам не выделена память

Метод EXTEND не сможет выделить память для ассоциированного массива


DECLARE

type card is table of varchar2(5 char)
index by binary_integer;

cards card;

BEGIN

if cards.COUNT < > 0 then
    dbms_output.put_line(cards(1));
else
    cards.EXTEND;
-- error 00306
end if;

END;
/



Инициализация ассоциативных массивов

DECLARE

type mv is varray(12) of varchar2(9 char);

type calt is table of varchar2(9 char)
index by binary_integer;

month mv := mv('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December');

calendar calt;

-- тут month - это varray
-- calendar - это ассоциативный массив

BEGIN
if calendar.COUNT = 0 then
    dbms_output.put_line('Assigment loop:');
    dbms_output.put_line('---------------');
    for i in month.FIRST .. month.LAST LOOP
        calendar(i):= '';
        -- инициализируем
        dbms_output.put_line('Index[' || i || '] is [' calendar(i) || ']');
        calendar(i) := month(i);
    end loop;

    dbms_output.put_line(chr(10));
    dbms_output.put_line('Post-assigment loop:');
    dbms_output.put_line('--------------------');

    for i in calendar.FIRST .. calendar.LAST LOOP
        dbms_output.put_line('Index[' || i || '] is [' calendar(i) || ']');
    end loop;
end if;

END;
/




Числовой индекс:

type mv is varray(12) of string(9 char);

type calt is table of varchar2(9 char)
index by binary_integer;

month mv := mv('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December');

calendar calt;
   
calendar(1) := '';
calendar(1) := month(1);

calendar(1) := 'January';

-- calendar(1) - > January



Строковый индекс:

type mv is varray(12) of string(9 char);

type calt is table of varchar2(9 char)
index by varchar2(9 char);

month mv := mv('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December');

calendar calt;
   
calendar(January) := '1';
.....
calendar(December) := '12';


calendar(April) - > 4
.....
calendar(September) - > 9


Для строкового индекса процесс изменяется:

-- Присвоим значения:
if calendar.COUNT = 0 then
    for i in month.FIRST .. month.LAST LOOP
        calendar(month(i)):= i;
    end loop;


-- Вывод значений:
    for i in calendar.FIRST .. calendar.LAST LOOP
        dbms_output.put_line('Index[' || i || '] is [' calendar(i) || ']');
    end loop;
end if;

Инициализация сработает
А второй цикл for при выводе значений выдаст ошибку

calendar(April) - > 4
.....
calendar(September) - > 9


При попытке вызвать функции FIRST и LAST мы получим ORA-06502
Ошибка преобразования числа

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

Для нечисловых значений индексов необходимо знать с чего следует начинать и как наращивать.

Методы FIRST и NEXT дают нам такое средство

Рассмотрим логику обхода уникальных строковых индексов.

Определим две переменные, которые будут использованы для управления навигацией
по ассоциативному массиву с уникальными строковыми индексами

current varchar2(9 char);
element integer;

Цикл по всем элементам фссоциативного массива:

for i in 1 .. calendar.COUNT loop

    -- проверьте, является ли элемент первым в списке
    if i = 1 then
        -- присвойте первый символьный индекс переменной
        current := calendar.FIRST;

        -- используйте полученный индекс для нахождения следующего индекса
        element := calendar(current);
    else
        -- проверьте существует ли следующее значение индекса
        if calendar.NEXT(current) is not null then

            -- присвойте символьный индекс переменной
            current := calendar.NEXT(current);

            -- используйте полученный индекс для нахождения следующего индекса
            element := calendar(current);
        else
            exit
            -- было прочитано последнее значение индекса
        end if
    else if

    dbms_output.put_line('Index[' || current || '] is [' element || ']');

end loop;



Метод FORALL

Есть таблица t1  с полем id типа number.

Обычная вставка данных:

begin

for i in 1 .. 10000 loop
    insert into t1 values(i);
end loop;
commit;
end;
/

Вставка данных из коллекции:

begin

forall i in 1 .. num_list.COUNT
    insert into t1 values(num_list(i));
commit;
end;
/

Метод FORALL позволяет посылать операторы DML пакетами

Метод FORALL предлагает средство для переноса содержимого ассоциативного массива
или вложенной таблицы в объект БД.

declare

type td is table of t1.id%type
index by binary_integer;

num_list td;

begin

for i in 1 .. 10000 loop
    insert into t1 values(i);
end loop;


forall i in 1 .. num_list.COUNT
    insert into t1 values(num_list(i));
commit;
-- фиксируем результат каждого пакета вставок
end;
/


Метод BULK COLLECT

Метод BULK COLLECT позволяет выбирать наборы записей ,
которые можно хранить в массивах переменной длины и во вложенных таблицах.

Метод BULK COLLECT позволяет, как единое множество,
присвоить коллекцию значений %ROWTYPE или %TYPE ассоциативному массиву
или вложенной таблице.

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

Память во вложенной таблице будет неявно выделять bulk collect

Для ассоциированного массива не требуется конструктора,
достаточно группового присваивания.

Выборка данных в ассоциативный массив:

declare

type td is table of t1.id%type
index by binary_integer;

num_list td;

begin

select id bulk collect into num_list from t1 order by 1;

for i in num_list.FIRST .. num_list.LAST loop
    if i < = 2  or i > = 9999 then
        dbms_ounput.put_line(num_list(i));
    end if;
end loop;
end;
/