ChatGPT解决这个技术问题 Extra ChatGPT

在实体框架中插入的最快方式

我正在寻找插入实体框架的最快方法。

我之所以问这个问题是因为您有一个活动的 TransactionScope 并且插入量很大(4000+)。它可能会持续超过 10 分钟(事务的默认超时),这将导致事务不完整。

你目前是怎么做的?
创建 TransactionScope,实例化 DBContext,打开连接,并在 for-each 语句中执行插入和 SavingChanges(对于每条记录),注意:TransactionScope 和 DBContext 在 using 语句中,我在 finally 中关闭连接堵塞
另一个参考答案:stackoverflow.com/questions/5798646/…
插入 SQL 数据库的最快方法不涉及 EF。 AFAIK 它的 BCP 然后 TVP+合并/插入。
对于那些将阅读评论的人:最适用的现代 answer 在这里。

S
Shimmy Weitzhandler

对于您在问题评论中的评论:

“...SavingChanges(针对每条记录)...”

这是你能做的最糟糕的事情!为每条记录调用 SaveChanges() 会大大降低批量插入的速度。我会做一些简单的测试,这很可能会提高性能:

在所有记录之后调用 SaveChanges() 一次。

例如,在 100 条记录之后调用 SaveChanges()。

在例如 100 条记录之后调用 SaveChanges() 并处理上下文并创建一个新的。

禁用更改检测

对于批量插入,我正在尝试使用这样的模式:

using (TransactionScope scope = new TransactionScope())
{
    MyDbContext context = null;
    try
    {
        context = new MyDbContext();
        context.Configuration.AutoDetectChangesEnabled = false;

        int count = 0;            
        foreach (var entityToInsert in someCollectionOfEntitiesToInsert)
        {
            ++count;
            context = AddToContext(context, entityToInsert, count, 100, true);
        }

        context.SaveChanges();
    }
    finally
    {
        if (context != null)
            context.Dispose();
    }

    scope.Complete();
}

private MyDbContext AddToContext(MyDbContext context,
    Entity entity, int count, int commitCount, bool recreateContext)
{
    context.Set<Entity>().Add(entity);

    if (count % commitCount == 0)
    {
        context.SaveChanges();
        if (recreateContext)
        {
            context.Dispose();
            context = new MyDbContext();
            context.Configuration.AutoDetectChangesEnabled = false;
        }
    }

    return context;
}

我有一个测试程序,它将 560.000 个实体(9 个标量属性,无导航属性)插入数据库。使用此代码,它可以在不到 3 分钟的时间内运行。

为了提高性能,在“许多”记录(大约 100 或 1000 个左右的“许多”)之后调用 SaveChanges() 很重要。它还提高了在 SaveChanges 之后处理上下文并创建新上下文的性能。这会清除所有实体的上下文,SaveChanges 不会这样做,实体仍附加到状态 Unchanged 的上下文。上下文中附加实体的不断增长的大小逐渐减慢了插入速度。因此,在一段时间后清除它是有帮助的。

以下是我的 560000 个实体的一些测量结果:

commitCount = 1,recreateContext = false:很多小时(这是您当前的程序)

commitCount = 100,recreateContext = false:超过 20 分钟

commitCount = 1000,recreateContext = false:242 秒

commitCount = 10000,recreateContext = false:202 秒

commitCount = 100000,recreateContext = false:199 秒

commitCount = 1000000,recreateContext = false:内存不足异常

commitCount = 1, recreateContext = true: 超过 10 分钟

commitCount = 10,recreateContext = true:241 秒

commitCount = 100,recreateContext = true:164 秒

commitCount = 1000,recreateContext = true:191 秒

上面第一个测试中的行为是性能非常非线性,并且随着时间的推移会急剧下降。 (“许多小时”是一个估计值,我从未完成此测试,我在 20 分钟后停在 50.000 个实体处。)这种非线性行为在所有其他测试中并不那么重要。


@Bongo Sharp:不要忘记在 DbContext 上设置 AutoDetectChangesEnabled = false;。它还具有很大的附加性能影响:stackoverflow.com/questions/5943394/…
是的,问题是我使用的是 Entity Framework 4,而 AutoDetectChangesEnabled 是 4.1 的一部分,但是,我进行了性能测试,我得到了惊人的结果,它从 00:12:00 到 00:00:22 SavinChanges每个实体都在做 olverload... 非常感谢您的回答!这就是我要找的
谢谢你的 context.Configuration.AutoDetectChangesEnabled = false;提示,它有很大的不同。
@dahacker89:您使用的是正确的版本 EF >= 4.1 和 DbContext,而不是 ObjectContext
@dahacker89:我建议您为您的问题创建一个单独的问题,并提供更多详细信息。我无法弄清楚这里出了什么问题。
a
arkhivania

这种组合可以很好地提高速度。

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

不要盲目禁用 ValidateOnSaveEnabled 您可能依赖于该行为,直到为时已晚才意识到它。然后,您可能再次在代码中的其他地方执行验证,并且再次进行 EF 验证是完全没有必要的。
在我的测试中,保存 20.000 行从 101 秒下降到 88 秒。不是很多,有什么影响。
@JeremyCook我认为你想要得到的是这个答案会更好,如果它解释了从默认值更改这些属性的可能影响(除了性能改进)。我同意。
这对我有用,但如果您在上下文中更新记录,您将需要显式调用 DetectChanges()
这些可以禁用,然后使用 try-finally 块重新启用:msdn.microsoft.com/en-us/data/jj556205.aspx
A
Adam Rackis

您应该考虑为此使用 System.Data.SqlClient.SqlBulkCopy。这里是 documentation,当然还有很多在线教程。

