H.4. pljava

H.4.1. Описание
H.4.2. Установка
H.4.3. Дескриптор развёртывания SQLJ
H.4.4. Сопоставление функций
H.4.5. Триггеры
H.4.6. Сопоставление типов по умолчанию
H.4.7. Сопоставление SQL-типа с Java-классом
H.4.8. Создание скалярного пользовательского типа
H.4.9. Возврат сложных типов
H.4.10. Функции, возвращающие множества
H.4.11. Использование JDBC
H.4.12. Обработка исключений
H.4.13. Точки сохранения
H.4.14. Протоколирование
H.4.15. Функции SQLJ
H.4.16. Параметры конфигурации

H.4.1. Описание

Модуль pljava позволяет писать хранимые процедуры, триггеры и функции на языке Java и выполнять их обслуживающим процессом Postgres Pro.

pljava предоставляет следующие основные возможности:

  • Возможность писать функции, триггеры и пользовательские типы, используя последние версии Java.

  • Стандартизированные утилиты для установки и поддержания Java-кода в базе данных.

  • Стандартизированные сопоставления параметров и результатов. Поддержка скалярных и составных пользовательских типов (user-defined types, UDT), псевдотипов, массивов и множеств.

  • Встроенный высокопроизводительный драйвер JDBC, использующий внутренние SPI-функции Postgres Pro.

  • Поддержка метаданных для драйвера JDBC. Включены как DatabaseMetaData, так и ResultSetMetaData.

  • Интеграция с точками сохранения и обработкой исключений Postgres Pro.

  • Возможность использовать параметры IN, INOUT и OUT.

  • Два обработчика языка: javau (поведение функций не ограничено, только суперпользователи могут создавать их) и java (функции выполняются под управлением менеджера безопасности, который блокирует доступ к файловой системе, а пользователи, имеющие права создавать функции, настраиваются с помощью команд GRANT и REVOKE).

  • Слушатели (listeners) транзакций и точек сохранения, позволяющие выполнять код при фиксации или откате транзакции или точки сохранения.

Серверные функции и триггеры пишутся на языке Java с помощью подключённой напрямую эффективной версии стандартного JDBC API, который прозрачно поддерживается pljava, а также с помощью расширенных возможностей, которые есть в pljava API.

Функция или триггер в SQL-коде сопоставляется со статическим методом в Java-классе. Для выполнения функции соответствующий класс должен быть установлен в базе данных. pljava предоставляет набор функций, которые помогают устанавливать и поддерживать Java-классы.

Компилятор Java также создаёт дескриптор развёртывания SQLJ, содержащий SQL-операторы, которые должны быть выполнены при установке и удалении скомпилированного Java-кода обслуживающим процессом Postgres Pro.

Скомпилированный Java-код и файл дескриптора развёртывания хранятся вместе в архиве Java (JAR-файл). Функция sqlj.install_jar загружает код в обслуживающий процесс Postgres Pro и выполняет необходимые SQL-команды из дескриптора развёртывания, делая новые типы, функции и триггеры доступными для использования.

pljava реализует стандартизированный способ передачи параметров и возвращаемых значений. Сложные типы и множества передаются с помощью стандартного класса ResultSet драйвера JDBC. Большое внимание было уделено тому, чтобы не вводить никакие собственные интерфейсы, за исключением случаев крайней необходимости, чтобы Java-код, написанный с помощью pljava, был максимально независимым от баз данных.

Драйвер JDBC включён в pljava. Этот драйвер написан непосредственно поверх внутренних SPI-функций Postgres Pro. Этот драйвер очень важен, поскольку функции и триггеры очень часто повторно используют базу данных. В этих случаях они должны использовать те же границы транзакций, которые использовались вызывающим кодом.

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

Стандартный интерфейс Java Native Interface (JNI) используется при передаче вызовов из обслуживающего процесса в виртуальную машину Java и обратно.

H.4.2. Установка

Модуль pljava поставляется вместе с Postgres Pro Enterprise в виде отдельного пакета pljava-ent-15 (подробная инструкция по установке приведена в Главе 17).

H.4.3. Дескриптор развёртывания SQLJ

Функции sqlj.install_jar, sqlj.replace_jar и sqlj.remove_jar могут затрагивать дескриптор развёртывания, позволяя SQL-командам выполняться после установки JAR-файла или перед его удалением.

Дескриптор добавляется в JAR-файл как обычный текстовый файл. В манифесте JAR-файла должна быть запись, указывающая на то, что этот файл является дескриптором развёртывания SQLJ.

Name: deployment/examples.ddr
SQLJDeploymentDescriptor: TRUE

Такой файл может быть написан вручную в соответствии с форматом ниже, но обычно в исходный код добавляются определённые аннотации Java, как описано в разделе Автоматическая генерация SQL-кода. Затем компилятор генерирует файл дескриптора развёртывания во время компиляции исходного Java-кода. Скомпилированные классы и файл .ddr могут быть вместе помещены в JAR-файл.

Формат дескриптора развёртывания устанавливается стандартом ISO/IEC 9075-13:2003.

<файл_дескриптора> ::=
  SQLActions <левая_скобка> <правая_скобка> <знак_равенства>
  { [ <двойные_кавычки> <группа_действий> <двойные_кавычки>
    [ <запятая> <двойные_кавычки> <группа_действий> <двойные_кавычки> ] ] }

<группа_действий> ::=
    <действия_установки>
  | <действия_удаления>

<действия_установки> ::=
  BEGIN INSTALL [ <команда> <точка_с_запятой> ]... END INSTALL

<действия_удаления> ::=
  BEGIN REMOVE [ <команда> <точка_с_запятой> ]... END REMOVE

<команда> ::=
    <выражение_SQL>
  | <блок_исполнителей>

<выражение_SQL> ::= <компонент_SQL>...

<блок_исполнителей> ::=
  BEGIN <имя_исполнителя> <компонент_SQL>... END <имя_исполнителя>

<имя_исполнителя> ::= <идентификатор>

