Скасувати зміни в сутностях фреймворку


116

це може бути тривіальним питанням, але: Оскільки структура сутності ADO.NET автоматично відстежує зміни (у створених об'єктах) і, таким чином, зберігає початкові значення, то як я можу відкатати зміни, внесені до об'єктів сутності?

У мене є форма, яка дозволяє користувачеві редагувати набір об'єктів "Клієнт" у вигляді сітки.

Тепер у мене є дві кнопки «Прийняти» та «Повернути»: якщо натиснути «Прийняти», я зателефоную, Context.SaveChanges()і змінені об’єкти записуються назад у базу даних. Якщо натиснути кнопку "Повернути", я б хотів, щоб усі об'єкти отримали свої початкові значення властивостей. Який би був код для цього?

Дякую

Відповіді:


69

У EF не відбувається операція відновлення чи скасування змін. Кожна організація має ObjectStateEntryв ObjectStateManager. Запис у штаті містить оригінальні та фактичні значення, тому ви можете використовувати початкові значення для перезапису поточних значень, але це потрібно робити вручну для кожної сутності. Він не буде шанувати зміни властивостей / відносин навігації.

Поширений спосіб "повернути зміни" - це розміщення контексту та перезавантаження об'єктів. Якщо ви хочете уникнути перезавантаження, ви повинні створити клони сутностей і змінити ці клони в новому контексті об'єкта. Якщо користувач скасує зміни, у вас все ще будуть оригінальні сутності.


4
@LadislavMrnka Безумовно, Context.Refresh()є протилежним прикладом вашого твердження про відсутність операції відновлення? Використання Refresh()здається кращим підходом (тобто легше орієнтуватися на конкретні суб'єкти), ніж розміщення контексту та втрата всіх відслідковуваних змін.
— Роб

14
@robjb: Ні. Refresh може оновити лише одне ціле об'єднання або колекцію сутностей, яке ви вручну визначаєте, але функція оновлення впливає лише на прості властивості (не на відносини). Це також не вирішує проблему з доданими або видаленими об'єктами.
— Ladislav Mrnka

153

Запит запитів ChangeTracker DbContext щодо брудних предметів. Встановіть стан видалених елементів як незмінний, а додані - до від'єднаних. Для модифікованих елементів використовуйте початкові значення та встановлюйте поточні значення запису. Нарешті встановіть стан зміненого запису незмінним:

public void RollBack()
{
    var context = DataContextFactory.GetDataContext();
    var changedEntries = context.ChangeTracker.Entries()
        .Where(x => x.State != EntityState.Unchanged).ToList();

    foreach (var entry in changedEntries)
    {
        switch(entry.State)
        {
            case EntityState.Modified:
                entry.CurrentValues.SetValues(entry.OriginalValues);
                entry.State = EntityState.Unchanged;
                break;
            case EntityState.Added:
                entry.State = EntityState.Detached;
                break;
            case EntityState.Deleted:
                entry.State = EntityState.Unchanged;
                break;
        }
    }
 }

3
Спасибі - це мені справді допомогло!
— Метт

5
Напевно, ви також повинні встановити вихідні значення також для видалених записів. Можливо, ви спершу змінили елемент і видалили його після цього.
— Бас де Раад

22
Налаштування Stateдля EntityState.Unchanged також замінить усі значення Original Values, тому немає необхідності викликати SetValuesметод.
— Абольфазл Госноддін

10
Прибиральник версія цієї відповіді: stackoverflow.com/a/22098063/2498426
— Jerther

1
Мате, це приголомшливо! Єдина зміна, яку я вніс, - це використовувати загальну версію Entries <T> (), щоб вона працювала для моїх сховищ. Це дає мені більше контролю, і я можу відкотитись за типом сутності. Дякую!
— Даніель Макей

33
dbContext.Entry(entity).Reload();

Затвердження до MSDN :

Перезавантажує об'єкт із бази даних, перезаписуючи будь-які значення властивостей зі значеннями з бази даних. Після виклику цього методу суб'єкт перебуватиме в незмінному стані.

Зауважте, що повернення через запит до бази даних має деякі недоліки:

  • мережевий трафік
  • Перевантаження БД
  • збільшений час відповіді програми

17

Це працювало для мене:

dataContext.customer.Context.Refresh(RefreshMode.StoreWins, item);

Де itemсуб'єкт замовника повинен бути повернутий.


12

Простий спосіб без відстеження змін. Це має бути швидше, ніж дивитися на всі сутності.

public void Rollback()
{
    dataContext.Dispose();
    dataContext= new MyEntities(yourConnection);
}

Час створення єдиного об’єкта права ... який становить пару мс (50 мс). Прокручування колекції може бути швидшою чи довшою, залежно від її розміру. Ефективність роботи O (1) рідко є проблемою порівняно з O (n). Велика нотація
— Guish

Не слідкуючи за вами - виконання утилізації та відтворення з'єднання. Я перевірив його на існуючому проекті, і він закінчився дещо швидше, ніж вище Rollbackпроцедури, що робить його набагато кращим вибором, якщо хочеться відновити весь стан бази даних. Відкат може вишне вибирати тхо.
— majkinetor

'n' означає кількість об'єктів. Відтворення з'єднання займає близько 50 мс ... O (1) означає, що це завжди той самий час 50ms+0*n= 50ms. O (n) означає, що на продуктивність впливає кількість об'єктів ... на ефективність можливо 2ms+0.5ms*n... тому нижче 96 об'єктів було б швидше, але час збільшуватиметься лінійно із кількістю даних.
— Гайш

Якщо ви не збираєтеся вибирати вишню, що повертається назад, це шлях, якщо ви не турбуєтесь про пропускну здатність.
— Ентоні Ніколс

6
// Undo the changes of all entries. 
foreach (DbEntityEntry entry in context.ChangeTracker.Entries()) 
{ 
    switch (entry.State) 
    { 
        // Under the covers, changing the state of an entity from  
        // Modified to Unchanged first sets the values of all  
        // properties to the original values that were read from  
        // the database when it was queried, and then marks the  
        // entity as Unchanged. This will also reject changes to  
        // FK relationships since the original value of the FK  
        // will be restored. 
        case EntityState.Modified: 
            entry.State = EntityState.Unchanged; 
            break; 
        case EntityState.Added: 
            entry.State = EntityState.Detached; 
            break; 
        // If the EntityState is the Deleted, reload the date from the database.   
        case EntityState.Deleted: 
            entry.Reload(); 
            break; 
        default: break; 
    } 
} 

Це працювало для мене. Однак ви повинні перезавантажити свої дані з контексту, щоб принести старі дані. Джерело тут


3

"Це працювало для мене:

dataContext.customer.Context.Refresh(RefreshMode.StoreWins, item);

Де itemсуб'єкт замовника має бути повернений ".


Я зробив тести з ObjectContext.Refresh у SQL Azure, і "RefreshMode.StoreWins" запускає запит на базу даних для кожної сутності та викликає витік продуктивності. На основі документації про мікрософт ():

ClientWins: Зміни властивості, внесені до об'єктів у контексті об'єкта, не замінюються значеннями з джерела даних. Під час наступного виклику SaveChanges ці зміни надсилаються до джерела даних.

StoreWins: Зміни властивостей, внесені до об'єктів у контексті об'єкта, замінюються значеннями з джерела даних.

ClientWins також не є хорошою ідеєю, тому що запуск .SaveChanges здійснить "відкинуті" зміни до джерела даних.

Я не знаю, який найкращий спосіб поки що, тому що розміщення контексту та створення нового викликає виняток із повідомленням: "Базовий постачальник не вдається відкрити", коли я намагаюся запустити будь-який запит у новому створеному контексті.

З повагою,

Анріке Клауз


2

Щодо мене, кращий спосіб зробити це - встановити EntityState.Unchangedкожну сутність, на якій потрібно скасувати зміни. Це запевняє, що зміни повернуті на FK і мають трохи більш чіткий синтаксис.


4
Примітка. Зміни повернуться, якщо об'єкт буде змінено знову.
— Нік Уолі

2

Я вважав, що це добре працює в моєму контексті:

Context.ObjectStateManager.ChangeObjectState(customer, EntityState.Unchanged);


1
Я вважаю, що це не дасть змін суті зберігатися під час виклику DbContext.SaveChanges(), але не поверне значення сутності до початкових значень. І якщо стан сутності буде змінено з наступної зміни, можливо, всі попередні модифікації зберігатимуться після збереження?
— Карл Г

1
Перевірте це посилання code.msdn.microsoft.com/How-to-undo-the-changes-in-00aed3c4 У ньому йдеться про те, що встановлення сутності в стан Unchaged відновлює початкові значення "під обкладинками".
— Ганніш

2

Це приклад того, про що говорить Mrnka. Наступний метод перезаписує поточні значення організації з вихідними значеннями і не викликає базу даних. Ми робимо це, використовуючи властивість OriginalValues ​​DbEntityEntry, і використовуємо відображення для загального встановлення значень. (Це працює як EntityFramework 5.0)

/// <summary>
/// Undoes any pending updates 
/// </summary>
public void UndoUpdates( DbContext dbContext )
{
    //Get list of entities that are marked as modified
    List<DbEntityEntry> modifiedEntityList = 
        dbContext.ChangeTracker.Entries().Where(x => x.State == EntityState.Modified).ToList();

    foreach(  DbEntityEntry entity in modifiedEntityList ) 
    {
        DbPropertyValues propertyValues = entity.OriginalValues;
        foreach (String propertyName in propertyValues.PropertyNames)
        {                    
            //Replace current values with original values
            PropertyInfo property = entity.Entity.GetType().GetProperty(propertyName);
            property.SetValue(entity.Entity, propertyValues[propertyName]); 
        }
    }
}

1

Ми використовуємо EF 4 з контекстом Legacy Object. Жодне з перерахованих вище рішень прямо для мене не відповіло - хоча це DID відповіло на це в довгостроковій перспективі, підштовхуючи мене в правильному напрямку.

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

Нижче наше рішення цього ж питання:

    public static void UndoAllChanges(OurEntities ctx)
    {
        foreach (ObjectStateEntry entry in
            ctx.ObjectStateManager.GetObjectStateEntries(~EntityState.Detached))
        {
            if (entry.State != EntityState.Unchanged)
            {
                ctx.Refresh(RefreshMode.StoreWins, entry.Entity);
            }
        }
    }

Я сподіваюся, що це допомагає іншим.


0

Деякі гарні ідеї вище, я вирішив реалізувати ICloneable, а потім простий метод розширення.

Знайдено тут: Як клонувати загальний список у C #?

Для використання в якості:

ReceiptHandler.ApplyDiscountToAllItemsOnReciept(LocalProductsOnReciept.Clone(), selectedDisc);

Таким чином я зміг клонувати свій список організацій товарів, застосувати знижку на кожен товар і не потрібно турбуватися про повернення будь-яких змін у початковій сутності. Не потрібно розмовляти з DBContext і просити оновлення або працювати з ChangeTracker. Ви можете сказати, що я не використовую в повній мірі EF6, але це дуже приємна і проста реалізація і дозволяє уникнути попадання БД. Я не можу сказати, чи це хіт продуктивності.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.