Статьи

Введение в PostgreSQL PL / Java

Современные базы данных позволяют хранить хранимые процедуры на разных языках. Одним из обычно реализуемых языков является java.NB, в этой статье обсуждается реализация java для PostgreSQL. Детали будут различаться в зависимости от других баз данных, но концепции будут такими же.

Установка PL / Java

Установка PL / Java в системе Ubuntu проста. Сначала я создам новый шаблон template_java , чтобы я мог создавать базы данных без расширений pl / java.

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

1
2
3
4
5
6
# apt-get install postgresql-9.1
# apt-get install postgresql-9.1-pljava-gcj
 
$ createdb template_java
$ psql -d template_java -c 'update db_database set datistemplate='t' where datnam='template_java''
$ psql -d template_java -f /usr/share/postgresql-9.1-pljava/install.sql


Ограничения

Предварительно упакованный пакет Ubuntu использует Java-реализацию Gnu GCJ, а не стандартную реализацию OpenJDK или Sun. GCJ компилирует исходные файлы Java в собственный объектный код вместо байтового кода. Самые последние версии PL / Java являются «доверенными» — на них можно положиться, чтобы они оставались в своей песочнице. Среди прочего это означает, что вы не можете получить доступ к файловой системе на сервере.

Если вам нужно сломать доверие, есть второй язык, «javaU», который можно использовать. Ненадежные функции могут быть созданы только суперпользователем базы данных.

Что еще более важно, эта реализация является однопоточной. Это важно иметь в виду, если вам нужно общаться с другими серверами.

Стоит подумать, хотите ли вы скомпилировать свои собственные часто используемые библиотеки с GCJ и загрузить их на сервер PostgreSQL в качестве разделяемых библиотек. Совместно используемые библиотеки находятся в /usr/lib/postgresql/9.1/lib, и я могу рассказать об этом позже.

Быстрая проверка

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

1
2
3
4
5
CREATE FUNCTION getsysprop(VARCHAR) RETURNS VARCHAR
  AS 'java.lang.System.getProperty'
  LANGUAGE java;
 
SELECT getsysprop('user.home');

В результате вы должны получить «/ var / lib / postgresql».

Установка наших собственных методов

Это хорошее начало, но мы не сильно выиграем, если не сможем вызвать наши собственные методы. К счастью, это не сложно добавить нашу собственную.

Простая процедура PL / Java

01
02
03
04
05
06
07
08
09
10
11
package sandbox;
 
public class PLJava {
    public static String hello(String name) {
        if (name == null) {
            return null;
        }
 
        return 'Hello, ' + name + '!';
    }
}

Есть два простых правила для методов, реализующих процедуры PL / Java:

  • они должны быть публичными
  • они должны возвращать ноль, если какой-либо параметр равен нулю

Вот и все.

Импортировать класс java на сервер PostgreSQL очень просто. Предположим, что классы пакетов находятся в /tmp/sandbox.jar, а наша база данных с поддержкой java — mydb . Наши команды тогда

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
--
-- load java library
--
-- parameters:
--   url_path - where the library is located
--   url_name - how the library is referred to later
--   deploy   - should the deployment descriptor be used?
--
select sqlj.install_jar('file:///tmp/sandbox.jar', 'sandbox', true);
 
--
-- set classpath to include new library.
--
-- parameters
--   schema    - schema (or database) name
--   classpath - colon-separated list of url_names.
--
select sqlj.set_classpath('mydb', 'sandbox');
 
-- -------------------
-- other procedures --
-- -------------------
 
--
-- reload java library
--
select sqlj.replace_jar('file:///tmp/sandbox.jar', 'sandbox', true);
 
--
-- remove java library
--
-- parameters:
--   url_name - how the library is referred to later
--   undeploy - should the deployment descriptor be used?
--
select sqlj.remove_jar('sandbox', true);
 
--
-- list classpath
--
select sqlj.get_classpath('mydb');
 
--

Важно помнить, чтобы установить путь к классу. Библиотеки автоматически удаляются из пути к классам, когда они выгружаются, но они НЕ добавляются автоматически в путь к классам при их установке.

Мы еще не закончили — нам все еще нужно рассказать системе о нашей новой функции.

01
02
03
04
05
06
07
08
09
10
11
12
13
--
-- create function
--
CREATE FUNCTION mydb.hello(varchar) RETURNS varchar
  AS 'sandbox.PLJava.hello'
  LANGUAGE java;
 
--
-- drop this function
--
DROP FUNCTION mydb.hello(varchar);
 
--

Теперь мы можем вызывать наш Java-метод так же, как и любые другие хранимые процедуры.

Дескриптор развертывания

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

К счастью, есть решение этой проблемы — дескрипторы развертывания. Точный формат определен в ИСО / МЭК 9075-13: 2003, но достаточно простого примера.

01
02
03
04
05
06
07
08
09
10
11
SQLActions[] = {
  'BEGIN INSTALL
     CREATE FUNCTION javatest.hello(varchar)
       RETURNS varchar
       AS 'sandbox.PLJava.hello'
       LANGUAGE java;
   END INSTALL',
  'BEGIN REMOVE
     DROP FUNCTION javatest.hello(varchar);
   END REMOVE'
}

Вы должны сообщить установщику о дескрипторе развертывания в файле MANIFEST.MF. Примером плагина Maven является

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-jar-plugin</artifactId>
   <version>2.3.1</version>
   <configuration>
      <archive>
         <manifestSections>
            <manifestSection>
               <name>postgresql.ddr</name> <!-- filename -->
               <manifestEntries>
                  <SQLJDeploymentDescriptor>TRUE</SQLJDeploymentDescriptor>
               </manifestEntries>
            </manifestSection>
         </manifestSections>
      </archive>
   </configuration>
</plugin>

База данных теперь будет знать о наших методах по мере их установки и удаления.

Внутренние Запросы

Одно из «больших преимуществ» хранимых процедур заключается в том, что запросы выполняются на самом сервере и работают НАМНОГО быстрее, чем запускать их через программный интерфейс. Я видел процесс, который требовал более 30 минут через Java, который сбрасывался до доли секунды, просто перемещая запрашиваемый цикл с клиента на сервер.

URL-адрес JDBC для внутреннего соединения: «jdbc: default: connection». Вы не можете использовать транзакции (так как вы находитесь в транзакции вызывающего абонента), но вы можете использовать точки сохранения, пока вы остаетесь в пределах одного вызова. Я не знаю, можете ли вы использовать CallableStatements (другие хранимые процедуры) — вы не смогли в версии 1.2, но пакет Ubuntu 11.10 использует версию 1.4.2.

Списки скалярных значений возвращаются как Итераторы в мире Java и SETOF в мире SQL.

