SQL按顺序一次更新一个项目

时间:2014-10-22 14:31:09

标签: c# sql sql-server ado.net

我有这个c#代码执行SQL Update,可以一次执行多个更新。现在我正在更新的表有一个名为SortOrder的列,所以当我进行这些多次更新时,我想按sortOrder列的顺序进行更新......这是否可能?

这是我的代码:

public void PostScheduledTasks(List<CellModel> cells)
        {
conn = new SqlConnection(connectionString);
                cmd = new SqlCommand(
                    @"UPDATE ScheduleTasks_Copy  
                      SET 
                          ActualStart=@actualStart,
                          ActualFinish=@actualFinish,
                          ActualEndDate=@actualEndDate,
                          UserDate1=@userDateOne,
                          IsCompleted=@isCompleted
                      WHERE ScheduleTaskID = @scheduleTaskID");
                cmd.Parameters.Add("@isCompleted", System.Data.SqlDbType.Bit);
                cmd.Parameters.Add("@userDateOne", System.Data.SqlDbType.DateTime);
                cmd.Parameters.Add("@actualStart", System.Data.SqlDbType.DateTime);
                cmd.Parameters.Add("@actualFinish", System.Data.SqlDbType.DateTime);
                cmd.Parameters.Add("@actualEndDate", System.Data.SqlDbType.DateTime);
                cmd.Parameters.Add("@scheduleTaskID", System.Data.SqlDbType.Int);


                cmd.Connection = conn;

                conn.Open();
                for (int i = 0; i < cells.Count; i++)
                {
                    cmd.Parameters["@isCompleted"].Value = cmd.Parameters["@percentComplete"].Value = (cells[i].selected == true) ? 1 : 0;
                    cmd.Parameters["@userDateOne"].Value = !string.IsNullOrEmpty(cells[i].scheduledDate) ? cells[i].scheduledDate : (object)DBNull.Value;
                    cmd.Parameters["@actualStart"].Value = !string.IsNullOrEmpty(cells[i].actualDate) ? cells[i].actualDate : (object)DBNull.Value;
                    cmd.Parameters["@actualFinish"].Value = !string.IsNullOrEmpty(cells[i].finishedDate) ? cells[i].finishedDate : (object)DBNull.Value;
                    cmd.Parameters["@actualEndDate"].Value = !string.IsNullOrEmpty(cells[i].finishedDate) ? cells[i].finishedDate : (object)DBNull.Value;
                    cmd.Parameters["@scheduleTaskID"].Value = cells[i].scheduleTaskID;
                    cmd.ExecuteNonQuery();
                }

                conn.Close();
}

7 个答案:

答案 0 :(得分:3)

如果“SortOrder”可以从源对象“cells”中确定,就像其他属性一样:

然后最有效的方法是在迭代之前通过SortOrder对“单元格”进行排序。这样做的确切方法超出了问题的范围,因为你没有告诉我们究竟是什么“单元格”(一个列表?一个数组?一个自定义对象?一组?)

如果只能通过查询数据库确定SortOrder:

然后,毫不奇怪,您需要查询数据库:

SELECT ScheduleTaskID, SortOrder FROM ScheduleTasks_Copy ORDER BY SortOrder

您遍历该行集,每次抓取ScheduleTaskID。对于每个ScheduleTaskID,迭代“cells”直到找到匹配的任务(cells[i].scheduleTaskID == TaskID),然后使用源表中的匹配任务进行数据库更新。

这是非常粗略的代码,我暂时没有编写C#:

using (connection)
{
    SqlCommand command = new SqlCommand("SELECT ScheduleTaskID, SortOrder FROM ScheduleTasks_Copy ORDER BY SortOrder;", connection);
    connection.Open();
    SqlDataReader reader = command.ExecuteReader();

    if (reader.HasRows)
    {
        while (reader.Read())
        {
            int taskid = reader.GetInt32(0);
            for (int i = 0; i < cells.Count; i++)
            {
             if (cells[i].scheduleTaskID == taskid) {
                 cmd.Parameters["@isCompleted"].Value = cmd.Parameters["@percentComplete"].Value = (cells[i].selected == true) ? 1 : 0;
                 cmd.Parameters["@userDateOne"].Value = !string.IsNullOrEmpty(cells[i].scheduledDate) ? cells[i].scheduledDate : (object)DBNull.Value;
                 cmd.Parameters["@actualStart"].Value = !string.IsNullOrEmpty(cells[i].actualDate) ? cells[i].actualDate : (object)DBNull.Value;
                 cmd.Parameters["@actualFinish"].Value = !string.IsNullOrEmpty(cells[i].finishedDate) ? cells[i].finishedDate : (object)DBNull.Value;
                 cmd.Parameters["@actualEndDate"].Value = !string.IsNullOrEmpty(cells[i].finishedDate) ? cells[i].finishedDate : (object)DBNull.Value;
                 cmd.Parameters["@scheduleTaskID"].Value = cells[i].scheduleTaskID;
                 cmd.ExecuteNonQuery();
                 }
            }
        }
    }
    reader.Close();
 }

答案 1 :(得分:2)

退一步并忽略关于同时运行更新的实际问题,让我们假设您的问题不是“我可以同时运行一堆单个更新吗?”而是“如何尽可能快速/有效地更新List<CellModel> cells中的每个项目?”。

显然,当cells.Count变大时,顺序运行单个更新不起作用,这可能是由于执行更新的成本与在网络上执行多次往返所产生的开销相比相形见绌。因此,同时运行一堆单个更新可能也不是最有效的解决方案。

相反,让我们一次移动整个cells列表。这意味着服务器拥有处理我们请求所需的一切。如果cells.Count很大,则SqlBulkCopy(由Will建议)将很难被击败。或者,Table-Valued Parameter可以适用于较小的集合。

一旦数据可供服务器使用,您需要做的就是针对临时表/变量执行单个更新加入,并且您已完成(MSDN有一些很好的此技术示例。)但请注意,如果cells包含重复的scheduleTaskID,您可能还需要执行一些额外的预处理(确保最高的sortOrder获胜等)。

示例:

create table #temp(scheduleTaskID int, isCompleted bit, userDateOne datetime, actualStart datetime, actualFinish datetime, actualEndDate datetime)

--populate #temp via SqlBulkCopy, etc

update st
set st.ActualStart = t.actualStart
  , st.ActualFinish = t.actualFinish
  , st.ActualEndDate = t.actualEndDate
  , st.UserDate1 = t.userDateOne
  , st.IsCompleted = t.isCompleted
from dbo.ScheduleTasks_Copy st
     inner join #temp t on st.ScheduleTaskID = t.scheduleTaskID

drop table #temp

更新的连接语法并不像它应该的那样众所周知 - 一旦你习惯了轻微的尴尬,它就会变得非常有用......

答案 2 :(得分:1)

您最初必须获取SortOrder列的值,然后在WHERE子句中使用SortOrder = X进行迭代更新:

var dt = new DataTable();

从查询中填充dt,例如:SELECT SortOrder FROM ScheduleTasks_Copy

var conn = new SqlConnection(connectionString);

foreach (DataRow dr in dt.Rows)
{
    cmd = new SqlCommand(
        @"UPDATE ScheduleTasks_Copy  
        SET 
        ActualStart=@actualStart,
        ActualFinish=@actualFinish,
        ActualEndDate=@actualEndDate,
        UserDate1=@userDateOne,
        IsCompleted=@isCompleted
        WHERE ScheduleTaskID = @scheduleTaskID
        AND SortOrder = @sortOrder");

    cmd.Parameters.Add("@isCompleted", System.Data.SqlDbType.Bit);
    cmd.Parameters.Add("@userDateOne", System.Data.SqlDbType.DateTime);
    cmd.Parameters.Add("@actualStart", System.Data.SqlDbType.DateTime);
    cmd.Parameters.Add("@actualFinish", System.Data.SqlDbType.DateTime);
    cmd.Parameters.Add("@actualEndDate", System.Data.SqlDbType.DateTime);
    cmd.Parameters.Add("@scheduleTaskID", System.Data.SqlDbType.Int);
    cmd.Parameters.Add("@sortOrder", dr["SortOrder"].ToString());
}

(我没有测试过这段代码的语法,但是它涵盖了一般的想法)

虽然我猜这取决于你想要这样做的原因,但在我看来,这需要的原因有限。

答案 3 :(得分:1)

CellModel是否具有sortOrder属性?因为它应该。在这种情况下,您可以添加以下行:

cells = cells.OrderBy(o => o.sortOrder); // add this -> order your cells first
for (int i = 0; i < cells.Count; i++) { 
...
}

如果您没有sortOrder属性,您应该找到另一种方法来对此集合进行排序。

另一个解决方案是使用以下内容获取ScheduleTaskIDsortOrder排序的 select ScheduleTaskID from ScheduleTasks_Copy order by SortOrder

cells

然后你应该浏览所有这些项目,如果你发现ScheduleTaskID中的项目具有相同的{{1}},那么你应该进行更新。

答案 4 :(得分:0)

我相信您可以做的是将结果上传到临时表中,然后使用游标服务器端滚动并按指定顺序更新所有行。如果您能够使用sql用户定义的类型和存储过程,这可能会更容易。

var conn = new SqlConnection(connectionString);

conn.Open();
SqlCommand cmd = new SqlCommand("create table ##Tasks(id int, isCompleted bit, userDateOne datetime, actualStart datetime, actualFinish datetime, actualEndDate datetime)", conn);
cmd.ExecuteNonQuery();

DataTable localTempTable = new DataTable("Tasks");
localTempTable.Columns.Add("id", typeof(int));
localTempTable.Columns.Add("isCompleted", typeof(bool));
localTempTable.Columns.Add("userDateOne", typeof(DateTime));
localTempTable.Columns.Add("actualStart", typeof(DateTime));
localTempTable.Columns.Add("actualFinish", typeof(DateTime));
localTempTable.Columns.Add("actualEndDate", typeof(DateTime));

for (int i = 0; i < cells.Count; i++)
{
    var row = localTempTable.NewRow();
    row["id"] = cells[i].scheduleTaskID;
    row["isCompleted"] = (cells[i].selected == true);
    row["userDateOne"] = !string.IsNullOrEmpty(cells[i].scheduledDate) ? cells[i].scheduledDate : (object)DBNull.Value;
    row["actualStart"] = !string.IsNullOrEmpty(cells[i].actualDate) ? cells[i].actualDate : (object)DBNull.Value;
    row["actualFinish"] = !string.IsNullOrEmpty(cells[i].finishedDate) ? cells[i].finishedDate : (object)DBNull.Value;
    row["actualEndDate"] = !string.IsNullOrEmpty(cells[i].finishedDate) ? cells[i].finishedDate : (object)DBNull.Value;
}

localTempTable.AcceptChanges();

using (SqlBulkCopy bulkCopy = new SqlBulkCopy(connection))
{
    bulkCopy.DestinationTableName = "##Tasks";
    bulkCopy.WriteToServer(localTempTable);
}

SqlCommand cmd = new SqlCommand(@"
    declare orderedUpdate cursor 
    for 
    select tempTasks.* from ##tasks tempTasks
    inner join ScheduleTasks tasks on tempTasks.id =  tasks.ScheduleTaskID
    order by tasks.sortOrder;

declare @id int, @isCompleted bit, @actualStart datetime, @actualFinish datetime, @actualEndDate datetime, @userDateOne datetime;

open orderedUpdate;
fetch next from orderedUpdate INTO @id, @isCompleted, @userDateOne, @actualStart, @actualFinish, @actualEndDate 
while @@fetch_status = 0
begin 
    UPDATE ScheduleTasks_Copy  
        SET 
            ActualStart=@actualStart,
            ActualFinish=@actualFinish,
            ActualEndDate=@actualEndDate,
            UserDate1=@userDateOne,
            IsCompleted=@isCompleted
        WHERE ScheduleTaskID = @id;
end
close orderedUpdate
deallocate orderedUpdate", conn);

cmd.ExecuteNonQuery();

答案 5 :(得分:0)

虽然仍然不清楚为什么需要以任何特定的顺序更新记录集(是否有触发器会导致一些下游效应,这将受益于处理按照sortOrder字段的顺序?),应该指出在单个UPDATE语句中执行所有更新会使排序无关紧要。已经有一些人已经指出采用基于集合的方法会更快(当然也会如此),但是在单个UPDATE中完成意味着事务上没有顺序(在实际级别上)。

已经提到过这个基于集合的SqlBulkCopy,但我更喜欢使用表值参数(TVP),因为它们允许将Cells集合直接流式传输到存储过程将进行更新(嗯,技术上通过[tempdb],但足够接近)。相反,使用SqlBulkCopy意味着需要以“DataTable”的形式复制现有的Cells集合。

以下是用于将现有流程转换为使用TVP的T-SQL和C#代码。应该注意的是,如果仍然有某些原因需要按特定顺序执行此操作,则唯一需要更改的是如何处理存储过程中的表变量。

T-SQL代码

-- First: You need a User-Defined Table Type
CREATE TYPE dbo.ScheduleTasksImport AS TABLE
(
   ScheduleTaskID INT NOT NULL, -- optionally mark this field as PRIMARY KEY
   IsCompleted BIT NOT NULL,
   ActualStart DATETIME NULL,
   ActualFinish DATETIME NULL,
   ActualEndDate DATETIME NULL,
   UserDate1 DATETIME NULL
);
GO

GRANT EXECUTE ON TYPE::[dbo].[ScheduleTasksImport] TO [user_or_role];
GO

-- Second: Use the UDTT as an input param to an import proc.
--         Hence "Tabled-Valued Parameter" (TVP)
CREATE PROCEDURE dbo.ImportData (
   @ImportTable    dbo.ScheduleTasksImport READONLY
)
AS
SET NOCOUNT ON;

UPDATE stc
SET    stc.ActualStart = imp.ActualStart,
       stc.ActualFinish = imp.ActualFinish,
       stc.ActualEndDate = imp.ActualEndDate,
       stc.UserDate1 = imp.UserDate1,
       stc.IsCompleted = imp.IsCompleted
FROM ScheduleTasks_Copy stc
INNER JOIN @ImportTable imp
        ON imp.ScheduleTaskID = stc.ScheduleTaskID
GO

GRANT EXECUTE ON dbo.ImportData TO [user_or_role];


C#代码

第1部分:定义将采集集合并将其作为IEnumerable<SqlDataRecord>

返回的方法
using System.Collections;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using Microsoft.SqlServer.Server;

private static IEnumerable<SqlDataRecord> SendRows(List<CellModel> Cells)
{
   SqlMetaData[] _TvpSchema = new SqlMetaData[] {
      new SqlMetaData("ScheduleTaskID", SqlDbType.Int),
      new SqlMetaData("IsCompleted", SqlDbType.Bit),
      new SqlMetaData("ActualStart", SqlDbType.DateTime),
      new SqlMetaData("ActualFinish", SqlDbType.DateTime),
      new SqlMetaData("ActualEndDate", SqlDbType.DateTime),
      new SqlMetaData("UserDate1", SqlDbType.DateTime)
   };

   SqlDataRecord _DataRecord = new SqlDataRecord(_TvpSchema);

   // read a row, send a row
   for (int _Index = 0; _Index < Cells.Count; _Index++)
   {
      // Unlike BCP and BULK INSERT, you have the option here to create an
      // object, do manipulation(s) / validation(s) on the object, then pass
      // the object to the DB or discard via "continue" if invalid.
      _DataRecord.SetInt32(0, Cells[_Index].scheduleTaskID);
      _DataRecord.SetBoolean(1, Cells[_Index].selected); // IsCompleted
      _DataRecord.SetDatetime(2, Cells[_Index].actualDate); // ActualStart
      _DataRecord.SetDatetime(3, Cells[_Index].finishedDate); // ActualFinish
      _DataRecord.SetDatetime(4, Cells[_Index].finishedDate); // ActualEndDate
      _DataRecord.SetDatetime(5, Cells[_Index].scheduledDate); // UserDate1

      yield return _DataRecord;
   }
}

第2部分:将当前PostScheduledTasks方法替换为仅执行ImportData存储过程的以下方法。执行存储过程时,它将询问@ImportTable输入参数的值,该值将启动流式传输记录的过程。

public static void PostScheduledTasks(List<CellModel> Cells)
{
   SqlConnection _Connection = new SqlConnection(connectionString);
   SqlCommand _Command = new SqlCommand("ImportData", _Connection);
   _Command.CommandType = CommandType.StoredProcedure;

   SqlParameter _TVParam = new SqlParameter();
   _TVParam.ParameterName = "@ImportTable";
   _TVParam.SqlDbType = SqlDbType.Structured;
   _TVParam.Value = SendRows(Cells); // method return value is streamed data
   _Command.Parameters.Add(_TVParam);

   try
   {
      _Connection.Open();

      // Send the data and process the UPDATE
      _Command.ExecuteNonQuery();
   }
   finally
   {
      _Connection.Close();
   }

   return;
}

请注意,这两个C#列表确实存放在同一个文件中,但为了便于阅读,这些文件在此处分开。如果将它们放入单独的.cs文件中,则需要将using语句复制到第二个列表中。

答案 6 :(得分:0)

忽略了为什么要这样做的明显问题,你可以试试这个黑客: 将数据插入临时表(使用SqlBulkCopy),然后在源中使用带有ORDER BY子句的MERGE。在这个例子中,我使用ScheduleTasks_Copy作为“临时”表,并使用ScheduleTasks作为目的地。

MERGE ScheduleTasks AS T
USING (SELECT TOP 99999999 C.* 
       FROM ScheduleTasks_Copy C
       JOIN ScheduleTasks S
         ON C.ID = S.ID
       ORDER BY S.SortOrder ASC) AS S
ON S.ID = T.ID
WHEN MATCHED THEN 
  UPDATE SET T.ActualStart = S.ActualStart,
      T.ActualFinish = S.ActualFinish,
      T.ActualEndDate = S.ActualEndDate,
      T.UserDate1 = S.UserDate1,
      T.IsCompleted = S.IsCompleted,
      T.UpdatedOn =  dbo.GetDateValue();

不要担心愚蠢的GetDateValue函数和UpdatedOn列。我只是将它们包含在内,以便您可以看到更新的执行顺序。如果要以相反的顺序更新记录,只需将排序顺序更改为DESC。

http://sqlfiddle.com/#!6/dbc5f/16

我个人建议避免这样的事情并重新设计你的解决方案,以便完全避免这个问题。