Когда я должен вызвать SaveChanges() при создании 1000 объектов Entity Framework? (например, во время импорта)

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

Какой из них имеет наибольший смысл:

  • Запустите SaveChanges() каждый вызов AddToClassName().
  • Запустите SaveChanges() каждое n число вызовов AddToClassName().
  • Запустите SaveChanges() после всех вызовов AddToClassName().

Первый вариант, вероятно, медленный? Поскольку он должен будет анализировать объекты EF в памяти, генерировать SQL и т.д.

Я предполагаю, что второй вариант является лучшим из обоих миров, так как мы можем обернуть попытку поймать этот вызов SaveChanges() и просто потерять n количество записей за раз, если один из них не работает. Может быть, хранить каждую партию в списке < > . Если вызов SaveChanges() завершен, избавьтесь от списка. Если это не удается, зарегистрируйте элементы.

Последний вариант, вероятно, тоже будет очень медленным, так как каждый отдельный объект EF должен быть в памяти до тех пор, пока не будет вызван SaveChanges(). И если сохранение не удалось, то ничего не было бы сделано, верно?

Ответ 1

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

Если вам нужно ввести все строки в одну транзакцию, вызовите ее после класса AddToClassName. Если строки можно вводить независимо, сохраните изменения после каждой строки. Консистенция базы данных важна.

Второй вариант мне не нравится. Для меня было бы непонятно (с точки зрения конечных пользователей), если бы я сделал импорт в систему, и это снизит 10 строк из 1000, только потому, что 1 плохо. Вы можете попробовать импортировать 10, и если это не удается, попробуйте один за другим, а затем запишите.

Проверьте, требуется ли длительное время. Не пишите "propably". Вы еще этого не знаете. Только когда это действительно проблема, подумайте о другом решении (marc_s).

ИЗМЕНИТЬ

Я провел несколько тестов (время в миллисекундах):

10000 строк:

SaveChanges() после 1 строки: 18510,534
SaveChanges() после 100 строк: 4350,3075
SaveChanges() после 10000 строк: 5233,0635

50000 строк:

SaveChanges() после 1 строки: 78496,929
SaveChanges() после 500 строк: 22302,2835
SaveChanges() после 50000 строк: 24022,8765

Таким образом, на самом деле быстрее фиксировать после n строк, чем в конце.

Моя рекомендация:

  • SaveChanges() после n строк.
  • Если одна ошибка завершается неудачно, попробуйте один за другим найти неисправную строку.

Тестовые классы:

Таблица:

CREATE TABLE [dbo].[TestTable](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [SomeInt] [int] NOT NULL,
    [SomeVarchar] [varchar](100) NOT NULL,
    [SomeOtherVarchar] [varchar](50) NOT NULL,
    [SomeOtherInt] [int] NULL,
 CONSTRAINT [PkTestTable] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

Класс:

public class TestController : Controller
{
    //
    // GET: /Test/
    private readonly Random _rng = new Random();
    private const string _chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    private string RandomString(int size)
    {
        var randomSize = _rng.Next(size);

        char[] buffer = new char[randomSize];

        for (int i = 0; i < randomSize; i++)
        {
            buffer[i] = _chars[_rng.Next(_chars.Length)];
        }
        return new string(buffer);
    }


    public ActionResult EFPerformance()
    {
        string result = "";

        TruncateTable();
        result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(10000, 1).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 100 rows:" + EFPerformanceTest(10000, 100).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 10000 rows:" + EFPerformanceTest(10000, 10000).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(50000, 1).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 500 rows:" + EFPerformanceTest(50000, 500).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 50000 rows:" + EFPerformanceTest(50000, 50000).TotalMilliseconds + "<br/>";
        TruncateTable();

        return Content(result);
    }

    private void TruncateTable()
    {
        using (var context = new CamelTrapEntities())
        {
            var connection = ((EntityConnection)context.Connection).StoreConnection;
            connection.Open();
            var command = connection.CreateCommand();
            command.CommandText = @"TRUNCATE TABLE TestTable";
            command.ExecuteNonQuery();
        }
    }

    private TimeSpan EFPerformanceTest(int noOfRows, int commitAfterRows)
    {
        var startDate = DateTime.Now;

        using (var context = new CamelTrapEntities())
        {
            for (int i = 1; i <= noOfRows; ++i)
            {
                var testItem = new TestTable();
                testItem.SomeVarchar = RandomString(100);
                testItem.SomeOtherVarchar = RandomString(50);
                testItem.SomeInt = _rng.Next(10000);
                testItem.SomeOtherInt = _rng.Next(200000);
                context.AddToTestTable(testItem);

                if (i % commitAfterRows == 0) context.SaveChanges();
            }
        }

        var endDate = DateTime.Now;

        return endDate.Subtract(startDate);
    }
}

Ответ 2

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

Я обнаружил, что большую часть времени при обработке SaveChanges, независимо от того, обрабатывает ли она 100 или 1000 записей одновременно, связана с ЦП. Таким образом, обработав контексты с шаблоном производителя/потребителя (реализованным с помощью BlockingCollection), я смог намного лучше использовать ядра ЦП и получил от 4000 изменений в секунду (как сообщает возвращаемое значение SaveChanges), чтобы более 14 000 изменений/сек. Загрузка процессора переместилась с примерно 13% (у меня 8 ядер) примерно до 60%. Даже используя несколько потребительских потоков, я едва облагался налогом (очень быстрая) дисковая система ввода-вывода и использование ЦП SQL Server не превышало 15%.

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

Я обнаружил, что создание 1 потока производителей и (# процессорных ядер) -1 потребительских потоков позволило мне настроить количество записей, зафиксированных за каждую партию, так что количество элементов в BlockingCollection колебалось между 0 и 1 (после потребителя нить взяла один предмет). Таким образом, было достаточно работы для оптимальной работы потребляющих потоков.

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

Ответ 4

Используйте хранимую процедуру.

  • Создайте пользовательский тип данных на сервере Sql.
  • Создайте и заполните массив этого типа в коде (очень быстро).
  • Передайте массив в хранимую процедуру с помощью одного вызова (очень быстро).

Я считаю, что это был бы самый простой и быстрый способ сделать это.

Ответ 5

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

У меня была та же проблема, но есть возможность проверить изменения до их фиксации. Мой код выглядит так, и он работает нормально. С chUser.LastUpdated я проверяю, является ли это новой записью или только изменение. Поскольку невозможно перезагрузить запись, которая еще не находится в базе данных.

// Validate Changes
var invalidChanges = _userDatabase.GetValidationErrors();
foreach (var ch in invalidChanges)
{
    // Delete invalid User or Change
    var chUser  =  (db_User) ch.Entry.Entity;
    if (chUser.LastUpdated == null)
    {
        // Invalid, new User
        _userDatabase.db_User.Remove(chUser);
        Console.WriteLine("!Failed to create User: " + chUser.ContactUniqKey);
    }
    else
    {
        // Invalid Change of an Entry
        _userDatabase.Entry(chUser).Reload();
        Console.WriteLine("!Failed to update User: " + chUser.ContactUniqKey);
    }                    
}

_userDatabase.SaveChanges();