<компонент_SQL> ::= ! лексическая единица SQL, указанная под термином
                "<token>" в подпункте 5.2 в стандарте ISO/IEC 9075-2.

Если используются блоки исполнителей, pljava по умолчанию рассматривает только те, у которых имя исполнителя PostgreSQL (без учёта регистра). Пример дескриптора развёртывания:

SQLActions[] = {
  "BEGIN INSTALL
    CREATE FUNCTION javatest.java_getTimestamp()
      RETURNS timestamp
      AS 'org.postgresql.pljava.example.Parameters.getTimestamp'
      LANGUAGE java;
  END INSTALL",
  "BEGIN REMOVE
    DROP FUNCTION javatest.java_getTimestamp();
  END REMOVE"
}

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

H.4.4. Сопоставление функций

H.4.4.1. Функции

Java-функция объявляется с именем класса и с публичным статическим методом этого класса. Этот класс разрешается с помощью параметра classpath, заданного для той схемы, в которой объявлена эта функция. Если classpath не был указан для этой схемы, то используется схема public. Учтите, что загрузчик System ClassLoader всегда имеет приоритет. Переопределить классы, загруженные с помощью этого загрузчика, невозможно.

Можно объявить следующую функцию для доступа к статическому методу getProperty класса java.lang.System:

CREATE FUNCTION getsysprop(VARCHAR)
    RETURNS VARCHAR
    AS 'java.lang.System.getProperty'
    LANGUAGE java;

SELECT getsysprop('java.version');

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

CREATE FUNCTION getsysprop(VARCHAR)
    RETURNS VARCHAR
    AS 'java.lang.String=java.lang.System.getProperty(java.lang.String)'
    LANGUAGE java;

Этот способ объявления функции полезен, когда сопоставление по умолчанию является некорректным. pljava использует стандартное явное приведение типов Postgres Pro, когда SQL-тип параметра или возвращаемого значения не соответствует Java-типу, указанному в сопоставлении.

Обратите внимание, что явное приведение типов, которое здесь упоминается, осуществляется не путём создания фактического SQL-выражения CAST, а в основном аналогичными способами.

H.4.4.2. Автоматическая генерация SQL-кода

Наиболее простой способ написать объявление SQL-функции, которое соответствует Java-коду, — это поручить компилятору Java выполнить следующее:

public class Hello {
    @Function
    public static String hello(String toWhom) {
        return "Hello, " + toWhom + "!";
    }
}

При компиляции этой функции также создаётся дескриптор развёртывания, содержащий правильное объявление SQL-функций. Когда дескриптор включён в JAR-файл вместе со скомпилированным кодом, функция sqlj.install_jar модуля pljava создаёт объявление SQL-функции во время загрузки файла.

H.4.5. Триггеры

Сигнатура метода триггера предопределена. Метод триггера всегда должен возвращать тип void и иметь параметр, реализующий интерфейс org.postgresql.pljava.TriggerData. Интерфейс TriggerData предоставляет доступ к двум экземплярам java.sql.ResultSet: один экземпляр представляет старую строку, а другой — новую. Старая строка доступна только для чтения, а новая строка может быть обновлена.

Экземпляры ResultSet доступны только для триггеров, которые срабатывают для каждой строки. Триггеры удаления не имеют новой строки, а триггеры вставки не имеют старой строки. Только у триггеров обновления есть обе строки.

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

CREATE TABLE mdt (
    id int4,
    idesc text,
    moddate timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL);

CREATE FUNCTION moddatetime()
    RETURNS trigger
    AS 'org.postgresql.pljava.example.Triggers.moddatetime'
    LANGUAGE java;

CREATE TRIGGER mdt_moddatetime
    BEFORE UPDATE ON mdt
    FOR EACH ROW
    EXECUTE PROCEDURE moddatetime (moddate);

Соответствующий Java-код выглядит следующим образом:

/**
* Обновить время изменения при обновлении строки
*/
static void moddatetime(TriggerData td)
throws SQLException
{
    if(td.isFiredForStatement())
    throw new TriggerException(td, "can't process STATEMENT events");

    if(td.isFiredAfter())
    throw new TriggerException(td, "must be fired before event");

    if(!td.isFiredByUpdate())
    throw new TriggerException(td, "can only process UPDATE events");

    ResultSet _new = td.getNew();
    String[] args = td.getArguments();
    if(args.length != 1)
    throw new TriggerException(td, "one argument was expected");

    _new.updateTimestamp(args[0], new Timestamp(System.currentTimeMillis()));
}

H.4.6. Сопоставление типов по умолчанию

H.4.6.1. Скалярные типы

Скалярные типы сопоставляются напрямую. Таблица ниже показывает текущие сопоставления.

Таблица H.3. Сопоставление скалярных типов

Postgres ProJava
boolboolean
«char»byte
int2short
int4int
int8long
float4float
float8double
charjava.lang.String
varcharjava.lang.String
textjava.lang.String
namejava.lang.String
byteabyte[]
datejava.sql.Date
timejava.sql.Time (хранимое значение обрабатывается как местное время)
timetzjava.sql.Time
timestampjava.sql.Timestamp (хранимое значение обрабатывается как местное время)
timestamptzjava.sql.Timestamp

H.4.6.2. Скалярные типы массивов

Все скалярные типы могут быть представлены в виде массивов. Хотя Postgres Pro позволяет объявлять многомерные массивы с фиксированными размерами, pljava рассматривает все массивы как имеющие одно измерение (за исключением byte[], который сопоставляется с byte[][]). Причина этого заключается в том, что информация об измерениях и размерах нигде не хранится и никак не применяется.

Однако текущая реализация не устанавливает ограничений на размеры массивов — поведение такое же, как и для массивов с неопределённой длиной.

На самом деле текущая реализация также не контролирует объявленное количество измерений. Массивы, состоящие из элементов определённого типа, считаются массивами этого же типа независимо от размера и количества измерений. Таким образом, указание количества измерений или размера в команде CREATE TABLE не влияет на механизм работы с массивом.