1
2
3
4
public static Iterator<String> colors() {
    List<String> colors = Arrays.asList('red', 'green', 'blue');
    return colors.iterator();
}

и

1
2
3
4
CREATE FUNCTION javatest.colors()
    RETURNS SETOF varchar
    AS 'sandbox.PLJava.colors'
    IMMUTABLE LANGUAGE java;

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

Вам не нужно знать результаты или даже размер результатов, прежде чем начать. Ниже приводится последовательность, которая, как считается, всегда заканчивается, но это не было доказано. (К сожалению, я забыл название последовательности.) Как примечание, это не полное решение, так как оно не проверяет переполнение — правильная реализация должна либо проверять это, либо использовать BigInteger.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public static Iterator seq(int start) {
    Iterator iter = null;
    try {
        iter = new SeqIterator(start);
    } catch (IllegalArgumentException e) {
        // should log error...
    }
    return iter;
}
 
public static class SeqIterator implements Iterator {
    private int next;
    private boolean done = false;
     
    public SeqIterator(int start) {
        if (start <= 0) {
            throw new IllegalArgumentException();
        }
        this.next = start;
    }
 
    @Override
    public boolean hasNext() {
        return !done;
    }
 
    @Override
    public Integer next() {
        int value = next;
        next = (next % 2 == 0) ? next / 2 : 3 * next + 1;
        done = (value == 1);
        return value;
    }
 
    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }
}
1
2
3
4
CREATE FUNCTION javatest.seq(int)
    RETURNS SETOF int
    AS 'sandbox.PLJava.seq'
    IMMUTABLE LANGUAGE java;

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

Одиночные кортежи

Один кортеж возвращается в ResultSet.

1
2
3
4
5
public static boolean singleWord(ResultSet receiver) throws SQLException {
    receiver.updateString('English', 'hello');
    receiver.updateString('Spanish', 'hola');
    return true;
}

и

1
2
3
4
5
6
7
8
CREATE TYPE word AS (
    English varchar,
    Spanish varchar);
 
CREATE FUNCTION javatest.single_word()
    RETURNS word
    AS 'sandbox.PLJava.singleWord'
    IMMUTABLE LANGUAGE java;

Верный результат указывается возвращением true , нулевой результат указывается возвращением false . Сложный тип может быть передан в метод Java таким же образом — это ResultSet только для чтения, содержащий одну строку.

Списки кортежей

Для возврата списков сложных значений требуется класс, реализующий один из двух интерфейсов.

org.postgresql.pljava.ResultSetProvider

ResultSetProvider используется, когда результаты могут быть созданы программно или по мере необходимости.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public static ResultSetProvider listWords() {
     return new WordProvider();
 }
 
 public static class WordProvider implements ResultSetProvider {
     private final Map<String,String> words = new HashMap<String,String>();
     private final Iterator<String> keys;
      
     public WordProvider() {
         words.put('one', 'uno');
         words.put('two', 'dos');
         words.put('three', 'tres');
         words.put('four', 'quatro');
         keys = words.keySet().iterator();
     }
      
     @Override
     public boolean assignRowValues(ResultSet receiver, int currentRow)
             throws SQLException {
         if (!keys.hasNext()) {
             return false;
         }
         String key = keys.next();
         receiver.updateString('English', key);
         receiver.updateString('Spanish', words.get(key));
         return true;
     }
 
     @Override
     public void close() throws SQLException {
     }
 }

и

1
2
3
4
CREATE FUNCTION javatest.list_words()
  RETURNS SETOF word
  AS 'sandbox.PLJava.listWords'
  IMMUTABLE LANGUAGE java;

org.postgresql.pljava.ResultSetHandle

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
public static ResultSetHandle listUsers() {
    return new UsersHandle();
}
 
public static class UsersHandle implements ResultSetHandle {
    private Statement stmt;
 
    @Override
    public ResultSet getResultSet() throws SQLException {
        stmt = DriverManager.getConnection('jdbc:default:connection').createStatement();
        return stmt.executeQuery('SELECT * FROM pg_user');
    }
 
    @Override
    public void close() throws SQLException {
        stmt.close();
    }     
}

и

1
2
3
4
CREATE FUNCTION javatest.list_users()
    RETURNS SETOF pg_user
    AS 'sandbox.PLJava.listUsers'
    LANGUAGE java;


Интерфейсы

Мне не удалось получить последнюю копию pljava jar в стандартном репозитории maven. Моим решением было извлечь интерфейсы из исходного архива PL / Java. Они предоставлены здесь для вашего удобства.

ResultSetProvider

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden
 // Distributed under the terms shown in the file COPYRIGHT
 // found in the root folder of this project or at
  
package org.postgresql.pljava;
 
import java.sql.ResultSet;
import java.sql.SQLException;
 
 
 // An implementation of this interface is returned from functions and procedures
 // that are declared to return <code>SET OF</code> a complex type.    //Functions that
 // return <code>SET OF</code> a simple type should simply return an
 // {@link java.util.Iterator Iterator}.
 // @author Thomas Hallgren
  
public interface ResultSetProvider
{
  
  // This method is called once for each row that should be returned from
  // a procedure that returns a set of rows. The receiver
  // is a {@link org.postgresql.pljava.jdbc.SingleRowWriter SingleRowWriter}
  // writer instance that is used for capturing the data for the row.
  // @param receiver Receiver of values for the given row.
  // @param currentRow Row number. First call will have row number 0.
  // @return <code>true</code> if a new row was provided,   <code>false</code>
  // if not (end of data).
  // @throws SQLException
   
 boolean assignRowValues(ResultSet receiver, int currentRow)
 throws SQLException;
  
  
  // Called after the last row has returned or when the query evaluator dec       ides
  // that it does not need any more rows.
  //
 void close()
 throws SQLException;
}

ResultSetHandle

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden
// Distributed under the terms shown in the file COPYRIGHT
// found in the root directory of this distribution or at
  
package org.postgresql.pljava;
 
import java.sql.ResultSet;
import java.sql.SQLException;
 
 
 // An implementation of this interface is returned from functions and procedures
 // that are declared to return <code>SET OF</code> a complex type in the form
 // of a {@link java.sql.ResultSet}. The primary motivation for this interface is
 // that an implementation that returns a ResultSet must be able to close the
 // connection and statement when no more rows are requested.
 // @author Thomas Hallgren
  
public interface ResultSetHandle
{
  
  // An implementation of this method will probably execute a query
  // and return the result of that query.
  // @return The ResultSet that represents the rows to be returned.
  // @throws SQLException
   
 ResultSet getResultSet()
 throws SQLException;
 
 
 // Called after the last row has returned or when the query evaluator decides
 // that it does not need any more rows.
  
 void close()
 throws SQLException;
}

Триггеры

Триггер базы данных — это хранимая процедура, которая автоматически запускается во время одной из трех из четырех операций CRUD (create-read-update-delete).

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

