C# 中的批量更新

Posted

技术标签:

【中文标题】C# 中的批量更新【英文标题】:Bulk Update in 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 更新到数据库

【问题讨论】:

为史诗般的术语“更新”+1 不确定我是否明白您要存档的意思...如果您的问题是关于快速替换整个表格内容,我会选择 truncate (technet.microsoft.com/en-us/library/ms177570.aspx) 并批量插入新的数据部分。但是这种方法仅在您没有外键约束的情况下才有效。如果你想要真正的更新而不是寻找answer from Guillermo Gutiérrez。 我会建议使用表值参数,这两者都可以。 @dormisher 更新是印度常用的一个术语:english.stackexchange.com/questions/68169/… 我的印度同事确认了这个词...@EvertonAgner 【参考方案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 reference 和 article with examples 用于合并。

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

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

【讨论】:

您的回答被低估了。 MSDN 甚至提供了通过表值参数进行批量更新的示例:msdn.microsoft.com/en-us/library/bb675163(v=vs.110).aspx @A.K-R 谢谢。我是这么想的。 :) 批量更新的绝妙方式...应该接受答案。【参考方案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 全部废弃。

以上是关于C# 中的批量更新的主要内容,如果未能解决你的问题,请参考以下文章

C#如何将datatable中的数据批量更新到MYSQL数据库

在 C# 中批量更新 mongoDB 文档

C# mongodb driver 2.0 - 如何在批量操作中更新插入?

在C#中完成海量数据的批量插入和更新

sql语句将Excel中的一列批量更新到sql server中的一列中?

sql在update更新时如何快速且大批量的更新数据(C#中写的)