Cofnij zmiany w jednostkach struktury encji

116

może to być trywialne pytanie, ale: Ponieważ struktura encji ADO.NET automatycznie śledzi zmiany (w wygenerowanych jednostkach) i dlatego zachowuje oryginalne wartości, w jaki sposób mogę wycofać zmiany wprowadzone w obiektach jednostek?

Mam formularz, który umożliwia użytkownikowi edycję zbioru encji „Klient” w widoku siatki.

Teraz mam dwa przyciski „Akceptuj” i „Przywróć”: po kliknięciu „Akceptuj” wywołuję, Context.SaveChanges()a zmienione obiekty są zapisywane z powrotem do bazy danych. Jeśli kliknięto „Przywróć”, chciałbym, aby wszystkie obiekty otrzymały oryginalne wartości właściwości. Jaki byłby na to kod?

Dzięki

MartinStettner
źródło

Odpowiedzi:

69

W EF nie ma operacji przywracania ani anulowania zmian. Każda jednostka ma ObjectStateEntryw ObjectStateManager. Wpis stanu zawiera wartości oryginalne i rzeczywiste, więc można użyć oryginalnych wartości do nadpisania bieżących wartości, ale należy to zrobić ręcznie dla każdej jednostki. Nie będzie odzwierciedlać zmian we właściwościach / relacjach nawigacji.

Typowym sposobem „cofania zmian” jest usuwanie kontekstu i ponowne ładowanie jednostek. Jeśli chcesz uniknąć ponownego ładowania, musisz utworzyć klony obiektów i zmodyfikować te klony w nowym kontekście obiektu. Jeśli użytkownik anuluje zmiany, nadal będziesz mieć oryginalne encje.

Ladislav Mrnka
źródło
4
@LadislavMrnka Na pewno Context.Refresh()jest kontrprzykładem na twoje twierdzenie, że nie ma operacji przywracania? Stosowanie Refresh()wydaje się lepszym podejściem (tj. Łatwiejszym do nakierowania na określone podmioty) niż pozbycie się kontekstu i utrata wszystkich śledzonych zmian.
Rob
14
@robjb: Nie. Odświeżanie może odświeżyć tylko pojedynczą jednostkę lub kolekcję jednostek, które definiujesz ręcznie, ale funkcja odświeżania wpływa tylko na proste właściwości (nie na relacje). Nie rozwiązuje również problemu z dodanymi lub usuniętymi jednostkami.
Ladislav Mrnka
153

Zapytanie ChangeTracker o DbContext dla brudnych elementów. Ustaw stan elementów usuniętych na niezmieniony i elementy dodane do odłączenia. W przypadku zmodyfikowanych pozycji użyj oryginalnych wartości i ustaw bieżące wartości pozycji. Na koniec ustaw stan zmodyfikowanego wpisu na niezmieniony:

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;
        }
    }
 }
Taher Rahgooy
źródło
3
Dzięki - to mi naprawdę pomogło!
Matt
5
Prawdopodobnie powinieneś ustawić oryginalne wartości również dla usuniętych wpisów. Możliwe, że najpierw zmieniłeś element, a potem go usunąłeś.
Bas de Raad
22
Ustawienie Statedo EntityState.Unchanged zastąpi wszystkie wartości z Original Valuestak dobrze, więc nie ma potrzeby, aby zadzwonić do SetValuesmetody.
Abolfazl Hosnoddin
10
Czystsza wersja tej odpowiedzi: stackoverflow.com/a/22098063/2498426
Jerther
1
Stary, to jest niesamowite! Jedyną modyfikacją, którą wprowadziłem, jest użycie ogólnej wersji Entries <T> (), aby działała ona w moich repozytoriach. Daje mi to większą kontrolę i mogę wycofać zmiany w zależności od typu jednostki. Dzięki!
Daniel Mackay
33
dbContext.Entry(entity).Reload();

Zgodnie z MSDN :

Ponownie ładuje jednostkę z bazy danych, nadpisując wszystkie wartości właściwości wartościami z bazy danych. Jednostka będzie w stanie niezmienionym po wywołaniu tej metody.

Zwróć uwagę, że powrót przez żądanie do bazy danych ma pewne wady:

  • ruch sieciowy
  • Przeciążenie DB
  • zwiększony czas odpowiedzi aplikacji
Lu55
źródło
17

To zadziałało dla mnie:

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

Gdzie itemjest jednostka klienta, która ma zostać cofnięta.

Matyas
źródło
12

Łatwy sposób bez śledzenia zmian. Powinno być szybsze niż patrzenie na wszystkie podmioty.

public void Rollback()
{
    dataContext.Dispose();
    dataContext= new MyEntities(yourConnection);
}
Guish
źródło
Czas tworzenia pojedynczego obiektu uprawnień ... który wynosi kilka ms (50 ms). Zapętlanie kolekcji może być szybsze lub dłuższe w zależności od jej rozmiaru. Jeśli chodzi o wydajność, O (1) rzadko jest problemem w porównaniu do O (n). Notacja Big O
Guish
Nie podążanie za tobą - wykonanie usuwania i ponownego tworzenia połączenia. Przetestowałem to na istniejącym projekcie i zakończyło się nieco szybciej niż powyższa Rollbackprocedura, co czyni go znacznie lepszym wyborem, jeśli chce się przywrócić stan całej bazy danych. Rollback może mimo wszystko wybierać.
majkinetor
„n” oznacza liczbę obiektów. Ponowne odtworzenie połączenia zajmuje około 50 ms ... O (1) oznacza, że ​​jest to zawsze ten sam czas 50ms+0*n= 50ms. O (n) oznacza, że ​​wydajność zależy od liczby obiektów ... wydajność może być 2ms+0.5ms*n... więc poniżej 96 obiektów byłoby szybciej, ale czas wzrósłby liniowo wraz z ilością danych.
Guish
Jeśli nie zamierzasz wybierać tego, co jest (nie) wycofane, jest to droga, pod warunkiem, że nie martwisz się o przepustowość.
Anthony Nichols
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; 
    } 
} 

