我正在寻找插入实体框架的最快方法。
我之所以问这个问题是因为您有一个活动的 TransactionScope
并且插入量很大(4000+)。它可能会持续超过 10 分钟(事务的默认超时),这将导致事务不完整。
对于您在问题评论中的评论:
“...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 个实体处。)这种非线性行为在所有其他测试中并不那么重要。
这种组合可以很好地提高速度。
context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ValidateOnSaveEnabled = false;
您应该考虑为此使用 System.Data.SqlClient.SqlBulkCopy
。这里是 documentation,当然还有很多在线教程。
抱歉,我知道您正在寻找一个简单的答案来让 EF 做您想做的事,但是批量操作并不是 ORM 的真正用途。
最快的方法是使用我开发的 bulk insert extension
注意:这是一个商业产品,不是免费的
https://i.stack.imgur.com/AdWD3.png
用法非常简单
context.BulkInsert(hugeAmountOfEntities);
因为这里从未提到过,所以我想推荐 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);
我同意亚当·拉基斯的观点。 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();
}
}
}
AsDataReader()
扩展方法,在这个答案中解释:stackoverflow.com/a/36817205/1507899
我会推荐这篇关于如何使用 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 范围内)
我已经调查了 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 是最优的,也找不到任何合乎逻辑的解释。
[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
正如其他人所说,如果您想要非常好的插入性能,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>
的自动查找集合的通用版本 InsertAll
和 UpdateAll
也会很好。
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()
包装它,这里不显示它们以保持代码干净
我知道这是一个非常古老的问题,但是这里的一个人说开发了一种扩展方法来使用 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);
我正在寻找插入实体框架的最快方法
有一些支持 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;
});
保存列表的最快方法之一,您必须应用以下代码
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();
是的,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();
}
}
}
}
}
尝试使用存储过程来获取您要插入的数据的 XML。
我对上面@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();
}
}
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;
}
}
}
以下是在实际示例中使用 Entity Framework 和使用 SqlBulkCopy 类之间的性能比较:How to Bulk Insert Complex Objects into SQL Server Database
正如其他人已经强调的那样,ORM 并不打算用于批量操作。它们提供了灵活性、关注点分离和其他好处,但批量操作(批量读取除外)不是其中之一。
使用 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;
}
据我所知,EntityFramework
中有 no BulkInsert
可提高巨大插入的性能。
在这种情况下,您可以使用 ADO.net
中的 SqlBulkCopy 来解决您的问题
WriteToServer
,需要一个 DataTable
。
这里写的所有解决方案都无济于事,因为当您执行 SaveChanges() 时,插入语句会被一一发送到数据库,这就是 Entity 的工作方式。
例如,如果您访问数据库并返回数据库的时间为 50 毫秒,那么插入所需的时间为记录数 x 50 毫秒。
您必须使用 BulkInsert,这是链接:https://efbulkinsert.codeplex.com/
通过使用它,我将插入时间从 5-6 分钟减少到 10-12 秒。
另一种选择是使用 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。免责声明:我是这个库的作者,任何观点都是我自己的观点。
[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。
秘诀是插入到相同的空白临时表中。插入物快速变亮。然后从中运行一个插入到您的主大表中。然后截断暂存表,为下一批做好准备。
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
您是否曾经尝试过通过后台工作人员或任务插入?
在我的例子中,我插入了 7760 个寄存器,分布在 182 个具有外键关系的不同表中(通过 NavigationProperties)。
没有任务,花了2分半钟。在一个任务 ( Task.Factory.StartNew(...)
) 中,它需要 15 秒。
我只在将所有实体添加到上下文后才执行 SaveChanges()
。 (确保数据完整性)
记下几条笔记,这是我的改进实现以及其他答案和评论。
改进:
从我的实体获取 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); } 事务。提交(); } 捕捉(异常){ 事务。回滚(); } } }
您可以使用 Bulk package 库。 Bulk Insert 1.0.0 版本用于具有 Entity framework >=6.0.0 的项目。
更多描述可以在这里找到 - Bulkoperation source code
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;
}
}
Configuration.LazyLoadingEnabled = false; Configuration.ProxyCreationEnabled = false;
如果没有 AutoDetectChangesEnabled = false,这些对速度影响太大;我建议使用来自 dbo 的不同表头。通常我使用像 nop、sop、tbl 等。
AutoDetectChangesEnabled = false;
。它还具有很大的附加性能影响:stackoverflow.com/questions/5943394/…DbContext
,而不是ObjectContext
?