Таблица H.4. Сопоставление скалярных типов массивов

Postgres ProJava
bool[]boolean[]
«char»[]byte[]
int2[]short[]
int4[]int[]
int8[]long[]
float4[]float[]
float8[]double[]
char[]java.lang.String[]
varchar[]java.lang.String[]
text[]java.lang.String[]
name[]java.lang.String[]
bytea[]byte[][]
date[]java.sql.Date[]
time[]java.sql.Time[] (хранимое значение обрабатывается как местное время)
timetz[]java.sql.Time[]
timestamp[]java.sql.Timestamp[] (хранимое значение обрабатывается как местное время)
timestamptz[]java.sql.Timestamp[]

H.4.6.3. Типы доменов

Тип домена сопоставляется в соответствии с типом, который он расширяет, если только не установлено специальное сопоставление для переопределения этого поведения.

H.4.6.4. Псевдотипы

Таблица H.5. Сопоставление псевдотипов

Postgres ProJava
«any»java.lang.Object
anyelementjava.lang.Object
anyarrayjava.lang.Object[]
cstringjava.lang.String
recordjava.sql.ResultSet
triggerorg.postgresql.pljava.TriggerData (обратитесь к разделу Триггеры)

H.4.6.5. Обработка NULL для примитивов

Скалярные типы, которые сопоставляются с примитивами Java, не могут передаваться как значения NULL. Чтобы разрешить это, такие типы могут иметь альтернативное сопоставление. Можно сделать это сопоставление, указав его в явном виде в ссылке на метод.

CREATE FUNCTION trueIfEvenOrNull(integer)
    RETURNS bool
    AS 'foo.fee.Fum.trueIfEvenOrNull(java.lang.Integer)'
    LANGUAGE java;

В Java-коде надо написать примерно так:

package foo.fee;

public class Fum
{
    static boolean trueIfEvenOrNull(Integer value)
    {
    return (value == null)
        ? true
        : (value.intValue() % 1) == 0;
    }
}

Следующие операторы должны выдавать true:

SELECT trueIfEvenOrNull(NULL);
SELECT trueIfEvenOrNull(4);

Чтобы вернуть значения NULL из Java-метода, используйте тип объектов, который соответствует примитиву (например, возвращайте java.lang.Integer вместо int). Механизм сопоставления pljava найдёт этот метод в любом случае. Поскольку в Java не может быть разных типов возвращаемых значений для методов с одинаковыми именами, это не создаёт никаких неоднозначностей.

Также значения NULL могут быть в массивах. pljava обрабатывает их тем же образом, что и обычные примитивы, например, можно объявить методы, которые используют параметр java.lang.Integer[] вместо параметра int[].

H.4.6.6. Составные типы

Составной тип по умолчанию передаётся как экземпляр java.sql.ResultSet, доступный только для чтения и содержащий одну строку. ResultSet уже установлен на эту строку, поэтому вызывать next() не нужно. Значения составного типа извлекаются с помощью стандартных методов считывания (геттеров) ResultSet.

CREATE TYPE compositeTest
    AS(base integer, incbase integer, ctime timestamptz);

CREATE FUNCTION useCompositeTest(compositeTest)
    RETURNS VARCHAR
    AS 'foo.fee.Fum.useCompositeTest'
    IMMUTABLE LANGUAGE java;

В классе Fum добавляется следующий статический метод:

public static String useCompositeTest(ResultSet compositeTest)
throws SQLException
{
    int base = compositeTest.getInt(1);
    int incbase = compositeTest.getInt(2);
    Timestamp ctime = compositeTest.getTimestamp(3);
    return "Base = \\"" + base +
    "\\", incbase = \\"" + incbase +
    "\\", ctime = \\"" + ctime + "\\"";
}

H.4.6.7. Сопоставление по умолчанию

Типы, у которых нет сопоставлений, на текущий момент сопоставляются с типом java.lang.String. При преобразовании значений используются стандартные функции textin и textout Postgres Pro, зарегистрированные для соответствующих типов.

H.4.7. Сопоставление SQL-типа с Java-классом

С помощью pljava можно установить сопоставление между произвольным типом и Java-классом. Для этого необходимо выполнить следующие предварительные требования:

  • Необходимо знать структуру хранения SQL-типа, который сопоставляется.

  • Java-класс, который сопоставляется, должен реализовывать интерфейс java.sql.SQLData.

H.4.7.1. Сопоставление существующего SQL-типа с Java-классом

В этом примере показано, как сопоставить геометрический тип точки Postgres Pro с Java-классом. Точка хранится в виде двух значений float8: координаты x и y.

Когда известна структура хранения типа точки, можно создать реализацию java.sql.SQLData, которая использует класс java.sql.SQLInput для чтения данных и класс java.sql.SQLOutput для записи.

package org.postgresql.pljava.example;

import java.sql.SQLData;
import java.sql.SQLException;
import java.sql.SQLInput;
import java.sql.SQLOutput;

public class Point implements SQLData {
    private double m_x;
    private double m_y;
    private String m_typeName;

    public String getSQLTypeName() {
    return m_typeName;
    }

    public void readSQL(SQLInput stream, String typeName) throws SQLException {
    m_x = stream.readDouble();
    m_y = stream.readDouble();
    m_typeName = typeName;
    }

    public void writeSQL(SQLOutput stream) throws SQLException {
    stream.writeDouble(m_x);
    stream.writeDouble(m_y);
    }

    /* Значимый код, который действительно что-то делает с этим типом,
    * был намеренно опущен
    */
}

Наконец, установите сопоставление типов с помощью команды add_type_mapping:

SELECT sqlj.add_type_mapping('point', 'org.postgresql.pljava.example.Point');

Теперь можно использовать этот новый класс. pljava сопоставляет любой параметр с типом точки с классом org.postgresql.pljava.example.Point.

H.4.7.2. Создание составного пользовательского типа и сопоставление его с Java-классом

Здесь приведён пример сложного типа, созданного как составной пользовательский тип.

CREATE TYPE javatest.complextuple AS (x float8, y float8);

SELECT sqlj.add_type_mapping('javatest.complextuple',
        'org.postgresql.pljava.example.ComplexTuple');
package org.postgresql.pljava.example;

import java.sql.SQLData;
import java.sql.SQLException;
import java.sql.SQLInput;
import java.sql.SQLOutput;

public class ComplexTuple implements SQLData {
    private double m_x;
    private double m_y;
    private String m_typeName;

    public String getSQLTypeName()
    {
    return m_typeName;
    }

    public void readSQL(SQLInput stream, String typeName) throws SQLException
    {
    m_typeName = typeName;
    m_x = stream.readDouble();
    m_y = stream.readDouble();
    }

    public void writeSQL(SQLOutput stream) throws SQLException
    {
    stream.writeDouble(m_x);
    stream.writeDouble(m_y);
    }

    /* Значимый код, который действительно что-то делает с этим типом,
    * был намеренно опущен
    */
}

H.4.7.3. Автоматическая генерация SQL-кода

SQL-код, показанный выше для этого примера, будет написан компилятором Java, если для класса ComplexTuple указать аннотацию, что этот класс является «сопоставляемым пользовательским типом», и задать необходимое имя и структуру SQL.

@MappedUDT(schema="javatest", name="complextuple",
structure={"x float8", "y float8"})
public class ComplexTuple implements SQLData {
...

Генерация SQL-кода снижает нагрузку на поддержание определений в двух местах.

H.4.8. Создание скалярного пользовательского типа

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

С точки зрения SQL создание нового скалярного типа с помощью Java-функций очень похоже на создание типа с помощью C-функций, но на самом деле отличается, если посмотреть на фактическую реализацию. Java требует, чтобы сопоставление между Java-классом и соответствующим SQL-типом выполнялось с помощью интерфейсов java.sql.SQLData, java.sql.SQLInput и java.sql.SQLOutput, используемых pljava. Кроме того, система типов Postgres Pro требует, чтобы каждый тип имел текстовое представление.

В примере ниже показано, как создать тип с именем javatest.complex. Имя соответствующего Java-класса будет org.postgresql.pljava.example.ComplexScalar.

Java-класс для скалярного пользовательского типа должен реализовывать интерфейс java.sql.SQLData. Кроме того, он также должен реализовывать метод parse(), который создаёт и возвращает экземпляр этого класса, и метод toString(), который возвращает то, что может быть разобрано методом parse().

package org.postgresql.pljava.example;

import java.io.IOException;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.sql.SQLData;
import java.sql.SQLException;
import java.sql.SQLInput;
import java.sql.SQLOutput;
import java.util.logging.Logger;

import org.postgresql.pljava.annotation.Function;
import org.postgresql.pljava.annotation.SQLType;
import org.postgresql.pljava.annotation.BaseUDT;

import static org.postgresql.pljava.annotation.Function.Effects.IMMUTABLE;
import static
        org.postgresql.pljava.annotation.Function.OnNullInput.RETURNS_NULL;

@BaseUDT(schema="javatest", name="complex",
        internalLength=16, alignment=BaseUDT.Alignment.DOUBLE)
public class ComplexScalar implements SQLData
{
    private double m_x;
    private double m_y;
    private String m_typeName;

    @Function(effects=IMMUTABLE, onNullInput=RETURNS_NULL)
    public static ComplexScalar parse(String input, String typeName)
    throws SQLException
    {
    try
    {
        StreamTokenizer tz = new StreamTokenizer(new StringReader(input));
        if(tz.nextToken() == '('
        && tz.nextToken() == StreamTokenizer.TT_NUMBER)
        {
            double x = tz.nval;
            if(tz.nextToken() == ','
            && tz.nextToken() == StreamTokenizer.TT_NUMBER)
            {
                double y = tz.nval;
                if(tz.nextToken() == ')')
                {
                return new ComplexScalar(x, y, typeName);
                }
            }
        }
        throw new SQLException("Unable to parse complex from string \""
        + input + '"');
    }
    catch(IOException e)
    {
        throw new SQLException(e.getMessage());
    }
    }

    public ComplexScalar()
    {
    }

    public ComplexScalar(double x, double y, String typeName)
    {
    m_x = x;
    m_y = y;
    m_typeName = typeName;
    }

    @Override
    public String getSQLTypeName()
    {
    return m_typeName;
    }

    @Function(effects=IMMUTABLE, onNullInput=RETURNS_NULL)
    @Override
    public void readSQL(SQLInput stream, String typeName) throws SQLException
    {
    m_x = stream.readDouble();
    m_y = stream.readDouble();
    m_typeName = typeName;
    }

    @Function(effects=IMMUTABLE, onNullInput=RETURNS_NULL)
    @Override
    public void writeSQL(SQLOutput stream) throws SQLException
    {
    stream.writeDouble(m_x);
    stream.writeDouble(m_y);
    }

    @Function(effects=IMMUTABLE, onNullInput=RETURNS_NULL)
    @Override
    public String toString()
    {
    s_logger.info(m_typeName + " toString");
    StringBuffer sb = new StringBuffer();
    sb.append('(');
    sb.append(m_x);
    sb.append(',');
    sb.append(m_y);
    sb.append(')');
    return sb.toString();
    }

