Самый быстрый способ вставки в Entity Framework


682

Я ищу самый быстрый способ вставки в Entity Framework.

Я спрашиваю об этом из-за сценария, где у вас есть активный TransactionScope, и вставка огромна (4000+). Потенциально он может длиться более 10 минут (время ожидания транзакций по умолчанию), и это приведет к незавершенной транзакции.


1
Как ты сейчас это делаешь?
Дастин Лэйн

Создание TransactionScope, создание экземпляра DBContext, открытие соединения и выполнение оператора for-each для вставки и SavingChanges (для каждой записи), ПРИМЕЧАНИЕ. блок
Бонго Шарп

Еще один ответ для справки: stackoverflow.com/questions/5798646/…
Ладислав Мрнка

2
Самый быстрый способ вставки в базу данных SQL не требует EF. AFAIK Это BCP, затем TVP + слияние / вставка.
StingyJack

1
Для тех, кто будет читать комментарии: наиболее подходящий, современный ответ здесь.
Танвеер Бадар

Ответы:


986

К вашему замечанию в комментариях к вашему вопросу:

"... SavingChanges ( для каждой записи ) ..."

Это худшее, что вы можете сделать! Вызов SaveChanges()для каждой записи значительно замедляет массовые вставки. Я бы сделал несколько простых тестов, которые, скорее всего, улучшат производительность:

  • Звоните SaveChanges()один раз после ВСЕХ записей.
  • Позвоните SaveChanges()после, например, 100 записей.
  • Вызовите, SaveChanges()например, 100 записей, удалите контекст и создайте новый.
  • Отключить обнаружение изменений

Для массовых вставок я работаю и экспериментирую с таким шаблоном:

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. Это растущий размер присоединяемых объектов в контексте, который замедляет вставку шаг за шагом. Таким образом, полезно очистить его через некоторое время.

Вот несколько измерений для моих 560000 объектов:

  • commitCount = 1, refreshateContext = false: много часов (это ваша текущая процедура)
  • commitCount = 100, воссоздатьContext = false: более 20 минут
  • commitCount = 1000, пересоздатьContext = ложь: 242 сек
  • commitCount = 10000, пересоздатьContext = ложь: 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 = true: 191 сек

Поведение в первом тесте, приведенном выше, заключается в том, что производительность очень нелинейная и сильно снижается со временем. («Много часов» - оценка, я никогда не заканчивал этот тест, я остановился на 50 000 сущностей через 20 минут.) Это нелинейное поведение не столь существенно во всех других тестах.


89
@ Бонго Шарп: Не забудьте установить AutoDetectChangesEnabled = false;на DbContext. Он также имеет большой дополнительный эффект производительности: stackoverflow.com/questions/5943394/…
Slauma

6
Да, проблема в том, что я использую Entity Framework 4, а AutoDetectChangesEnabled является частью 4.1, тем не менее, я провел тест производительности и получил УДИВИТЕЛЬНЫЕ РЕЗУЛЬТАТЫ, он прошел с 00:12:00 до 00:00:22 SavinChanges на каждом объекте делали огромную нагрузку ... СПАСИБО так много за ваш ответ! это то, что я искал
Бонго Шарп

10
Спасибо за контекст .Configuration.AutoDetectChangesEnabled = false; совет, это имеет огромное значение.
Дуглаз

1
@ dahacker89: Вы используете правильную версию EF> = 4.1 и DbContext, НЕТ ObjectContext?
Слаума

3
@ dahacker89: Я предлагаю вам создать отдельный вопрос для вашей проблемы, возможно, с более подробной информацией. Я не могу понять, что здесь не так.
Слаума

176

Эта комбинация достаточно хорошо увеличивает скорость.

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

46
Не отключайте вслепую ValidateOnSaveEnabled, вы можете зависеть от этого поведения, и не осознайте это, пока не станет слишком поздно. С другой стороны, вы, возможно, выполняете проверку в другом месте кода, и повторная проверка EF совершенно не нужна.
Джереми Кук

1
В моем тесте сохранение 20 000 строк уменьшилось с 101 до 88 секунд. Не много и каковы последствия.
Ах.

27
@JeremyCook Я думаю, что вы пытаетесь понять, что этот ответ был бы намного лучше, если бы он объяснил возможные последствия изменения этих свойств по сравнению со значениями по умолчанию (кроме повышения производительности). Согласен.
псевдокодер

1
Это сработало для меня, хотя, если вы обновляете записи в контексте, вам нужно явно вызвать DetectChanges ()
hillstuk

