J.1. Работа с пакетами

В Postgres Pro реализованы пакеты, подобные пакетам в Oracle. Они могут быть полезны при портировании с PL/SQL на PL/pgSQL. В этом разделе описаны различия между пакетами в Postgres Pro и Oracle.

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

Ниже приведён пример спецификации пакета counter на PL/SQL в Oracle, содержащего глобальную переменную n и функцию inc:

CREATE PACKAGE counter IS
    n int;

    FUNCTION inc RETURN int;
END;

В теле пакета counter в Oracle объявлены функция inc и глобальная переменная k (доступна только в теле пакета).

CREATE PACKAGE BODY counter IS
    k int := 3; -- переменная доступна только в теле пакета

    FUNCTION inc RETURN int IS
    BEGIN
        n := n + 1;
        RETURN n;
    END;

-- Инициализация пакета
BEGIN
    n := 1;
    FOR i IN 1..10 LOOP
        n := n + n;
    END LOOP;
END;

Глобальные переменные пакета существуют в течение сеанса. Обратите внимание, что в теле пакета была определена секция инициализации — блок кода, который выполняется один раз за сеанс при первом обращении к любому элементу пакета. В Oracle к элементам пакета можно обращаться, используя запись с точкой: имя_пакета.элемент_пакета.

SET SERVEROUTPUT ON

BEGIN
    dbms_output.put_line(counter.n);
    dbms_output.put_line(counter.inc());
END;
/

1024
1025

В Postgres Pro пакет по сути представляет собой схему, которая содержит функцию инициализации и может содержать только функции, процедуры и составные типы. Функция инициализации — это функция на PL/pgSQL с именем __init__, которая не имеет аргументов и возвращает значение типа void. Функция инициализации должна быть определена до любой другой функции пакета. По умолчанию все переменные, объявленные в функции инициализации пакета, функциях пакета и процедурах пакета, являются публичными, поэтому к ним можно обращаться снаружи пакета, используя запись с точкой в других функциях, процедурах и анонимных блоках, импортирующих пакет. Модификатор #private определяет функции и процедуры как внутренние, а модификатор #export определяет, какие переменные пакета являются публичными. Например, на переменную bar, объявленную в функции __init__ пакета foo, можно ссылаться как foo.bar.

Переменные пакета доступны только из кода на языке PL/pgSQL. Для получения значений глобальных переменных пакета из SQL-запроса (SELECT) или DML-оператора (INSERT, UPDATE, DELETE), необходимо создать функцию-геттер, возвращающую значение глобальной переменной. Для определения переменных пакета, объявленных как константы, используется стандартный для PL/pgSQL синтаксис. Функция инициализации пакета вызывается автоматически при первом обращении к любому из следующих элементов в текущем сеансе:

  • Любая функция данного пакета с модификатором #package
  • Блок кода (анонимный или функция), импортирующий данный пакет

Функцию инициализации можно вызвать вручную, чтобы сбросить значения переменных пакета. Для этого также можно использовать встроенную функцию plpgsql_reset_packages(), но она сбрасывает значения глобальных переменных всех пакетов в текущем сеансе (аналог DBMS_SESSION.RESET_PACKAGES в Oracle).

Описанный выше пример пакета counter в Oracle после портирования в Postgres Pro примет следующий вид:

CREATE PACKAGE counter

    CREATE FUNCTION __init__() RETURNS void AS $$ -- инициализация пакета
    #export n
    DECLARE
        n int := 1; -- публичная переменная n, доступная и внутри, и снаружи пакета
        k int := 3; -- внутренняя переменная k, доступная только внутри пакета
    BEGIN
        FOR i IN 1..10 LOOP
            n := n + n;
        END LOOP;
    END;
    $$

    CREATE FUNCTION inc() RETURNS int AS $$
    BEGIN
        n := n + 1;
        RETURN n;
    END;
    $$
;

Использовать этот пакет в Postgres Pro можно так:

DO $$
#import counter
BEGIN
    RAISE NOTICE '%', counter.n;
    RAISE NOTICE '%', counter.inc();
