oracle stored procedure return resultset - c#

Can I define the stored procedure without using the RefCursor ? (like "return refcursor")
I do not want to use OracleDbType.RefCursor because it is not sent as dbparameter in other databases.
Also DbParameter.DbType = OracleDbType.RefCursor; does not supported
I do not want to define "retval IN OUT SYS_REFCURSOR" in the code below. Is there another way?
CREATE OR REPLACE procedure SYSTEM.customer_select_row(
p_email IN CUSTOMER.Email%TYPE,
p_password IN CUSTOMER."Password"%TYPE,
retval IN OUT SYS_REFCURSOR
)
IS
BEGIN
OPEN retval FOR
SELECT CustomerId, FirstName, LastName FROM CUSTOMER
WHERE Email = p_email AND "Password" = p_password
END customer_select_row;

You could use a pipeline Function,
It is a function that works exacltly as a table
you can call it this way
SELECT *
FROM TABLE(TEST_PIPELINE.STOCKPIVOT(10));
the TEST_PIPELINE.STOCKPIVOT(10) is a function
you can build it this way:
create or replace PACKAGE TEST_PIPELINE AS
-- here you declare a type record
type t_record is record
(
field_1 VARCHAR2(100),
field_2 VARCHAR2(100));
-- declare a table type from your previously created type
TYPE t_collection IS TABLE OF t_record;
-- declare that the function will return the collection pipelined
FUNCTION StockPivot(P_LINES NUMBER) RETURN t_collection PIPELINED;
END;
/
create or replace PACKAGE BODY TEST_PIPELINE IS
FUNCTION StockPivot(P_LINES NUMBER) RETURN t_collection PIPELINED IS
-- declare here a type of the record
T_LINE T_RECORD;
BEGIN
-- here is a loop example for insert some lines on pipeline
FOR I IN 1..P_LINES LOOP
-- inser data on your line this way
T_LINE.field_1 := 'LINE - ' || I;
T_LINE.field_2 := 'LINE - ' || I;
-- then insert insert the line for result (this kind of functions should not have a return statement)
PIPE ROW (T_LINE );
END LOOP;
END;
END;

Related

Sending a string with commas from asp.net to Oracle seems to cause an issue

