4

我们的应用程序中有一个用例,其中用户触发了一个请求,该请求将导致插入 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 秒,现在需要大约 100 毫秒(!)

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. 提高实体框架中的批量插入性能
  3. 在实体框架中插入的最快方法(这是关于具有许多待处理插入的 SaveChanges() 性能,应该在每 X 次插入时调用它,而不是在处理结束时调用 1000s 待处理)
4

3 回答 3

4

您通过 EF 所做的任何事情都不会像 SqlBulkCopy 一样快。事实上,原始 SQLINSERT没有那么快。所以你只需要重新阅读位置。通过使用MergeOption.OverwriteChanges重新读取来刷新查询。

于 2012-05-01T14:13:01.040 回答
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# 数据表符合 dbo.YourType 的格式,您就可以按照通常将参数传递给 SqlCommand 的相同方式传递它。

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

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

于 2012-05-01T16:13:02.897 回答
0

放:

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

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

由于在所有这些插入过程中,上下文是相同的,因此您可以每 1000 次插入重建上下文对象。var yourContext = new YourContext();

在我的导入数据过程中进行此改进,将其从 7 分钟缩短到 6 秒。

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

于 2013-08-25T22:41:21.070 回答