Отменить изменения в сущностях структуры сущностей

116

это может быть тривиальный вопрос, но: Поскольку структура сущностей ADO.NET автоматически отслеживает изменения (в сгенерированных сущностях) и, следовательно, сохраняет исходные значения, как я могу отменить изменения, внесенные в объекты сущностей?

У меня есть форма, которая позволяет пользователю редактировать набор сущностей «Клиент» в виде сетки.

Теперь у меня есть две кнопки «Принять» и «Вернуть»: если щелкнуть «Принять», я вызываю, Context.SaveChanges()и измененные объекты записываются обратно в базу данных. Если щелкнуть «Вернуть», я хотел бы, чтобы все объекты получили свои исходные значения свойств. Какой для этого будет код?

Спасибо

MartinStettner
источник

Ответы:

69

В EF нет операции возврата или отмены изменений. Каждая сущность имеет ObjectStateEntryв ObjectStateManager. Запись состояния содержит исходные и фактические значения, поэтому вы можете использовать исходные значения для перезаписи текущих значений, но вы должны делать это вручную для каждого объекта. Изменения в свойствах / отношениях навигации не будут сохраняться.

Обычный способ «отменить изменения» - удалить контекст и перезагрузить сущности. Если вы хотите избежать перезагрузки, вы должны создать клоны сущностей и изменить эти клоны в контексте нового объекта. Если пользователь отменит изменения, у вас останутся исходные объекты.

Ладислав Мрнка
источник
4
@LadislavMrnka Несомненно Context.Refresh(), это контрпример к вашему утверждению об отсутствии операции возврата? Использование Refresh()кажется лучшим подходом (т.е. более легко нацеленным на определенные объекты), чем удаление контекста и потеря всех отслеживаемых изменений.
Роб
14
@robjb: Нет. Refresh может обновлять только одну сущность или коллекцию сущностей, которые вы определяете вручную, но функциональность обновления влияет только на простые свойства (не отношения). Это также не решает проблему с добавленными или удаленными объектами.
Ладислав Мрнка
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
Вероятно, вам также следует установить исходные значения для удаленных записей. Возможно, вы сначала изменили элемент, а затем удалили его.
Bas de Raad
22
Установка Stateдля EntityState.Unchanged перекроет все значения с Original Values, а поэтому нет необходимости вызова SetValuesметода.
Абольфазл Хосноддин
10
Более чистая
1
Приятель, это круто! Единственная модификация, которую я сделал, - это использование общей версии Entries <T> (), чтобы она работала для моих репозиториев. Это дает мне больше контроля, и я могу выполнять откат для каждого типа объекта. Спасибо!
Дэниел Маккей
33
dbContext.Entry(entity).Reload();

Согласно MSDN :

Повторно загружает объект из базы данных, перезаписывая любые значения свойств значениями из базы данных. После вызова этого метода сущность будет в состоянии Unchanged.

Обратите внимание, что возврат через запрос к базе данных имеет некоторые недостатки:

  • сетевой трафик
  • Перегрузка БД
  • увеличенное время отклика приложения
Lu55
источник
17

Это сработало для меня:

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

Где itemдолжна быть восстановлена ​​организация-покупатель.

Матиас
источник
12

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

public void Rollback()
{
    dataContext.Dispose();
    dataContext= new MyEntities(yourConnection);
}
Guish
источник
Время создания единого объекта прав ... это пара мс (50 мс). Цикл по коллекции может быть быстрее или дольше в зависимости от ее размера. С точки зрения производительности O (1) редко является проблемой по сравнению с O (n). Нотация Big O
Guish
Не следовать за вами - выполнение избавления и воссоздания связи. Я протестировал его на существующем проекте, и он завершился несколько быстрее, чем Rollbackописанная выше процедура, что делает его гораздо лучшим выбором, если кто-то хочет полностью восстановить состояние базы данных. Откат может быть лучше.
majkinetor
«n» означает количество объектов. Восстановление соединения занимает около 50 мс ... O (1) означает, что время всегда одно и то же 50ms+0*n= 50ms. O (n) означает, что на производительность влияет количество объектов ... производительность может быть 2ms+0.5ms*n... так что ниже 96 объектов это будет быстрее, но время будет линейно увеличиваться с объемом данных.
Guish
Если вы не собираетесь выбирать, что (не) откатывается, это способ, если вы не беспокоитесь о пропускной способности.
Энтони Николс,
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 запускает запрос к базе данных для каждого объекта и вызывает утечку производительности. На основе документации Microsoft ():

ClientWins: изменения свойств, внесенные в объекты в контексте объекта, не заменяются значениями из источника данных. При следующем вызове SaveChanges эти изменения отправляются в источник данных.

StoreWins: изменения свойств, внесенные в объекты в контексте объекта, заменяются значениями из источника данных.

ClientWins тоже не является хорошей идеей, потому что запуск .SaveChanges зафиксирует "отклоненные" изменения в источнике данных.

Я еще не знаю, как лучше всего, потому что удаление контекста и создание нового вызывает исключение с сообщением: «Базовый провайдер не удалось открыть», когда я пытаюсь выполнить любой запрос в новом созданном контексте.

С уважением,

Энрике Клаузинг

Энрике Клаузинг Кунья
источник
2

На мой взгляд, лучший способ сделать это - установить EntityState.Unchangedдля каждого объекта, для которого вы хотите отменить изменения. Это гарантирует возврат изменений в FK и имеет немного более понятный синтаксис.

Наз
источник
4
Примечание: изменения вернутся, если объект будет изменен снова.
Nick Whaley
2

Я обнаружил, что это нормально работает в моем контексте:

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

Алекс Поп
источник
1
Я считаю, что это предотвратит сохранение изменений объекта при вызове DbContext.SaveChanges(), но не вернет значения объекта к исходным значениям. И если состояние объекта будет изменено в результате более позднего изменения, возможно, все предыдущие изменения сохранятся при сохранении?
Carl G
1
Проверьте эту ссылку code.msdn.microsoft.com/How-to-undo-the-changes-in-00aed3c4 В ней говорится, что установка объекта в состояние Unchaged восстанавливает исходные значения «под покровом».
Hannish
2

Это пример того, о чем говорит Мрнка. Следующий метод перезаписывает текущие значения объекта исходными значениями и не вызывает базу данных. Мы делаем это, используя свойство 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]); 
        }
    }
}
BizarroDavid
источник
1

Мы используем EF 4 с контекстом устаревшего объекта. Ни одно из вышеперечисленных решений не дало мне прямого ответа на этот вопрос, хотя в конечном итоге оно ДЕЙСТВИТЕЛЬНО ответило на него, подтолкнув меня в правильном направлении.

Мы не можем просто удалить и перестроить контекст, потому что некоторые объекты, которые мы храним в памяти (черт возьми, эта ленивая загрузка !!), все еще прикреплены к контексту, но имеют дочерние элементы, которые еще не загружены. В этих случаях нам нужно вернуть все к исходным значениям, не забивая базу данных и не разрывая существующее соединение.

Ниже представлено наше решение этой же проблемы:

    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 в полной мере, но это очень хорошая и простая реализация, позволяющая избежать попадания в базу данных. Я не могу сказать, влияет ли это на производительность.

ИбрарМумтаз
источник