On my front end, I'm sending VarChar variables to be used in a Stored Procedure in Oracle. My front end code looks like this:
protected void Page_Load(object sender, EventArgs e)
{
OracleConnection conn = GetConnection();
try
{
{
string vw_AuditID = Convert.ToString(Request.QueryString["AuditID"]);
string vw_PlanID = Convert.ToString(Request.QueryString["PlanID"]);
conn.ConnectionString = ConfigurationManager.ConnectionStrings["ConnCST"].ToString();
OracleCommand cmd3 = new OracleCommand();
cmd3.CommandType = CommandType.StoredProcedure;
cmd3.CommandText = "CSTAPP_O.CST_ADMIN_REPORT";
cmd3.Connection = conn;
cmd3.Parameters.Add("RAudit", OracleType.VarChar).Value = vw_AuditID;
cmd3.Parameters.Add("RPlan", OracleType.VarChar).Value = vw_PlanID;
cmd3.Parameters.Add("cursor_", OracleType.Cursor).Direction = ParameterDirection.Output;
var SearchAdapter = new OracleDataAdapter(cmd3);
var ds = new DataSet();
SearchAdapter.Fill(ds);
// Perform the binding.
GridView_AuditReport.DataSource = ds;
GridView_AuditReport.DataBind();
}
}
catch (Exception ex)
{
// Handle the error
Console.WriteLine("Error making Call to " + ex + "");
}
}
Note that AuditID and PlanID are coming from a previous page, and are passed like
?AuditID='10294'&PlanID='322, 323, 324'
On the Oracle side, I'm running a stored procedure like this:
create or replace PROCEDURE "CST_ADMIN_REPORT"
(
RAudit VARCHAR2,
RPlan VARCHAR2,
cursor_ OUT SYS_REFCURSOR
) AS
BEGIN
open cursor_ for
SELECT BLAH // Edited the lengthy SELECT section out, no need for it.
Where FAP.Audit_ID = RAudit
AND FAP.Plan_ID in (RPlan)
ORDER BY FAP.PLAN_DESC ASC;
END CST_ADMIN_REPORT;
So, if I pass one Plan ID, it runs perfectly fine. However, if I pass multiple PlanIDs (such as '322, 323, 324'), the Oracle procedure tells me the number is invalid.
Can anyone tell me what might be wrong with the way I'm passing the info to Oracle?
SELECT BLAH // Edited the lengthy SELECT section out, no need for it.
Where FAP.Audit_ID = RAudit
AND FAP.Plan_ID in (RPlan)
ORDER BY FAP.PLAN_DESC ASC;
RPlan is a bind variable and is a single value - even though you are putting in a comma delimited string it is still a single value and it is trying to match the numeric value in your Plan_ID column to a string containing numbers and commas and is effectively doing:
AND TO_CHAR( FAP.Plan_ID ) = '322,323,324'
which is not going to generate a match.
If you want to pass in an array then use a collection:
CREATE OR REPLACE TYPE IntList IS TABLE OF INTEGER;
/
and pass that into the procedure:
create or replace PROCEDURE CST_ADMIN_REPORT
(
RAudit IN FAP.Audit_ID%TYPE,
RPlan IN IntList,
cursor_ OUT SYS_REFCURSOR
) AS
BEGIN
open cursor_ for
SELECT BLAH // Edited the lengthy SELECT section out, no need for it.
Where FAP.Audit_ID = RAudit
AND FAP.Plan_ID MEMBER OF RPlan
ORDER BY FAP.PLAN_DESC ASC;
END CST_ADMIN_REPORT;
However, you might find that the C# driver only supports passing associative arrays (rather than collections) so you might need to pass in an associative array (which can be used in PL/SQL but not in SQL) and copy the values into a collection which you can then use in an SQL statement:
CREATE OR REPLACE PACKAGE CST_ADMIN_PKG
IS
TYPE IntAssocArrayType IS TABLE OF INTEGER INDEX BY PLS_INTEGER;
PROCEDURE CST_ADMIN_REPORT
(
RAudit IN FAP.Audit_ID%TYPE,
RPlan IN IntAssocArrayType,
cursor_ OUT SYS_REFCURSOR
);
END;
/
CREATE OR REPLACE PACKAGE BODY CST_ADMIN_PKG
IS
PROCEDURE CST_ADMIN_REPORT
(
RAudit IN FAP.Audit_ID%TYPE,
RPlan IN IntAssocArrayType, -- Pass assoc. array in.
cursor_ OUT SYS_REFCURSOR
)
AS
-- Create a collection
p_rplan IntList := IntList();
i PLS_INTEGER;
BEGIN
-- Copy assoc. array into the collection
i := RPlan.FIRST:
WHILE i IS NOT NULL LOOP
p_rplan.EXTEND;
p_rplan(p_rplan.COUNT) := RPlan(i);
i := RPlan.NEXT(i);
END LOOP;
open cursor_ for
SELECT BLAH
Where FAP.Audit_ID = RAudit
AND FAP.Plan_ID MEMBER OF p_RPlan -- Use the collection in the select.
ORDER BY FAP.PLAN_DESC ASC;
END CST_ADMIN_REPORT;
END;
/
Oracle doesn't accept a string in that in (RPlan) clause. Not sure if it's the best solution, but you can parse the parameter string like that:
in (SELECT REGEXP_SUBSTR(RPlan,'[^,]+', 1, LEVEL)
FROM DUAL
CONNECT BY REGEXP_SUBSTR(RPlan, '[^,]+', 1, LEVEL) IS NOT NULL)

Can I write the stored procedure with table as output parameter as a single query in Oracle?

