Deshacer cambios en las entidades del marco de la entidad

116

esta podría ser una pregunta trivial, pero: dado que ADO.NET entity framework rastrea automáticamente los cambios (en las entidades generadas) y, por lo tanto, mantiene los valores originales, ¿cómo puedo revertir los cambios realizados en los objetos de la entidad?

Tengo un formulario que permite al usuario editar un conjunto de entidades "Cliente" en una vista de cuadrícula.

Ahora tengo dos botones "Aceptar" y "Revertir": si se hace clic en "Aceptar", llamo Context.SaveChanges()y los objetos modificados se vuelven a escribir en la base de datos. Si se hace clic en "Revertir", me gustaría que todos los objetos obtengan sus valores de propiedad originales. ¿Cuál sería el código para eso?

Gracias

MartinStettner
fuente

Respuestas:

69

No hay operación de reversión o cancelación de cambios en EF. Cada entidad tiene ObjectStateEntryen ObjectStateManager. La entrada de estado contiene valores originales y reales, por lo que puede usar valores originales para sobrescribir los valores actuales, pero debe hacerlo manualmente para cada entidad. No respetará los cambios en las propiedades / relaciones de navegación.

La forma común de "revertir los cambios" es eliminar el contexto y recargar las entidades. Si desea evitar la recarga, debe crear clones de entidades y modificar esos clones en un nuevo contexto de objeto. Si el usuario cancela los cambios, aún tendrá entidades originales.

Ladislav Mrnka
fuente
4
@LadislavMrnka ¿Seguramente Context.Refresh()es un contraejemplo a su afirmación de que no hay operación de reversión? Usar Refresh()parece un mejor enfoque (es decir, más fácilmente dirigido a entidades específicas) que eliminar el contexto y perder todos los cambios registrados.
Rob
14
@robjb: No. Actualizar solo puede actualizar una sola entidad o colección de entidades que usted define manualmente, pero la funcionalidad de actualización solo afecta propiedades simples (no relaciones). Tampoco resuelve el problema con las entidades agregadas o eliminadas.
Ladislav Mrnka
153

Consulta ChangeTracker de DbContext para elementos sucios. Establezca el estado de los elementos eliminados como sin cambios y los elementos agregados como separados. Para los elementos modificados, utilice los valores originales y establezca los valores actuales de la entrada. Finalmente, establezca el estado de la entrada modificada como sin cambios:

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
fuente
3
Gracias, ¡esto realmente me ayudó!
Matt
5
Probablemente también debería establecer los valores originales para las entradas eliminadas. Es posible que primero haya cambiado un elemento y luego lo haya eliminado.
Bas de Raad
22
La configuración Stateen EntityState.Unchanged también anulará todos los valores, por Original Valueslo que no es necesario llamar al SetValuesmétodo.
Abolfazl Hosnoddin
10
Versión más limpia de esta respuesta: stackoverflow.com/a/22098063/2498426
Jerther
1
¡Amigo, esto es increíble! La única modificación que hice fue usar la versión genérica de Entries <T> () para que funcione para mis repositorios. Esto me da más control y puedo revertir por tipo de entidad. ¡Gracias!
Daniel Mackay
33
dbContext.Entry(entity).Reload();

De acuerdo con MSDN :

Recarga la entidad de la base de datos sobrescribiendo cualquier valor de propiedad con valores de la base de datos. La entidad estará en el estado Sin cambios después de llamar a este método.

Tenga en cuenta que volver a través de la solicitud a la base de datos tiene algunos inconvenientes:

  • tráfico de red
  • Sobrecarga de base de datos
  • el mayor tiempo de respuesta de la aplicación
Lu55
fuente
17

Esto funcionó para mí:

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

Dónde itemse revertirá la entidad cliente.

Matyas
fuente
12

Manera fácil sin realizar un seguimiento de los cambios. Debería ser más rápido que mirar todas las entidades.

public void Rollback()
{
    dataContext.Dispose();
    dataContext= new MyEntities(yourConnection);
}
Guish
fuente
Tiempo de creación de un único objeto de entidad ... que es un par de ms (50 ms). Recorrer la colección puede ser más rápido o más largo dependiendo de su tamaño. En cuanto al rendimiento, O (1) rara vez es un problema en comparación con O (n). Notación Big O
Guish
No seguirte - desempeño de deshacerse y recrear la conexión. Lo probé en un proyecto existente y terminó un poco más rápido que el Rollbackprocedimiento anterior , lo que lo convierte en una opción mucho mejor si uno quiere revertir el estado completo de la base de datos. Rollback podría elegir bien.
Majkinetor
'n' significa el número de objetos. La recreación de la conexión tarda unos 50 ms ... O (1) significa que siempre es el mismo tiempo 50ms+0*n= 50ms. O (n) significa que el rendimiento está influenciado por la cantidad de objetos ... el rendimiento es quizás 2ms+0.5ms*n... así que debajo de 96 objetos sería más rápido, pero el tiempo aumentaría linealmente con la cantidad de datos.
Guish
Si no va a elegir lo que se revierte (no), este es el camino a seguir, siempre que no esté preocupado por el ancho de banda.
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; 
    } 
} 

