Usando transações ou SaveChanges (false) e AcceptAllChanges ()?

346

Tenho vindo a investigar transações e parece que eles cuidar de si na EF, enquanto eu passar falsepara SaveChanges()e, em seguida, chamar AcceptAllChanges()se não há erros:

SaveChanges(false);
// ...
AcceptAllChanges();

E se algo der errado? não preciso reverter ou, assim que meu método estiver fora do escopo, a transação será encerrada?

O que acontece com qualquer coluna de identificação que tenha sido atribuída na metade da transação? Presumo que se alguém adicionou um registro após o meu antes que o meu ficasse ruim, isso significa que haverá um valor de identidade ausente.

Existe algum motivo para usar a TransactionScopeclasse padrão no meu código?

Mark Smith
fonte
11
Isso me ajudou a entender por que SaveChanges(fase); ... AcceptAllChanges();havia um padrão em primeiro lugar. Observe como a resposta aceita para a pergunta acima é escrita pelo autor de um blog - e esse blog é referenciado na outra pergunta. Tudo vem junto.
The Red Pea

Respostas:

451

Com o Entity Framework, a maior parte do tempo SaveChanges()é suficiente. Isso cria uma transação ou inscreve-se em qualquer transação ambiental e faz todo o trabalho necessário nessa transação.

Às vezes, embora o SaveChanges(false) + AcceptAllChanges() emparelhamento seja útil.

O local mais útil para isso é em situações nas quais você deseja fazer uma transação distribuída em dois contextos diferentes.

Ou seja, algo assim (ruim):

using (TransactionScope scope = new TransactionScope())
{
    //Do something with context1
    //Do something with context2

    //Save and discard changes
    context1.SaveChanges();

    //Save and discard changes
    context2.SaveChanges();

    //if we get here things are looking good.
    scope.Complete();
}

Se context1.SaveChanges()for bem-sucedido, mas context2.SaveChanges()falhar, toda a transação distribuída é abortada. Infelizmente, porém, o Entity Framework já descartou as alterações context1, portanto você não pode reproduzir ou registrar efetivamente a falha.

Mas se você alterar seu código para ficar assim:

using (TransactionScope scope = new TransactionScope())
{
    //Do something with context1
    //Do something with context2

    //Save Changes but don't discard yet
    context1.SaveChanges(false);

    //Save Changes but don't discard yet
    context2.SaveChanges(false);

    //if we get here things are looking good.
    scope.Complete();
    context1.AcceptAllChanges();
    context2.AcceptAllChanges();

}

Enquanto a chamada SaveChanges(false)envia os comandos necessários para o banco de dados, o contexto em si não é alterado; portanto, você pode fazê-lo novamente, se necessário, ou pode interrogar oObjectStateManager que desejar.

Isso significa que, se a transação realmente lançar uma exceção, você poderá compensar, tentando novamente ou registrando o estado de cada contexto ObjectStateManager algum lugar.

Veja meu blog para mais.

Alex James
fonte
3
Isso é ótimo, obrigado ... Então, se algo falhar, não tenho que reverter ?? SaveChanges, marca como sendo salvo, mas na verdade não confirma até que eu aceite todas as alterações .. mas se algo der errado .. eu precisarei reverter, não irei para que meu objeto retorne ao seu estado correto?
Mark Smith
33
@ Mark: se você quer dizer "reverter", reverta seus objetos para o estado em que estão no banco de dados, então não, você não gostaria de fazer isso porque perderia todas as alterações do usuário nos objetos . SaveChanges(false)faz a atualização real no banco de dados, enquanto AcceptAllChanges()informa à EF: "Ok, você pode esquecer quais coisas precisam ser salvas, porque elas foram salvas com êxito". Se SaveChanges(false)falhar, AcceptAllChanges()nunca será chamado e o EF ainda considerará seu objeto como tendo propriedades que foram alteradas e precisam ser salvas novamente no banco de dados.
BlueRaja - Danny Pflughoeft 29/03
Você pode aconselhar como fazer isso usando o Code First? Não há parâmetro para o método SaveChanges ou AcceptAllChanges
Kirsten Greed
2
Eu tenho uma pergunta sobre como utilizar esta técnica com Code First aqui
Kirsten Greed
13
Isso não é mais possível no EF 6.1. Você sabe que tipo de ajuste precisa ser feito para funcionar agora?
31814 Alex Dresko
113

