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

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.


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

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

17

To zadziałało dla mnie:

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

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


12

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

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

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


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


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ę.


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


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

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.


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.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.