Annuler les modifications dans les entités du cadre d'entité

116

cela peut être une question triviale mais: Étant donné que l'infrastructure d'entité ADO.NET suit automatiquement les modifications (dans les entités générées) et conserve donc les valeurs d'origine, comment puis-je annuler les modifications apportées aux objets d'entité?

J'ai un formulaire qui permet à l'utilisateur d'éditer un ensemble d'entités "Client" dans une vue en grille.

Maintenant, j'ai deux boutons "Accepter" et "Revenir": si "Accepter" est cliqué, j'appelle Context.SaveChanges()et les objets modifiés sont réécrits dans la base de données. Si vous cliquez sur "Revert", je voudrais que tous les objets obtiennent leurs valeurs de propriété d'origine. Quel serait le code pour cela?

Merci

MartinStettner
la source

Réponses:

69

Il n'y a pas d'opération de retour ou d'annulation des modifications dans EF. Chaque entité a ObjectStateEntryen ObjectStateManager. L'entrée d'état contient les valeurs d'origine et réelles afin que vous puissiez utiliser les valeurs d'origine pour écraser les valeurs actuelles, mais vous devez le faire manuellement pour chaque entité. Il ne rétablira pas les modifications des propriétés / relations de navigation.

La méthode courante pour «annuler les modifications» consiste à supprimer le contexte et à recharger les entités. Si vous souhaitez éviter le rechargement, vous devez créer des clones d'entités et modifier ces clones dans un nouveau contexte d'objet. Si l'utilisateur annule les modifications, vous aurez toujours les entités d'origine.

Ladislav Mrnka
la source
4
@LadislavMrnka Context.Refresh()est sûrement un contre-exemple à votre affirmation selon laquelle il n'y a pas d'opération de retour? L'utilisation Refresh()semble être une meilleure approche (c'est-à-dire ciblée plus facilement sur des entités spécifiques) que d'éliminer le contexte et de perdre tous les changements suivis.
Rob
14
@robjb: Non. L'actualisation ne peut actualiser qu'une seule entité ou un ensemble d'entités que vous définissez manuellement, mais la fonctionnalité d'actualisation n'affecte que les propriétés simples (pas les relations). Cela ne résout pas non plus le problème avec les entités ajoutées ou supprimées.
Ladislav Mrnka
153

Requête ChangeTracker de DbContext pour les éléments sales. Définissez l'état des éléments supprimés sur inchangé et les éléments ajoutés sur détachés. Pour les éléments modifiés, utilisez les valeurs d'origine et définissez les valeurs actuelles de l'entrée. Enfin, définissez l'état de l'entrée modifiée sur inchangé:

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
la source
3
Merci - cela m'a vraiment aidé!
Matt
5
Vous devriez probablement également définir les valeurs d'origine sur les entrées supprimées. Il est possible que vous ayez d'abord modifié un élément et que vous l'ayez ensuite supprimé.
Bas de Raad
22
La définition Statede EntityState.Unchanged remplacera toutes les valeurs avec Original Valuesainsi il n'est pas nécessaire d'appeler la SetValuesméthode.
Abolfazl Hosnoddin
10
Version plus propre de cette réponse: stackoverflow.com/a/22098063/2498426
Jerther
1
Mate, c'est génial! La seule modification que j'ai faite est d'utiliser la version générique d'Entrées <T> () afin qu'elle fonctionne pour mes référentiels. Cela me donne plus de contrôle et je peux revenir en arrière par type d'entité. Merci!
Daniel Mackay
33
dbContext.Entry(entity).Reload();

Accession à MSDN :

Recharge l'entité à partir de la base de données en écrasant toutes les valeurs de propriété par les valeurs de la base de données. L'entité sera dans l'état inchangé après l'appel de cette méthode.

Notez que le retour de la requête à la base de données présente certains inconvénients:

  • trafic réseau
  • Surcharge DB
  • l'augmentation du temps de réponse des applications
Lu55
la source
17

Cela a fonctionné pour moi:

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

itemest l'entité client à rétablir.

Matyas
la source
12

Un moyen facile sans suivre les modifications. Cela devrait être plus rapide que de regarder toutes les entités.

public void Rollback()
{
    dataContext.Dispose();
    dataContext= new MyEntities(yourConnection);
}
Guish
la source
Temps de création d'un objet de droit unique ... qui est de quelques ms (50 ms). Faire une boucle dans la collection peut être plus rapide ou plus longue en fonction de sa taille. En termes de performances, O (1) est rarement un problème par rapport à O (n). Notation Big O
Guish
Ne pas vous suivre - performances de suppression et de recréation de la connexion. Je l'ai testé sur un projet existant et il s'est terminé un peu plus rapidement que la Rollbackprocédure ci-dessus , ce qui en fait un bien meilleur choix si l'on veut rétablir l'état de la base de données entière. Le retour en arrière pourrait choisir Tho.
majkinetor
«n» signifie le nombre d'objets. La recréation de la connexion prend environ 50 ms ... O (1) signifie que c'est toujours la même heure 50ms+0*n= 50ms. O (n) signifie que la performance est influencée par le nombre d'objets ... la performance est peut-être 2ms+0.5ms*n... donc en dessous de 96 objets, ce serait plus rapide mais le temps augmenterait linéairement avec la quantité de données.
Guish
Si vous n'allez pas choisir ce qui est (non) annulé, c'est la voie à suivre à condition que vous ne soyez pas préoccupé par la bande passante.
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; 
    } 
} 