抱歉,我知道您正在寻找一个简单的答案来让 EF 做您想做的事,但是批量操作并不是 ORM 的真正用途。


我在研究这个时遇到过 SqlBulkCopy 几次,但它似乎更面向表到表的插入,遗憾的是我并不期待简单的解决方案,而是性能提示,例如管理状态手动连接,让 EF 为您完成
我使用 SqlBulkCopy 直接从我的应用程序中插入大量数据。您基本上必须创建一个 DataTable,将其填满,然后将其传递给 BulkCopy。在设置 DataTable 时有一些陷阱(遗憾的是,我忘记了其中的大部分),但它应该可以正常工作
我做了概念验证,正如所承诺的那样,它运行得非常快,但我使用 EF 的原因之一是因为关系数据的插入更容易,例如,如果我插入一个已经包含关系数据的实体,它也会插入它,你有没有遇到过这种情况?谢谢!
不幸的是,将对象网络插入 DBMS 并不是 BulkCopy 真正会做的事情。这就是像 EF 这样的 ORM 的好处,其代价是它无法扩展以有效地处理数百个类似的对象图。
如果您需要原始速度或者您将重新运行此插入,SqlBulkCopy 绝对是您的最佳选择。我之前用它插入了几百万条记录,而且速度非常快。也就是说,除非您需要重新运行此插入,否则仅使用 EF 可能更容易。
m
maxlego

最快的方法是使用我开发的 bulk insert extension

注意:这是一个商业产品,不是免费的

https://i.stack.imgur.com/AdWD3.png

用法非常简单

context.BulkInsert(hugeAmountOfEntities);

快速但只执行层次结构的顶层。
它不是免费的。
广告变得越来越聪明......这是付费产品,对于自由职业者来说非常昂贵。被警告!
1 年支持和升级 600 美元?你疯了吗?
我不再是产品的所有者
M
Manfred Wippel

因为这里从未提到过,所以我想推荐 EFCore.BulkExtensions here

context.BulkInsert(entitiesList);                 context.BulkInsertAsync(entitiesList);
context.BulkUpdate(entitiesList);                 context.BulkUpdateAsync(entitiesList);
context.BulkDelete(entitiesList);                 context.BulkDeleteAsync(entitiesList);
context.BulkInsertOrUpdate(entitiesList);         context.BulkInsertOrUpdateAsync(entitiesList);         // Upsert
context.BulkInsertOrUpdateOrDelete(entitiesList); context.BulkInsertOrUpdateOrDeleteAsync(entitiesList); // Sync
context.BulkRead(entitiesList);                   context.BulkReadAsync(entitiesList);

我赞同这个建议。在尝试了许多自制解决方案后,这将我的插入时间从 50 多秒缩短到 1 秒。而且,它的 MIT 许可证很容易合并。
这对 ef 6.x 有用吗
如果超过 10 个实体,这仅比使用 AddRange 更高效
10 000 次插入从 9 分钟缩短到 12 秒。这值得更多关注!
如果有任何方法可以更改接受的答案,那么这应该是现在接受的现代答案。我希望 EF 团队开箱即用地提供这个。
a
akjoshi

我同意亚当·拉基斯的观点。 SqlBulkCopy 是将批量记录从一个数据源传输到另一个数据源的最快方式。我用它复制了 20K 条记录,只用了不到 3 秒。看看下面的例子。

public static void InsertIntoMembers(DataTable dataTable)
{           
    using (var connection = new SqlConnection(@"data source=;persist security info=True;user id=;password=;initial catalog=;MultipleActiveResultSets=True;App=EntityFramework"))
    {
        SqlTransaction transaction = null;
        connection.Open();
        try
        {
            transaction = connection.BeginTransaction();
            using (var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, transaction))
            {
                sqlBulkCopy.DestinationTableName = "Members";
                sqlBulkCopy.ColumnMappings.Add("Firstname", "Firstname");
                sqlBulkCopy.ColumnMappings.Add("Lastname", "Lastname");
                sqlBulkCopy.ColumnMappings.Add("DOB", "DOB");
                sqlBulkCopy.ColumnMappings.Add("Gender", "Gender");
                sqlBulkCopy.ColumnMappings.Add("Email", "Email");

                sqlBulkCopy.ColumnMappings.Add("Address1", "Address1");
                sqlBulkCopy.ColumnMappings.Add("Address2", "Address2");
                sqlBulkCopy.ColumnMappings.Add("Address3", "Address3");
                sqlBulkCopy.ColumnMappings.Add("Address4", "Address4");
                sqlBulkCopy.ColumnMappings.Add("Postcode", "Postcode");

                sqlBulkCopy.ColumnMappings.Add("MobileNumber", "MobileNumber");
                sqlBulkCopy.ColumnMappings.Add("TelephoneNumber", "TelephoneNumber");

                sqlBulkCopy.ColumnMappings.Add("Deleted", "Deleted");

                sqlBulkCopy.WriteToServer(dataTable);
            }
            transaction.Commit();
        }
        catch (Exception)
        {
            transaction.Rollback();
        }

    }
}

我尝试了这篇文章中提供的许多解决方案,而 SqlBulkCopy 是迄今为止最快的。纯 EF 花了 15 分钟,但通过混合解决方案和 SqlBulkCopy,我能够缩短到 1.5 分钟!这是200万条记录!没有任何数据库索引优化。
List 比 DataTable 更容易。有一个 AsDataReader() 扩展方法,在这个答案中解释:stackoverflow.com/a/36817205/1507899
但它只适用于顶级实体而不是关系实体
@ZahidMustafa:是的。它正在做 BulkInsert,而不是 Bulk-Analysis-And-Relation-Tracing-On-Object-Graphs .. 如果你想涵盖关系,你必须分析和确定插入顺序,然后批量插入单个级别,并可能将一些键更新为需要,您将获得快速定制的解决方案。或者,您可以依靠 EF 来执行此操作,您无需执行任何操作,但运行时速度较慢。
S
ShaTin

