Desfazer alterações nas entidades da estrutura da entidade

116

esta pode ser uma questão trivial, mas: Visto que a estrutura de entidade ADO.NET rastreia automaticamente as alterações (em entidades geradas) e, portanto, mantém os valores originais, como posso reverter as alterações feitas nos objetos de entidade?

Eu tenho um formulário que permite ao usuário editar um conjunto de entidades "Cliente" em uma visão de grade.

Agora tenho dois botões "Aceitar" e "Reverter": se "Aceitar" for clicado, eu chamo Context.SaveChanges()e os objetos alterados são gravados de volta no banco de dados. Se "Reverter" for clicado, gostaria que todos os objetos obtivessem seus valores de propriedade originais. Qual seria o código para isso?

obrigado

MartinStettner
fonte

Respostas:

69

Não há operação de reversão ou cancelamento de alterações no EF. Cada entidade tem ObjectStateEntryem ObjectStateManager. A entrada de estado contém os valores originais e reais, portanto, você pode usar os valores originais para substituir os valores atuais, mas deve fazer isso manualmente para cada entidade. Não reverterá as alterações nas propriedades / relações de navegação.

A maneira comum de "reverter alterações" é eliminar o contexto e recarregar as entidades. Se quiser evitar o recarregamento, você deve criar clones de entidades e modificar esses clones no novo contexto do objeto. Se o usuário cancelar as alterações, você ainda terá entidades originais.

Ladislav Mrnka
fonte
4
@LadislavMrnka Certamente Context.Refresh()é um contra-exemplo à sua afirmação de que não há operação de reversão? Usar Refresh()parece uma abordagem melhor (ou seja, mais facilmente direcionado a entidades específicas) do que descartar o contexto e perder todas as alterações rastreadas.
Rob
14
@robjb: Não. Atualizar é capaz de atualizar apenas uma única entidade ou coleção de entidades que você define manualmente, mas a funcionalidade de atualização afeta apenas propriedades simples (não relações). Também não resolve o problema com entidades adicionadas ou excluídas.
Ladislav Mrnka
153

Consulte ChangeTracker de DbContext para itens sujos. Defina o estado dos itens excluídos como inalterado e os itens adicionados como desanexados. Para itens modificados, use os valores originais e defina os valores atuais da entrada. Por fim, defina o estado da entrada modificada como inalterado:

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
fonte
3
Obrigado - isso realmente me ajudou!
Matt,
5
Você provavelmente também deve definir os valores originais para entradas excluídas. É possível que você primeiro alterou um item e o excluiu depois disso.
Bas de Raad
22
Definir Statecomo EntityState.Unchanged substituirá todos os valores Original Valuestambém, portanto, não há necessidade de chamar o SetValuesmétodo.
Abolfazl Hosnoddin
10
Versão mais limpa desta resposta: stackoverflow.com/a/22098063/2498426
Jerther
1
Cara, isso é incrível! A única modificação que fiz foi usar a versão genérica de Entries <T> () para que funcione para meus repositórios. Isso me dá mais controle e posso reverter por tipo de entidade. Obrigado!
Daniel Mackay
33
dbContext.Entry(entity).Reload();

Accroding to MSDN :

Recarrega a entidade do banco de dados sobrescrevendo quaisquer valores de propriedade com valores do banco de dados. A entidade estará no estado Inalterado após chamar este método.

Observe que reverter por meio da solicitação para o banco de dados tem algumas desvantagens:

  • tráfego de rede
  • Sobrecarga de banco de dados
  • o aumento do tempo de resposta do aplicativo
Lu55
fonte
17

Isso funcionou para mim:

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

Onde itemestá a entidade do cliente a ser revertida.

Matyas
fonte
12

Maneira fácil sem rastrear quaisquer alterações. Deve ser mais rápido do que olhar para todas as entidades.

public void Rollback()
{
    dataContext.Dispose();
    dataContext= new MyEntities(yourConnection);
}
Guish
fonte
Tempo de criação de um único objeto de serviço ... que é alguns ms (50 ms). O loop pela coleção pode ser mais rápido ou mais longo, dependendo de seu tamanho. O desempenho sábio O (1) raramente é um problema em comparação com O (n). Notação Big O
Guish 02 de
Não te seguindo - desempenho de descartar e recriar conexão. Eu testei em um projeto existente e ele terminou um pouco mais rápido do que o Rollbackprocedimento acima , o que o torna uma escolha muito melhor se alguém deseja reverter todo o estado do banco de dados. A reversão poderia escolher a cereja do bolo.
majkinetor
'n' significa o número de objetos. A recriação da conexão leva cerca de 50 ms ... O (1) significa que é sempre ao mesmo tempo 50ms+0*n= 50ms. O (n) significa que o desempenho é influenciado pelo número de objetos ... o desempenho é talvez 2ms+0.5ms*n... então abaixo de 96 objetos seria mais rápido, mas o tempo aumentaria linearmente com a quantidade de dados.
Guish
Se você não vai escolher o que é (não) revertido, este é o caminho a seguir, desde que você não esteja preocupado com a largura 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; 
    } 
} 

