Самый быстрый способ вставки в Entity Framework - PullRequest
614 голосов
/ 09 мая 2011

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

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

Ответы [ 28 ]

927 голосов
/ 10 мая 2011

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

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

Вот несколько измерений для моих 560.000 сущностей:

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

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

169 голосов
/ 12 августа 2012

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

context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ValidateOnSaveEnabled = false;
96 голосов
/ 18 февраля 2014

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

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

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

context.BulkInsert(hugeAmountOfEntities);
75 голосов
/ 09 мая 2011

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

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

48 голосов
/ 15 января 2013

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

    }
}
19 голосов
/ 20 июня 2014

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

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

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

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

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

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

17 голосов
/ 19 декабря 2014

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

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

Единственный код, который вам нужен:

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

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

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

14 голосов
/ 28 февраля 2013

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

8 голосов
/ 20 января 2018

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