2
Их можно отключить, а затем снова включить с помощью блока try-finally: msdn.microsoft.com/en-us/data/jj556205.aspx
yellavon

98

Самый быстрый способ будет использовать расширение массовой вставки , которое я разработал

примечание: это коммерческий продукт, не бесплатный

Он использует SqlBulkCopy и пользовательские устройства чтения данных, чтобы получить максимальную производительность. В результате это более чем в 20 раз быстрее, чем при использовании обычной вставки или AddRange. EntityFramework.BulkInsert против EF AddRange

использование очень просто

context.BulkInsert(hugeAmountOfEntities);

10
Быстро, но только верхний слой иерархии.
САПР bloke

66
Это не бесплатно.
Амир Саниян

72
Реклама становится умнее ... это платный продукт и очень дорогой для фрилансера. Имейте в виду!
JulioQc

35
600 долларов США за 1 год поддержки и обновлений? Ты что, с ума сошел?
Камило Теревинто

7
я больше не владелец продукта
maxlego

83

Вы должны посмотреть на использование System.Data.SqlClient.SqlBulkCopyдля этого. Вот документация , и, конечно, есть много учебных пособий онлайн.

Извините, я знаю, что вы искали простой ответ, чтобы заставить EF делать то, что вы хотите, но массовые операции не совсем то, для чего предназначены ORM.


1
Я пару раз сталкивался с SqlBulkCopy при исследовании этого, но, похоже, он больше ориентирован на вставки из таблицы в таблицу, к сожалению, я ожидал не простых решений, а скорее советов по производительности, таких как, например, управление состоянием соединение вручную, позволяя EF сделать это за вас
Bongo Sharp

7
Я использовал SqlBulkCopy для вставки больших объемов данных прямо из моего приложения. Вы в основном должны создать DataTable, заполнить ее, а затем передать , что в BulkCopy. Есть несколько ошибок, когда вы настраиваете свой DataTable (большинство из которых я забыл, к сожалению), но он должен работать просто отлично
Адам Ракис

2
Я сделал проверку концепции, и, как и обещал, он работает очень быстро, но одна из причин, почему я использую EF, заключается в том, что вставка реляционных данных проще, например, если я вставляю сущность, которая уже содержит реляционные данные , он также вставит его, вы когда-нибудь попадали в этот сценарий? Спасибо!
Бонго Шарп

2
К сожалению, вставка сети объектов в СУБД на самом деле не то, что делает BulkCopy. В этом преимущество ORM, такого как EF, а стоимость заключается в том, что он не сможет эффективно выполнять сотни подобных графов объектов.
Адам Рэкис

2
SqlBulkCopy - определенно правильный путь, если вам нужна грубая скорость или вы будете перезапускать эту вставку. Я вставил несколько миллионов записей раньше, и это очень быстро. Тем не менее, если вам не потребуется перезапустить эту вставку, может быть проще использовать EF.
Нил

49

Я согласен с Адамом Рэкисом. 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();
        }

    }
}

1
Я попробовал многие из решений, представленных в этом посте, и SqlBulkCopy был самым быстрым. Чистый EF занял 15 минут, но со смесью раствора и SqlBulkCopy мне удалось получить до 1,5 минут! Это было с 2 миллионами записей! Без какой-либо оптимизации индекса БД.
Джонас

Список проще, чем DataTable. В AsDataReader()этом ответе описан метод расширения: stackoverflow.com/a/36817205/1507899
RJB,

Но это только для высшего
Захид Мустафа

1
@ZahidMustafa: да. Он выполняет BulkInsert, а не Bulk-Analysis-And-Relation-Tracing-On-Object-Graphs ... если вы хотите охватить отношения, вы должны проанализировать и определить порядок вставки, а затем выполнить массовую вставку отдельных уровней и, возможно, обновить некоторые ключи как необходимо, и вы получите быстрое индивидуальное решение. Или, вы можете положиться на EF, чтобы сделать это, никакой работы на вашей стороне, но медленнее во время выполнения.
Кетцалькоатль

23

Я бы порекомендовал эту статью о том, как делать массовые вставки с использованием EF.

Entity Framework и медленные массовые вставки

Он исследует эти области и сравнивает производительность:

  1. EF по умолчанию (57 минут для завершения добавления 30 000 записей)
  2. Замена кодом ADO.NET (25 секунд для тех же 30 000)
  3. Разброс контекста - сохраняйте размер активного графа контекста небольшим, используя новый контекст для каждой единицы работы (те же 30 000 вставок занимают 33 секунды)
  4. Большие списки - отключите AutoDetectChangesEnabled (сокращает время до 20 секунд)
  5. Пакетирование (до 16 секунд)
  6. DbTable.AddRange () - (производительность находится в диапазоне 12)

