Question

I'm working on converting an existing application from SQL Server over to Oracle, and I've hit a roadblock. I'm trying to execute an anonymous block as dynamic SQL and return a result set. However nothing I've tried seems to be able to return any values. Stored procedures are out due to design constraints.

My query is defined as:

DECLARE type id_array IS TABLE OF number;
t_Ids id_array;
BEGIN
UPDATE CSM_RECORDS SET MIGRATION_STATE = 1, LAST_UPDATE = CURRENT_DATE
WHERE OBJECT_UID IN 
(SELECT OBJECT_UID 
FROM CSM_RECORDS obj 
WHERE MIGRATION_STATE = 0
AND ROWNUM <= :BatchSize)
AND (:BatchName IS NULL OR obj.BATCH_NAME = :BatchName)
RETURNING OBJECT_UID BULK COLLECT INTO t_Ids;

OPEN rcursor FOR SELECT * FROM CSM_RECORDS;-- WHERE OBJECT_UID IN (t_Ids);
END;

You can see I've commented out the WHERE clause on the cursor in an attempt just to get anything to return at all.

Over on the C# side, I've got:

OracleCommand getNextNodesC = new OracleCommand(SQL_AS_SHOWN_ABOVE, conn);
getNextNodesC.BindByName = true;

OracleParameter batchSizeP = new OracleParameter("BatchSize", OracleDbType.Int32);
batchSizeP.Value = batchSize;

getNextNodesC.Parameters.Add(batchSizeP);

OracleParameter batchNameP = new OracleParameter("BatchName", OracleDbType.Varchar2);
batchNameP.Value = batchName;

getNextNodesC.Parameters.Add(batchNameP);


OracleParameter returnCursor = new OracleParameter("rcursor", OracleDbType.RefCursor);
returnCursor.Direction = ParameterDirection.Output;
getNextNodesC.Parameters.Add(returnCursor);

getNextNodesC.ExecuteNonQuery();

return ((Oracle.ManagedDataAccess.Types.OracleRefCursor)returnCursor.Value).GetDataReader();

The end goal is a DbDataReader that I can use, but in the above code, the returnCursor.Value seems to remain null. I've tried various combinations of Output vs. ReturnValue parameters and ExecuteNonQuery() and ExecuteReader() to no avail.

Any pointers would be appreciated, but an example of code that would actually accomplish what I'm looking for would be spectacular.

Was it helpful?

Solution 4

Thanks for the feedback everyone. It turns out that while debugging this issue, at some point I switched out the variable that defined my query, so that changes I was making to the query weren't actually being applied when running the application. As a result, I'm not sure exactly which fix ended up being the answer. But just for completeness, here's what I ended up using that worked.

First, run:

create type id_array as table of number;

(From answer on this question)

Query:

