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

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

Что из этого наиболее разумно:

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

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

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

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


person John B    schedule 18.12.2009    source источник


Ответы (5)


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

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

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

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

ИЗМЕНИТЬ

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

10 000 строк:

SaveChanges() после 1 строки: 18510 534
SaveChanges() после 100 строк: 4350 3075
SaveChanges() после 10 000 строк: 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);
    }
}
person LukLed    schedule 18.12.2009
comment
Причина, по которой я написал, вероятно, заключается в том, что я сделал обоснованное предположение. Чтобы было понятнее, что я не уверен, я превратил это в вопрос. Кроме того, я думаю, что имеет смысл подумать о потенциальных проблемах ДО того, как я столкнусь с ними. Вот почему я задал этот вопрос. Я надеялся, что кто-нибудь узнает, какой метод будет наиболее эффективным, и я мог бы использовать его сразу же. - person John B; 21.12.2009
comment
Классно, чувак. Именно то, что я искал. Спасибо, что нашли время, чтобы проверить это! Я предполагаю, что я могу сохранить каждую партию в памяти, попробовать зафиксировать, а затем, если это не удастся, пройтись по каждому отдельно, как вы сказали. Затем, как только этот пакет будет готов, освободите ссылки на эти 100 элементов, чтобы их можно было очистить из памяти. Спасибо еще раз! - person John B; 31.12.2009
comment
Память не будет освобождена, потому что все объекты будут удерживаться ObjectContext, но 50000 или 100000 в контексте в наши дни не занимают много места. - person LukLed; 31.12.2009
comment
На самом деле я обнаружил, что производительность снижается между каждым вызовом SaveChanges(). Решение этой проблемы состоит в том, чтобы фактически удалять контекст после каждого вызова SaveChanges() и повторно создавать новый экземпляр для следующего добавляемого пакета данных. - person Shawn de Wet; 16.12.2013
comment
@ShawndeWet: Если вы посмотрите на тестовый класс, это то, что я на самом деле сделал. - person LukLed; 16.12.2013
comment
@LukLed не совсем ... вы вызываете SaveChanges внутри своего цикла For ... поэтому код может продолжать добавлять больше элементов для сохранения внутри цикла for в том же экземпляре ctx и вызывать SaveChanges позже снова в том же экземпляре . - person Shawn de Wet; 16.12.2013
comment
Отличный ответ, мистер. Именно то, что мне было нужно, но мне просто интересно, как мы можем сохранить отдельные записи, когда пакет не удается? Не могли бы вы объяснить реализацию по этому поводу? Спасибо :) - person Jeancarlo Fontalvo; 01.12.2019

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

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

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

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

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

person Eric J.    schedule 28.10.2012
comment
Привет, @eric-j, не могли бы вы немного уточнить эту строку, обработав контексты с помощью шаблона производителя/потребителя (реализованного с помощью BlockingCollection), чтобы я мог попробовать свой код? - person Foyzul Karim; 05.09.2017

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

person marc_s    schedule 18.12.2009
comment
Я ненавижу, когда люди не отвечают на мой вопрос :) Ну, допустим, мне нужно использовать EF. Что тогда? - person John B; 21.12.2009
comment
Что ж, если вы действительно ДОЛЖНЫ использовать EF, я бы попытался зафиксировать после партии, скажем, 500 или 1000 записей. В противном случае вы в конечном итоге будете использовать слишком много ресурсов, и сбой потенциально может привести к откату всех 99999 строк, которые вы обновили, когда произойдет сбой 100000-й строки. - person marc_s; 21.12.2009
comment
С той же проблемой я закончил с использованием SqlBulkCopy, который в этом случае намного более эффективен, чем EF. Хотя я не люблю использовать несколько способов доступа к базе данных. - person Julien N; 19.05.2010
comment
Я также изучаю это решение, так как у меня та же проблема... Массовое копирование было бы отличным решением, но моя служба хостинга запрещает его использование (и я предполагаю, что другие тоже), так что это нежизнеспособно вариант для некоторых. - person Dennis Ward; 30.12.2010
comment
@marc_s: Как вы справляетесь с необходимостью применения бизнес-правил, присущих бизнес-объектам, при использовании SqlBulkCopy? Я не понимаю, как не использовать EF без избыточной реализации правил. - person Eric J.; 29.10.2012

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

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

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

person David    schedule 09.07.2015
comment
Как правило, в SO заявления о том, что это быстрее всего, должны быть подтверждены тестовым кодом и результатами. - person Michael Blackburn; 11.05.2016

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

У меня была та же проблема, но есть возможность проверить изменения перед их фиксацией. Мой код выглядит так, и он работает нормально. С помощью 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();
person Jan Leuenberger    schedule 14.06.2017
comment
Да, это примерно та же проблема, верно? При этом вы можете добавить все 1000 записей и перед запуском saveChanges()вы можете удалить те, которые могут вызвать ошибку. - person Jan Leuenberger; 15.06.2017
comment
Но акцент в вопросе делается на том, сколько вставок/обновлений нужно эффективно зафиксировать в одном вызове SaveChanges. Вы не решаете эту проблему. Обратите внимание, что потенциальных причин сбоя SaveChanges больше, чем ошибок проверки. Кстати, вы также можете просто пометить объекты как Unchanged вместо того, чтобы перезагружать/удалять их. - person Gert Arnold; 20.06.2017
comment
Вы правы, это не касается напрямую вопроса, но я думаю, что у большинства людей, наткнувшихся на эту ветку, возникают проблемы с проверкой, хотя есть и другие причины, по которым SaveChanges не работает. И это решает проблему. Если этот пост действительно беспокоит вас в этой теме, я могу удалить его, моя проблема решена, я просто пытаюсь помочь другим. - person Jan Leuenberger; 21.06.2017
comment
У меня есть вопрос об этом. Когда вы вызываете GetValidationErrors(), он имитирует вызов базы данных и извлекает ошибки или что? Спасибо за ответ :) - person Jeancarlo Fontalvo; 01.12.2019