Улучшение производительности массовой вставки в платформе Entity - PullRequest
109 голосов
/ 24 мая 2011

Я хочу вставить 20000 записей в таблицу по структуре объекта, и это займет около 2 минут. Есть ли способ, кроме как использовать SP для улучшения его производительности. Это мой код:

 foreach (Employees item in sequence)
 {
   t = new Employees ();
   t.Text = item.Text;
   dataContext.Employees.AddObject(t);                  
 }
 dataContext.SaveChanges();

Ответы [ 12 ]

207 голосов
/ 27 марта 2013

Существует возможность для нескольких улучшений (если вы используете DbContext):

Установка:

yourContext.Configuration.AutoDetectChangesEnabled = false;
yourContext.Configuration.ValidateOnSaveEnabled = false;

Делайте SaveChanges() в упаковках по 100 вставок ... или вы можете попробовать с пакетами по 1000 наименований и посмотреть изменения в производительности.

Поскольку во время всех этих вставок контекст один и тот же, и он становится больше, вы можете перестраивать свой объект контекста каждые 1000 вставок. var yourContext = new YourContext(); Я думаю, что это большой выигрыш.

Выполнение этого улучшения в моем процессе импорта данных заняло от 7 минут до 6 секунд.

Фактические числа ... в вашем случае не могут быть 100 или 1000 ... попробуйте и настройте.

42 голосов
/ 24 мая 2011

Невозможно заставить EF улучшить производительность при этом.Проблема в том, что EF выполняет каждую вставку в отдельном цикле в базу данных.Удивительно, не правда ли?Даже DataSets поддерживали пакетную обработку.Проверьте эту статью для некоторого обходного пути.Другой обходной путь может заключаться в использовании настраиваемой хранимой процедуры, принимающей табличный параметр, но для этого вам нужен необработанный ADO.NET.

33 голосов
/ 07 марта 2014

Вы можете использовать расширение массовой вставки

Вот небольшая сравнительная таблица

EntityFramework.BulkInsert vs EF AddRange

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

context.BulkInsert(hugeAmountOfEntities);

надеюсь, это поможет

25 голосов
/ 06 ноября 2013

Используя приведенный ниже код, вы можете расширить класс частичного контекста с помощью метода, который будет принимать коллекцию объектов-сущностей и массово копировать их в базу данных.Просто замените имя класса из MyEntities на имя класса вашей сущности и добавьте его в ваш проект в правильном пространстве имен.После этого все, что вам нужно сделать, это вызвать метод BulkInsertAll, передавая объекты сущностей, которые вы хотите вставить.Не используйте повторно контекстный класс, вместо этого создавайте новый экземпляр каждый раз, когда вы его используете.Это необходимо, по крайней мере, в некоторых версиях EF, поскольку используемые здесь данные аутентификации, связанные с SQLConnection, теряются после однократного использования класса.Я не знаю почему.

Эта версия для EF 5

public partial class MyEntities
{
    public void BulkInsertAll<T>(T[] entities) where T : class
    {
        var conn = (SqlConnection)Database.Connection;

        conn.Open();

        Type t = typeof(T);
        Set(t).ToString();
        var objectContext = ((IObjectContextAdapter)this).ObjectContext;
        var workspace = objectContext.MetadataWorkspace;
        var mappings = GetMappings(workspace, objectContext.DefaultContainerName, typeof(T).Name);

        var tableName = GetTableName<T>();
        var bulkCopy = new SqlBulkCopy(conn) { DestinationTableName = tableName };

        // Foreign key relations show up as virtual declared 
        // properties and we want to ignore these.
        var properties = t.GetProperties().Where(p => !p.GetGetMethod().IsVirtual).ToArray();
        var table = new DataTable();
        foreach (var property in properties)
        {
            Type propertyType = property.PropertyType;

            // Nullable properties need special treatment.
            if (propertyType.IsGenericType &&
                propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                propertyType = Nullable.GetUnderlyingType(propertyType);
            }

            // Since we cannot trust the CLR type properties to be in the same order as
            // the table columns we use the SqlBulkCopy column mappings.
            table.Columns.Add(new DataColumn(property.Name, propertyType));
            var clrPropertyName = property.Name;
            var tableColumnName = mappings[property.Name];
            bulkCopy.ColumnMappings.Add(new SqlBulkCopyColumnMapping(clrPropertyName, tableColumnName));
        }

        // Add all our entities to our data table
        foreach (var entity in entities)
        {
            var e = entity;
            table.Rows.Add(properties.Select(property => GetPropertyValue(property.GetValue(e, null))).ToArray());
        }

        // send it to the server for bulk execution
        bulkCopy.BulkCopyTimeout = 5 * 60;
        bulkCopy.WriteToServer(table);

        conn.Close();
    }

