EF Codefirst批量插入

5
我需要使用EF Code First插入大约2500行数据。
我的原始代码看起来像这样:
foreach(var item in listOfItemsToBeAdded)
{
    //biz logic
    context.MyStuff.Add(i);
}

这花费了很长时间。每个DBSet.Add()调用大约需要2.2秒,相当于90分钟。

我将代码重构为以下形式:

var tempItemList = new List<MyStuff>();
foreach(var item in listOfItemsToBeAdded)
{
    //biz logic
    tempItemList.Add(item)
}
context.MyStuff.ToList().AddRange(tempItemList);

这只需要大约4秒钟就可以运行。然而,.ToList()查询了表中当前所有的项,这是极其必要的,但从长远来看可能会很危险甚至更加耗时。一种解决方法是像这样做:context.MyStuff.Where(x=>x.ID = *空guid*).AddRange(tempItemList),因为我知道永远不会返回任何内容。
但我很好奇是否有其他人知道使用EF Code First进行批量插入的有效方法?

6
你的第二个例子实际上并没有执行任何插入操作。4秒钟只是在传输数据,tempItemList被添加到一个新的List<T>中,并未直接添加到上下文中... - Reed Copsey
@Reed 但是一旦我调用SaveChanges()并从Management Studio查询MyStuff,我就会在数据库中插入2500行。 - James
4
@James - 我认为你的结论是错误的,也许你在看以前运行的数据,因为据我所知,Reed 是正确的。第二个版本是将对象添加到内存中的临时列表,而不是 DbSet 本身。 - Pete
9个回答

13

验证通常是 EF 的一部分,而且开销很大。我通过使用以下方法禁用它来获得了很大的性能提升:

```csharp dbContext.Configuration.ValidateOnSaveEnabled = false; ```

context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ValidateOnSaveEnabled = false;

我相信我在一个类似的SO问题中找到了这个--也许就是这个答案

那个问题的另一个答案指出,如果你真的需要批量插入性能,应该考虑使用System.Data.SqlClient.SqlBulkCopy。对于这个问题,EF和ADO.NET之间的选择实际上取决于你的优先事项。


这是我正在考虑的事情,但我还在进行更新,这可能会影响到它们。 - James

2
我推荐这篇关于如何使用EF进行批量插入的文章。 Entity Framework and slow bulk INSERTs 他探讨了以下几个方面并进行了性能比较:
  1. 默认EF(添加30,000条记录需要57分钟)
  2. 替换为ADO.NET代码(相同的30,000条记录只需25秒)
  3. 上下文膨胀-每个工作单元都使用新的上下文,以保持活动上下文图形的小型化(相同的30,000次插入需要33秒)
  4. 大列表-关闭AutoDetectChangesEnabled(时间约为20秒)
  5. 批处理(时间缩短到16秒)
  6. DbTable.AddRange() - (性能在12左右)

2

我有一个疯狂的想法,但我认为它会帮助你。

每添加100个项目后,请调用SaveChanges。 我有一种感觉,EF中的Track Changes在处理大量数据时性能非常差。


2
这可能取决于运行代码的机器,但根据我的经验,过于频繁地调用 SaveChanges() 会比尽可能少地调用它更加影响性能。 - STW
2
我在添加/删除大量数据时使用Repository和UnitOfWork模式,我不会一次性进行批量操作,而是将更改分成小的相关组,并在完成每个组后调用SaveChanges,然后释放unitOfWork以释放内存。如果我一次性进行所有更改,可以在10分钟内释放200 GB的数据库,但如果分批进行,则需要数小时。 - Bassam Alugili

2
正如STW所指出的,每次调用Add方法时调用DetectChanges方法非常昂贵。
常见的解决方案有:
  • 使用AddRange而不是Add
  • 将AutoDetectChanges设置为false
  • 将SaveChanges拆分为多个批次
请参见:提高Entity Framework Add性能 重要的是要注意,使用AddRange并不执行BulkInsert,它只是在添加所有实体后一次调用DetecthChanges方法,这极大地提高了性能。

但我很好奇是否还有其他有效的方法可以使用EF Code First进行批量插入