Триггер может быть запущен до или после операции. Вы бы выполнили триггер перед операцией, если хотите изменить значения; вы бы выполнили его после операции, если хотите записать результаты.

Типичное использование

Вставка и обновление: проверка данных

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

Вставка и обновление: нормализация и дезинфекция данных

Иногда значения могут иметь несколько представлений или потенциально опасны. Предварительный триггер — это шанс очистить данные, например, привести в порядок XML или заменить <на <и> на>.

Все операции: ведение аудита

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

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

Триггеры могут использоваться для создания «обновляемых представлений».

Реализация PL / Java

Любой java-метод может быть использован в триггере при условии, что это открытый статический метод, возвращающий void, который принимает один аргумент, объект TriggerData . Триггеры могут называться «НА КАЖДОЙ СТРОКЕ» или «НА ЗАЯВЛЕНИИ».

TriggerDatas, которые находятся «НА КАЖДОЙ СТРОКЕ», содержат однострочный, только для чтения, ResultSet как «старое» значение для обновлений и удалений, и однорядный обновляемый ResultSet в качестве «нового» значения для вставок и обновлений. Это может быть использовано для изменения содержимого, регистрации действий и т. Д.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class AuditTrigger {
 
    public static void auditFoobar(TriggerData td) throws SQLException {
 
        Connection conn = DriverManager
                .getConnection('jdbc:default:connection');
        PreparedStatement ps = conn
                .prepareStatement('insert into javatest.foobar_audit(what, whenn, data) values (?, ?, ?::xml)');
 
        if (td.isFiredByInsert()) {
            ps.setString(1, 'INSERT');
        } else if (td.isFiredByUpdate()) {
            ps.setString(1, 'UPDATE');
        } else if (td.isFiredByDelete()) {
            ps.setString(1, 'DELETE');
        }
        ps.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
 
        ResultSet rs = td.getNew();
        if (rs != null) {
            ps.setString(3, toXml(rs));
        } else {
            ps.setNull(3, Types.VARCHAR);
        }
 
        ps.execute();
        ps.close();
    }
 
    // simple marshaler. We could use jaxb or similar library
    static String toXml(ResultSet rs) throws SQLException {
        String foo = rs.getString(1);
        if (rs.wasNull()) {
            foo = '';
        }
        String bar = rs.getString(2);
        if (rs.wasNull()) {
            bar = '';
        }
        return String.format('<my-class><foo>%s</foo><bar>%s</bar></my-class>', foo, bar);
    }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
CREATE TABLE javatest.foobar (
     foo   varchar(10),
     bar   varchar(10)
);
 
CREATE TABLE javatest.foobar_audit (
     what  varchar(10) not null,
     whenn timestamp not null,
     data  xml
);
 
CREATE FUNCTION javatest.audit_foobar()
    RETURNS trigger
    AS 'sandbox.AuditTrigger.auditFoobar'
    LANGUAGE 'java';
 
CREATE TRIGGER foobar_audit
    AFTER INSERT OR UPDATE OR DELETE ON javatest.foobar
    FOR EACH ROW
    EXECUTE PROCEDURE javatest.audit_foobar();


правила

Расширение PostgreSQL — это Правила . Они похожи на триггеры, но немного более гибкие. Одним из важных отличий является то, что правила могут быть вызваны оператором SELECT, а не только INSERT, UPDATE и DELETE.

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

Интерфейс

Как и прежде, я не смог найти Maven-репозиторий последней версии и включаю файлы для вашего удобства.

TriggerData

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden
 // Distributed under the terms shown in the file COPYRIGHT
 // found in the root folder of this project or at
  
package org.postgresql.pljava;
 
import java.sql.ResultSet;
import java.sql.SQLException;
 
 
 // The SQL 2003 spec. does not stipulate a standard way of mapping
 // triggers to functions. The PLJava mapping use this interface. All
 // functions that are intended to be triggers must be public, static,
 // return void, and take a <code>TriggerData</code> as their argument.
 //
 // @author Thomas Hallgren
  
public interface TriggerData
{
  
  // Returns the ResultSet that represents the new row. This ResultSet wil
  // be null for delete triggers and for triggers that was fired for
  // statement.
         //The returned set will be updateable and positioned on a
  // valid row. When the trigger call returns, the trigger manager will se
  // the changes that has been made to this row and construct a new tuple
  // which will become the new or updated row.
  //
  // @return An updateable <code>ResultSet</code> containing one row or
  // null
  // @throws SQLException
  //             if the contained native buffer has gone stale.
  //
 ResultSet getNew() throws SQLException;
 
   
  // Returns the ResultSet that represents the old row. This ResultSet wil
  // be null for insert triggers and for triggers that was fired for
  // statement.The returned set will be read-only and positioned on a
  // valid row.
  //
  // @return A read-only ResultSet containing one row or
  //         null.
  // @throws SQLException
  //             if the contained native buffer has gone stale.
  //
 ResultSet getOld() throws SQLException;
 
 //
 // Returns the arguments for this trigger (as declared in the <code>CREAT        // E TRIGGER</code>
 // statement. If the trigger has no arguments, this method will return an
 // array with size 0.
 //
 // @throws SQLException
 //             if the contained native buffer has gone stale.
  
 String[] getArguments() throws SQLException;
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// Returns the name of the trigger (as declared in theCREATE TRIGGER
  // statement).
  //
 // @throws SQLException
  //             if the contained native buffer has gone stale.
  //
 String getName() throws SQLException;
/**
//Returns the name of the table for which this trigger was created (as
//* declared in the <code>CREATE TRIGGER</code statement). * * @throws SQLException* if the contained native buffer has gone stale.
String getTableName() throws SQLException;
/// Returns the name of the schema of the table for which this trigger was created (as * declared in the <code>CREATE TRIGGER</code statement).
//@throws SQLException * if the contained native buffer has gone stale. */
 
String getSchemaName() throws SQLException;
// Returns <code>true</code> if the trigger was fired after the statement  or row action that it is associated with.
//@throws SQLException * if the contained native buffer has gone stale.
 
boolean isFiredAfter() throws SQLException;
//Returns <code>true</code> if the trigger was fired before the * //statement or row action that it is associated with. * * @throws SQLException * if //the contained native buffer has gone stale. */
boolean isFiredBefore() throws SQLException;
//Returns <code>true</code> if this trigger is fired once for each row * //(as opposed to once for the entire statement). * * @throws SQLException * if the //contained native buffer has gone stale. */
boolean isFiredForEachRow() throws SQLException;
//Returns <code>true</code> if this trigger is fired once for the entire //statement (as opposed to once for each row). * * @throws SQLException * if the //contained native buffer has gone stale. */
boolean isFiredForStatement() throws SQLException;
//Returns <code>true</code> if this trigger was fired by a <code>DELETE</code>. * * @throws SQLException * if the contained native //buffer has gone stale. */
boolean isFiredByDelete() throws SQLException;
//Returns <code>true</code> if this trigger was fired by an //<code>INSERT</code>. * * @throws SQLException * if the contained native //buffer has gone stale. */
boolean isFiredByInsert() throws SQLException;
//Returns <code>true</code> if this trigger was fired by an //<code>UPDATE</code>. * * @throws SQLException * if the contained native //buffer has gone stale. */
boolean isFiredByUpdate() throws SQLException;
 
// Returns the name of the table for which this trigger was created (as
// declared in the <code>CREATE TRIGGER</code statement). * * @throws //SQLException* if the contained native buffer has gone stale. */
String getTableName() throws SQLException;
// Returns the name of the schema of the table for which this trigger was created (as / declared in the <code>CREATE TRIGGER</code statement). * * @throws //SQLException * if the contained native buffer has gone stale. */
String getSchemaName() throws SQLException;
//Returns <code>true</code> if the trigger was fired after the statement // or row action that it is associated with. * * @throws SQLException * if the //contained native buffer has gone stale. */
boolean isFiredAfter() throws SQLException;
// Returns <code>true</code> if the trigger was fired before the * //statement or row action that it is associated with. * * @throws SQLException * if //the contained native buffer has gone stale. */
boolean isFiredBefore() throws SQLException;
// Returns <code>true</code> if this trigger is fired once for each row * //(as opposed to once for the entire statement). * * @throws SQLException * if the //contained native buffer has gone stale. */
boolean isFiredForEachRow() throws SQLException;
// Returns <code>true</code> if this trigger is fired once for the entire // statement (as opposed to once for each row). * * @throws SQLException * if the //contained native buffer has gone stale. */
boolean isFiredForStatement() throws SQLException;
// Returns <code>true</code> if this trigger was fired by a //<code>DELETE</code>. * * @throws SQLException * if the contained native //buffer has gone stale. */
boolean isFiredByDelete() throws SQLException;
// Returns <code>true</code> if this trigger was fired by an //<code>INSERT</code>. * * @throws SQLException * if the contained native //buffer has gone stale. */
boolean isFiredByInsert() throws SQLException;
// Returns <code>true</code> if this trigger was fired by an //<code>UPDATE</code>. * * @throws SQLException * if the contained native //buffer has gone stale. */
boolean isFiredByUpdate() throws SQLException; }/**
// Returns the name of the table for which this trigger was created (as
// declared in the <code>CREATE TRIGGER</code statement). * * @throws //SQLException* if the contained native buffer has gone stale. */
String getTableName() throws SQLException;
// Returns the name of the schema of the table for which this trigger was created (as // declared in the <code>CREATE TRIGGER</code statement). * * @throws //SQLException * if the contained native buffer has gone stale. */
String getSchemaName() throws SQLException;
/// Returns <code>true</code> if the trigger was fired after the //statement * or row action that it is associated with. * * @throws SQLException * if //the contained native buffer has gone stale. */
boolean isFiredAfter() throws SQLException;
// Returns <code>true</code> if the trigger was fired before the * //statement or row action that it is associated with. * * @throws SQLException * if //the contained native buffer has gone stale. */
boolean isFiredBefore() throws SQLException;
// Returns <code>true</code> if this trigger is fired once for each row * (//as opposed to once for the entire statement). * * @throws SQLException * if the //contained native buffer has gone stale. */
boolean isFiredForEachRow() throws SQLException;
// Returns <code>true</code> if this trigger is fired once for the entire // statement (as opposed to once for each row). * * @throws SQLException * if the //contained native buffer has gone stale. */
boolean isFiredForStatement() throws SQLException;
// Returns <code>true</code> if this trigger was fired by a //<code>DELETE</code>. * * @throws SQLException * if the contained native //buffer has gone stale. */
boolean isFiredByDelete() throws SQLException;
// Returns <code>true</code> if this trigger was fired by an //<code>INSERT</code>. * * @throws SQLException * if the contained native //buffer has gone stale. */
boolean isFiredByInsert() throws SQLException;
// Returns <code>true</code> if this trigger was fired by an //<code>UPDATE</code>. * * @throws SQLException * if the contained native //buffer has gone stale. */
boolean isFiredByUpdate() throws SQLException; }