    private string GetTableName<T>() where T : class
    {
        var dbSet = Set<T>();
        var sql = dbSet.ToString();
        var regex = new Regex(@"FROM (?<table>.*) AS");
        var match = regex.Match(sql);
        return match.Groups["table"].Value;
    }

    private object GetPropertyValue(object o)
    {
        if (o == null)
            return DBNull.Value;
        return o;
    }

    private Dictionary<string, string> GetMappings(MetadataWorkspace workspace, string containerName, string entityName)
    {
        var mappings = new Dictionary<string, string>();
        var storageMapping = workspace.GetItem<GlobalItem>(containerName, DataSpace.CSSpace);
        dynamic entitySetMaps = storageMapping.GetType().InvokeMember(
            "EntitySetMaps",
            BindingFlags.GetProperty | BindingFlags.NonPublic | BindingFlags.Instance,
            null, storageMapping, null);

        foreach (var entitySetMap in entitySetMaps)
        {
            var typeMappings = GetArrayList("TypeMappings", entitySetMap);
            dynamic typeMapping = typeMappings[0];
            dynamic types = GetArrayList("Types", typeMapping);

            if (types[0].Name == entityName)
            {
                var fragments = GetArrayList("MappingFragments", typeMapping);
                var fragment = fragments[0];
                var properties = GetArrayList("AllProperties", fragment);
                foreach (var property in properties)
                {
                    var edmProperty = GetProperty("EdmProperty", property);
                    var columnProperty = GetProperty("ColumnProperty", property);
                    mappings.Add(edmProperty.Name, columnProperty.Name);
                }
            }
        }

        return mappings;
    }

    private ArrayList GetArrayList(string property, object instance)
    {
        var type = instance.GetType();
        var objects = (IEnumerable)type.InvokeMember(property, BindingFlags.GetProperty | BindingFlags.NonPublic | BindingFlags.Instance, null, instance, null);
        var list = new ArrayList();
        foreach (var o in objects)
        {
            list.Add(o);
        }
        return list;
    }

    private dynamic GetProperty(string property, object instance)
    {
        var type = instance.GetType();
        return type.InvokeMember(property, BindingFlags.GetProperty | BindingFlags.NonPublic | BindingFlags.Instance, null, instance, null);
    }
}

Эта версия для EF 6

public partial class CMLocalEntities
{
    public void BulkInsertAll<T>(T[] entities) where T : class
    {
        var conn = (SqlConnection)Database.Connection;

        conn.Open();

        Type t = typeof(T);
        Set(t).ToString();
        var objectContext = ((IObjectContextAdapter)this).ObjectContext;
        var workspace = objectContext.MetadataWorkspace;
        var mappings = GetMappings(workspace, objectContext.DefaultContainerName, typeof(T).Name);

        var tableName = GetTableName<T>();
        var bulkCopy = new SqlBulkCopy(conn) { DestinationTableName = tableName };

        // Foreign key relations show up as virtual declared 
        // properties and we want to ignore these.
        var properties = t.GetProperties().Where(p => !p.GetGetMethod().IsVirtual).ToArray();
        var table = new DataTable();
        foreach (var property in properties)
        {
            Type propertyType = property.PropertyType;

            // Nullable properties need special treatment.
            if (propertyType.IsGenericType &&
                propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                propertyType = Nullable.GetUnderlyingType(propertyType);
            }

            // Since we cannot trust the CLR type properties to be in the same order as
            // the table columns we use the SqlBulkCopy column mappings.
            table.Columns.Add(new DataColumn(property.Name, propertyType));
            var clrPropertyName = property.Name;
            var tableColumnName = mappings[property.Name];
            bulkCopy.ColumnMappings.Add(new SqlBulkCopyColumnMapping(clrPropertyName, tableColumnName));
        }

        // Add all our entities to our data table
        foreach (var entity in entities)
        {
            var e = entity;
            table.Rows.Add(properties.Select(property => GetPropertyValue(property.GetValue(e, null))).ToArray());
        }

        // send it to the server for bulk execution
        bulkCopy.BulkCopyTimeout = 5*60;
        bulkCopy.WriteToServer(table);

        conn.Close();
    }

