在EF中插入很多行太慢,如何从SqlBulkCopy获取主键?

时间:2012-05-01 14:04:26

标签: sql entity-framework sqlbulkcopy

我们的应用程序中有一个用例,用户触发的请求将导致插入100到1000行 在插入之后,我们需要对象继续处理并创建更多对象,这些对象是最初插入的对象的外键,换句话说,我们需要插入对象的主键ID。

到目前为止,我们已经使用EF在foreach循环中执行此操作,这太慢了,并且需要大约15-20秒来完成大约600行。 (在阻止用户时,坏:()

原始代码(也处理更新,但我们不关心那里的性能,它不会阻止用户):

foreach (Location updatedLoc in locationsLoaded)
{
    // find it in the collection from the database
    Location fromDb = existingLocations.SingleOrDefault(loc => loc.ExtId.Equals(updatedLoc.ExtId));

    // update or insert
    if (fromDb != null)
    {
        // link ids for update
        updatedLoc.Id = fromDb.Id;

        // set values for update 
        db.Entry(fromDb).CurrentValues.SetValues(updatedLoc);
    }
    else
    {
        System.Diagnostics.Trace.WriteLine("Adding new location: " + updatedLoc.Name, "loadSimple");

        // insert a new location <============ This is the bottleneck, takes about 20-40ms per row
        db.Locations.Add(updatedLoc);
    }
}

// This actually takes about 3 seconds for 600 rows, was actually acceptable
db.SaveChanges();

所以在研究了SO和互联网之后,我发现我使用EF的方式不对,需要使用SqlBulkCopy

因此代码被重写了,过去需要大约20秒,现在需要~100ms(!)

foreach (Location updatedLoc in locationsLoaded)
{
    // find it in the collection from the database
    Location fromDb = existingLocations.SingleOrDefault(loc => loc.ExtId.Equals(updatedLoc.ExtId));

    // update or insert
    if (fromDb != null)
    {
        // link ids for update
        updatedLoc.Id = fromDb.Id;

        // set values for update
        db.Entry(fromDb).CurrentValues.SetValues(updatedLoc);
    }
    else
    {
        System.Diagnostics.Trace.WriteLine("Adding new location: " + updatedLoc.Name, "loadSimple");

        // insert a new location
        dataTable.Rows.Add(new object[] { \\the 14 fields of the location.. });
    }
}

System.Diagnostics.Trace.WriteLine("preparing to bulk insert", "loadSimple");

// perform the bulk insert
using (var bulkCopy = new System.Data.SqlClient.SqlBulkCopy(System.Configuration.ConfigurationManager.ConnectionStrings["bulk-inserter"].ConnectionString))
{
    bulkCopy.DestinationTableName = "Locations";

    for (int i = 0; i < dataTable.Columns.Count; i++)
    {
        bulkCopy.ColumnMappings.Add(i, i + 1);
    }

    bulkCopy.WriteToServer(dataTable);
}

// for update
db.SaveChanges();

问题是,在批量复制之后,Locations集合中的对象(EF ORM的一部分)不会更改(这是好的和预期的),但我需要插入的ID才能继续处理这些对象。

一个简单的解决方案是立即从数据库中再次选择数据,我手边有数据,我可以简单地将其重新选择到不同的集合中。

但该解决方案感觉不正确,是否无法将ID作为插入的一部分。

编辑:简单的解决方案有效,请参阅下面接受的答案,了解如何轻松将其同步回EF。

也许我不应该使用SqlBulkCopy(我希望最多约1000行,不再使用)并使用其他东西?

请注意,一些相关的SO问题和解决方案,似乎都离开了EF ..

  1. Possible to get PrimayKey IDs back after a SQL BulkCopy?
  2. Improving bulk insert performance in Entity framework
  3. Fastest Way of Inserting in Entity Framework(这是关于SaveChanges()性能与许多挂起的插入,应该在每次X插入时调用它,而不是在1000个待处理的处理结束时调用它)

3 个答案:

答案 0 :(得分:4)

你通过EF做的事情都不会像SqlBulkCopy一样快。实际上,原始SQL INSERT并不快。所以你只需要重新阅读地点。通过使用MergeOption.OverwriteChanges重新阅读来刷新查询。

答案 1 :(得分:4)

如果您使用的是SQL-Server 2008或更高版本,则可以使用存储过程来执行您所需的操作。您需要定义一个TYPE,它与SQL中的数据表相同:

CREATE TYPE dbo.YourType AS TABLE (ID INT, Column1 INT, Column2 VARCHAR(5)...)

然后将此类型传递给存储过程。

CREATE PROCEDURE dbo.InsertYourType (@YourType dbo.YourType READONLY)
AS
    BEGIN
        DECLARE @ID TABLE (ID INT NOT NULL PRIMARY KEY)
        INSERT INTO YourTable (Column1, Column2...)
        OUTPUT inserted.ID INTO @ID
        SELECT  Column1, Column2...
        FROM    @YourType

        SELECT  *
        FROM    YourTable
        WHERE   ID IN (SELECT ID FROM @ID)

    END

这将捕获插入行的ID,并返回所有新行。只要你的c#datatable符合dbo.YourType的格式,就可以像通常将参数传递给SqlCommand那样传递它。

SqlCommand.Parameters.Add("@YourType", YourDataTable)

我意识到这与您重新选择数据的建议类似,但是选择应该很快,因为它只使用标识列。虽然您仍然遇到使用SQL插入而不是批量复制的问题,但您将恢复到基于集合的解决方案,而不是基于过程的EF解决方案。这与您发布的其中一个链接中的已接受答案非常相似,但我已使用表变量删除了几个阶段。

答案 2 :(得分:0)

集:

yourContext.Configuration.AutoDetectChangesEnabled = false;
yourContext.Configuration.ValidateOnSaveEnabled = false;

在100个插入包中执行SaveChanges()...尝试使用1000并查看更改。

因为在所有这些插入期间,上下文是相同的,所以您可以每1000次插入重建上下文对象。 var yourContext = new YourContext();

在我的导入数据流程中进行此改进,从7分钟到6秒。

实际数字......在你的情况下不可能是100的1000 ...尝试并调整它。