Июнь 2005


Профессионалу разработчику


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

Разберите свои пакеты
(Picking Your Packages
by Steven Feuerstein
)

Источник: журнал Oracle Magazine, no.5,
http://www.oracle.com/technology/oramag/oracle/05-may/o35plsql.html

Узнайте когда нужно, а когда – нет, упаковывать свой PL/SQL код.

Использовать язык Oracle PL/SQL – сплошное удовольствие; это простой и исключительно удобочитаемый инструмент программирования. Как лазерный луч, он сфокусирован на своей конкретной задаче: выполнении операций в базе данных Oracle,.

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

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

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

Понятия пакета

Давайте сначала рассмотрим некоторые наиболее важные компоненты и понятия, которые лежат в основе структуры пакета в PL/SQL.

Спецификация и тело. Пакет состоит из двух различных фрагментов кода: спецификации пакета и тела пакета.

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

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

Хранение реализации (тела) отдельно от интерфейса (спецификации) является важным, позволяя нам, время от времени, изменять приложения. Пользователи наших приложений постоянно меняют свое мнение, а Oracle постоянно улучшает PL/SQL и базу данных. Эти факторы заставляют нас поддерживать форму, и искать способы улучшения кода.

Общий и частный. Общий (Public) код объявлен в спецификации пакета и доступен любой схеме, которая имеет привилегию EXECUTE на пакет (включая схему, которая владеет пакетом). Частный (Private) код, напротив, объявлен и является видимым только внутри пакета. Внешние программы, использующие пакет, не могут видеть или использовать частный код.

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

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

Постоянство сессии (Session persistence). Как программист базы данных вы должны быть знакомы с концепцией постоянства. Как-никак база данных вся – о постоянстве. Например, я вставляю строку в базу данных в понедельник, лечу на Багамские острова на оставшуюся неделю, и когда я возвращаюсь на работу в следующий понедельник, моя строка все еще находится в базе данных. Другой разновидностью постоянства является постоянство сессии, которое поддерживают PL/SQL пакеты. Это означает, что если я присоединюсь к базе данных Oracle (установлю сессию) и выполню программу, которая присваивает значение пакетной переменной (переменной, объявленной в пакете, вне какой-либо программы пакета), то значение этой переменной останется постоянным на протяжении всей сессии, и она сохранит свое значение, даже если программа, которая выполнила присваивание, закончилась.

Основные преимущества пакетов.

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

Сбор связанных элементов. Одной из приятнейших вещей в пакетах является то, что они предоставляют контейнер, в который можно положить связанные элементы. Вместо того, чтобы иметь 10 или 20 отдельных программ, в которых выполняются различные ипотечные вычисления, можно положить их все в пакет mortgage_calc.

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

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

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

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

Когда инициализация не работает

Как упоминалось ранее, раздел инициализации пакета запускается только один раз в сессию, чтобы выполнить какую-либо установочную логику, необходимую пакету. Здесь возникает логичный вопрос: Что происходит, когда инициализация не выполняется? Давайте рассмотрим пример.

Возьмем очень простой пакет, приведенный в Листинге 1. Спецификация пакета valerr содержит единственную функцию, которая возвращает значение переменной g_private. Переменная g_private объявлена в теле пакета, при объявлении ей присваивается значение abc.

Код Листинга 1: Пакет valerr

CREATE OR REPLACE PACKAGE valerr
IS
   FUNCTION private_variable RETURN VARCHAR2;
END valerr;
/
CREATE OR REPLACE PACKAGE BODY valerr
IS
   g_private VARCHAR2(1) := 'abc';

   FUNCTION private_variable RETURN VARCHAR2 
   IS
   BEGIN
      RETURN g_private;
   END private_variable;
BEGIN
   DBMS_OUTPUT.PUT_LINE ('Before I show you v...');

EXCEPTION
  WHEN OTHERS 
  THEN
    DBMS_OUTPUT.PUT_LINE ('Trapped the error!');
	 
END valerr;
/

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

Как можно догадаться из имени этого пакета, когда я пытаюсь присвоить значение по умолчанию переменной g_private, база данных Oracle вызывает исключение VALUE_ERROR, ORA-06502, поскольку эта переменная может хранить только один символ, а я пытаюсь присвоить ей три. Но что происходит с этим исключением после того, как оно вызвано?

Когда я запускаю эту программу сразу же после компиляции пакета, я вижу:

SQL> exec DBMS_OUTPUT.PUT_LINE 
(valerr.private_variable)

BEGIN DBMS_OUTPUT.PUT_LINE 
(valerr.private_variable); 
END;

*
ERROR at line 1:
ORA-06502: PL/SQL: numeric or 
value error: character string 
buffer too small
ORA-06512: at "SCOTT.VALERR", line 3
ORA-06512: at line 1

На первый взгляд это очень неестественно. В моем пакете есть раздел обработки исключений, который ловит “любую ошибку” (с помощью WHEN OTHERS) и отображает сообщение “Поймал ошибку!”. Вместо этого ошибка остается необработанной.

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