21

как это никогда не упоминалось здесь, я хочу рекомендовать 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);

1
Я второе это предложение. После того, как я попробовал много домашних решений, это сократило мою вставку до 1 секунды с 50 секунд. И это лицензия MIT, которую легко включить.
SouthShoreAK

эта польза для ef 6.x
Alok

это более производительно, чем использование AddRange, если в нем более 10 объектов
Jackal

5
10 000 вставок ушло от 9 минут до 12 секунд. Это заслуживает большего внимания!
Каллисто

2
Если есть какой-либо способ изменить принятые ответы, это должен быть современный принятый ответ сейчас. И я бы хотел, чтобы команда EF предоставила это из коробки.
Танвеер Бадар

18

Я изучил ответ Слаумы (это потрясающе, спасибо за идею), и я уменьшал размер партии, пока не достиг оптимальной скорости. Глядя на результаты Slauma:

  • commitCount = 1, воссоздатьContext = true: более 10 минут
  • commitCount = 10, воссоздатьContext = true: 241 сек
  • commitCount = 100, воссоздатьContext = true: 164 сек
  • commitCount = 1000, воссоздатьContext = 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 оптимален, и я не смог найти логического объяснения этому.


2
Я нашел то же самое с Postrges и чистым SQL (это зависит от SQL, а не от EF), что 30 является оптимальным.
Камиль Гареев

Мой опыт показывает, что оптимум отличается для разных скоростей соединения и размера строки. Для быстрого соединения и небольших рядов оптимально может быть даже> 200 рядов.
Цзин

18

Как говорили другие люди, SqlBulkCopy - это способ сделать это, если вы хотите действительно хорошую производительность вставки.

Это немного громоздко для реализации, но есть библиотеки, которые могут вам в этом помочь. Есть несколько, но на этот раз я постыдно подключу свою собственную библиотеку: https://github.com/MikaelEliasson/EntityFramework.Utilities#batch-insert-entities

Единственный код, который вам понадобится:

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

Так насколько это быстрее? Трудно сказать, потому что это зависит от множества факторов, производительности компьютера, сети, размера объекта и т. Д. И т. Д. Проведенные мною тесты производительности показывают, что 25 тыс. Объектов можно вставить примерно в 10 секунд стандартным способом. на локальном хосте, если вы оптимизируете свою конфигурацию EF, например упоминается в других ответах. С EFUtilities это занимает около 300 мс. Еще интереснее то, что я сэкономил около 3 миллионов объектов менее чем за 15 секунд, используя этот метод, в среднем около 200 000 объектов в секунду.

Единственная проблема, конечно, если вам нужно вставить опубликованные данные. Это может быть эффективно выполнено на сервере sql с использованием описанного выше метода, но для этого требуется, чтобы у вас была стратегия генерации идентификаторов, которая позволяла бы генерировать идентификаторы в коде приложения для родительского элемента, чтобы вы могли устанавливать внешние ключи. Это можно сделать с помощью идентификаторов GUID или чего-то вроде создания идентификатора HiLo.


Работает хорошо. Хотя синтаксис немного многословен. Думаю, было бы лучше, если бы EFBatchOperationимел конструктор, который вы передаете в, DbContextа не в каждый статический метод. Общие версии InsertAllи UpdateAllкоторые автоматически находят коллекцию, похоже DbContext.Set<T>, тоже подойдут.
kjbartel

Просто быстрый комментарий, чтобы сказать спасибо! Этот код позволил мне сохранить 170 тыс. Записей за 1,5 секунды! Полностью уносит любой другой метод, который я пробовал, из воды.
Том Гленн

@Mikael Одна проблема связана с полями идентичности. У вас есть способ включить вставку личности?
Джо Филлипс

1
В отличие от EntityFramework.BulkInsert, эта библиотека оставалась бесплатной. +1
Rudey

14

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()командой try catch и, если вам нужно, не показывайте их здесь для поддержания чистоты кода


1
Это замедлило вставку (AddRange) с использованием Entity Framework 6.0. Вставка 20 000 строк увеличилась с 101 до 118 секунд.
Ах.

1
@ Стефен Хо: Я также пытаюсь избежать избавления от моего контекста. Я могу понять, что это медленнее, чем воссоздание контекста, но я хочу знать, если вы нашли это достаточно быстро, чем не воссоздание контекста, но с установленным значением commitCount.
Учащийся

