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

entity-framework sql 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. 可以在SQL BulkCopy之后返回PrimayKey ID吗?
  2. 在Entity框架中提高批量插入性能
  3. 实体框架中最快的插入方式 (这是关于具有许多挂起插入的SaveChanges()性能,应该在每次X插入时调用它,而不是在1000个待处理的处理结束时调用它)

一般承认的答案

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


热门答案

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

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解决方案。这与您发布的其中一个链接中的已接受答案非常相似,但我已使用表变量删除了几个阶段。



许可下: CC-BY-SA with attribution
不隶属于 Stack Overflow
这个KB合法吗? 是的,了解原因
许可下: CC-BY-SA with attribution
不隶属于 Stack Overflow
这个KB合法吗? 是的,了解原因