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

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

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

533705 次浏览

您应该考虑为此使用System.Data.SqlClient.SqlBulkCopy。这是留档,当然网上有很多教程。

对不起,我知道你一直在寻找一个简单的答案,让EF做你想做的事情,但批量操作并不是ORM的真正目的。

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

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

在这种情况下,您可以使用SqlBulkCopy所有文档 inADO.net来解决您的问题

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

即。

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_tabletruncate table some_staging_table

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

"…保存更改(为每个记录)…"

这是你能做的最糟糕的事情!为每条记录调用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()很重要。它还提高了在Save更改后处理上下文并创建一个新上下文的性能。这会清除所有实体的上下文,SaveChanges不会这样做,实体仍然在状态Unchanged中附加到上下文。上下文中附加实体的大小越来越大,从而减慢了插入速度。所以,过一段时间后清除它是有帮助的。

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

  • 提交计数=1,重新创建上下文=假:许多小时(这是您当前的过程)
  • Context=false:超过20分钟//初始化请求失败
  • Context=1000,//重新创建Context=false:242秒//请求参数
  • 202秒//提交数=10000,重新创建上下文
  • Context=100000,//重新创建Context=false:199秒
  • Context=true:超过10分钟//请求参数
  • Context=true:241秒//请求参数
  • Context=true:164秒//请求计数
  • Context=1000, recreateContext=true:191秒//请求参数

上面第一个测试中的行为是性能非常非线性,并且随着时间的推移而急剧下降(“许多小时”是一个估计,我从未完成过这个测试,20分钟后我停在了50.000个实体上)。

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

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

我同意Adam Rackis的观点。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();}
}}

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

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

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

public void AddAndSave<TEntity>(List<TEntity> entities) where TEntity : class {
const int CommitCount = 1000; //set your own best performance number hereint currentCount = 0;
while (currentCount < entities.Count()){//make sure it don't commit more than the entities you haveint 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 conextfor (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 contextfor (int i = currentCount; i < (currentCount + commitCount); i++)_context.Entry(entities[i]).State = System.Data.EntityState.Detached;
currentCount += commitCount;} }

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

我研究了Slauma的答案(太棒了,谢谢你的想法),我减少了批处理大小,直到达到最佳速度。看看Slauma的结果:

  • 提交计数=1,重新创建上下文=真:超过10分钟
  • 提交计数=10,重新创建上下文=true: 241秒
  • //提交计数=100,重新创建上下文=true: 164秒
  • //提交计数=1000,重新创建上下文=true: 191秒

可以看出,从1移动到10和从10移动到100时速度会增加,但是从100到1000的插入速度又下降了。

因此,我专注于当您将批处理大小减小到10到100之间的某个值时会发生什么,以下是我的结果(我使用不同的行内容,因此我的时间具有不同的值):

Quantity    | Batch size    | Interval1000    1   310000   1   34100000  1   368
1000    5   110000   5   12100000  5   133
1000    10  110000   10  11100000  10  101
1000    20  110000   20  9100000  20  92
1000    27  010000   27  9100000  27  92
1000    30  010000   30  9100000  30  92
1000    35  110000   35  9100000  35  94
1000    50  110000   50  10100000  50  106
1000    100 110000   100 14100000  100 141

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

你有没有尝试过通过后台工作人员或任务插入?

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

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

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

以下是在实际示例中使用Entity Framework和使用SqlBulkCopy类之间的性能比较:如何批量插入复杂对象到SQL服务器数据库

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

最快的方法是使用我开发的批量插入扩展

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

它使用SqlBulkCopy和自定义datareader来获得最大性能。因此,它比使用常规插入或AddRange快20倍以上EntityFramework. BulkIn vs EF AddRange

用法非常简单

context.BulkInsert(hugeAmountOfEntities);

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

实体框架和慢批量INSERT