我会推荐这篇关于如何使用 EF 进行批量插入的文章。

Entity Framework and slow bulk INSERTs

他探索了这些领域并比较了性能:

默认 EF(添加 30,000 条记录需要 57 分钟)替换为 ADO.NET 代码(同样的 30,000 条记录需要 25 秒) 上下文膨胀 - 通过为每个工作单元使用新上下文来保持活动上下文图较小(同样的 30,000 次插入需要 33秒)大型列表 - 关闭 AutoDetectChangesEnabled(将时间减少到大约 20 秒)批处理(减少到 16 秒) DbTable.AddRange() -(性能在 12 范围内)


A
Admir Tuzović

我已经调查了 Slauma 的答案(这太棒了,谢谢你的想法),并且我已经减少了批量大小,直到达到最佳速度。查看 Slauma 的结果:

commitCount = 1, recreateContext = true: 超过 10 分钟

commitCount = 10,recreateContext = true:241 秒

commitCount = 100,recreateContext = true:164 秒

commitCount = 1000,recreateContext = true:191 秒

可以看出从1到10、从10到100都有速度提升,但是从100到1000插入速度又下降了。

所以我专注于当你将批量大小减少到 10 到 100 之间时会发生什么,这是我的结果(我使用不同的行内容,所以我的时间具有不同的价值):

Quantity    | Batch size    | Interval
1000    1   3
10000   1   34
100000  1   368

1000    5   1
10000   5   12
100000  5   133

1000    10  1
10000   10  11
100000  10  101

1000    20  1
10000   20  9
100000  20  92

1000    27  0
10000   27  9
100000  27  92

1000    30  0
10000   30  9
100000  30  92

1000    35  1
10000   35  9
100000  35  94

1000    50  1
10000   50  10
100000  50  106

1000    100 1
10000   100 14
100000  100 141

根据我的结果,批量大小的实际最佳值约为 30。它小于 10 和 100。问题是,我不知道为什么 30 是最优的,也找不到任何合乎逻辑的解释。


我发现 Postrges 和纯 SQL(它取决于 SQL 而不是 EF)相同,30 是最佳的。
我的经验是,对于不同的连接速度和行的大小,最佳值会有所不同。对于快速连接和小行,最佳值甚至可以超过 200 行。
j
julianstark999

[2019 更新] EF Core 3.1

按照上面所说的,在 EF Core 中禁用 AutoDetectChangesEnabled 效果很好:插入时间除以 100(从几分钟到几秒,10k 条具有跨表关系的记录)

更新后的代码是:

context.ChangeTracker.AutoDetectChangesEnabled = false;
foreach (IRecord record in records) {
    //Add records to your database        
}
context.ChangeTracker.DetectChanges();
context.SaveChanges();
context.ChangeTracker.AutoDetectChangesEnabled = true; //do not forget to re-enable

M
Mikael Eliasson

正如其他人所说,如果您想要非常好的插入性能,SqlBulkCopy 就是这样做的方法。

实现起来有点麻烦,但是有一些库可以帮助你。有一些,但这次我会无耻地插入我自己的库:https://github.com/MikaelEliasson/EntityFramework.Utilities#batch-insert-entities

您需要的唯一代码是:

 using (var db = new YourDbContext())
 {
     EFBatchOperation.For(db, db.BlogPosts).InsertAll(list);
 }

那么它快了多少呢?很难说,因为它取决于很多因素,计算机性能、网络、对象大小等。我所做的性能测试表明,如果您优化 EF 配置,可以在 10 秒左右以标准方式在 localhost 上插入 25k 个实体,例如在其他答案中提到。使用 EFUtilities 大约需要 300 毫秒。更有趣的是,我使用这种方法在 15 秒内保存了大约 300 万个实体,平均每秒大约 20 万个实体。

一个问题当然是如果您需要插入相关数据。这可以使用上述方法有效地在 sql server 中完成,但它要求您有一个 Id 生成策略,让您在父应用程序代码中生成 id,以便您可以设置外键。这可以使用 GUID 或类似 HiLo id 生成的东西来完成。


效果很好。虽然语法有点冗长。认为如果 EFBatchOperation 有一个您将 DbContext 传递给而不是传递给每个静态方法的构造函数会更好。类似于 DbContext.Set<T> 的自动查找集合的通用版本 InsertAllUpdateAll 也会很好。
只是一个快速的评论说谢谢!这段代码让我可以在 1.5 秒内保存 170k 条记录!完全将我尝试过的任何其他方法从水中吹走。
@Mikael 一个问题是处理身份字段。你有办法启用身份插入吗?
与 EntityFramework.BulkInsert 相比,这个库仍然是免费的。 +1
它适用于 EF Core 吗?
R
RobJan

Dispose() 如果您Add() 的实体依赖于上下文中的其他预加载实体(例如导航属性),则上下文会产生问题

我使用类似的概念来保持我的上下文很小以实现相同的性能

但不是 Dispose() 上下文并重新创建,我只是分离已经 SaveChanges() 的实体