Даже еще интереснее, если я пытаюсь запустить эту программу снова, я не получаю ошибки. Вместо этого функция выполняется и возвращает значение g_private, равное NULL:

SQL> BEGIN
  2    DBMS_OUTPUT.PUT_LINE 
  3   ('Value='||valerr.private_variable);
  4   END;

SQL> /

Value=

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

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

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

Вообще, если вы столкнетесь со следующим сценарием: ошибка происходит один раз, при первом использовании, и не повторяется (тотчас же) в течении сессии – вам следует вспомнить о пакетах, которые вы недавно меняли, и особенно сосредоточиться на разделе объявлений в спецификации или теле пакета.

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

Код Листинга 2: Использование процедуры инициализации в теле пакета

CREATE OR REPLACE PACKAGE BODY valerr
IS
   g_private   VARCHAR2 (1);

   FUNCTION private_variable
      RETURN VARCHAR2
   IS
   BEGIN
      RETURN g_private;
   END private_variable;

   PROCEDURE initialize
   IS
   BEGIN
      DBMS_OUTPUT.put_line ('Before I show you v...');
      g_private := 'abc';
   EXCEPTION
      WHEN OTHERS
      THEN
         DBMS_OUTPUT.put_line ('Trapped the error!');
   END initialize;
BEGIN
   initialize;
END valerr;
/

При таком размещении программы инициализации вызов функции private_variable немедленно после компиляции (это позволяет быть уверенным, что инициализация произошла) приведет к совершенно другому поведению:

SQL> @valerr.pks

SQL> exec DBMS_OUTPUT.PUT_LINE 
(valerr.private_variable)

Перед тем как я покажу вам...
Поймал ошибку!

Иначе говоря, на этот раз сработал раздел инициализации, и ошибка была перехвачена.

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

Когда использовать пакет

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

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

Когда не использовать пакеты

Несмотря на все преимущества и полезность пакетов, существуют некоторые стороны пакетов, которые могут заставить призадуматься – и иногда, хотя и редко, отказаться от использования пакетов. Рассмотрим это подробнее.

Предположим, у меня есть пакет chip_util часто используемых утилит для производства картофельных чипсов. Он содержит, в частности, функцию company_tagline, которая возвращает рекламный лозунг этой марки картофельных чипсов (“Все хотят еще!”). Этот лозунг появляется внизу электронных писем компании, внутри отчетов, на экранах и так далее. Он вызывается сотнями программ системы, и никогда не меняется (эта компания не из тех, что тратят уйму денег на маркетинг!). В Листинге 3 показано, как может выглядеть пакет chip_util.

Код Листинга 3: Пакет chip_util

CREATE OR REPLACE PACKAGE chip_util
IS
   FUNCTION company_tagline RETURN VARCHAR2;
   
   FUNCTION max_chip_diameter (brand_in IN VARCHAR2) RETURN NUMBER;
   
   FUNCTION to_metric (weight_in in NUMBER) RETURN NUMBER;
   
   ...
   
   FUNCTION nutrition_label_template RETURN VARCHAR2; 
END chip_util;
/

CREATE OR REPLACE PACKAGE BODY chip_util
IS
   FUNCTION company_tagline RETURN VARCHAR2
   IS
   BEGIN
      RETURN 'Everybody wants more!';
   END company_tagline;	  
   
   ... all the rest of the programs ...
   
END chip_util;
/

В пакет chip_util постоянно добавляются новые утилиты, что требует перекомпиляции спецификации пакета chip_util. Эта перекомпиляция, в свою очередь, приводит к недостоверности и перекомпиляции всех других программ, которые вызывают chip_util.company_tagline —хотя сама программа не меняется.

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

CREATE OR REPLACE FUNCTION 
company_tagline RETURN VARCHAR2
IS
BEGIN
  RETURN 'Everybody wants more!'
END company_tagline;
/

Следующие шаги

ПРОЧИТАЙТЕ еще Фернстайна
oracle.com/technology/oramag
oracle.com/technology/pub/articles
oreillynet.com/cs/catalog/view/au/344

СКАЧАЙТЕ Oracle Database 10g

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

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

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

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

Кроме проблемы перекомпиляции, стоит также избегать пакетов, когда в приложении необходимо использовать преимущества объектно-ориентированных структур. В такой ситуации наилучшим выбором будет использование объектного типа Oracle – первоначально появившегося в Oracle8, и значительно улучшенного в Oracle9i, благодаря поддержке наследования.

Используйте пакеты широко, но разумно

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

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


Стивен Фернстайн (steven@stevenfeuerstein.com) - авторитет в языке PL/SQL. Он является автором девяти книг по PL/SQL (все - издательства O'Reilly Media, Inc.), включая Oracle PL/SQL Best Practices и Oracle PL/SQL Programming. Фернстайн разработал новый активный обучающий инструментарий для разработчиков, называемый Qnxo, предлагает обучение PL/SQL, и является старшим технологическим консультантом компании Quest Software.

E-mail this page