U mnie zadziałało. Musisz jednak ponownie załadować dane z kontekstu, aby przywrócić stare dane. Źródło tutaj

Alejandro del Río
źródło
3

„To zadziałało dla mnie:

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

Gdzie itemjest jednostka klienta, która ma zostać przywrócona. "


Wykonałem testy z ObjectContext.Refresh w SQL Azure, a „RefreshMode.StoreWins” odpala zapytanie do bazy danych dla każdej jednostki i powoduje przeciek wydajności. Na podstawie dokumentacji Microsoft ():

ClientWins: zmiany właściwości wprowadzone w obiektach w kontekście obiektu nie są zastępowane wartościami ze źródła danych. Przy następnym wywołaniu SaveChanges te zmiany są wysyłane do źródła danych.

StoreWins: zmiany właściwości wprowadzone w obiektach w kontekście obiektu są zastępowane wartościami ze źródła danych.

ClientWins też nie jest dobrym pomysłem, ponieważ odpalenie .SaveChanges spowoduje „odrzucenie” zmian w źródle danych.

Nie wiem jeszcze, jaki jest najlepszy sposób, ponieważ usunięcie kontekstu i utworzenie nowego powoduje wyjątek z komunikatem: „Podstawowy dostawca nie powiódł się przy otwieraniu”, gdy próbuję uruchomić dowolne zapytanie w nowo utworzonym kontekście.

pozdrowienia,

Henrique Clausing

Henrique Clausing Cunha
źródło
2

Jak dla mnie lepszą metodą jest ustawienie EntityState.Unchangedna każdym obiekcie, na którym chcesz cofnąć zmiany. Zapewnia to cofnięcie zmian w FK i ma nieco bardziej przejrzystą składnię.

Naz
źródło
4
Uwaga: zmiany powrócą, jeśli jednostka zostanie ponownie zmieniona.
Nick Whaley
2

Okazało się, że działa dobrze w moim kontekście:

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

Alex Pop
źródło
1
Uważam, że zapobiegnie to utrwalaniu zmian w jednostce po wywołaniu DbContext.SaveChanges(), ale nie zwróci wartości jednostki do oryginalnych wartości. A jeśli stan encji zostanie zmodyfikowany po późniejszej zmianie, być może wszystkie poprzednie modyfikacje zostaną zachowane po zapisaniu?
Carl G
1
Sprawdź ten link code.msdn.microsoft.com/How-to-undo-the-changes-in-00aed3c4 Mówi, że ustawienie jednostki w stan Unchaged przywraca oryginalne wartości „pod osłonami”.
Hannish
2

To jest przykład tego, o czym mówi Mrnka. Poniższa metoda zastępuje bieżące wartości jednostki oryginalnymi wartościami i nie wywołuje bazy danych. Robimy to, używając właściwości OriginalValues ​​DbEntityEntry i używamy odbicia do ustawiania wartości w sposób ogólny. (Działa od 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
źródło
1

Używamy EF 4 z kontekstem Legacy Object. Żadne z powyższych rozwiązań nie odpowiadało mi bezpośrednio na to - chociaż na dłuższą metę NIE odpowiadało, popychając mnie we właściwym kierunku.

Nie możemy po prostu pozbyć się i odbudować kontekstu, ponieważ niektóre z obiektów, które trzymamy w pamięci (do cholery, to leniwe ładowanie !!) są nadal dołączone do kontekstu, ale mają dzieci, które jeszcze nie zostały załadowane. W takich przypadkach musimy przywrócić wszystko do oryginalnych wartości bez wbijania bazy danych i bez porzucania istniejącego połączenia.

Poniżej znajduje się nasze rozwiązanie tego samego problemu:

    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);
            }
        }
    }

Mam nadzieję, że to pomoże innym.

Nocnik
źródło
0

Kilka dobrych pomysłów powyżej, zdecydowałem się zaimplementować ICloneable, a następnie prostą metodę rozszerzenia.

Znaleziono tutaj: Jak sklonować listę ogólną w języku C #?

Do wykorzystania jako:

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

W ten sposób mogłem sklonować listę jednostek produktów, zastosować rabat do każdej pozycji i nie musiałem martwić się o cofnięcie jakichkolwiek zmian w pierwotnej jednostce. Nie ma potrzeby rozmawiać z DBContext i prosić o odświeżenie lub pracę z ChangeTracker. Można powiedzieć, że nie wykorzystuję w pełni EF6, ale jest to bardzo ładna i prosta implementacja, która pozwala uniknąć trafienia w DB. Nie mogę powiedzieć, czy jest to hit wydajnościowy.

IbrarMumtaz
źródło