Some of the biggest limitations when working with Oracle PL/SQL from Java is the lack of support for a variety of PL/SQL features through the JDBC interface. This lack of support is actually not limited to JDBC, but also extends to Oracle SQL. For instance, if you’re using the useful PL/SQL BOOLEAN type as such:
CREATE OR REPLACE FUNCTION yes RETURN boolean AS BEGIN RETURN true; END yes; /
It would now be terrific if you could do this:
SELECT yes FROM dual;
But it’s not possible. You’ll be getting an error along the lines of the following:
ORA-06552: PL/SQL: Statement ignored ORA-06553: PLS-382: expression is of wrong type 06552. 00000 - "PL/SQL: %s" *Cause: *Action:
It’s crazy to think that the Oracle SQL language still doesn’t support the SQL standard boolean type, which is so useful as I’ve shown in previous blog posts. Here’s where you can upvote the feature request:
https://community.oracle.com/ideas/2633
BOOLEAN isn’t the only “inaccessible” SQL feature
Now, there are a couple of other data types, which cannot be “bridged” to the SQL engine, and thus (for some reason only the OJDBC driver gods can fathom) cannot be “bridged” to a JDBC client. Among them: The very useful PL/SQL RECORD
type.
Very often, you want to do this:
CREATE PACKAGE customers AS TYPE person IS RECORD ( first_name VARCHAR2(50), last_name VARCHAR2(50) ); FUNCTION get_customer(p_customer_id NUMBER) RETURN person; END customers; / CREATE PACKAGE BODY customers AS FUNCTION get_customer(p_customer_id NUMBER) RETURN person IS v_person customers.person; BEGIN SELECT c.first_name, c.last_name INTO v_person FROM customer c WHERE c.customer_id = p_customer_id; RETURN v_person; END get_customer; END customers; /
(we’re running this on the SAKILA database).
As in any language with the least bit of sophistication, we can define “structs” or records in PL/SQL, which we can now frequently reuse. Everyone knows what a PERSON
is and we can pass them around between procedures and functions.
For instance, in PL/SQL client code:
SET SERVEROUTPUT ON DECLARE v_person customers.person; BEGIN v_person := customers.get_customer(1); dbms_output.put_line(v_person.first_name); dbms_output.put_line(v_person.last_name); END; /
… which yields:
MARY SMITH
What about JDBC client code?
After having added support for PL/SQL BOOLEAN
types in jOOQ 3.9, with jOOQ 3.9, we now finally support PL/SQL record types in stored procedures as well, at least in standalone calls, which are not embedded in SQL statements. The jOOQ code generator will pick up all of these package-level PL/SQL record types and their structures and generate the boring boiler plate code for you. E.g. (simplified):
package org.jooq.demo.sakila.packages.customers.records; public class PersonRecord extends UDTRecordImpl<PersonRecord> { public void setFirstName(String value) { ... } public String getFirstName() { ... } public void setLastName(String value) { ... } public String getLastName() { ... } public PersonRecord() { ... } public PersonRecord(String firstName, String lastName) { ... } }
Notice how jOOQ doesn’t really make any difference in its API between the generated code for an Oracle SQL OBJECT
type or an Oracle PL/SQL RECORD
type. They’re essentially the same thing (from a jOOQ API perspective).
More interesting, what happened to the generated package and the function? This code is generated (simplified):
public class Customers extends PackageImpl { public static PersonRecord getCustomer( Configuration configuration, Long pCustomerId ) { ... } }
That’s all! So all we now need to do is pass the ubiquitous jOOQ Configuration
(which contains information such as SQLDialect or JDBC Connection) and the actual stored function parameter, the P_CUSTOMER_ID
value, and we’re done!
This is how jOOQ client code might look:
PersonRecord person = Customers.getCustomer(configuration(), 1L); System.out.println(person);
As you can see, this is just the same thing as the corresponding PL/SQL code. And the output of this println
call is this:
SAKILA.CUSTOMERS.PERSON('MARY', 'SMITH')
A fully qualified RECORD
declaration with schema, package, and type name.
How does it work?
Let’s turn on jOOQ’s built-in TRACE logging to see what jOOQ did behind the scenes:
Calling routine : DECLARE r1 "CUSTOMERS"."PERSON"; BEGIN r1 := "CUSTOMERS"."GET_CUSTOMER"("P_CUSTOMER_ID" => ?); ? := r1."FIRST_NAME"; ? := r1."LAST_NAME"; END; Binding variable 1 : 1 (class java.lang.Long) Registering variable 2 : class java.lang.String Registering variable 3 : class java.lang.String Fetched OUT parameters : +-----------------+ : |RETURN_VALUE | : +-----------------+ : |('MARY', 'SMITH')| : +-----------------+
So, jOOQ usually doesn’t use JDBC’s very limited escape syntax to call stored procedures, it just produces an anonymous PL/SQL block with a local variable of type CUSTOMER.PERSON
, i.e. of our RECORD
type. The function call is then assigned to this local variable, and the local variable is descructured into its individual parts.
In the TRACE log, you can see the individual bind variables, i.e. there’s an IN variable at index 1 of type Long, and two OUT variables of type String at indexes 2 and 3.
How does jOOQ know the record types?
At runtime, all the information is hard-wired to the generated code. So, the magic is inside of the code generator. Warning: Some serious SQL ahead!
This beauty here queries the dictionary views for PL/SQL record types:
AllArguments a = ALL_ARGUMENTS.as("a"); AllArguments x = ALL_ARGUMENTS.as("x"); Field<BigDecimal> nextSibling = field(name("next_sibling"), x.SEQUENCE.getDataType()); DSL.using(configuration) .select(x.TYPE_OWNER, x.TYPE_NAME, x.TYPE_SUBNAME) .select( a.ARGUMENT_NAME .as(ALL_TYPE_ATTRS.ATTR_NAME), a.SEQUENCE .as(ALL_TYPE_ATTRS.ATTR_NO), a.TYPE_OWNER .as(ALL_TYPE_ATTRS.ATTR_TYPE_OWNER), nvl2(a.TYPE_SUBNAME, a.TYPE_NAME, inline(null, a.TYPE_NAME)) .as("package_name"), coalesce(a.TYPE_SUBNAME, a.TYPE_NAME, a.DATA_TYPE) .as(ALL_TYPE_ATTRS.ATTR_TYPE_NAME), a.DATA_LENGTH .as(ALL_TYPE_ATTRS.LENGTH), a.DATA_PRECISION .as(ALL_TYPE_ATTRS.PRECISION), a.DATA_SCALE .as(ALL_TYPE_ATTRS.SCALE)) .from(a) .join(table( select( a.TYPE_OWNER, a.TYPE_NAME, a.TYPE_SUBNAME, min(a.OWNER ).keepDenseRankFirstOrderBy(a.OWNER, a.PACKAGE_NAME, a.SUBPROGRAM_ID, a.SEQUENCE).as(a.OWNER), min(a.PACKAGE_NAME ).keepDenseRankFirstOrderBy(a.OWNER, a.PACKAGE_NAME, a.SUBPROGRAM_ID, a.SEQUENCE).as(a.PACKAGE_NAME), min(a.SUBPROGRAM_ID).keepDenseRankFirstOrderBy(a.OWNER, a.PACKAGE_NAME, a.SUBPROGRAM_ID, a.SEQUENCE).as(a.SUBPROGRAM_ID), min(a.SEQUENCE ).keepDenseRankFirstOrderBy(a.OWNER, a.PACKAGE_NAME, a.SUBPROGRAM_ID, a.SEQUENCE).as(a.SEQUENCE), min(nextSibling ).keepDenseRankFirstOrderBy(a.OWNER, a.PACKAGE_NAME, a.SUBPROGRAM_ID, a.SEQUENCE).as(nextSibling), min(a.DATA_LEVEL ).keepDenseRankFirstOrderBy(a.OWNER, a.PACKAGE_NAME, a.SUBPROGRAM_ID, a.SEQUENCE).as(a.DATA_LEVEL)) .from(table( select( lead(a.SEQUENCE, 1, a.SEQUENCE).over( partitionBy(a.OWNER, a.PACKAGE_NAME, a.SUBPROGRAM_ID, a.DATA_LEVEL) .orderBy(a.SEQUENCE) ).as("next_sibling"), a.TYPE_OWNER, a.TYPE_NAME, a.TYPE_SUBNAME, a.OWNER, a.PACKAGE_NAME, a.SUBPROGRAM_ID, a.SEQUENCE, a.DATA_LEVEL, a.DATA_TYPE) .from(a) .where(a.OWNER.in(getInputSchemata())) ).as("a")) .where(a.TYPE_OWNER.in(getInputSchemata())) .and(a.OWNER.in(getInputSchemata())) .and(a.DATA_TYPE.eq("PL/SQL RECORD")) .groupBy(a.TYPE_OWNER, a.TYPE_NAME, a.TYPE_SUBNAME) ).as("x")) .on(row(a.OWNER, a.PACKAGE_NAME, a.SUBPROGRAM_ID).eq(x.OWNER, x.PACKAGE_NAME, x.SUBPROGRAM_ID)) .and(a.SEQUENCE.between(x.SEQUENCE).and(nextSibling)) .and(a.DATA_LEVEL.eq(x.DATA_LEVEL.plus(one()))) .orderBy(x.TYPE_OWNER, x.TYPE_NAME, x.TYPE_SUBNAME, a.SEQUENCE) .fetch();
This is a nice little jOOQ query, which corresponds to the following equially impressive SQL query, which you can run directly in your SQL developer, or some other SQL client for Oracle:
SELECT "x"."TYPE_OWNER", "x"."TYPE_NAME", "x"."TYPE_SUBNAME", "a"."ARGUMENT_NAME" "ATTR_NAME", "a"."SEQUENCE" "ATTR_NO", "a"."TYPE_OWNER" "ATTR_TYPE_OWNER", nvl2("a"."TYPE_SUBNAME", "a"."TYPE_NAME", NULL) "package_name", COALESCE("a"."TYPE_SUBNAME", "a"."TYPE_NAME", "a"."DATA_TYPE") "ATTR_TYPE_NAME", "a"."DATA_LENGTH" "LENGTH", "a"."DATA_PRECISION" "PRECISION", "a"."DATA_SCALE" "SCALE" FROM "SYS"."ALL_ARGUMENTS" "a" JOIN ( SELECT "a"."TYPE_OWNER", "a"."TYPE_NAME", "a"."TYPE_SUBNAME", MIN("a"."OWNER") KEEP (DENSE_RANK FIRST ORDER BY "a"."OWNER" ASC, "a"."PACKAGE_NAME" ASC, "a"."SUBPROGRAM_ID" ASC, "a"."SEQUENCE" ASC) "OWNER", MIN("a"."PACKAGE_NAME") KEEP (DENSE_RANK FIRST ORDER BY "a"."OWNER" ASC, "a"."PACKAGE_NAME" ASC, "a"."SUBPROGRAM_ID" ASC, "a"."SEQUENCE" ASC) "PACKAGE_NAME", MIN("a"."SUBPROGRAM_ID") KEEP (DENSE_RANK FIRST ORDER BY "a"."OWNER" ASC, "a"."PACKAGE_NAME" ASC, "a"."SUBPROGRAM_ID" ASC, "a"."SEQUENCE" ASC) "SUBPROGRAM_ID", MIN("a"."SEQUENCE") KEEP (DENSE_RANK FIRST ORDER BY "a"."OWNER" ASC, "a"."PACKAGE_NAME" ASC, "a"."SUBPROGRAM_ID" ASC, "a"."SEQUENCE" ASC) "SEQUENCE", MIN("next_sibling") KEEP (DENSE_RANK FIRST ORDER BY "a"."OWNER" ASC, "a"."PACKAGE_NAME" ASC, "a"."SUBPROGRAM_ID" ASC, "a"."SEQUENCE" ASC) "next_sibling", MIN("a"."DATA_LEVEL") KEEP (DENSE_RANK FIRST ORDER BY "a"."OWNER" ASC, "a"."PACKAGE_NAME" ASC, "a"."SUBPROGRAM_ID" ASC, "a"."SEQUENCE" ASC) "DATA_LEVEL" FROM ( SELECT lead("a"."SEQUENCE", 1, "a"."SEQUENCE") OVER ( PARTITION BY "a"."OWNER", "a"."PACKAGE_NAME", "a"."SUBPROGRAM_ID", "a"."DATA_LEVEL" ORDER BY "a"."SEQUENCE" ASC ) "next_sibling", "a"."TYPE_OWNER", "a"."TYPE_NAME", "a"."TYPE_SUBNAME", "a"."OWNER", "a"."PACKAGE_NAME", "a"."SUBPROGRAM_ID", "a"."SEQUENCE", "a"."DATA_LEVEL", "a"."DATA_TYPE" FROM "SYS"."ALL_ARGUMENTS" "a" WHERE "a"."OWNER" IN ('SAKILA', 'SYS') -- Possibly replace schema here ) "a" WHERE ("a"."TYPE_OWNER" IN ('SAKILA', 'SYS') -- Possibly replace schema here AND "a"."OWNER" IN ('SAKILA', 'SYS') -- Possibly replace schema here AND "a"."DATA_TYPE" = 'PL/SQL RECORD') GROUP BY "a"."TYPE_OWNER", "a"."TYPE_NAME", "a"."TYPE_SUBNAME" ) "x" ON (("a"."OWNER", "a"."PACKAGE_NAME", "a"."SUBPROGRAM_ID") = (("x"."OWNER", "x"."PACKAGE_NAME", "x"."SUBPROGRAM_ID")) AND "a"."SEQUENCE" BETWEEN "x"."SEQUENCE" AND "next_sibling" AND "a"."DATA_LEVEL" = ("x"."DATA_LEVEL" + 1)) ORDER BY "x"."TYPE_OWNER" ASC, "x"."TYPE_NAME" ASC, "x"."TYPE_SUBNAME" ASC, "a"."SEQUENCE" ASC
Whew.
The output shows that we got all the required information for our RECORD
type:
[ TYPE INFO COLUMNS ] ATTR_NAME ATTR_TYPE_NAME LENGTH SAKILA CUSTOMERS PERSON FIRST_NAME VARCHAR2 50 SAKILA CUSTOMERS PERSON LAST_NAME VARCHAR2 50
All of this also works for:
- Nested types
- Multiple IN and OUT parameters
I’ll blog about a more advanced use-case in the near future, so stay tuned.
Filed under: jooq-development, sql Tagged: jooq, Oracle, PL/SQL, RECORD types, sql