    private string GetTableName<T>() where T : class
    {
        var dbSet = Set<T>();
        var sql = dbSet.ToString();
        var regex = new Regex(@"FROM (?<table>.*) AS");
        var match = regex.Match(sql);
        return match.Groups["table"].Value;
    }

    private object GetPropertyValue(object o)
    {
        if (o == null)
            return DBNull.Value;
        return o;
    }

    private Dictionary<string, string> GetMappings(MetadataWorkspace workspace, string containerName, string entityName)
    {
        var mappings = new Dictionary<string, string>();
        var storageMapping = workspace.GetItem<GlobalItem>(containerName, DataSpace.CSSpace);
        dynamic entitySetMaps = storageMapping.GetType().InvokeMember(
            "EntitySetMaps",
            BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance,
            null, storageMapping, null);

        foreach (var entitySetMap in entitySetMaps)
        {
            var typeMappings = GetArrayList("EntityTypeMappings", entitySetMap);
            dynamic typeMapping = typeMappings[0];
            dynamic types = GetArrayList("Types", typeMapping);

            if (types[0].Name == entityName)
            {
                var fragments = GetArrayList("MappingFragments", typeMapping);
                var fragment = fragments[0];
                var properties = GetArrayList("AllProperties", fragment);
                foreach (var property in properties)
                {
                    var edmProperty = GetProperty("EdmProperty", property);
                    var columnProperty = GetProperty("ColumnProperty", property);
                    mappings.Add(edmProperty.Name, columnProperty.Name);
                }
            }
        }

        return mappings;
    }

    private ArrayList GetArrayList(string property, object instance)
    {
        var type = instance.GetType();
        var objects = (IEnumerable)type.InvokeMember(
            property, 
            BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance, null, instance, null);
        var list = new ArrayList();
        foreach (var o in objects)
        {
            list.Add(o);
        }
        return list;
    }

    private dynamic GetProperty(string property, object instance)
    {
        var type = instance.GetType();
        return type.InvokeMember(property, BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance, null, instance, null);
    }

}

И, наконец, кое-что дляВы любители Linq-To-Sql.

partial class MyDataContext
{
    partial void OnCreated()
    {
        CommandTimeout = 5 * 60;
    }

    public void BulkInsertAll<T>(IEnumerable<T> entities)
    {
        entities = entities.ToArray();

        string cs = Connection.ConnectionString;
        var conn = new SqlConnection(cs);
        conn.Open();

        Type t = typeof(T);

        var tableAttribute = (TableAttribute)t.GetCustomAttributes(
            typeof(TableAttribute), false).Single();
        var bulkCopy = new SqlBulkCopy(conn) { 
            DestinationTableName = tableAttribute.Name };

        var properties = t.GetProperties().Where(EventTypeFilter).ToArray();
        var table = new DataTable();

        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 => GetPropertyValue(
              property.GetValue(entity, null))).ToArray());
        }

        bulkCopy.WriteToServer(table);
        conn.Close();
    }

    private bool EventTypeFilter(System.Reflection.PropertyInfo p)
    {
        var attribute = Attribute.GetCustomAttribute(p, 
            typeof (AssociationAttribute)) as AssociationAttribute;

        if (attribute == null) return true;
        if (attribute.IsForeignKey == false) return true; 

        return false;
    }

    private object GetPropertyValue(object o)
    {
        if (o == null)
            return DBNull.Value;
        return o;
    }
}
8 голосов
/ 11 марта 2012

Может быть, этот ответ здесь вам поможет.Кажется, вы хотите периодически избавляться от контекста.Это потому, что контекст становится все больше и больше по мере роста связанных сущностей.

4 голосов
/ 08 января 2017

Существуют две основные проблемы с производительностью вашего кода:

  • Использование метода добавления
  • Использование SaveChanges

Использование метода добавления

Метод Add становится все медленнее и медленнее для каждой добавляемой сущности.

См .: http://entityframework.net/improve-ef-add-performance

Например, добавление 10 000 сущностей с помощью:

  • Добавить (принять ~ 105 000 мс)
  • AddRange (занять ~ 120 мс)

Примечание: объекты еще не сохранены в базе данных!

Проблема заключается в том, что метод Add пытается обнаруживать изменения в каждой добавленной сущности, в то время как AddRange делает это один раз после добавления всех сущностей в контекст.

Распространенные решения:

  • Использовать AddRange сверх Add
  • Устанавливать AutoDetectChanges в false
  • SPLIT SaveChanges в несколько пакетов

Использование SaveChanges

Entity Framework не был создан для массовых операций.Для каждой сохраняемой сущности выполняется обход базы данных.

Итак, если вы хотите вставить 20 000 записей, вы выполните 20 000 обращений к базе данных, что составляет INSANE !

Некоторые сторонние библиотеки поддерживают массовую вставкудоступно:

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

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

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


Отказ от ответственности : я владелец EntityFramework 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;
});

РЕДАКТИРОВАТЬ: Ответить на вопрос в комментарии

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

Не слишком высоко, не слишком низко.Не существует конкретного значения, подходящего для всех сценариев, поскольку оно зависит от нескольких факторов, таких как размер строки, индекс, триггер и т. Д.

Обычно рекомендуется составлять около 4000.

Также есть способ связать все это в одну транзакцию и не беспокоиться о том, что время ожидания истекло

Вы можете использовать транзакцию Entity Framework.Наша библиотека использует транзакцию, если она запущена.Но будьте осторожны, транзакция, которая занимает слишком много времени, сопровождается такими проблемами, как блокировка строки / индекса / таблицы.

4 голосов
/ 09 февраля 2015

В среде Azure с базовым веб-сайтом, на котором имеется 1 экземпляр. Я пытался вставить пакет из 1000 записей одновременно из 25000 записей, используя цикл for, это заняло 11,5 минут, но при параллельном выполнении это заняло менее минуты. рекомендуем использовать TPL (Task Parallel Library).

         var count = (you collection / 1000) + 1;
         Parallel.For(0, count, x =>
        {
            ApplicationDbContext db1 = new ApplicationDbContext();
            db1.Configuration.AutoDetectChangesEnabled = false;

            var records = members.Skip(x * 1000).Take(1000).ToList();
            db1.Members.AddRange(records).AsParallel();

            db1.SaveChanges();
            db1.Dispose();
        });
4 голосов
/ 17 июля 2013

Лучший способ - полностью пропустить Entity Framework для этой операции и использовать класс SqlBulkCopy. Другие операции могут продолжать использовать EF, как и раньше.

Это увеличивает стоимость обслуживания решения, но в любом случае помогает сократить время, необходимое для вставки больших коллекций объектов в базу данных, на один-два порядка по сравнению с использованием EF.

Вот статья, в которой сравнивается класс SqlBulkCopy с EF для объектов с отношениями родитель-потомок (также описываются изменения в дизайне, необходимые для реализации массовой вставки): Как массово вставить сложные объекты в базу данных SQL Server

4 голосов
/ 24 мая 2011

В настоящее время лучшего способа нет, однако может быть незначительное улучшение, если переместить SaveChanges внутрь цикла for для, вероятно, 10 элементов.

int i = 0;

foreach (Employees item in sequence)
{
   t = new Employees ();
   t.Text = item.Text;
   dataContext.Employees.AddObject(t);   

   // this will add max 10 items together
   if((i % 10) == 0){
       dataContext.SaveChanges();
       // show some progress to user based on
       // value of i
   }
   i++;
}
dataContext.SaveChanges();

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

3 голосов
/ 06 февраля 2014

Попробуйте использовать Bulk Insert ....

http://code.msdn.microsoft.com/LinqEntityDataReader

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

        var bulkCopy = new SqlBulkCopy(connection);
        bulkCopy.DestinationTableName = TableName;
        var dataReader = storeEntities.AsDataReader();
        bulkCopy.WriteToServer(dataReader);

Есть одна ошибка с этим кодом. Убедитесь, что определение Entity Framework для сущности точно соответствует определению таблицы, убедитесь, что свойства сущности в том же порядке в модели сущности, что и столбцы таблицы SQL Server. Невыполнение этого условия приведет к исключению.

...