public void AddAndSave<TEntity>(List<TEntity> entities) where TEntity : class {

const int CommitCount = 1000; //set your own best performance number here
int currentCount = 0;

while (currentCount < entities.Count())
{
    //make sure it don't commit more than the entities you have
    int commitCount = CommitCount;
    if ((entities.Count - currentCount) < commitCount)
        commitCount = entities.Count - currentCount;

    //e.g. Add entities [ i = 0 to 999, 1000 to 1999, ... , n to n+999... ] to conext
    for (int i = currentCount; i < (currentCount + commitCount); i++)        
        _context.Entry(entities[i]).State = System.Data.EntityState.Added;
        //same as calling _context.Set<TEntity>().Add(entities[i]);       

    //commit entities[n to n+999] to database
    _context.SaveChanges();

    //detach all entities in the context that committed to database
    //so it won't overload the context
    for (int i = currentCount; i < (currentCount + commitCount); i++)
        _context.Entry(entities[i]).State = System.Data.EntityState.Detached;

    currentCount += commitCount;
} }

如果需要,用 try catch 和 TrasactionScope() 包装它,这里不显示它们以保持代码干净


这减慢了使用 Entity Framework 6.0 的插入 (AddRange)。插入 20.000 行从大约 101 秒增加到 118 秒。
@Stephen Ho:我也试图避免处理我的上下文。我可以理解这比重新创建上下文要慢,但我想知道您是否发现这比不重新创建上下文但设置了 commitCount 更快。
@Learner:我认为这比重新创建上下文要快。但我现在不记得了,因为我最后改用了 SqlBulkCopy。
我最终不得不使用这种技术,因为出于某种奇怪的原因,在第二次通过 while 循环时发生了一些遗留跟踪,即使我将所有内容都包装在 using 语句中,甚至在 DbContext 上调用了 Dispose() .当我将添加到上下文(在第二次传递)时,上下文集计数将跳转到 6 而不是只有 1。任意添加的其他项目已经在第一次通过 while 循环时插入,因此对 SaveChanges 的调用将在第二次通过时失败(原因很明显)。
G
Guilherme

我知道这是一个非常古老的问题,但是这里的一个人说开发了一种扩展方法来使用 EF 的批量插入,当我检查时,我发现该库今天的价格为 599 美元(对于一位开发人员)。也许这对整个库都有意义,但是对于批量插入来说,这太多了。

这是我做的一个非常简单的扩展方法。我首先将它与数据库配对使用(不要先用代码测试,但我认为它的工作原理相同)。将 YourEntities 更改为您的上下文名称:

public partial class YourEntities : DbContext
{
    public async Task BulkInsertAllAsync<T>(IEnumerable<T> entities)
    {
        using (var conn = new SqlConnection(Database.Connection.ConnectionString))
        {
            await conn.OpenAsync();

            Type t = typeof(T);

            var bulkCopy = new SqlBulkCopy(conn)
            {
                DestinationTableName = GetTableName(t)
            };

            var table = new DataTable();

            var properties = t.GetProperties().Where(p => p.PropertyType.IsValueType || p.PropertyType == typeof(string));

            foreach (var property in properties)
            {
                Type propertyType = property.PropertyType;
                if (propertyType.IsGenericType &&
                    propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
                {
                    propertyType = Nullable.GetUnderlyingType(propertyType);
                }

                table.Columns.Add(new DataColumn(property.Name, propertyType));
            }

            foreach (var entity in entities)
            {
                table.Rows.Add(
                    properties.Select(property => property.GetValue(entity, null) ?? DBNull.Value).ToArray());
            }

            bulkCopy.BulkCopyTimeout = 0;
            await bulkCopy.WriteToServerAsync(table);
        }
    }

    public void BulkInsertAll<T>(IEnumerable<T> entities)
    {
        using (var conn = new SqlConnection(Database.Connection.ConnectionString))
        {
            conn.Open();

            Type t = typeof(T);

            var bulkCopy = new SqlBulkCopy(conn)
            {
                DestinationTableName = GetTableName(t)
            };

            var table = new DataTable();

            var properties = t.GetProperties().Where(p => p.PropertyType.IsValueType || p.PropertyType == typeof(string));

            foreach (var property in properties)
            {
                Type propertyType = property.PropertyType;
                if (propertyType.IsGenericType &&
                    propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
                {
                    propertyType = Nullable.GetUnderlyingType(propertyType);
                }

                table.Columns.Add(new DataColumn(property.Name, propertyType));
            }

            foreach (var entity in entities)
            {
                table.Rows.Add(
                    properties.Select(property => property.GetValue(entity, null) ?? DBNull.Value).ToArray());
            }

            bulkCopy.BulkCopyTimeout = 0;
            bulkCopy.WriteToServer(table);
        }
    }

    public string GetTableName(Type type)
    {
        var metadata = ((IObjectContextAdapter)this).ObjectContext.MetadataWorkspace;
        var objectItemCollection = ((ObjectItemCollection)metadata.GetItemCollection(DataSpace.OSpace));

        var entityType = metadata
                .GetItems<EntityType>(DataSpace.OSpace)
                .Single(e => objectItemCollection.GetClrType(e) == type);

        var entitySet = metadata
            .GetItems<EntityContainer>(DataSpace.CSpace)
            .Single()
            .EntitySets
            .Single(s => s.ElementType.Name == entityType.Name);

        var mapping = metadata.GetItems<EntityContainerMapping>(DataSpace.CSSpace)
                .Single()
                .EntitySetMappings
                .Single(s => s.EntitySet == entitySet);

        var table = mapping
            .EntityTypeMappings.Single()
            .Fragments.Single()
            .StoreEntitySet;

        return (string)table.MetadataProperties["Table"].Value ?? table.Name;
    }
}

您可以对从 IEnumerable 继承的任何集合使用它,如下所示:

await context.BulkInsertAllAsync(items);

请完成您的示例代码。批量复制在哪里
它已经在这里了:await bulkCopy.WriteToServerAsync(table);
也许我不清楚,在您的文章中,您建议您进行扩展...我认为这意味着不需要第三部分库,而实际上在这两种方法中都使用 SqlBulkCopy 库。这完全依赖于 SqlBulkCopy,当我问为什么 bulkCopy 来自哪里时,它是一个扩展库,您在其上编写了一个扩展库。在这里说一下我如何使用 SqlBulkCopy 库更有意义。
应该在异步版本中使用 conn.OpenAsync
@guiherme 我是否正确,您的代码中的 SqlBulkCopy 真的是 .net 内置的 SqlClient.SqlBulkCopy 类?
J
Jonathan Magnan

我正在寻找插入实体框架的最快方法

有一些支持 Bulk Insert 的第三方库可用:

Z.EntityFramework.Extensions(推荐)

EFutilities

EntityFramework.BulkInsert

请参阅: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;
});

