Самый быстрый способ вставки в Entity Framework
Я ищу самый быстрый способ вставки в Entity Framework.
Я спрашиваю об этом из-за сценария, где у вас есть активный TransactionScope, и вставка огромна (4000+). Это может потенциально длиться более 10 минут (время ожидания транзакций по умолчанию), и это приведет к незавершенной транзакции.
33 ответа
To your remark in the comments to your question:
"...SavingChanges (for each record)..."
That's the worst thing you can do! призвание SaveChanges()
for each record slows bulk inserts extremely down. I would do a few simple tests which will very likely improve the performance:
- Вызов
SaveChanges()
once after ALL records. - Вызов
SaveChanges()
after for example 100 records. - Вызов
SaveChanges()
after for example 100 records and dispose the context and create a new one. - Disable change detection
For bulk inserts I am working and experimenting with a pattern like this:
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 минуты.
Для выступления важно позвонить SaveChanges()
после "много" записей ("много" около 100 или 1000). Это также повышает производительность для удаления контекста после SaveChanges и создания нового. Это очищает контекст от всех энтузиастов, SaveChanges
не делает этого, объекты по-прежнему привязаны к контексту в состоянии Unchanged
, Это растущий размер присоединяемых объектов в контексте, который замедляет вставку шаг за шагом. Таким образом, полезно очистить его через некоторое время.
Вот несколько измерений для моих 560.000 объектов:
- commitCount = 1, refreshateContext = false: много часов (это ваша текущая процедура)
- commitCount = 100, воссоздать Context = false: более 20 минут
- commitCount = 1000, пересоздать Context = ложь: 242 сек
- commitCount = 10000, пересоздать Context = false: 202 сек
- commitCount = 100000, воссоздать Context = false: 199 секунд
- commitCount = 1000000, воссоздать Context = false: нехватка памяти
- commitCount = 1, воссоздать Context = true: более 10 минут
- commitCount = 10, пересоздать Context = true: 241 сек
- commitCount = 100, воссоздать Context = true: 164 сек
- commitCount = 1000, пересоздать Context = истина: 191 сек
Поведение в первом тесте, приведенном выше, заключается в том, что производительность очень нелинейная и сильно уменьшается со временем. ("Много часов" - это оценка, я никогда не заканчивал этот тест, я остановился на 50 000 объектов через 20 минут.) Это нелинейное поведение не столь существенно во всех других тестах.
Эта комбинация достаточно хорошо увеличивает скорость.
context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ValidateOnSaveEnabled = false;
Как это никогда не упоминалось здесь, я хочу рекомендовать EFCore.BulkExtensions здесь
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 и пользовательские устройства чтения данных, чтобы получить максимальную производительность. В результате это более чем в 20 раз быстрее, чем при использовании обычной вставки или AddRange
использование очень просто
context.BulkInsert(hugeAmountOfEntities);
Вы должны посмотреть на использование System.Data.SqlClient.SqlBulkCopy
за это. Вот документация, и, конечно, есть много онлайн-уроков.
Извините, я знаю, что вы искали простой ответ, чтобы заставить EF делать то, что вы хотите, но массовые операции не совсем то, для чего предназначены ORM.
Я согласен с Адамом Ракисом. 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();
}
}
}
[Обновление 2019] EF Core 3.1
Следуя тому, что было сказано выше, отключение AutoDetectChangesEnabled в EF Core сработало отлично: время вставки было разделено на 100 (от многих минут до нескольких секунд, 10 тыс. Записей с взаимосвязями между таблицами)
Обновленный код:
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
Я бы порекомендовал эту статью о том, как делать массовые вставки с использованием EF.
Entity Framework и медленные массовые вставки
Он исследует эти области и сравнивает производительность:
- EF по умолчанию (57 минут для добавления 30 000 записей)
- Замена кодом ADO.NET (25 секунд для тех же 30 000)
- Разброс контекста - сохраняйте размер активного графа контекста небольшим, используя новый контекст для каждой единицы работы (те же 30 000 вставок занимают 33 секунды)
- Большие списки - отключите AutoDetectChangesEnabled (сокращает время до 20 секунд)
- Пакетирование (до 16 секунд)
- DbTable.AddRange () - (производительность находится в диапазоне 12)
Как говорили другие люди, SqlBulkCopy - это способ сделать это, если вы хотите действительно хорошую производительность вставки.
Это немного громоздко для реализации, но есть библиотеки, которые могут вам в этом помочь. Есть несколько, но на этот раз я постыдно подключу свою собственную библиотеку: https://github.com/MikaelEliasson/EntityFramework.Utilities
Единственный код, который вам понадобится:
using (var db = new YourDbContext())
{
EFBatchOperation.For(db, db.BlogPosts).InsertAll(list);
}
Так насколько это быстрее? Трудно сказать, потому что это зависит от многих факторов, производительности компьютера, сети, размера объекта и т. Д. И т. Д. Проведенные мною тесты производительности показывают, что 25 тыс. Объектов можно вставлять примерно в 10 секунд стандартным способом на локальном хосте, если вы оптимизируете свою конфигурацию EF, например упоминается в других ответах. С EFUtilities это занимает около 300 мс. Еще более интересно то, что я сэкономил около 3 миллионов объектов менее чем за 15 секунд, используя этот метод, в среднем около 200 000 объектов в секунду.
Единственная проблема, конечно, если вам нужно вставить опубликованные данные. Это может быть эффективно выполнено на сервере sql с использованием описанного выше метода, но для этого требуется, чтобы у вас была стратегия генерации идентификаторов, которая позволяла бы генерировать идентификаторы в коде приложения для родительского элемента, чтобы вы могли устанавливать внешние ключи. Это можно сделать с помощью идентификаторов GUID или чего-то вроде создания идентификатора HiLo.
Я изучил ответ Слаумы (это здорово, спасибо за идею) и уменьшил размер партии, пока не достигну оптимальной скорости. Глядя на результаты Slauma:
- commitCount = 1, воссоздать Context = true: более 10 минут
- commitCount = 10, пересоздать Context = true: 241 сек
- commitCount = 100, воссоздать Context = true: 164 сек
- commitCount = 1000, пересоздать Context = истина: 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 оптимален, и я не смог найти логического объяснения этому.
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;
} }
оберните его попыткой поймать и 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))
{
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;
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);
Да,
SqlBulkUpdate
действительно самый быстрый инструмент для этого типа задач. Я хотел найти для себя общий способ с наименьшими усилиями в.NET Core, поэтому в итоге я использовал отличную библиотеку от Марка Гравелла под названием 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();
}
}
}
}
}
Один из самых быстрых способов сохранить список, вы должны применить следующий код
context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ValidateOnSaveEnabled = false;
AutoDetectChangesEnabled = false
Add, AddRange & SaveChanges: не обнаруживает изменений.
ValidateOnSaveEnabled = false;
Не обнаруживает изменения трекер
Вы должны добавить нюгет
Install-Package Z.EntityFramework.Extensions
Теперь вы можете использовать следующий код
var context = new MyContext();
context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ValidateOnSaveEnabled = false;
context.BulkInsert(list);
context.BulkSaveChanges();
Я ищу самый быстрый способ вставки в Entity Framework
Существует несколько сторонних библиотек, поддерживающих Bulk Insert:
- Z.EntityFramework.Extensions (рекомендуется)
- EFUtilities
- EntityFramework.BulkInsert
Смотрите: Entity Framework Библиотека массовой вставки
Будьте внимательны при выборе библиотеки массовой вставки. Только Entity Framework Extensions поддерживает все виды ассоциаций и наследований, и это единственный, который все еще поддерживается.
Отказ от ответственности: я владелец http://entityframework-extensions.net/
Эта библиотека позволяет вам выполнять все массовые операции, необходимые для ваших сценариев:
- Массовое сохранение изменений
- Массовая вставка
- Массовое удаление
- Массовое обновление
- Массовое слияние
пример
// 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;
});
Попробуйте использовать хранимую процедуру, которая получит 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 на реалистичном примере: Как массово вставить сложные объекты в базу данных SQL Server
Как уже подчеркивали другие, 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;
}
[НОВОЕ РЕШЕНИЕ ДЛЯ POSTGRESQL] Эй, я знаю, что это довольно старый пост, но недавно я столкнулся с подобной проблемой, но мы использовали Postgresql. Я хотел использовать эффективный булькинсерт, что оказалось довольно сложно. Я не нашел подходящей бесплатной библиотеки для этой базы данных. Я нашел только этого помощника: https://bytefish.de/blog/postgresql_bulk_insert/ который также находится на Nuget. Я написал небольшой преобразователь, который автоматически сопоставляет свойства способом Entity Framework:
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));
Я показал пример с транзакцией, но это также можно сделать с обычным соединением, полученным из контекста. takekingsToAdd - это перечисление обычных записей сущностей, которые я хочу добавить в БД.
Это решение, к которому я пришел после нескольких часов исследований и попыток, как вы могли ожидать, намного быстрее и, наконец, простое в использовании и бесплатное! Я действительно советую вам использовать это решение не только по причинам, указанным выше, но и потому, что оно единственное, с которым у меня не было проблем с самим Postgresql, многие другие решения работают безупречно, например с SqlServer.
Другой вариант - использовать SqlBulkTools, доступный от Nuget. Он очень прост в использовании и обладает некоторыми мощными функциями.
Пример:
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();
}
См. Документацию для большего количества примеров и продвинутого использования. Отказ от ответственности: я являюсь автором этой библиотеки, и любые взгляды имеют собственное мнение.
Все написанные здесь решения не помогают, потому что когда вы выполняете SaveChanges(), операторы вставки отправляются в базу данных один за другим, именно так работает Entity.
А если ваша поездка в базу данных и обратно составляет, например, 50 мс, то время, необходимое для вставки, равно числу записей x 50 мс.
Вы должны использовать BulkInsert, вот ссылка: https://efbulkinsert.codeplex.com/
Я использовал время вставки с 5-6 минут до 10-12 секунд, используя его.
Сделав несколько заметок, комментариев и ответов, это моя реализация, получение строки подключения SQL из моей Entity (я использую SQLBulk только в некоторых частях, остальные Entity Framework) и использование тех же имен столбцов Datetable, которые используют базу данных 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 Columns
foreach (DataColumn column in dataTable.Columns) {
sqlBulkCopy.ColumnMappings.Add(column.ColumnName, column.ColumnName);
}
sqlBulkCopy.WriteToServer(dataTable);
}
transaction.Commit();
}
catch (Exception)
{
transaction.Rollback();
}
}
}
Насколько мне известно, есть no BulkInsert
в EntityFramework
увеличить производительность огромных вкладышей.
В этом сценарии вы можете перейти с SqlBulkCopy в ADO.net
чтобы решить вашу проблему
Вы когда-нибудь пытались вставить через фоновый рабочий или задачу?
В моем случае я вставляю 7760 регистров, распределенных в 182 разных таблицах с отношениями внешних ключей (по NavigationProperties).
Без задачи это заняло 2 с половиной минуты. Внутри задачи (Task.Factory.StartNew(...)
), это заняло 15 секунд.
Я только делаю SaveChanges()
после добавления всех объектов в контекст. (для обеспечения целостности данных)
Секрет заключается в том, чтобы вставить в идентичный пустой промежуточный стол. Вставки быстро загораются. Затем запустите одну вставку из этого в ваш главный большой стол. Затем обрежьте промежуточный стол, готовый к следующей партии.
то есть.
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
Вы можете использовать пакетную библиотеку. Bulk Insert 1.0.0 version используется в проектах, имеющих Entity Framework >=6.0.0 .
Более подробное описание можно найти здесь - исходный код Bulkoperation
TL;DR Я знаю, что это старый пост, но я реализовал решение, начиная с одного из предложенных, расширив его и решив некоторые проблемы; кроме того, я также прочитал другие представленные решения, и по сравнению с ними мне кажется, что я предлагаю решение, которое намного больше подходит для запросов, сформулированных в исходном вопросе.
В этом решении я расширяю подход Slauma, который, я бы сказал, идеально подходит для случая, предложенного в исходном вопросе, а именно использовать Entity Framework и Transaction Scope для дорогостоящей операции записи в db.
В решении Slauma, которое, кстати, было черновиком и использовалось только для получения представления о скорости EF со стратегией реализации массовой вставки, были проблемы из-за:
- таймаут транзакции (по умолчанию 1 минута с возможностью продления с помощью кода до 10 минут);
- дублирование первого блока данных с шириной, равной размеру фиксации, используемой в конце транзакции (эта проблема довольно странная и ее можно обойти с помощью обходного пути).
Я также расширил тематическое исследование, представленное Slauma, сообщив о примере, который включает контекстную вставку нескольких зависимых сущностей.
Производительность, которую я смог проверить, составляла 10 Кбайт / мин при вставке в БД блока из 200 Кбайт записей шириной приблизительно 1 Кбайт каждая. Скорость была постоянной, производительность не снижалась, и тест длился около 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 и т. д.