TriggerException

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden
 // Distributed under the terms shown in the file COPYRIGHT
 // found in the root folder of this project or at
  
package org.postgresql.pljava;
 
import java.sql.SQLException;
 
 
 // An exception specially suited to be thrown from within a method
 // designated to be a trigger function. The message generated by
 // this exception will contain information on what trigger and
 // what relation it was that caused the exception
 //
 // @author Thomas Hallgren
  
public class TriggerException extends SQLException
{
    private static final long serialVersionUID = 5543711707414329116L;
 
    private static boolean s_recursionLock = false;
 
    public static final String TRIGGER_ACTION_EXCEPTION = '09000';
 
    private static final String makeMessage(TriggerData td, String message)
    {
        StringBuffer bld = new StringBuffer();
        bld.append('In Trigger ');
        if(!s_recursionLock)
        {
            s_recursionLock = true;
            try
            {
                bld.append(td.getName());
                bld.append(' on relation ');
                bld.append(td.getTableName());
            }
            catch(SQLException e)
            {
                bld.append('(exception while generating exception message)');
            }
            finally
            {
                s_recursionLock = false;
            }
        }
        if(message != null)
        {
            bld.append(': ');
            bld.append(message);
        }
        return bld.toString();
    }
 
     
     // Create an exception based on the <code>TriggerData</code> that was
     // passed to the trigger method.
     // @param td The <code>TriggerData</code> that was passed to the trigger
     // method.
      
    public TriggerException(TriggerData td)
    {
        super(makeMessage(td, null), TRIGGER_ACTION_EXCEPTION);
    }
 
     
     // Create an exception based on the <code>TriggerData</code> that was
     // passed to the trigger method and an additional message.
     // @param td The <code>TriggerData</code> that was passed to the trigger
     // method.
     // @param reason An additional message with info about the exception.
      
    public TriggerException(TriggerData td, String reason)
    {
        super(makeMessage(td, reason), TRIGGER_ACTION_EXCEPTION);
    }
}

Пользовательские типы в базе данных противоречивы. Они не являются стандартными — в какой-то момент АБД должен их создать — и это порождает проблемы с переносимостью. Стандартные инструменты не будут знать о них. Вы должны получить к ним доступ через методы struct в ResultSets и PreparedStatements.

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

Какой будет хороший пользовательский тип? Он должен быть атомарным и иметь возможность выполнять значимую работу с помощью хранимых процедур. Примечание: пользовательский тип базы данных — это не то же самое, что класс java. Почти все Java-классы должны храниться как стандартные кортежи, и вы должны использовать UDT базы данных только в том случае, если есть веская причина.

