How to make parameterized query involving multiple tables with ADO.NET / C#? - c#

I would like to make parameterized query for SQLquery that can be something like this:
SELECT * FROM Table1 WHERE Col1 IN (SELECT Col2 FROM Table2 WHERE Col3 IN (1, 2, 3));
The values come from WS interface and although I can trust the consumers I want to play it safe and use parameterized query involving DbParameters to prevent SQL injection. How to do it?
Thanks!

The key point, as you note, is to use parameters. IN clauses are notoriously problematic for that, annoyingly. Now, if you know the values are all integers (for example you are taking an int[] parameter to your C# method), then you can just about get away with things like:
cmd.CommandText = "SELECT * FROM Table1 WHERE Col1 IN (SELECT Col2 FROM Table2 WHERE Col3 IN ("
+ string.Join(",", values) + "))"; // please don't!!!
It is horrible, suggests a very bad practice (if somebody copies it for strings, you are in a world of pain), and can't use query plan caching. You could do something like:
var sb = new StringBuilder("SELECT * FROM Table1 WHERE Col1 IN (SELECT Col2 FROM Table2 WHERE Col3 IN (");
int idx = 0;
foreach(var val in values) {
if(idx != 0) sb.Append(',');
sb.Append("#p").Append(idx);
cmd.Parameters.AddWithValue("#p" + idx, val);
idx++
}
sb.Append("))");
cmd.CommandText = sb.ToString();
Which is preferable but awkward.
Or simplest: with a tool like dapper, let the library worry about it:
var data = conn.Query<YourType>(
"SELECT * FROM Table1 WHERE Col1 IN (SELECT Col2 FROM Table2 WHERE Col3 IN #values)",
new { values });
Here dapper spots the usage and "does the right thing". It also handles the "0 values" case for you.

Here is one example:
SqlCommand cmd = new SqlCommand("SELECT * FROM Table1 WHERE Col1 IN (SELECT Col2 FROM Table2 WHERE Col3 = #myparam", conn);
//define parameters used in command object
SqlParameter param = new SqlParameter();
param.ParameterName = "#myparam";
param.Value = "myvalue";
//add new parameter to command object
cmd.Parameters.Add(param);
// get data stream
reader = cmd.ExecuteReader();
Check out this link for more details:
http://csharp-station.com/Tutorial/AdoDotNet/Lesson06

The difficulty here is parameterizing each individual IN clause so that you can pass through a variable number of IDs.
Have a look at this useful article as to one approach to solve this: http://www.mikesdotnetting.com/Article/116/Parameterized-IN-clauses-with-ADO.NET-and-LINQ
Basically, it involves a little bit of string manipulation to build up the parameterized list of IN clauses, so you end up with a SQL statement that looks like this for your particular example with 3 IDs:
select * from TABLE1 where COL1 in (select COL2 from TABLE2 where COL3 IN (#p1, #p2, #p3));

Related

How to i get result based on ids list passed as a varchar?

I am passing ids list as a varchar(500) and based upon that ids records are required.My sql code is
declare #Ids varchar(500) = '12964,12965,12966'
select *
from tblBooks
where BookID in (#Ids)
where BookID is varchar(50).Number of Ids can be 100.Converting #Ids into int gives following error
Conversion failed when converting the varchar value
'12964,12965,12966' to data type int
How do i find result as #Id are not converted into Int.
Use a table variable:
DECLARE #Ids TABLE (ID INT);
INSERT #Ids VALUES (12964),(12965),(12966);
SELECT *
FROM tblBooks
WHERE BookID in (SELECT ID FROM #Ids);
If you need to pass this to a procedure then you can use a table valued parameter:
CREATE TYPE dbo.ListOfInt AS TABLE (ID INT);
GO
CREATE PROCEDURE dbo.GetBooks #IDs dbo.ListOfInt READONLY
AS
BEGIN
SELECT *
FROM tblBooks
WHERE BookID in (SELECT ID FROM #Ids);
END
GO
DECLARE #IDs dbo.ListofInt;
INSERT #Ids VALUES (12964),(12965),(12966);
EXECUTE dbo.GetBooks #Ids;
Or From c#
var table = new DataTable();
table.Columns.Add("ID", typeof(int));
// ADD YOUR LIST TO THE TABLE
using (var connection = new SqlConnection("Connection String"))
using (var command = new SqlCommand("dbo.GetBooks", connection))
{
command.CommandType = CommandType.StoredProcedure;
var param = new SqlParameter("#Ids", SqlDbType.Structured);
param.TypeName = "dbo.ListofInt";
param.Value = table;
command.Parameters.Add(table);
connection.Open();
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
// do something
}
}
}
Once the TYPE is in place, you don't even need to use a stored procedure. You can simply call a normal query:
using (var connection = new SqlConnection("Connection String"))
using (var command = new SqlCommand("SELECT * FROM tblBooks WHERE BookID IN (SELECT ID FROM #IDs)", connection))
{
var param = new SqlParameter("#Ids", SqlDbType.Structured);
param.TypeName = "dbo.ListofInt";
param.Value = table;
command.Parameters.Add(table);
connection.Open();
// ETC
}
Doing the split in c# using String.Split() and passing the list to SQL will be more efficient than any approach that does the split in SQL
You can write the query as this:
declare #Ids varchar(500) = '12964,12965,12966'
select *
from tblBooks
where ','+cast(BookID as varchar(500))+',' like '%,'+#Ids+',%';
But you don't want to do that because the performance is bad -- the query cannot use indexes.
Three other options. Use dynamic SQL and plug the list directly into the query. Or use a split function to split the string. Or use a table variable:
declare #ids table (id int);
insert into #ids(id)
select 12964 union all select 12965 union all select 12966;
select b.*
from tblBooks b
where b.BookId in (select id from #ids);
This won't work. SQL Server does not split strings for you implicitly and there is no built in string split function in SQL Server either.
If you are driving this via C# you can use Table value parameters. You can also pass your query through Dapper-Dot-Net which will automatically parameterize an "In" query.
If you really must do this in T-SQL, you can also use a string splitting logic here is a relatively concise one.
SELECT i.value('./text()[1]', 'int') [id] into #ids
FROM( values(CONVERT(xml,'<r>' + REPLACE(#Ids+left(##dbts,0),',','</r><r>') + '</r>')) ) a(_)
CROSS APPLY _.nodes('./r') x(i)
select *
from tblBooks a
join #ids i on i.id = a.bookId
Create this function:
CREATE FUNCTION [dbo].[SplitDelimiterString] (#StringWithDelimiter VARCHAR(8000), #Delimiter VARCHAR(8))
RETURNS #ItemTable TABLE (Item VARCHAR(8000))
AS
BEGIN
DECLARE #StartingPosition INT;
DECLARE #ItemInString VARCHAR(8000);
SELECT #StartingPosition = 1;
--Return if string is null or empty
IF LEN(#StringWithDelimiter) = 0 OR #StringWithDelimiter IS NULL RETURN;
WHILE #StartingPosition > 0
BEGIN
--Get starting index of delimiter .. If string
--doesn't contain any delimiter than it will returl 0
SET #StartingPosition = CHARINDEX(#Delimiter,#StringWithDelimiter);
--Get item from string
IF #StartingPosition > 0
SET #ItemInString = SUBSTRING(#StringWithDelimiter,0,#StartingPosition)
ELSE
SET #ItemInString = #StringWithDelimiter;
--If item isn't empty than add to return table
IF( LEN(#ItemInString) > 0)
INSERT INTO #ItemTable(Item) VALUES (#ItemInString);
--Remove inserted item from string
SET #StringWithDelimiter = SUBSTRING(#StringWithDelimiter,#StartingPosition +
LEN(#Delimiter),LEN(#StringWithDelimiter) - #StartingPosition)
--Break loop if string is empty
IF LEN(#StringWithDelimiter) = 0 BREAK;
END
RETURN
END
Then call it like this:
declare #Ids varchar(500) = '12964,12965,12966'
select *
from tblBooks
where BookID in (SELECT * FROM dbo.SplitDelimiterString(#ids,','))
one way is to cast int to varchar. many other ways....
select *
from tblBooks
where CAST(BookID as varchar(50)) in (#Ids)
related: Define variable to use with IN operator (T-SQL)

Select more columns with MAX function in SQLCE

Need to find max value of id, and by this value I need to read value of others column. But it is influenced by another column type.
I used this sql command:
"SELECT * FROM Table WHERE id = (SELECT MAX(id) FROM Table WHERE type = 1)"
ID column is bigint type, and type is nchar. I tried use it with type = '1' too, but same problem.
Error is after "id = " section
Thanks for reply
EDIT:
SqlCeCommand com = new SqlCeCommand();
if (LocalType == '1') { com = new SqlCeCommand("SELECT req_id FROM Requisition WHERE id = (SELECT MAX(id) FROM Requisition WHERE type = 1)", con); }
else if (LocalType == '2') { com = new SqlCeCommand("SELECT req_id FROM Requisition WHERE id = (SELECT MAX(b.id) FROM Requisition AS b WHERE b.type <> 1)", con); }
using (com)
{
SqlCeDataReader reader = com.ExecuteReader();
}
The easiest way to do this is using top. If this is your real code, then you need to "escape" the word "table" because it is a reserved word:
select top 1 t.*
from [table] t
where type = '1'
order by id desc
Try naming the tables:
SELECT *
FROM Table AS a
WHERE id = (SELECT MAX(b.id) FROM Table AS b WHERE b.type = 1)
After a while on google, find that SQLCE in version 3.5 support SELECT TOP expression, but with difference with formating. It must be write with brackets
SELECT TOP(1) * FROM MyTable

Invalid Column Name though it's there!

I'm trying to print out Tables from the DB that have the EntityId column equals to DataclassId column here is the code
public void getRootTables_checkSP()
{
string connect = "Data Source= EUADEVS06\\SS2008;Initial Catalog=TacOps_4_0_0_4_test;integrated security=SSPI; persist security info=False;Trusted_Connection=Yes";
SqlDataReader rootTables_List = null;
SqlConnection conn = new SqlConnection(connect);
conn.Open();
SqlCommand s_cmd = new SqlCommand("SELECT * FROM sys.Tables WHERE EntityId = DataclassId", conn);
rootTables_List = s_cmd.ExecuteReader();
while (rootTables_List.Read())
{
string test = rootTables_List[0].ToString();
Console.WriteLine("ROOT TABLES ARE {0}", test);
}
rootTables_List.Close();
conn.Close();
}
but it keeps saying that these columns are invalid though when I printed out all the columns in the DB "syscolumns" they were there...
Can anyone tell me why I'm getting such an error?
EDIT
What I really want is to query the db TacOps_4_0_0_4_test not the system. I just realized that
EDIT 2
Here is an example of the Tables in my DB
Table_1
ID Sequence Type Heigh Weight EntityId DataclassId
0 1 s 1.4 2.5 42-2c-Qi 42-2c-Qi
1 2 s 2.4 2.5 zh-km-xd zh-km-xd
2 3 s 3.4 2.5 8n-tr-l7 8n-tr-l7
Table_2
ID Data Person EntityId DataclassId
0 1 Dave 58-zj-4o 41-2c-Q7
1 2 Sara 99-op-t6 oy-7j-mf
2 3 Silve 75-qy-47 2d-74-ds
Table_3
ID Name Genre EntityId DataclassId
0 LR Ac 78-jd-o9 78-jd-o9
1 OI Dr 4t-jb-qj 4t-jb-qj
2 DH Do 7j-3e-ol 7j-3e-ol
The output should be
Table_1
Table_3
EntityId and DataclassId are indeed no columns that exists in the sys.tables.
You're selecting data from sys.tables, there's no notion of syscolumns in your query, so i do not know why you're mentionning 'syscolumns' in your explanation ?
I think I may understand what you're trying now based on your comment to Frederik's answer
I tried "syscolumns" just to make sure
that the columns do exist. But when I
do the query where EntityId =
DataclassId it says "Invalid column
name
It sounds like EntityId and Dataclassid are columns in a table (or tables) that you have in your database and you want to find the rows from those tables that contain the same value in both those columns??
If that's the case, you are querying sys.Tables incorrectly - you'd need to query the specific tables directly i.e.
SELECT * FROM Table1 WHERE EntityId = DataClassId
Can you clarify?
Edit:
You can find all the tables that contain both those columns using this:
SELECT t.name
FROM sys.tables t
WHERE EXISTS(SELECT * FROM sys.columns c WHERE c.object_id = t.object_id AND c.name='EntityId')
AND EXISTS(SELECT * FROM sys.columns c WHERE c.object_id = t.object_id AND c.name='DataClassId')
From this, you could either iterate round each table and run the query to find rows that match on EntityId/DataClassId values - could insert into a temp table and return 1 resultset at the end.
OR, you could create a view to UNION all the tables together and then query that view (would need to update the view each time you added a new table).
OR, you could do some dynamic SQL generation based on the above to generate a SELECT statement on-the-fly to UNION all the tables together.
Update:
Here's a generic way to do it in pure TSQL - this way means if new tables are added it will automatically include them:
DECLARE #SQL VARCHAR(MAX)
SELECT #SQL = COALESCE(#SQL + CHAR(10) + 'UNION ALL' + CHAR(10), '')
+ 'SELECT ''' + REPLACE(QUOTENAME(t.Name), '''', '''''') + ''' AS TableName, COUNT(*) AS RowsMatched FROM ' + QUOTENAME(t.name)
+ ' WHERE EntityId = DataClassId'
FROM sys.tables t
WHERE EXISTS(SELECT * FROM sys.columns c WHERE c.object_id = t.object_id AND c.name='EntityId')
AND EXISTS(SELECT * FROM sys.columns c WHERE c.object_id = t.object_id AND c.name='DataClassId')
SET #SQL = 'SELECT x.TableName, x.RowsMatched FROM (' + #SQL + ') x WHERE x.RowsMatched > 0 ORDER BY x.TableName'
EXECUTE(#SQL)
If you don't need it to be dynamic, change the above EXECUTE to a PRINT to see the SQL it generates, and then create a view from it. You can then SELECT from that view.
Of course, you could either loop round each table 1 by 1 as you are trying.
Based on all the comments, i think what you might be trying to find is ALL tables in your database that have both EntityID and DataClassID columns.
I know...its a pretty WILD guess but dont blame me for trying!! :-)
If my shot in the pretty awesome darkness that is your question is correct, try this out:
SELECT tabs.name
FROM sys.tables tabs INNER JOIN sys.columns cols
ON tabs.object_id = cols.object_id
AND cols.name IN ('EntityId', 'DataClassId')
Well, if you do a sp_help 'sys.Tables' in SQL Management Studio you'll see that, indeed, those columns are not part of sys.Tables...

SELECT and update the row in DataTable

I have a following SQL statement and would like to update Database table if row found.
string sql = "Select A.CNum FROM TABLEA A, TABLEB B WHERE A.CID= B.CID AND A.CNum is NULL AND CID=#cID"
DataTable dt = querySql(sql, params);
if (dt.Rows.Count > 0)
{
// I would like to update CNum from TableA
}
What is the best method to update the row from SQL Statement?
Thank you..
It should be possible to do this in one statement without round-tripping any data from the database and back:
UPDATE
TABLEA
SET
CNum = newValueHere
FROM
TABLEA A,
INNER JOIN TABLEB B ON B.CID = A.CID
WHERE
A.CNum is NULL
AND A.CID=#cID
note I qualified the CID reference in the last line ( I think you'll get an error without this as it exists on both tables, and used an inner join to connect your tables. Note that newValueHere can be an expression of any of the columns in A or B.
Using the SQL from Tom's answer with the following C# will give you what you want.
using(SqlCommand cmd = new SqlCommand(sql, conection))
{
int rowsAffected = cmd.ExecuteNonQuery();
}
you can use EXISTS construct to do this in just one query.
string sql = "IF EXISTS(Select A.CNum FROM TABLEA A, TABLEB B WHERE A.CID= B.CID AND A.CNum is NULL AND CID=#cID) BEGIN ..... END"

C# datatable from sql join 2 columns same name

I have a sql select like so which returns a datatable:
select * from table1 a join table2 b on a.id=b.id
Both table1 and table2 have a column named itemno .
How do i reference the 2 separate columns? Normally I would use something like:
datarow["itemno"].ToString(); <=first column named itemno only
datarow["b.itemno"].ToString(); <= fail
However this only seems to get the first column named itemno.
Is there a way to reference the second column named itemno without changing my sql statement? (I know i can change my sql statement, take out the * and put in column aliases).
You can reference the columns by index instead:
datarow[0].ToString();
I'd much prefer aliasing them though to be honest.
Given an SQL query like this
select a.id, b.id, a.columnA,a.columnB,a.itemno,b.itemno
from table1 a
join table2 b on a.id=b.id
Your C# code would/could look like this to read all rows and all columns:
using (SqlCommand getAllColumns = new SqlCommand("select a.id, b.id,a.columnA,a.columnB,a.itemno,b.itemno from table1 a join table2 b on a.id=b.id", conn))
{
using (var drreader = getAllColumns.ExecuteReader())
{
DataTable tb = new DataTable();
tb.BeginLoadData();
tb.Load(drreader);
tb.EndLoadData();
foreach(DataRow row in tb.Rows.Cast<DataRow>().ToList())
{
// assuming these are all varchar columns
string idA = (string)row["id"];
string idB = (string)row["id1"];
string columnA = (string)row["columnA"];
string columnB = (string)row["columnB"];
string columnAItemNo = (string)row["itemno"]; //fetches the first itemno column, a.itemno in this case
string columnBItemNo = (string)row["itemno1"]; //fetches the second itemno column, b.itemno in this case
}
}
}
I use this on .NET Framework 4.5. If you want to verify or debug this, put a breakpoint on the foreach line and inspect the DataTable object. The second itemno column should be titled differently compared to the first one.

Categories