DECLARE t_Ids id_array;
BEGIN
UPDATE CSM_RECORDS SET MIGRATION_STATE = 1, LAST_UPDATE = CURRENT_DATE
    WHERE OBJECT_UID IN 
    (SELECT OBJECT_UID 
    FROM CSM_RECORDS obj 
    WHERE MIGRATION_STATE = 0
    AND ROWNUM <= :BatchSize
    AND (:BatchName IS NULL OR obj.BATCH_NAME = :BatchName)
RETURNING OBJECT_UID BULK COLLECT INTO t_Ids;

OPEN :rcursor FOR SELECT * FROM CSM_RECORDS WHERE OBJECT_UID IN (SELECT * FROM TABLE(cast(t_Ids as id_array)));
END;

And the C# looks like:

OracleCommand getNextNodesC = new OracleCommand(QUERY_DEFINED_ABOVE.Replace("\r\n", "\n"), conn);

getNextNodesC.BindByName = true;

OracleParameter batchSizeP = new OracleParameter("BatchSize", OracleDbType.Int32);
batchSizeP.Value = batchSize;
getNextNodesC.Parameters.Add(batchSizeP);

OracleParameter batchNameP = new OracleParameter("BatchName", OracleDbType.Varchar2);
batchNameP.Value = batchName;
getNextNodesC.Parameters.Add(batchNameP);

OracleParameter returnCursor = new OracleParameter("rcursor", OracleDbType.RefCursor);
returnCursor.Direction = ParameterDirection.ReturnValue;
getNextNodesC.Parameters.Add(returnCursor);

return getNextNodesC.ExecuteReader();

b_levitt's suggestion about the missing colon on the cursor bind variable was spot-on though. And I also needed to replace all the "\r\n"s in my query with "\n".

I'm still not clear on why I need to run CREATE TYPE... rather than just using DECLARE TYPE... since the latter seems to work fine for the BULK COLLECT part of the block, but it does seem to work fine now.

Thanks again for the help.

OTHER TIPS

TLDR: You're missing the colon on your cursor bind variable:

OPEN :rcursor FOR SELECT * FROM CSM_RECORDS;-- WHERE OBJECT_UID IN (t_Ids);

Full answer: As you probably know, in oracle there is the pl/sql context (your anonymous block) and the SQLplus context. Here is a block with SQLplus vars, the block with the related bind variables, and a SQLplus print at the end:

var rcursor refcursor
var fromDate varchar2(50)
var toDate varchar2(50)
exec :fromDate := '1-mar-2014';
exec :toDate := '1-apr-2014';

begin
  open :rcursor for
    SELECT
     trunc(to_date(:fromDate,'dd-mon-yyyy')) + NUMTODSINTERVAL(n,'day') AS Full_Date
    FROM (
    select (level-1) n
    from dual
    connect by level-1 <= trunc(to_date(:toDate,'dd-mon-yyyy')) - trunc(to_date(:fromDate,'dd-mon-yyyy'))
   )
  ;
end;
/
print rcursor

When executing a block in .net, ODP.net is taking care of the preparation done at the SQLplus level. Here is the same block executed from .net (as an nunit test):

[Test]
public void RefCursorFromBatch()
{
  OracleCommand cmd = new OracleCommand();
  cmd.CommandText = @"
  begin
    open :rcursor for
      SELECT
       trunc(to_date(:fromDate,'dd-mon-yyyy')) + NUMTODSINTERVAL(n,'day') AS Full_Date
      FROM (
      select (level-1) n
      from dual
      connect by level-1 <= trunc(to_date(:toDate,'dd-mon-yyyy')) - trunc(to_date(:fromDate,'dd-mon-yyyy'))
     )
    ;
  end;";
  cmd.BindByName = true;
  cmd.Parameters.Add("fromDate", OracleDbType.Date).Value = DateTime.Today.AddDays(-30);
  cmd.Parameters.Add("toDate", OracleDbType.Date).Value = DateTime.Today;
  cmd.Parameters.Add("rcursor", OracleDbType.RefCursor).Direction = ParameterDirection.Output;

  using (cmd.Connection = new OracleConnection("..."))
  {
    cmd.Connection.Open();
    var reader = cmd.ExecuteReader();

    OracleDataAdapter da = new OracleDataAdapter(cmd);
    DataTable dt = new DataTable();
    da.Fill(dt);

    Assert.Greater(dt.Rows.Count, 0);

  }
}

You can read more here: http://www.brothersincode.com/post/executing-SQL-Plus-Batches-from-Net.aspx

In case you do not have an answer yet, here is some example code that I have confirmed is working, that you can use as a starting point.

using System;
using System.Data;
using Oracle.ManagedDataAccess.Client;
using Oracle.ManagedDataAccess.Types;

namespace ConsoleApplication1
{
  class Class1
  {

    [STAThread]
    static void Main(string[] args)
    {
      try
      { 
        string conString = "User Id=scott;Password=tiger;Data Source=orcl;Pooling=false;";

        OracleConnection con = new OracleConnection();
        con.ConnectionString = conString;
        con.Open();

        string cmdtxt = "BEGIN " +
          "OPEN :1 for select ename, deptno from emp where deptno = 10; " +
          "OPEN :2 for select ename, deptno from emp where deptno = 20; " +
          "OPEN :3 for select ename, deptno from emp where deptno = 30; " +
          "END;";

        OracleCommand cmd = con.CreateCommand();
        cmd.CommandText = cmdtxt;

        OracleParameter p1 = cmd.Parameters.Add("refcursor1",
            OracleDbType.RefCursor);
        p1.Direction = ParameterDirection.Output;

        OracleParameter p2 = cmd.Parameters.Add("refcursor2",
            OracleDbType.RefCursor);
        p2.Direction = ParameterDirection.Output;

        OracleParameter p3 = cmd.Parameters.Add("refcursor3",
            OracleDbType.RefCursor);
        p3.Direction = ParameterDirection.Output;


        cmd.ExecuteNonQuery();


        OracleDataReader dr1 =
          ((OracleRefCursor)cmd.Parameters[2].Value).GetDataReader();
        OracleDataReader dr2 =
          ((OracleRefCursor)cmd.Parameters[1].Value).GetDataReader();


        while (dr1.Read() && dr2.Read())
        {
          Console.WriteLine("Employee Name: " + dr1.GetString(0) + ", " +
              "Employee Dept:" + dr1.GetDecimal(1));
          Console.WriteLine("Employee Name: " + dr2.GetString(0) + ", " +
              "Employee Dept:" + dr2.GetDecimal(1));
          Console.WriteLine();
        }

        Console.WriteLine("Press 'Enter' to continue");
        Console.ReadLine();


      }
      catch (Exception ex)
      {
        Console.WriteLine(ex.Message);
        Console.WriteLine(ex.InnerException);
        Console.WriteLine(ex.Data);
      }
    }
  }
}

Try this as your dynamic sql...

DECLARE type id_array IS TABLE OF number;
t_Ids id_array;
BEGIN
UPDATE CSM_RECORDS SET MIGRATION_STATE = 1, LAST_UPDATE = CURRENT_DATE
WHERE OBJECT_UID IN 
(SELECT OBJECT_UID 
FROM CSM_RECORDS obj 
WHERE MIGRATION_STATE = 0
AND ROWNUM <= :BatchSize)
RETURNING OBJECT_UID BULK COLLECT INTO t_Ids;

SELECT * FROM CSM_RECORDS;-- WHERE OBJECT_UID IN (t_Ids);
END;
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top