@Learner: я думаю, что это было быстрее, чем воссоздавать контекст. Но я не помню сейчас, потому что я наконец-то перешел на использование SqlBulkCopy.
Стивен Хо

В итоге мне пришлось использовать эту технику, потому что по какой-то странной причине во время второго прохода в цикле while происходило некоторое остаточное отслеживание, хотя у меня все было обернуто в операторе using и даже вызвано Dispose () в DbContext , Когда я добавляю к контексту (на втором проходе), счетчик набора контекста переходит к 6 вместо одного. Другие элементы, которые были добавлены произвольно, уже были вставлены во время первого прохода в цикле while, поэтому вызов SaveChanges потерпит неудачу во втором проходе (по очевидным причинам).
Hallmanac

9

Я знаю, что это очень старый вопрос, но один парень сказал, что разработал метод расширения для использования массовой вставки с 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);

Пожалуйста, заполните ваш пример кода. где находится массовая копия
Seabizkit

1
Это уже здесь:await bulkCopy.WriteToServerAsync(table);
Гильерме

Может быть, я не совсем понял, в своей статье вы предлагаете сделать расширение ... которое я имел в виду, что никакой третьей части lib не требовалось, хотя на самом деле в обоих методах используется SqlBulkCopy lib. Это полностью зависит от SqlBulkCopy, когда я спросил, откуда взялась массовая копия, это библиотека расширений, над которой вы написали библиотеку расширений. Было бы просто более разумно сказать, вот как я использовал SqlBulkCopy lib.
Seabizkit

следует использовать conn.OpenAsync в асинхронной версии
Robert

6

Попробуйте использовать хранимую процедуру , которая получит XML-данные, которые вы хотите вставить.


9
Передача данных в формате XML не требуется, если вы не хотите хранить их в формате XML. В SQL 2008 вы можете использовать табличный параметр.
Ладислав Мрнка

я не уточнил это, но мне нужно также поддерживать SQL 2005
Бонго Шарп

4

Я сделал общее расширение примера @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();
    }
}

4

Я ищу самый быстрый способ вставки в Entity Framework

Доступны некоторые сторонние библиотеки, поддерживающие Bulk Insert:

  • Z.EntityFramework.Extensions ( рекомендуется )
  • EFUtilities
  • EntityFramework.BulkInsert

Смотрите: Entity Framework Библиотека массовой вставки

Будьте внимательны при выборе библиотеки массовой вставки. Только Entity Framework Extensions поддерживает все виды ассоциаций и наследований, и пока поддерживается только одна.


Отказ от ответственности : я владелец Entity Framework Extensions

Эта библиотека позволяет вам выполнять все массовые операции, необходимые для ваших сценариев:

  • Массовые SaveChanges
  • Массовая вставка
  • Массовое удаление
  • Массовое обновление
  • Массовое слияние

пример

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

19
это отличное расширение, но не бесплатное .
Окан Коцигит

2
Этот ответ довольно хорош, и EntityFramework.BulkInsert выполняет массовую вставку 15K строк за 1,5 секунды, довольно хорошо работает для внутреннего процесса, такого как служба Windows.
Пастор Кортес

4
Ага, 600 $ за объемную вставку. Всего стоит.
Eocron

1
@eocron Yeat это того стоит, если вы используете его в коммерческих целях. Я не вижу проблем с 600 долларами за то, что мне не нужно тратить часы на его создание, что обойдется мне намного больше, чем 600 долларов. Да, это стоит денег, но, глядя на мою почасовую ставку, это хорошо потраченные деньги!
Джорди ван Эйк

3

Используйте 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;
}

3

Один из самых быстрых способов сохранить список, вы должны применить следующий код

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

я могу использовать Ваш образец кода для массового обновления?
AminGolmahalle

4
Библиотека Z не бесплатна
SHADOW.NET

3

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

3

[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

2

Вот реалистичное сравнение производительности между использованием Entity Framework и классом SqlBulkCopy: Как массово вставить сложные объекты в базу данных SQL Server

Как уже подчеркивали другие, ORM не предназначены для массовых операций. Они предлагают гибкость, разделение проблем и другие преимущества, но массовые операции (кроме массового чтения) не являются одними из них.


2

Другой вариант - использовать 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();
}

См. Документацию для большего количества примеров и продвинутого использования. Отказ от ответственности: я являюсь автором этой библиотеки, и любые взгляды имеют собственное мнение.


2
Этот проект был удален как из NuGet, так и из GitHub.
0xced

1

По моим сведениям есть no BulkInsertв EntityFrameworkувеличении производительности огромных вставок.

