One of the great features of Oracle is the ability to create a table function. That is, a function returning a collection type that can be used as the source for a query. These functions then allow a developer to provide a simple SQL api to query rows and columns from a variety of data sources. Any piece of information you can retrieve with PL/SQL may then be returned as a collection.
However, as robust as PL/SQL is, there are times when it simply lacks the functionality to achieve a particular purpose. When this happens it maybe useful to turn to Java stored procedures as an option. Alternately, it may be that a working Java class already exists and, even if possible, there could be little or no value in rewriting the class in PL/SQL.
A few weeks ago I wrote an article showing how to use a Java stored procedure to return a single value from a SQL Server database as an Oracle function usable within SQL or PL/SQL statements. In this article I’ll extend the same idea, but instead of returning just one value, the function will return a nested-table collection of objects.
For this function, the goal is to return a list of items ordered from each salesperson.
SELECT p.personid saldpersonid, p.fullname salesperson, o.customerid, o.orderid, o.orderdate, l.orderlineid, l.unitprice, l.quantity, i.stockitemname FROM application.people p JOIN sales.orders o ON o.salespersonpersonid = p.personid JOIN sales.orderlines l ON l.orderid = o.orderid JOIN warehouse.stockitems i ON i.stockitemid = l.stockitemid;
SALESPERSONID | SALESPERSON | CUSTOMERID | ORDERID | ORDERDATE | ORDERLINEID | UNITPRICE | QUANTITY | STOCKITEMNAME |
---|---|---|---|---|---|---|---|---|
13 |
Hudson Hollinworth | 185 |
44773 |
2015-03-11 | 141507 |
2.9 |
182 |
Clear packaging tape 48mmx75m |
15 |
Taj Shand | 930 |
18428 |
2013-12-11 | 58433 |
18 |
24 |
"The Gu" red shirt XML tag t-shirt (Black) L |
6 |
Sophia Hinton | 85 |
29217 |
2014-06-19 | 92634 |
32 |
8 |
Animal with big feet slippers (Brown) L |
7 |
Amy Trefl | 1004 |
8919 |
2013-06-18 | 28345 |
32 |
2 |
Dinosaur battery-powered slippers (Green) M |
3 |
Hudson Onslow | 531 |
68540 |
2016-03-16 | 215660 |
18 |
40 |
20 mm Double sided bubble wrap 10m |
2 |
Kayla Woodcock | 402 |
44893 |
2015-03-12 | 141891 |
18.5 |
40 |
Office cube periscope (Black) |
First, we’ll need an object type to hold each row’s data, and a collection type to return a set of those objects.
CREATE OR REPLACE TYPE wwi_salesperson_lineitem_type
AS OBJECT
(
salespersonid INTEGER,
salesperson VARCHAR2(50),
customerid INTEGER,
orderid INTEGER,
orderdate DATE,
orderlineid INTEGER,
unitprice NUMBER,
quantity INTEGER,
stockitemname VARCHAR2(100)
);
/
CREATE OR REPLACE TYPE wwi_salesperson_lineitem_tab
AS TABLE OF wwi_salesperson_lineitem_type;
/
The JDBC and query portion is essentially the same as in the previous article; except instead of single value, we will iterate through a result set. For each row, append STRUCT object to a java array. When the array is full, we then use an ArrayDescriptor to create an oracle.sql.ARRAY from our java array. Also of note, the function will not only have a jdbc connection to the SQL Server database, it will also utilize the local default connection to the Oracle database as well.
CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED SDS.WWI_SALESPERSON_LINEITEMS RESOLVER ((* SDS)(* SQLJDBC)(* PUBLIC)) as import java.sql.*; import java.util.*; import com.microsoft.sqlserver.jdbc.*; import oracle.sql.*; import oracle.jdbc.driver.*; public class wwi_salesperson_lineitems { public static oracle.sql.ARRAY get_items( String p_server, String p_instance, int p_port, String p_database, String p_user, String p_password, int p_personid ) throws Exception { SQLServerDataSource ds = new SQLServerDataSource(); ds.setServerName(p_server); if (p_instance != null) { ds.setInstanceName(p_instance); } ds.setPortNumber(p_port); ds.setDatabaseName(p_database); ds.setUser(p_user); ds.setPassword(p_password); Connection localconn = DriverManager.getConnection("jdbc:default:connection:"); StructDescriptor oneItem = StructDescriptor.createDescriptor("WWI_SALESPERSON_LINEITEM_TYPE", localconn); Object[] salesItem = new Object[9]; Connection remoteconn = ds.getConnection(); PreparedStatement pstmt = remoteconn.prepareStatement( "SELECT p.personid salespersonid," + " p.fullname salesperson," + " o.customerid," + " o.orderid," + " o.orderdate," + " l.orderlineid," + " l.unitprice," + " l.quantity," + " i.stockitemname," + " count(*) over() cnt" + " FROM application.people p" + " JOIN sales.orders o ON o.salespersonpersonid = p.personid" + " JOIN sales.orderlines l ON l.orderid = o.orderid" + " JOIN warehouse.stockitems i ON i.stockitemid = l.stockitemid" + " WHERE ? < 1 or p.personid = ?"); pstmt.setInt(1, p_personid); pstmt.setInt(2, p_personid); ResultSet rs = pstmt.executeQuery(); STRUCT[] temparray = null; int i=0; while (rs.next()) { if (i==0) { temparray = new STRUCT[rs.getInt("cnt")]; } salesItem[0] = rs.getInt("salespersonid"); salesItem[1] = rs.getString("salesperson"); salesItem[2] = rs.getInt("customerid"); salesItem[3] = rs.getInt("orderid"); salesItem[4] = rs.getDate("orderdate"); salesItem[5] = rs.getInt("orderlineid"); salesItem[6] = rs.getFloat("unitprice"); salesItem[7] = rs.getInt("quantity"); salesItem[8] = rs.getString("stockitemname"); temparray[i++] = new STRUCT(oneItem, localconn, salesItem); } rs.close(); remoteconn.close(); ArrayDescriptor SalesItemArray = ArrayDescriptor.createDescriptor("WWI_SALESPERSON_LINEITEM_TAB", localconn); return new ARRAY(SalesItemArray, localconn, temparray); } } /
Then of course we need a pl/sql api to expose the java function.
CREATE OR REPLACE FUNCTION sds.wwi_get_salesperson_items( p_server IN VARCHAR2, p_instance IN VARCHAR2, p_port IN NUMBER, p_database IN VARCHAR2, p_user IN VARCHAR2, p_password IN VARCHAR2, p_personid IN NUMBER) RETURN wwi_salesperson_lineitem_tab IS LANGUAGE JAVA NAME 'wwi_salesperson_lineitems.get_items( java.lang.String, java.lang.String, int, java.lang.String, java.lang.String, java.lang.String, int) return oracle.sql.ARRAY' ;
Finally, use the function in a query. I'll look up the first rows for salesid 7.
SELECT * FROM TABLE(wwi_get_salesperson_items('my_remote_server', NULL, 1433, 'WideWorldImporters', 'wwi', 'pa55w0rd', 7)) ORDER BY orderdate
SALESPERSONID | SALESPERSON | CUSTOMERID | ORDERID | ORDERDATE | ORDERLINEID | UNITPRICE | QUANTITY | STOCKITEMNAME |
---|---|---|---|---|---|---|---|---|
7 |
Amy Trefl | 949 |
19 |
2013-01-01 | 70 |
13 |
7 |
DBA joke mug - it depends (White) |
7 |
Amy Trefl | 949 |
59 |
2013-01-01 | 69 |
99 |
60 |
10 mm Anti static bubble wrap (Blue) 50m |
7 |
Amy Trefl | 964 |
8 |
2013-01-01 | 30 |
13 |
1 |
Developer joke mug - old C developers never die (White) |
7 |
Amy Trefl | 173 |
23 |
2013-01-01 | 80 |
18 |
24 |
Halloween zombie mask (Light Brown) L |
7 |
Amy Trefl | 173 |
23 |
2013-01-01 | 83 |
13 |
3 |
IT joke mug - that behavior is by design (White) |
7 |
Amy Trefl | 949 |
59 |
2013-01-01 | 68 |
18 |
108 |
"The Gu" red shirt XML tag t-shirt (White) 7XL |
7 |
Amy Trefl | 77 |
51 |
2013-01-01 | 31 |
18 |
24 |
"The Gu" red shirt XML tag t-shirt (Black) XL |
7 |
Amy Trefl | 964 |
8 |
2013-01-01 | 27 |
32 |
4 |
USB food flash drive - banana |
7 |
Amy Trefl | 77 |
9 |
2013-01-01 | 33 |
32 |
4 |
Animal with big feet slippers (Brown) S |
7 |
Amy Trefl | 173 |
23 |
2013-01-01 | 81 |
32 |
2 |
Dinosaur battery-powered slippers (Green) XL |
7 |
Amy Trefl | 105 |
47 |
2013-01-01 | 10 |
230 |
9 |
Ride on toy sedan car (Pink) 1/12 scale |
Another use case, possibly more common, is reading file lists from the host server. The idea is the same, create an object to hold the rows, a collection to hold the objects, iterate through files creating a STRUCT to add to an array, and finally convert the array to an oracle.sql.ARRAY. By default most users won't have access to read the server directories. If this functionality is required, your DBA will need to grant the appropriate permissions.
Again, first create the SQL objects.
CREATE OR REPLACE TYPE file_type
AS OBJECT
(
file_name VARCHAR2(512),
file_size NUMBER(15, 0),
file_type VARCHAR2(1),
last_modified DATE,
readable VARCHAR2(1),
writeable VARCHAR2(1)
);
/
CREATE OR REPLACE TYPE file_table
AS TABLE OF file_type;
/
Next, create the java class to read the directories and return a collection. I'll overload the class to allow for user specification of file size units (bytes vs kilobytes, megabytes, or gigabytes.) File permissions are protected separately from the permissions needed to view the file name, type, and size. I included a limited exception handler to simply return "?" as unknown if the user receives an error while trying to read them. As with the previous example, the function needs to establish a jdbc connection to the local default connection in order to create the SQL objects.
CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED "DirList" as import java.io.*; import java.sql.*; import java.util.*; import oracle.sql.*; import oracle.jdbc.driver.*; public class DirList{ public static oracle.sql.ARRAY getListasObjects(String p_directory) throws Exception { return getListasObjects(p_directory,"Bytes"); } public static oracle.sql.ARRAY getListasObjects(String p_directory, String p_size_units) throws Exception { Connection conn = DriverManager.getConnection("jdbc:default:connection:"); StructDescriptor oneFile = StructDescriptor.createDescriptor("FILE_TYPE", conn); Object[] fileattributes = new Object[6]; File[] files = new File(p_directory).listFiles(); STRUCT[] temparray = new STRUCT[files.length]; for( int i = 0; i < files.length; i++ ) { fileattributes[0] = files[i].getName(); String tempunits = p_size_units.toUpperCase(); if (tempunits.equals("G") || tempunits.equals("GB") ) fileattributes[1] = Math.ceil((files[i].length() + 1.0) /1024/1024/1024); else if (tempunits.equals("M") || tempunits.equals("MB") ) fileattributes[1] = Math.ceil((files[i].length() + 1.0) /1024/1024); else if (tempunits.equals("K") || tempunits.equals("KB") ) fileattributes[1] = Math.ceil((files[i].length() + 1.0) /1024); else { fileattributes[1] = files[i].length(); } fileattributes[2] = (files[i].isDirectory() ? "D" : "F"); fileattributes[3] = new java.sql.Timestamp(files[i].lastModified()); // If you don't have permissions granted to read or write // you'll get an error simply by looking at the attribute // For example... // ORA-29532: Java call terminated by uncaught Java exception: // java.security.AccessControlException: // the Permission (java.io.FilePermission YOURFILE write) // has not been granted to YOURUSER. // The PL/SQL to grant this is // dbms_java.grant_permission('YOURUSER', // 'SYS:java.io.FilePermission', // 'YOURFILE', // 'write') try { fileattributes[4] = (files[i].canRead() ? "Y" : "N"); } catch (java.security.AccessControlException e) { fileattributes[4] = "N"; } try { fileattributes[5] = (files[i].canWrite() ? "Y" : "N"); } catch (java.security.AccessControlException e) { fileattributes[5] = "N"; } temparray[i] = new STRUCT(oneFile, conn, fileattributes); } ArrayDescriptor FileArray = ArrayDescriptor.createDescriptor("FILE_TABLE", conn); return new ARRAY(FileArray, conn, temparray); } }
Instead of creating a standalone function, this class will be exposed through a pl/sql package in order to invoke the overloaded methods. This could also have been implemented through a default value in the pl/sql wrapper function. This implementation more directly mirrors the java api which may be more appropriate for other examples.
CREATE OR REPLACE PACKAGE files AS FUNCTION dir_list(p_directory IN VARCHAR2, p_size_units IN VARCHAR2 DEFAULT NULL) RETURN file_table; END; / CREATE OR REPLACE PACKAGE BODY files AS FUNCTION dir_list1(p_directory IN VARCHAR2) RETURN file_table AS LANGUAGE JAVA NAME 'DirList.getListasObjects( java.lang.String ) return oracle.sql.ARRAY' ; FUNCTION dir_list2(p_directory IN VARCHAR2, p_size_units IN VARCHAR2) RETURN file_table AS LANGUAGE JAVA NAME 'DirList.getListasObjects( java.lang.String, java.lang.String ) return oracle.sql.ARRAY' ; FUNCTION dir_list(p_directory IN VARCHAR2, p_size_units IN VARCHAR2 DEFAULT NULL) RETURN file_table IS BEGIN RETURN CASE WHEN p_size_units IS NULL THEN dir_list1(p_directory) ELSE dir_list2(p_directory, p_size_units) END; END; END; /
And then finally, use the package to look up the contents of a directory. This does assume permissions have been granted and the directory and files exist.
select * from files.dir_list('/tmp/customer_files');
FILE_NAME | FILE_SIZE | FILE_TYPE | LAST_MODIFIED | READABLE | WRITEABLE |
---|---|---|---|---|---|
messages.json | 722 |
F | 2019-03-21 01:19:29 | Y | ? |
datadump.csv | 3709 |
F | 2019-03-21 01:19:13 | Y | ? |
sales.xml | 2473 |
F | 2019-03-21 01:18:56 | Y | ? |
I hope these example help. A natural question to follow up might be "could these functions returned a pipelined collection?" Yes, they can through the Data Cartridge api (ODCI) and I hope to provide examples of those options in a future article.
Questions and comments, as always, are welcome.