Пробный камень, который мне нравится, спрашивает, соблазняетесь ли вы когда-нибудь кэшировать неизменную информацию о типе или о кортеже в дополнение к самому объекту. Например, цифровой сертификат X.509 имеет ряд неизменяемых полей, которые будут действительными поисковыми терминами, но извлекать эту информацию для каждой строки дорого. (Sidenote: вы можете использовать триггеры для извлечения информации при вставке и обновлении записи. Это гарантирует, что кэшированные значения всегда точны.)

Примеры:

  • комплексные числа (хранимые процедуры: арифметика)
  • рациональные числа (хранимые процедуры: арифметика)
  • номера полей Галуа (хранимые процедуры: арифметика по модулю фиксированного значения)
  • изображения (хранимые процедуры: получить размеры)
  • PDF документы (хранимые процедуры: элементы извлечения)
  • цифровые сертификаты и закрытые ключи (хранимые процедуры: крипто)

Что-то, что также должно быть обращено, является правильным языком для реализации. В PL / Java легко создавать прототипы, но вы можете привести веские аргументы в пользу того, что типы должны быть в конечном итоге реализованы в виде стандартных расширений PostgreSQL, поскольку они с большей вероятностью будут доступны в будущем, если вы посмотрите на 20-летнего подростка. свалка. В некоторых важных аспектах это лишь малая часть проблемы — вопрос не в том, написана ли фактическая реализация хранилища и функций на C или Java, а в том, как она связана с остальной частью системы.

Реализация PL / Java

Пользовательский тип PL / Java должен реализовывать интерфейс java.sql.SQLData , статический метод, который создает объект из String, и метод экземпляра, который создает String из объекта. Эти методы должны дополнять друг друга — должна быть возможность выполнить значение в течение полного цикла в любом направлении и вернуть исходное значение.

NB, это часто невозможно с двойными числами — вот почему вы получаете числа, такие как 4.000000001 или 2.999999999. В этих случаях вы должны делать все возможное и предупредить пользователя.

Во многих случаях объект может храниться более эффективно в двоичном формате. В терминах PostgreSQL это типы TOAST. Это достигается путем реализации двух новых методов, которые работают с потоками SQLInput и SQLOutput.

Далее следует простая реализация рационального типа.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class Rational implements SQLData {
    private long numerator;
    private long denominator;
    private String typeName;
 
    public static Rational parse(String input, String typeName)
            throws SQLException {
        Pattern pattern = Pattern.compile('(-?[0-9]+)( */ *(-?[0-9]+))?');
        Matcher matcher = pattern.matcher(input);
        if (!matcher.matches()) {
            throw new SQLException('Unable to parse rational from string \'' + input
                    + ''');
        }
        if (matcher.groupCount() == 3) {
            if (matcher.group(3) == null) {
                return new Rational(Long.parseLong(matcher.group(1)));
            }
            return new Rational(Long.parseLong(matcher.group(1)),
                    Long.parseLong(matcher.group(3)));
        }
        throw new SQLException('invalid format: \'' + input
                + ''');
    }
 
    public Rational(long numerator) throws SQLException {
        this(numerator, 1);
    }
 
    public Rational(long numerator, long denominator) throws SQLException {
        if (denominator == 0) {
            throw new SQLException('demominator must be non-zero');
        }
 
        // do a little bit of normalization
        if (denominator < 0) {
            numerator = -numerator;
            denominator = -denominator;
        }
 
        this.numerator = numerator;
        this.denominator = denominator;
    }
 
    public Rational(int numerator, int denominator, String typeName)
            throws SQLException {
        this(numerator, denominator);
        this.typeName = typeName;
    }
 
    public String getSQLTypeName() {
        return typeName;
    }
 
    public void readSQL(SQLInput stream, String typeName) throws SQLException {
        this.numerator = stream.readLong();
        this.denominator = stream.readLong();
        this.typeName = typeName;
    }
 
    public void writeSQL(SQLOutput stream) throws SQLException {
        stream.writeLong(numerator);
        stream.writeLong(denominator);
    }
 
    public String toString() {
        String value = null;
        if (denominator == 1) {
            value = String.valueOf(numerator);
        } else {
            value = String.format('%d/%d', numerator, denominator);
        }
        return value;
    }
 
    /*
     * Meaningful code that actually does something with this type was
     * intentionally left out.
     */
}

и

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* The shell type */
CREATE TYPE javatest.rational;
 
/* The scalar input function */
CREATE FUNCTION javatest.rational_in(cstring)
  RETURNS javatest.rational
  AS 'UDT[sandbox.Rational] input'
  LANGUAGE java IMMUTABLE STRICT;
 
/* The scalar output function */
CREATE FUNCTION javatest.rational_out(javatest.rational)
  RETURNS cstring
  AS 'UDT[sandbox.Rational] output'
  LANGUAGE java IMMUTABLE STRICT;
 
/* The scalar receive function */
CREATE FUNCTION javatest.rational_recv(internal)
  RETURNS javatest.rational
  AS 'UDT[sandbox.Rational] receive'
  LANGUAGE java IMMUTABLE STRICT;
 
/* The scalar send function */
CREATE FUNCTION javatest.rational_send(javatest.rational)
  RETURNS bytea
  AS 'UDT[sandbox.Rational] send'
  LANGUAGE java IMMUTABLE STRICT;
 
CREATE TYPE javatest.rational (
  internallength = 16,
  input = javatest.rational_in,
  output = javatest.rational_out,
  receive = javatest.rational_recv,
  send = javatest.rational_send,
  alignment = int);


Модификаторы типа

PostgreSQL позволяет типам иметь модификаторы. Примеры приведены в «varchar (200)» или «numeric (8,2)».

PL / Java в настоящее время не поддерживает эту функцию (с помощью методов typmod_in и typmod_out), но я отправил запрос на нее.

Слепки

Пользовательские типы не особенно полезны, если все, что вы можете сделать, это сохранить и извлечь значения в виде непрозрачных объектов. Почему бы не использовать bytea и покончить с этим?

На самом деле существует множество UDT, в которых имеет смысл иметь возможность приводить UDT к другому типу. Числовые типы, такие как комплексные или рациональные числа, должны быть в состоянии преобразовываться в и из стандартных типов целых и плавающих чисел (хотя и с ограничениями).

Это должно быть сделано с сдержанностью.

Приведения реализуются как статические методы с одним аргументом. В мире Java эти методы часто называют newInstance, поэтому я делаю то же самое здесь.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public static Rational newInstance(String input) throws SQLException {
    if (input == null) {
        return null;
    }
    return parse(input, 'javatest.rational');
}
 
public static Rational newInstance(int value) throws SQLException {
    return new Rational(value);
}
 
public static Rational newInstance(Integer value) throws SQLException {
    if (value == null) {
        return null;
    }
    return new Rational(value.intValue());
}
 
public static Rational newInstance(long value) throws SQLException {
    return new Rational(value);
}
 
public static Rational newInstance(Long value) throws SQLException {
    if (value == null) {
        return null;
    }
    return new Rational(value.longValue());
}
 
public static Double value(Rational value) throws SQLException {
    if (value == null) {
        return null;
    }
    return value.doubleValue();
}

и

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
CREATE FUNCTION javatest.rational_string_as_rational(varchar) RETURNS javatest.rational
      AS 'sandbox.Rational.newInstance'
      LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE FUNCTION javatest.rational_int_as_rational(int4) RETURNS javatest.rational
      AS 'sandbox.Rational.newInstance'
      LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE FUNCTION javatest.rational_long_as_rational(int8) RETURNS javatest.rational
      AS 'sandbox.Rational.newInstance'
      LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE FUNCTION javatest.rational_as_double(javatest.rational) RETURNS float8
      AS 'sandbox.Rational.value'
      LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE CAST (varchar AS javatest.rational)
    WITH FUNCTION javatest.rational_string_as_rational(varchar)
    AS ASSIGNMENT;
 
CREATE CAST (int4 AS javatest.rational)
    WITH FUNCTION javatest.rational_int_as_rational(int4)
    AS ASSIGNMENT;
 
CREATE CAST (int8 AS javatest.rational)
    WITH FUNCTION javatest.rational_long_as_rational(int8)
    AS ASSIGNMENT;
 
CREATE CAST (javatest.rational AS float8)
    WITH FUNCTION javatest.rational_as_double(javatest.rational)
    AS ASSIGNMENT;

(Sidenote: STRICT означает, что функция вернет NULL, если какой-либо аргумент равен NULL. Это позволяет базе данных выполнить некоторые оптимизации.)

(Примечание: мы можем использовать флаг IMMUTABLE только в том случае, если java-объекты также являются неизменяемыми. Вероятно, мы должны сделать наши объекты Rational неизменяемыми, поскольку другие числовые типы неизменны.)

Агрегатные функции

Как насчет min () ? Рациональные числа являются числовым типом, поэтому не должны ли они поддерживать все стандартные агрегатные функции?

Определить новые агрегатные функции просто. Простые агрегатные функции нуждаются только в статической функции-члене, которая принимает два значения UDT и возвращает одно. Это легко увидеть с помощью максимумов, минимумов, сумм, продуктов и т. Д. Более сложные агрегаты требуют вспомогательного UDT, который содержит информацию о состоянии, статический метод, который принимает один UDT состояния и один UDT и возвращает UDT состояния, и метод финализации, который принимает окончательное состояние UDT и выдает результаты. Это легко увидеть по средним значениям — вам нужен тип состояния, который содержит счетчик и промежуточную сумму.

Ниже приведены несколько примеров агрегатной функции первого типа.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// compare two Rational objects. We use BigInteger to avoid overflow.
public static int compare(Rational p, Rational q) {
    if (p == null) {
        return 1;
    } else if (q == null) {
        return -1;
    }
    BigInteger l = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator()));
    BigInteger r = BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator()));
    return l.compareTo(r);
}
 
