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

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

Что из этого наиболее логично:

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

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

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

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

Ответы [ 5 ]

54 голосов
/ 19 декабря 2009

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

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

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

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

EDIT

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

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);
    }
}
17 голосов
/ 28 октября 2012

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

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

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

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

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

12 голосов
/ 19 декабря 2009

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

2 голосов
/ 09 июля 2015

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

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

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

1 голос
/ 14 июня 2017

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

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