WHERE IN (array of IDs) - c#

I have webservice which is passed an array of ints.
I'd like to do the select statement as follows but keep getting errors. Do I need to change the array to a string?
[WebMethod]
public MiniEvent[] getAdminEvents(int buildingID, DateTime startDate)
{
command.CommandText = #"SELECT id,
startDateTime, endDateTime From
tb_bookings WHERE buildingID IN
(#buildingIDs) AND startDateTime <=
#fromDate";
SqlParameter buildID = new SqlParameter("#buildingIDs", buildingIDs);
}

You can't (unfortunately) do that. A Sql Parameter can only be a single value, so you'd have to do:
WHERE buildingID IN (#buildingID1, #buildingID2, #buildingID3...)
Which, of course, requires you to know how many building ids there are, or to dynamically construct the query.
As a workaround*, I've done the following:
WHERE buildingID IN (#buildingID)
command.CommandText = command.CommandText.Replace(
"#buildingID",
string.Join(buildingIDs.Select(b => b.ToString()), ",")
);
which will replace the text of the statement with the numbers, ending up as something like:
WHERE buildingID IN (1,2,3,4)
Note that this is getting close to a Sql injection vulnerability, but since it's an int array is safe. Arbitrary strings are not safe, but there's no way to embed Sql statements in an integer (or datetime, boolean, etc).

First you're going to need a function and a sproc. The function will split your data and return a table:
CREATE function IntegerCommaSplit(#ListofIds nvarchar(1000))
returns #rtn table (IntegerValue int)
AS
begin
While (Charindex(',',#ListofIds)>0)
Begin
Insert Into #Rtn
Select ltrim(rtrim(Substring(#ListofIds,1,Charindex(',',#ListofIds)-1)))
Set #ListofIds = Substring(#ListofIds,Charindex(',',#ListofIds)+len(','),len(#ListofIds))
end
Insert Into #Rtn
Select ltrim(rtrim(#ListofIds))
return
end
Next you need a sproc to use that:
create procedure GetAdminEvents
#buildingids nvarchar(1000),
#startdate datetime
as
SELECT id,startDateTime, endDateTime From
tb_bookings t INNER JOIN
dbo.IntegerCommaSplit(#buildingids) i
on i.IntegerValue = t.id
WHERE startDateTime <= #fromDate
Finally, your code:
[WebMethod]
public MiniEvent[] getAdminEvents(int[] buildingIDs, DateTime startDate)
command.CommandText = #"exec GetAdminEvents";
SqlParameter buildID= new SqlParameter("#buildingIDs", buildingIDs);
That goes way beyond what your question asked but it will do what you need.
Note: should you pass in anything that's not an int, the whole database function will fail. I leave the error handling for that as an exercise for the end user.

NOTE: I am not generally for using unparameterized queries. IN THIS INSTANCE, however, given that we are dealing with an integer array, you could do such a thing and it would be more efficient. However, given that everyone seems to want to downgrade the answer because it doesn't meet their criteria of valid advice, I will submit another answer that performs horribly but would probably run in LINK2SQL.
Assuming, as your question states, that you have an array of ints, you can use the following code to return a string that would contain a comma delimited list that SQL would accept:
private string SQLArrayToInString(Array a)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < a.GetUpperBound(0); i++)
sb.AppendFormat("{0},", a.GetValue(i));
string retVal = sb.ToString();
return retVal.Substring(0, retVal.Length - 1);
}
Then, I would recommend you skip trying to parameterize the command given that this is an array of ints and just use:
command.CommandText = #"SELECT id,
startDateTime, endDateTime From
tb_bookings WHERE buildingID IN
(" + SQLArrayToInString(buildingIDs) + ") AND startDateTime <=
#fromDate";

A superfast XML Method which requires no unsafe code or user defined functions :
You can use a stored procedure and pass the comma separated list of Building IDs :
Declare #XMLList xml
SET #XMLList=cast('<i>'+replace(#buildingIDs,',','</i><i>')+'</i>' as xml)
SELECT x.i.value('.','varchar(5)') from #XMLList.nodes('i') x(i))
All credit goes to Guru Brad Schulz's Blog

Visit T-SQL stored procedure that accepts multiple Id values for ideas on how to do this.

I use that approach and works for me.
My variable act = my list of ID's at string.
act = "1, 2, 3, 4"
command = new SqlCommand("SELECT x FROM y WHERE x.id IN (#actions)", conn);
command.Parameters.AddWithValue("#actions", act);
command.CommandText = command.CommandText.Replace("#actions", act);

[WebMethod]
public MiniEvent[] getAdminEvents(int buildingID, DateTime startDate)
...
SqlParameter buildID= new SqlParameter("#buildingIDs", buildingIDs);
Perhaps I'm being over detailed, but this method accepts a single int, not an array of ints. If you expect to pass in an array, you will need to update your method definition to have an int array. Once you get that array, you will need to convert the array to a string if you plan to use it in a SQL query.

You can use this. Execute in SQLServer to create a function on your DB (Only once):
IF EXISTS(
SELECT *
FROM sysobjects
WHERE name = 'FN_RETORNA_ID_FROM_VARCHAR_TO_TABLE_INT')
BEGIN
DROP FUNCTION FN_RETORNA_ID_FROM_VARCHAR_TO_TABLE_INT
END
GO
CREATE FUNCTION [dbo].FN_RETORNA_ID_FROM_VARCHAR_TO_TABLE_INT (#IDList VARCHAR(8000))
RETURNS
#IDListTable TABLE (ID INT)
AS
BEGIN
DECLARE
--#IDList VARCHAR(100),
#LastCommaPosition INT,
#NextCommaPosition INT,
#EndOfStringPosition INT,
#StartOfStringPosition INT,
#LengthOfString INT,
#IDString VARCHAR(100),
#IDValue INT
--SET #IDList = '11,12,113'
SET #LastCommaPosition = 0
SET #NextCommaPosition = -1
IF LTRIM(RTRIM(#IDList)) <> ''
BEGIN
WHILE(#NextCommaPosition <> 0)
BEGIN
SET #NextCommaPosition = CHARINDEX(',',#IDList,#LastCommaPosition + 1)
IF #NextCommaPosition = 0
SET #EndOfStringPosition = LEN(#IDList)
ELSE
SET #EndOfStringPosition = #NextCommaPosition - 1
SET #StartOfStringPosition = #LastCommaPosition + 1
SET #LengthOfString = (#EndOfStringPosition + 1) - #StartOfStringPosition
SET #IDString = SUBSTRING(#IDList,#StartOfStringPosition,#LengthOfString)
IF #IDString <> ''
INSERT #IDListTable VALUES(#IDString)
SET #LastCommaPosition = #NextCommaPosition
END --WHILE(#NextCommaPosition <> 0)
END --IF LTRIM(RTRIM(#IDList)) <> ''
RETURN
ErrorBlock:
RETURN
END --FUNCTION
After create the function you have to call this on your code:
command.CommandText = #"SELECT id,
startDateTime, endDateTime From
tb_bookings WHERE buildingID IN
(SELECT ID FROM FN_RETORNA_ID_FROM_VARCHAR_TO_TABLE_INT(#buildingIDs))) AND startDateTime <=
#fromDate";
command.Parameters.Add(new SqlParameter(){
DbType = DbType.String,
ParameterName = "#buildingIDs",
Value = "1,2,3,4,5" //Enter the parameters here separated with commas
});
This function get the text inner commas on "array" and make an table with this values as int, called ID. When this function is on you DB you can use in any project.
Thanks to Microsoft MSDN.
Igo S Ventura
Microsoft MVA
Sistema Ari de Sá
igo1-2#hotmail.com
P.S.: I'm from Brazil. Apologize my english... XD

Here's a Linq solution I thought up. It'll automatically insert all items in the list as parameters #item0, #item1, #item2, #item3, etc.
[WebMethod]
public MiniEvent[] getAdminEvents(Int32[] buildingIDs, DateTime startDate)
{
// Gets a list with numbers from 0 to the max index in buildingIDs,
// then transforms it into a list of strings using those numbers.
String idParamString = String.Join(", ", (Enumerable.Range(0, buildingIDs.Length).Select(i => "#item" + i)).ToArray());
command.CommandText = #"SELECT id,
startDateTime, endDateTime From
tb_bookings WHERE buildingID IN
(" + idParamString + #") AND startDateTime <=
#fromDate";
// Reproduce the same parameters in idParamString
for (Int32 i = 0; i < buildingIDs.Length; i++)
command.Parameters.Add(new SqlParameter ("#item" + i, buildingIDs[i]));
command.Parameters.Add(new SqlParameter("#fromDate", startDate);
// the rest of your code...
}

Related

Unable to create dynamic sql query in asp.net C#

I am keep getting
Conversion failed when converting the varchar value '46434,15864' to data type int.
I have this texbox which accepts numeric and commas. I need to create query with emp_num in (46434,15864) like syntax.
The query generated from codebehind is this, which runs fine manually in sql server:
SELECT * -- column names
FROM [DBO].[tablename] LPR
WHERE LPR.[EMPLOYEE_NUMBER] in (46434,15864)
code:
if (txtEmpNum.Text.Trim() != "")
{
////sb.Append(" and LPR.[EMPLOYEE_NUMBER] like '%'+ #empnumber + '%' ");
sb.Append(" and LPR.[EMPLOYEE_NUMBER] in (#empnumber) ");
cmd.Parameters.Add("#empnumber", SqlDbType.VarChar).Value = txtEmpNum.Text.Trim(); //.Replace("," , "','");
}
cmd.CommandText = sb.ToString();
DataTable dt = GetData(cmd);
gvdetails.DataSource = dt;
gvdetails.DataBind();
Table:
You will have to parametrize every value. This way you'll have dynamically created query, but NOT prone to SQL Inject
Here's code:
//where IN part of your query
string inClause = "and LPR.[EMPLOYEE_NUMBER] in ({0})";
// string arrays for values and variables of your query
string[] paramValues = txtEmpNum.Text.Trim().Split(',');
string[] paramVars = paramValues.Select((s, i) => "#empNo" + i.ToString()).ToArray();
//create query, ie. and LPR.[EMPLOYEE_NUMBER] in (#empNo0, #empNo1...)
inClause = string.Format(inClause, string.Join(", ", paramVars));
//add vars and values to command
for (int i = 0; i < paramVars.Length; i++)
{
cmd.Parameters.Add(paramVars[i], SqlDbType.Int).Value = paramValues[i];
}
You need a split function to create a list from an string. You can create that function running this script once :
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION [dbo].[fnSplitString]
(
#string NVARCHAR(MAX),
#delimiter CHAR(1)
)
RETURNS #output TABLE(splitdata NVARCHAR(MAX)
)
BEGIN
set #delimiter = coalesce(#delimiter, dbo.cSeparador());
DECLARE #start INT, #end INT
SELECT #start = 1, #end = CHARINDEX(#delimiter, #string)
WHILE #start < LEN(#string) + 1 BEGIN
IF #end = 0
SET #end = LEN(#string) + 1
INSERT INTO #output (splitdata)
VALUES(SUBSTRING(#string, #start, #end - #start))
SET #start = #end + 1
SET #end = CHARINDEX(#delimiter, #string, #start)
END
RETURN
END
Now your query will be :
SELECT *
FROM [DBO].[tablename] LPR
WHERE LPR.[EMPLOYEE_NUMBER] in (select * from fnSplitString(#empnumber, ','))
You can call it from C# exactly the same way you called your original code.
Basically the error is saying that your variable #empnumber is varchar and LPR.[EMPLOYEE_NUMBER] is integer
You can just append the value of your textbox to your query.
Edit: As others have suggested, this would be prone to SQL Injection. Marc Guillot and Nino solutions are better.

How to process an integer list row by row sent to a procedure in SQL Server 2008 R2?

I have an ArrayList (in C#) that contains some int numbers (those are IDs in a table), I want to select some data for each number(s) in this ArrayList and return a table variable or a #temporary table :)
I found a solution for passing this ArrayList as an user-defined table type to my stored procedure:
CREATE TYPE [dbo].[integer_list_tbltype] AS TABLE(
[n] [int] NOT NULL,
PRIMARY KEY CLUSTERED ([n] ASC)
WITH (IGNORE_DUP_KEY = OFF)
)
GO
CREATE PROCEDURE [dbo].[Sp_apr_get_apraisors]
(#listNumbers INTEGER_LIST_TBLTYPE readonly)
AS
....
but I didn't find an efficient way to read this array as easily as in C# :(
Is there any way to write a loop for each of these numbers and save data in a temp table and finally return it to C#??
SQL is set based, so your best option is to write a single select statement that would join your input table to the tables containing the data you would like to look up. The select statement would be the result set to be sent back to your application. Then if you want to use straight ADO.Net, you can use the SqlDataReader class to read back into C#, or you could use an ORM like Linq2Sql, Entity Framework, or NHibernate. By the way, if you must do a loop in Sql, please avoid cursors. They are slow and unnecessarily complicated both to manage and to develop. Use a while loop instead.
I would suggest you change the procedure parameter to varchar(n) and then send in those values as comma-delimited string.
DECLARE #IDs VARCHAR(MAX)
SELECT #IDs = '1,2,3'
DECLARE #ID INT
WHILE LEN(#IDs) > 0
BEGIN
SELECT #ID = CONVERT(INT, LEFT(#IDs, CHARINDEX(',', #IDs + ',') -1)
-- Do something with the ID here...
SELECT #IDs = STUFF(#IDs, 1, CHARINDEX(',', #IDs + ','), '')
END
mmmmm :), after 24h (!) search aorund the www , i found my problem Answer, #Toni's answer helped me on this :) Tanx #Toni :*
1) first define stored procedure
CREATE PROCEDURE [spName]( #list_entry VARCHAR(max)=NULL)
AS
BEGIN
SELECT [Column1,column2,...]
FROM [TABLE(s)]
WHERE ( #list_entry IS NULL
OR Column1 IN (SELECT value FROM Fn_split(#list_person, ',')) )
END
2) write a function to split items (comma delimited)
CREATE FUNCTION [dbo].[fn_Split](#text varchar(8000), #delimiter varchar(20) = ' ')
RETURNS #Strings TABLE
(
position int IDENTITY PRIMARY KEY,
value varchar(8000)
)
AS
BEGIN
DECLARE #index int
SET #index = -1
WHILE (LEN(#text) > 0)
BEGIN
SET #index = CHARINDEX(#delimiter , #text)
IF (#index = 0) AND (LEN(#text) > 0)
BEGIN
INSERT INTO #Strings VALUES (#text)
BREAK
END
IF (#index > 1)
BEGIN
INSERT INTO #Strings VALUES (LEFT(#text, #index - 1))
SET #text = RIGHT(#text, (LEN(#text) - #index))
END
ELSE
SET #text = RIGHT(#text, (LEN(#text) - #index))
END
RETURN
END
GO
3) pass my array as a comma-delimited string from .NET
//defin sample array list or your data
ArrayList array = new ArrayList();
//fill array with some data
for (int i = 1000; i<1010;i++)
array.Add(i);
//define connection and command
using(SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["connString"].ConnectionString))
{
connection.Open();
SqlCommand cmd = new SqlCommand("",connection);
cmd.Parameters.AddWithValue("#list_entry", SqlDbType.varchar,8000,Get_comma_delimited_string(array));
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "yourSpName";
cmd.ExecuteNonQuery();
}
/// <summary>
/// Resturns a comma delimited string (sepearte each item in list with ',' )
/// </summary>
public string Get_comma_delimited_string(ArrayList arrayList)
{
string result = string.Empty;
foreach (object item in arrayList)
result += item.ToString() + ",";
return result.Remove(result.Length - 1);
}

Invalid object name 'dbo.CategoryIdArray'

My Motive is to pass long array of ID as parameter to stored procedure and select data on the basis of ID. So i created Type in SQL Server
CREATE TYPE [dbo].[CategoryIdArray] AS TABLE(
[CategoryId] [bigint] NULL
)
GO
and stored procedure
ALTER PROCEDURE [dbo].[GetNewestArticleByCatsPageWise]
#dt as [dbo].[CategoryIdArray] READONLY,
#PageIndex INT = 1
,#PageSize INT = 10
,#PageCount INT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SELECT ROW_NUMBER() OVER
(
ORDER BY [dateadded]
)AS RowNumber,[desid]
INTO #Results
FROM [DB_user1212].[dbo].[discussions] as d , [DB_user1212].[dbo].[CategoryMap] as c where d.desid=c.[Topic Id] and c.[Category Id] in (select CategoryId from [dbo].[CategoryIdArray]) and [TopicType]='1' order by [dateadded]
DECLARE #RecordCount INT
SELECT #RecordCount = COUNT(*) FROM #Results
SET #PageCount = CEILING(CAST(#RecordCount AS DECIMAL(10, 2)) / CAST(#PageSize AS DECIMAL(10, 2)))
PRINT #PageCount
SELECT * FROM #Results
WHERE RowNumber BETWEEN(#PageIndex -1) * #PageSize + 1 AND(((#PageIndex -1) * #PageSize + 1) + #PageSize) - 1
DROP TABLE #Results
END
Tried to use above stored procedure by Code below
public List<String> getNewestArticleByCategoryPageWise( long[] categoryId)
{
List<string> topicId= new List<string>();
try
{
DataTable dt_Categories = new DataTable();
dt_Categories.Columns.Add("Category", typeof(String));
DataRow workRow;
foreach(long cat in categoryId)
{
workRow = dt_Categories.NewRow();
workRow["Category"] = cat;
dt_Categories.Rows.Add(workRow);
}
int pageIndex = 1;
SqlCommand cmd = new SqlCommand("dbo.GetNewestArticleByCatsPageWise", con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("#PageIndex", pageIndex);
cmd.Parameters.AddWithValue("#PageSize", 10);
cmd.Parameters.Add("#PageCount", SqlDbType.Int, 4).Direction = ParameterDirection.Output;
SqlParameter tvparam = cmd.Parameters.AddWithValue("#dt", dt_Categories);
tvparam.SqlDbType = SqlDbType.Structured;
con.Open();
sdr= cmd.ExecuteReader();
while(sdr.Read())
{
topicId.Add(sdr.GetString(0));
}
con.Close();
}
catch(Exception ex)
{
con.Close();
throw ex;
}
return topicId;
}
When i run above function exception is thrown Invalid object name 'dbo.CategoryIdArray'. But i created it as type. Help me out what i missed out. I refferred this.
Problem is with this line in stored procedure is with this line
select CategoryId from [dbo].[CategoryIdArray] .
We can not select from type like this, we should use
select CategoryId from #dt
The first thing that I do when I get these questions is to create a sample database. The code below creates the following.
1 - database named [test]
2 - table named [Discussions]
3 - table named [CategoryMap]
4 - user defined table type named [CategoryIdArray]
5 - load the tables with 100 records of data
--
-- Create a test db
--
USE [master];
go
CREATE DATABASE [Test];
GO
--
-- Create the user defined type
--
USE [Test];
go
CREATE TYPE [CategoryIdArray] AS
TABLE
(
[CategoryId] [bigint] NULL
);
--
-- Create skelton tables
--
create table Discussions
(
dis_id int identity (1,1),
dis_name varchar(64),
dis_added_dte datetime default getdate()
);
go
create table CategoryMap
(
cat_id int identity(1,1),
cat_topic_id int,
cat_topic_type char(1)
);
go
-- clear tables
truncate table Discussions;
truncate table CategoryMap;
go
--
-- Create 100 rows of dummy data
--
declare #cnt int = 0;
while #cnt < 100
begin
insert into Discussions (dis_name)
values ('sample discussion record # ' + str(#cnt, 2, 0));
insert into CategoryMap (cat_topic_id, cat_topic_type)
values (#cnt+1, '1')
set #cnt = #cnt + 1;
end;
go
--
-- Show the sample data
--
select * from Discussions;
go
select * from CategoryMap;
go
The second step is to re-write the stored procedure. If you are using below 2012, go with a window function rownumber(). In 2012, the offset and fetch clauses of the order by were included for paging.
http://technet.microsoft.com/en-us/library/ms188385(v=sql.110).aspx
--
-- Create my procedure
--
create procedure [GetArticlesByPage]
#Tvp as [CategoryIdArray] READONLY,
#PageIndex INT = 1,
#PageSize INT = 10,
#PageCount INT OUTPUT
AS
BEGIN
-- Declare variables
DECLARE #var_recs int = 0;
DECLARE #var_offset int = 0;
-- Do not count the records
SET NOCOUNT ON;
-- Start of paging
SET #var_offset = #var_offset + ((#PageIndex - 1) * #PageSize);
-- Set page count variable
SELECT #var_recs = count(*)
FROM
[dbo].[Discussions] as d
JOIN
[dbo].[CategoryMap] as c
ON
d.dis_id = c.cat_topic_id
JOIN
#TVP a
ON
c.cat_id = a.CategoryId
WHERE
cat_topic_type = '1';
set #PageCount = ceiling(cast(#var_recs as real) / cast(#PageSize as real));
--
-- Return the record set
--
SELECT
dis_id
FROM
[dbo].[Discussions] as d
JOIN
[dbo].[CategoryMap] as c
ON
d.dis_id = c.cat_topic_id
JOIN
#TVP a
ON
c.cat_id = a.CategoryId
WHERE
cat_topic_type = '1'
ORDER BY
dis_added_dte
OFFSET #var_offset ROWS
FETCH NEXT #PageSize ROWS ONLY;
END;
GO
I did leave the page count in place; However, I do not think it is needed since you can repeat the call until the result set is empty.
Please do not dump the record set into a temporary table since it could be quite large if you were return all the columns to display. I choose two separate calls. One for a total count. One for a single page.
The last TSQL part is to test the stored procedure from SSMS.
--
-- Call the stored procedure
--
-- instantiate tvp
DECLARE #my_tvp as [CategoryIdArray];
DECLARE #my_page_cnt as int;
-- add 25 entries
declare #cnt int = 25;
while #cnt < 50
begin
insert into #my_tvp (CategoryId)
values (#cnt + 1);
set #cnt = #cnt + 1;
end;
-- show the data in the tvp
select * from #my_tvp
-- call the function
exec [GetArticlesByPage] #my_tvp, 1, 10, #PageCount = #my_page_cnt OUTPUT;
-- show the data in the output
select #my_page_cnt as 'my_pages';
go
In my test example, I wanted rows 26 to 50 paged as 10 rows. Result 1 is the 25 rows, Result 2 is the 10 rows that were paged, and Result 3 is how many pages. Therefore, the TSQL part of the solution is sound.
Stay tuned for a C# program debug session later tonight.
http://www.mssqltips.com/sqlservertip/2112/table-value-parameters-in-sql-server-2008-and-net-c/
Take a look at this post. It is doing exactly what you are trying to do.
Here are some ideas to try.
1 - Make sure the connection properties, login's default database is [Test] for my example.
2 - Is the type defined in the [Test] database? Please double check this.
3 - Is this correct? The column name is [CategoryId] in the database type. You have the following - [Category]. Try changing the name in the C# code.
dt_Categories.Columns.Add("Category", typeof(String));
4 - Remove the [dbo]. from the type in the SP. It is not in the example from MS SQL Tips. Might be confusing the issue. SQL server will resolve the name.
5 - I noticed the type is defined as big int but the id in the tables is int? Make sure the data types are consistent.
Please try these suggestions. Get back to me on how you make out.
Can you get me a detailed call stack trace and error message if this is still an issue??
So here is a C# console application that I promised.
It works as expected.
You were mixing up some ideas that are the foundation of ADO.NET and data tables. You should get used to looking at the immediate window and local variables. This will help you track down issues.
Here is my sample call to the Stored Procedure.
1 - Setup data table (50 to 74)
2 - Page the data by 5's
3 - Look at second page
//
// Good Ref. - http://msdn.microsoft.com/en-us/library/ms254937(v=vs.110).aspx
//
// Basic stuff from C# console app
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// Required for data table
using System.Data;
using System.Data.SqlClient;
// Standard stuff ...
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
// Debug info
Console.WriteLine("Test - Start");
// Create the table with one column
DataTable my_Table;
my_Table = new DataTable("Category");
my_Table.Columns.Add("CategoryId", typeof(string));
// Add data to table
for (int my_Cnt = 50; my_Cnt < 75; my_Cnt++)
{
DataRow my_Row = my_Table.NewRow();
my_Row["CategoryId"] = my_Cnt.ToString();
my_Table.Rows.Add(my_Row);
}
// Debug info
Console.WriteLine("Test - created data set");
// Create a connection
SqlConnection my_Conn;
string str_Conn = "Server=localhost;Database=Test;Trusted_Connection=True;";
my_Conn = new SqlConnection(str_Conn);
// Debug info
Console.WriteLine("Test - create connection");
// Create the command and set its properties.
SqlCommand my_Cmd = new SqlCommand();
my_Cmd.Connection = my_Conn;
my_Cmd.CommandText = "dbo.GetArticlesByPage";
my_Cmd.CommandType = CommandType.StoredProcedure;
// Add parameter 0
SqlParameter my_Parm0 = new SqlParameter();
my_Parm0.ParameterName = "#Tvp";
my_Parm0.SqlDbType = SqlDbType.Structured;
my_Parm0.Direction = ParameterDirection.Input;
my_Parm0.Value = my_Table;
my_Cmd.Parameters.Add(my_Parm0);
// Add parameter 1
SqlParameter my_Parm1 = new SqlParameter();
my_Parm1.ParameterName = "#PageIndex";
my_Parm1.SqlDbType = SqlDbType.Int;
my_Parm1.Direction = ParameterDirection.Input;
my_Parm1.Value = 2;
my_Cmd.Parameters.Add(my_Parm1);
// Add parameter 2
SqlParameter my_Parm2 = new SqlParameter();
my_Parm2.ParameterName = "#PageSize";
my_Parm2.SqlDbType = SqlDbType.Int;
my_Parm2.Direction = ParameterDirection.Input;
my_Parm2.Value = 5;
my_Cmd.Parameters.Add(my_Parm2);
// Add parameter 3
SqlParameter my_Parm3 = new SqlParameter();
my_Parm3.ParameterName = "#PageCount";
my_Parm3.SqlDbType = SqlDbType.Int;
my_Parm3.Direction = ParameterDirection.Output;
my_Parm3.Value = 5;
my_Cmd.Parameters.Add(my_Parm3);
// Open the connection
my_Conn.Open();
// Debug info
Console.WriteLine("Test - execute reader");
// Execute the reader
SqlDataReader my_Reader = my_Cmd.ExecuteReader();
if (my_Reader.HasRows)
{
while (my_Reader.Read())
{
Console.WriteLine("{0}", my_Reader[0].ToString());
}
}
else
{
Console.WriteLine("No rows found.");
}
// Close the reader
my_Reader.Close();
// Number of pages (output after reader - order is important)
Console.WriteLine("Pages = ");
Console.WriteLine(my_Cmd.Parameters["#PageCount"].Value.ToString());
// Close the connection
my_Conn.Close();
// Debug info
Console.WriteLine("Test - close connection");
// Debug info
Console.WriteLine("Test - End");
// Pause to view output
Console.Read();
}
}
}
Here is a snapshot of the correct output from the C# console application.
I have to thank you for your question!
It has been a while since I coded in C#. But like a bike, does not take long to get back on it. The T-SQL examples were done with SSMS 2012 and the C# program was done with VS 2013. The latest and greatest.
Good nite!
I make no claim about efficient or correct -- but readable modern syntax your base query can be written like this:
SELECT ROW_NUMBER() OVER (ORDER BY [dateadded]) AS RowNumber,[desid]
INTO #Results
FROM [DB_user1212].[dbo].[discussions] as d
JOIN [DB_user1212].[dbo].[CategoryMap] as c ON d.desid=c.[Topic Id]
JOIN [dbo].[CategoryIdArray] arr ON c.[Category Id] = arr.CategoryID
WHERE [TopicType]='1'
Here is your solution:
In your stored procedure, in your WHERE statement, you are selecting * from a "TYPE" rather than the actual parameter object being passed in. It is like doing "SELECT * FROM VARCHAR", which makes no sense. Try this:
...
and c.[Category Id] in (
select CategoryId from #dt -- select from the actual parameter, not its TYPE
)
...
Instead of:
workRow["Category"] = cat;
use
workRow["CategoryId"] = cat;
Check in the SQL server management studio if the user has default database set to the database you're trying to access. I had the same type of error and got stuck for days. Finally found out the user had Master set as its' default DB.

Update a table from two comma separated parameter as input

I have a Gridview in front end where Grid have two columns : ID and Order like this:
ID Order
1 1
2 2
3 3
4 4
Now user can update the order like in front end Gridview:
ID Order
1 2
2 4
3 1
4 3
Now if the user click the save button the ID and order data is being sent to Stored Procedure as #sID = (1,2,3,4) and #sOrder = (2,4,1,3)
Now if I want to update the order and make save I want to store it into database. Through Stored procedure how can update into the table so that the table is updated and while select it gives me the results like:
ID Order
1 2
2 4
3 1
4 3
There is no built in function to parse these comma separated string. However, yo can use the XML function in SQL Server to do this. Something like:
DECLARE #sID VARCHAR(100) = '1,2,3,4';
DECLARE #sOrder VARCHAR(10) = '2,4,1,3';
DECLARE #sIDASXml xml = CONVERT(xml,
'<root><s>' +
REPLACE(#sID, ',', '</s><s>') +
'</s></root>');
DECLARE #sOrderASXml xml = CONVERT(xml,
'<root><s>' +
REPLACE(#sOrder, ',', '</s><s>') +
'</s></root>');
;WITH ParsedIDs
AS
(
SELECT ID = T.c.value('.','varchar(20)'),
ROW_NUMBER() OVER(ORDER BY (SELECT 1)) AS RowNumber
FROM #sIDASXml.nodes('/root/s') T(c)
), ParsedOrders
AS
(
SELECT "Order" = T.c.value('.','varchar(20)'),
ROW_NUMBER() OVER(ORDER BY (SELECT 1)) AS RowNumber
FROM #sOrderASXml.nodes('/root/s') T(c)
)
UPDATE t
SET t."Order" = p."Order"
FROM #tableName AS t
INNER JOIN
(
SELECT i.ID, p."Order"
FROM ParsedOrders p
INNER JOIN ParsedIDs i ON p.RowNumber = i.RowNumber
) AS p ON t.ID = p.ID;
Live Demo
Then you can put this inside a stored procedure or whatever.
Note that: You didn't need to do all of this manually, it should be some way to make this gridview update the underlying data table automatically through data binding. You should search for something like this instead of all this pain.
You could use a table valued parameter to avoid sending delimiter-separated values or even XML to the database. To do this you need to:
Declare a parameter type in the database, like this:
CREATE TYPE UpdateOrderType TABLE (ID int, Order int)
After that you can define the procedure to use the parameter as
CREATE PROCEDURE UpdateOrder (#UpdateOrderValues UpdateOrderType readonly)
AS
BEGIN
UPDATE t
SET OrderID = tvp.Order
FROM <YourTable> t
INNER JOIN #UpdateOrderValues tvp ON t.ID=tvp.ID
END
As you can see, the SQL is trivial compared to parsing XML or delimited strings.
Use the parameter from C#:
using (SqlCommand command = connection.CreateCommand()) {
command.CommandText = "dbo.UpdateOrder";
command.CommandType = CommandType.StoredProcedure;
//create a table from your gridview data
DataTable paramValue = CreateDataTable(orderedData)
SqlParameter parameter = command.Parameters
.AddWithValue("#UpdateOrderValues", paramValue );
parameter.SqlDbType = SqlDbType.Structured;
parameter.TypeName = "dbo.UpdateOrderType";
command.ExecuteNonQuery();
}
where CreateDataTable is something like:
//assuming the source data has ID and Order properties
private static DataTable CreateDataTable(IEnumerable<OrderData> source) {
DataTable table = new DataTable();
table.Columns.Add("ID", typeof(int));
table.Columns.Add("Order", typeof(int));
foreach (OrderData data in source) {
table.Rows.Add(data.ID, data.Order);
}
return table;
}
(code lifted from this question)
As you can see this approach (specific to SQL-Server 2008 and up) makes it easier and more formal to pass in structured data as a parameter to a procedure. What's more, you're working with type safety all the way, so much of the parsing errors that tend to crop up in string/xml manipulation are not an issue.
You can use charindex like
DECLARE #id VARCHAR(MAX)
DECLARE #order VARCHAR(MAX)
SET #id='1,2,3,4,'
SET #order='2,4,1,3,'
WHILE CHARINDEX(',',#id) > 0
BEGIN
DECLARE #tmpid VARCHAR(50)
SET #tmpid=SUBSTRING(#id,1,(charindex(',',#id)-1))
DECLARE #tmporder VARCHAR(50)
SET #tmporder=SUBSTRING(#order,1,(charindex(',',#order)-1))
UPDATE dbo.Test SET
[Order]=#tmporder
WHERE ID=convert(int,#tmpid)
SET #id = SUBSTRING(#id,charindex(',',#id)+1,len(#id))
SET #order=SUBSTRING(#order,charindex(',',#order)+1,len(#order))
END

T-SQL Select statement returns zero rows when using IN

I'm having some trouble with a t-sql select query in C#. The query is as follows:
#"SELECT * FROM
(SELECT *, ROW_NUMBER() OVER (ORDER BY id) as row_number
FROM [mytable]) t0
WHERE row_number BETWEEN #skip and #take
AND
uploadDate IN (#years)
AND NOT photoId IN (#shownPhotos)
ORDER BY NEWID()";
cmd.Parameters.Add("#skip", SqlDbType.Int).Value = skip;
cmd.Parameters.Add("#take", SqlDbType.Int).Value = take;
cmd.Parameters.Add("#shownPhotos", SqlDbType.VarChar).Value = shownPhotos;
cmd.Parameters.Add("#years", SqlDbType.VarChar).Value = years;
The method takes four parameters: skip count, take count, shownPhotos (commaseparated string which contains photo ids), years (comma separated string which contains the years to show the photos from)
So basically I want it to return photos from my database which matches any of the years from the commaseparated string and which are not already shown.
The problem (I think) is that my parameters are strings, so they are intepreted as '1234,567,890' and '2011,2012'. When running the query in the SQL server management tool without the 's it works fine.
Anyone got a workaround for this? :-)
Thanks a lot in advance!
Updated code:
// Create datatables
DataTable yearsTable = new DataTable();
yearsTable.Columns.Add("YearID", typeof(string));
yearsTable.Columns.Add("Year", typeof(string));
foreach(string year in years.Split(','))
yearsTable.Rows.Add(new object[] { year, year });
using (var conn = new SqlConnection(GlobalSettings.DbDSN))
{
conn.Open();
using (var cmd = conn.CreateCommand())
{
cmd.CommandType = CommandType.Text;
cmd.CommandText =
#"SELECT * FROM
(SELECT *, ROW_NUMBER() OVER (ORDER BY id) as row_number
FROM [mytable]) t0
WHERE row_number BETWEEN #skip and #take
AND
uploadedDate IN (#years)
ORDER BY NEWID()";
SqlParameter skipParam = cmd.Parameters.AddWithValue("#skip", skip);
skipParam.SqlDbType = SqlDbType.Int;
SqlParameter takeParam = cmd.Parameters.AddWithValue("#take", take);
takeParam.SqlDbType = SqlDbType.Int;
SqlParameter yearsParam = cmd.Parameters.AddWithValue("#years", yearsTable);
yearsParam.SqlDbType = SqlDbType.Structured;
yearsParam.TypeName = "dbo.MyTableType";
var reader = cmd.ExecuteReader();
}
You are right in your evaluation of the problem - #shownphotos and #years will be interpreted as a single string.
Table valued parameters are the answer
http://msdn.microsoft.com/en-us/library/bb675163.aspx
(Well done on parameterising your SQL statement - don't be tempted to work around this issue by concatenating a SQL statement from strings!)
Indeed, your IN are being interpreted as "is in this set of 1 value", i.e. what you have is identical to:
AND uploadDate = #years
AND NOT photoId = #shownPhotos
evaluated as regular string equality on the strings '1234,567,890' and '2011,2012'. At the TSQL level, a common trick is to write a "split" UDF that separates a single varchar into multiple row values. Many libraries have tools for this too; for example, in "dapper" we adding a syntax trick to pre-expand them, i.e.
int[] years = {2011,2012};
int[] shownPhotos = {1234,567,890};
int skip = 0, take = 10;
var rows = connection.Query<RowType>(
#"SELECT * FROM
(SELECT *, ROW_NUMBER() OVER (ORDER BY id) as row_number
FROM [mytable]) t0
WHERE row_number BETWEEN #skip and #take
AND uploadDate IN #years
AND NOT photoId IN #shownPhotos
ORDER BY NEWID()", new {skip,take,years,shownPhotos}).ToList();
Note the lack of brackets next to IN, which we interpret as "figure this out yourself" to the library, which then does exactly what you want. It will actually be evaluated as:
AND uploadDate IN (#years0,#years1)
AND NOT photoId IN (#shownPhotos0,#shownPhotos1,#shownPhotos2)
where #years0 has the value 2011, #years1 has the value 2012, etc.
Looks like you're comparing the entire date against a set of years.
Why not extract just the year and compare?
#"SELECT * FROM
(SELECT *, ROW_NUMBER() OVER (ORDER BY id) as row_number
FROM [mytable]) t0
WHERE row_number BETWEEN #skip and #take
AND
DATEPART(yyyy,uploadDate) IN (#years)
AND NOT photoId IN (#shownPhotos)
ORDER BY NEWID()";
Read more on DATEPART here: http://www.w3schools.com/sql/func_datepart.asp

Categories

Resources