    /* Значимый код, который действительно что-то делает с этим типом,
    * был намеренно опущен
    */
}

Для самого класса указана аннотация @BaseUDT, задающая SQL-схему, имя, а также длину и выравнивание, необходимые для внутренней формы хранения.

Так как компилятор знает, что класс является BaseUDT, он ожидает наличие методов parse(), toString(), readSQL() и writeSQL() и будет генерировать корректный SQL-код, чтобы объявить их в виде функций для Postgres Pro. Аннотации @Function здесь используются только, чтобы объявить функцию как постоянную, и определить поведение при входном значении NULL, поскольку эти значения не используются по умолчанию при объявлении функции.

H.4.9. Возврат сложных типов

pljava обрабатывает возвращаемое значение сложного типа как параметр IN или OUT. Если объявлена функция, которая возвращает сложный тип, нужно использовать Java-метод с логическим возвращаемым значением и с последним параметром типа java.sql.ResultSet, добавленным после всех видимых параметров метода. Выходной параметр будет инициализирован экземпляром ResultSet, который содержит одну строку и может обновляться.

CREATE FUNCTION createComplexTest(int, int)
  RETURNS complexTest
  AS 'foo.fee.Fum.createComplexTest'
  IMMUTABLE LANGUAGE java;

Механизм сопоставления метода pljava теперь будет находить следующий метод в классе foo.fee.Fum:

public static boolean complexReturn(int base, int increment, ResultSet receiver)
throws SQLException
{
  receiver.updateInt(1, base);
  receiver.updateInt(2, base + increment);
  receiver.updateTimestamp(3, new Timestamp(System.currentTimeMillis()));
  return true;
}

Возвращаемое значение указывает, следует ли считать параметр receiver актуальным кортежем (true) или NULL (false).

H.4.10. Функции, возвращающие множества

Возвращать множества довольно сложно. Не нужно сначала строить всё множество, а затем возвращать его, поскольку большие множества требуют излишних ресурсов. Лучше создавать по одной строке за один раз. Именно этого и ожидает обслуживающий процесс Postgres Pro от функции, которая возвращает SETOF <type>. <type> может быть скалярным типом, например int, float или varchar, сложным типом или типом RECORD.

H.4.10.1. Возврат множества скалярного типа

Чтобы вернуть множество скалярного типа, нужно создать Java-метод, который возвращает реализацию интерфейса java.util.Iterator.

CREATE FUNCTION javatest.getNames()
  RETURNS SETOF varchar
  AS 'foo.fee.Bar.getNames'
  IMMUTABLE LANGUAGE java;

Соответствующий Java-класс:

package foo.fee;
import java.util.Iterator;

import org.postgresql.pljava.annotation.Function;
import static org.postgresql.pljava.annotation.Function.Effects.IMMUTABLE;

public class Bar
{
    @Function(schema="javatest", effects=IMMUTABLE)
    public static Iterator<String> getNames()
    {
        ArrayList<String> names = new ArrayList<>();
        names.add("Lisa");
        names.add("Bob");
        names.add("Bill");
        names.add("Sally");
        return names.iterator();
    }
}

H.4.10.2. Возврат множества сложного типа

Метод, возвращающий множество сложного типа, должен использовать либо интерфейс org.postgresql.pljava.ResultSetProvider, либо org.postgresql.pljava.ResultSetHandle. Причина наличия двух интерфейсов заключается в необходимости оптимальной обработки для двух различных сценариев использования. Первый интерфейс отлично подходит, когда нужно динамически создавать каждую строку, которая должна быть возвращена функцией SETOF. Второй интерфейс имеет смысл, когда нужно вернуть результат выполненного запроса.

H.4.10.2.1. Использование интерфейса ResultSetProvider

Этот интерфейс имеет два метода: boolean assignRowValues(java.sql.ResultSet tupleBuilder, int rowNumber) и void close(). Анализатор запросов Postgres Pro будет последовательно вызывать метод assignRowValues, пока он не вернёт false или пока анализатор не решит, что ему больше не нужно строк. Затем он вызовет метод close.

Этот интерфейс можно использовать следующим образом:

CREATE FUNCTION javatest.listComplexTests(int, int)
  RETURNS SETOF complexTest
  AS 'foo.fee.Fum.listComplexTest'
  IMMUTABLE LANGUAGE java;

Эта функция сопоставляется со статическим Java-методом, который возвращает экземпляр, реализующий интерфейс ResultSetProvider.

public class Fum implements ResultSetProvider
{
  private final int m_base;
  private final int m_increment;
  public Fum(int base, int increment)
  {
    m_base = base;
    m_increment = increment;
  }
  public boolean assignRowValues(ResultSet receiver, int currentRow)
  throws SQLException
  {
    // Остановить при достижении 12 строк
    //
    if(currentRow >= 12)
      return false;
    receiver.updateInt(1, m_base);
    receiver.updateInt(2, m_base + m_increment * currentRow);
    receiver.updateTimestamp(3, new Timestamp(System.currentTimeMillis()));
    return true;
  }
  public void close()
  {
    // В этом примере ничего не требуется
  }
  @Function(effects=IMMUTABLE, schema="javatest", type="complexTest")
  public static ResultSetProvider listComplexTests(int base, int increment)
  throws SQLException
  {
    return new Fum(base, increment);
  }
}

Метод listComplexTests(int base, int increment) вызывается один раз. Он может вернуть NULL, если нет доступных результатов, или экземпляр ResultSetProvider. Здесь класс Fum реализует этот интерфейс, поэтому он может вернуть экземпляр самого себя. Затем несколько раз будет вызываться метод assignRowValues(ResultSet receiver, int currentRow), пока он не вернёт false. В это время будет вызван метод close.

В некоторых случаях параметр currentRow может быть полезен, а в других — не нужен. При первом вызове для параметра будет передаваться значение 0, при каждом последующем вызове значение будет увеличиваться на 1. Если экземпляр ResultSetProvider возвращает результаты из какого-либо источника (например, Iterator), который запоминает свою позицию, то параметр currentRow может просто игнорироваться.

H.4.10.2.2. Использование интерфейса ResultSetHandle

Этот интерфейс похож на интерфейс ResultSetProvider тем, что у него тоже есть метод close, который вызывается в конце. Но вместо метода, вызываемого анализатором для формирования одной строки за раз, у этого интерфейса есть метод, который возвращает ResultSet. Анализатор запросов будет перебирать это множество и передавать его содержимое по одному кортежу за раз, пока функция next не вернёт false или пока анализатор не решит, что больше строк не требуется.

Здесь приведён пример, в котором выполняется запрос с использованием оператора, полученного с помощью подключения по умолчанию. SQL-код выглядит так:

CREATE FUNCTION javatest.listSupers()
  RETURNS SETOF pg_user
  AS 'org.postgresql.pljava.example.Users.listSupers'
  LANGUAGE java;

CREATE FUNCTION javatest.listNonSupers()
  RETURNS SETOF pg_user
  AS 'org.postgresql.pljava.example.Users.listNonSupers'
  LANGUAGE java;

Java-код выглядит так:

public class Users implements ResultSetHandle
{
  private final String m_filter;
  private Statement m_statement;