他探讨了这些领域并比较了业绩:

  1. 默认EF(57分钟完成添加30,000条记录)
  2. 替换为ADO.NET代码(25对于相同的30,000)
  3. 上下文膨胀-通过为每个工作单元使用新的上下文来保持活动的上下文图小(相同的30,000个插入需要33秒)
  4. 大列表-关闭自动检测更改启用(使时间减少到大约20秒)
  5. 批处理(缩短至16秒)
  6. DbTable. AddRange()-(性能在12范围内)

正如其他人所说,如果你想要真正好的插入性能,SqlBulkCopy是这样做的。

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

您需要的唯一代码是:

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

那么它有多快呢?很难说,因为它取决于很多因素,机器性能,网络,对象大小等。我所做的性能测试表明,如果你像其他答案中提到的那样优化EF配置,25k实体可以以大约10秒的速度插入localhost。使用EFU工具大约需要300毫秒。更有趣的是,我使用这种方法在15秒内保存了大约300万个实体,平均每秒大约200k个实体。

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

这里写的所有解决方案都没有帮助,因为当您执行SaveChange()时,插入语句会一个接一个地发送到数据库,这就是Entity的工作方式。

例如,如果您往返数据库的时间为50毫秒,则插入所需的时间为记录数x 50毫秒。

你必须使用BulkInett,这里是链接:https://efbulkinsert.codeplex.com/

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

我对上面的@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();}}

另一种选择是使用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();}

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

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

有一些支持批量插入的第三方库可用:

  • Z. EntityFramework(推荐)//实体框架
  • 公用事业
  • 实体框架。批量插入

见:实体框架批量插入库

选择批量插入库时要小心。只有实体框架扩展支持所有类型的关联和继承,它是唯一仍然受支持的。


免责声明:我是实体框架扩展的主人

此库允许您执行场景所需的所有批量操作:

  • 批量保存更改
  • 批量插入
  • 批量删除
  • 批量更新
  • 批量合并

示例

// Easy to usecontext.BulkSaveChanges();
// Easy to customizecontext.BulkSaveChanges(bulk => bulk.BatchSize = 100);
// Perform Bulk Operationscontext.BulkDelete(customers);context.BulkInsert(customers);context.BulkUpdate(customers);
// Customize Primary Keycontext.BulkMerge(customers, operation => {operation.ColumnPrimaryKeyExpression =customer => customer.Code;});

您可以使用散装库。批量插入1.0.0版本用于实体框架>=6.0.0的项目。

更多描述可以在这里找到-批量操作源代码

使用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;}

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

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;}

我使用它的方式如下(我有实体名为承接):

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

我展示了一个事务示例,但它也可以通过从上下文中检索的正常连接来完成。

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

但是,对于超过(+4000)的插入,我建议使用存储过程。我没有插入它11.788行在20"输入图片描述

这就是代码

 public void InsertDataBase(MyEntity entity){repository.Database.ExecuteSqlCommand("sp_mystored " +"@param1, @param2"new SqlParameter("@param1", entity.property1),new SqlParameter("@param2", entity.property2));}

我知道这是一个非常古老的问题,但是这里的一个人说他开发了一种扩展方法来使用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);

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

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

自动检测更改是否启用

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

ValidateOnSaveEnable=false;//初始化保存为空//初始化保存为空

未检测到更改跟踪器

您必须添加nuget

Install-Package Z.EntityFramework.Extensions

现在您可以使用以下代码

var context = new MyContext();
context.Configuration.AutoDetectChangesEnabled = false;context.Configuration.ValidateOnSaveEnabled = false;
context.BulkInsert(list);context.BulkSaveChanges();

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 thatmyDataTable.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;}}}

由于这里从未提到过,我想重新评论EFCore. BulkEx的这里

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

使用此技术可以提高在Entity Framework中插入记录的速度。这里我使用一个简单的存储过程来插入记录。为了执行此存储过程,我使用. Entity Framework的FromSql()方法来执行RawSQL。