END;
$$;

NOTICE:  1024
NOTICE:  1025

Стоит отметить следующее.

  • Пакет не может содержаться в схеме, так как сам является схемой.

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

  • Пакет может содержать только составные типы, глобальные переменные, процедуры и функции (например, создание таблиц, представлений, последовательностей в пакетах не поддерживается).

  • Все переменные пакета являются публичными, если используется модификатор #export on или если модификатор #export отсутствует. Все функции и процедуры пакета являются публичными, если не используется модификатор #private. Публичные переменные пакета доступны из любого блока кода, импортирующего пакет.

  • Спецификация пакета и тело пакета не определяются отдельно.

  • Как функции пакета, так и импортирующие его блоки кода должны быть написаны на языке PL/pgSQL.

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

  • Глобальные переменные пакета могут инициализироваться как при объявлении:

    DECLARE x int := 42;

    так и позже в теле функции инициализации __init__. Для внешнего кода, использующего пакет, эти варианты эквивалентны.

  • Типы должны быть объявлены в начале пакета, до определения первой подпрограммы.

  • Функция __init__ должна быть первой подпрограммой, определённой в пакете.

Для поддержки пакетов Postgres Pro включает следующие усовершенствования:

J.1.1. Модификаторы функций и переменных

Модификаторы функций и переменных начинаются с символа # и располагаются между именем и оператором DECLARE (пример приведён ниже).

J.1.1.1. Модификатор #package

Модификатор #package указывает, что данная функция должна трактоваться как пакетная, что означает следующее:

  • Вызов функции приводит к автоматической инициализации содержащего её пакета, если пакет не был инициализирован ранее в текущем сеансе.

  • Функция может обращаться к переменным своего пакета напрямую.

Функция __init__ не должна содержать модификатор #package. Создание функции с модификатором #package возможно только в схеме, уже содержащей функцию __init__ (то есть в пакете). Если функция создаётся при выполнении команды CREATE OR REPLACE PACKAGE, модификатор #package можно не указывать, так как он автоматически добавляется этой командой.

J.1.1.2. Модификатор #import

Модификатор #import указывает, что данная функция будет работать с переменными некоторого внешнего пакета (или нескольких пакетов). Это называется импортированием пакета и означает следующее:

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

  • Функция может обращаться к переменным импортированного пакета по его имени. Для импорта пакета используйте следующий синтаксис:

    #import список_пакетов

    Здесь список_пакетов — список имён импортируемых пакетов. Пакеты также можно импортировать отдельно, например:

    #import foo, bar

    равнозначно следующему:

    #import foo
    #import bar

Пакеты, импортированные в функции __init__, автоматически импортируются для всех остальных функций данного пакета.

В следующем примере использования модификатора #import процедура showValues вызывает функцию p из пакета htp и функцию set_action из пакета dbms_application_info.

CREATE OR REPLACE PROCEDURE showValues(p_Str varchar) AS $$
#import htp, dbms_application_info
BEGIN
  CALL dbms_application_info.set_action('Show hello');

  CALL htp.p('<p>' || p_Str || '</p>');
END;
$$LANGUAGE plpgsql;

J.1.1.3. Модификатор #private

Модификатор #private определяет, что функция является внутренней, то есть она доступна только внутри пакета и на неё нельзя ссылаться снаружи. Внутренние функции необходимы для обслуживающих процессов пакета. Они доступны только в указывающем их пакете.

J.1.1.4. Модификатор #export

Модификатор #export определяет, что переменная пакета является публичной, то есть на неё можно ссылаться снаружи пакета. Модификатор #export используется при инициализации пакета:

#export имя_переменной_1, имя_переменной_2, ... имя_переменной_N
  • Все переменные пакета являются публичными, если используется модификатор #export on или если модификатор #export отсутствует.

  • Если пакет содержит модификатор #export с указанными переменными, эти переменные являются публичными, а все остальные переменные пакета — внутренними.

  • Модификатор #export можно использовать в функции __init__ несколько раз, тогда все указанные с ним переменные считаются публичными.

  • Модификатор #export off означает, что никакие переменные не должны быть публичными, и указание экспортируемых переменных в этом случае вызовет ошибку.

  • Модификатор #export on делает все переменные публичными, и последующее использование модификатора #export вызовет ошибку.

  • Модификатор #export должен содержать хотя бы одну переменную.

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

  • Публичные переменные можно использовать как внутри, так и снаружи пакета, если функции, процедуры и анонимные блоки содержат модификатор #import с именем пакета.

J.1.2. Встроенные функции

В PL/pgSQL имеется встроенная функция plpgsql_reset_packages(), которая возвращает все пакеты в данном сеансе в исходное состояние.

SELECT plpgsql_reset_packages();

J.1.3. Команды SQL

Работать с пакетами можно, используя обычные команды для схем (например, CREATE SCHEMA и DROP SCHEMA) с модификаторами функций, но удобнее использовать специальные команды CREATE OR REPLACE PACKAGE и DROP PACKAGE.

J.1.3.1. CREATE OR REPLACE PACKAGE

CREATE OR REPLACE PACKAGE либо создаст новый пакет, либо заменит существующее определение.

CREATE [OR REPLACE] PACKAGE имя_пакета тело_пакета;

Здесь имя_пакета — это имя схемы, а тело_пакета — набор команд создания пакета с рядом особенностей, о которых следует помнить:

  • Команда CREATE SCHEMA выполняется автоматически.

  • Модификатор #package можно не использовать — все функции получают его автоматически.

  • При создании функций не указывается язык.

  • Имена создаваемых функций и типов указываются без имени пакета.

  • Отдельные подкоманды верхнего уровня в теле пакета не завершаются точкой с запятой.

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

В приведённом ниже примере показано, как можно создать пакет посредством команды CREATE SCHEMA.

CREATE SCHEMA foo;
CREATE TYPE foo.footype AS (a int, b int);
CREATE FUNCTION foo.__init__() RETURNS void AS $$
DECLARE x int := 1;
BEGIN
  RAISE NOTICE 'foo initialized';
END;
$$ LANGUAGE plpgsql;

CREATE FUNCTION foo.get() RETURNS int AS $$
#package
BEGIN
  RETURN x;
END;
$$ LANGUAGE plpgsql;

CREATE FUNCTION foo.inc() returns void AS $$
#package
BEGIN
  x := x + 1;
END;
$$ LANGUAGE plpgsql;

Ниже показано, как можно добиться того же результата командой CREATE PACKAGE:

CREATE PACKAGE foo
  CREATE TYPE footype AS (a int, b int)

  CREATE FUNCTION __init__() RETURNS void AS $$
  DECLARE
    x int := 1;
  BEGIN
    RAISE NOTICE 'foo initialized';
  END;
  $$

  CREATE FUNCTION get() RETURNS int AS $$
  BEGIN
    RETURN x;
  END;
  $$

  CREATE FUNCTION inc() RETURNS void AS $$
  BEGIN
    x := x + 1;
  ENG;
  $$
;

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

  • Команду CREATE OR REPLACE PACKAGE можно использовать только для замены схем, являющихся пакетами (то есть содержащих только функции, составные типы и функцию инициализации).

  • Элементы старого пакета, не определённые в новом пакете, будут удалены, если у них нет зависимых объектов; в противном случае команда CREATE OR REPLACE PACKAGE завершится ошибкой.

  • Если при замене пакета не меняется сигнатура функции, тело функции заменяется, а зависимые объекты сохраняются. Если сигнатура функции меняется, фактически создаётся новая, отдельная функция. Последнее возможно только при отсутствии зависимых объектов, иначе CREATE OR REPLACE PACKAGE завершится ошибкой.

  • Те же ограничения касаются замены типов.

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

CREATE OR REPLACE PACKAGE — это полностью транзакционная команда, поэтому любые ошибки при определении элементов пакета приведут к полному откату всех изменений.

J.1.3.2. DROP PACKAGE

