74

For inserting a huge amount of data in a database, I used to collect all the inserting information into a list and convert this list into a DataTable. I then insert that list to a database via SqlBulkCopy.

Where I send my generated list
LiMyList
which contain information of all bulk data which I want to insert to database
and pass it to my bulk insertion operation

InsertData(LiMyList, "MyTable");

Where InsertData is

 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;
        }

Now I want to do an update operation, is there any way as for inserting data is done by SqlBulkCopy for Updating data to DataBase From C#.Net

daniel_aren
  • 1,714
  • 3
  • 24
  • 41
Amit Bisht
  • 4,870
  • 14
  • 54
  • 83
  • 39
    +1 for the epic term 'Updation' – jcvandan Dec 17 '13 at 13:47
  • 1
    i will suggest to use table valued parameter,which can do both. – KumarHarsh Dec 17 '13 at 14:08
  • 3
    @dormisher Updation is a term commonly used on India: http://english.stackexchange.com/questions/68169/is-updation-a-correct-word – everton Dec 26 '13 at 17:06
  • My Indian workmate confirmed the term... @EvertonAgner – Roger Oliveira Apr 29 '15 at 01:48
  • Not sure I got the point you are going to archive... If your question is about quick replacing entire table content, than I would go for `truncate` (http://technet.microsoft.com/en-us/library/ms177570.aspx) and bulk insert of a new portion of data. But this approach will only work in case you have no foreign key constraints. If you want to real update than look for the [answer from Guillermo Gutiérrez](https://stackoverflow.com/a/20635881). – Yaugen Vlasau Dec 17 '13 at 13:59

9 Answers9

85

What I've done before is perform a bulk insert from the data into a temp table, and then use a command or stored procedure to update the data relating the temp table with the destination table. The temp table is an extra step, but you can have a performance gain with the bulk insert and massive update if the amount of rows is big, compared to updating the data row by row.

Example:

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();
            }
        }
    }
}

Notice that a single connection is used to perform the whole operation, in order to be able to use the temp table in each step, because the scope of the temp table is per connection.

Guillermo Gutiérrez
  • 17,273
  • 17
  • 89
  • 116
  • thats a good tricky way but i am searching for any way to update directly from front end – Amit Bisht Dec 17 '13 at 15:17
  • Mmmm, I don't know a way to perform a direct bulk update from .NET as is. However, other alternative to handle multiple data could be using a DataSet, but you must select the data before in it (which is a disadvantage for big amounts of data), then update the corresponding DataTable and finally persist the changes in database. But I don't think it handles the update internally as a bulk, but more as a batch of updates. – Guillermo Gutiérrez Dec 17 '13 at 15:55
  • 2
    the temp table may safely be ommited, using a table valued parameter in the update statement: – A. K-R Oct 23 '14 at 11:07
  • What is clsBulkOperation ? It is not recognized in my program – Niloofar Oct 07 '15 at 15:10
  • @Niloofar that was part of the sample code of the question, probably a class defined in the program where the code was extracted. It is not part of the .NET framework. – Guillermo Gutiérrez Oct 16 '15 at 16:40
  • 1
    When loading the data, shouldn't it be `bulkcopy.DestinationTableName = "#TmpTable";`? Otherwise you're just loading the data into the real table. I'm pretty sure this is not what you're going for. – Danny Mar 06 '17 at 22:41
  • @Danny indeed. Fixed. Thank you very much for pointing it out :) – Guillermo Gutiérrez Mar 08 '17 at 16:59
  • @GuillermoGutiérrez This would work fine if you only want to update the table provided the records already exist in the Table. But what if my Datatable contains new records plus the old records which needs to be updated and the new rows in the Datatable need to be inserted into the Table. – Sajith K Aug 14 '20 at 12:17
  • @SajithK you can use the SQL `MERGE` statement instead of `UPDATE`: https://www.sqlservertutorial.net/sql-server-basics/sql-server-merge/ – Guillermo Gutiérrez Aug 14 '20 at 21:37
51

In my personal experience, the best way to handled this situation is utilizing a Stored Procedure with a Table-Valued Parameter and a User-Defined Table Type. Just set up the type with the columns of the data table, and pass in said-data table as a parameter in the SQL command.

Within the Stored Procedure, you can either join directly on some unique key (if all rows you are updating exist), or - if you might run into a situation where you are having to do both updates and inserts - use the SQL Merge command within the stored procedure to handle both the updates and inserts as applicable.

Microsoft has both syntax reference and an article with examples for the Merge.

For the .NET piece, it's a simple matter of setting the parameter type as SqlDbType.Structured and setting the value of said-parameter to the Data Table that contains the records you want to update.

This method provides the benefit of both clarity and ease of maintenance. While there may be ways that offer performance improvements (such as dropping it into a temporary table then iterating over that table), I think they're outweighed by the simplicity of letting .NET and SQL handle transferring the table and updating the records itself. K.I.S.S.

Michael
  • 1,036
  • 1
  • 11
  • 22
7

Bulk Update:

Step 1: put the data which you want to update and primary key in a list.

Step 2: pass this list and ConnectionString to BulkUpdate Method As shown below

Example:

         //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();
                }
            }
        }
    }

Step 3: put The ConvertToDataTable Method as shown Below.

Example:

    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;
    }

Notes: WhereEver SquareBracket[] is there, put your own value.

Azametzin
  • 5,223
  • 12
  • 28
  • 46
Rakesh Kumar
  • 1
  • 1
  • 2
4

Try out SqlBulkTools available on Nuget.

Disclaimer: I'm the author of this library.

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();
}  

Only 'SomeColumn1' and 'SomeColumn2' will be updated. More examples can be found here