存储过程代码:

CREATE PROCEDURE TestProc@FirstParam VARCHAR(50),@SecondParam VARCHAR(50)
ASInsert into SomeTable(Name, Address) values(@FirstParam, @SecondParam)GO

接下来,遍历所有4000条记录并添加执行存储的Entity Framework代码

过程每100次循环一次。

为此,我创建了一个字符串查询来执行此过程,继续将每组记录附加到它。

然后检查循环是否以100的倍数运行,在这种情况下,使用.FromSql()执行它。

所以对于4000条记录,我只需要执行这个过程4000/100=40倍。

检查下面的代码:

string execQuery = "";var context = new MyContext();for (int i = 0; i < 4000; i++){execQuery += "EXEC TestProc @FirstParam = 'First'" + i + "'', @SecondParam = 'Second'" + i + "''";
if (i % 100 == 0){context.Student.FromSql(execQuery);execQuery = "";}}

[2019更新]EF Core 3.1

按照上面所说的,在EF Core中禁用AutoDetectChange启用效果很好:插入时间除以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

太长别读我知道这是一个古老的职位,但我已经实现了一个解决方案,从其中一个建议开始,通过扩展它并解决其中的一些问题;此外,我还阅读了所提出的其他解决方案,与这些解决方案相比,我似乎提出了一个更适合原始问题中提出的要求的解决方案。

在这个解决方案中,我扩展了Slauma的方法,我想说它非常适合原始问题中提出的情况,即使用实体框架和事务范围对db进行昂贵的写入操作。

在Slauma的解决方案中——顺便提一下,它是一个草案,仅用于了解EF的速度和实现批量插入的策略——存在以下问题:

  1. 交易超时(默认为1分钟,可通过代码扩展到最大10分钟);
  2. 第一个数据块的重复,其宽度等于事务结束时使用的提交的大小(这个问题很奇怪,通过解决方法规避了这个问题)。

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

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

解决方案详细

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

abstract class SomeRepository {
protected MyDbContext myDbContextRef;
public void ImportData<TChild, TFather>(List<TChild> entities, TFather entityFather)where TChild : class, IEntityChildwhere 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; }}

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

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;}}

是的,SqlBulkUpdate确实是这种类型任务的最快工具。我想在. NET Core中为我找到“最省力”的通用方法,所以我最终使用来自Marc Gravell的大型图书馆称为Fast会员并为实体框架DB上下文编写了一个微小的扩展方法。工作快如闪电:

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();}}}}}

LazyLoadingConfiguration=false;延迟加载

配置文件

这些是太影响速度没有自动检测更改启用=假;我建议使用不同的表头从dbo。

注意几个注意事项,这是我的实现与改进我的和其他答案和评论。

改进点:

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

  • 仅在某些部分使用SQLBulk,其余部分仅使用实体框架

  • 使用与使用SQL数据库相同的数据表列名,而无需映射每个列

  • 使用相同的可数据化名称,使用SQL可数据化

    public  void InsertBulkDatatable(DataTable dataTable){EntityConnectionStringBuilder entityBuilder =  new EntityConnectionStringBuilder(ConfigurationManager.ConnectionStrings["MyDbContextConnectionName"].ConnectionString);string cs = entityBuilder.ProviderConnectionString;using (var connection = new SqlConnection(cs)){SqlTransaction transaction = null;connection.Open();try{transaction = connection.BeginTransaction();using (var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, transaction)){sqlBulkCopy.DestinationTableName = dataTable.TableName; //Uses the SQL datatable to name the datatable in c#//Maping Columnsforeach (DataColumn column in dataTable.Columns) {sqlBulkCopy.ColumnMappings.Add(column.ColumnName, column.ColumnName);
    }
    
    sqlBulkCopy.WriteToServer(dataTable);}transaction.Commit();}catch (Exception){transaction.Rollback();}
    }}