DROP PACKAGE удаляет пакеты (схемы) из базы данных вместе с зависимыми объектами.

DROP PACKAGE [IF EXISTS] список_имён_пакетов [CASCADE];

Здесь список_имён_пакетов — список пакетов, которые нужно удалить. Команда является полностью транзакционной, поэтому либо удаляются все пакеты, либо, если команда завершилась ошибкой, не удаляется ни один. Эта команда аналогична DROP SCHEMA с несколькими особенностями, о которых следует помнить:

  • Команду DROP PACKAGE можно использовать только для удаления схем, являющихся пакетами (то есть содержащих только функции, составные типы и функцию инициализации).

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

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

Все зависимости, упомянутые в данном разделе, являются зависимостями в стандартном для Postgres Pro смысле (см. Раздел 5.14). Например, использование в определении одной функции сигнатуры другой создаёт такую зависимость, а просто вызов одной функции из другой — нет. Обращение функции к переменным пакета вне пакета также не создаёт зависимостей (даже в случае функции-геттера).

J.1.4. Ограничения

  • В Oracle пакет представляет собой неделимую структуру, которую можно изменить только целиком, используя команду CREATE OR REPLACE. В Postgres Pro Enterprise пакет представляет собой схему, поэтому его элементы можно изменять по отдельности командами CREATE OR REPLACE PROCEDURE, CREATE OR REPLACE FUNCTION или ALTER TYPE. Таким образом, можно легко заменить функцию или тип, которые не имеют зависимостей, а не заменять весь пакет. Если заменяется пакет, необходимо вручную заменить все зависимые пакеты и функции.

  • Поскольку в Postgres Pro пакет является схемой, пользователь должен обладать правами для данной схемы:

    GRANT USAGE ON SCHEMA foo, bar TO hr_user;

    Если пакет должен выполняться с правами создателя, пользователь должен обладать всеми правами для данной схемы:

    GRANT ALL ON SCHEMA hr_main TO hr_user;

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

    GRANT SELECT demo.employee_tab TO hr_user;
  • В отличие от Oracle, где сохраняется вся информация о зависимостях пакетов и вообще любых объектов PL/SQL (триггеров, функций, процедур и т. д.), Postgres Pro не хранит зависимости на уровне тела функции и объявления переменных, поэтому эти зависимости невозможно отследить при изменении пакета.

  • В Oracle при нарушении зависимостей пакет получает статус INVALID. В этом случае пакет всё ещё существует в базе данных, но не работает. В отличие от этого, в Postgres Pro любой объект, созданный или изменённый в базе данных, должен быть рабочим. Таким образом, если в результате какой-либо операции в Postgres Pro пакет становится нерабочим (например, удаляется таблица, используемая в объявлении типа пакета), она не может быть выполнена — сначала этот пакет необходимо удалить, так как он не может оставаться нерабочим в базе данных.

  • При замене пакетов убедитесь, что никакие другие сеансы не используют их (другими словами, перед заменой пакетов остановите использующее их приложение). После этого перезапустите приложение, и обновлённые версии пакетов будут доступны во всех сеансах. Это ограничение вызвано отсутствием библиотечного кеша в Postgres Pro (в отличие от Oracle), поскольку каждый сервер кеширует код PL/pgSQL. Если бы пакет менялся в одном сеансе, а другой сеанс всё ещё работал бы со старой версией пакета, это могло бы привести к несогласованности, особенно если бы изменился список глобальных переменных пакета. На данный момент не реализованы глобальная блокировка для изменения пакета (аналог соответствующих механизмов блокировки в Oracle) и отслеживание изменений пакетов (аналог исключения «ORA-04068: existing state of packages has been discarded» (существующее состояние пакетов было сброшено) в Oracle).

  • В отличие от Oracle, пакеты не разделяются на спецификацию и тело.

  • Предложения, изменяющие объявление процедуры (такие как RESULT_CACHE, DETERMINISTIC, в Oracle) не поддерживаются. Директивы компилятора PL/SQL также не поддерживаются (например, INLINE pragma, UDF pragma и т. д.).