В этом случае вы можете пойти с SqlBulkCopy в , ADO.netчтобы решить вашу проблему


Я смотрел на этот класс, но он, кажется, больше ориентирован на вставки из таблицы в таблицу, не так ли?
Бонго Шарп

Не уверен, что вы имеете в виду, он перегружен, WriteToServerчто занимает DataTable.
Блинди

нет, вы можете вставить из .Net объектов в SQL также. Что вы ищете?
anishMarokey

Способ вставить потенциально тысячи записей в базу данных в блоке TransactionScope
Bongo Sharp

Вы можете использовать .Net TransactionScope technet.microsoft.com/en-us/library/bb896149.aspx
anishMarokey

1

Вы когда-нибудь пытались вставить через фоновый рабочий или задачу?

В моем случае я вставляю 7760 регистров, распределенных в 182 разных таблицах с отношениями внешних ключей (по NavigationProperties).

Без задачи это заняло 2 с половиной минуты. Внутри задачи (Task.Factory.StartNew(...) ) это заняло 15 секунд.

Я делаю только SaveChanges()после добавления всех объектов в контекст. (для обеспечения целостности данных)


2
Я уверен, что контекст не является потокобезопасным. Есть ли у вас тесты, чтобы убедиться, что все объекты были сохранены?
Дэнни Варод

Я знаю, что вся структура сущностей вообще не является поточно-ориентированной, но я просто добавляю объекты в контекст и сохраняю в конце ... Это отлично работает здесь.
Рафаэль AMS

Итак, вы вызываете DbContext.SaveChanges () в главном потоке, но добавление сущностей в контекст выполняется в фоновом потоке, верно?
Прокурорс

1
Да, добавить данные в потоках; ждать, пока все закончится; и сохранить изменения в основной ветке
Рафаэль AMS

Хотя я думаю, что этот путь опасен и подвержен ошибкам, я нахожу его очень интересным.
Учащийся

1

Все написанные здесь решения не помогают, потому что когда вы выполняете SaveChanges (), операторы вставки отправляются в базу данных один за другим, именно так работает Entity.

И если ваша поездка в базу данных и обратно составляет, например, 50 мс, то время, необходимое для вставки, равно числу записей x 50 мс.

Вы должны использовать BulkInsert, вот ссылка: https://efbulkinsert.codeplex.com/

Я использовал время вставки с 5-6 минут до 10-12 секунд, используя его.



1

[НОВОЕ РЕШЕНИЕ ДЛЯ 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.


0

Секрет в том, чтобы вставить в идентичный пустой промежуточный стол. Вставки быстро осветляются. Затем запустить один вставку из этого в ваш главный большой стол. Затем обрежьте промежуточный стол, готовый к следующей партии.

то есть.

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. Затем очистите свой промежуточный стол. Это очень быстрый способ вставить много данных в уже большую таблицу.
Саймон Хьюз

13
Когда вы говорите с помощью EF, добавляете записи в промежуточную таблицу, вы действительно пробовали это с EF? Поскольку EF отправляет отдельный вызов в базу данных при каждой вставке, я подозреваю, что вы увидите тот же самый удар, который пытается избежать OP. Как промежуточный стол позволяет избежать этой проблемы?
Джим Вули

-1

Но для более чем (+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));
    }

-1

Используйте хранимую процедуру, которая принимает входные данные в форме XML для вставки данных.

Из вашего кода на c # вставьте данные в формате xml.

Например, в C # синтаксис будет выглядеть так:

object id_application = db.ExecuteScalar("procSaveApplication", xml)

-7

Используйте эту технику, чтобы увеличить скорость вставки записей в Entity Framework. Здесь я использую простую хранимую процедуру для вставки записей. И для выполнения этой хранимой процедуры я использую метод .FromSql () Entity Framework, который выполняет Raw SQL.

Код хранимой процедуры:

CREATE PROCEDURE TestProc
@FirstParam VARCHAR(50),
@SecondParam VARCHAR(50)

AS
  Insert into SomeTable(Name, Address) values(@FirstParam, @SecondParam) 
GO

Затем переберите все ваши 4000 записей и добавьте код Entity Framework, который выполняет сохраненные

Процедура начинается каждый сотый цикл.

Для этого я создаю строковый запрос для выполнения этой процедуры, продолжая добавлять к нему каждый набор записей.

Затем проверьте, что цикл работает кратно 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 = "";
    }
}

Это может быть эффективным, но эквивалентно НЕ использовать структуру объекта. Вопрос OP заключался в том, как максимизировать эффективность в контексте Entity Framework
kall2sollies
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.