这是一个很棒的扩展,但不是免费的。
这个答案非常好,EntityFramework.BulkInsert 在 1.5 秒内执行了 15K 行的批量插入,非常适合 Windows 服务等内部进程。
是的,批量插入 600 美元。完全值得。
@eocron Yeat 如果您在商业上使用它是值得的。我认为 600 美元对于我不必花费数小时自己建造的东西没有任何问题,这将花费我超过 600 美元。是的,它要花钱,但看看我的每小时费率,这钱花得值!
R
Reza Jenabi

保存列表的最快方法之一,您必须应用以下代码

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

AutoDetectChangesEnabled = 假

Add、AddRange 和 SaveChanges:不检测更改。

ValidateOnSaveEnabled = 假;

不检测更改跟踪器

您必须添加 nuget

Install-Package Z.EntityFramework.Extensions

现在您可以使用以下代码

var context = new MyContext();

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

context.BulkInsert(list);
context.BulkSaveChanges();

我可以使用您的示例代码进行批量更新吗?
Z库不是免费的
谢谢@reza-jenabi。它救了我
M
Michal Hosala

是的,SqlBulkUpdate 确实是此类任务最快的工具。我想在 .NET Core 中为我找到“最省力”的通用方式,因此我最终使用 great library from Marc Gravell called FastMember 并为实体框架数据库上下文编写了一个微小的扩展方法。快速工作:

using System.Collections.Generic;
using System.Linq;
using FastMember;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;

namespace Services.Extensions
{
    public static class DbContextExtensions
    {
        public static void BulkCopyToServer<T>(this DbContext db, IEnumerable<T> collection)
        {
            var messageEntityType = db.Model.FindEntityType(typeof(T));

            var tableName = messageEntityType.GetSchema() + "." + messageEntityType.GetTableName();
            var tableColumnMappings = messageEntityType.GetProperties()
                .ToDictionary(p => p.PropertyInfo.Name, p => p.GetColumnName());

            using (var connection = new SqlConnection(db.Database.GetDbConnection().ConnectionString))
            using (var bulkCopy = new SqlBulkCopy(connection))
            {
                foreach (var (field, column) in tableColumnMappings)
                {
                    bulkCopy.ColumnMappings.Add(field, column);
                }

                using (var reader = ObjectReader.Create(collection, tableColumnMappings.Keys.ToArray()))
                {
                    bulkCopy.DestinationTableName = tableName;
                    connection.Open();
                    bulkCopy.WriteToServer(reader);
                    connection.Close();
                }
            }
        }
    }
}

更省力的通用方法是遵循这样的方法(再次使用 SqlBulkCopy):codingsight.com/…
M
Maxim

尝试使用存储过程来获取您要插入的数据的 XML。


如果您不想将数据存储为 XML,则不需要将数据作为 XML 传递。在 SQL 2008 中,您可以使用表值参数。
我没有澄清这一点,但我还需要支持 SQL 2005
S
Sgedda

我对上面@Slauma 的示例进行了通用扩展;

public static class DataExtensions
{
    public static DbContext AddToContext<T>(this DbContext context, object entity, int count, int commitCount, bool recreateContext, Func<DbContext> contextCreator)
    {
        context.Set(typeof(T)).Add((T)entity);

        if (count % commitCount == 0)
        {
            context.SaveChanges();
            if (recreateContext)
            {
                context.Dispose();
                context = contextCreator.Invoke();
                context.Configuration.AutoDetectChangesEnabled = false;
            }
        }
        return context;
    }
}

用法:

public void AddEntities(List<YourEntity> entities)
{
    using (var transactionScope = new TransactionScope())
    {
        DbContext context = new YourContext();
        int count = 0;
        foreach (var entity in entities)
        {
            ++count;
            context = context.AddToContext<TenancyNote>(entity, count, 100, true,
                () => new YourContext());
        }
        context.SaveChanges();
        transactionScope.Complete();
    }
}

P
Philip Johnson

SqlBulkCopy 超级快

这是我的实现:

// at some point in my calling code, I will call:
var myDataTable = CreateMyDataTable();
myDataTable.Rows.Add(Guid.NewGuid,tableHeaderId,theName,theValue); // e.g. - need this call for each row to insert

var efConnectionString = ConfigurationManager.ConnectionStrings["MyWebConfigEfConnection"].ConnectionString;
var efConnectionStringBuilder = new EntityConnectionStringBuilder(efConnectionString);
var connectionString = efConnectionStringBuilder.ProviderConnectionString;
BulkInsert(connectionString, myDataTable);

private DataTable CreateMyDataTable()
{
    var myDataTable = new DataTable { TableName = "MyTable"};
// this table has an identity column - don't need to specify that
    myDataTable.Columns.Add("MyTableRecordGuid", typeof(Guid));
    myDataTable.Columns.Add("MyTableHeaderId", typeof(int));
    myDataTable.Columns.Add("ColumnName", typeof(string));
    myDataTable.Columns.Add("ColumnValue", typeof(string));
    return myDataTable;
}

private void BulkInsert(string connectionString, DataTable dataTable)
{
    using (var connection = new SqlConnection(connectionString))
    {
        connection.Open();
        SqlTransaction transaction = null;
        try
        {
            transaction = connection.BeginTransaction();

            using (var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, transaction))
            {
                sqlBulkCopy.DestinationTableName = dataTable.TableName;
                foreach (DataColumn column in dataTable.Columns) {
                    sqlBulkCopy.ColumnMappings.Add(column.ColumnName, column.ColumnName);
                }

                sqlBulkCopy.WriteToServer(dataTable);
            }
            transaction.Commit();
        }
        catch (Exception)
        {
            transaction?.Rollback();
            throw;
        }
    }
}

Z
Zoran Horvat

以下是在实际示例中使用 Entity Framework 和使用 SqlBulkCopy 类之间的性能比较:How to Bulk Insert Complex Objects into SQL Server Database

正如其他人已经强调的那样,ORM 并不打算用于批量操作。它们提供了灵活性、关注点分离和其他好处,但批量操作(批量读取除外)不是其中之一。


A
Amir Saniyan

使用 SqlBulkCopy

void BulkInsert(GpsReceiverTrack[] gpsReceiverTracks)
{
    if (gpsReceiverTracks == null)
    {
        throw new ArgumentNullException(nameof(gpsReceiverTracks));
    }

    DataTable dataTable = new DataTable("GpsReceiverTracks");
    dataTable.Columns.Add("ID", typeof(int));
    dataTable.Columns.Add("DownloadedTrackID", typeof(int));
    dataTable.Columns.Add("Time", typeof(TimeSpan));
    dataTable.Columns.Add("Latitude", typeof(double));
    dataTable.Columns.Add("Longitude", typeof(double));
    dataTable.Columns.Add("Altitude", typeof(double));

    for (int i = 0; i < gpsReceiverTracks.Length; i++)
    {
        dataTable.Rows.Add
        (
            new object[]
            {
                    gpsReceiverTracks[i].ID,
                    gpsReceiverTracks[i].DownloadedTrackID,
                    gpsReceiverTracks[i].Time,
                    gpsReceiverTracks[i].Latitude,
                    gpsReceiverTracks[i].Longitude,
                    gpsReceiverTracks[i].Altitude
            }
        );
    }

    string connectionString = (new TeamTrackerEntities()).Database.Connection.ConnectionString;
    using (var connection = new SqlConnection(connectionString))
    {
        connection.Open();
        using (var transaction = connection.BeginTransaction())
        {
            using (var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, transaction))
            {
                sqlBulkCopy.DestinationTableName = dataTable.TableName;
                foreach (DataColumn column in dataTable.Columns)
                {
                    sqlBulkCopy.ColumnMappings.Add(column.ColumnName, column.ColumnName);
                }

                sqlBulkCopy.WriteToServer(dataTable);
            }
            transaction.Commit();
        }
    }

    return;
}

a
anishMarokey

据我所知,EntityFramework 中有 no BulkInsert 可提高巨大插入的性能。

在这种情况下,您可以使用 ADO.net 中的 SqlBulkCopy 来解决您的问题


我正在看那个类,但它似乎更倾向于表到表的插入,不是吗?
不知道你的意思,它有一个重载的 WriteToServer,需要一个 DataTable
不,您也可以从 .Net 对象插入 SQL。您在寻找什么?
一种在 TransactionScope 块内的数据库中插入潜在数千条记录的方法
您可以使用 .Net TransactionScope technet.microsoft.com/en-us/library/bb896149.aspx
A
Aleksa

这里写的所有解决方案都无济于事,因为当您执行 SaveChanges() 时,插入语句会被一一发送到数据库,这就是 Entity 的工作方式。

例如,如果您访问数据库并返回数据库的时间为 50 毫秒,那么插入所需的时间为记录数 x 50 毫秒。

您必须使用 BulkInsert,这是链接:https://efbulkinsert.codeplex.com/

通过使用它,我将插入时间从 5-6 分钟减少到 10-12 秒。


G
Greg R Taylor

另一种选择是使用 Nuget 提供的 SqlBulkTools。它非常易于使用并具有一些强大的功能。

例子:

var bulk = new BulkOperations();
var books = GetBooks();

using (TransactionScope trans = new TransactionScope())
{
    using (SqlConnection conn = new SqlConnection(ConfigurationManager
    .ConnectionStrings["SqlBulkToolsTest"].ConnectionString))
    {
        bulk.Setup<Book>()
            .ForCollection(books)
            .WithTable("Books") 
            .AddAllColumns()
            .BulkInsert()
            .Commit(conn);
    }

    trans.Complete();
}

有关更多示例和高级用法,请参阅 the documentation。免责声明:我是这个库的作者,任何观点都是我自己的观点。


此项目已从 NuGet 和 GitHub 中删除。
M
Michał Pilarek

[POSTGRESQL 的新解决方案] 嘿,我知道这是一篇很老的帖子,但我最近遇到了类似的问题,但我们使用的是 Postgresql。我想使用有效的bulkinsert,结果非常困难。我还没有在这个数据库上找到任何合适的免费库。我只找到了这个助手:https://bytefish.de/blog/postgresql_bulk_insert/,它也在 Nuget 上。我写了一个小映射器,它以实体框架的方式自动映射属性:

public static PostgreSQLCopyHelper<T> CreateHelper<T>(string schemaName, string tableName)
        {
            var helper = new PostgreSQLCopyHelper<T>("dbo", "\"" + tableName + "\"");
            var properties = typeof(T).GetProperties();
            foreach(var prop in properties)
            {
                var type = prop.PropertyType;
                if (Attribute.IsDefined(prop, typeof(KeyAttribute)) || Attribute.IsDefined(prop, typeof(ForeignKeyAttribute)))
                    continue;
                switch (type)
                {
                    case Type intType when intType == typeof(int) || intType == typeof(int?):
                        {
                            helper = helper.MapInteger("\"" + prop.Name + "\"",  x => (int?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type stringType when stringType == typeof(string):
                        {
                            helper = helper.MapText("\"" + prop.Name + "\"", x => (string)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type dateType when dateType == typeof(DateTime) || dateType == typeof(DateTime?):
                        {
                            helper = helper.MapTimeStamp("\"" + prop.Name + "\"", x => (DateTime?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type decimalType when decimalType == typeof(decimal) || decimalType == typeof(decimal?):
                        {
                            helper = helper.MapMoney("\"" + prop.Name + "\"", x => (decimal?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type doubleType when doubleType == typeof(double) || doubleType == typeof(double?):
                        {
                            helper = helper.MapDouble("\"" + prop.Name + "\"", x => (double?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type floatType when floatType == typeof(float) || floatType == typeof(float?):
                        {
                            helper = helper.MapReal("\"" + prop.Name + "\"", x => (float?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type guidType when guidType == typeof(Guid):
                        {
                            helper = helper.MapUUID("\"" + prop.Name + "\"", x => (Guid)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                }
            }
            return helper;
        }

我通过以下方式使用它(我有一个名为 Undertaking 的实体):

var undertakingHelper = BulkMapper.CreateHelper<Model.Undertaking>("dbo", nameof(Model.Undertaking));
undertakingHelper.SaveAll(transaction.UnderlyingTransaction.Connection as Npgsql.NpgsqlConnection, undertakingsToAdd));

我展示了一个带有事务的示例,但也可以通过从上下文中检索到的正常连接来完成。企业ToAdd 是可枚举的普通实体记录,我想将其批量插入到数据库中。

经过几个小时的研究和尝试,我得到了这个解决方案,正如您所期望的那样,它的速度要快得多,而且最终易于使用和免费!我真的建议你使用这个解决方案,不仅因为上面提到的原因,而且因为它是唯一一个我对 Postgresql 本身没有问题的解决方案,许多其他解决方案都可以完美地工作,例如 SqlServer。


S
Simon Hughes

秘诀是插入到相同的空白临时表中。插入物快速变亮。然后从中运行一个插入到您的主大表中。然后截断暂存表,为下一批做好准备。

IE。

insert into some_staging_table using Entity Framework.

-- Single insert into main table (this could be a tiny stored proc call)
insert into some_main_already_large_table (columns...)
   select (columns...) from some_staging_table
truncate table some_staging_table

使用 EF,将所有记录添加到一个空的临时表中。然后使用 SQL 在单个 SQL 指令中插入主(大而慢)表。然后清空您的临时表。这是将大量数据插入已经很大的表中的一种非常快速的方法。
当您说使用 EF 时,将记录添加到临时表中,您实际上是否尝试过使用 EF?由于 EF 每次插入都会对数据库发出单独的调用,我怀疑您将看到 OP 试图避免的相同性能命中。临时表如何避免这个问题?
R
Rafael A. M. S.

您是否曾经尝试过通过后台工作人员或任务插入?

在我的例子中,我插入了 7760 个寄存器,分布在 182 个具有外键关系的不同表中(通过 NavigationProperties)。

没有任务,花了2分半钟。在一个任务 ( Task.Factory.StartNew(...) ) 中,它需要 15 秒。

我只在将所有实体添加到上下文后才执行 SaveChanges()。 (确保数据完整性)


我很确定上下文不是线程安全的。您是否进行了测试以确保所有实体都已保存?
我知道整个实体框架根本不是线程安全的,但我只是将对象添加到上下文并在最后保存......它在这里完美运行。
所以,您在主线程中调用 DbContext.SaveChanges(),但是将实体添加到上下文是在后台线程中执行的,对吗?
是的,在线程内添加数据;等待全部完成;并在主线程中保存更改
虽然我觉得这种方式很危险,容易出错,但我觉得很有趣。
L
Leandro Bardelli

记下几条笔记,这是我的改进实现以及其他答案和评论。

改进:

从我的实体获取 SQL 连接字符串

仅在某些部分使用 SQLBulk,其余仅 Entity Framework

使用与 SQL 数据库相同的 Datetable 列名,无需映射每一列

使用与 SQL 数据表相同的数据表名称 public void InsertBulkDatatable(DataTable dataTable) { EntityConnectionStringBuilder entityBuilder = new EntityConnectionStringBuilder(ConfigurationManager.ConnectionStrings["MyDbContextConnectionName"].ConnectionString);字符串 cs = entityBuilder.ProviderConnectionString;使用 (var connection = new SqlConnection(cs)) { SqlTransaction transaction = null;连接.打开();尝试 { 交易 = 连接.BeginTransaction();使用 (var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, transaction)) { sqlBulkCopy.DestinationTableName = dataTable.TableName; //使用SQL数据表命名c#中的数据表 //Maping Columns foreach (DataColumn column in dataTable.Columns) { sqlBulkCopy.ColumnMappings.Add(column.ColumnName, column.ColumnName); } sqlBulkCopy.WriteToServer(dataTable); } 事务。提交(); } 捕捉(异常){ 事务。回滚(); } } }


N
Nadeem

您可以使用 Bulk package 库。 Bulk Insert 1.0.0 版本用于具有 Entity framework >=6.0.0 的项目。

更多描述可以在这里找到 - Bulkoperation source code


C
Ciro Corvino

TL; DR 我知道这是一篇旧帖子,但我已经实施了一个解决方案,从提出的一个解决方案开始,通过扩展它并解决一些问题;此外,我还阅读了提出的其他解决方案,并且与这些解决方案相比,我似乎提出了一个更适合原始问题中提出的要求的解决方案。

在这个解决方案中,我扩展了 Slauma's approach,我认为它非常适合原始问题中提出的案例,即使用实体框架和事务范围在数据库上进行昂贵的写入操作。

在 Slauma 的解决方案中 - 顺便说一下,这是一个草稿,仅用于通过实施批量插入的策略来了解 EF 的速度 - 存在以下问题:

交易超时(默认为 1 分钟,可通过代码延长至最多 10 分钟);复制第一个数据块的宽度等于事务结束时使用的提交的大小(这个问题很奇怪,可以通过变通方法规避)。

我还扩展了 Slauma 提出的案例研究,报告了一个示例,其中包括几个依赖实体的上下文插入。

我能够验证的性能是 10K rec/min 在数据库中插入一个 200K 宽的记录块,每个记录大约 1KB。速度恒定,性能没有下降,测试大约需要 20 分钟才能成功运行。

详细解决方案

主持插入示例存储库类中的批量插入操作的方法:

abstract class SomeRepository { 

    protected MyDbContext myDbContextRef;

    public void ImportData<TChild, TFather>(List<TChild> entities, TFather entityFather)
            where TChild : class, IEntityChild
            where TFather : class, IEntityFather
    {

        using (var scope = MyDbContext.CreateTransactionScope())
        {

            MyDbContext context = null;
            try
            {
                context = new MyDbContext(myDbContextRef.ConnectionString);

                context.Configuration.AutoDetectChangesEnabled = false;

                entityFather.BulkInsertResult = false;
                var fileEntity = context.Set<TFather>().Add(entityFather);
                context.SaveChanges();

                int count = 0;

                //avoids an issue with recreating context: EF duplicates the first commit block of data at the end of transaction!!
                context = MyDbContext.AddToContext<TChild>(context, null, 0, 1, true);

                foreach (var entityToInsert in entities)
                {
                    ++count;
                    entityToInsert.EntityFatherRefId = fileEntity.Id;
                    context = MyDbContext.AddToContext<TChild>(context, entityToInsert, count, 100, true);
                }

                entityFather.BulkInsertResult = true;
                context.Set<TFather>().Add(fileEntity);
                context.Entry<TFather>(fileEntity).State = EntityState.Modified;

                context.SaveChanges();
            }
            finally
            {
                if (context != null)
                    context.Dispose();
            }

            scope.Complete();
        }

    }

}

仅用于示例目的的接口:

public interface IEntityChild {

    //some properties ...

    int EntityFatherRefId { get; set; }

}

public interface IEntityFather {

    int Id { get; set; }
    bool BulkInsertResult { get; set; }
}

db 上下文,我将解决方案的各种元素实现为静态方法:

public class MyDbContext : DbContext
{

    public string ConnectionString { get; set; }


    public MyDbContext(string nameOrConnectionString)
    : base(nameOrConnectionString)
    {
        Database.SetInitializer<MyDbContext>(null);
        ConnectionString = Database.Connection.ConnectionString;
    }


    /// <summary>
    /// Creates a TransactionScope raising timeout transaction to 30 minutes
    /// </summary>
    /// <param name="_isolationLevel"></param>
    /// <param name="timeout"></param>
    /// <remarks>
    /// It is possible to set isolation-level and timeout to different values. Pay close attention managing these 2 transactions working parameters.
    /// <para>Default TransactionScope values for isolation-level and timeout are the following:</para>
    /// <para>Default isolation-level is "Serializable"</para>
    /// <para>Default timeout ranges between 1 minute (default value if not specified a timeout) to max 10 minute (if not changed by code or updating max-timeout machine.config value)</para>
    /// </remarks>
    public static TransactionScope CreateTransactionScope(IsolationLevel _isolationLevel = IsolationLevel.Serializable, TimeSpan? timeout = null)
    {
        SetTransactionManagerField("_cachedMaxTimeout", true);
        SetTransactionManagerField("_maximumTimeout", timeout ?? TimeSpan.FromMinutes(30));

        var transactionOptions = new TransactionOptions();
        transactionOptions.IsolationLevel = _isolationLevel;
        transactionOptions.Timeout = TransactionManager.MaximumTimeout;
        return new TransactionScope(TransactionScopeOption.Required, transactionOptions);
    }

    private static void SetTransactionManagerField(string fieldName, object value)
    {
        typeof(TransactionManager).GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static).SetValue(null, value);
    }


    /// <summary>
    /// Adds a generic entity to a given context allowing commit on large block of data and improving performance to support db bulk-insert operations based on Entity Framework
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="context"></param>
    /// <param name="entity"></param>
    /// <param name="count"></param>
    /// <param name="commitCount">defines the block of data size</param>
    /// <param name="recreateContext"></param>
    /// <returns></returns>
    public static MyDbContext AddToContext<T>(MyDbContext context, T entity, int count, int commitCount, bool recreateContext) where T : class
    {
        if (entity != null)
            context.Set<T>().Add(entity);

        if (count % commitCount == 0)
        {
            context.SaveChanges();
            if (recreateContext)
            {
                var contextConnectionString = context.ConnectionString;
                context.Dispose();
                context = new MyDbContext(contextConnectionString);
                context.Configuration.AutoDetectChangesEnabled = false;
            }
        }

        return context;
    }
}

S
SelcukBah

Configuration.LazyLoadingEnabled = false; Configuration.ProxyCreationEnabled = false;

如果没有 AutoDetectChangesEnabled = false,这些对速度影响太大;我建议使用来自 dbo 的不同表头。通常我使用像 nop、sop、tbl 等。