Every employee of example.com
is required to write a function that says Hello, world!
. There is no escape from this requirement. This page is dedicated to all those example.com
employees faced with that terrible task.
Clearly, a function that only returns Hello, world!
is useless for illustrating how to pass parameters. So, the goal will be a function that works like this:
# \df List of functions Schema | Name | Result data type | Argument data types | Type --------+-------+-------------------+--------------------------+-------- public | hello | character varying | towhom character varying | normal (1 row) # select hello('world'); hello --------------- Hello, world! (1 row)
Employees of example.com
never forget to put their Java code in a package that begins with com.example
, so very quickly this program takes shape:
package com.example.proj; public class Hello { public static String hello(String toWhom) { return "Hello, " + toWhom + "!"; } }
Such a small program might not need a build system like Maven, but like any project, the requirements could grow over time, so it might as well be set up right from the beginning.
In preparation, PL/Java must have been built, using the command mvn clean install
. Recall from the installation page that the install
Maven goal has nothing to do with installing PL/Java into PostgreSQL, but does register PL/Java in your Maven repository. With that done, any project of your own needs only declare pljava-api
as a dependency, and Maven can compile and package it for you.
The project begins as an empty directory (named proj
in this example), and the first thing to go in that directory is a pom.xml
file. The Project Object Model is what tells Maven all it needs to know about this project. It is XML and verbose, and more lines than the Hello program itself! But it is mostly unchanging boilerplate and, as you can see, only has a few places to change for information specific to the project.
An important Maven feature is POM inheritance. In an organization with many similar projects, there might be one pom.xml
like this one, and many individual projects with shorter pom.xml
files naming this as the parent.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation= "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion>4.0.0</modelVersion> <!-- Above is all boilerplate. Next: your project's "Maven coordinates" --> <groupId>com.example</groupId> <artifactId>proj</artifactId> <version>0.0.1-SNAPSHOT</version> <!-- Coordinates are nice, but so are names and descriptions for humans. --> <name>Hello in PL/Java</name> <description>Project that provides a Hello function</description> <!-- Many Maven plugins care what character set encoding your files are in. For this example I've chosen the most restrictive (US-ASCII). Change if your files use a different encoding, but be sure not to lie. You should be sure the encoding named here IS the way your source files are coded. --> <properties> <project.build.sourceEncoding>US-ASCII</project.build.sourceEncoding> </properties> <!-- Here's where you say your project depends on a pljava-api version. --> <dependencies> <dependency> <groupId>org.postgresql</groupId> <artifactId>pljava-api</artifactId> <version>1.6.4</version> </dependency> </dependencies> <!-- The rest here is pretty much boilerplate. --> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <release>9</release> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.6</version> <configuration> <archive> <manifest> <!-- This identifies and version-stamps the jar. Not essential, but easy and useful. --> <addDefaultImplementationEntries> true </addDefaultImplementationEntries> </manifest> <manifestSections> <!-- This identifies a file in the jar named pljava.ddr as an SQLJDeploymentDescriptor. --> <manifestSection> <name>pljava.ddr</name> <manifestEntries> <SQLJDeploymentDescriptor> true </SQLJDeploymentDescriptor> </manifestEntries> </manifestSection> </manifestSections> </archive> </configuration> </plugin> </plugins> </build> </project>
So far, so good. There is a new proj
directory with only this pom.xml
file in it:
proj | pom.xml
What happens if mvn clean package
is run in this directory, even before any Java code has been written?
[WARNING] JAR will be empty - no content was marked for inclusion! [INFO] Building jar: /var/tmp/proj/target/proj-0.0.1-SNAPSHOT.jar [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------
Now there is a target
subdirectory with something in it:
proj | pom.xml | target | proj-0.0.1-SNAPSHOT.jar
Although Maven warned the jar would be empty, in fact it is not completely empty:
$ jar tf target/proj-0.0.1-SNAPSHOT.jar META-INF/ META-INF/MANIFEST.MF META-INF/maven/ META-INF/maven/com.example/ META-INF/maven/com.example/proj/ META-INF/maven/com.example/proj/pom.xml META-INF/maven/com.example/proj/pom.properties
It has a manifest file, and a couple of files Maven adds to save information about the build. What is in the manifest?
$ unzip -c target/proj-0.0.1-SNAPSHOT.jar META-INF/MANIFEST.MF Manifest-Version: 1.0 Implementation-Title: Hello in PL/Java Implementation-Version: 0.0.1-SNAPSHOT Implementation-Vendor-Id: com.example Name: pljava.ddr SQLJDeploymentDescriptor: true
Clearly, Maven did what the POM told it to do. It created a manifest with naming and version information for the project, and declaring that the file pljava.ddr
(if there were such a file in the jar) is special because it is an SQLJ deployment descriptor.
This proves that Maven can successfully build an empty project with no code!
Maven has a convention over configuration philosophy: it has strict expectations of how the project directories will be laid out, and if those expectations are followed, it knows what to do, without need to add more information in the POM. Sources go in src
, and they are split between main
and test
for sources that are only built for tests. Sources written in Java go in a java
subdirectory. So, the Java class that was shown earlier could be saved as Hello.java
, here:
proj | pom.xml | src | | main | | java | | Hello.java | target | proj-0.0.1-SNAPSHOT.jar
After running mvn clean package
again, what is now in the jar?
$ jar tf target/proj-0.0.1-SNAPSHOT.jar META-INF/ META-INF/MANIFEST.MF com/ com/example/ com/example/proj/ com/example/proj/Hello.class META-INF/maven/ META-INF/maven/com.example/ META-INF/maven/com.example/proj/ META-INF/maven/com.example/proj/pom.xml META-INF/maven/com.example/proj/pom.properties
This is good progress. The class file is placed in the jar at its correct, package-based path, even though the Java file was saved directly in src/main/java
. That is convenient for such a small project. A larger project with many classes would probably organize the source files into package-based subdirectories of src/main/java
also.
But something is still missing from this jar. It does not contain any pljava.ddr
file to tell PL/Java what to do when loading it.
That can be fixed by adding two lines to the Java code:
package com.example.proj; import org.postgresql.pljava.annotation.Function; public class Hello { @Function public static String hello(String toWhom) { return "Hello, " + toWhom + "!"; } }
The @Function annotation declares that the hello
function should be available from SQL, so a pljava.ddr
file will be added to the jar, containing the SQL commands to make that happen.
One more try with mvn clean package
and there it is:
$ jar tf target/proj-0.0.1-SNAPSHOT.jar META-INF/ META-INF/MANIFEST.MF com/ com/example/ com/example/proj/ com/example/proj/Hello.class pljava.ddr META-INF/maven/ META-INF/maven/com.example/ META-INF/maven/com.example/proj/ META-INF/maven/com.example/proj/pom.xml META-INF/maven/com.example/proj/pom.properties
Curious about what is in the pljava.ddr
file?
$ unzip -c target/proj-0.0.1-SNAPSHOT.jar pljava.ddr SQLActions[]={ "BEGIN INSTALL BEGIN PostgreSQL CREATE OR REPLACE FUNCTION hello( toWhom pg_catalog.varchar) RETURNS pg_catalog.varchar LANGUAGE java VOLATILE AS 'java.lang.String=com.example.proj.Hello.hello(java.lang.String)' END PostgreSQL; END INSTALL", "BEGIN REMOVE BEGIN PostgreSQL DROP FUNCTION hello( toWhom pg_catalog.varchar) END PostgreSQL; END REMOVE" }
There you have it. A jar file containing the new class, and the instructions PL/Java needs when installing or removing it.
The time has come to load this jar file into PostgreSQL and try it out! Within PL/Java, each jar has a short name; this one can be myjar
. The final true
parameter to install_jar
means that the deployment descriptor commands should be used.
# select sqlj.install_jar( 'file:/home/me/proj/target/proj-0.0.1-SNAPSHOT.jar', 'myjar', true); install_jar ------------- (1 row)
The result returned by install_jar
isn’t very interesting, but it does not show an error, so is the function ready to use?
# \df List of functions Schema | Name | Result data type | Argument data types | Type --------+-------+-------------------+--------------------------+-------- public | hello | character varying | towhom character varying | normal (1 row) # select hello('world'); ERROR: java.lang.ClassNotFoundException: com.example.proj.Hello
Not so fast! In PL/Java, there is a classpath for every schema. (This is not quite what the SQL-JRT standard intended, but it is manageable for some related functions grouped into a schema.) The hello
function was put into the public
schema. Why could the class not be found?
# select sqlj.get_classpath('public'); get_classpath --------------- (1 row)
An empty classpath would have that effect. The short name myjar
should be added.
# select sqlj.set_classpath('public', 'myjar'); set_classpath --------------- (1 row) # select hello('world'); hello --------------- Hello, world! (1 row)
Success!
The function says hello, but it also does this:
# select hello(null); hello -------------- Hello, null! (1 row)
The function has not been written to notice when the parameter is null. In this case, Java substitutes the word null
and nothing worse happens, but perhaps the function should do something different. If the function should return null whenever a parameter is null, there is no need to even add any code to the function. It can be annotated to declare that behavior, and then PostgreSQL will consider it to return null without even calling the function any time a parameter is null.
@Function(onNullInput=Function.OnNullInput.RETURNS_NULL) public static String hello(String toWhom) { ...
Another optimization suggests itself because the hello
function has no side effects, and its result depends on nothing except the parameter passed to it. By default, functions are assumed to possibly have side effects, depend on database contents or outside influences, and otherwise be harder to reason about. The PostgreSQL optimizer will be helped if this function is declared IMMUTABLE
. That leads to a program like this:
package com.example.proj; import org.postgresql.pljava.annotation.Function; import static org.postgresql.pljava.annotation.Function.Effects.IMMUTABLE; import static org.postgresql.pljava.annotation.Function.OnNullInput.RETURNS_NULL; public class Hello { @Function(onNullInput=RETURNS_NULL, effects=IMMUTABLE) public static String hello(String toWhom) { return "Hello, " + toWhom + "!"; } }
From here, consider: