1

How to handle the case when select statement don't return any record in SYS_REFCURSOR ? I am using dynamic sql generation process using bind variables.

 create or replace
        procedure test_dynamic_sql
          (
            last_name varchar2,
            rc out sys_refcursor,
            jobid varchar2,
            sdate date,
            edate date,
            status out varchar2,
            message out varchar2 )
        is
          q long;
          lname varchar2(240);

        begin
           q := 'select employee_id    
        from employees e           
        where 1=1';
            if last_name is not null then
              q := q || 'and (e.LAST_NAME = :LAST_NAME)';
            else
              q := q || 'and (1=1 or :LAST_NAME is null)';
            end if;
            if jobid is not null then
              q := q || 'and (e.JOB_ID = :JOBID)';
            else
              q := q || 'and (1=1 or :JOBID is null)';
            end if;
            if sdate is not null then
              q := q || 'and (e.hire_date >= :sdate)';
            else
              q := q || 'and (1=1 or :sdate is null)';
            end if;
            if edate is not null then
              q := q || 'and (e.hire_date <= :edate)';
            else
              q := q || 'and (1=1 or :edate is null)';
            end if;

            open rc for q using last_name, jobid, sdate, edate;
          /*     
          IF rc%NOTFOUND THEN
            STATUS  := 'NR';
            MESSAGE := 'Not Found';
          ELSE
            STATUS  := 'S';
            MESSAGE := 'Found';
          END IF;
          */ 

        exception
        when others then
          STATUS  :='E';
          message := sqlcode||'-->'||sqlerrm;
        end;

I have tried %NOTFOUND and %FOUND attributes but its not working. I have also tried NO_DATA_FOUND exception but its also not working.

I need to return status as 'S', 'E', 'NR'

  • S --> SUCCESS (when records found)
  • E --> ERROR (when any error occured)
  • NR--> NO RECORDS (when 0 records)

Thanks!

11
  • Thanks SKG. Can you confirm that the goal here is to report on whether the cursor has any data, but you can't/aren't planning to FETCH from it or consume its data? Commented May 21, 2019 at 22:21
  • You need to add a space between each condition you are adding in the where clause to avoid errors. To check just for existence, I would change the query to return SELECT COUNT(1) so it will return 0 for no rows found, and also adding a and rownum < 2 to limit the rows processed to get the result. Or, keep everything as you have and just do one single FETCH and check %ROWCOUNT. Commented May 22, 2019 at 10:52
  • Currently there's nothing stopping the code from finding two employees by the same name. If the ONLY parameter passed to this procedure is name and all other parameters are passed as NULL the code currently doesn't handle this. Commented May 22, 2019 at 16:18
  • @alexgibbs It is giving output to the out parameter and i am able to print records by using anonymous block. Is fetch statement is really needed in this code as i am getting the output in rec i.e. sys_refcursor out parameter. I'll be grateful for your help. Commented May 22, 2019 at 18:02
  • @CodeNovice This is just a sample code. The main concern is to handle 'no data found'. In my original code, vendor code will be passed which is unique.And if there are records with the same name then what's the issue? It'll return multiple records as per search criteria and if we need to differentiate then we have to pass another parameters like joining date, dob etc. Commented May 22, 2019 at 18:14

2 Answers 2

3

This answer is to address the issues you are having while working with Ref Cursors as Out Parameters. The below code calls your test_dynamic_sql() Procedure. Within this Proc we OPEN the cursor, FETCH the data it is pointing to and we DO NOT CLOSE the cursor as we are immediately using that Cursor again outside of the test_dynamic_sql() Procedure. One thing to NOTE - When FETCH is used that Cursor will no longer provide you with the data and must be opened again. Since your Cursor is utilizing Dynamic SQL we must Declare our Dynamic 'Query' in the same place we are declaring the rest of our Global Variables.

"Cursors are NOT designed to be re-used: you read them once, keep moving forward and as you're doing so you're discarding any previously scanned rows." This fact was stolen from this SO Post: Oracle. Reuse cursor as parameter in two procedures.

Outside of this procedure we first must check if the Cursor was successfully initialized by using an IF statement to check if the Cursor exists: IF (g_rc IS NOT NULL) THEN.

Full Code Sample below:

DECLARE

  /* g for Global */
  g_status          VARCHAR2(5);
  g_message         VARCHAR2(100);

  g_rc              SYS_REFCURSOR;

  /* Store Dynamic SQL Query */
  g_SQL             VARCHAR2(200);

  /* Bind Variables */
  g_jobid           NUMBER;
  g_last_name       VARCHAR2(240);

  /* Declare Global Record used to FETCH data into */
  g_rec_employee      employees%ROWTYPE;

  PROCEDURE test_dynamic_sql(pv_last_name VARCHAR2,
                              p_rc OUT SYS_REFCURSOR,
                              pv_jobid VARCHAR2,
                              pv_status OUT VARCHAR2,
                              pv_message OUT VARCHAR2,
                              pv_q OUT VARCHAR2)
  AS

    /* Declare Record used to FETCH data into */
    rec_employee    employees%ROWTYPE;  

    /* Bind Variables */
    jobid           NUMBER        :=  to_number(pv_jobid);
    last_name       VARCHAR2(240) :=  pv_last_name;  

  BEGIN

    /* Reset/Initialize Cursor Output Variable */
    p_rc            :=  NULL;

    /* Dynamic SQL statement with placeholder: */
    pv_q := 'SELECT * FROM employees WHERE 1=1';

      IF last_name IS NOT NULL
        THEN pv_q := pv_q || ' AND (lastname = :LAST_NAME)';
        ELSE pv_q := pv_q || ' AND (1=1 or :LAST_NAME is null)';
      END IF;

      IF jobid IS NOT NULL
        THEN pv_q   := pv_q || ' AND (ID = :JOBID)';
        ELSE pv_q   := pv_q || ' AND (1=1 or :JOBID is null)';
      END IF;

    /* Open cursor & specify bind argument in USING clause: */
    OPEN p_rc FOR pv_q USING last_name, jobid;    

    LOOP
      /* In order to work with any Data that a Cursor 'points' to we must FETCH the data.  This allows us to use %ROWCOUNT and %NOTFOUND */
      FETCH p_rc INTO rec_employee;     
      EXIT WHEN p_rc%NOTFOUND;    
    END LOOP;

    IF p_rc%ROWCOUNT = 0 THEN
      pv_status  :=  'NR';
      pv_message :=  'Not Found';
      --EXIT;
    ELSIF p_rc%ROWCOUNT = 1 THEN
      pv_status  :=  'S';
      pv_message :=  'Found';
      --EXIT;
    ELSE
      pv_status  :=  'MU';
      pv_message :=  'Multiple Records';

    END IF;

    --dbms_output.put_line('Final Count: ' || p_rc%ROWCOUNT);

    /* Close Cursor - We don't close the Cursor here as we want to use this cursor as an OUT Parameter outside of this Proc */
    CLOSE p_rc;

  EXCEPTION
          WHEN OTHERS THEN
            pv_status  :='E';
            pv_message := sqlcode||'-->'||sqlerrm;
            dbms_output.put_line('STATUS: ' || pv_status);
            dbms_output.put_line('MESSAGE: ' || pv_message);
            CLOSE p_rc;

  END test_dynamic_sql;

BEGIN

  g_jobid     :=    null;
  g_last_name :=    'Loch';

  test_dynamic_sql(pv_last_name => g_last_name,
                    p_rc        => g_rc,      /* Out Parameter */
                    pv_jobid    => g_jobid,
                    pv_status   => g_status,  /* Out Parameter */
                    pv_message  => g_message, /* Out Parameter */
                    pv_q        => g_SQL      /* Out Parameter */
                  );

  /* Output OUT Variables aka Provide Output to User */
  dbms_output.put_line('STATUS: '  || g_status);
  dbms_output.put_line('MESSAGE: ' || g_message);

  IF (g_rc IS NOT NULL) THEN
    dbms_output.put_line('We have something here. Fetching Data Now:');

    OPEN g_rc FOR g_sql USING g_last_name, g_jobid;

    LOOP    
      FETCH g_rc INTO g_rec_employee;
      EXIT WHEN g_rc%NOTFOUND; 
      /* Print the Job ID just to show it is working */
      dbms_output.put_line('Job_ID: ' || g_rec_employee.id || ' is the id');      
    END LOOP;

    dbms_output.put_line('Total of: '|| g_rc%ROWCOUNT || ' records Fetched.');

    CLOSE g_rc;
  ELSE
    dbms_output.put_line('null');
  END IF;



EXCEPTION
  WHEN OTHERS THEN
    DBMS_OUTPUT.put_line('Error '||TO_CHAR(SQLCODE)||': '||SQLERRM);
    CLOSE g_rc;

END;

The above works with the same Employees table data as in my first Answer to this SO Question.

Employees Table and Data

Sign up to request clarification or add additional context in comments.

3 Comments

@SKG My answer above is kept as close to the original code as possible however in order to OPEN the CURSOR again certain variables that were only used within the test_dynamic_sql() Proc were needing to be pulled out and Declared Globally. We could have instead declared the employees record as an OUT Parameter. In my opinion declaring an employees record as an OUT parameter is the route to take as that would alleviate from having to OPEN the cursor again outside of the CALL. However we would need to use an employees TABLE/Collection for calls that produce multiple records.
Finally my problem has been resolved. This is the perfect answer.
Perfect. I was happy to assist.
0

There are a few things you are missing if you wish to work with a cursor in the manner you are attempting regardless of the implicit Ref Cursor due to the Dynamic SQL or an explicit Cursor.

In order to make use of %ROWCOUNT or %NOTFOUND you must first FETCH the Cursor. This Link "PL/SQL 101 : Understanding Ref Cursors" that provides a ton of information on this topic but all that is needed to help with answering your question is to know that you must FETCH the data first.

Below is an image showing the data in my Employees table. NOTE that there are two employees with the last name 'Loch'.

Employees Table and Data

The below Code is all within it's own anonymous block but can easily be converted into a Procedure/Function. It has all of your required Status's and Messages. In order to handle searches that have more than one result I added an additional Status/Message to tell the user that Multiple Records were returned. Last, just to make it easier to work with your code I took out all but two of your Parameters. NOTE: If the Procedure's Parameters are all passed in as NULL the Dynamic SQL That is generated will query the entire table as it basically removes all of the Filters in the WHERE clause.

DECLARE

  /* Parameters */
  rc              SYS_REFCURSOR;
  q               VARCHAR2(200);
  status          VARCHAR2(5);
  message         VARCHAR2(100);

  /* Declare Record used to FETCH data into */
  rec_employee    employees%ROWTYPE;  

  /* Bind Variables */
  jobid           NUMBER        :=  null;
  last_name       VARCHAR2(240) :=  'Loch';  

BEGIN

  /* Dynamic SQL statement with placeholder: */
  q := 'SELECT * FROM employees WHERE 1=1';

    IF last_name IS NOT NULL
      THEN q := q || ' AND (lastname = :LAST_NAME)';
      ELSE q := q || ' AND (1=1 or :LAST_NAME is null)';
    END IF;

    IF jobid IS NOT NULL
      THEN q   := q || ' AND (ID = :JOBID)';
      ELSE q   := q || ' AND (1=1 or :JOBID is null)';
    END IF;

  /* Open cursor & specify bind argument in USING clause: */
  OPEN rc FOR q USING last_name, jobid;    

  LOOP
    /* In order to work with any Data that a Cursor 'points' to we must FETCH the data.  This allows us to use %ROWCOUNT and %NOTFOUND */
    FETCH rc INTO rec_employee;    
    EXIT WHEN rc%NOTFOUND;    
  END LOOP;

  IF rc%ROWCOUNT = 0 THEN
    STATUS  :=  'NR';
    MESSAGE :=  'Not Found';
    --EXIT;
  ELSIF rc%ROWCOUNT = 1 THEN
    STATUS  :=  'S';
    MESSAGE :=  'Found';
    --EXIT;
  ELSE
    STATUS  :=  'MU';
    MESSAGE :=  'Multiple Records';

  END IF;

  dbms_output.put_line('Final Count: ' || rc%ROWCOUNT);

  /* Close Cursor */
  CLOSE rc;

  /* Return Variables or Provide Output to User */
  dbms_output.put_line('STATUS: ' || STATUS);
  dbms_output.put_line('MESSAGE: ' || MESSAGE);

EXCEPTION
        WHEN OTHERS THEN
          STATUS  :='E';
          message := sqlcode||'-->'||sqlerrm;
          dbms_output.put_line('STATUS: ' || STATUS);
          dbms_output.put_line('MESSAGE: ' || MESSAGE);

END;

enter image description here

6 Comments

Thanking you for your help.
@SKG Please by all means... if my answer helps you or satisfies your requirements Accept this as the answer or simply mark my answer as helpful. I was intrigued by this question and the answer took me a lot of time to come up with. I learned a ton working thru this one.
sure....currently i am changing my code as per your suggestion. I have complied your code andit is working. But now i am changing a little bit as per my requirement. once done i'll mark your answer.
CodeNovice : hi...very sorry! I am still facing some issues. when i converted it into a procedure, I declared sys refcursor variable as out parameter and fetch records, It is giving me count, status and message but it does not return records and gives error 'invalid cursor'.
Hmmm interesting. What are you doing with the Cursor in the code outside of the procedure? Keep in mind that a cursor is a pointer to a query and is NOT data. If you are attempting to work with the cursor outside of the Procedure make sure you OPEN the Cursor and once again utilize FETCH to tell Oracle to use the Cursor Query to get the data. If you don't first OPEN a cursor you will get the error 'invalid cursor'. Without knowing more or seeing your 'other' code I'm only able to guess at what is wrong.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.