Avatar ·

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

Всем привет! Я начинающий C# разработчик и столкнулся с проблемой производительности при массовой вставке данных через Entity Framework. У меня есть приложение, которое каждую минуту получает около 10 000 записей из внешнего API и сохраняет их в базу данных SQL Server.

Сейчас я использую обычный цикл с Add() и SaveChanges() для каждой записи, но это занимает около 3-4 минут, что критично для моего приложения. Вот пример моего кода:

public void SaveRecords(List<MyEntity> records)
{
    using (var context = new MyDbContext())
    {
        foreach (var record in records)
        {
            context.MyEntities.Add(record);
            context.SaveChanges(); // Вызываю для каждой записи
        }
    }
}

Я слышал, что есть более эффективные способы: использование AddRange(), пакетная обработка, Bulk Insert. Но я не уверен, какой подход даст максимальную производительность. Также меня интересует, стоит ли отключать отслеживание изменений (AutoDetectChangesEnabled) и стоит ли использовать транзакции.

Подскажите, пожалуйста, как правильно организовать массовую вставку в Entity Framework, чтобы уложиться в 10-15 секунд? Буду благодарен за примеры кода и объяснения.

<<>>

Оптимизация массовой вставки в Entity Framework

Ваша проблема типична для начинающих разработчиков. Использование SaveChanges() в цикле — одна из самых медленных операций. Давайте разберем правильные подходы.

1. Используйте AddRange() + один SaveChanges()

Самый простой и безопасный способ:

public void SaveRecords(List<MyEntity> records)
{
    using (var context = new MyDbContext())
    {
        context.Configuration.AutoDetectChangesEnabled = false; // Отключаем авто-детект
        context.MyEntities.AddRange(records);
        context.SaveChanges();
    }
}

Этот подход уменьшит время с 3 минут до ~30-40 секунд для 10 000 записей. Но этого может быть недостаточно.

2. Пакетная обработка (Batch Insert) с транзакцией

Разбивайте записи на пакеты по 500-1000 штук:

public void SaveRecordsInBatches(List<MyEntity> records, int batchSize = 500)
{
    using (var context = new MyDbContext())
    {
        context.Configuration.AutoDetectChangesEnabled = false;
        
        using (var transaction = context.Database.BeginTransaction())
        {
            for (int i = 0; i < records.Count; i += batchSize)
            {
                var batch = records.Skip(i).Take(batchSize).ToList();
                context.MyEntities.AddRange(batch);
                context.SaveChanges();
                // Очищаем контекст для следующего пакета
                context.ChangeTracker.Clear();
            }
            transaction.Commit();
        }
    }
}

Этот метод снижает время до ~15-20 секунд.

3. Bulk Insert через библиотеку EntityFramework.Extensions

Если вам нужна максимальная производительность, используйте NuGet пакет EntityFramework.BulkInsert или Z.EntityFramework.Extensions (платный, но есть пробная версия):

// Установите: Install-Package Z.EntityFramework.Extensions
public void BulkInsertRecords(List<MyEntity> records)
{
    using (var context = new MyDbContext())
    {
        context.BulkInsert(records, options => 
        {
            options.BatchSize = 1000;
            options.AutoMapOutputDirection = false;
        });
    }
}

Этот метод выполняет вставку за 1-2 секунды для 10 000 записей, используя SQL Bulk Insert.

4. Ручной Bulk Insert через DataTable и SqlBulkCopy

Самый быстрый способ без сторонних библиотек:

public void BulkInsertWithSqlBulkCopy(List<MyEntity> records)
{
    var dataTable = new DataTable();
    dataTable.Columns.Add("Id", typeof(int));
    dataTable.Columns.Add("Name", typeof(string));
    // Добавьте все столбцы вашей таблицы

    foreach (var record in records)
    {
        dataTable.Rows.Add(record.Id, record.Name);
    }

    using (var connection = new SqlConnection(connectionString))
    {
        connection.Open();
        using (var bulkCopy = new SqlBulkCopy(connection))
        {
            bulkCopy.DestinationTableName = "MyEntities";
            bulkCopy.BatchSize = 1000;
            bulkCopy.WriteToServer(dataTable);
        }
    }
}

Время вставки: 0.5-1 секунда для 10 000 записей.

Рекомендации по выбору:

  • Для небольших проектов (< 1000 записей): используйте AddRange() с отключенным AutoDetectChanges
  • Для средних нагрузок (до 50 000 записей): пакетная обработка с транзакцией
  • Для высоких нагрузок (> 50 000 записей): SqlBulkCopy или платные расширения

Не забывайте также отключать ProxyCreationEnabled и LazyLoadingEnabled при массовых операциях для дополнительного ускорения.

<<>>

Альтернативный подход: использование Dapper для массовой вставки

Если вы не привязаны строго к Entity Framework, рассмотрите использование Dapper — легковесного ORM от разработчиков Stack Overflow. Он идеально подходит для высокопроизводительных операций.

Пример массовой вставки с Dapper:

public void BulkInsertWithDapper(List<MyEntity> records)
{
    using (var connection = new SqlConnection(connectionString))
    {
        connection.Open();
        using (var transaction = connection.BeginTransaction())
        {
            // Используется табличная переменная или временная таблица
            var sql = @"
                INSERT INTO MyEntities (Id, Name, CreatedDate)
                VALUES (@Id, @Name, @CreatedDate)";
            
            connection.Execute(sql, records, transaction: transaction);
            transaction.Commit();
        }
    }
}

Преимущества Dapper:

  • В 2-3 раза быстрее EF Core для массовых операций
  • Минимальное потребление памяти
  • Нет накладных расходов на отслеживание изменений
  • Простая интеграция с существующим кодом

Недостатки:

  • Нет автоматического маппинга сложных связей
  • Требуется писать SQL вручную
  • Меньше возможностей для абстракции

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