Connor McDonald offered a handy reminder of the ability to declare cursors at the package level in his Public / private cursors blog post.
I offer this follow-on to make sure you are aware of the use cases for doing this, and also the different behavior you will see for package-level cursors.
Generally, first of all, let's remind ourselves of the difference between items declared at the package level ("global") and those declared within subprograms of the package ("local").
Global vs Local variable (community contribution)
Global (Package-level) vs Local (Declared in block) Variables (mine)
But this principle also applies to cursors, and the ramifications can be a bit more critical to your application. Let's explore in more detail.
From Understanding the PL/SQL Package Specification:
But in this case, we are more interested in what you "see" when you reference that packaged item: the state of that item.
Global items maintain their state (for a variable, its value; for a cursor, as you are about to see, its status of open or closed and if open where in the result set the cursor is pointing) until you explicitly change that state, your session terminates, or the package state is reset.
Is The Cursor Closed?
Let's watch this in action, via a little quiz for you drawn from the Oracle Dev Gym. Suppose I execute the following statements below (try it in LiveSQL):
Which of the blocks below will display the following two lines of text after execution?
Block 1
Block 2
Block 3
Block 4
And the answer is: blocks 2 - 4 will display the desired output, while block 1 fails with:
Block 2 works just fine because show_name2 uses a locally-declared cursor. The cursor is opened locally and when the procedure terminates, the cursor is closed.
But in block 1, I am calling show_name1, which opens the package-based cursor. And since the cursor is declared at the package level, once it is opened, it stays open in your session. Even when the procedure in which it was opened terminates.
If you do, however, explicitly close the cursor, then you are able to open it again in that same session, which is why block 3 succeeds.
Block 4 shows the desired output as well, because the packaged cursor is only opened by the first procedure call. The second uses the local "copy" and so there is no error. If, however, you tried to call show_name1 again in the same session, ORA-06511 will be raised.
since the packaged cursor was opened, but not closed, inside the procedure. It remains open when the plch_show_name1 procedure is called a second time.
Use Cases for Package-Level Cursors
OK, hopefully you've got a handle on the different behavior of cursors defined at the package level. Why would you want to define a cursor this way, as opposed to just "burying" the cursor/SQL inside a particular block?
1. Hide the SQL
Hiding information is often a very good idea when it comes to software development.
We can so easily got lost in the "weeds" - the details of how things are implemented - rather than stay at a higher level that focuses on how to use the element in question. That's the whole idea behind APIs.
How does that work with cursors? You can declare the cursor header in the package specification and move the SELECT to the package body! As in:
The type of the RETURN statement must be a record type. If you try to return say a scalar value type, as in "employees.last_name%TYPE, you will get this compilation error:
2. Share the Cursor
The main driver for going to package-level cursors is that the cursor is not embedded within a single procedure, function or anonymous block. Which means that you can reference (open-fetch-close) that cursor from multiple subprograms and blocks.
That's nice - you avoid repetition of the same cursor.
That's potentially an issue - because you need to make sure the cursor is not open already, before you try to open it. And if it is open, what should you do?
Bottom line for this use case: each user of the cursor must be sure to close the cursor when they are done.
Have you declared cursors at the package level? Any interesting stories you'd like to share with us?
I offer this follow-on to make sure you are aware of the use cases for doing this, and also the different behavior you will see for package-level cursors.
Generally, first of all, let's remind ourselves of the difference between items declared at the package level ("global") and those declared within subprograms of the package ("local").
Items declared locally are automatically cleaned up ("closed" and memory released) when the block terminates.
Items declared globally are kept "open" (memory allocated, state preserved) until the session terminates.Here's are two LiveSQL scripts that demonstrates the difference for a numeric and string variable (not that the types matter).
Global vs Local variable (community contribution)
Global (Package-level) vs Local (Declared in block) Variables (mine)
But this principle also applies to cursors, and the ramifications can be a bit more critical to your application. Let's explore in more detail.
From Understanding the PL/SQL Package Specification:
The scope of a public item is the schema of the package. A public item is visible everywhere in the schema.and when it comes to the package body:
The package body can also declare and define private items that cannot be referenced from outside the package, but are necessary for the internal workings of the package.The "scope" describes who/where in your code the packaged element can be referenced. Roughly, any schema or program unit with the execute privilege granted to the package can reference any element in the package specification.
But in this case, we are more interested in what you "see" when you reference that packaged item: the state of that item.
Global items maintain their state (for a variable, its value; for a cursor, as you are about to see, its status of open or closed and if open where in the result set the cursor is pointing) until you explicitly change that state, your session terminates, or the package state is reset.
Is The Cursor Closed?
Let's watch this in action, via a little quiz for you drawn from the Oracle Dev Gym. Suppose I execute the following statements below (try it in LiveSQL):
CREATE TABLE employees
(
employee_id INTEGER,
last_name VARCHAR2 (100)
)
/
BEGIN
INSERT INTO employees VALUES (100, 'Thomsoni');
INSERT INTO employeesVALUES (200, 'Edulis');
COMMIT;
END;
/
CREATE OR REPLACE PACKAGE pkg
IS
CURSOR emp_cur (id_in IN employees.employee_id%TYPE)
IS
SELECT last_name
FROM employees
WHERE employee_id = id_in;
END;
/
CREATE OR REPLACE PROCEDURE show_name1 (
id_in IN employees.employee_id%TYPE)
IS
l_name employees.last_name%TYPE;
BEGIN
OPEN pkg.emp_cur (id_in);
FETCH pkg.emp_cur INTO l_name;
DBMS_OUTPUT.put_line (l_name);
END;
/
CREATE OR REPLACE PROCEDURE show_name2 (
id_in IN employees.employee_id%TYPE)
IS
CURSOR emp_cur (id_in IN employees.employee_id%TYPE)
IS
SELECT last_name
FROM employees
WHERE employee_id = id_in;
l_name employees.last_name%TYPE;
BEGIN
OPEN emp_cur (id_in);
FETCH emp_cur INTO l_name;
DBMS_OUTPUT.put_line (l_name);
END;
/
Which of the blocks below will display the following two lines of text after execution?
Thomsoni
Edulis
Block 1
BEGIN
show_name1 (100);
show_name1 (200);
END;
Block 2
BEGIN
show_name2 (100);
show_name2 (200);
END;
Block 3
BEGIN
show_name1 (100);
CLOSE pkg.emp_cur;
show_name1 (200);
CLOSE pkg.emp_cur;
END;
Block 4
BEGIN
show_name1 (100);
show_name2 (200);
END;
And the answer is: blocks 2 - 4 will display the desired output, while block 1 fails with:
ORA-06511: PL/SQL: cursor already open
Block 2 works just fine because show_name2 uses a locally-declared cursor. The cursor is opened locally and when the procedure terminates, the cursor is closed.
But in block 1, I am calling show_name1, which opens the package-based cursor. And since the cursor is declared at the package level, once it is opened, it stays open in your session. Even when the procedure in which it was opened terminates.
If you do, however, explicitly close the cursor, then you are able to open it again in that same session, which is why block 3 succeeds.
Block 4 shows the desired output as well, because the packaged cursor is only opened by the first procedure call. The second uses the local "copy" and so there is no error. If, however, you tried to call show_name1 again in the same session, ORA-06511 will be raised.
since the packaged cursor was opened, but not closed, inside the procedure. It remains open when the plch_show_name1 procedure is called a second time.
Use Cases for Package-Level Cursors
OK, hopefully you've got a handle on the different behavior of cursors defined at the package level. Why would you want to define a cursor this way, as opposed to just "burying" the cursor/SQL inside a particular block?
1. Hide the SQL
Hiding information is often a very good idea when it comes to software development.
We can so easily got lost in the "weeds" - the details of how things are implemented - rather than stay at a higher level that focuses on how to use the element in question. That's the whole idea behind APIs.
How does that work with cursors? You can declare the cursor header in the package specification and move the SELECT to the package body! As in:
CREATE OR REPLACE PACKAGE pkg
IS
CURSOR emp_cur (id_in IN employees.employee_id%TYPE)
RETURN employees%ROWTYPE;
END;
/
CREATE OR REPLACE PACKAGE BODY pkg
IS
CURSOR emp_cur (id_in IN employees.employee_id%TYPE)
RETURN employees%ROWTYPE
IS
SELECT *
FROM employees
WHERE employee_id = id_in;
END;
/
The type of the RETURN statement must be a record type. If you try to return say a scalar value type, as in "employees.last_name%TYPE, you will get this compilation error:
PLS-00320: the declaration of the type of this expression is incomplete or malformed
2. Share the Cursor
The main driver for going to package-level cursors is that the cursor is not embedded within a single procedure, function or anonymous block. Which means that you can reference (open-fetch-close) that cursor from multiple subprograms and blocks.
That's nice - you avoid repetition of the same cursor.
That's potentially an issue - because you need to make sure the cursor is not open already, before you try to open it. And if it is open, what should you do?
Bottom line for this use case: each user of the cursor must be sure to close the cursor when they are done.
Have you declared cursors at the package level? Any interesting stories you'd like to share with us?
Hi Steven,
ReplyDeleteI usually use package cursors -> Always for to share/refactoring.
I use FOR LOOP - END LOOP -> Always to close.
I didn't know the use "Hide the SQL" -> Great!
One note, It happened to me... if your function is recursive and the function use a cursor with recursive call inside.... I can't use un package cursor.... It wil give error:
{code}
CREATE OR REPLACE PACKAGE PTEST IS
FUNCTION funcRecursive(entrance NUMBER) RETURN BOOLEAN;
END;
/
{code}
{code}
CREATE OR REPLACE PACKAGE BODY PTEST IS
CURSOR cTest(entrance NUMBER) IS
SELECT COL
FROM
(SELECT 2 AS COL
FROM DUAL
UNION ALL
SELECT 3 AS COL
FROM DUAL
UNION ALL
SELECT 4 AS COL
FROM DUAL
)
WHERE COL = entrance;
FUNCTION funcRecursive(entrance NUMBER) RETURN BOOLEAN IS
newEntrance NUMBER(5);
BEGIN
dbms_output.put_line('funcRecursive: ' || entrance);
IF entrance = 0 THEN
RETURN FALSE;
END IF;
FOR rTest IN cTest(entrance) LOOP
newEntrance := entrance - 1;
RETURN funcRecursive(newEntrance);
END LOOP;
RETURN TRUE;
END funcRecursive;
END;
/
{code}
{code}
SET SERVEROUTPUT ON
DECLARE
BEGIN
IF PTEST.funcRecursive(4) THEN
dbms_output.put_line('EXISTS');
ELSE
dbms_output.put_line('SORRY... NO');
END IF;
END;
/
funcRecursive: 4
funcRecursive: 3
DECLARE
*
ERROR at line 1:
ORA-06511: PL/SQL: cursor already open
ORA-06512: at "GEFE.PTEST", line 4
ORA-06512: at "GEFE.PTEST", line 25
ORA-06512: at "GEFE.PTEST", line 27
ORA-06512: at line 4
{code}
Then, change:
{code}
CREATE OR REPLACE PACKAGE BODY PTEST IS
FUNCTION funcRecursive(entrance NUMBER) RETURN BOOLEAN IS
newEntrance NUMBER(5);
CURSOR cTest(entrance NUMBER) IS
SELECT COL
FROM
(SELECT 2 AS COL
FROM DUAL
UNION ALL
SELECT 3 AS COL
FROM DUAL
UNION ALL
SELECT 4 AS COL
FROM DUAL
)
WHERE COL = entrance;
BEGIN
dbms_output.put_line('funcRecursive: ' || entrance);
IF entrance = 0 THEN
RETURN FALSE;
END IF;
FOR rTest IN cTest(entrance) LOOP
newEntrance := entrance - 1;
RETURN funcRecursive(newEntrance);
END LOOP;
RETURN TRUE;
END funcRecursive;
END;
/
{code}
{code}
SET SERVEROUTPUT ON
DECLARE
BEGIN
IF PTEST.funcRecursive(4) THEN
dbms_output.put_line('EXISTS');
ELSE
dbms_output.put_line('SORRY... NO');
END IF;
END;
/
funcRecursive: 4
funcRecursive: 3
funcRecursive: 2
funcRecursive: 1
EXISTS
PL/SQL procedure successfully completed.
{code}
Thank you.
Thanks for raising this point about package-level cursors and a recursive function + all that code to prove your point!
DeleteHello All,
ReplyDeleteJust a short comment regarding the code above:
You have the following loop:
FOR rTest IN cTest(entrance) LOOP
newEntrance := entrance - 1;
RETURN funcRecursive(newEntrance);
END LOOP;
I see several problems with writing such code:
1. The loop does NOT use the rTest iterator record at all.
2. A RETURN statement is executed already as part of the first iteration of the loop,
and, since the cursor is a package level cursor, it remains open.
I think that using a FOR loop for executing one iteration only is better to be avoided.
Anyway, even if executing a single iteration, placing the RETURN statement AFTER
the END LOOP is a cleaner coding style, and, as an additional result, the cursor will be closed.
Thanks a lot & Best Regards,
Iudith