开发技术网站开发技术路线,怎么自己做个网站做链接跳转,关键词权重,代理游戏网站现代数据库允许以多种语言编写存储过程。 一种常见的实现语言是java.NB#xff0c;本文讨论了PostgreSQL特定的Java实现。 其他数据库的详细信息会有所不同#xff0c;但是概念是相同的。 PL / Java的安装 在Ubuntu系统上安装PL / Java很简单。 我将首先创建一个新模板templ… 现代数据库允许以多种语言编写存储过程。 一种常见的实现语言是java.NB本文讨论了PostgreSQL特定的Java实现。 其他数据库的详细信息会有所不同但是概念是相同的。 PL / Java的安装 在Ubuntu系统上安装PL / Java很简单。 我将首先创建一个新模板template_java 因此我仍然可以创建没有pl / java扩展名的数据库。 在命令行上假设您是数据库超级用户请输入 # 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 datistemplatet where datnamtemplate_java
$ psql -d template_java -f /usr/share/postgresql-9.1-pljava/install.sql 局限性 预包装的Ubuntu软件包使用Gnu GCJ Java实现而不是标准的OpenJDK或Sun实现。 GCJ将Java源文件编译为本机目标代码而不是字节码。 PL / Java的最新版本是“受信任的” –可以依靠它们保留在其沙箱中。 除其他外这意味着您无法访问服务器上的文件系统。 如果必须打破信任关系则可以使用第二种语言“ javaU”。 不受信任的函数只能创建一个数据库超级用户。 更重要的是此实现是单线程的。 如果您需要与其他服务器通信请记住这一点至关重要。 需要考虑的事情是是否要使用GCJ编译自己的常用库并将它们作为共享库加载到PostgreSQL服务器中。 共享库位于/usr/lib/postgresql/9.1/lib中 稍后我可能要说更多。 快速验证 通过编写快速测试功能我们可以轻松地检查安装。 使用template_java创建临时数据库然后输入以下SQL CREATE FUNCTION getsysprop(VARCHAR) RETURNS VARCHARAS java.lang.System.getPropertyLANGUAGE java;SELECT getsysprop(user.home); 结果您应该得到“ / var / lib / postgresql”。 安装我们自己的方法 这是一个不错的开始但是如果我们不能调用自己的方法那么我们并不会真正受益。 幸运的是添加我们自己的并不难。 一个简单的PL / Java过程是 package sandbox;public class PLJava {public static String hello(String name) {if (name null) {return null;}return Hello, name !;}
} 实现PL / Java过程的方法有两个简单的规则 它们必须是公共静态的 如果任何参数为空 他们必须返回null 而已。 将Java类导入PostgreSQL服务器很简单。 假设包类在/tmp/sandbox.jar中而我们启用Java的数据库是mydb 。 然后我们的命令是 --
-- 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);-- 记住要设置类路径这一点很重要。 库在卸载时会自动从类路径中删除但安装后不会自动添加到类路径中。 我们还没有完全完成–我们仍然需要将新功能告诉系统。 --
-- create function
--
CREATE FUNCTION mydb.hello(varchar) RETURNS varcharAS sandbox.PLJava.helloLANGUAGE java;--
-- drop this function
--
DROP FUNCTION mydb.hello(varchar);-- 现在我们可以以与其他任何存储过程相同的方式调用我们的java方法。 部署描述符 这里令人头疼–在安装库时必须显式创建函数而在删除库时将其删除。 除了最简单的情况之外这都是耗时且容易出错的。 幸运的是有一个解决此问题的方法-部署描述符。 精确的格式由ISO / IEC 9075-132003定义但是一个简单的示例就足够了。 SQLActions[] {BEGIN INSTALLCREATE FUNCTION javatest.hello(varchar)RETURNS varcharAS sandbox.PLJava.helloLANGUAGE java;END INSTALL,BEGIN REMOVEDROP FUNCTION javatest.hello(varchar);END REMOVE
} 您必须在jar的MANIFEST.MF文件中告知部署人员有关部署描述符的信息。 一个示例Maven插件是 plugingroupIdorg.apache.maven.plugins/groupIdartifactIdmaven-jar-plugin/artifactIdversion2.3.1/versionconfigurationarchivemanifestSectionsmanifestSectionnamepostgresql.ddr/name !-- filename --manifestEntriesSQLJDeploymentDescriptorTRUE/SQLJDeploymentDescriptor/manifestEntries/manifestSection/manifestSections/archive/configuration
/plugin 现在数据库将在安装和删除我们的方法时知道它们。 内部查询 存储过程的“大赢家”之一是查询是在服务器本身上执行的比通过编程接口运行查询要快得多。 我已经看到了一个过程只需将查询到的循环从客户端移动到服务器就可以通过Java花费30分钟以上将其缩短至不到一秒的时间。 内部连接的JDBC URL是“ jdbcdefaultconnection”。 您不能使用事务因为您处于呼叫者的事务之内但是只要您停留在单个呼叫中就可以使用保存点。 我不知道您是否可以使用CallableStatements还有其他存储过程–您无法使用1.2版但Ubuntu 11.10软件包使用1.4.2版。 标量值列表在Java世界中以迭代器的形式返回 在SQL世界中以SETOF的形式返回 。 public static IteratorString colors() {ListString colors Arrays.asList(red, green, blue);return colors.iterator();} 和 CREATE FUNCTION javatest.colors()RETURNS SETOF varcharAS sandbox.PLJava.colorsIMMUTABLE LANGUAGE java; 我添加了IMMUTABLE关键字因为此函数将始终返回相同的值。 这允许数据库执行缓存和查询优化。 在开始之前您不需要知道结果甚至不需要知道结果的大小。 以下是被认为总是会终止的序列但尚未得到证实。 不幸的是我忘记了序列的名称。作为一个旁注这不是一个完整的解决方案因为它不检查溢出-正确的实现应对此进行检查或使用BigInteger。 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;}Overridepublic boolean hasNext() {return !done;}Overridepublic Integer next() {int value next;next (next % 2 0) ? next / 2 : 3 * next 1;done (value 1);return value;}Overridepublic void remove() {throw new UnsupportedOperationException();}}CREATE FUNCTION javatest.seq(int)RETURNS SETOF intAS sandbox.PLJava.seqIMMUTABLE LANGUAGE java; 在所有条件都相同的情况下最好根据需要创建每个结果。 如果查询具有LIMIT子句通常可以减少内存占用并避免不必要的工作。 单元组 在ResultSet中返回一个元组。 public static boolean singleWord(ResultSet receiver) throws SQLException {receiver.updateString(English, hello);receiver.updateString(Spanish, hola);return true;} 和 CREATE TYPE word AS (English varchar,Spanish varchar);CREATE FUNCTION javatest.single_word()RETURNS wordAS sandbox.PLJava.singleWordIMMUTABLE LANGUAGE java; 返回true表示有效结果返回false表示无效结果。 可以用相同的方式将复杂类型传递给java方法-它是一个只读ResultSet 只包含一行。 元组列表 返回复杂值列表需要一个实现两个接口之一的类。 org.postgresql.pljava.ResultSetProvider 当可以以编程方式或根据需要创建结果时将使用ResultSetProvider 。 public static ResultSetProvider listWords() {return new WordProvider();}public static class WordProvider implements ResultSetProvider {private final MapString,String words new HashMapString,String();private final IteratorString keys;public WordProvider() {words.put(one, uno);words.put(two, dos);words.put(three, tres);words.put(four, quatro);keys words.keySet().iterator();}Overridepublic 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;}Overridepublic void close() throws SQLException {}} 和 CREATE FUNCTION javatest.list_words()RETURNS SETOF wordAS sandbox.PLJava.listWordsIMMUTABLE LANGUAGE java; org.postgresql.pljava.ResultSetHandle 当方法使用内部查询时通常使用ResultSetHandle 。 public static ResultSetHandle listUsers() {return new UsersHandle();}public static class UsersHandle implements ResultSetHandle {private Statement stmt;Overridepublic ResultSet getResultSet() throws SQLException {stmt DriverManager.getConnection(jdbc:default:connection).createStatement();return stmt.executeQuery(SELECT * FROM pg_user);}Overridepublic void close() throws SQLException {stmt.close();} } 和 CREATE FUNCTION javatest.list_users()RETURNS SETOF pg_userAS sandbox.PLJava.listUsersLANGUAGE java; 介面 我无法在标准maven存储库中获得pljava jar的最新副本。 我的解决方案是从PL / Java源tarball提取接口。 为了方便您在此处提供它们。 ResultSetProvider // 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// http://eng.tada.se/osprojects/COPYRIGHT.htmlpackage 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 codeSET OF/code a complex type. //Functions that// return codeSET OF/code a simple type should simply return an// {link java.util.Iterator Iterator}.// author Thomas Hallgrenpublic 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 codetrue/code if a new row was provided, codefalse/code// if not (end of data).// throws SQLExceptionboolean 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 // 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
// http://eng.tada.se/osprojects/COPYRIGHT.htmlpackage 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 codeSET 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 Hallgrenpublic 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 SQLExceptionResultSet 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创建-读取-更新-删除操作中的三个操作之一期间自动运行。 插入 –为触发器提供了新值并且可以修改值或直接禁止操作。 更新 -触发提供旧 的和新的值。 同样它能够修改值或禁止操作。 删除 –为触发器提供旧值。 它不能修改该值但可以禁止该操作。 触发器可以在操作之前或之后运行。 如果要修改值可以在操作之前执行触发器。 如果要记录结果可以在操作后执行它。 典型用法 插入和更新数据验证 插入和更新操作的预触发可用于强制数据完整性和一致性。 在这种情况下结果要么被接受要么被禁止操作。 插入和更新数据标准化和消毒 有时值可能具有多种表示形式或者可能具有危险性。 预触发是清理数据的机会例如整理XML或将替换为和替换为。 所有操作审核日志记录 所有操作的后触发可用于强制执行审核日志记录。 应用程序可以记录自己的操作但不能记录对数据库的直接访问。 这是解决此问题的方法。 可以为每一行或在完成整个语句后运行触发器。 更新触发器也可以是有条件的。 触发器可用于创建“可更新视图”。 PL / Java实现 可以在触发器中使用任何java方法只要它是一个返回void且使用单个参数 TriggerData对象的公共静态方法即可。 触发器可以称为“按行”或“按状态”。 “按行”的TriggerData包含一个单行只读ResultSet作为更新和删除时的“旧”值以及一个单行可更新ResultSet作为插入和更新时的“新”值。 这可用于修改内容记录操作等。 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 librarystatic 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-classfoo%s/foobar%s/bar/my-class, foo, bar);}
}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 triggerAS sandbox.AuditTrigger.auditFoobarLANGUAGE java;CREATE TRIGGER foobar_auditAFTER INSERT OR UPDATE OR DELETE ON javatest.foobarFOR EACH ROWEXECUTE PROCEDURE javatest.audit_foobar(); 规则 PostgreSQL扩展是Rules 。 它们与触发器类似但更加灵活。 一个重要的区别是可以在SELECT语句上触发规则而不仅仅是INSERTUPDATE和DELETE。 规则与触发器不同使用标准函数。 介面 和以前一样我找不到最新版本的Maven存储库为了方便起见我还包含了这些文件。 触发数据 // 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// http://eng.tada.se/osprojects/COPYRIGHT.htmlpackage 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 codeTriggerData/code as their argument.// // author Thomas Hallgrenpublic 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 codeResultSet/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 codeCREAT // 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;// 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 codeCREATE 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 codeCREATE TRIGGER/code statement).
//throws SQLException * if the contained native buffer has gone stale. */String getSchemaName() throws SQLException;
// Returns codetrue/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 codetrue/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 codetrue/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 codetrue/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 codetrue/code if this trigger was fired by a codeDELETE/code. * * throws SQLException * if the contained native //buffer has gone stale. */
boolean isFiredByDelete() throws SQLException;
//Returns codetrue/code if this trigger was fired by an //codeINSERT/code. * * throws SQLException * if the contained native //buffer has gone stale. */
boolean isFiredByInsert() throws SQLException;
//Returns codetrue/code if this trigger was fired by an //codeUPDATE/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 codeCREATE 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 codeCREATE TRIGGER/code statement). * * throws //SQLException * if the contained native buffer has gone stale. */
String getSchemaName() throws SQLException;
//Returns codetrue/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 codetrue/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 codetrue/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 codetrue/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 codetrue/code if this trigger was fired by a //codeDELETE/code. * * throws SQLException * if the contained native //buffer has gone stale. */
boolean isFiredByDelete() throws SQLException;
// Returns codetrue/code if this trigger was fired by an //codeINSERT/code. * * throws SQLException * if the contained native //buffer has gone stale. */
boolean isFiredByInsert() throws SQLException;
// Returns codetrue/code if this trigger was fired by an //codeUPDATE/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 codeCREATE 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 codeCREATE TRIGGER/code statement). * * throws //SQLException * if the contained native buffer has gone stale. */
String getSchemaName() throws SQLException;
/// Returns codetrue/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 codetrue/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 codetrue/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 codetrue/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 codetrue/code if this trigger was fired by a //codeDELETE/code. * * throws SQLException * if the contained native //buffer has gone stale. */
boolean isFiredByDelete() throws SQLException;
// Returns codetrue/code if this trigger was fired by an //codeINSERT/code. * * throws SQLException * if the contained native //buffer has gone stale. */
boolean isFiredByInsert() throws SQLException;
// Returns codetrue/code if this trigger was fired by an //codeUPDATE/code. * * throws SQLException * if the contained native //buffer has gone stale. */
boolean isFiredByUpdate() throws SQLException; } TriggerException // 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// http://eng.tada.se/osprojects/COPYRIGHT.htmlpackage 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 Hallgrenpublic 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 codeTriggerData/code that was// passed to the trigger method.// param td The codeTriggerData/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 codeTriggerData/code that was// passed to the trigger method and an additional message.// param td The codeTriggerData/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);}
} 数据库中用户定义的类型存在争议。 它们不是标准的-在某些时候DBA必须创建它们-这就引入了可移植性问题。 标准工具对此一无所知。 您必须通过ResultSets和PreparedStatements中的“ struct”方法访问它们。 另一方面还有很多东西只能通过byte []支持。 这样可以防止数据库函数和存储过程轻松地操作它们。 什么是好的用户定义类型 它必须是原子的并且必须可以通过存储过程来完成有意义的工作。 注意数据库用户定义类型与Java类不是同一回事。 几乎所有Java类都应存储为标准元组并且只有在有充分理由的情况下才应使用数据库UDT。 我喜欢的试金石是询问您是否曾经想过除了对象本身之外还缓存有关类型而不是元组的不变信息。 例如X.509数字证书具有许多不可变字段这些字段将是有效的搜索词但是为每一行提取该信息非常昂贵。 旁注插入和更新记录时可以使用触发器来提取信息。这可确保缓存的值始终准确。 例子 复数存储过程算术 有理数存储过程算术 galois字段编号存储过程算术运算为固定值 图像存储过程获取尺寸 PDF文档存储过程提取元素 数字证书和私钥存储过程加密 还应该解决的是正确的实施语言。 在PL / Java中创建原型很容易但是您可以提出一个强有力的论点即类型最终应实现为标准的PostgreSQL扩展因为将来您有20岁的历史时它们很可能会出现倾倒。 在某些重要方面这只是问题的一小部分-问题不在于实际的存储和函数实现是用C还是Java编写的而是它与系统其余部分的联系方式。 PL / Java实现 PL / Java用户定义的类型必须实现java.sql.SQLData接口从字符串创建对象的静态方法以及从对象创建字符串的实例方法。 这些方法必须相辅相成–必须有可能在整个方向上沿任一方向运行值并将其取回原始值。 注意双打通常是不可能的–这就是为什么您得到数字4.000000001或2.999999999的原因。 在这些情况下您将尽力而为并警告用户。 在许多情况下可以以二进制格式更有效地存储对象。 在PostgreSQL术语中这些是TOAST类型。 这是通过实现两个与SQLInput和SQLOutput流一起使用的新方法来处理的。 接下来是有理类型的简单实现。 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 normalizationif (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.*/
} 和 /* The shell type */CREATE TYPE javatest.rational;/* The scalar input function */CREATE FUNCTION javatest.rational_in(cstring)RETURNS javatest.rationalAS UDT[sandbox.Rational] inputLANGUAGE java IMMUTABLE STRICT;/* The scalar output function */CREATE FUNCTION javatest.rational_out(javatest.rational)RETURNS cstringAS UDT[sandbox.Rational] outputLANGUAGE java IMMUTABLE STRICT;/* The scalar receive function */CREATE FUNCTION javatest.rational_recv(internal)RETURNS javatest.rationalAS UDT[sandbox.Rational] receiveLANGUAGE java IMMUTABLE STRICT;/* The scalar send function */CREATE FUNCTION javatest.rational_send(javatest.rational)RETURNS byteaAS UDT[sandbox.Rational] sendLANGUAGE 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允许类型具有修饰符。 示例在“ varchar200”或“ numeric8,2”中。 PL / Java当前不支持此功能通过typmod_in和typmod_out方法但是我已经提交了一个请求。 演员表 如果您所能做的就是将值存储和检索为不透明对象则自定义类型并不是特别有用。 为什么不使用bytea并完成它呢 实际上在许多UDT中能够将UDT强制转换为其他类型是有意义的。 像复数或有理数之类的数字类型应该能够与标准整数和浮点数字类型相互转换尽管有限制。 这应该克制。 强制转换作为单参数静态方法实现。 在Java世界中这些方法通常被命名为newInstance因此我在这里也做同样的事情。 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();} 和 CREATE FUNCTION javatest.rational_string_as_rational(varchar) RETURNS javatest.rationalAS sandbox.Rational.newInstanceLANGUAGE JAVA IMMUTABLE STRICT;CREATE FUNCTION javatest.rational_int_as_rational(int4) RETURNS javatest.rationalAS sandbox.Rational.newInstanceLANGUAGE JAVA IMMUTABLE STRICT;CREATE FUNCTION javatest.rational_long_as_rational(int8) RETURNS javatest.rationalAS sandbox.Rational.newInstanceLANGUAGE JAVA IMMUTABLE STRICT;CREATE FUNCTION javatest.rational_as_double(javatest.rational) RETURNS float8AS sandbox.Rational.valueLANGUAGE 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; 旁注 STRICT表示如果任何参数为NULL则该函数将返回NULL。这允许数据库进行一些优化。 旁注如果java对象也是不可变的我们可能只能使用IMMUTABLE标志。我们可能应该使Rational对象不可变因为其他数字类型是不可变的。 汇总功能 min呢 有理数是数字类型因此它们不应该支持所有标准的聚合函数吗 定义新的聚合函数很简单。 简单的集合函数只需要一个带有两个UDT值并返回一个的静态成员函数。 通过最大值最小值总和乘积等可以很容易地看出这一点。更复杂的聚合需要包含状态信息的辅助UDT采用一个状态UDT和一个UDT并返回状态UDT的静态方法以及确定方法采用最终状态UDT并产生结果。 用平均值很容易看到–您需要一个包含计数器和运行总和的状态类型。 以下是前一种类型的聚合函数的一些示例。 // 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());} 和 CREATE FUNCTION javatest.min(javatest.rational, javatest.rational) RETURNS javatest.rationalAS sandbox.Rational.minLANGUAGE JAVA IMMUTABLE STRICT;CREATE FUNCTION javatest.max(javatest.rational, javatest.rational) RETURNS javatest.rationalAS sandbox.Rational.maxLANGUAGE 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用户定义的数据类型来执行封送处理但它可以确保一致性。 DbRationalType是上面的Rational类。 可以在两个地方使用相同的类但是会将对Hibernate接口的依赖性引入PL / Java类。 如果您从Hibernate源代码中提取单个接口这可能是可以接受的。 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;}Overridepublic 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());}Overridepublic Serializable disassemble(Object value) throws HibernateException {if (!(value instanceof Rational)) {throw new HibernateException(invalid argument);}return (Rational) value;}Overridepublic 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());}Overridepublic boolean isMutable() {return true;}//// important: PGobject is postgresql-specific// Overridepublic 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// Overridepublic 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);}}Overridepublic 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());}Overridepublic Class returnedClass() {return Rational.class;}Overridepublic int[] sqlTypes() {return sqlTypesSupported;}Overridepublic String toString() {String value ;if (denominator 1) {value String.valueOf(numerator);} else {value String.format(%d/%d, numerator, denominator);}return value;}// for UserTypeOverridepublic int hashCode(Object value) {Rational r (Rational) value;return (int) (31 * r.getNumerator() r.getDenominator());}Overridepublic int hashCode() {return hashCode(this);}// for UserTypeOverridepublic 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());}Overridepublic boolean equals(Object value) {return equals(this, value);}
} CustomTypes.hbm.xml ?xml version1.0 encodingutf-8?
!DOCTYPE hibernate-mapping PUBLIC-//Hibernate/Hibernate Mapping DTD 3.0//ENhttp://www.hibernate.org/dtd/hibernate-mapping-3.0.dtdhibernate-mappingtypedef namejavatest.rational classsandbox.RationalType//hibernate-mapping TestTable.hbm.xml ?xml version1.0 encodingutf-8?
!DOCTYPE hibernate-mapping PUBLIC-//Hibernate/Hibernate Mapping DTD 3.0//ENhttp://www.hibernate.org/dtd/hibernate-mapping-3.0.dtdhibernate-mappingclass namesandbox.TestTable tabletest_tableid nameid/property namevalue typejavatest.rational //class/hibernate-mapping 经营者 运算符是普通的PL / Java方法也通过CREATE OPERATOR语句标记为运算符。 支持有理数的基本算法为 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());} 和 CREATE FUNCTION javatest.rational_negate(javatest.rational) RETURNS javatest.rationalAS sandbox.Rational.negateLANGUAGE JAVA IMMUTABLE STRICT;CREATE FUNCTION javatest.rational_add(javatest.rational, javatest.rational)RETURNS javatest.rationalAS sandbox.Rational.addLANGUAGE JAVA IMMUTABLE STRICT;CREATE FUNCTION javatest.rational_subtract(javatest.rational, javatest.rational)RETURNS javatest.rationalAS sandbox.Rational.subtractLANGUAGE JAVA IMMUTABLE STRICT;CREATE FUNCTION javatest.rational_multiply(javatest.rational, javatest.rational)RETURNS javatest.rationalAS sandbox.Rational.multiplyLANGUAGE JAVA IMMUTABLE STRICT;CREATE FUNCTION javatest.rational_divide(javatest.rational, javatest.rational)RETURNS javatest.rationalAS sandbox.Rational.divideLANGUAGE 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); 运算符字符是“ – * / 〜”中的1到63个字符。 ^| “” 有一些限制以避免与SQL注释的开头混淆。 换向运算符是第二个运算符可能是相同的如果交换左值和右值其结果相同。 由优化器使用。 否定运算符是一个如果相反交换左值和右值的结果。 它仅对返回布尔值的过程有效。 再次由优化器使用。 订购运营商 可以通过某种方式订购许多UDT。 这可能是显而易见的事情例如对有理数进行排序或者是更加任意的事情例如对复数进行排序。 我们可以按照与上述相同的方式定义排序操作。 注意这些运算符不再有特殊之处–对于不熟悉的UDT您不能假设的真正含义是“小于”。 唯一的例外是“”它始终由解析器重写为“”。 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;} 请注意我已经定义了比较两个有理数或一个有理数和一个双数的方法。 CREATE FUNCTION javatest.rational_lt(javatest.rational, javatest.rational)RETURNS boolAS sandbox.Rational.lessThanLANGUAGE JAVA IMMUTABLE STRICT;CREATE FUNCTION javatest.rational_le(javatest.rational, javatest.rational)RETURNS boolAS sandbox.Rational.lessThanOrEqualsLANGUAGE JAVA IMMUTABLE STRICT;CREATE FUNCTION javatest.rational_eq(javatest.rational, javatest.rational)RETURNS boolAS sandbox.Rational.equalsLANGUAGE JAVA IMMUTABLE STRICT;CREATE FUNCTION javatest.rational_ge(javatest.rational, javatest.rational)RETURNS boolAS sandbox.Rational.greaterThanOrEqualsLANGUAGE JAVA IMMUTABLE STRICT;CREATE FUNCTION javatest.rational_gt(javatest.rational, javatest.rational)RETURNS boolAS sandbox.Rational.greaterThanLANGUAGE JAVA IMMUTABLE STRICT;CREATE FUNCTION javatest.rational_cmp(javatest.rational, javatest.rational)RETURNS intAS sandbox.Rational.compareLANGUAGE JAVA IMMUTABLE STRICT;CREATE FUNCTION javatest.rational_lt(javatest.rational, float8)RETURNS boolAS sandbox.Rational.lessThanLANGUAGE JAVA IMMUTABLE STRICT;CREATE FUNCTION javatest.rational_le(javatest.rational, float8)RETURNS boolAS sandbox.Rational.lessThanOrEqualsLANGUAGE JAVA IMMUTABLE STRICT;CREATE FUNCTION javatest.rational_eq(javatest.rational, float8)RETURNS boolAS sandbox.Rational.equalsLANGUAGE JAVA IMMUTABLE STRICT;CREATE FUNCTION javatest.rational_ge(javatest.rational, float8)RETURNS boolAS sandbox.Rational.greaterThanOrEqualsLANGUAGE JAVA IMMUTABLE STRICT;CREATE FUNCTION javatest.rational_gt(javatest.rational, float8)RETURNS boolAS sandbox.Rational.greaterThanLANGUAGE 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 ); 限制是优化估计程序。 通常使用适当的标准过程是安全的。 Join是一个优化估计器过程。 通常使用适当的标准过程是安全的。 哈希表示该运算符可用于哈希联接。 合并表示可以在合并联接中使用运算符。 指标 索引在三个地方使用–强制执行唯一性约束并加快WHERE和JOIN子句。 -- btree joinCREATE OPERATOR CLASS rational_opsDEFAULT FOR TYPE javatest.rational USING btree ASOPERATOR 1 ,OPERATOR 2 ,OPERATOR 3 ,OPERATOR 4 ,OPERATOR 5 ,FUNCTION 1 javatest.rational_cmp(javatest.rational, javatest.rational);-- hash joinCREATE OPERATOR CLASS rational_opsDEFAULT FOR TYPE javatest.rational USING hash ASOPERATOR 1 ,FUNCTION 1 javatest.rational_hashCode(javatest.rational); 运营商家庭 最后PostgreSQL具有“操作者家族”的概念该概念将相关的操作者类别归为一类。 例如您可能有一个家族支持int2int4和int8值之间的交叉比较。 每个都可以单独指定但是通过创建一个运算符族您可以给PostgreSQL优化器更多提示。 更多信息 创建类型PostgreSQL PostgreSQL的“创建触发器”文档 。 PostgreSQL的“创建规则”文档 。 Java 创建运算符PostgreSQL 创建操作员类PostgreSQL 创建操作员家庭PostgreSQL 运算符优化PostgreSQL 连接索引扩展PostreSQL 用Java创建标量UDT 用户指南 CREATE AGGREGATE文档PostgreSQL 创建CAST文档PostgreSQL 创建类型文档PostgreSQL 创建操作员文档PostgreSQL 创建操作员类文档PostgreSQL 将用户定义的类型与索引接口PostgreSQL 参考 PostgreSQL PL / Java简介第1部分 PostgreSQL PL / Java简介第2部分使用列表 PostgreSQL PL / Java简介第3部分触发器 PostgreSQL PL / Java简介第4部分用户定义类型 PostgreSQL / PLJava简介第5部分我们的JCG合作伙伴 Bear Giles在Invariant Properties博客上提供。 翻译自: https://www.javacodegeeks.com/2012/10/introduction-to-postgresql-pljava.html