public static Rational min(Rational p, Rational q) {
    if ((p == null) || (q == null)) {
        return null;
    }
    return (p.compareTo(q) <= 0) ? p : q;
}
 
public static Rational max(Rational p, Rational q) {
    if ((p == null) || (q == null)) {
        return null;
    }
    return (q.compareTo(p) < 0) ? p : q;
}
 
public static Rational add(Rational p, Rational q) throws SQLException {
    if ((p == null) || (q == null)) {
        return null;
    }
    BigInteger n = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator())).add(
            BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator())));
    BigInteger d = BigInteger.valueOf(p.getDenominator()).multiply(BigInteger.valueOf(q.getDenominator()));
    BigInteger gcd = n.gcd(d);
    n = n.divide(gcd);
    d = d.divide(gcd);
    return new Rational(n.longValue(), d.longValue());
}

и

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
CREATE FUNCTION javatest.min(javatest.rational, javatest.rational) RETURNS javatest.rational
    AS 'sandbox.Rational.min'
    LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE FUNCTION javatest.max(javatest.rational, javatest.rational) RETURNS javatest.rational
    AS 'sandbox.Rational.max'
    LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE AGGREGATE min(javatest.rational) (
  sfunc = javatest.min,
  stype = javatest.rational
);
 
CREATE AGGREGATE max(javatest.rational) (
  sfunc = javatest.max,
  stype = javatest.rational
);
 
CREATE AGGREGATE sum(javatest.rational) (
  sfunc = javatest.add,
  stype = javatest.rational
);


Интеграция с Hibernate

Можно связать пользовательские типы PL / Java и пользовательские типы Hibernate. Предупреждение: код гибернации сильно зависит от базы данных.

Это спящий пользовательский тип. PostgreSQL 9.1 не поддерживает тип STRUCT и вместо этого использует строки. Нам не нужно использовать пользовательский тип данных PL / Java для выполнения маршалинга, но он обеспечивает согласованность. TheDbRationalType является классом Rational выше. Один и тот же класс может использоваться в обоих местах, но он привнесет зависимость от интерфейса Hibernate в класс PL / Java. Это может быть приемлемо, если вы извлекаете этот единственный интерфейс из исходного кода Hibernate.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
public class Rational implements UserType, Serializable {
    private final int[] sqlTypesSupported = new int[] { Types.OTHER };
    private long numerator;
    private long denominator;
 
    public Rational() {
        numerator = 0;
        denominator = 1;
    }
 
    public Rational(long numerator, long denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
    }
 
    public long getNumerator() {
        return numerator;
    }
 
    public long getDenominator() {
        return denominator;
    }
 
    @Override
    public Object assemble(Serializable cached, Object owner)
            throws HibernateException {
        if (!(cached instanceof Rational)) {
            throw new HibernateException('invalid argument');
        }
        Rational r = (Rational) cached;
        return new Rational(r.getNumerator(), r.getDenominator());
    }
 
    @Override
    public Serializable disassemble(Object value) throws HibernateException {
        if (!(value instanceof Rational)) {
            throw new HibernateException('invalid argument');
        }
        return (Rational) value;
    }
 
    @Override
    public Object deepCopy(Object value) throws HibernateException {
        if (value == null) {
            return null
        }
        if (!(value instanceof Rational)) {
            throw new HibernateException('invalid argument');
        }
        Rational v = (Rational) value;
        return new Rational(v.getNumerator(), v.getDenominator());
    }
 
