【问题标题】:Bulk Update in C#C# 中的批量更新
【发布时间】:2014-01-05 07:29:27
【问题描述】:

为了在数据库中插入大量数据,我曾经将所有插入信息收集到一个列表中,并将此列表转换为DataTable。然后我通过SqlBulkCopy将该列表插入数据库。

我将生成的列表发送到哪里
LiMyList
,其中包含我要插入数据库的所有批量数据的信息
并将其传递给我的批量插入操作

InsertData(LiMyList, "MyTable");

InsertData 在哪里

 public static void InsertData<T>(List<T> list,string TableName)
        {
                DataTable dt = new DataTable("MyTable");
                clsBulkOperation blk = new clsBulkOperation();
                dt = ConvertToDataTable(list);
                ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal);
                using (SqlBulkCopy bulkcopy = new SqlBulkCopy(ConfigurationManager.ConnectionStrings["SchoolSoulDataEntitiesForReport"].ConnectionString))
                {
                    bulkcopy.BulkCopyTimeout = 660;
                    bulkcopy.DestinationTableName = TableName;
                    bulkcopy.WriteToServer(dt);
                }
        }    

public static DataTable ConvertToDataTable<T>(IList<T> data)
        {
            PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(T));
            DataTable table = new DataTable();
            foreach (PropertyDescriptor prop in properties)
                table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
            foreach (T item in data)
            {
                DataRow row = table.NewRow();
                foreach (PropertyDescriptor prop in properties)
                    row[prop.Name] = prop.GetValue(item) ?? DBNull.Value;
                table.Rows.Add(row);
            }
            return table;
        }

现在我想做一个更新操作,有什么方法可以通过SqlBulkCopy 将数据从 C#.Net 更新到数据库

【问题讨论】:

标签: c# .net sql-server bulk


【解决方案1】:

完整答案,免责声明:箭头代码;这是我的研究成果;发表于 SqlRapper。它使用自定义属性而不是属性来确定键是否是主键。是的,超级复杂。是的,超级可重复使用。是的,需要重构。是的,它是一个 nuget 包。不,github 上的文档不是很好,但它存在。它适用于一切吗?可能不是。它适用于简单的东西吗?哦对了。

设置后使用起来有多容易?

public class Log
{
    [PrimaryKey]
    public int? LogId { get; set; }
    public int ApplicationId { get; set; }
    [DefaultKey]
    public DateTime? Date { get; set; }
    public string Message { get; set; }
}


var logs = new List<Log>() { log1, log2 };
success = db.BulkUpdateData(logs);

它是这样工作的:

