Greenplum的PL/Java语言扩展
本节包含Greenplum数据库的PL/Java语言的概述。
Parent topic: Greenplum数据库参考指南
关于PL/Java
使用Greenplum数据库的PL/Java扩展,用户可以使用自己喜欢的Java IDE编写Java方法,并将包含这些方法的JAR文件安装到Greenplum数据库中。
Greenplum数据库的PL/Java包基于开源PL/Java 1.5.0。 Greenplum数据库的PL/Java提供以下功能。
- 能够使用Java 8或Java 11执行PL/Java函数。
- 能够指定Java运行时。
- 在数据库中安装和维护Java代码的标准化实用程序(在SQL 2003提案之后设计)
- 参数和结果的标准化映射。支持复杂类型和集合。
- 使用Greenplum数据库内部SPI例程的嵌入式高性能JDBC驱动程序。
- 元数据支持JDBC驱动程序。 包括DatabaseMetaData和ResultSetMetaData。
- 能够从查询中返回ResultSet,作为逐行构建ResultSet的替代方法。
- 完全支持保存点和异常处理。
- 能够使用IN,INPUT和OUT参数。
- 两种独立的Greenplum数据库语言:
- pljava, TRUSTED PL/Java language
- pljavau, UNTRUSTED PL/Java language
- 当一个事务或者保存点提交或者回滚时,事务和保存点监听器能够被编译执行。
- 在所选平台上与GNU GCJ集成。
SQL中的一个函数将在Java类中指定一个静态方法。 为了使函数能够执行,所指定的类必须能够通过Greenplum数据库服务器上的pljava_classpath配置参数来指定类路径。 PL/Java扩展添加了一组有助于安装和维护java类的函数。 类存储在普通的Java档案——JAR文件中。 JAR文件可以选择性地包含部署描述符,该描述符又包含在部署或取消部署JAR时要执行的SQL命令。 这些函数是按照SQL 2003提出的标准进行设计的。
PL/Java实现了传递参数和返回值的标准化方法。 使用标准JDBC ResultSet类传递复杂类型和集合。
PL/Java中包含JDBC驱动程序。 此驱动程序调用Greenplum数据库内部SPI例程。 驱动程序是必不可少的,因为函数通常将调用数据库以获取数据。 当PL/Java函数提取数据时,它们必须使用与输入PL/Java执行上下文的主函数使用的相同的事务边界。
PL/Java针对性能进行了优化。 Java虚拟机在与后端相同的进程中执行,以最小化调用开销。 PL/Java的设计目的是为了使数据库本身能够实现Java的强大功能,以便数据库密集型业务逻辑可以尽可能靠近实际数据执行。
当后端和Java VM之间的桥梁被调用时,将使用标准Java本机接口(JNI)。
关于Greenplum数据库PL/Java
在标准PostgreSQL和Greenplum数据库中实现PL/Java有一些关键的区别。
函数
以下函数在Greenplum数据库中不被支持。 在分布式的Greenplum数据库环境中,classpath的处理方式与PostgreSQL环境下不同。
- sqlj.install_jar
- sqlj.replace_jar
- sqlj.remove_jar
- sqlj.get_classpath
- sqlj.set_classpath
Greenplum数据库使用pljava_classpath服务器配置参数代替sqlj.set_classpath函数。
服务器配置参数
以下服务器配置参数由PL/Java在Greenplum数据库中使用。 这些参数取代了标准PostgreSQL PL/Java实现中使用的pljava.*参数:
pljava_classpath
冒号(:)分隔的包含任何PL/Java函数中使用的Java类的jar文件列表。 所有的jar文件必须安装在所有的Greenplum数据库主机的相同位置。 使用可信的PL/Java语言处理程序,jar文件路径必须相对于$GPHOME/lib/postgresql/java/目录。 使用不受信任的语言处理程序(javaU语言标记),路径可以相对于$GPHOME/lib/postgresql/java/或使用绝对路径。
服务器配置参数pljava_classpath_insecure控制服务器配置参数pljava_classpath是否可以由用户设置, 无需Greenplum数据库超级用户权限。 当启用pljava_classpath_insecure时, 正在开发PL/Java函数的Greenplum数据库开发人员不必是数据库超级用户身份才能来更改pljava_classpath。
Warning: 启用pljava_classpath_insecure通过为非管理员数据库用户提供能够运行未经授权的Java方法暴露了安全风险。
pljava_statement_cache_size
为准备语句设置最近使用(MRU)缓存的大小(KB)。
pljava_release_lingering_savepoints
如果为TRUE,在函数退出后,长期持续的保存点将会释放。 如果为FALSE,它们将被回滚。
pljava_vmoptions
定义Greenplum数据库Java VM的启动选项。
参阅Greenplum数据库参考指南有关Greenplum数据库服务器配置参数的信息。
启用PL/Java并安装JAR文件
以Greenplum数据库管理员gpadmin的身份执行以下步骤。
通过执行CREATE EXTENSION命令注册语言,以在数据库中启用PL/Java。 例如,此命令在testdb数据库中启用PL/Java:
$ psql -d testdb -c 'CREATE EXTENSION pljava;'
Note: 不推荐使用PL/Java install.sql脚本(在以前的版本中用于注册该语言)。
将Java归档(JAR文件)复制到所有Greenplum数据库主机上的同一目录。 本示例使用Greenplum数据库gpscp程序将文件myclasses.jar复制到目录$GPHOME/lib/postgresql/java/:
$ gpscp -f gphosts_file myclasses.jar
=:/usr/local/greenplum-db/lib/postgresql/java/
文件gphosts_file包含一个Greenplum数据库主机的列表。
设置pljava_classpath服务器配置参数在master的postgresql.conf文件中。 对于此示例,参数值是冒号(:)分隔的JAR文件列表。例如:
$ gpconfig -c pljava_classpath -v 'examples.jar:myclasses.jar'
当用户使用gppkg实用程序安装PL/Java扩展包时,将安装examples.jar文件。
Note: 如果将JAR文件安装在除$GPHOME/lib/postgresql/java/之外的目录中,则必须指定JAR文件的绝对路径。 所有的Greenplum数据库主机上的每个JAR文件必须位于相同的位置。 有关指定JAR文件位置的更多信息,参阅Greenplum数据库参考指南中的pljava_classpath服务器配置参数。
重新加载postgresql.conf文件。
$ gpstop -u
(可选)Greenplum提供了一个包含可用于测试的示例PL/Java函数的examples.sql文件。 运行此文件中的命令来创建测试函数 (它使用examples.jar中的Java类)。
$ psql -f $GPHOME/share/postgresql/pljava/examples.sql
编写PL/Java函数
有关使用PL/Java编写函数的信息。
SQL声明
一个Java函数被声明为该类的一个类名称和静态方法。 该类将用于为该函数声明的schema定义的classpath进行解析。 如果没有为该schema定义classpath,则使用public schema。 如果没有找到classpath,则使用系统类加载器解析该类。
可以声明以下函数来访问java.lang.System类上的静态方法getProperty:
CREATE FUNCTION getsysprop(VARCHAR)
RETURNS VARCHAR
AS 'java.lang.System.getProperty'
LANGUAGE java;
运行以下命令返回Jave user.home属性:
SELECT getsysprop('user.home');
类型映射
标量类型以简单的方式映射。此表列出了当前的映射。
PostgreSQL | Java |
---|---|
bool | boolean |
char | byte |
int2 | short |
int4 | int |
int8 | long |
varchar | java.lang.String |
text | java.lang.String |
bytea | byte[ ] |
date | java.sql.Date |
time | java.sql.Time (stored value treated as local time) |
timetz | java.sql.Time |
timestamp | java.sql.Timestamp (stored value treated as local time) |
timestamptz | java.sql.Timestamp |
complex | java.sql.ResultSet |
setof complex | java.sql.ResultSet |
所有其他类型都映射到java.lang.String, 并将使用为各自类型注册的标准textin/textout例程。
NULL处理
映射到java基元的标量类型不能作为NULL值传递。 要传递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() % 2) == 0;
}
}
以下两个语句都产生true:
SELECT trueIfEvenOrNull(NULL);
SELECT trueIfEvenOrNull(4);
为了从Java方法返回NULL值, 可以使用与原始对象相对应的对象类型 (例如,返回java.lang.Integer而不是int)。 PL/Java解析机制无论如何都会找到该方法。 由于Java对于具有相同名称的方法不能具有不同的返回类型,因此不会引入任何歧义。
复杂类型
复杂类型将始终作为只读的java.sql.ResultSet传递,只有一行。 ResultSet位于其行上,因此不应该调用next()。 使用ResultSet的标准getter方法检索复杂类型的值。
例如:
CREATE TYPE complexTest
AS(base integer, incbase integer, ctime timestamptz);
CREATE FUNCTION useComplexTest(complexTest)
RETURNS VARCHAR
AS 'foo.fee.Fum.useComplexTest'
IMMUTABLE LANGUAGE java;
在java类Fum中,我们添加以下静态方法:
public static String useComplexTest(ResultSet complexTest)
throws SQLException
{
int base = complexTest.getInt(1);
int incbase = complexTest.getInt(2);
Timestamp ctime = complexTest.getTimestamp(3);
return "Base = \"" + base +
"\", incbase = \"" + incbase +
"\", ctime = \"" + ctime + "\"";
}
返回复杂类型
Java没有规定任何创建ResultSet的方法。 因此,返回ResultSet不是一个选项。 SQL-2003草案建议将复杂的返回值作为IN/OUT参数处理。 PL/Java以这种方式实现了一个ResultSet。 如果用户声明一个返回复杂类型的函数,则需要使用带有最后一个参数类型为java.sql.ResultSet的布尔返回类型的Java方法。 该参数将被初始化为一个空的可更新结果集,它只包含一行。
假设已经创建了上一节中的complexTest类型。
CREATE FUNCTION createComplexTest(int, int)
RETURNS complexTest
AS 'foo.fee.Fum.createComplexTest'
IMMUTABLE LANGUAGE java;
PL/Java方法解析现在将在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;
}
返回值表示接收方是否应被视为有效的元组(true)或NULL(false)。
返回集的函数
返回结果集时,不要在返回结果集之前构建结果集,因为构建大型结果集将消耗大量资源。 最好一次产生一行。 顺便提一句,那就是Greenplum数据库后端期望一个使用SETOF返回的函数。 那用户就可以返回SETOF的一个标量类型, 如int, float或varchar或者可以返回一个复合类型的SETOF。
返回SETOF<标量类型>
为了返回一组标量类型,用户需要创建一个实现java.util.Iterator接口的Java方法。 这是一个返回一个SETOF的varchar的方法的例子:
CREATE FUNCTION javatest.getSystemProperties()
RETURNS SETOF varchar
AS 'foo.fee.Bar.getNames'
IMMUTABLE LANGUAGE java;
这个简单的Java方法返回一个迭代器:
package foo.fee;
import java.util.Iterator;
public class Bar
{
public static Iterator getNames()
{
ArrayList names = new ArrayList();
names.add("Lisa");
names.add("Bob");
names.add("Bill");
names.add("Sally");
return names.iterator();
}
}
返回SETOF<复杂类型>
返回SETOF<复杂类型>的方法必须使用接口org.postgresql.pljava.ResultSetProvider或org.postgresql.pljava.ResultSetHandle。 具有两个接口的原因是它们满足两种不同用例的最佳处理。 前者适用于要动态创建要从SETOF函数返回的每一行的情况。 在用户要返回执行查询的结果的情况下,后者将生成。
使用ResultSetProvider接口
该接口有两种方法。 布尔型assignRowValues(java.sql.ResultSet tupleBuilder, int rowNumber)和void close()方法。 Greenplum数据库的查询执行器将重复调用assignRowValues直到它返回假或者直到执行器决定不需要更多行为止。 然后它会调用close。
用户可以通过以下方式使用此接口:
CREATE FUNCTION javatest.listComplexTests(int, int)
RETURNS SETOF complexTest
AS 'foo.fee.Fum.listComplexTest'
IMMUTABLE LANGUAGE java;
该函数映射到一个返回实现ResultSetProvider接口实例的静态java方法。
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
{
// Stop when we reach 12 rows.
//
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()
{
// Nothing needed in this example
}
public static ResultSetProvider listComplexTests(int base,
int increment)
throws SQLException
{
return new Fum(base, increment);
}
}
listComplextTests方法被调用一次。 如果没有可用结果或ResultSetProvider实例,将返回NULL。 这里的Java类Fum实现了这个接口,所以它返回一个自己的实例。 然后将重复调用assignRowValues方法,直到返回false。 到那候,将会调用close。
使用ResultSetHandle接口
该接口类似于ResultSetProvider接口因为它也有将在最后调用的close()方法。 但是,不是让evaluator调用一次构建一行的方法,而是返回一个ResultSet的方法。 查询evaluator将遍历该集合,并将RestulSet内容一次一个元组传递给调用者,直到对next()的调用返回false或者evaluator决定不需要更多行。
这是一个使用默认连接获取的语句执行查询的示例。 适用于部署描述符的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包org.postgresql.pljava.example中加入了一个Users类:
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").cr
eateStatement();
return m_statement.executeQuery("SELECT * FROM pg_user
WHERE " + m_filter);
}
public void close()
throws SQLException
{
m_statement.close();
}
public static ResultSetHandle listSupers()
{
return new Users("usesuper = true");
}
public static ResultSetHandle listNonSupers()
{
return new Users("usesuper = false");
}
}
使用JDBC
PL/Java包含映射到PostgreSQL SPI函数的JDBC驱动程序。 可以使用以下语句获取映射到当前事务的连接:
Connection conn =
DriverManager.getConnection("jdbc:default:connection");
获取连接后,可以准备和执行类似于其他JDBC连接的语句。 这些是PL/Java JDBC驱动程序的限制:
- 事务无法以任何方式进行管理。因此,连接后用户不能用如下方法:
- commit()
- rollback()
- setAutoCommit()
- setTransactionIsolation()
- 在保存点上也有一些限制。 保存点不能超过其设置的功能,并且必须由同一功能回滚或释放。
- 从executeQuery()返回的结果集始终为FETCH_FORWARD和CONCUR_READ_ONLY。
- 元数据仅在PL/Java 1.1或更高版本中可用。
- CallableStatement(用于存储过程)没有实现。
- Clob和Blob类型未完全实现,需要更多工作。 byte[]和String可分别用于bytea和text。
异常处理
您可以像其他任何异常一样捕获并处理Greenplum数据库后端中的异常。 后端的ErrorData结构作为一个名为org.postgresql.pljava.ServerException(从java.sql.SQLException中派生)的类中的属性公开, 并且Java try/catch机制与后端机制同步。
Important: 在函数返回之前,用户将无法继续执行后端函数,并且在后端生成异常时传播错误,除非用户使用了保存点。 当回滚保存点时,异常条件被重置,用户可以继续执行。
保存点
Greenplum数据库保存点使用java.sql.Connection接口公开。有两个限制。
- 必须在设置的函数中回滚或释放保存点。
- 保存点不能超过其设置的功能。
日志
PL/Java使用标准的Java Logger。因此,用户可以按如下写:
Logger.getAnonymousLogger().info( "Time is " + new
Date(System.currentTimeMillis()));
目前,记录器使用一个处理程序来映射Greenplum数据库配置设置的当前状态log_min_messages到有效的Logger级别, 并使用Greenplum数据库后端功能输出所有消息elog()。
Note: 第一次执行会话中的PL/Java函数时,将从数据库中读取log_min_messages设置。 在Java端,在特定会话中执行第一个PL/Java函数之后,设置不会更改,直到重新启动使用PL/Java的Greenplum数据库会话。
Logger级别和Greenplum数据库后端级别之间适用以下映射。
java.util.logging.Level | Greenplum数据库级别 |
---|---|
SEVERE ERROR | ERROR |
WARNING | WARNING |
CONFIG | LOG |
INFO | INFO |
FINE | DEBUG1 |
FINER | DEBUG2 |
FINEST | DEBUG3 |
安全
安装
只有数据库超级用户才可以安装PL/Java。 使用SECURITY DEFINER安装PL/Java实用程序函数,以便它们执行以授予函数创建者的访问权限。
可信语言
PL/Java是一种可信语言。 可信的PL/Java语言无法访问PostgreSQL定义可信语言所规定的文件系统。 任何数据库用户都可以创建和访问受信任的语言的函数。
PL/Java还为语言javau安装语言处理程序。 此版本不受信任,只有超级用户可以创建使用它的新函数。 任何用户都可以调用这些函数。
一些PL/Java问题和解决方案
当编写PL/Java时,将JVM映射到与Greenplum数据库后端代码相同的进程空间中,对于多个线程,异常处理和内存管理,已经出现了一些问题。 这里是简要说明如何解决这些问题。
多线程
Java本身就是多线程的。 Greenplum数据库后端不是。 没有什么可以阻止开发人员在Java代码中使用多个Threads类。 调用后端的终结器可能是从背景垃圾回收线程中产生的。 可能使用的几个第三方Java包使用多个线程。 该模式在同一过程中如何与Greenplum数据库后端共存?
解决方案
解决方案很简单。 PL/Java定义了一个特殊对象Backend.THREADLOCK。 当初始化PL/Java时,后端立即抓取该对象监视器(即它将在此对象上同步)。 当后端调用Java函数时,监视器将被释放,然后在调用返回时立即恢复。 来自Java的所有调用到后端代码都在同一个锁上同步。 这确保一次只能有一个线程可以从Java调用后端,并且只能在后端正在等待返回Java函数调用的时候调用。
异常处理
Java经常使用try/catch/finally块。 Greenplum数据库有时会使用一个异常机制来调用longjmp来将控件转移到已知状态。 这样的跳转通常会有效地绕过JVM。
解决方案
后端现在允许使用宏PG_TRY/PG_CATCH/PG_END_TRY捕获错误, 并且在catch块中,可以使用ErrorData结构检查错误。 PL/Java实现了一个名为org.postgresql.pljava.ServerException的java.sql.SQLException子类。 可以从该异常中检索和检查ErrorData。 允许捕获处理程序发送回滚到保存点。 回滚成功后,执行可以继续。
Java垃圾收集器与palloc()和堆栈分配
原始类型始终按值传递。 包括String类型(这是必需的,因为Java使用双字节字符)。 复杂类型通常包含在Java对象中并通过引用传递。 例如,Java对象可以包含指向palloc’ed或stack分配的内存的指针,并使用原生的JNI调用来提取和操作数据。 一旦调用结束,这些数据将变得陈旧。 进一步尝试访问这些数据最多只会产生非常不可预知的结果,但更有可能导致内存错误和崩溃。
解决方案
PL/Java包含的代码可以确保当MemoryContext或堆栈分配超出范围时,陈旧的指针被清除。 Java包装器对象可能会生效,但是使用它们的任何尝试将导致陈旧的原生处理异常。
示例
以下简单的Java示例创建一个包含单个方法并运行该方法的JAR文件。
Note: 该示例需要Java SDK来编译Java文件。
以下方法返回一个子字符串。
{
public static String substring(String text, int beginIndex,
int endIndex)
{
return text.substring(beginIndex, endIndex);
}
}
在文本文件example.class中输入这些Java代码。
manifest.txt文件的内容:
Manifest-Version: 1.0
Main-Class: Example
Specification-Title: "Example"
Specification-Version: "1.0"
Created-By: 1.6.0_35-b10-428-11M3811
Build-Date: 01/20/2013 10:09 AM
编译java代码:
javac *.java
创建名为analytics.jar的JAR存档,其中包含JAR中的类文件和清单文件MANIFEST文件。
jar cfm analytics.jar manifest.txt *.class
将jar文件上传到Greenplum master主机。
运行gpscp实用程序将jar文件复制到Greenplum Java目录。 使用-f选项指定包含master节点和segment节点主机列表的文件。
gpscp -f gphosts_file analytics.jar
=:/usr/local/greenplum-db/lib/postgresql/java/
使用gpconfig程序设置Greenplumpljava_classpath服务器配置参数。 该参数列出已安装的jar文件。
gpconfig -c pljava_classpath -v 'analytics.jar'
运行gpstop实用程序-u选项重新加载配置文件。
gpstop -u
来自psql命令行,运行以下命令显示已安装的jar文件。
show pljava_classpath
以下SQL命令创建一个表并定义一个Java函数来测试jar文件中的方法:
create table temp (a varchar) distributed randomly;
insert into temp values ('my string');
--Example function
create or replace function java_substring(varchar, int, int)
returns varchar as 'Example.substring' language java;
--Example execution
select java_substring(a, 1, 5) from temp;
用户可以将内容放在一个文件mysample.sql中,并从psql命令行运行该命令:
> \i mysample.sql
输出类似于:
java_substring
----------------
y st
(1 row)
参考
The PL/Java Github wiki page - https://github.com/tada/pljava/wiki.
PL/Java 1.5.0 release - https://github.com/tada/pljava/tree/REL1_5_STABLE.