有一些支持批量插入的第三方库可用:
请参见:Entity Framework Bulk Insert library
免责声明:我是 Entity Framework Extensions 的所有者。
这个库允许你执行你需要的所有批量操作:
- 批量保存更改 - 批量插入 - 批量删除 - 批量更新 - 批量合并
示例:
// Easy to use
context.BulkSaveChanges();

// Easy to customize
context.BulkSaveChanges(bulk => bulk.BatchSize = 100);

// Perform Bulk Operations
context.BulkDelete(customers);
context.BulkInsert(customers);
context.BulkUpdate(customers);

// Customize Primary Key
context.BulkMerge(customers, operation => {
   operation.ColumnPrimaryKeyExpression = 
        customer => customer.Code;
});

1
虽然有点晚了,上面发布的答案和评论非常有用,但我会留在这里,希望对那些遇到与我相同问题并来到此贴寻找答案的人有所帮助。如果您想要使用Entity Framework批量插入记录的方法,此贴仍然在谷歌排名较高(截至本回答发布时)。

我在MVC 5应用程序中使用Entity Framework和Code First时遇到了类似的问题。用户提交一个表单导致数万条记录被插入到一个表中。用户需要等待超过2分30秒,而60,000个记录正在被插入。

经过大量搜索,我偶然发现了BulkInsert-EF6,它也可以作为NuGet软件包使用。重构OP的代码:

var tempItemList = new List<MyStuff>();
foreach(var item in listOfItemsToBeAdded)
{
    //biz logic
    tempItemList.Add(item)
}

using (var transaction = context.Transaction())
{
    try
    {
        context.BulkInsert(tempItemList);
        transaction.Commit();
    }
    catch (Exception ex)
    {
        // Handle exception
        transaction.Rollback();
    }
}

我的代码从处理60000条记录需要2分钟以上,变成不到1秒钟就能完成。


1
EF并不适用于批量操作(我认为通常情况下ORM也不适用)。这个运行缓慢的特定原因是因为EF中的更改跟踪器。实际上,EF API的每次调用都会导致内部对TrackChanges()的调用,包括DbSet.Add()。当您添加2500个时,该函数将被调用2500次。每次调用越多,速度就越慢。因此,在EF中禁用更改跟踪应该会很有帮助:
dataContext.Configuration.AutoDetectChangesEnabled = false;

更好的解决方案是将大型批量操作分成2500个较小的事务,每个事务都使用自己的数据上下文运行。您可以使用msmq或其他可靠消息传递机制来启动每个较小的事务。
但如果您的系统围绕着大量的批量操作构建,我建议在数据访问层中寻找不同的解决方案而不是EF。

1
尽管回复晚了,但我还是发布答案,因为我也经历过同样的痛苦。 我已经创建了一个新的GitHub项目,目前它支持使用SqlBulkCopy透明地对Sql Server进行批量插入/更新/删除。

https://github.com/MHanafy/EntityExtensions

除此之外还有其他好处,希望将来可以扩展更多功能。

使用它非常简单:

var insertsAndupdates = new List<object>();
var deletes = new List<object>();
context.BulkUpdate(insertsAndupdates, deletes);

希望它有所帮助!

0

0
    public static void BulkInsert(IList list, string tableName)
    {
        var conn = (SqlConnection)Db.Connection;
        if (conn.State != ConnectionState.Open) conn.Open();

        using (var bulkCopy = new SqlBulkCopy(conn))
        {
            bulkCopy.BatchSize = list.Count;
            bulkCopy.DestinationTableName = tableName;

            var table = ListToDataTable(list);
            bulkCopy.WriteToServer(table);
        }
    }

    public static DataTable ListToDataTable(IList list)
    {
        var dt = new DataTable();
        if (list.Count <= 0) return dt;

        var properties = list[0].GetType().GetProperties();
        foreach (var pi in properties)
        {
            dt.Columns.Add(pi.Name, Nullable.GetUnderlyingType(pi.PropertyType) ?? pi.PropertyType);
        }

        foreach (var item in list)
        {
            DataRow row = dt.NewRow();
            properties.ToList().ForEach(p => row[p.Name] = p.GetValue(item, null) ?? DBNull.Value);
            dt.Rows.Add(row);
        }
        return dt;
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接