public class PrimaryKeyAttribute : Attribute
{
}

    private static bool IsPrimaryKey(object[] attributes)
    {
        bool skip = false;
        foreach (var attr in attributes)
        {
            if (attr.GetType() == typeof(PrimaryKeyAttribute))
            {
                skip = true;
            }
        }

        return skip;
    }

    private string GetSqlDataType(Type type, bool isPrimary = false)
    {
        var sqlType = new StringBuilder();
        var isNullable = false;
        if (Nullable.GetUnderlyingType(type) != null)
        {
            isNullable = true;
            type = Nullable.GetUnderlyingType(type);
        }
        switch (Type.GetTypeCode(type))
        {
            case TypeCode.String:
                isNullable = true;
                sqlType.Append("nvarchar(MAX)");
                break;
            case TypeCode.Int32:
            case TypeCode.Int64:
            case TypeCode.Int16:
                sqlType.Append("int");
                break;
            case TypeCode.Boolean:
                sqlType.Append("bit");
                break;
            case TypeCode.DateTime:
                sqlType.Append("datetime");
                break;
            case TypeCode.Decimal:
            case TypeCode.Double:
                sqlType.Append("decimal");
                break;
        }
        if (!isNullable || isPrimary)
        {
            sqlType.Append(" NOT NULL");
        }
        return sqlType.ToString();
    }

    /// <summary>
    /// SqlBulkCopy is allegedly protected from Sql Injection.
    /// Updates a list of simple sql objects that mock tables.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="rows">A list of rows to insert</param>
    /// <param name="tableName">a Table name if your class isn't your table name minus s.</param>
    /// <returns>bool success</returns>
    public bool BulkUpdateData<T>(List<T> rows, string tableName = null)
    {
        var template = rows.FirstOrDefault();
        string tn = tableName ?? template.GetType().Name + "s";
        int updated = 0;
        using (SqlConnection con = new SqlConnection(ConnectionString))
        {
            using (SqlCommand command = new SqlCommand("", con))
            {
                using (SqlBulkCopy sbc = new SqlBulkCopy(con))
                {
                    var dt = new DataTable();
                    var columns = template.GetType().GetProperties();;
                    var colNames = new List<string>();
                    string keyName = "";
                    var setStatement = new StringBuilder();
                    int rowNum = 0;
                    foreach (var row in rows)
                    {
                        dt.Rows.Add();
                        int colNum = 0;
                        foreach (var col in columns)
                        {
                            var attributes = row.GetType().GetProperty(col.Name).GetCustomAttributes(false);
                            bool isPrimary = IsPrimaryKey(attributes);
                            var value = row.GetType().GetProperty(col.Name).GetValue(row);

                            if (rowNum == 0)
                            {
                                colNames.Add($"{col.Name} {GetSqlDataType(col.PropertyType, isPrimary)}");
                                dt.Columns.Add(new DataColumn(col.Name, Nullable.GetUnderlyingType(col.PropertyType) ?? col.PropertyType));
                                if (!isPrimary)
                                {
                                    setStatement.Append($" ME.{col.Name} = T.{col.Name},");
                                }

                            }
                            if (isPrimary)
                            {
                                keyName = col.Name;
                                if (value == null)
                                {
                                    throw new Exception("Trying to update a row whose primary key is null; use insert instead.");
                                }
                            }
                            dt.Rows[rowNum][colNum] = value ?? DBNull.Value;
                            colNum++;
                        }
                        rowNum++;
                    }
                    setStatement.Length--;
                    try
                    {
                        con.Open();

                        command.CommandText = $"CREATE TABLE [dbo].[#TmpTable]({String.Join(",", colNames)})";
                        //command.CommandTimeout = CmdTimeOut;
                        command.ExecuteNonQuery();

                        sbc.DestinationTableName = "[dbo].[#TmpTable]";
                        sbc.BulkCopyTimeout = CmdTimeOut * 3;
                        sbc.WriteToServer(dt);
                        sbc.Close();

                        command.CommandTimeout = CmdTimeOut * 3;
                        command.CommandText = $"UPDATE ME SET {setStatement} FROM {tn} as ME INNER JOIN #TmpTable AS T on ME.{keyName} = T.{keyName}; DROP TABLE #TmpTable;";
                        updated = command.ExecuteNonQuery();
                    }
                    catch (Exception ex)
                    {
                        if (con.State != ConnectionState.Closed)
                        {
                            sbc.Close();
                            con.Close();
                        }
                        //well logging to sql might not work... we could try... but no.
                        //So Lets write to a local file.
                        _logger.Log($"Failed to Bulk Update to Sql:  {rows.ToCSV()}", ex);
                        throw ex;
                    }
                }
            }
        }
        return (updated > 0) ? true : false;
    }

