Правила построения пакетов (package) в PL/SQL на примерах

package  PL/SQL - правила созданияСтруктура пакета 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 <имя_пакета>;
/

 

Вас заинтересует / Intresting for you:

Процедуры PL/SQL: программиров...
Процедуры PL/SQL: программиров... 22035 просмотров Анатолий Tue, 18 Sep 2018, 18:07:40
Трассировка кода PL/SQL: механ...
Трассировка кода PL/SQL: механ... 11757 просмотров Stas Belkov Sat, 27 Oct 2018, 16:56:38
Шифрование и дешифрование в PL...
Шифрование и дешифрование в PL... 11117 просмотров Илья Дергунов Thu, 13 Dec 2018, 13:30:08
Кэширование данных в Oracle дл...
Кэширование данных в Oracle дл... 7856 просмотров Александров Попков Thu, 22 Nov 2018, 13:53:50
Войдите чтобы комментировать