I have a C# method which takes a SQL string statement and saves the data into xml format.
public XmlDocument GetDBRequestXml(String sql)
{
}
I have a stored procedure with output parameter as table. Is there any way to pass this stored procedure as an executable single SQL statement in the above C# method? Can somebody please help me on this!!!
create or replace PACKAGE BODY EMPLOYEE_DETAILS AS
PROCEDURE GET_EMPLOYEES(
EMP_DEPT_ID EMPLOYEES.DEPARTMENT_ID%TYPE,
EMP_SALARY employees.salary%TYPE,
TBL_EMPLOYEES OUT TABLE_EMPLOYEES)
IS
LC_SELECT SYS_REFCURSOR;
LR_DETAILS DETAILS;
TBL_EMPLOYEE EMPLOYEE_DETAILS.TABLE_EMPLOYEES := EMPLOYEE_DETAILS.TABLE_EMPLOYEES();
BEGIN
OPEN LC_SELECT FOR
SELECT EMPLOYEE_ID, FIRST_NAME, LAST_NAME
FROM EMPLOYEES
WHERE DEPARTMENT_ID = EMP_DEPT_ID AND
EMPLOYEES.SALARY > EMP_SALARY;
LOOP
FETCH LC_SELECT INTO LR_DETAILS;
EXIT WHEN LC_SELECT%NOTFOUND;
IF LR_DETAILS.EMPLOYEE_ID > 114 THEN
TBL_EMPLOYEE.extend();
TBL_EMPLOYEE(TBL_EMPLOYEE.count()) := LR_DETAILS;
END IF;
END LOOP;
CLOSE LC_SELECT;
TBL_EMPLOYEES := TBL_EMPLOYEE;
END GET_EMPLOYEES;
END EMPLOYEE_DETAILS;
I had a good run at this today. I don't know if you still need it but this is good to understand. You can output this from stored proc:
CREATE OR REPLACE PACKAGE Eidmadm.TestPkg AS
TYPE stringTbl IS TABLE OF varchar2(250) INDEX BY BINARY_INTEGER;
PROCEDURE TestProc (p_strings out stringTbl );
END;
The other table, like below, didn't work
TYPE numTbl IS TABLE OF varchar2(100);
The c# code for this is:
OracleParameter p2 = new OracleParameter(":p_strings", OracleDbType.Varchar2, ParameterDirection.Output);
p2.CollectionType = OracleCollectionType.PLSQLAssociativeArray;
p2.Size = 100; // allocate enough extra space to retrieve expected result
// assign amount of space for each member of returning array
p2.ArrayBindSize = Enumerable.Repeat(250, 100).ToArray();
cmd.Parameters.Add(p2);
cmd.ExecuteNonQuery();
// And this is how you retrieve values
OracleString[] oraStrings = (OracleString[])p2.Value;
string[] myP2Values = new string[oraStrings.Length];
for (int i = 0; i < oraNumbers.Length; i++)
myP2Values[i] = oraStrings[i].Value;
**But most important is this: **
When you fill your pl/sql table, it needs to start from something larger than `0`, and preferably from `1`. Because and also - if you have index with skipped numbers, i.e. `2,4,6,8`, all those spaces will be part of returning `oracle array` and there will be `oracle null` in them. You would need to check for `null` in your loop
if !oraStrings[i].IsNull {....}
else {....}
Enjoy!

Pass a list of integers from C# into Oracle stored procedure

