Iterate between column names with user-defined table - c#

I'm currently working with c# and sql server managment studio:
I have a table like:
+--------+-------------+-------------+------+
| TaskId | Item2(bool) | Item3(bool) | etc… |
+--------+-------------+-------------+------+
I don't know bool column names because there are created dinamically from code (user create them).
Well I insert data when form open, with TaskId and all bools with DEFAULT(0), si I have something like:
+--------------------------------------+-------------+-------------+------+
| TaskId | Item2(bool) | Item3(bool) | etc… |
+--------------------------------------+-------------+-------------+------+
| 9B093907-2072-4F59-A55C-0003CE89EF9E | 0 | 0 | 0 |
+--------------------------------------+-------------+-------------+------+
I want to update booleans, so in c# I create datatable to use User-defined tables:
DataTable checkboxList = new DataTable();
var checkboxNameColumn = checkboxList.Columns.Add("CheckBoxName", typeof(string));
var checkBoxValueColumns = checkboxList.Columns.Add("CheckBoxValue", typeof(bool));
foreach (var c in checkBoxes)
{
var row = checkboxList.NewRow();
row[checkboxNameColumn] = c.Name;
row[checkBoxValueColumns] = c.Checked;
checkboxList.Rows.Add(row);
}
Guid currentUser = new Guid(EBResources.EmpGuid);
Guid currentTaskId = new Guid(TaskId);
db.ExeSQLRedMarkItems($"usp_RedMarkItem_Insert", checkboxList, "#CheckBoxList", currentTaskId, currentUser);
CheckBoxName equals to my column Name and CheckBoxValue is value of that column. So I have my Datatable correct, now I send it to sql as:
User-defined table
CREATE TYPE [HELPER].CheckBoxValues AS TABLE(
CheckBoxName VARCHAR(255) NOT NULL,
CheckBoxValue BIT NOT NULL )
Stored Procedure:
CREATE OR ALTER PROCEDURE [dbo].[usp_RedMarkItem_Insert]
-- Add the parameters for the stored procedure here
#TaskId UNIQUEIDENTIFIER
, #CheckBoxList [Helper].[CheckBoxValues] READONLY
, #CurrentUser UNIQUEIDENTIFIER
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #CurrentCheckboxName VARCHAR(255) = (SELECT
[CheckBoxName]
FROM #CheckBoxList)
UPDATE [m]
SET
#CurrentCheckboxName = [c].[CheckBoxValue]
FROM [RedMarkItems] [m]
JOIN #CheckBoxList [c] ON [c].[CheckBoxName] = #CurrentCheckboxName
WHERE m.TaskId = #TaskId
END
So as you can see I declare #CurrentCheckboxName, but it throws me an error because that select return more than one value:
Subquery returned more than 1 value. This is not permitted when the
subquery follows =, !=, <, <= , >, >= or when the subquery is used as
an expression.
What I need to do to iterate between column names? Regards

you don't need the line below
DECLARE #CurrentCheckboxName VARCHAR(255) = (SELECT
[CheckBoxName]
FROM #CheckBoxList)
you already have this inside #CheckBoxList. You need something like below
UPDATE [m]
SET m.val = [c].Val
JOIN #CheckBoxList [c]
ON [c].[CheckBoxName] = m.CheckBoxName
WHERE m.TaskId = #TaskId
you can get column names from Information_schema
SELECT
TABLE_NAME,
COLUMN_NAME
FROM
INFORMATION_SCHEMA.COLUMNS

Your database design is going to give you a lot of problems. SQL Server works best with pre-defined tables with set numbers of named columns: while you can set up a system to have user-defined field labels, having actual user-defined fields is, in a sense, creating a database that runs on top of a database. You're going to need a lot of infrastructure, that you haven't created yet, to use and manage that.
My suggestion: if your need is for a single row with a collection of user-defined boolean values, store the boolean values as XML.
CREATE TABLE dbo.Task
(
TaskId int,
Checkboxes xml
)
Your XML would be defined something like this (though there are lots of ways you could define it!)
<Checkboxes>
<Checkbox name="User-defined name" value="True" />
<Checkbox name="Some other name" value="False" />
</Checkboxes>
You will still have some challenges -- for example, if you want to pull out all the rows that have a checkbox called "Apple" that had the value true -- but you'll have fewer than you would with an actual user-defined table.
And querying XML in a useful way is, in fact, possible.

Related

SQL Table Value Parameter only works when its data is copied to temporary table

