I'm trying to execute a SQL command that insert a record on a table and returns the generate primary key.
I'm using .NET Core 3.1 and Oracle.ManagedDataAccess.Core package.
This is the C# code to execute the SQL command (it uses some extension methods but is clear how it works):
private int PutSomethingInTheDatabase(string entity)
{
string sqlComamnd = File.ReadAllText("SQL//Insert Card.sql");
using (var connection = new Oracle.ManagedDataAccess.Client.OracleConnection(connectionString))
using (var command = connection.OpenAndUse().CreateTextCommand(sqlComamnd))
{
//var reader = command.ExecuteReader();
//reader.Close();
//var result = command.ExecuteScalar();
//return (int)(decimal)result;
return -1;
}
}
Ideally I will receive a single value and read it with ExecuteScalar().
It is an itegration test (thats why I read the SQL from a file).
The SQL I want to use should INSERT the new record and return the generated sequence within the same scope/transaction, that's whi I'm using Begin/End but I'm not sure it is the right way.
My problem is that I cannot find the right syntax to execute the last SELECT to return the generated sequence_id, I also tried with RETURN...
This is the SQL:
declare new_id number;
BEGIN
select seq_stage_card.NEXTVAL into new_id from dual;
INSERT INTO spin_d.stage_card (
sequence_id,
field_1,
field_2
)
VALUES (
new_id,
'aaa'
TO_DATE('2003/05/03 21:02:44', 'yyyy/mm/dd hh24:mi:ss')
);
select new_id from dual where 1 = 1 ; -- not valid
END;
-- return new_id ; -- not valid
-- select new_id from dual ; -- not valid
How to change the SQL in order to return the new_id ?
There is another (better) way to achieve the same result?
Is it safe (isolated scope), or the select will return a wrong ID if there is a concurrent insert?
[Update]
Someone suggested to use RETURNING (see here: Oracle - return newly inserted key value)
I already tried to use RETURN and RETURNING but I haven't find any real example of usage with the .NET (or other frameworks) driver, eg. OracleSqlCommand and the right call to execute.
Maybe it works but I still cannot figure out how to use it.
In general case (when you have to implement some logics within anonymous block, and when returning is not an option) try bind variables: first, turn new_id into :new_id in the query:
BEGIN
SELECT seq_stage_card.NEXTVAL
INTO :new_id -- bind variable to return back to c#
FROM dual;
INSERT INTO spin_d.Stage_Card (
sequence_id,
field_1,
field_2
)
VALUES (
:new_id,
'aaa',
TO_DATE('2003/05/03 21:02:44', 'yyyy/mm/dd hh24:mi:ss')
);
END;
Then use it in C# code:
...
using (var command = connection.OpenAndUse().CreateTextCommand(sqlComamnd))
{
//TODO: check the syntax and RDBMS type
command.Parameters.Add(
":new_id",
OracleDbType.Int32).Direction = ParameterDirection.Output;
// Execute query
command.ExecuteNonQuery();
// Bind variable reading
return Convert.ToInt32(command.Parameters[0].Value);
}
Related
I just started learning Dapper in C# but I am having difficulty in executing stored procedures with two or more SQL Statements in one transaction.
How do you get the output parameter of a Stored Procedure that
contains both Insert and Select statements in C# using Dapper?
Here is my stored procedure:
ALTER PROCEDURE [dbo].[AddToFileDetailsAndGetPrimaryKey]
-- 1. declare input variables
#file_name NVARCHAR(100) = NULL,
-- 2. declare output variable
#file_details_pk UNIQUEIDENTIFIER OUTPUT
AS
-- 3. instantiate holder table
DECLARE #pk_holder TABLE
(
retrieved_pk UNIQUEIDENTIFIER
)
-- 4. insert into FileDetails
INSERT INTO dbo.FileDetails
(
file_name
)
OUTPUT INSERTED.file_details_pk INTO #pk_holder
VALUES
(
#file_name
);
-- 5. set FileDetails primary key to OUTPUT variable
SELECT #file_details_pk = retrieved_pk
FROM #pk_holder
Here is the code I'm using to execute the stored procedure:
using (IDbConnection connection = new System.Data.SqlClient.SqlConnection(Configuration.GetConnectionString("TESTDB")))
{
List<FileDetails> fileList = new List<FileDetails>();
fileList.Add(new FileDetails { file_name = fileName});
Guid outputPrimaryKey;
connection.Execute("dbo.AddToFileDetailsAndGetPrimaryKey #file_name, #file_details_pk", fileList, outputPrimaryKey);
}
Is this the correct way to do it? Should I use connection.Execute or
connection.Query? I am also getting an error of "cannot convert from
System.Guid to System.Data.IDbTransaction in my outputPrimaryKey"
A. Dapper does not have a Query and Execute "combined" method, to my best knowledge.
B. However, since your stored procedure is a black box with input and output parameters, you can try this: (pseudo code below, not tested)
var p = new DynamicParameters();
p.Add("#file_name", "fileOne");
p.Add("#file_details_pk", dbType: DbType.Guid, direction: ParameterDirection.Output);
cnn.Execute("dbo.AddToFileDetailsAndGetPrimaryKey", p, commandType: CommandType.StoredProcedure);
Guid b = p.Get<Guid>("#file_details_pk");
From:
https://github.com/perliedman/dapper-dot-net
("stored procedures")
In general:
Dapper is built for speed.
Also. Dapper has limited functionality:
From
https://github.com/perliedman/dapper-dot-net
Dapper is a single file you can drop in to your project that will
extend your IDbConnection interface.
It provides 3 helpers:
See
Comparing QUERY and EXECUTE in Dapper
and
https://github.com/perliedman/dapper-dot-net/blob/master/Dapper%20NET40/SqlMapper.cs
PS..........
You seem to have a small bug in your "output" clause.....
You are pushing the file_name into the holding-table, not new value of the newly inserted PK.
-- 4. insert into FileDetails
INSERT INTO dbo.FileDetails
(
file_name
)
OUTPUT INSERTED.file_details_pk INTO #pk_holder
VALUES
(
#file_name /* << this looks wrong */
);
I am using Sp to produce some logic and return a table object.If no object found i just simply return null of a entity
CREATE proc SpDealerDistributionOracle
(
#DealerCode varchar(50),
#imei varchar(50)
)
as
BEGIN
if (some logic)
select top 1 * from tblBarCodeInv
else
select null;
END
Works fine ..But when I wrote query in EF 6 like this
tblBarCodeInv returnValue = null;
using (var db=new RBSYNERGYEntities())
{
String query = String.Format("SpDealerDistributionOracle 'DealerCode','101001'");
returnValue = db.Database.SqlQuery<tblBarCodeInv>(query).FirstOrDefault();
}
return returnValue;
It throws an exception.
I simply want to return a object if not found return null and do some logic in C#.Can anybody help??
Change your procedure to still return a "collection" in both cases. However in the second case it will be an empty collection and thus will reach the "Default" of the FirstOrDefault
BEGIN
if (some logic)
select top 1 * from tblBarCodeInv
else
SELECT TOP 0 * from tblBarCodeInv
END
I am trying to access the return value of a stored procedure with Linq
DECLARE #ValidToken int = 0 //I have also tried using a bit instead of an int here.
IF EXISTS(SELECT 1 FROM Tests WHERE TestToken = #Token)
select #ValidToken = 1
return #ValidToken
This works when running the SP through sql studio. However I am trying to run it with linq using the datacontext class and it is always returning -1.
using (DataEntities dataEntities = new DataEntities())
{
int query = data.ValidateToken(id);
Response.Write(query.ToString());
}
query will always equal -1 and I am not sure why.
I have looked online and it would appear that there are easier ways to get the return value however I would rather stick to Linq as that is what the rest of the program is using.
Why are you all using a stored procedure as function?
Though a stored procedure has return, it is not a function, so you should not use return to return data,
Rather, you should use Output parameters and other ways to return data, as documented at MSDN
And the use of return is documented at MSDN
misusing return is probably the reason why LINQ does not understand your stored procedures.
this was I did and it worked.
In Sqlserver:
ALTER PROCEDURE [dbo].[ValidateToken]
-- Add the parameters for the stored procedure here
#Id int
AS
BEGIN
DECLARE #Ret INT
SELECT
#Ret = COUNT(*)
FROM Test
set #Ret =#Id+1;
select #Ret
END
In C#:
static void Main(string[] args)
{
using (SergioEntities dm = new SergioEntities())
{
int? validToken = 1;
int? a = dm.ValidateToken(validToken).First();
}
}
Give it a try :)
Try to change the RETURN 0/1 statements to SELECT 0/1
The problem with you code is you use IF(query) instead of that you need to user it like IF EXISTS(query) so you code will be like as below
CREATE PROCEDURE [dbo].[MyProc]
AS
BEGIN
DECLARE #Ret INT
//use of IF EXISTS instead of IF
IF EXISTS(SELECT * from Tests WHERE TestToken = #Token)
SELECT #Ret = 0
ELSE
SELECT #Ret = 1
RETURN #Ret
END
Your code may look like,
using (TestDBDataContext db = new TestDBDataContext())
{
//For Stored Procedure with Return value (for Integer)
//returns Int
var q = db.MyProc();
Console.WriteLine(q);
}
If this doesnt work than go for the other solutions
Output Parameter
Using Scalar-Values Functions
Which are discuss here : LINQ to SQL : Returning Scalar Value from Stored Procedure
CREATE PROCEDURE [dbo].[MyProc]
AS
BEGIN
DECLARE #Ret INT
// use of IF EXISTS instead of IF
IF EXISTS(SELECT * from Tests WHERE TestToken = #Token)
SELECT #Ret = 0
ELSE
SELECT #Ret = 1
RETURN #Ret
END
I am trying to get the return value of a stored procedure. Here is an example of such a stored procedure:
select
Name,
IsEnabled
from
dbo.something
where
ID = #ID
if ##rowcount = 0
return 1
return
This is a simple select. If 0 rows are found, my result set will be null, but I will still have a return value.
This is a bad example, as this is a select, so sure I could find if 0 rows were returned. However, on an Insert, delete, or other calls, we need this return value to know if there was a problem. I have been unable to find a way to get this return value. I can get output values, I can get result sets, but no return value.
I can get the return value if I call SQL manually, or even if I run a SqlCommand using the Entity Framework, but this is not what I want to do.
Has anyone ever been able to get the return value from a stored procedure using Entity Framework?
Thanks for the help!
I guess support of stored procedure return values depends on version of Entity framework. Although directly returning value didn't work for me I managed to get value using OUTPUT stored procedure parameter. Example:
USE [YOUR_DB_NAME]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[TestReturnValue]
#InputValue int = 0,
#Result int OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SELECT #Result = #InputValue
RETURN(#InputValue);
END
This is test code:
void TestReturnValue()
{
using (var db = new YourDBContext())
{
var result = new System.Data.Objects.ObjectParameter("Result", 0);
Console.WriteLine("SP returned {0}", db.TestReturnValue(123, result));
Console.WriteLine("Output param {0}", result.Value);
}
}
And this is output:
SP returned -1
Output param 123
As you see directly returned value output some rubbish but OUTPUT parameters works!
This article provides general info on two ways of returning values from SP
Hope this helps
No. Entity Framework doesn't have rich stored procedure support because its an ORM, not a SQL replacement.
As you have already discovered, if you need to use less common (more advanced?) features of stored procedures, you'll have to use good old fashioned ADO.NET.
You must use the "Context.Database.SqlQuery",and specify the output type of the stored procedure
var spReturnVaue = Context.Database.SqlQuery<Type>("[schema].YourSp").FirstOrDefault();
int firstId = Context.Database.SqlQuery<int>("[dbo].GetFirstId").FirstOrDefault();
Based on Hadi Salehy's answer, I wrote this code and works for me.
My SP
CREATE PROCEDURE [schema].[ProcedureName]
#param1 int,
#param2 varchar(20),
#param3 varchar(50)
AS
UPDATE Table1
SET Field2 = #param2, Fiedl3 = #param3
WHERE (Field1 = #param1)
SELECT ##ROWCOUNT;
My C# code.
int quantity = await _context.Database.ExecuteSqlRawAsync("[schema].[ProcedureName] {0}, {1}, {2}", myModel.Param1, myModel.Param2, myModel.Param3);
In order to get the interested result in EF, you should return a same structure (not 1 and Name, IsEnabled)
In this query you should return '',0 for example instead of 1 in the if condition:
select
Name,
IsEnabled
from
dbo.something
where
ID = #ID
if ##rowcount = 0
return '',0
Scenario
I have a stored procedure that takes a single parameter. I want to update this stored procedure to take a VARIABLE NUMBER OF PARAMETERS - a number that I will never know.
I currently use SQLConnections through a C# interface in order to pass in a single parameter to the stored procedure and return a result.
The SQL Part
Lets say that I have a stored procedure that returns a list of results based on a single input parameter "#ccy" - (Currency). Now lets say that I want to update this stored procedure to take a list of Currencies instead of a single one, but that this number will be variable depending on the situation.
The SQL Code
ALTER PROCEDURE [dbo].[SEL_BootStrapperInstRICs]
(
#ccy varchar(10)
)
AS
SELECT DISTINCT i.CCY, i.Instrument, i.Tenor, r.RIC, r.[Server], r.RIType
FROM MDR.dbo.tblBootStrapperInstruments as i, MDR.dbo.tblBootStrapperRICs as r
WHERE i.Instrument = r.MurexInstrument
AND
i.Tenor = r.Tenor
AND i.CCY = r.CCY
AND i.CCY = #ccy
AND r.RIType NOT LIKE '%forward%'
The C# Part
This particular stored procedure is called from a C# WinForms application that uses the "SqlCommand.Parameters.AddWithValue()" method. As mentioned earlier this method currently passes in a single Currency as the parameter to the stored procedure and returns the result as a DataSet.
public DataSet GetBootStrapperInstRICsDS(List<string> ccys)
{
DataSet ds;
SqlConnection dbConn = null;
SqlCommand dbCmd = new SqlCommand();
try
{
dbConn = GetSQLConnection();
dbCmd = GetSqlCommand();
dbCmd.CommandType = CommandType.StoredProcedure;
dbCmd.CommandText = Utils.Instance.GetSetting ("SELBootStrapInsRics", "default");
foreach(string ccy in ccys)
dbCmd.Parameters.AddWithValue("#ccy", ccy);
dbCmd.CommandTimeout = 600;
dbCmd.Connection = dbConn;
SqlDataAdapter adapter = new SqlDataAdapter(dbCmd);
ds = new DataSet();
adapter.Fill(ds, "tblBootStrapperInstRICs");
dbCmd.Connection.Open();
return ds;
}
catch (Exception ex)
{
ApplicationException aex = new ApplicationException ("GetBootStrapperInstRICsDS", ex);
aex.Source = "Dal.GetBootStrapperInstRICsDS " + ex.Message;
MainForm.job.Log(aex.Source, Job.MessageType.Error);
Job.incurredErrors = true;
throw aex;
}
finally
{
if (dbCmd != null)
dbCmd.Dispose();
if (dbConn != null)
{
dbConn.Close();
dbConn.Dispose();
}
}
}
The Question
On the C# side I think my best option is to use a "foreach/for loop" in order to iterate through a list of parameters and dynamically add a new one to the SPROC. (I have already made this update in the C# code above).
HOWEVER - Is there some way that I can do this in the SQL Stored Procedure too? My thoughts are split with two potential options - Either create 20 or more parameters in the SPROC (each with the same name but with an incrementing number at the end e.g. - #ccy1,#ccy2 etc.) and use "for(int i=0;i
for(int i=0;i<NumberOfCurrenciesToAdd;i++)
dbCmd.Parameters.AddWithValue("#ccy"+i, currencyArray[i]);
Or the other option is to do something completely different and less rubbish and hack-esque. Help greatly appreciated.
EDIT - SQL Server 2005
EDIT2 - Must Use SPROCS - Company Specification Requirement.
You never specified SQL Server version, but for 2008 there are Table-Valued Parameters, which may help you:
Table-valued parameters are a new parameter type in SQL Server 2008. Table-valued parameters are declared by using user-defined table types. You can use table-valued parameters to send multiple rows of data to a Transact-SQL statement or a routine, such as a stored procedure or function, without creating a temporary table or many parameters.
I worked for a company that had to do this. It is much easier to just pass an nvarchar that is really a list that is comma delimited and then parse it when you get into the stored proc and insert the values into a temp table. The other option would be to have an xml parameter in your proc. That should also work. This is all for SQL 2005. 2008 does give you the table variable and that would be your best option.
I would try to stay away from dynamically changing your stored proc because I think that would be hard to maintain. At any given time if you try to look at the proc it could be different. Also, what happens when 2 people are trying to use your site and hit that proc at the same moment? One person's session will be modifying the procedure and the others will try to do it. This could cause a lock on the stored proc or it could cause other issues. Regardless it would be pretty inefficient.
Here is another option - though I think Anton's answer is better. You can pass in a csv string as a single parameter. Use a user-defined function to convert the csv string into a table of values, which you can join in your query. There are several csv parsing functions listed on SO and other places (though, sorry, I can't come up with a link right now).
edit: here is another option. Pass in the same csv string, then generate the sql query as a string in the procedure, and execute the string. Use the csv in an 'in' clause :
where i.ccy in (1,2,3,4)
I would not try to change the stored procedure, but (since you are on SQL Server 2005 and don't have table variable parameters) just pass in a comma separated list of values and let the procedure split them apart. You can change your C# loop to just build a CSV string and once you create a SQL split procedure, use it like:
SELECT
*
FROM YourTable y
INNER JOIN dbo.yourSplitFunction(#Parameter) s ON y.ID=s.Value
I prefer the number table approach to split a string in TSQL
For this method to work, you need to do this one time table setup:
SELECT TOP 10000 IDENTITY(int,1,1) AS Number
INTO Numbers
FROM sys.objects s1
CROSS JOIN sys.objects s2
ALTER TABLE Numbers ADD CONSTRAINT PK_Numbers PRIMARY KEY CLUSTERED (Number)
Once the Numbers table is set up, create this split function:
CREATE FUNCTION [dbo].[FN_ListToTable]
(
#SplitOn char(1) --REQUIRED, the character to split the #List string on
,#List varchar(8000)--REQUIRED, the list to split apart
)
RETURNS TABLE
AS
RETURN
(
----------------
--SINGLE QUERY-- --this will not return empty rows
----------------
SELECT
ListValue
FROM (SELECT
LTRIM(RTRIM(SUBSTRING(List2, number+1, CHARINDEX(#SplitOn, List2, number+1)-number - 1))) AS ListValue
FROM (
SELECT #SplitOn + #List + #SplitOn AS List2
) AS dt
INNER JOIN Numbers n ON n.Number < LEN(dt.List2)
WHERE SUBSTRING(List2, number, 1) = #SplitOn
) dt2
WHERE ListValue IS NOT NULL AND ListValue!=''
);
GO
You can now easily split a CSV string into a table and join on it:
select * from dbo.FN_ListToTable(',','1,2,3,,,4,5,6777,,,')
OUTPUT:
ListValue
-----------------------
1
2
3
4
5
6777
(6 row(s) affected)
Your can pass in a CSV string into a procedure and process only rows for the given IDs:
SELECT
y.*
FROM YourTable y
INNER JOIN dbo.FN_ListToTable(',',#GivenCSV) s ON y.ID=s.ListValue
I use this function to split CSV text into a table of numbers, it has great performance due to various optimizations (like returning a table with a primary key which greatly influence the query optimizer to produce good query plans ever for extremely large data sets).
Also it's not limited to 4000 characters, so you can pass in very large strings.
CREATE Function [dbo].[TextSplitToInt](#list text,
#delim char(1) = N',')
RETURNS #T TABLE (ID_T int primary key)
BEGIN
DECLARE #slices TABLE (slice nvarchar(4000) NOT NULL)
DECLARE #slice nvarchar(4000),
#textpos int,
#maxlen int,
#stoppos int
SELECT #textpos = 1, #maxlen = 4000 - 2
WHILE datalength(#list) / 2 - (#textpos - 1) >= #maxlen
BEGIN
SELECT #slice = substring(#list, #textpos, #maxlen)
SELECT #stoppos = #maxlen - charindex(#delim, reverse(#slice))
INSERT #slices (slice) VALUES (#delim + left(#slice, #stoppos) + #delim)
SELECT #textpos = #textpos - 1 + #stoppos + 2 -- On the other side of the comma.
END
INSERT #slices (slice)
VALUES (#delim + substring(#list, #textpos, #maxlen) + #delim)
INSERT #T (ID_T)
SELECT distinct Cast(str as int)
FROM (SELECT str = ltrim(rtrim(substring(s.slice, N.Number + 1,
charindex(#delim, s.slice, N.Number + 1) - N.Number - 1)))
FROM Numbers N
JOIN #slices s ON N.Number <= len(s.slice) - 1
AND substring(s.slice, N.Number, 1) = #delim) AS x
RETURN
END