I'm trying to find better approach for selecting from REFCURSOR. Let's say there is a such function:
CREATE OR REPLACE FUNCTION get_batches_to_export(p_batch_type varchar2 := NULL)
RETURN SYS_REFCURSOR
AS
o_cursor SYS_REFCURSOR;
BEGIN
OPEN o_cursor FOR
SELECT batch_id
FROM batches
WHERE batch_type = p_batch_type OR p_batch_type IS NULL;
RETURN o_cursor;
END;
Now I need to select content from this table using requral select (it is limitation of client application - NiFi usign JDBC connection - it supports only scalar types to be returned, deseralization of REFCURSOR fails; also direct select from source table is not possible)
I found this select, it creates XML from REFCURSOR and then extracts values from this XML:
SELECT extractvalue(value(batches_list),'ROW/BATCH_ID') batch_id
FROM table(xmlsequence(get_batches_to_export())) batches_list
Any idea how to avoid the XML part (the xmlsequence, extractvalue and value function calls)? There is also limitation I cannot create any other helper data types or objects - only select can be used.
Related
A query can be returned in a "collection query" mode as a JSON, simply as SELECT * FROM SOMETABLE.
In PL/SQL however, this is not possible. How would the equivalent be implemented in this mode?
Easiest way is to return a refcursor from your procedure, as a RESULTSET parameter type.
For example:
DECLARE
cur SYS_REFCURSOR;
BEGIN
OPEN cur FOR
SELECT * FROM myTable ORDER BY id;
:result := cur;
END;
With your OUT parameter set to bind result.
Example sample schema, data, and REST Code here - For a longer full example.
I want to create a procedure in PL/SQL that has 5 steps. Step 1 and 2 execute first and return an ID. In step 3, we have a SELECT statement that has a condition with that returned ID. I want then to take all of the results of that SELECT statement and use them in a JOIN in another SELECT statement and use THOSE results in a 3rd SELECT statement again using JOIN. From what I've seen, I can't use CURSOR in JOIN statements. Some of my co-workers have suggested that I save the results in a CURSOR and then use a loop to iterate through each row and use that data for the next SELECT. However since I'm going to do 2 selects this will create a huge fork of inside loops and that's exactly what I'm trying to avoid.
Another suggestion was to use Temprary Tables to store the data. However this procedure could be executed at the same time by many users and the table's data would conflict with each other. Right now I'm looking at LOCAL Temporary tables that supposedly filter the data according the the session but I'm not really sure I want to create dummy tables for my procedures since I want to avoid leaving trash in the schema (this procedure is for a custom part of the application). Is there a standard way of doing this? Any ideas?
Sample:
DECLARE
USERID INT := 1000000;
TEXT1 VARCHAR(100);
TEXT_INDEX INT;
CURSOR NODES IS SELECT * FROM NODE_TABLE WHERE DESCRIPTION LIKE TEXT || '%';
CURSOR USERS IS SELECT * FROM USERGROUPS JOIN NODES ON NODES.ID = USERGROUPS.ID;
BEGIN
SELECT TEXT INTO TEXT1 FROM TABLE_1 WHERE ID = USERID;
TEXT_INDEX = INSTR(TEXT, '-');
TEXT = SUBSTR(TEXT, 0, TEXT_INDEX);
OPEN NODES;
OPEN USERS;
END;
NOTE: This does NOT work. Oracle doesn't support joins between cursors.
NOTE2: This CAN be done in a single query but for the sake of argument (and in my real use case) I want to break those steps down in a procedure. The sample code is a depiction of what I'm trying to achieve IF joins between cursors worked. But they don't and I'm looking for an alternative.
I ended up using a function (although a procedure could be used as well) along with tables. Things I've learned and one should pay attention to:
PL/SQL functions can only return types that have been declared in the schema in advance and are clear. You can't create a function that returns something like MY_TABLE%ROWTYPE, even though it seems the type information is available it is not acceptable. You have to instead create a custom type of MY_TABLE%ROWTYPE is you want to return it.
Oracle treats tables of declared types differently from tables of %ROWTYPE. This confused the hell out of me at first but from what I've gathered this is how it works.
DECLARE TYPE MY_CUSTOM_TABLE IS TABLE OF MY_TABLE%ROWTYPE;
Declares a collection of types of MY_TABLE row. In order to add to this we must use BULK COLLECT INTO from an SQL statement that queries MY_TABLE. The resulting collection CANNOT be used in JOIN statements is not queryable and CANNOT be returned by a function.
DECLARE
CREATE TYPE MY_CUSTOM_TYPE AS OBJECT (COL_A NUMBER, COL_B NUMBER);
CREATE TYPE MY_CUSTOM_TABLE AS TABLE OF MY_CUSTOM_TYPE;
my_custom_tab MY_CUSTOM_TABLE;
This create my_custom_tab which is a table (not a collection) and if populated can be queried at using TABLE(my_custmo_tab) in the FROM statement. As a table which is declared in advance in the schema this CAN be returned from a function. However it CANNOT be populated using BULK COLLECT INTO since it is not a collection. We must instead use the normal SELECT INTO statement. However, if we want to populate it with data from an existing table that has 2 number columns we cannot simply do SELECT * INTO my_custom_tab FROM DOUBLE_NUMBER_TABLE since my_custom_tab hasn't been initialized and doesn't contain enough rows to receive the data. And if we don't know how many rows a query returns we can't initialize it. The trick into populating the table is to use the CAST command and cast our select result set as a MY_CUSTOM_TABLE and THEN add it.
SELECT CAST(MULTISET(SELECT COL_A, COL_B FROM DOUBLE_NUMBER_TABLE) AS MY_CUSTOM_TABLE) INTO my_custom_tab FROM DUAL
Now we can easily use my_custom_tab in queries etc through the use of the TABLE() function.
SELECT * FROM TABLE(my_custom_tab)
is valid.
You can do such decomposition in many ways, but all of them have a significant performance penalty in comaration with single SQL statement.
Maintainability improvement are also questionable and depends on specific situation.
To review all possibilities please look through documentation.
Below is some possible variants based on simple logic:
calculate Oracle user name prefix based on given Id;
get all users whose name starts with this prefix;
find all tables owned by users from step 2;
count a total number of found tables.
1. pipelined
Prepare types to be used by functions:
create or replace type TUserRow as object (
username varchar2(30),
user_id number,
created date
)
/
create or replace type TTableRow as object (
owner varchar2(30),
table_name varchar2(30),
status varchar2(8),
logging varchar2(3)
-- some other useful fields here
)
/
create or replace type TUserList as table of TUserRow
/
create or replace type TTableList as table of TTableRow
/
Simple function to find prefix by user id:
create or replace function GetUserPrefix(piUserId in number) return varchar2
is
vUserPrefix varchar2(30);
begin
select substr(username,1,3) into vUserPrefix
from all_users
where user_id = piUserId;
return vUserPrefix;
end;
/
Function searching for users:
create or replace function GetUsersPipe(
piNameStart in varchar2
)
return TUserList pipelined
as
vUserList TUserList;
begin
for cUsers in (
select *
from
all_users
where
username like piNameStart||'%'
)
loop
pipe row( TUserRow(cUsers.username, cUsers.user_id, cUsers.created) ) ;
end loop;
return;
end;
Function searching for tables:
create or replace function GetUserTablesPipe(
piUserNameStart in varchar2
)
return TTableList pipelined
as
vTableList TTableList;
begin
for cTables in (
select *
from
all_tables tab_list,
table(GetUsersPipe(piUserNameStart)) user_list
where
tab_list.owner = user_list.username
)
loop
pipe row ( TTableRow(cTables.owner, cTables.table_name, cTables.status, cTables.logging) );
end loop;
return;
end;
Usage in code:
declare
vUserId number := 5;
vTableCount number;
begin
select count(1) into vTableCount
from table(GetUserTablesPipe(GetUserPrefix(vUserId)));
dbms_output.put_line('Users with name started with "'||GetUserPrefix(vUserId)||'" owns '||vTableCount||' tables');
end;
2. Simple table functions
This solution use same types as a variant with pipelined functions above.
Function searching for users:
create or replace function GetUsers(piNameStart in varchar2) return TUserList
as
vUserList TUserList;
begin
select TUserRow(username, user_id, created)
bulk collect into vUserList
from
all_users
where
username like piNameStart||'%'
;
return vUserList;
end;
/
Function searching for tables:
create or replace function GetUserTables(piUserNameStart in varchar2) return TTableList
as
vTableList TTableList;
begin
select TTableRow(owner, table_name, status, logging)
bulk collect into vTableList
from
all_tables tab_list,
table(GetUsers(piUserNameStart)) user_list
where
tab_list.owner = user_list.username
;
return vTableList;
end;
/
Usage in code:
declare
vUserId number := 5;
vTableCount number;
begin
select count(1) into vTableCount
from table(GetUserTables(GetUserPrefix(vUserId)));
dbms_output.put_line('Users with name started with "'||GetUserPrefix(vUserId)||'" owns '||vTableCount||' tables');
end;
3. cursor - xml - cursor
It's is a specific case, which may be implemented without user-defined types but have a big performance penalty, involves unneeded type conversion and have a low maintainability.
Function searching for users:
create or replace function GetUsersRef(
piNameStart in varchar2
)
return sys_refcursor
as
cUserList sys_refcursor;
begin
open cUserList for
select * from all_users
where username like piNameStart||'%'
;
return cUserList;
end;
Function searching for tables:
create or replace function GetUserTablesRef(
piUserNameStart in varchar2
)
return sys_refcursor
as
cTableList sys_refcursor;
begin
open cTableList for
select
tab_list.*
from
(
XMLTable('/ROWSET/ROW'
passing xmltype(GetUsersRef(piUserNameStart))
columns
username varchar2(30) path '/ROW/USERNAME'
)
) user_list,
all_tables tab_list
where
tab_list.owner = user_list.username
;
return cTableList;
end;
Usage in code:
declare
vUserId number := 5;
vTableCount number;
begin
select count(1) into vTableCount
from
XMLTable('/ROWSET/ROW'
passing xmltype(GetUserTablesRef(GetUserPrefix(vUserId)))
columns
table_name varchar2(30) path '/ROW/TABLE_NAME'
)
;
dbms_output.put_line('Users with name started with "'||GetUserPrefix(vUserId)||'" owns '||vTableCount||' tables');
end;
Of course, all variants may be mixed, but SQL looks better at least for simple cases:
declare
vUserId number := 5;
vUserPrefix varchar2(100);
vTableCount number;
begin
-- Construct prefix from Id
select max(substr(user_list.username,1,3))
into vUserPrefix
from
all_users user_list
where
user_list.user_id = vUserId
;
-- Count number of tables owned by users with name started with vUserPrefix string
select
count(1) into vTableCount
from
all_users user_list,
all_tables table_list
where
user_list.username like vUserPrefix||'%'
and
table_list.owner = user_list.username
;
dbms_output.put_line('Users with name started with "'||vUserPrefix||'" owns '||vTableCount||' tables');
end;
P.S. All code only for demonstration purposes: no optimizations and so on.
I would like to know how can I return a nested table from a stored procedure in oracle to consume in my .net client.
Basically I used the following format for creating nested table :
TYPE changes IS RECORD(
col1 VARCHAR2(20),
col2 VARCHAR2(20),
col3 VARCHAR2(20)
);
TYPE collection is table of changes;
I'm populating this with values in stored procedure logic.
Now I want to return these values for my .net client.
Can we try to dump nested tables values to cursor and return back. If yes then how ?
First off, if using ODP.net then know that you can directly pass collection types between a procedure and your UI. It is finicky, but it works.
If you want to simply dump a collection into a cursor to be returned, then look at the TABLE() function. In your example you could pass back a ref_cursor for the UI to traverse using something like (forgive any minor syntax glitches - I'm away from my database at the moment):
FUNCTION collection_to_cursor
return sys_refcursor
IS
p_cursor sys_refcursor;
p_change changes;
BEGIN
Open p_cursor for (
SELECT col1, col2, col3
FROM TABLE(p_change));
RETURN p_cursor;
end;
You could use a ref cursor, something along the lines of
DECLARE
lt_collection collection := collection();
lrc_collection SYS_REFCURSOR;
BEGIN
lt_collection := f_populate_collection_somehow;
OPEN lrc_collection
SELECT col1
,col2
,col3
FROM TABLE(CAST(lt_collection AS collection));
END;
.NET can then retrieve the data from the ref cursor, I'm not sure of the details of how this is done.
There is some good further information on using ref cursors or associative arrays
here
Please note, I am not conversant with .NET, so I am writing this literally to your question "how can I return a nested table from a stored procedure in oracle".
I am not sure about the "to consume in my .net client" portion, as I am not sure if a returned record type/table type can directly be used in .net code.
I did some research and what I learnt is that there is no direct support of record type in any oracle client interface. What people generally do is to create a wrapper procedure around the returned table type from function to convert it into a ref cursor and use the ref cursor in custom code.
CREATE OR REPLACE TYPE changes AS OBJECT(
col1 VARCHAR2(20),
col2 VARCHAR2(20),
col3 VARCHAR2(20)
);
CREATE OR REPLACE TYPE collection is table of changes;
CREATE OR REPLACE PACKAGE test_pkg AS
FUNCTION test_fn RETURN collection;
END test_pkg;
CREATE OR REPLACE PACKAGE BODY test_pkg
AS
FUNCTION test_fn RETURN collection AS
l_collection collection;
BEGIN
l_collection := collection();
l_collection.EXTEND;
l_collection(l_collection.LAST) := changes('Subhasis','Mukherjee','Male');
RETURN l_collection;
END test_fn;
END test_pkg;
SELECT * FROM table(test_pkg.test_fn)
col1 col2 col3
----------- ------------- --------------
Subhasis Mukherjee Male
If I run this static SQL in ORACLE SQL DEVELOPER:
SELECT appl_id
FROM grant_appls where full_appl_num IN(
'1R01HL129077-01','2R01HL075494-10A1','2P01HL062426-16') AND SUBPROJECT_ID is not null;
I get these results:
APPL_ID
8855105
8855112
8855104
8855108
8855109
8855107
8855106
Now I write the PROCUDERE and put the Static SQL in there:
create or replace PROCEDURE GET_APPLIDS_BY_FULL_GRANT_NUM (
fullGrantNumList IN VARCHAR2,
applIdRecordSet OUT SYS_REFCURSOR)
AS
BEGIN
OPEN applIdRecordSet FOR
SELECT appl_id
FROM grant_appls where full_appl_num IN
(
'1R01HL129077-01','2R01HL075494-10A1','2P01HL062426-16'
) AND SUBPROJECT_ID is not null;
END GET_APPLIDS_BY_FULL_GRANT_NUM;
I could have sworn at one point I was getting results since I have the static comma delimeted list.
But now I can't even get results with this.
The Ouput Variables window has the variable APPLIDRECORDSET but there are no values for APPL_ID.
The final version should look something like this:
create or replace PROCEDURE GET_APPLIDS_BY_FULL_GRANT_NUM (
fullGrantNumList IN VARCHAR2,
applIdRecordSet OUT SYS_REFCURSOR)
AS
BEGIN
OPEN applIdRecordSet FOR
SELECT appl_id
FROM grant_appls where full_appl_num IN
(
fullGrantNumList
) AND SUBPROJECT_ID is not null;
END GET_APPLIDS_BY_FULL_GRANT_NUM;
So of course when I run this I am getting back null:
VARIABLE cur REFCURSOR
EXECUTE GET_APPLIDS_BY_FULL_GRANT_NUM("'1R01HL129077-01','2R01HL075494- 10A1','2P01HL062426-16'",:cur);
SELECT :cur FROM dual;
Your parameter fullGrantNumList won't be used by Oracle the way you think. Oracle takes the bind variables and treats it as one value, it doesn't do a text replace like you think. Here is what is actually happening to your query:
select appl_id from grant_appls where full_appl_num in ('''1R01HL129077-01'',''2R01HL075494- 10A1'',''2P01HL062426-16''') amd subproject_id is null;
This is actually one of the nice things about bind variables is that they protect you from SQL Injection attacks.
My recommendation would be to either pass in a list of values as a table type or convert the statement to a string and use dynamic SQL to execute it.
Dynamic SQL
I am following these steps, but I continue to get an error and don't see the issue:
1) Create a custom Oracle datatype that represents the database columns that you want to retrieve:
CREATE TYPE my_object AS OBJECT
(COL1 VARCHAR2(50),
COL2 VARCHAR2(50),
COL3 VARCHAR2(50));
2) Create another datatype that is a table of the object you just created:
TYPE MY_OBJ_TABLE AS TABLE OF my_object;
3) Create a function that returns this table. Also use a pipeline clause so that results are pipelined back to the calling SQL, for example:
CREATE OR REPLACE
FUNCTION MY_FUNC (PXOBJCLASS varchar2)
RETURN MY_OBJ_TABLE pipelined IS
TYPE ref1 IS REF CURSOR
Cur1 ref1,
out_rec_my_object := my_object(null,null,null);
myObjClass VARCHAR2(50);
BEGIN
myObjClass := PXOBJCLASS
OPEN Cur1 For ‘select PYID, PXINSNAME, PZINSKEY from PC_WORK where PXOBJCLass = ;1’USING myObjClass,
LOOP
FETCH cur1 INTO out_rec.COL1, out_rec.COL2, out_rec.COL3;
EXIT WHEN Cur1%NOTFOUND;
PIPE ROW (out_rec);
END LOOP;
CLOSE Cur1;
RETURN;
END MY_FUNC;
NOTE: In the example above, you can easily replace the select statement with a call to another stored procedure that returns a cursor variable.
4) In your application, call this function as a table function using the following SQL statement:
select COL1, COL2, COL3 from TABLE(MY_FUNC('SomeSampletask'));
There is no need to use dynamic sql (dynamic sql is always a little bit slower) and there are too many variables declared. Also the for loop is much easier. I renamed the argument of the function from pxobjclass to p_pxobjclass.
Try this:
create or replace function my_func (p_pxobjclass in varchar2)
return my_obj_table pipelined
is
begin
for r_curl in (select pyid,pxinsname,pzinskey
from pc_work
where pxobjclass = p_pxobjclass) loop
pipe row (my_object(r_curl.pyid,r_curl.pxinsname,r_curl.pzinskey));
end loop;
return;
end;
EDIT1:
It is by the way faster to return a ref cursor instead of a pipelined function that returns a nested table:
create or replace function my_func2 (p_pxobjclass in varchar2)
return sys_refcursor
is
l_sys_refcursor sys_refcursor;
begin
open l_sys_refcursor for
select pyid,pxinsname,pzinskey
from pc_work
where pxobjclass = p_pxobjclass;
return l_sys_refcursor;
end;
This is faster because creating objects (my_object) takes some time.
I see two problems:
The dynamic query does not work that way, try this:
'select PYID, PXINSNAME, PZINSKEY from PC_WORK where PXOBJCLass ='''||PXOBJCLASS||''''
You don't need myObjClass, and it seems all your quotes are wrong.
The quoting on 'SomeSampletask'...
select COL1, COL2, COL3 from TABLE(MY_FUNC('SomeSampletask'));
Maybe I'm misunderstanding something here, but it seems like you want to be using a VIEW.