Funcionou para mim No entanto, você deve recarregar seus dados do contexto para trazer os dados antigos. Fonte aqui

Alejandro del Río
fonte
3

"Isso funcionou para mim:

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

Onde itemestá a entidade do cliente a ser revertida. "


Eu fiz testes com ObjectContext.Refresh no SQL Azure e o "RefreshMode.StoreWins" dispara uma consulta no banco de dados para cada entidade e causa um vazamento de desempenho. Com base na documentação da Microsoft ():

ClientWins: as alterações de propriedade feitas em objetos no contexto do objeto não são substituídas por valores da fonte de dados. Na próxima chamada para SaveChanges, essas alterações são enviadas para a fonte de dados.

StoreWins: as alterações de propriedade feitas em objetos no contexto do objeto são substituídas por valores da fonte de dados.

ClientWins também não é uma boa ideia, porque disparar .SaveChanges irá comprometer as alterações "descartadas" na fonte de dados.

Ainda não sei qual é a melhor maneira, porque descartar o contexto e criar um novo causa uma exceção com a mensagem: "O provedor subjacente falhou ao abrir" quando tento executar qualquer consulta em um novo contexto criado.

Saudações,

Henrique Clausing

Henrique Clausing Cunha
fonte
2

Quanto a mim, o melhor método para fazer isso é definir EntityState.Unchangedtodas as entidades nas quais você deseja desfazer as alterações. Isso garante que as alterações sejam revertidas no FK e tenham uma sintaxe um pouco mais clara.

Naz
fonte
4
Nota: As alterações voltarão se a entidade for alterada novamente.
Nick Whaley
2

Achei que isso estava funcionando bem no meu contexto:

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

Alex Pop
fonte
1
Acredito que isso impedirá que as alterações na entidade persistam ao chamar DbContext.SaveChanges(), mas não retornará os valores da entidade aos valores originais. E se o estado da entidade for modificado a partir de uma alteração posterior, possivelmente todas as modificações anteriores persistirão ao salvar?
Carl G
1
Verifique este link code.msdn.microsoft.com/How-to-undo-the-changes-in-00aed3c4 Diz que definir uma entidade para o estado Unchaged restaura os valores originais "nos bastidores".
Hannish,
2

Este é um exemplo do que Mrnka está falando. O método a seguir substitui os valores atuais de uma entidade pelos valores originais e não chama o banco de dados. Fazemos isso usando a propriedade OriginalValues ​​de DbEntityEntry e usamos a reflexão para definir valores de uma maneira genérica. (Funciona a partir do 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
fonte
1

Estamos usando EF 4, com o contexto Legacy Object. Nenhuma das soluções acima respondeu a isso diretamente para mim - embora ela tenha respondido a longo prazo, me empurrando na direção certa.

Não podemos simplesmente descartar e reconstruir o contexto porque alguns dos objetos que temos pendurados na memória (maldito carregamento lento !!) ainda estão anexados ao contexto, mas têm filhos que ainda estão para ser carregados. Para esses casos, precisamos colocar tudo de volta aos valores originais sem martelar o banco de dados e sem interromper a conexão existente.

Abaixo está nossa solução para este mesmo 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 isso ajude outros.

Jerry
fonte
0

Algumas boas idéias acima, eu escolhi implementar ICloneable e um método de extensão simples.

Encontrado aqui: Como faço para clonar uma lista genérica em C #?

Para ser usado como:

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

Desta forma, consegui clonar minha lista de entidades de produto, aplicar um desconto a cada item e não ter que me preocupar em reverter nenhuma alteração na entidade original. Não há necessidade de falar com o DBContext e pedir uma atualização ou trabalhar com o ChangeTracker. Você pode dizer que não estou fazendo uso completo do EF6, mas esta é uma implementação muito boa e simples e evita um impacto no banco de dados. Não posso dizer se isso teve ou não um impacto no desempenho.

IbrarMumtaz
fonte