Funcionó para mí. Sin embargo, debe volver a cargar sus datos desde el contexto para traer los datos antiguos. Fuente aquí

Alejandro del Río
fuente
3

"Esto funcionó para mí:

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

¿Dónde itemse revertirá la entidad cliente? ".


He hecho pruebas con ObjectContext.Refresh en SQL Azure, y "RefreshMode.StoreWins" dispara una consulta contra la base de datos para cada entidad y causa una pérdida de rendimiento. Basado en documentación de microsoft ():

ClientWins: los cambios de propiedad realizados en los objetos en el contexto del objeto no se reemplazan con valores del origen de datos. En la próxima llamada a SaveChanges, estos cambios se envían a la fuente de datos.

StoreWins: los cambios de propiedad realizados en los objetos en el contexto del objeto se reemplazan con valores del origen de datos.

ClientWins tampoco es una buena idea, porque la activación de .SaveChanges enviará cambios "descartados" a la fuente de datos.

No sé cuál es la mejor manera todavía, porque eliminar el contexto y crear uno nuevo genera una excepción con el mensaje: "El proveedor subyacente falló al abrirse" cuando intento ejecutar cualquier consulta en un nuevo contexto creado.

Saludos,

Henrique Clausing

Henrique Clausing Cunha
fuente
2

En cuanto a mí, el mejor método para hacerlo es establecer EntityState.Unchangeden cada entidad en la que desea deshacer los cambios. Esto asegura que los cambios se reviertan en FK y tiene una sintaxis un poco más clara.

Naz
fuente
4
Nota: Los cambios volverán si la entidad se cambia nuevamente.
Nick Whaley
2

Encontré que esto funciona bien en mi contexto:

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

Alex Pop
fuente
1
Creo que esto evitará que los cambios en la entidad persistan al llamar DbContext.SaveChanges(), pero no devolverá los valores de la entidad a los valores originales. Y si el estado de la entidad se modifica a partir de un cambio posterior, ¿posiblemente todas las modificaciones anteriores se mantendrán al guardar?
Carl G
1
Verifique este enlace code.msdn.microsoft.com/How-to-undo-the-changes-in-00aed3c4 Dice que establecer una entidad en el estado Unchaged restaura los valores originales "bajo las sábanas".
Hannish
2

Este es un ejemplo de lo que habla Mrnka. El siguiente método sobrescribe los valores actuales de una entidad con los valores originales y no llama a la base de datos. Hacemos esto haciendo uso de la propiedad OriginalValues ​​de DbEntityEntry y usamos la reflexión para establecer valores de una manera genérica. (Esto funciona a partir de 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
fuente
1

Estamos usando EF 4, con el contexto de objetos heredados. Ninguna de las soluciones anteriores me respondió directamente a esto, aunque sí lo hizo a largo plazo empujándome en la dirección correcta.

No podemos simplemente deshacernos y reconstruir el contexto porque algunos de los objetos que tenemos en la memoria (¡¡maldita sea la carga diferida !!) todavía están adjuntos al contexto pero tienen hijos que aún no se han cargado. Para estos casos, debemos restablecer todo a los valores originales sin martillar la base de datos y sin eliminar la conexión existente.

A continuación se muestra nuestra solución a este mismo problema:

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

Espero que esto ayude a otros.

alemán
fuente
0

Algunas buenas ideas anteriores, elegí implementar ICloneable y luego un método de extensión simple.

Encontrado aquí: ¿Cómo clono una lista genérica en C #?

Para ser utilizado como:

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

De esta manera pude clonar mi lista de entidades de productos, aplicar un descuento a cada artículo y no tener que preocuparme por revertir los cambios en la entidad original. No es necesario hablar con DBContext y solicitar una actualización o trabajar con ChangeTracker. Podría decirse que no estoy haciendo un uso completo de EF6, pero esta es una implementación muy agradable y simple y evita un golpe de DB. No puedo decir si esto tiene un impacto en el rendimiento.

IbrarMumtaz
fuente