  public Users(String filter)
  {
    m_filter = filter;
  }

  public ResultSet getResultSet()
  throws SQLException
  {
    m_statement = DriverManager.getConnection("jdbc:default:connection")
      .createStatement();
    return m_statement.executeQuery("SELECT * FROM pg_user WHERE " + m_filter);
  }

  public void close()
  throws SQLException
  {
    m_statement.close();
  }

  @Function(schema="javatest", type="pg_user")
  public static ResultSetHandle listSupers()
  {
    return new Users("usesuper = true");
  }

  @Function(schema="javatest", type="pg_user")
  public static ResultSetHandle listNonSupers()
  {
    return new Users("usesuper = false");
  }
}

H.4.11. Использование JDBC

pljava содержит драйвер JDBC, который сопоставляется с SPI-функциями Postgres Pro. Подключение, которое сопоставляется с текущей транзакцией, можно получить с помощью следующего оператора:

Connection conn = DriverManager.getConnection("jdbc:default:connection");

Теперь можно подготавливать и выполнять операторы, как и с любым другим подключением JDBC. Есть несколько ограничений:

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

    • commit()

    • rollback()

    • setAutoCommit()

    • setTransactionIsolation()

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

  • Для экземпляров ResultSet, которые возвращаются функцией executeQuery(), всегда используются константы FETCH_FORWARD (считывание в прямом порядке) и CONCUR_READ_ONLY (доступ только для чтения).

  • Интерфейс CallableStatement (для хранимых процедур) ещё не реализован.

  • Типы Clob/Blob нуждаются в доработке. Типы byte[] и String хорошо работают для типов bytea и text, соответственно. Планируется более эффективное сопоставление, при котором сам массив не копируется.

H.4.12. Обработка исключений

В обслуживающем процессе Postgres Pro можно перехватывать и обрабатывать исключения точно так же, как и любое другое исключение. Серверная структура ErrorData представлена в виде свойства класса ServerException, унаследованного от java.sql.SQLException. Механизм try/catch в Java синхронизирован с механизмом в обслуживающем процессе.

Примечание

По нескольким причинам в настоящее время не рекомендуется ссылаться на ServerException и ErrorData из кода, а в будущем это может стать невозможным. В будущих версиях ожидается улучшение этого механизма. До тех пор рекомендуется по возможности использовать только стандартный класс java.sql.SQLException, предоставляемый Java API, и его стандартные атрибуты (такие как SQLState).

pljava всегда будет перехватывать исключения, которые вы не перехватываете. Они вызовут ошибку Postgres Pro, и сообщение будет записано в журнал с помощью утилит протоколирования Postgres Pro. Также будет выводиться трассировка стека исключения, если для параметра конфигурации log_min_messages установлено значение DEBUG1 или ниже.

Примечание

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

H.4.13. Точки сохранения

Для точек сохранения Postgres Pro можно использовать стандартные методы setSavepoint() и releaseSavepoint() интерфейса java.sql.Connection. Применяются следующие ограничения:

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

  • Точка сохранения не должна существовать дольше, чем функция, в которой она была установлена.

Здесь под «функцией» имеется в виду функция pljava, которая вызывается из SQL-кода. Ограничения не запрещают организовывать Java-код в несколько методов, но точка сохранения не может существовать после окончательного возврата из Java-кода в вызывающий SQL-код.

H.4.14. Протоколирование

pljava использует стандартный класс java.util.logging.Logger. Поэтому можно написать код так:

Logger.getAnonymousLogger().info(
    "Time is " + new Date(System.currentTimeMillis()));

В настоящее время Logger жёстко привязан к обработчику, который сопоставляет уровень протоколирования, указанный в параметре конфигурации log_min_messages, с корректным уровнем Logger и выводит все сообщения с помощью серверной функции ereport().

Важно отметить, что методы Logger позволяют быстро отбросить любое сообщение, которое протоколируется на более детальном уровне, чем уровень, сопоставленный из параметра Postgres Pro во время первого использования pljava в текущем сеансе. Такие сообщения даже никогда не доходят до функции ereport(), даже если значение параметра Postgres Pro позже изменяется.

Таким образом, если ожидаемые сообщения из Java-кода не показываются, убедитесь, что параметры Postgres Pro настроены достаточным образом во время первого использования pljava в сеансе, чтобы Java-код не отбрасывал эти сообщения. После запуска pljava параметры могут изменяться по мере необходимости и будут обычным образом управлять тем, что функция ereport() делает с сообщениями, которые pljava доставляет в неё.

Уровень для отсечки в Java устанавливается на основе более точного значения из значений параметров log_min_messages и client_min_messages.

Следующее сопоставление применяется между уровнями Logger и уровнями Postgres Pro:

Таблица H.6. Сопоставление уровней протоколирования

Уровень java.util.logging.LevelУровень Postgres Pro
SEVEREERROR
WARNINGWARNING
INFOINFO
FINEDEBUG1
FINERDEBUG2
FINESTDEBUG3

H.4.15. Функции SQLJ

sqlj.install_jar

Загружает JAR-файл из местоположения, указанного в URL, в репозиторий SQLJ. Если JAR-файл с таким именем уже существует в репозитории, возникает ошибка.

Использование:

SELECT sqlj.install_jar(<url_jar>, <имя_jar>, <развернуть>);

Параметры:

  • url_jar: URL, указывающий местоположение JAR-файла, который должен быть загружен.

  • имя_jar: имя, по которому можно обращаться к JAR-файлу после его загрузки.