【讨论】:

    【解决方案2】:

    批量更新:

    第一步:将要更新的数据和主键放在一个列表中。

    第2步:将这个列表和ConnectionString传递给BulkUpdate方法如下所示

    例子:

             //Method for Bulk Update the Data
        public static void BulkUpdateData<T>(List<T> list, string connetionString)
        {
    
            DataTable dt = new DataTable("MyTable");
            dt = ConvertToDataTable(list);
    
            using (SqlConnection conn = new SqlConnection(connetionString))
            {
                using (SqlCommand command = new SqlCommand("CREATE TABLE 
                      #TmpTable([PrimaryKey],[ColumnToUpdate])", conn))
                {
                    try
                    {
                        conn.Open();
                        command.ExecuteNonQuery();
    
                        using (SqlBulkCopy bulkcopy = new SqlBulkCopy(conn))
                        {
                            bulkcopy.BulkCopyTimeout = 6600;
                            bulkcopy.DestinationTableName = "#TmpTable";
                            bulkcopy.WriteToServer(dt);
                            bulkcopy.Close();
                        }
    
    
                        command.CommandTimeout = 3000;
                        command.CommandText = "UPDATE P SET P.[ColumnToUpdate]= T.[ColumnToUpdate] FROM [TableName Where you want to update ] AS P INNER JOIN #TmpTable AS T ON P.[PrimaryKey] = T.[PrimaryKey] ;DROP TABLE #TmpTable;";
                        command.ExecuteNonQuery();
                    }
                    catch (Exception ex)
                    {
                        // Handle exception properly
                    }
                    finally
                    {
                        conn.Close();
                    }
                }
            }
        }
    

    第三步:把ConvertToDataTable方法如下图所示。

    例子:

        public static DataTable ConvertToDataTable<T>(IList<T> data)
        {
            PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(T));
            DataTable table = new DataTable();
            foreach (PropertyDescriptor prop in properties)
                table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
            foreach (T item in data)
            {
                DataRow row = table.NewRow();
                foreach (PropertyDescriptor prop in properties)
                    row[prop.Name] = prop.GetValue(item) ?? DBNull.Value;
                table.Rows.Add(row);
            }
            return table;
        }
    

    注意:无论SquareBracket[] 在哪里,都输入你自己的值。

    【讨论】:

      【解决方案3】:

      试用 Nuget 上提供的 SqlBulkTools。

      免责声明:我是这个库的作者。

      var bulk = new BulkOperations();
      var records = GetRecordsToUpdate();
      
      using (TransactionScope trans = new TransactionScope())
      {
          using (SqlConnection conn = new SqlConnection(ConfigurationManager
          .ConnectionStrings["SqlBulkToolsTest"].ConnectionString))
          {
              bulk.Setup<MyTable>()
                  .ForCollection(records)
                  .WithTable("MyTable")
                  .AddColumn(x => x.SomeColumn1)
                  .AddColumn(x => x.SomeColumn2)
                  .BulkUpdate()
                  .MatchTargetOn(x => x.Identifier)
                  .Commit(conn);
          }
      
          trans.Complete();
      }  
      

      只有“SomeColumn1”和“SomeColumn2”会被更新。更多例子可以找到here

      【讨论】:

      • 根据您的姓名和 Github 帐户的名称,我会说您是这个 SqlBulkTools 库的作者。没有错,但你应该清楚地披露它。否则,它可以被视为垃圾邮件并被删除。谢谢!
      • @GregRTaylor,亲爱的作者,泰勒先生。我正在尝试使用 SqlBulkTool 更新已填充的 DataTable(.NET 普通 DataTable)。我尝试了 PrepareDatatable 但我认为示例似乎不够精确,足够......我看到你正在处理你的 Github 站点中的现有 DataTable。我认为我可以在制作 List 后使用 PrepareDataTable,或者我可以等待您使用现有 DataTable 进行更新。您能指导我如何进行操作或更详细地介绍 DataTable 真实示例吗?非常感谢您提供出色的 SqlBulkTools!
      • @GregRTaylor SqlBulkTools 发生了什么?离开 Git 和 nuget。
      • @Steve 有市售吗?
      • 对不起@Magnus,我当时需要钱。
      【解决方案4】:

      我之前所做的是将数据批量插入到临时表中,然后使用命令或存储过程来更新将临时表与目标表相关联的数据。临时表是一个额外的步骤,但与逐行更新数据相比,如果行数很大,您可以通过批量插入和大规模更新获得性能提升。

      例子:

      public static void UpdateData<T>(List<T> list,string TableName)
      {
          DataTable dt = new DataTable("MyTable");
          dt = ConvertToDataTable(list);
      
          using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SchoolSoulDataEntitiesForReport"].ConnectionString))
          {
              using (SqlCommand command = new SqlCommand("", conn))
              {
                  try
                  {
                      conn.Open();
      
                      //Creating temp table on database
                      command.CommandText = "CREATE TABLE #TmpTable(...)";
                      command.ExecuteNonQuery();
      
                      //Bulk insert into temp table
                      using (SqlBulkCopy bulkcopy = new SqlBulkCopy(conn))
                      {
                          bulkcopy.BulkCopyTimeout = 660;
                          bulkcopy.DestinationTableName = "#TmpTable";
                          bulkcopy.WriteToServer(dt);
                          bulkcopy.Close();
                      }
      
                      // Updating destination table, and dropping temp table
                      command.CommandTimeout = 300;
                      command.CommandText = "UPDATE T SET ... FROM " + TableName + " T INNER JOIN #TmpTable Temp ON ...; DROP TABLE #TmpTable;";
                      command.ExecuteNonQuery();
                  }
                  catch (Exception ex)
                  {
                      // Handle exception properly
                  }
                  finally
                  {
                      conn.Close();
                  }
              }
          }
      }
      

      请注意,使用单个连接来执行整个操作,以便能够在每个步骤中使用临时表,因为临时表的范围是每个连接。

      【讨论】:

      • 这是一个很好的棘手方法,但我正在寻找任何直接从前端更新的方法
      • 嗯,我不知道按原样从 .NET 执行直接批量更新的方法。但是,处理多个数据的其他替代方法可能是使用 DataSet,但您必须先选择其中的数据(这对于大量数据来说是不利的),然后更新相应的 DataTable,最后将更改持久化到数据库中。但我不认为它在内部将更新作为批量处理,而是作为一批更新处理。
      • 可以安全地省略临时表,在更新语句中使用表值参数:
      • 什么是 clsBulkOperation ?我的程序无法识别它
      • 加载数据的时候不应该是bulkcopy.DestinationTableName = "#TmpTable";吗?否则,您只是将数据加载到真实表中。我很确定这不是你想要的。
      【解决方案5】:

      我会在临时表中插入新值,然后对目标表进行合并,如下所示:

      MERGE [DestTable] AS D 
      USING #SourceTable S
          ON D.ID = S.ID
      WHEN MATCHED THEN 
          UPDATE SET ...
      WHEN NOT MATCHED 
      THEN INSERT (...) 
      VALUES (...);
      

      【讨论】:

        【解决方案6】:

        根据我的个人经验,处理这种情况的最佳方法是使用带有Table-Valued ParameterUser-Defined Table Type 的存储过程。只需用数据表的列设置类型,并在 SQL 命令中将所述数据表作为参数传入。

        在存储过程中,您可以直接加入某个唯一键(如果您要更新的所有行都存在),或者 - 如果您可能遇到必须同时进行更新和插入的情况 - 使用 SQL存储过程中的Merge 命令来处理适用的更新和插入。

        Microsoft 有 syntax referencearticle with examples 用于合并。

        对于.NET 部分,只需将参数类型设置为SqlDbType.Structured,并将所述参数的值设置为包含您要更新的记录的数据表即可。

        此方法具有清晰性和易于维护的优点。虽然可能有一些方法可以提供性能改进(例如将其放入临时表然后迭代该表),但我认为让 .NET 和 SQL 处理传输表和更新记录本身的简单性超过了它们。亲吻

        【讨论】:

        【解决方案7】:

        我会选择 TempTable 方法,因为这样你就不会锁定任何东西。但是,如果您的逻辑只需要在前端并且您需要使用批量复制,我会尝试删除/插入方法,但在同一个 SqlTransaction 中以确保完整性,这将是这样的:

        // ...
        
        dt = ConvertToDataTable(list);
        
        using (SqlConnection cnx = new SqlConnection(myConnectionString))
        {
            using (SqlTranscation tran = cnx.BeginTransaction())
            {
                DeleteData(cnx, tran, list);
        
                using (SqlBulkCopy bulkcopy = new SqlBulkCopy(cnx, SqlBulkCopyOptions.Default, tran))
                {
                    bulkcopy.BulkCopyTimeout = 660;
                    bulkcopy.DestinationTableName = TabelName;
                    bulkcopy.WriteToServer(dt);
                }
        
                tran.Commit();
            }
        }
        

        【讨论】:

          【解决方案8】:

          您可以尝试构建一个包含所有数据的查询。使用case。它可能看起来像这样

          update your_table
          set some_column = case when id = 1 then 'value of 1'
                                 when id = 5 then 'value of 5'
                                 when id = 7 then 'value of 7'
                                 when id = 9 then 'value of 9'
                            end
          where id in (1,5,7,9)
          

          【讨论】:

          • 如果您使用的是 sql server 2008+,那么我仍然建议使用 table value 参数。它可以同时做,而且只需要一个 proc。使用 Merge.when 匹配然后在不匹配时插入然后更新。只需参考 msdn 的语法。然后将 SqlBulkCopy 全部废弃。
          猜你喜欢
          • 1970-01-01
          • 2012-05-05
          • 1970-01-01
          • 2021-12-13
          • 2018-12-03
          • 2019-10-08
          • 2012-01-10
          • 2017-03-25
          • 2020-02-18
          相关资源
          最近更新 更多