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


80

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

Что из этого имеет наибольший смысл:

  1. Запускайте SaveChanges()каждый AddToClassName()звонок.
  2. Выполнить SaveChanges()все п число AddToClassName()вызовов.
  3. Запуск SaveChanges()после всех из AddToClassName()вызовов.

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

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

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

Ответы:


62

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

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

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

Протестируйте, если это займет много времени. Не пишите «возможно». Вы еще этого не знаете. Только когда это действительно проблема, подумайте о другом решении (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);
    }
}

Я написал «вероятно» потому, что сделал обоснованное предположение. Чтобы прояснить, что «я не уверен», я сделал это вопросом. Кроме того, я думаю, что имеет смысл подумать о потенциальных проблемах ДО того, как я столкнусь с ними. Вот почему я задал этот вопрос. Я надеялся, что кто-то узнает, какой метод будет наиболее эффективным, и я сразу же воспользуюсь этим.
Джон Бубриски

Классно, чувак. Именно то, что я искал. Спасибо, что нашли время проверить это! Я предполагаю, что я могу сохранить каждый пакет в памяти, попробовать выполнить фиксацию, а затем, если он не удастся, пройти каждую из них индивидуально, как вы сказали. Затем, как только этот пакет будет выполнен, освободите ссылки на эти 100 элементов, чтобы их можно было очистить из памяти. Еще раз спасибо!
Джон Бубриски

3
Память не будет освобождена, потому что все объекты будут удерживаться ObjectContext, но наличие 50000 или 100000 в контексте в наши дни не занимает много места.
LukLed

6
Я действительно обнаружил, что производительность снижается между каждым вызовом SaveChanges (). Решением этой проблемы является удаление контекста после каждого вызова SaveChanges () и повторное создание нового экземпляра для следующего пакета данных, который будет добавлен.
Shawn de Wet

1
@LukLed не совсем ... вы вызываете SaveChanges внутри цикла For ... чтобы код мог продолжать добавлять элементы, которые должны быть сохранены внутри цикла for в том же экземпляре ctx, и снова вызывать SaveChanges в том же экземпляре .
Shawn de Wet

18

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

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

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

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

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


Привет, @ eric-j, не могли бы вы немного уточнить эту строку, «обрабатывая контексты с помощью шаблона производитель / потребитель (реализованный с помощью BlockingCollection)», чтобы я мог попробовать свой код?
Фойзул Карим 05

14

Если вам нужно импортировать тысячи записей, я бы использовал что-то вроде SqlBulkCopy, а не Entity Framework для этого.


15
Ненавижу, когда люди не отвечают на мой вопрос :) Что ж, допустим, мне "нужно" использовать EF. Что тогда?
Джон Бубриски

3
Что ж, если вы действительно ДОЛЖНЫ использовать EF, я бы попытался выполнить фиксацию после пакета, скажем, из 500 или 1000 записей. В противном случае вы в конечном итоге будете использовать слишком много ресурсов, и сбой потенциально приведет к откату всех 99999 строк, которые вы обновили, когда выйдет из строя 100000-я.
marc_s

С той же проблемой я закончил, используя SqlBulkCopy, который в этом случае более производительный, чем EF. Хотя я не люблю использовать несколько способов доступа к базе данных.
Жюльен Н.

2
Я также изучаю это решение, так как у меня такая же проблема ... Массовое копирование было бы отличным решением, но моя служба хостинга запрещает его использование (и я предполагаю, что другие тоже), так что это не жизнеспособный вариант для некоторых людей.
Деннис Уорд

3
@marc_s: Как вы справляетесь с необходимостью применения бизнес-правил, присущих бизнес-объектам, при использовании SqlBulkCopy? Я не понимаю, как не использовать EF без избыточной реализации правил.
Эрик Дж.

2

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

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

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


7
Обычно в SO утверждения «это самый быстрый» необходимо подтверждать тестовым кодом и результатами.
Майкл Блэкберн

2

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

У меня была такая же проблема, но есть возможность проверить изменения перед их фиксацией. Мой код выглядит так, и он работает нормально. С помощью 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();

Да, это примерно та же проблема, правда? При этом вы можете добавить все 1000 записей и перед запуском saveChanges()удалить те, которые могут вызвать ошибку.
Jan Leuenberger

1
Но акцент в вопросе заключается в том, сколько вставок / обновлений необходимо эффективно зафиксировать за один SaveChangesвызов. Вы не решаете эту проблему. Обратите внимание, что существует больше потенциальных причин сбоя SaveChanges, чем ошибок проверки. Кстати, вы также можете просто пометить объекты как Unchangedвместо того, чтобы перезагружать / удалять их.
Gert Arnold

1
Вы правы, это напрямую не решает вопрос, но я думаю, что большинство людей, наткнувшихся на эту ветку, имеют проблемы с проверкой, хотя есть и другие причины SaveChangesнеудач. И это решает проблему. Если этот пост действительно беспокоит вас в этой теме, я могу удалить его, моя проблема решена, я просто пытаюсь помочь другим.
Jan Leuenberger

У меня вопрос по этому поводу. Когда вы звоните GetValidationErrors(), он «фальсифицирует» вызов базы данных и извлекает ошибки или что? Спасибо за ответ :)
Jeancarlo Fontalvo 01
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.