  • развернуть: true, если JAR-файл должен быть развёрнут в соответствии с дескриптором развёртывания, в противном случае false.

sqlj.replace_jar

Заменяет загруженный JAR-файл другим JAR-файлом. Используйте его, чтобы обновить уже загруженные файлы. Если JAR-файл не найден, возникает ошибка.

Использование:

SELECT sqlj.replace_jar(<url_jar>, <имя_jar>, <повторно_развернуть>);

Параметры:

  • url_jar: URL, указывающий местоположение JAR-файла, который должен быть загружен.

  • имя_jar: имя JAR-файла, который должен быть заменён.

  • повторно_развернуть: true, если JAR-файл должен быть удалён в соответствии с дескриптором развёртывания старого JAR-файла и заново развёрнут в соответствии с дескриптором развёртывания нового JAR-файла, в противном случае false.

sqlj.remove_jar

Удаляет JAR-файл из репозитория JAR. Любое значение classpath, которое ссылается на этот JAR-файл, обновляется соответствующим образом. Если JAR-файл не найден, возникает ошибка.

Использование:

SELECT sqlj.remove_jar(<имя_jar>, <отменить_развёртывание>);

Параметры:

  • имя_jar: имя JAR-файла, который должен быть удалён.

  • отменить_развёртывание: true, если для JAR-файла нужно отменить развёртывание в соответствии с дескриптором развёртывания, в противном случае false.

sqlj.get_classpath

Возвращает значение classpath, которое было задано для указанной схемы. Если для схемы не задано значение classpath , возвращается NULL. Если указанная схема не существует, возникает ошибка.

Использование:

SELECT sqlj.get_classpath(<схема>);

Параметры:

  • схема: имя схемы.

sqlj.set_classpath

Определяет значение classpath для указанной схемы. classpath представляет собой список имён JAR-файлов, разделённых двоеточиями. Если указанная схема не существует или один или несколько имён JAR-файлов обращаются к несуществующим файлам, возникает ошибка.

Использование:

SELECT sqlj.set_classpath(<схема>, <classpath>);

Параметры:

  • схема: имя схемы.

  • classpath: список имён JAR-файлов, разделённых двоеточиями.

sqlj.add_type_mapping

Устанавливает сопоставление между SQL-типом и Java-классом. После создания сопоставления параметры и возвращаемые значения сопоставляются соответствующим образом. За подробной информацией обратитесь к разделу Сопоставление SQL-типа с Java-классом.

Использование:

SELECT sqlj.add_type_mapping(<тип_sql>, <класс_java>);

Параметры:

  • тип_sql: имя SQL-типа. Имя может быть дополнено схемой (пространством имён). Если схема опущена, она будет определена в соответствии с текущим значением параметра search_path.

  • класс_java: имя класса. Класс должен быть найден по значению classpath, которое актуально для текущей схемы.

sqlj.drop_type_mapping

Удаляет сопоставление между SQL-типом и Java-классом.

Использование:

SELECT sqlj.drop_type_mapping(<тип_sql>);

Параметры:

  • тип_sql: имя SQL-типа. Имя может быть дополнено схемой (пространством имён). Если схема опущена, она будет определена в соответствии с текущим значением параметра search_path.

Примечание

Функции install_jar и replace_jar принимают URL (к которому у сервера должен быть доступ) к JAR-файлу. Используя правила для URL JAR-файлов, можно также создать URL, который обращается к JAR-файлу внутри другого JAR-файла. Например:

jar:file:outer.jar!/inner.jar

Однако кеширование «внешнего» JAR-файла может помешать попыткам заменить или перезагрузить более новую версию в рамках одного и того же сеанса.

H.4.16. Параметры конфигурации

Несколько параметров конфигурации могут влиять на работу pljava, включая некоторые общие параметры Postgres Pro, а также собственные параметры pljava.

H.4.16.1. Параметры Postgres Pro

check_function_bodies

Влияет на то, насколько строго pljava проверяет новую функцию во время выполнения команды CREATE FUNCTION или при установке JAR-файла, если среди его действий развёртывания выполняется CREATE FUNCTION. Если для параметра check_function_bodies установлено значение on, pljava проверяет, что задействованные класс и метод могут быть загружены и сопоставлены. Если задействованный класс зависит от классов в других JAR-файлах, то эти файлы тоже должны быть установлены и указаны в classpath, поэтому загрузка JAR-файлов с зависимостями в неправильном порядке может повлечь ошибки проверки. Если для параметра check_function_bodies установлено значение off, во время выполнения CREATE FUNCTION проверяется только базовый синтаксис, поэтому можно объявлять функции и устанавливать JAR-файлы в любом порядке, но при этом откладывая любые ошибки, связанные с неразрешёнными зависимостями, до более позднего момента, когда эти функции будут использованы.

dynamic_library_path

Влияет на то, где можно найти встроенные объекты кода pljava, если для команды LOAD не указан полный путь.

server_encoding

Влияет на все текстовые и символьные строки, которыми обмениваются Postgres Pro и Java. Строго рекомендуется указывать UTF8 для кодировки баз данных и сервера. Если используется другая кодировка, то это должна быть любая из доступных полностью определённых кодировок символов. В частности, псевдокодировка SQL_ASCII Postgres Pro не полностью определяет, что представляют любые значения за рамками ASCII. Она применима, но имеет ограничения.

H.4.16.2. Параметры pljava

pljava.allow_unenforced

Используется только при запуске pljava без применения политик безопасности и представляет собой список названий языков (например, javau и java), на которых разрешается выполнение функций. Этот параметр имеет пустое значение по умолчанию. Изменять его нужно с осторожностью.

pljava.allow_unenforced_udt

Используется только при запуске pljava без применения политик безопасности и определяет, разрешено ли выполнять функции преобразования данных, связанные с сопоставляемыми пользовательскими типами pljava. Значение по умолчанию — off. Изменять его нужно с осторожностью.

pljava.enable

Установка для этого параметра значения off предотвращает завершение запуска pljava до тех пор, пока для параметра позже не будет установлено значение on. Это может быть полезно в целях отладки.

pljava.implementors

Список «имён исполнителей», которые распознаёт pljava при обработке дескрипторов развёртывания внутри устанавливаемого или удаляемого JAR-файла. Дескрипторы развёртывания могут содержать команды без имени исполнителя, выполняемые всегда, или команды с именем исполнителя, выполняемые только в системах, распознающих это имя. По умолчанию этот список содержит только значение postgresql. Дескриптор развёртывания, содержащий команды с другими именами исполнителей, может обеспечивать элементарную форму условного выполнения, если более ранние команды изменяют этот список имён. Элементы этого списка разделены запятыми. Элементы, не являющиеся обычными идентификаторами, должны быть заключены в двойные кавычки.

pljava.java_thread_pg_entry

Выбор из значений allow, error, block или throw, контролирующих управление потоками pljava. В Java активно используется многопоточность, в то время как доступ к Postgres Pro одновременно несколькими потоками бывает невозможен. По историческим причинам pljava использует значение allow, которое сериализует доступ потоков Java к Postgres Pro, позволяя другому потоку Java получать доступ только тогда, когда текущий поток вызывается или возвращается в Java-код . В pljava ранее использовались финализаторы Java-объектов, что требовало такого подхода, поскольку финализаторы выполняются в собственном потоке.

Сам модуль pljava больше не требует возможности для потоков получать доступ к Postgres Pro, кроме исходного основного потока. Однако пользовательский код, разработанный для pljava может по-прежнему полагаться на такую возможность. Чтобы проверить это, можно использовать значение error или throw, и при любой попытке потока, отличного от основного, получить доступ к Postgres Pro возникнет исключение (и трассировка стека, записанная в стандартный канал ошибок сервера). При уверенности, что отсутствует код, которому требуется входить в Postgres Pro, за исключением основного потока, можно использовать значение block. Это позволит избежать частых получений и освобождений блокировок pljava при переходе основного потока между Postgres Pro и Java и просто навсегда заблокирует любой другой поток Java, который пытается войти в Postgres Pro. Это значение является эффективным, но может приводить к заблокированным потокам или взаимоблокировкам в обслуживающем процессе, если используется с кодом, который пытается получить доступ к Postgres Pro из более чем одного потока.

Значение throw очень похоже на значение error, но более эффективно. При значении error попытка входа неправильным потоком обнаруживается в C-коде только после операции блокировки и вызывается через JNI. При значении throw операции блокировки опускаются, и попытка входа неправильным потоком не приводит к вызову JNI, а исключение выводится напрямую в Java.

pljava.libjvm_location

Используется pljava для загрузки среды выполнения Java. Полный путь к разделяемому объекту libjvm. Версия библиотеки Java, на которую указывает этот параметр, определяет, может ли pljava запускаться с применением политик безопасности или без них.

pljava.module_path

Путь к модулю, который будет передан системному загрузчику классов Java. Значение по умолчанию вычисляется из конфигурации Postgres Pro и обычно является корректным, если только файлы pljava не установлены в нестандартном месте. Если путь необходимо установить явно, то должно быть как минимум две записи (и обычно только две): JAR-файл с API pljava и JAR-файл с внутренним устройством pljava.

pljava.policy_urls

Используется только при запуске pljava с применением политик безопасности. При запуске без применения политик этот параметр игнорируется. Он представляет собой список URL к файлам политик безопасности Java, которые определяют права, доступные функциям pljava. Каждый URL должен быть заключён в двойные кавычки. Если двойные кавычки являются частью URL, то для них можно указать двойные кавычки два раза (в стиле SQL) или %22, как в соглашении URL. В качестве разделителя между URL в двойных кавычках используется запятая.

Файл java.security инсталляции Java обычно определяет следующие расположения файлов политик:

  1. Общесистемная политика от поставщика Java, достаточная для штатного функционирования самой среды выполнения Java.

  2. Пользовательское местоположение, в котором при наличии файла политик он может дополнять политику из общесистемного файла.

Список из параметра pljava.policy_urls изменяет список из инсталляции Java. По умолчанию это происходит после первой записи, при этом сохраняется общесистемная политика, поставляемая Java, но заменяется привычный пользовательский файл. Возможно, этого файла нет в домашнем каталоге пользователя postgres, а если есть, то он не предназначен для pljava.

Любая запись в этом списке может начинаться с n = (внутри кавычек) для положительного целого числа n, чтобы указать, какую запись в списке местоположений политик Java нужно заменить. Запись 1 соответствует общесистемной политике, 2 — привычному пользовательскому файлу. URL, не имеющие префикса n =, рассматриваются последовательно. Если у первой записи тоже нет префикса, подразумевается 2=.

Последняя запись = (в требуемых двойных кавычках) предотвращает использование оставшихся записей в настроенном списке Java.

Значение по умолчанию — "file:${org.postgresql.sysconfdir}/pljava.policy","=".

pljava.release_lingering_savepoints

Определяет, как возвращаемое значение из функции pljava обрабатывает точки сохранения, которые были созданы в функции, но не были освобождены (аналог «фиксации» для точек сохранения) или откачены. При значении off (по умолчанию) они откатываются. При значении on они освобождаются/фиксируются. По возможности вместо установки для этого параметра значения on безопаснее было бы исправить функцию таким образом, чтобы освобождать её точки сохранения, когда это необходимо.

pljava.statement_cache_size

Количество последних подготовленных операторов, которые pljava может держать открытыми.

pljava.vmoptions

Любые параметры для передачи среде выполнения Java в том же формате, что и параметры, описанные в документации для команды java. Строка разбивается по пробелам, если только пробелы не заключены в одинарные или двойные кавычки. При использовании обратной косой черты следующий за ней символ считается буквально, но сама обратная косая черта остаётся в строке, поэтому не все значения могут быть выражены с помощью этих правил. Если кодировка сервера отличается от UTF8, только символы ASCII должны быть использованы в параметре pljava.vmoptions.