I'm passing the data through c# to sql stored procedure using dapper.
c# (using dapper):
var dt = new DataTable("dbo.TVPtype");
dt.SetTypeName("dbo.TVPtype");
dt.Columns.Add("ID", typeof(int));
dt.Rows.Add(5);
_db.Execute("mysp", param: new { TVP = dt.AsTableValuedParameter("dbo.TVPtype"), commandType: CommandType.StoredProcedure);
The bellow SQL query will not work and I don't know Why! And it removes all data from mytable.
SQL stored procedure:
CREATE PROCEDURE [dbo].[mysp] #TVP dbo.TVPtype READONLY AS
DELETE FROM [dbo].[mytable] WHERE NOT EXISTS
(
SELECT NULL FROM #TVP na
WHERE ID = na.ID
)
To solve the problem I've used a temporary table in the stored procedure like bellow, and it works well.
My solution using temporary table:
CREATE table temptb (ID int)
insert into temptb select * from #TVP
Now I use temptb instead of #TVP in the delete query:
DELETE FROM [dbo].[mytable] WHERE NOT EXISTS
(
SELECT NULL FROM temptb na
WHERE ID = na.ID
)
It works well and delete specific data (not all data) !
So what's wrong with my first query?
I can't tell you, why it's working with temporary table, but the problem comes from the DELETE statement if you change it to:
DELETE t1
FROM [dbo].[mytable] AS t1 WHERE NOT EXISTS
(
SELECT NULL FROM #TVP na
WHERE t1.ID = na.ID
)
It will work. See, that when I add alias to mytable condition becomes clear which ID's it should compare. When in your example it might compare #TVP ID with it self(that's my guess).
I want to add two more points to your query:
It's better to select something that is not NULL when you check for existence, as NULL is special in SQL(although it is working in your example). For example SELECT 1 FROM #TVP... would be more readable
Why not to do it like this:
Snippet:
DELETE FROM [dbo].[mytable] AS t1 WHERE ID NOT IN
(
SELECT na.ID FROM #TVP na
)
Qualify your column names! You think you are writing:
DELETE FROM [dbo].[mytable]
WHERE NOT EXISTS (SELECT 1
FROM #TVP na
WHERE mytable.ID = na.ID
);
But you are not. The scoping rules of SQL interpret your query as:
DELETE FROM [dbo].[mytable]
WHERE NOT EXISTS (SELECT 1
FROM #TVP na
WHERE na.ID = na.ID
);
This is rather non-sensical. The WHERE is not correlated to the outer query. It will delete either all rows (if #TVP is empty or the ID is always NULL) or no rows (any other situation).
If you qualify all column references, you will never have this problem.

SQL update query in C# foreach loop

I want to run an UPDATE SQL query within a C# foreach loop, such as:
var alterQuery = $#"
IF NOT EXISTS(SELECT 1 FROM sys.columns WHERE Name = 'MyColumn' AND Object_ID = Object_ID('MyTable'))
ALTER TABLE MyTable
ADD [MyColumn] nvarchar(255)
";
using (var connection = myConnection)
{
connection.Execute(alterQuery);
foreach (var obj in myObjects)
{
var query = $#"UPDATE [MyTable]
SET [MyColumn] = '{obj.Val}'
WHERE [ID] = '{obj.ID}'
";
// note: my Execute method uses ExecuteNonQuery() behind the scenes
connection.Execute(query);
}
}
But, I receive the following SQL Exception. I receive it when there are two or more values in myObjects but not when there is only one:
Additional information: Column names in each table must be unique. Column name MyColumn in table MyTable is specified more than once.
I believe it may be due to the queries running simultaneously and trying to access the same column (MyColumn). Should I be running my queries in such a way that each must wait until the previous completes?
How can I successfully run these queries?
I solved this issue by adding BEGIN and END around my ALTER TABLE query.
Does not work:
IF NOT EXISTS(SELECT 1 FROM sys.columns WHERE Name = 'MyColumn' AND
Object_ID = Object_ID('MyTable'))
ALTER TABLE MyTable
ADD [MyColumn] nvarchar(255)
Works:
IF NOT EXISTS(SELECT 1 FROM sys.columns WHERE Name = 'MyColumn' AND
Object_ID = Object_ID('MyTable'))
BEGIN
ALTER TABLE MyTable
ADD [MyColumn] nvarchar(255)
END
I find it weird that in the non-working case, the only line that the IF NOT EXISTScheck controls is the ALTER TABLE MyTable line, rather than both the ALTER TABLE MyTable and the ADD [MyColumn] nvarchar(255) lines, which should be interpreted as one statement.

stored procedure calling from c# and iteration issue in Merge

I have over a million records in the list. I pass all records at once from table to stored procedure .In stored procedure i have to have iteration to go thorugh all the rows in the table and for each row it takes table row modified date based on jobid and checks if it exist in database and based on it either it updates or insert the record. I feel that my procedure is not correct, would be glad if someone help on this.
foreach (No_kemi no_list in newforSQL)
{
DataTable _dt = new DataTable("table");
_dt.Columns.Add("JobID", typeof(string));
_dt.Columns.Add("CreatedDate", typeof(datetime));
_dt.Columns.Add("ModifiedDate", typeof(datetime));
_dt.Columns.Add("DbDate", typeof(datetime));
_dt.Columns.Add("SubGUID", typeof(string));
_dt.Columns.Add("eType", typeof(string));
// adding over a million records in the table
_dt.Rows.Add(no_list.ID,no_list.CreatedDate,no_list.ModifiedDate,no_list.DbDate,no_list.SubGUID,no_list.eType);
}
using (SqlCommand sqlCommand = new SqlCommand())
{
sqlCommand.CommandType = CommandType.StoredProcedure;
sqlCommand.CommandText = "Process_NO_table";
sqlCommand.Connection = connection;
SqlParameter typeParam = sqlCmd.Parameters.AddWithValue("#track", _dt);
typeParam .SqlDbType = SqlDbType.Structured;
sqlCmd.ExecuteNonQuery();
}
my tabletype and procedure:
CREATE TYPE TrackType AS TABLE
(
t_Id uniqueidentifier, t_JobID nvarchar(50), t_CreatedDate datetime2(7), t_ModifiedDate datetime2(7), t_DbDate datetime2(7)
t_SubGUID nvarchar(MAX), t_eType nvarchar(MAX)
);
GO
ALTER/CREATE PROCEDURE [dbo].[Process_NO_table] // i will change to alter after i create it
#track TrackType READONLY
AS
// i need to iterate all the rows of the table(over a million)
Declare #rows INT
Declare #i int = 0
Declare #count int = (SELECT COUNT(*) FROM #track)
DECLARE #is INT
WHILE (#i < #count)
BEGIN
-- first i check modified date from the database table
SELECT #is = COUNT(*) FROM NO_table WHERE [JobID] IN (SELECT [t_JobID] FROM #track)
MERGE [dbo].[NO_table] AS [Target]
USING #track AS [Source]
-- if the database modifed date is less than the modifeid date from the proceduretable(#track) then it updates the records
ON [Target].[ModifiedDate] < [Source].[t_ModifiedDate] AND JobID = t_JobID
WHEN MATCHED THEN
UPDATE SET [JobID] = [Source].[t_JobID],
[CreatedDate] = [Source].[t_CreatedDate]
[DbDate]= [Source].[t_DbDate]
[ModifiedDate] = [Source].[t_ModifiedDate]
[SubGUID] = [Source].[t_SubGUID]
[eType] = [Source].[t_eType]
-- if the database modifed dateis not existing then it insert the record
MERGE [dbo].[NO_table] AS [Target]
USING #track AS [Source]
ON (#is != 0)
WHEN NOT MATCHED THEN
INSERT INTO [NO_table] ( [JobID], [CreatedDate], [ModifiedDate], [DbDate], [SubGUID], [eType] )
VALUES ( [Source].[t_JobID], [Source].[t_CreatedDate], [Source].[t_ModifiedDate], [Source].[t_DbDate], [Source].[t_SubGUID], [Source].[t_eType] );
SET #i = #i + 1
END
GO
I think you have a large number of syntax errors in your SQL (assuming MS SQL), but your merge condition is probably giving you the invalid syntax near WHERE, because you need to use AND, not WHERE.
ON [Target].[ModifiedDate] < [Source].[t_ModifiedDate] WHERE JobID = t_JobID
should be
ON [Target].[ModifiedDate] < [Source].[t_ModifiedDate] AND JobID = t_JobID
The Select Top 1 and the WHEN MATCHED THEN after the null check for #dbmoddate need to go away as well, as those are also causing syntax issues.
The insert after the null check for #dbmoddate needs a table specified so it actually knows what to insert into.
You also need to end your merge statement with a semicolon.
UPDATED ANSWER:
Now that you have this more cleaned up, I can better see what you're trying to do. At a high level, you want to simply update existing records where the modified date is less than the modified date of on your custom type. If there does not exist a record in your table that does exist in your custom type, then insert it.
With that said, you don't actually need to loop because you aren't doing anything with your loop. What you currently have and what I'm posting below this is all set-based results, not iterative.
You can make this much simpler by getting rid of the merge statements and doing a simple Update and Insert like I have below. The merge would make more sense if your condition between the two statements was the same (i.e. if you didn't have the check for modified date, then merge would be OK) because then you can use the keywords WHEN MATCHED and WHEN NOT MATCHED and have it in one single merge statement. I personally stay away from MERGE statements because they tend to be a little buggy and there are a number of things you have to watch out for.
I think this solution will be better in the long run as it is easier to read and more maintainable...
CREATE TYPE TrackType AS TABLE
(
t_Id uniqueidentifier, t_JobID nvarchar(50), t_CreatedDate datetime2(7), t_ModifiedDate datetime2(7), t_DbDate datetime2(7)
,t_SubGUID nvarchar(MAX), t_eType nvarchar(MAX)
);
GO
CREATE PROCEDURE [dbo].[Process_NO_table] -- i will change to alter after i create it
#track TrackType READONLY
AS
-- i need to iterate all the rows of the table(over a million)
Update [NO_table]
SET [JobID] = T.[t_JobID],
[CreatedDate] = T.[t_CreatedDate],
[DbDate]= T.[t_DbDate],
[ModifiedDate] = T.[t_ModifiedDate],
[SubGUID] = T.[t_SubGUID] ,
[eType] = T.[t_eType]
From #track T
Where [NO_table].[JobID] = T.[t_JobID]
And [NO_table].[ModifiedDate] < T.[t_ModifiedDate]
Insert [NO_Table]
(
[JobID],
[CreatedDate],
[ModifiedDate],
[DbDate],
[SubGUID],
[eType]
)
Select T.[t_JobID],
T.[t_CreatedDate],
T.[t_ModifiedDate],
T.[t_DbDate],
T.[t_SubGUID],
T.[t_eType]
From #track T
Where Not Exists (Select 1 From [NO_table] where T.[t_JobID] = [NO_table].[JobID])
GO

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

Merge tables(add, update and delete records from different sources)

I have three tables tb1,tb2 and tbTotal. They have the same schemas. The tables have three columns, MetricID, Descr and EntryDE.
What I want is to merge tb1 with tbTotal. I have done this and it works fines.
My stored procedure is:
CREATE PROCEDURE [dbo].[Admin_Fill]
-- Add the parameters for the stored procedure here
#MetricId INT,
#Descr VARCHAR(100),
#EntryDE VARCHAR(20)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
--SET IDENTITY_INSERT dbo.tbTotal ON
-- Insert statements for procedure here
;WITH cte AS (SELECT MetricId=#MetricId,Descr=#Descr,EntryDE=#EntryDE)
MERGE tbTotal d
USING cte s
ON s.EntryDE = d.EntryDE
AND s.MetricId=d.MetricId
WHEN matched THEN UPDATE
set MetricId=s.MetricId,
Descr=s.Descr,
EntryDE=s.EntryDE
WHEN not matched BY TARGET THEN
INSERT(MetricId,Descr,EntryDE)
VALUES (s.MetricId,s.Descr,s.EntryDE);
END
My C# code:
foreach (DataRow row in dt.Rows) // pass datatable dt1
{
MetricId = Convert.ToInt32(row["MetricId"]);
Descr = row["Descr"].ToString();
EntryDE = row["EntryDE"].ToString();
parameters.Add("#MetricId", MetricId);
parameters.Add("#Descr", Descr);
parameters.Add("#EntryDE", EntryDE);
dbaccess.ExecuteNonQuery(strStoredProcedure, parameters); //cmd.ExecuteNonQuery();
parameters.Clear();
}
Also I want to remove all records in dt2 from dtTotal. I am not sure how to modify the stored procedure.
Thanks for help.
If i have understood what you are trying to do correctly, then this is potentially how I would prefer to implement the solution.
I would pass the 2 datatables as TABLE variables to an SP - similar to below and then use JOINs to both UPDATE and DELETE as required using SET operations - thus affecting multiple rows in one query and avoid looping through each row separately.
As mentioned by AdaTheDev in the related answer, you will end up creating a "TABLE" type but there is no drawback to having one extra type and this solution will scale a lot better than a looping approach would.
DISCLAIMER :- Code below may not be syntactically correct but i hope you get the picture of what I am proposing.
CREATE TYPE TableType AS TABLE
(
MetricId INT,
Descr VARCHAR(300) --or whatever length is appropriate,
EntryDE INT
);
GO
CREATE PROCEDURE [dbo].[Admin_Fill]
#RowsForUpdate TableType READONLY,
#RowsForDelete TableType READONLY
AS
BEGIN
-- Update all the Descriptions for all the rows
UPDATE
t
SET
t.Descr = u.Descr
FROM
tbTotal t
INNER JOIN #RowsForUpdate u
ON t.EntryDE = u.EntryDE AND t.MetricId = u.MetricId
-- Delete the rows to be deleted
DELETE t
FROM tbTotal t
INNER JOIN #RowsForDelete d
ON t.EntryDE = u.EntryDE AND t.MetricId = u.MetricId
END

Categories

Resources