I have a oracle stored procedure which updates a table with the following statement.
update boxes
set location = 'some value'
where boxid = passed value
I have a page where the user selects 100+ boxes and updates them with a new location value. Currently, I have to call the stored procedure 100+ times to update each box(by passing a boxid each time).
I want to know how I can pass a list of boxids from C# into the stored procedure so that I have to call the stored procedure just one time.
I am hoping to use a where in(boxids) kind of where clause in the update statement.
Please let know how can I achieve this. Thanks in advance!
Oracle allows you to pass arrays of values as parameters. Borrowing from this SO question and this one you can define an INT_ARRAY type like this:
create or replace type CHAR_ARRAY as table of INTEGER;
Then define your stored procedure as:
CREATE OR REPLACE PROCEDURE product_search(
...
myIds IN CHAR_ARRAY,
...)
AS
SELECT ...
...
WHERE SomeIdField IN (Select column_value FROM TABLE(myIds))
...
You can then pass the list of values by setting the OracleParameter.CollectionType property like this:
OracleParameter param = new OracleParameter();
param.OracleDbType = OracleDbType.Int32;
param.CollectionType = OracleCollectionType.PLSQLAssociativeArray;
I'd create a new procedure, designed to handle a list of values. An efficient approach would be to load the multiple values into a global temp table, using a bulk insert, and then have the procedure update using a join to the GTT.
A notional example would look like this:
OracleTransaction trans = conn.BeginTransaction(IsolationLevel.RepeatableRead);
OracleCommand cmd = new OracleCommand(insertSql, conn, trans);
cmd.Parameters.Add(new OracleParameter("BOX_ID", OracleDbType.Number));
cmd.Parameters[0].Value = listOfBoxIds; // int[] listOfBoxIds;
cmd.ExecuteArray();
OracleCommand cmd2 = new OracleCommand(storedProc, conn, trans);
cmd2.CommandType = CommandType.StoredProcedure;
cmd2.ExecuteNonQuery();
trans.Commit();
Your PL/SQL block may look like this one:
CREATE OR REPLACE PACKAGE YOUR_PACKAGE AS
TYPE TArrayOfNumber IS TABLE OF NUMBER INDEX BY BINARY_INTEGER;
PROCEDURE Update_Boxes(boxes IN TArrayOfNumber );
END YOUR_PACKAGE;
/
CREATE OR REPLACE PACKAGE BODY YOUR_PACKAGE AS
PROCEDURE Update_Boxes(boxes IN TArrayOfNumber) is
BEGIN
FORALL i IN INDICES OF boxes
update boxes
set location = boxes(i)
where boxid = ...;
END Update_Boxes;
END YOUR_PACKAGE;
The C# code you get already in answer from Panagiotis Kanavos
I understand your concern - the round trips will be taxing.
Unfortunately I don't have anything to test, but you can try
Oracle bulk updates using ODP.NET
or
-- edit1: go with Panagiotis Kanavos's answer if your provider supports it, else check below --
-- edit12 as highlighted by Wernfried, long is deprecated. Another thing consider is max length varchar2: it doesn't scale on a very big set. Use the one below as the last resort. --
changing your stored procedure to accept string
implement string_2_list in asktom.oracle.com.
create or replace type myTableType as table of varchar2 (255);
create or replace function in_list( p_string in varchar2 ) return myTableType
as
l_string long default p_string || ',';
l_data myTableType := myTableType();
n number;
begin
loop
exit when l_string is null;
n := instr( l_string, ',' );
l_data.extend;
l_data(l_data.count) :=
ltrim( rtrim( substr( l_string, 1, n-1 ) ) );
l_string := substr( l_string, n+1 );
end loop;
return l_data;
end;
Above is early variant and splice to varchar2, but if you read more (including other threads) at that site
you'll find more advanced variants (optimized, better exception handling)

PostgreSQL error: query string argument of EXECUTE is null

