c# - пример - работа с entity framework




Самый быстрый способ вставки в инфраструктуру Entity (17)

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

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


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

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

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

См. Библиотека полнотекстового ввода Entity Framework

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

Отказ от ответственности : я являюсь владельцем расширений Entity Framework

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

  • Массовые 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;
});

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

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


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

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

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

Im только делает SaveChanges()после добавления всех сущностей в контекст. (для обеспечения целостности данных)


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

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


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

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

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

  • commitCount = 1, recreateContext = false: много часов (это ваша текущая процедура)
  • commitCount = 100, recreateContext = false: более 20 минут
  • commitCount = 1000, recreateContext = false: 242 с
  • commitCount = 10000, recreateContext = false: 202 с
  • commitCount = 100000, recreateContext = false: 199 sec
  • commitCount = 1000000, recreateContext = false: исключение из памяти
  • commitCount = 1, recreateContext = true: более 10 минут
  • commitCount = 10, recreateContext = true: 241 с
  • commitCount = 100, recreateContext = true: 164 с
  • commitCount = 1000, recreateContext = true: 191 с

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


Как говорили другие люди, 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 тыс. Сущ. В секунду.

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


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

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


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


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

то есть.

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

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

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

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

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

  • commitCount = 1, recreateContext = true: более 10 минут
  • commitCount = 10, recreateContext = true: 241 с
  • commitCount = 100, recreateContext = true: 164 с
  • commitCount = 1000, recreateContext = true: 191 с

Видно, что при перемещении от 1 до 10 увеличивается скорость, а от 10 до 100, но от 100 до 1000 скорость вставки падает снова.

Поэтому я сосредоточился на том, что происходит, когда вы уменьшаете размер партии до значения от 10 до 100, и вот мои результаты (я использую другое содержимое строки, поэтому мои времена имеют разное значение):

Quantity    | Batch size    | Interval
1000    1   3
10000   1   34
100000  1   368

1000    5   1
10000   5   12
100000  5   133

1000    10  1
10000   10  11
100000  10  101

1000    20  1
10000   20  9
100000  20  92

1000    27  0
10000   27  9
100000  27  92

1000    30  0
10000   30  9
100000  30  92

1000    35  1
10000   35  9
100000  35  94

1000    50  1
10000   50  10
100000  50  106

1000    100 1
10000   100 14
100000  100 141

Исходя из моих результатов, фактический оптимум составляет около 30 для размера партии. Это меньше, чем 10 и 100. Проблема в том, что я понятия не имею, почему 30 оптимальны, и я не мог найти логического объяснения.


Я согласен с Адамом Ракисом. SqlBulkCopy - это самый быстрый способ передачи массивных записей из одного источника данных в другой. Я использовал это для копирования 20 тыс. Записей, и потребовалось менее 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();
        }

    }
}

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


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

Из вашего кода c # передайте данные вставки как xml.

например, в c #, синтаксис будет выглядеть следующим образом:

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

[NEW SOLUTION FOR POSTGRESQL] Эй, я знаю, что это довольно старый пост, но я столкнулся с аналогичной проблемой, но мы использовали Postgresql. Я хотел использовать эффективный bulkinsert, что оказалось довольно сложным. Я не нашел в этой БД надлежащей бесплатной библиотеки. Я нашел этого помощника: 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));

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

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


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






entity-framework