Se você estiver usando EF6 (Entity Framework 6+), isso foi alterado para chamadas de banco de dados para SQL.
Consulte: http://msdn.microsoft.com/en-us/data/dn456843.aspx

use context.Database.BeginTransaction.

Do MSDN:

using (var context = new BloggingContext()) 
{ 
    using (var dbContextTransaction = context.Database.BeginTransaction()) 
    { 
        try 
        { 
            context.Database.ExecuteSqlCommand( 
                @"UPDATE Blogs SET Rating = 5" + 
                    " WHERE Name LIKE '%Entity Framework%'" 
                ); 

            var query = context.Posts.Where(p => p.Blog.Rating >= 5); 
            foreach (var post in query) 
            { 
                post.Title += "[Cool Blog]"; 
            } 

            context.SaveChanges(); 

            dbContextTransaction.Commit(); 
        } 
        catch (Exception) 
        { 
            dbContextTransaction.Rollback(); //Required according to MSDN article 
            throw; //Not in MSDN article, but recommended so the exception still bubbles up
        } 
    } 
} 
user3885816
fonte
51
O try-catch com roolback não é necessário quando você está usando "using" na transação.
Robert
12
Estou abrindo uma exceção para capturar a exceção assim. Isso faz com que a operação do banco de dados falhe silenciosamente. Devido à natureza do SO, alguém pode pegar este exemplo e usá-lo em um aplicativo de produção.
B2K
3
@ B2K: Bom argumento, mas esse código é copiado do artigo vinculado da Microsoft. Espero que ninguém usa seu código na produção :)
J Bryan Preço
6
@ Robert De acordo com o artigo do MSDN, Rollback () é necessário. Eles propositalmente deixam de fora um comando Rollback para o exemplo TransactionScope. @ B2K Adicionei throw;ao snippet do MSDN e indiquei claramente que não é o original do artigo do MSDN.
Todd
6
(Se correto) Isso pode esclarecer: Parece que o EF + MSSQL não precisa de Rollback, mas o EF + outros provedores SQL podem. Como o EF deve ser independente do banco de dados com o qual está falando, Rollback()é chamado caso esteja falando com o MySql ou algo que não tenha esse comportamento automático.
Palavras como Jared
-5

Como algum banco de dados pode lançar uma exceção em dbContextTransaction.Commit (), é melhor que isso:

using (var context = new BloggingContext()) 
{ 
  using (var dbContextTransaction = context.Database.BeginTransaction()) 
  { 
    try 
    { 
      context.Database.ExecuteSqlCommand( 
          @"UPDATE Blogs SET Rating = 5" + 
              " WHERE Name LIKE '%Entity Framework%'" 
          ); 

      var query = context.Posts.Where(p => p.Blog.Rating >= 5); 
      foreach (var post in query) 
      { 
          post.Title += "[Cool Blog]"; 
      } 

      context.SaveChanges(false); 

      dbContextTransaction.Commit(); 

      context.AcceptAllChanges();
    } 
    catch (Exception) 
    { 
      dbContextTransaction.Rollback(); 
    } 
  } 
} 
eMeL
fonte
7
Estou abrindo uma exceção para capturar a exceção assim. Isso faz com que a operação do banco de dados falhe silenciosamente. Devido à natureza do SO, alguém pode pegar este exemplo e usá-lo em um aplicativo de produção.
B2K
6
Isso não é essencialmente o mesmo que essa outra resposta que atribuiu a página do MSDN citada? A única diferença que vejo é que você falseentra context.SaveChanges();e liga adicionalmente context.AcceptAllChanges();.
Wai Ha Lee
@ B2K a reversão não é necessária - se a transação não funcionar, nada será confirmado. Também chamada explícita para reversão pode falhar - veja minha resposta aqui stackoverflow.com/questions/41385740/...
Ken
A reversão não é o que estou objetando. O autor desta resposta atualizou seu código para refazer a exceção, resolvendo assim o que eu estava objetando.
B2K
Desculpe, comentei no meu telefone. Todd lança novamente a exceção, o eMeL não. Deve haver algo na captura que notifique o desenvolvedor ou o usuário de um problema que está causando uma reversão. Isso pode estar gravando em um arquivo de log, repetindo novamente a exceção ou retornando uma mensagem ao usuário.
B2K 12/12