I have a table called evidence with a trigger which calls a stored procedure which basically does table partitioning by month. However I get an obscure error when I start inserting lots of rows under load:
Npgsql.NpgsqlException: query string argument of EXECUTE is null
Severity: ERROR Code: 22004 at Npgsql.NpgsqlState.<ProcessBackendResponses_Ver_3>d__a.MoveNext() in c:\C#Apps\github.npgsql.Npgsql.stock\src\Npgsql\NpgsqlState.cs:line890 at Npgsql.ForwardsOnlyDataReader.GetNextResponseObject() in c:\C#Apps\github.npgsql.Npgsql.stock\src\Npgsql\NpgsqlDataReader.cs:line 1175 at
Npgsql.ForwardsOnlyDataReader.GetNextRowDescription() in c:\C#Apps\github.npgsql.Npgsql.stock\src\Npgsql\NpgsqlDataReader.cs:line 1191 at
Npgsql.ForwardsOnlyDataReader.NextResult() in c:\C#Apps\github.npgsql.Npgsql.stock\src\Npgsql\NpgsqlDataReader.cs:line 1377 at
Npgsql.NpgsqlCommand.ExecuteNonQuery() in c:\C#Apps\github.npgsql.Npgsql.stock\src\Npgsql\NpgsqlCommand.cs:line523
My system has automatic retry functionality and eventually every record gets inserted into the database, but after many many exceptions when the load is high.
Database is PostgreSQL 9.3 on a CentOS 6 server and client is C# .NET using Npgsql driver.
Table:
CREATE TABLE evidence
(
id uuid NOT NULL,
notification_id uuid NOT NULL,
feedback character varying(200),
result character varying(20),
trigger_action_type character varying(200),
trigger_action_id uuid,
data_type integer NOT NULL,
data bytea,
name character varying(30),
CONSTRAINT pk_evidence PRIMARY KEY (id)
);
Trigger:
CREATE TRIGGER evidence_move_to_partition_tables
BEFORE INSERT
ON evidence
FOR EACH ROW
EXECUTE PROCEDURE partition_evidence_by_month();
Trigger Function:
CREATE OR REPLACE FUNCTION partition_evidence_by_month()
RETURNS trigger AS
$BODY$
DECLARE
_notification_id uuid;
_raised_local_time timestamp without time zone;
_table_name character varying(35);
_start_date timestamp without time zone;
_end_date timestamp without time zone;
_table_space character varying(50) := 'ls_tablespace2';
_query text;
BEGIN
_notification_id := NEW.notification_id;
SELECT raised_local_time FROM notifications WHERE id=_notification_id INTO _raised_local_time;
_start_date := date_trunc('month', _raised_local_time);
_end_date := _start_date + '1 month'::interval;
_table_name := 'evidence-' || to_char(_start_date, 'YYYY-MM');
-- check to see if table already exists
PERFORM 1
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
AND c.relname = _table_name
AND n.nspname = 'public';
-- if the table doesn't exist, then create it now
IF NOT FOUND THEN
-- create partition table
_query := 'CREATE TABLE public.' || quote_ident(_table_name) || ' ( ) INHERITS (public.evidence)';
EXECUTE _query;
-- alter owner
--EXECUTE 'ALTER TABLE public.' || quote_ident(_table_name) || ' OWNER TO postgres';
-- add index
--EXECUTE 'ALTER TABLE public.' || quote_ident(_table_name) || ' ADD PRIMARY KEY (id)';
END IF;
-- move the data to the partition table
EXECUTE 'INSERT INTO public.' || quote_ident(_table_name) || ' VALUES ($1.*)' USING NEW;
RETURN NULL;
END;
$BODY$ LANGUAGE plpgsql VOLATILE COST 100;
Calling Code:
using (var cmd = db.CreateCommand())
{
cmd.CommandText = #"INSERT INTO evidence
(id, notification_id, feedback, result, trigger_action_type,
trigger_action_id, data_type, data, name)
VALUES (#id,#nid,#feedback,#result,#tat,#taid,#dt,#data,#name)";
cmd.Parameters.AddWithValue("#id", evItem.ID);
cmd.Parameters.AddWithValue("#nid", evItem.NotificationID);
cmd.Parameters.AddWithValue("#feedback", evItem.Feedback);
cmd.Parameters.AddWithValue("#result", evItem.Result);
cmd.Parameters.AddWithValue("#tat", evItem.TriggerActionType);
cmd.Parameters.AddWithValue("#taid", evItem.TriggerActionID);
cmd.Parameters.AddWithValue("#dt", (int)evItem.DataType);
cmd.Parameters.AddWithValue("#data", evItem.Data);
cmd.Parameters.AddWithValue("#name", evItem.Name);
cmd.ExecuteNonQuery();
}
Why would this bizarre error appear only when the system is under load? What can I do to prevent it happening?
Thanks!
The error message is
query string argument of EXECUTE is null
You have two EXECUTE commands:
_query := 'CREATE TABLE public.'
|| quote_ident(_table_name) || ' ( ) INHERITS (public.evidence)';
EXECUTE _query;
...
EXECUTE 'INSERT INTO public.'
|| quote_ident(_table_name) || ' VALUES ($1.*)' USING NEW;
The only part that can be NULL is table_name.
The only chance for table_name to become NULL is here:
SELECT raised_local_time FROM notifications WHERE id=_notification_id
INTO _raised_local_time;
So the cause must be one of two reasons:
NEW.notification_id is NULL.
There is no row in notifications for the given NEW.notification_id.
Try this modified trigger function for debugging:
CREATE OR REPLACE FUNCTION partition_evidence_by_month()
RETURNS trigger AS
$func$
DECLARE
_table_name text;
BEGIN
SELECT 'evidence-' || to_char(raised_local_time, 'YYYY-MM')
FROM public.notifications -- schema-qualify to be sure
WHERE id = NEW.notification_id
INTO _table_name;
IF _table_name IS NULL THEN
RAISE EXCEPTION '_table_name is NULL. Should not occur!';
END IF;
IF NOT EXISTS ( -- create table if it does not exist
SELECT 1
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
AND c.relname = _table_name
AND n.nspname = 'public') THEN
EXECUTE 'CREATE TABLE public.'
|| quote_ident(_table_name) || ' ( ) INHERITS (public.evidence)';
END IF;
EXECUTE 'INSERT INTO public.'
|| quote_ident(_table_name) || ' VALUES $1' -- Use NEW row directly
USING NEW; -- write data to the partition table
RETURN NULL;
END
$func$ LANGUAGE plpgsql;
Remove unused variables and simplify code. (This is obviously a simplified example.)
Among other things, you don't need date_trunc() at all. Simply feed the original timestamp to to_char().
No point in using varchar(n). Simply use text or varchar.
Avoid too many assignments where unnecessary - comparatively expensive in PL/pgSQL.
Add a RAISE to check my hypothesis.
If you get the error message, discriminating between the two possible causes would be the next step. Should be trivial ...

How to convert this stored procedure to simple query

I write this stored procedure but I am returning all sql queries to site code and I have a little problem to convert this query. I insert data in one table then insert it in another table with key that is generated in first table. I don't know what is the best way to write this from site code. To make three methods or what?
#m_UserId uniqueidentifier,
#m_WispTypeId int,
#m_CreatedOnDate datetime,
#m_PrivacyTypeId int,
#m_WispText nvarchar(200)
AS
SET XACT_ABORT, NOCOUNT ON
DECLARE #starttrancount int
BEGIN TRY
SELECT #starttrancount = ##TRANCOUNT
IF #starttrancount = 0
BEGIN TRANSACTION
DECLARE #wispId int
INSERT INTO dbo.tbl_Wisps
(UserId,WispTypeId,CreatedOnDate,PrivacyTypeId,WispText)
VALUES
(#m_UserId,#m_WispTypeId,#m_CreatedOnDate,#m_PrivacyTypeId,#m_WispText)
SELECT #wispId = SCOPE_IDENTITY()
INSERT INTO dbo.tbl_CommentableEntity
(ItemId)
VALUES
(#wispId)
DECLARE #ceid int
select #ceid = SCOPE_IDENTITY()
UPDATE dbo.tbl_Wisps SET CommentableEntityId = #ceid WHERE WispId = #wispId
IF #starttrancount = 0
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 AND #starttrancount = 0
ROLLBACK TRANSACTION
RAISERROR ('Error in adding new wisp', 16, 1)
END CATCH
There are a number of ways to use the OUTPUT clause. The following pattern may work for you:
insert dbo.tbl_CommentableEntity (ItemId)
select wispId from (
insert dbo.tbl_Wisps (UserId,WispTypeId,CreatedOnDate,PrivacyTypeId,WispText)
output inserted.wispId
values (#m_UserId, #m_WispTypeId, #m_CreatedOnDate, #m_PrivacyTypeId, #m_WispText)
) as ins
If you want to do it in code, I would split it into several methods and have the save methods return the identity. Then, you can just create a method that encompasses all three queries, and which emulates through code the same logic that is in the stored procedure
public int SaveThis()
{
return -1 //return identity
}
public int SaveThat(int thisID)
{
return -2 //return identity
}
public void SaveThisAndThat()
{
int thisID = this.SaveThis();
int thatID = this.SaveThat(thisID);
//so on and so forth
}

Categories