    @Override
    public boolean isMutable() {
        return true;
    }
 
    //
    // important: PGobject is postgresql-specific
    // 
    @Override
    public Object nullSafeGet(ResultSet rs, String[] names, Object owners)
            throws HibernateException, SQLException {
        PGobject pgo = (PGobject) rs.getObject(names[0]);
        if (rs.wasNull()) {
            return null;
        }
        TheDbRationalType r = TheDbRationalType.parse(pgo.getValue(), 'rational');
        return new Rational(r.getNumerator(), r.getDenominator());
    }
 
    //
    // important: using Types.OTHER may be postgresql-specific
    // 
    @Override
    public void nullSafeSet(PreparedStatement ps, Object value, int index)
            throws HibernateException, SQLException {
        if (value == null) {
            ps.setNull(index, Types.OTHER);
        } else if (!(value instanceof Rational)) {
            throw new HibernateException('invalid argument');
        } else {
            Rational t = (Rational) value;
            ps.setObject(index,
                    new TheDbRationalType(t.getNumerator(), t.getDenominator()), Types.OTHER);
        }
    }
 
    @Override
    public Object replace(Object original, Object target, Object owner)
            throws HibernateException {
        if (!(original instanceof Rational)
                || !(target instanceof Rational)) {
            throw new HibernateException('invalid argument');
        }
        Rational r = (Rational) original;
        return new Rational(r.getNumerator(), r.getDenominator());
    }
 
    @Override
    public Class returnedClass() {
        return Rational.class;
    }
 
    @Override
    public int[] sqlTypes() {
        return sqlTypesSupported;
    }
 
    @Override
    public String toString() {
        String value = '';
        if (denominator == 1) {
            value = String.valueOf(numerator);
        } else {
            value = String.format('%d/%d', numerator, denominator);
        }
        return value;
    }
 
    // for UserType
    @Override
    public int hashCode(Object value) {
        Rational r = (Rational) value;
        return (int) (31 * r.getNumerator() + r.getDenominator());
    }
     
    @Override
    public int hashCode() {
        return hashCode(this);
    }
 
    // for UserType
    @Override
    public boolean equals(Object left, Object right) {
        if (left == right) {
            return true;
        }
        if ((left == null) || (right == null)) {
            return false;
        }
        if (!(left instanceof Rational) || !(right instanceof Rational)) {
            return false;
        }
 
        Rational l = (Rational) left;
        Rational r = (Rational) right;
        return (l.getNumerator() == r.getNumerator())
                && (l.getDenominator() == r.getDenominator());
    }
     
    @Override
    public boolean equals(Object value) {
        return equals(this, value);
    }
}

CustomTypes.hbm.xml

01
02
03
04
05
06
07
08
09
10
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
        '-//Hibernate/Hibernate Mapping DTD 3.0//EN'
 
<hibernate-mapping>
 
    <typedef name='javatest.rational' class='sandbox.RationalType'/>
 
</hibernate-mapping>

TestTable.hbm.xml

01
02
03
04
05
06
07
08
09
10
11
12
13
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
        '-//Hibernate/Hibernate Mapping DTD 3.0//EN'
 
<hibernate-mapping>
 
    <class name='sandbox.TestTable' table='test_table'>
        <id name='id'/>
        <property name='value' type='javatest.rational' />
    </class>
 
</hibernate-mapping>

операторы

Операторы — это обычные методы PL / Java, которые также помечаются как операторы с помощью оператора CREATE OPERATOR.

Основная арифметика для рациональных чисел поддерживается как

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public static Rational negate(Rational p) throws SQLException {
    if (p == null) {
        return null;
    }
    return new Rational(-p.getNumerator(), p.getDenominator());
}
 
public static Rational add(Rational p, Rational q) throws SQLException {
    if ((p == null) || (q == null)) {
        return null;
    }
    BigInteger n = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator())).add(
            BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator())));
    BigInteger d = BigInteger.valueOf(p.getDenominator()).multiply(BigInteger.valueOf(q.getDenominator()));
    BigInteger gcd = n.gcd(d);
    n = n.divide(gcd);
    d = d.divide(gcd);
    return new Rational(n.longValue(), d.longValue());
}
 
public static Rational subtract(Rational p, Rational q) throws SQLException {
    if ((p == null) || (q == null)) {
        return null;
    }
    BigInteger n = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator())).subtract(
            BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator())));
    BigInteger d = BigInteger.valueOf(p.getDenominator()).multiply(BigInteger.valueOf(q.getDenominator()));
    BigInteger gcd = n.gcd(d);
    n = n.divide(gcd);
    d = d.divide(gcd);
    return new Rational(n.longValue(), d.longValue());
}
 
public static Rational multiply(Rational p, Rational q) throws SQLException {
    if ((p == null) || (q == null)) {
        return null;
    }
    BigInteger n = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getNumerator()));
    BigInteger d = BigInteger.valueOf(p.getDenominator()).multiply(BigInteger.valueOf(q.getDenominator()));
    BigInteger gcd = n.gcd(d);
    n = n.divide(gcd);
    d = d.divide(gcd);
    return new Rational(n.longValue(), d.longValue());
}

и

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
CREATE FUNCTION javatest.rational_negate(javatest.rational) RETURNS javatest.rational
    AS 'sandbox.Rational.negate'
    LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE FUNCTION javatest.rational_add(javatest.rational, javatest.rational)
    RETURNS javatest.rational
    AS 'sandbox.Rational.add'
    LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE FUNCTION javatest.rational_subtract(javatest.rational, javatest.rational)
    RETURNS javatest.rational
    AS 'sandbox.Rational.subtract'
    LANGUAGE JAVA IMMUTABLE STRICT;
     
CREATE FUNCTION javatest.rational_multiply(javatest.rational, javatest.rational)
    RETURNS javatest.rational
    AS 'sandbox.Rational.multiply'
    LANGUAGE JAVA IMMUTABLE STRICT;
     
CREATE FUNCTION javatest.rational_divide(javatest.rational, javatest.rational)
    RETURNS javatest.rational
    AS 'sandbox.Rational.divide'
    LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE OPERATOR - (
   rightarg = javatest.rational, procedure.rational_negate
);
 
CREATE OPERATOR + (
   leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_add,
   commutator = +
);
 
CREATE OPERATOR - (
   leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_subtract
);
 
CREATE OPERATOR * (
   leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_divide,
   commutator = *
);
 
CREATE OPERATOR / (
   leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_divide
);

Символы оператора — это от одного до 63 символов из набора «+ — * / <> = ~! @ #% ^ & | `?” с некоторыми ограничениями, чтобы избежать путаницы с началом комментариев SQL.

Коммутатор оператор является вторым оператором (возможно , то же самое) , что имеет те же результаты , если левые и правые значения меняются местами. Это используется оптимизатором.

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

