Структура пакета PL/SQL выглядит очень просто, но эта простота обманчива. Хотя синтаксис и правила построения пакетов вы изучите очень быстро, полное понимание нюансов реализации придет далеко не сразу. В этом разделе рассматриваются правила построения пакетов, а далее рассказывается, в каких ситуациях пакеты особенно эффективны.
Чтобы создать пакет, необходимо написать его спецификацию и почти всегда — тело. При этом нужно решить, какие элементы пакета будут указаны в спецификации, а какие скрыты в теле. В пакет также можно включить блок кода, который база данных Oracle будет выполнять при инициализации пакета.
Спецификация пакета PL/SQL
Спецификация пакета содержит список всех доступных элементов и предоставляет разработчику информацию, необходимую для использования пакета в приложениях. Ее часто называют программным интерфейсом — API (Application Programming Interface). Чтобы узнать, как применять описанные в спецификации элементы, разработчику не нужно изучать код, находящийся в теле пакета.
При разработке спецификации пакета необходимо руководствоваться следующими правилами:
- Элементы практически любого типа — числа, исключения, типы, коллекции и т. д. — могут объявляться на уровне пакета (то есть такие элементы не принадлежат конкретным процедурам или функциям этого пакета). Такие данные называются данными уровня пакетов. В общем случае объявлять переменные в спецификациях пакетов не рекомендуется, хотя объявления констант на уровне пакета вполне приемлемы. В пакете (как в спецификации, так и в теле) нельзя объявлять курсорные переменные (типа
REF CURSOR
), поскольку они не могут сохранять свое значение на протяжении сеанса. - В спецификации допускается объявление типов для любых структур данных: коллекций, записей или курсорных переменных.
- В спецификации можно объявлять процедуры и функции, но в ней должны быть указаны только их заголовки (часто определения процедуры или функции до ключевого слова IS или AS). Заголовок должен завершаться символом «
;
» (точка с запятой). - В спецификацию пакета могут включаться явные курсоры. Они могут быть представлены в одной из двух форм: SQL-запрос либо является частью объявления курсора, либо скрывается в теле пакета (тогда в объявлении присутствует только предложение
RETURN
). Эта тема подробно рассматривается в разделе «Пакетные курсоры». - Если в спецификации пакета объявляются процедуры или функции либо пакетный курсор без запроса, то тело пакета должно включать реализацию этих элементов.
- Спецификация пакета может содержать условие
AUTHID
, определяющее, как будут разрешаться ссылки на объекты данных: в соответствии с привилегиями владельца пакета (AUTHID DEFINER
) или того, кто его вызывает (AUTHID CURRENTJJSER
). - После команды
END
в конце спецификации пакета можно разместить необязательную метку, идентифицирующую пакет:
END my_package;
Для демонстрации этих правил рассмотрим простую спецификацию пакета:
PACKAGE favorites_pkg
AUTHID CURRENT_USER
IS /* или AS */
-- Две константы: вместо малопонятных значений
-- используются информативные имена.
c_chocolate CONSTANT PLS_INTEGER := 16;
c_strawberry CONSTANT PLS_INTEGER := 29;
-- Объявление типа вложенной таблицы
TYPE codes_nt IS TABLE OF INTEGER;
-- Вложенная таблица, объявленная на основе типа.
my_favorites codes_nt;
-- Курсорная переменная, возвращающая информацию из favorites.
TYPE fav_info_rct IS REF CURSOR RETURN favorites%ROWTYPE;
-- Процедура, принимающая список значений объявленного
-- выше типа codes_nt и выводящая соответствующую
-- информацию из таблицы..
PROCEDURE show_favorites (list_in IN codes_nt);
-- Функция, возвращающая всю информацию из таблицы
-- favorites о самом популярном элементе.
FUNCTION most_popular RETURN fav_info_rct;
END favorites_pkg; -- Закрывающая метка пакета
Как видите, пакет имеет почти такую же структуру спецификации, как раздел объявлений блока PL/SQL. Единственное отличие заключается в том, что спецификация не может содержать кода реализации.
Тело пакета
Тело пакета содержит весь код, необходимый для реализации спецификации пакета. Оно не является стопроцентно необходимым; примеры спецификаций пакетов без тела приведены в разделе «Когда используются пакеты». Тело пакета необходимо в том случае, если истинны хотя бы некоторые из следующих условий:
- Спецификация пакета содержит объявление курсора с секцией
RETURN
. В этом случае командаSELECT
должна быть указана в теле пакета. - Спецификация пакета содержит объявление процедуры или функции. В этом случае реализация модуля должна быть завершена в теле пакета.
- При инициализации пакета должен выполняться код, указанный в инициализационном разделе. Спецификация пакета не поддерживает исполняемый раздел (исполняемые команды в блоке
BEGIN-END
); эти команды могут находиться только в теле пакета.
Со структурной точки зрения тело пакета очень похоже на определение процедуры. Несколько правил, специфических для тел пакетов:
- Тело пакета может содержать раздел объявлений, исполняемый раздел и раздел исключения. Раздел объявлений содержит полную реализацию всех курсоров и программ, определяемых в спецификации, а также определение всех приватных элементов (не указанных в спецификации). Раздел объявлений может быть пустым — при условии, что в теле пакета присутствует инициализационный раздел.
- Исполняемый раздел пакета также называется инициализационным разделом; он содержит дополнительный код, выполняемый при инициализации пакета в сеансе. Эта тема будет рассмотрена в следующем разделе.
- В разделе исключений обрабатываются все исключения, инициированные в инициализационном разделе. Раздел исключений может располагаться в конце тела пакета только в том случае, если вы определили инициализационный раздел.
- Тело пакета может иметь следующую структуру: только раздел объявлений; только исполняемый раздел; исполняемый раздел и раздел исключений; раздел объявлений, исполняемый раздел и раздел исключений.
- Секция
AUTHID
не может входить в тело пакета; она должна размещаться в спецификации пакета. Все, что объявлено в спецификации, может использоваться в теле пакета. - Для тела и спецификации пакета действуют одни правила и ограничения объявления структур данных — например, невозможность объявления курсорных переменных.
- За командой
END
тела пакета может следовать необязательная метка с именем пакета:END my_package
;
Ниже приведена моя реализация тела favorites_pkg
:
PACKAGE BODY favorites_pkg
IS
-- Приватная переменная
g_most_popular PLS_INTEGER := c_strawberry;
-- Реализация функции
FUNCTION most_popular RETURN fav_info_rct
IS
retval fav_info_rct;
null_cv fav_info_rct;
BEGIN
OPEN retval FOR
SELECT *
FROM favorites
WHERE code = g_most_popular;
RETURN retval;
EXCEPTION
WHEN NO_DATA_FOUND THEN RETURN null_cv;
END most_popular;
-- Реализация процедуры
PROCEDURE show_favorites (list_in IN codes_nt) IS
BEGIN
FOR indx IN list_in.FIRST .. list_in.LAST
LOOP
DBMS_OUTPUT.PUT_LINE (list_in (indx));
END LOOP;
END show_favorites;
END favorites_pkg; -- Метка конца пакета
Другие примеры тел пакетов приведены в разделе «Когда используются пакеты».
Инициализация пакетов
Пакет может содержать структуры данных, сохраняющиеся на протяжении всего сеанса (см. раздел «Работа с данными пакета»). Когда в ходе сеанса впервые происходит обращение к пакету (вызывается объявленная в нем программа, считывается или записывается значение переменной либо используется объявленный в пакете тип), Oracle инициализирует его, выполняя следующие действия:
- Создание экземпляров данных уровня пакетов (значения переменных и констант).
- Присваивание переменным и константам значений по умолчанию, указанных в объявлениях.
- Выполнение блока кода, содержащегося в инициализационном разделе.
Oracle выполняет все эти действия только один раз за сеанс и только тогда, когда возникнет непосредственная необходимость в этой информации.
Пакет может быть повторно инициализирован в ходе сеанса, если он был перекомпилирован с момента последнего использования или был выполнен сброс состояния всего сеанса, на что указывает ошибка ORA-04068.
Инициализационный раздел пакета составляют все операторы, находящиеся между ключевым словом BEGIN
(вне определений процедур и функций) и ключевым словом END
, завершающим тело пакета. Например, инициализационный раздел пакета favorites_pkg
может выглядеть так:
PACKAGE BODY favorites_pkg
IS
g_most_popular PLS_INTEGER;
PROCEDURE show_favorites (list_in IN codes_nt) ... END;
FUNCTION most_popular RETURN fav_info_rct ... END;
PROCEDURE analyze_favorites (year_in IN INTEGER) ... END;
-- Инициализационный раздел
BEGIN
g_most_popular := c_chocolate;
-- Функция EXTRACT используется для извлечения года из значения,
-- возвращаемого функцией SYSDATE
analyze_favorites (EXTRACT (YEAR FROM SYSDATE));
END favorites_pkg;
PL/SQL автоматически определяет, когда должен выполняться код инициализацион- ного раздела. Это означает, что нет необходимости вызывать его явно и можно быть уверенными в том, что он будет выполнен только один раз. Когда следует использовать инициализационный раздел? Ниже описаны некоторые возможные причины.
Выполнение сложной логики инициализации
Конечно, значения по умолчанию могут присваиваться пакетным данным прямо в команде объявления. Тем не менее у этого подхода есть несколько потенциальных недостатков:
- Логика, необходимая для назначения значений по умолчанию, может быть слишком сложной для использования в конструкциях значений по умолчанию.
- Если при присваивании значения по умолчанию инициируется исключение, оно не может быть перехвачено в границах пакета; это исключение передается наружу необработанным. Эта тема более подробно рассматривается далее в разделе «Ошибки при инициализации».
Инициализация данных в инициализационном разделе обладает рядом преимуществ перед присваиванием значений по умолчанию. В частности, в исполняемом разделе вы обладаете полной гибкостью в определении, структуре и документировании ваших действий, а при возникновении исключения вы можете обработать его в разделе исключений инициализационного раздела.
Кэширование статической сеансовой информации
Другая причина для включения инициализационного раздела в пакет — кэширование статической информации, то есть остающейся неизменной на протяжении сеанса. Если значения данных не изменяются, зачем мириться с лишними затратами на запросы или повторное вычисление этих данных?
Кроме того, если вы хотите принять меры к тому, чтобы информация читалась в сеансе только один раз, инициализационный раздел становится идеальным автоматизированным решением.
При работе с кэшированными пакетными данными приходится учитывать важный компромисс между затратами памяти и вычислительных мощностей. Кэшируя данные в пакетных переменных, можно улучшить время выборки данных. Для этого данные размещаются «ближе» к пользователю, в области PGA каждого сеанса. При 1000 сеансах в системе существует 1000 копий кэшированных данных. Кэширование снижает нагрузку на процессор, но увеличивает затраты памяти — причем иногда весьма значительно.
За дополнительной информацией по этой теме обращайтесь к разделу «Кэширование статических данных сеанса для ускорения работы приложения».
Предотвращение побочных эффектов при инициализации
Избегайте присваивания значений глобальных данных в других пакетах (и вообще любых значений в других пакетах, если уж на то пошло). Эта защитная мера поможет предотвратить хаос при выполнении кода и потенциальную путаницу у программистов, занимающихся сопровождением. Код инициализационного раздела должен быть сконцентрирован на текущем пакете. Помните, что этот код выполняется тогда, когда ваше приложение в первый раз пытается использовать элемент пакета. Пользователи не должны сидеть сложа руки, пока пакет выполняет высокозатратные вычисления, которые можно вынести в другие пакеты или триггеры приложения. Пример кода, которого следует избегать:
PACKAGE BODY company IS
BEGIN
/*
|| Инициализационный раздел company_pkg обновляет глобальные
|| данные другого пакета. Ни в коем случае!
*/
SELECT SUM (salary)
INTO employee_pkg.max_salary
FROM employees;
END company;
Если ваши требования к инициализации отличны от представленных нами, рассмотрите альтернативу для инициализационного раздела — например, сгруппируйте стартовые команды в процедуре приложения. Присвойте процедуре содержательное имя (например, init_environment); затем в нужной точке процесса инициализации вызовите процедуру init_environment для настройки сеанса.
Ошибки при инициализации
Инициализация пакета проходит в несколько этапов: объявление данных, присваивание значений по умолчанию, выполнение инициализационного раздела (если он присутствует). А если произойдет ошибка, приводящая к сбою процесса инициализации? Оказывается, даже если пакет не может завершить свои действия по инициализации, база данных помечает пакет как инициализированный и не пытается снова выполнять стартовый код в этом сеансе. Чтобы убедиться в этом, рассмотрим следующий пакет:
PACKAGE valerr
IS
FUNCTION get RETURN VARCHAR2;
END valerr;
PACKAGE BODY valerr
IS
-- Глобальная - но при этом приватная - переменная уровня пакета
v VARCHAR2(1) := 'ABC';
FUNCTION get RETURN VARCHAR2
IS
BEGIN
RETURN v;
END;
BEGIN
D BMS_OUTPUT.PUT_LINE ('Before I show you v...');
EXCEPTION
WHEN OTHERS
THEN
DBMS_OUTPUT.PUT_LINE ('Trapped the error!');
END valerr;
Допустим, я подключаюсь к SQL*Plus
и пытаюсь выполнить функцию valerr.get
(первый раз в этом сеансе). Вот что я увижу:
SQL> EXEC DBMS_OUTPUT.PUT_LINE (valerr.get) *
ERROR at line 1:
ORA-06502: PL/SQL: numeric or value error: character string buffer too small
Иначе говоря, попытка объявления переменной v для присваивания значения «ABC» приводит к исключению VALUE_ERROR
. Раздел исключений в конце пакета не перехватывает ошибку; он может перехватывать только те ошибки, которые инициируются в самом инициализационном разделе. Таким образом, исключение остается необработанным. Однако следует заметить, что при повторном вызове этой функции в сеансе ошибка уже не выдается:
SQL> BEGIN
2 DBMS_OUTPUT.PUT_LINE ('V is set to ' || NVL (valerr.get, 'NULL'));
3 END;
4 /
V is set to NULL
Как интересно! Строка «Before I show you v...» вообще не выводится; более того, эта команда не выполняется. Ошибка происходит при первом вызове пакетной функции, но не при втором и всех последующих вызовах. Перед нами одна из классических «невоспроизводимых ошибок», а в мире PL/SQL
это типичная причина подобных проблем: сбой в ходе инициализации пакета.
Подобные ошибки усложняют диагностику. Чтобы снизить риск таких ошибок и упростить их обнаружение, лучше всего переместить присваивание значений по умолчанию в инициализационный раздел, чтобы раздел исключений мог корректно обрабатывать ошибки и сообщать об их вероятных причинах:
PACKAGE BODY valerr
IS
v VARCHAR2(1);
FUNCTION get RETURN VARCHAR2 IS BEGIN ... END;
BEGIN
v := 'ABC';
EXCEPTION
WHEN OTHERS
THEN
DBMS_OUTPUT.PUT_LINE ('Error initializing valerr:');
DBMS_OUTPUT.PUT_LINE (DBMS_UTILITY.FORMAT_ERROR_STACK);
DBMS_OUTPUT.PUT_LINE (DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
END valerr;
Вы даже можете стандартизировать структуру пакетов и потребовать обязательного включения процедуры инициализации, чтобы разработчики группы не забывали об этой проблеме:
PACKAGE BODY <package_name>
IS
-- Ниже размещаются приватные структуры данных.
-- Не используйте присваивание значений по умолчанию.
-- Вместо этого присваивайте их в процедуре инициализации
-- и проверяйте успешность присваивания в программе проверки.
-- Здесь размещаются приватные программы.
-- инициализационный раздел (не обязателен)
PROCEDURE initialize IS
BEGIN
NULL;
END initialize;
PROCEDURE verify_initialization (optional)
-- Программа используется для проверки состояния пакета.
-- Правильно ли прошло присваивание значений по умолчанию?
-- Были ли выполнены все необходимые действия?
IS
BEGIN
NULL;
END verify_initialization;
-- Здесь размещаются открытые программы.
BEGIN
initialize;
verify_initialization;
END <имя_пакета>;
/