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:
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, inline(new BigDecimal(99999999))).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, 99999999) 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.
Like this:
Like Loading...
That’s awesome! I’ve been having issues working with RECORD TYPES with JDBC, I was obligated to use ORACLE TYPES which in case it’s more verbose and harder to maintain.
Looking for solutions on Google to support RECORD TYPES was frustrating, the best solution I found was executing an anonymous PLSQL block as you did, but unfortunately I had to use ORACLE TYPES due to deadline.
Your solution is amazing and explaining it was the best part of your post. Thanks! Keep going with this great job!
Thank you very much for your feedback, I appreciate it! I hope you will be able to profit from this sometime soon. Yes, I do think as well that it shouldn’t be as frustrating to integrate with RECORD types. I think that JPublisher also makes them accessible through lower level (perhaps undocumented) OCI calls, but JPublisher is now deprecated, so this did come in the right moment…
can you please provide me the detailed code