Коли слід викликати SaveChanges () при створенні 1000-х об’єктів Entity Framework? (як під час імпорту)


80

Я запускаю імпорт, який матиме 1000 записів на кожному запуску. Просто шукаю підтвердження моїх припущень:

Що з цього має найбільший сенс:

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

Перший варіант, мабуть, повільний, чи не так? Оскільки йому потрібно буде проаналізувати об'єкти EF у пам'яті, сформувати SQL тощо.

Я припускаю, що другий варіант є найкращим з обох світів, оскільки ми можемо обернути спробу ловити навколо цього SaveChanges()дзвінка і втратити лише n кількість записів за раз, якщо один із них не вдається. Можливо, зберігати кожну партію у списку <>. Якщо 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 рядка: 78496929 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) до понад 14000 змін / секунду. Використання центрального процесора перейшло з приблизно 13% (у мене 8 ядер) до приблизно 60%. Навіть використовуючи кілька споживчих потоків, я ледве обклав податком (дуже швидку) систему дискового вводу-виводу, і використання ЦП SQL Server було не вище 15%.

Розвантажуючи збереження в кілька потоків, ви можете налаштувати як кількість записів до фіксації, так і кількість потоків, що виконують операції коміту.

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

Звичайно, цей сценарій вимагає створення нового контексту для кожної партії, який, на мою думку, є швидшим навіть у однопотоковому сценарії для мого випадку використання.


Привіт, @ eric-j, чи не міг би ти, будь ласка, трохи допрацювати цей рядок, "обробивши контексти шаблоном виробник / споживач (реалізований за допомогою BlockingCollection)", щоб я міг спробувати зі своїм кодом?
Foyzul Karim

14

Якщо вам потрібно імпортувати тисячі записів, я б використав щось на зразок SqlBulkCopy, а не Entity Framework для цього.


15
Я ненавиджу, коли люди не відповідають на моє запитання :) Ну, припустимо, мені "потрібно" використовувати EF. Що тоді?
Джон Бубріскі

3
Ну, якщо ви дійсно ПОВИННІ використовувати EF, тоді я спробую здійснити фіксацію після серії, скажімо, 500 або 1000 записів. В іншому випадку ви в кінцевому підсумку використаєте занадто багато ресурсів, і помилка потенційно може відкотити всі 99999 рядків, які ви оновили, коли 100000-й не вдається.
marc_s

З тією ж проблемою я закінчив із використанням SqlBulkCopy, який у цьому випадку є набагато ефективнішим, ніж EF. Хоча я не люблю користуватися декількома способами доступу до бази даних.
Julien N

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

3
@marc_s: Як ви вирішуєте необхідність дотримання ділових правил, властивих бізнес-об’єктам, під час використання SqlBulkCopy? Я не розумію, як не використовувати EF без зайвої реалізації правил.
Eric J.

2

Використовуйте збережену процедуру.

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

Я вважаю, що це був би найпростіший і найшвидший спосіб зробити це.


7
Зазвичай на SO твердження про "це найшвидше" потрібно обґрунтовувати тестовим кодом та результатами.
Майкл Блекберн,

2

На жаль, я знаю, що ця тема застаріла, але я думаю, що це може допомогти іншим людям із цією проблемою.

У мене була та сама проблема, але існує можливість перевірити зміни, перш ніж їх фіксувати. Мій код виглядає так, і він працює нормально. За допомогою chUser.LastUpdatedI перевіряю, це новий запис чи лише зміна. Оскільки перезавантажити запис, якого ще немає в базі даних, неможливо.

// 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()ви можете видалити ті, які можуть спричинити помилку.
Ян Лойенбергер

1
Але наголос на питанні полягає в тому, скільки вставок / оновлень ефективно зробити за один SaveChangesдзвінок. Ви не вирішуєте цю проблему. Зверніть увагу, що існує більше потенційних причин збою SaveChanges, ніж помилок перевірки. До речі, ви також можете просто позначати сутності як Unchangedзамість того, щоб перезавантажувати / видаляти їх.
Герт Арнольд

1
Ви маєте рацію, це не стосується безпосередньо питання, але я думаю, що більшість людей, що спотикаються про цю тему, мають проблеми з перевіркою, хоча є й інші причини, які SaveChangesне вдаються. І це вирішує проблему. Якщо ця публікація насправді заважає вам у цій темі, я можу видалити це, моя проблема вирішена, я просто намагаюся допомогти іншим.
Ян Лойенбергер,

У мене є питання щодо цього. Коли ви телефонуєте GetValidationErrors(), це "підробляє" дзвінок до бази даних і отримує помилки чи що? Дякуємо за відповідь :)
Жанкарло Фонталво
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.