Dongdong
  • 2,208
  • 19
  • 28
Greg R Taylor
  • 3,470
  • 1
  • 25
  • 19
  • 5
    Based on your name, and on the name of the Github account, I'd say you are the author of this SqlBulkTools library. There's nothing wrong with it, but you should disclose it clearly. Otherwise it can be considered as spam and removed. Thank you! – Fabio says Reinstate Monica Sep 13 '16 at 13:40
  • @GregRTaylor, Dear the author, Mr. Taylor. I'm trying SqlBulkTool to update with DataTable which are populated (.NET normal DataTable).I tried PrepareDatatable but I think examples seems not precise, sufficient... I saw you're working on existing DataTable in your Github site.I think I can use PrepareDataTable after making List or I can wait your update with exising DataTable..Can you guide me how to proceed or DataTable real example in more detail ? Thank you very much for excellent SqlBulkTools ! – Kay Lee Dec 01 '16 at 02:52
  • Hey Kay, at the moment the library does not take a DataTable but it is an active issue. In the meantime, you could write a stored procedure for your problem. – Greg R Taylor Dec 01 '16 at 08:49
  • 2
    @GregRTaylor What happened to SqlBulkTools? Gone from Git and nuget. – Magnus May 29 '17 at 09:43
  • @GregRTaylor he sold it – Steve Aug 13 '17 at 13:54
  • 1
    @Steve It is commercially available? – Magnus Sep 22 '17 at 07:47
  • 1
    Sorry @Magnus, I needed the money at the time. – Greg R Taylor Sep 22 '17 at 11:17
  • @GregRTaylor It is to bad, it was a great tool. But there are still many forks available fortunately. – Magnus Sep 22 '17 at 11:35
2

I would insert new values in a temporary table and then do a merge against the destination table, something like this:

MERGE [DestTable] AS D 
USING #SourceTable S
    ON D.ID = S.ID
WHEN MATCHED THEN 
    UPDATE SET ...
WHEN NOT MATCHED 
THEN INSERT (...) 
VALUES (...);
aggaton
  • 3,066
  • 2
  • 25
  • 36
1

You could try to build a query that contains all data. Use a case. It could look like this

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)
juergen d
  • 201,996
  • 37
  • 293
  • 362
  • if you are using sql server 2008+ then i still suggest use table value parameter.which can do both and only one proc require. use Merge.when matched then insert when not matched then update. just refer msdn for syntax.and scrap SqlBulkCopy all together. – KumarHarsh Dec 23 '13 at 03:41
1

I'd go for a TempTable approach because that way you aren't locking anything. But if your logic needs to be only in the front end and you need to use bulk copy, I'd try a Delete/Insert approach but in the same SqlTransaction to ensure integrity which would be something like this:

// ...

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();
    }
}
Mauro2
  • 1,235
  • 1
  • 7
  • 9
0

Complete answer, disclaimer: arrow code; this is mine built from research; Published in SqlRapper. It uses custom attributes over properties to determine whether a key is primary. Yes, super complicated. Yes super reusable. Yes, needs to be refactored. Yes, it is a nuget package. No, the documentation isn't great on github, but it exists. Will it work for everything? Probably not. Will it work for simple stuff? Oh yeah.

How easy is it to use after setup?

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);

Here's how it works:

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;
    }
Patrick Knott
  • 1,666
  • 15
  • 15
0

I made this generic solution with same idea as accepted answer (create temp table, fill it with bulk insert and then update target table) which uses reflection to read properties so you don't have to write lengthy UPDATE SET command:

public static class SqlHelper
{
    public static void BulkEdit<T>(SqlConnection connection, SqlCommand command, List<T> rowsToUpdate, string tableName, string idPropertyName)
    {
        var tempTableName = "#" + Guid.NewGuid().ToString().Replace("-", ""); // Use guid as table name to avoid collisions 

        // create temp table on database
        command.CommandText = $"SELECT * INTO {tempTableName} FROM {tableName} WHERE 1 = 0";
        command.ExecuteNonQuery();

        // fill temp table with updated data
        using (SqlBulkCopy bulkCopy = new SqlBulkCopy(connection.ConnectionString, SqlBulkCopyOptions.KeepIdentity))
        {
            bulkCopy.DestinationTableName = tempTableName;
            bulkCopy.WriteToServer(rowsToUpdate.AsDataReader()); // 
        }

        // merge temp table with original
        command.CommandText = $"MERGE {tableName} AS TargetTable " +
            $"USING {tempTableName} AS SourceTable " +
            $"ON TargetTable.{idPropertyName} = SourceTable.{idPropertyName} " +
            "WHEN MATCHED THEN UPDATE SET ";

        command.CommandText += string.Join(",", typeof(T)
            .GetProperties()
            .Where(x => x.Name != idPropertyName)
            .Select(x => $"TargetTable.{x.Name} = SourceTable.{x.Name}")
            .ToArray());
        command.CommandText += ";";
        command.ExecuteNonQuery();

        // delete temp table
        command.CommandText = $"DROP TABLE {tempTableName}";
        command.ExecuteNonQuery();
    }
}

This solution uses reflection to read properties so you don't have to write whole update command

NOTE: AsDataReader() is extension function from microsoft's ObjectDataReader which can be found here: https://github.com/microsoftarchive/msdn-code-gallery-community-m-r/tree/master/ObjectDataReader

you can use this solution like this

using (var sqlConnection = new SqlConnection("connectionString"))
{
    using (var sqlCommand = new SqlCommand("", connection))
    {
         SqlHelper.BulkEdit<MyTable>(sqlConnection, sqlCommand, tableDataRows, $"[dbo].{nameof(MyTable)}", nameof(MyTable.ID));
    }
}