Cela a fonctionné pour moi. Cependant, vous devez recharger vos données à partir du contexte pour ramener les anciennes données. Source ici

Alejandro del Río
la source
3

"Cela a fonctionné pour moi:

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

Où se itemtrouve l'entité cliente à rétablir. "


J'ai fait des tests avec ObjectContext.Refresh dans SQL Azure, et le "RefreshMode.StoreWins" déclenche une requête sur la base de données pour chaque entité et provoque une fuite de performances. Basé sur la documentation Microsoft ():

ClientWins: les modifications de propriété apportées aux objets dans le contexte de l'objet ne sont pas remplacées par les valeurs de la source de données. Lors du prochain appel à SaveChanges, ces modifications sont envoyées à la source de données.

StoreWins: les modifications de propriété apportées aux objets dans le contexte de l'objet sont remplacées par les valeurs de la source de données.

ClientWins n'est pas non plus une bonne idée, car le déclenchement de .SaveChanges validera les modifications "ignorées" dans la source de données.

Je ne sais pas encore quel est le meilleur moyen, car l'élimination du contexte et la création d'un nouveau sont provoquées une exception avec le message: "Le fournisseur sous-jacent a échoué à l'ouverture" lorsque j'essaye d'exécuter une requête sur un nouveau contexte créé.

Cordialement,

Henrique Clausing

Henrique Clausing Cunha
la source
2

Quant à moi, la meilleure méthode pour le faire est de définir EntityState.Unchangedchaque entité sur laquelle vous souhaitez annuler les modifications. Cela garantit que les modifications sont annulées sur FK et ont une syntaxe un peu plus claire.

Naz
la source
4
Remarque: les modifications reviendront si l'entité est à nouveau modifiée.
Nick Whaley
2

J'ai trouvé que cela fonctionnait bien dans mon contexte:

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

Alex Pop
la source
1
Je crois que cela empêchera les modifications apportées à l'entité de persister lors de l'appel DbContext.SaveChanges(), mais cela ne renverra pas les valeurs d'entité aux valeurs d'origine. Et si l'état de l'entité est modifié à partir d'un changement ultérieur, peut-être que toutes les modifications précédentes seront conservées lors de l'enregistrement?
Carl G
1
Vérifiez ce lien code.msdn.microsoft.com/How-to-undo-the-changes-in-00aed3c4 Il indique que la définition d'une entité à l'état Unchaged restaure les valeurs d'origine "sous les couvertures".
Hannish
2

C'est un exemple de ce dont parle Mrnka. La méthode suivante avec les valeurs actuelles écrase les valeurs d' origine et d'une entité n'appelle la base de données. Pour ce faire, nous utilisons la propriété OriginalValues ​​de DbEntityEntry et utilisons la réflexion pour définir des valeurs de manière générique. (Cela fonctionne à partir d'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
la source
1

Nous utilisons EF 4, avec le contexte d'objet hérité. Aucune des solutions ci-dessus n'a répondu directement à cela pour moi - même si elle a répondu à long terme en me poussant dans la bonne direction.

Nous ne pouvons pas simplement supprimer et reconstruire le contexte car certains des objets que nous avons en mémoire (putain de chargement paresseux !!) sont toujours attachés au contexte mais ont des enfants qui ne sont pas encore chargés. Dans ces cas, nous devons tout ramener à leurs valeurs d'origine sans marteler la base de données et sans abandonner la connexion existante.

Voici notre solution à ce même problème:

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

J'espère que cela aide les autres.

Jerry
la source
0

Quelques bonnes idées ci-dessus, j'ai choisi d'implémenter ICloneable puis une méthode d'extension simple.

Trouvé ici: Comment cloner une liste générique en C #?

A utiliser comme:

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

De cette façon, j'ai pu cloner ma liste d'entités de produits, appliquer une réduction à chaque article et ne pas avoir à me soucier d'annuler les modifications apportées à l'entité d'origine. Pas besoin de parler avec le DBContext et de demander une actualisation ou de travailler avec le ChangeTracker. Vous pourriez dire que je n'utilise pas pleinement EF6, mais il s'agit d'une implémentation très simple et très simple qui évite un coup DB. Je ne peux pas dire si cela a un impact sur les performances.

IbrarMumtaz
la source