Многие UDT можно заказать каким-либо образом. Это может быть что-то очевидное, например, упорядочение рациональных чисел, или что-то более произвольное, например, упорядочение комплексных чисел.

Мы можем определить операции заказа так же, как и выше. NB, больше нет ничего особенного в этих операторах — с незнакомым UDT вы не можете предположить, что <действительно означает «меньше чем». Единственное исключение — «! =», Которое всегда переписывается синтаксическим анализатором как «».

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public static int compare(Rational p, Rational q) {
    if (p == null) {
        return 1;
    } else if (q == null) {
        return -1;
    }
    BigInteger l = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator()));
    BigInteger r = BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator()));
    return l.compareTo(r);
}
 
public int compareTo(Rational p) {
    return compare(this, p);
}
 
public static int compare(Rational p, double q) {
    if (p == null) {
        return 1;
    }
    double d = p.doubleValue();
    return (d < q) ? -1 : ((d == q) ? 0 : 1);
}
 
public int compareTo(double q) {
    return compare(this, q);
}
 
public static boolean lessThan(Rational p, Rational q) {
    return compare(p, q) < 0;
}
 
public static boolean lessThanOrEquals(Rational p, Rational q) {
    return compare(p, q) <= 0;
}
     
public static boolean equals(Rational p, Rational q) {
    return compare(p, q) = 0;
}
 
public static boolean greaterThan(Rational p, Rational q) {
    return compare(p, q) > 0;
}
     
public static boolean lessThan(Rational p, double q) {
    if (p == null) {
        return false;
    }
    return p.compareTo(q) < 0;
}
 
public static boolean lessThanOrEquals(Rational p, double q) {
    if (p == null) {
        return false;
    }
    return p.compareTo(q) = 0;
}
 
public static boolean greaterThan(Rational p, double q) {
    if (p == null) {
        return true;
    }
    return p.compareTo(q) > 0;
}

Обратите внимание, что я определил методы для сравнения двух рациональных чисел или одного рационального числа и одного двойного числа.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
CREATE FUNCTION javatest.rational_lt(javatest.rational, javatest.rational)
    RETURNS bool
    AS 'sandbox.Rational.lessThan'
    LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE FUNCTION javatest.rational_le(javatest.rational, javatest.rational)
    RETURNS bool
    AS 'sandbox.Rational.lessThanOrEquals'
    LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE FUNCTION javatest.rational_eq(javatest.rational, javatest.rational)
    RETURNS bool
    AS 'sandbox.Rational.equals'
    LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE FUNCTION javatest.rational_ge(javatest.rational, javatest.rational)
    RETURNS bool
    AS 'sandbox.Rational.greaterThanOrEquals'
    LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE FUNCTION javatest.rational_gt(javatest.rational, javatest.rational)
    RETURNS bool
    AS 'sandbox.Rational.greaterThan'
    LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE FUNCTION javatest.rational_cmp(javatest.rational, javatest.rational)
    RETURNS int
    AS 'sandbox.Rational.compare'
    LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE FUNCTION javatest.rational_lt(javatest.rational, float8)
    RETURNS bool
    AS 'sandbox.Rational.lessThan'
    LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE FUNCTION javatest.rational_le(javatest.rational, float8)
    RETURNS bool
    AS 'sandbox.Rational.lessThanOrEquals'
    LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE FUNCTION javatest.rational_eq(javatest.rational, float8)
    RETURNS bool
    AS 'sandbox.Rational.equals'
    LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE FUNCTION javatest.rational_ge(javatest.rational, float8)
    RETURNS bool
    AS 'sandbox.Rational.greaterThanOrEquals'
    LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE FUNCTION javatest.rational_gt(javatest.rational, float8)
    RETURNS bool
    AS 'sandbox.Rational.greaterThan'
    LANGUAGE JAVA IMMUTABLE STRICT;
 
CREATE OPERATOR < (
   leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_lt,
   commutator = > , negator = >= ,
   restrict = scalarltsel, join = scalarltjoinsel, merges
);
 
CREATE OPERATOR <= (
   leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_le,
   commutator = >= , negator = > ,
   restrict = scalarltsel, join = scalarltjoinsel, merges
);
 
CREATE OPERATOR = (
   leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_eq,
   commutator = = , negator = <>, hashes, merges
);
 
CREATE OPERATOR >= (
   leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_lt,
   commutator = <= , negator = < ,
   restrict = scalarltsel, join = scalarltjoinsel, merges
);
 
CREATE OPERATOR > (
   leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_le,
   commutator = <= , negator = < ,
   restrict = scalargtsel, join = scalargtjoinsel, merges
);
 
CREATE OPERATOR < (
   leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_lt,
   commutator = > , negator = >=
);
 
CREATE OPERATOR <= (
   leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_le,
   commutator = >= , negator = >
);
 
CREATE OPERATOR = (
   leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_eq,
   commutator = = , negator = <>
);
 
CREATE OPERATOR >= (
   leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_ge,
   commutator = <= , negator = <
);
 
CREATE OPERATOR > (
   leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_gt,
   commutator = < , negator = <=
);

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

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

Хэши указывают, что оператор может использоваться в хеш-соединениях.

Слияния указывает, что оператор может использоваться в объединениях слияния. Индексы

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
  -- btree join
CREATE OPERATOR CLASS rational_ops
    DEFAULT FOR TYPE javatest.rational USING btree AS
      OPERATOR        1       < ,
      OPERATOR        2       <= ,
      OPERATOR        3       = ,
      OPERATOR        4       >= ,
      OPERATOR        5       > ,
      FUNCTION        1       javatest.rational_cmp(javatest.rational, javatest.rational);
 
  -- hash join
 CREATE OPERATOR CLASS rational_ops
    DEFAULT FOR TYPE javatest.rational USING hash AS
      OPERATOR        1       = ,
      FUNCTION        1       javatest.rational_hashCode(javatest.rational);


Оператор Семьи

Наконец, PostgreSQL имеет концепцию «Семейства операторов», которая группирует связанные классы операторов под одним зонтиком. Например, у вас может быть одно семейство, которое поддерживает перекрестное сравнение значений int2, int4 и int8. Каждый из них может быть указан индивидуально, но, создав семейство операторов, вы дадите несколько советов оптимизатору PostgreSQL. Дополнительная информация

Ссылка: Введение в PostgreSQL PL / Java, Часть 1 , Введение в PostgreSQL PL / Java, Часть 2: Работа со списками , Введение в PostgreSQL PL / Java, Часть 3: Триггеры , Введение в PostgreSQL PL / Java, Часть 4: Пользовательские Типы , Введение в PostgreSQL / PLJava, часть 5: Операции и индексы от нашего партнера JCG Беара Джайлса вблоге Invariant Properties .