¿Usando Transacciones o SaveChanges (false) y AcceptAllChanges ()?

346

He estado investigando las transacciones y parece que cuiden de sí mismos en EF, siempre y cuando paso falsea SaveChanges()y luego llamar AcceptAllChanges()si no hay errores:

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

¿Qué pasa si algo sale mal? ¿No tengo que retroceder o, tan pronto como mi método se sale del alcance, se finaliza la transacción?

¿Qué sucede con las columnas de sangría que se asignaron a la mitad de la transacción? Supongo que si alguien más agregó un registro después del mío antes de que el mío saliera mal, entonces esto significa que habrá un valor de identidad faltante.

¿Hay alguna razón para usar la TransactionScopeclase estándar en mi código?

Mark Smith
fuente
1
Esto me ayudó a entender por qué SaveChanges(fase); ... AcceptAllChanges();era un patrón en primer lugar. Observe cómo la respuesta aceptada a la pregunta anterior está escrita por el autor de un blog , y ese blog se menciona en la otra pregunta. Todo se une.
The Red Pea

Respuestas:

451

Con Entity Framework, la mayoría de las veces SaveChanges()es suficiente. Esto crea una transacción, o se alista en cualquier transacción ambiental, y realiza todo el trabajo necesario en esa transacción.

A veces, aunque el SaveChanges(false) + AcceptAllChanges()emparejamiento es útil.

El lugar más útil para esto es en situaciones en las que desea realizar una transacción distribuida en dos contextos diferentes.

Es decir, algo como esto (malo):

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

Si context1.SaveChanges()tiene éxito pero context2.SaveChanges()falla, se anula toda la transacción distribuida. Pero desafortunadamente, Entity Framework ya ha descartado los cambios context1, por lo que no puede reproducir o registrar efectivamente la falla.

Pero si cambia su código para que se vea así:

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

}

Si bien la llamada a SaveChanges(false)envía los comandos necesarios a la base de datos, el contexto en sí no cambia, por lo que puede hacerlo nuevamente si es necesario, o puede interrogar ObjectStateManagersi lo desea.

Esto significa que si la transacción realmente arroja una excepción que puede compensar, ya sea reintentando o registrando el estado de cada contexto en ObjectStateManageralguna parte.

Vea mi publicación de blog para más.

Alex James
fuente
3
Eso es genial, gracias ... Entonces, si algo falla, ¿no tengo que retroceder? SaveChanges, lo marca como guardado, pero en realidad no se compromete hasta que acepte todos los cambios ... pero si algo sale mal ... tendré que retroceder, ¿no es así para que mi objeto vuelva a su estado correcto?
Mark Smith
33
@Mark: si quiere decir "revertir", revierta sus objetos al estado en que se encuentran en la base de datos, entonces no, no querría hacerlo porque perdería todos los cambios del usuario en los objetos . SaveChanges(false)realiza la actualización real de la base de datos, mientras AcceptAllChanges()le dice a EF: "Está bien, puedes olvidar qué cosas hay que guardar, porque se han guardado correctamente". Si SaveChanges(false)falla, AcceptAllChanges()nunca se llamará y EF seguirá considerando que su objeto tiene propiedades que se modificaron y deben guardarse de nuevo en la base de datos.
BlueRaja - Danny Pflughoeft
¿Puedes aconsejar cómo hacer esto usando Code First? No hay ningún parámetro para el método SaveChanges o AcceptAllChanges
Kirsten Greed
2
He hecho una pregunta sobre el uso de esta técnica con Code First aquí
Kirsten Greed
13
Esto ya no es posible en EF 6.1. ¿Sabes qué tipo de ajustes se deben hacer para trabajar ahora?
Alex Dresko
113

Si está utilizando EF6 (Entity Framework 6+), esto ha cambiado para las llamadas de la base de datos a SQL.
Ver: http://msdn.microsoft.com/en-us/data/dn456843.aspx

use context.Database.BeginTransaction.

De 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
        } 
    } 
} 
usuario3885816
fuente
51
try-catch con roolback no es necesario cuando está usando "using" en la transacción.
Robert
12
Estoy tomando una excepción para atrapar la excepción de esta manera. Hace que la operación de la base de datos falle silenciosamente. Debido a la naturaleza de SO, alguien podría tomar este ejemplo y usarlo en una aplicación de producción.
B2K
3
@ B2K: Buen punto, pero este código se copia del artículo vinculado de Microsoft. Espero que nadie use su código en producción :)
J Bryan Precio
66
@Robert Según el artículo de MSDN, Rollback () es necesario. A propósito, omiten un comando Rollback para el ejemplo de TransactionScope. @ B2K throw;Agregué el fragmento de MSDN e indiqué claramente que no es el original del artículo de MSDN.
Todd
66
(Si es correcto) Esto podría aclarar las cosas: parece que EF + MSSQL no necesita Rollback, pero EF + otros proveedores de SQL sí. Como se supone que EF es independiente de la base de datos con la que está hablando, Rollback()se llama en caso de que esté hablando con MySql o algo que no tenga ese comportamiento automático.
Palabras como Jared
-5

Debido a que alguna base de datos puede lanzar una excepción en dbContextTransaction.Commit (), mejor esto:

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
fuente
77
Estoy tomando una excepción para atrapar la excepción de esta manera. Hace que la operación de la base de datos falle silenciosamente. Debido a la naturaleza de SO, alguien podría tomar este ejemplo y usarlo en una aplicación de producción.
B2K
66
¿No es esencialmente lo mismo que esta otra respuesta que atribuyó la página de MSDN que cita? La única diferencia que veo es que se pasa falseen context.SaveChanges();, y, además, llama context.AcceptAllChanges();.
Wai Ha Lee
@ B2K no se requiere la reversión; si la transacción no funciona, no se compromete nada. También puede fallar la llamada explícita a Rollback: vea mi respuesta aquí stackoverflow.com/questions/41385740/…
Ken
La reversión no es a lo que me opongo. El autor de esta respuesta actualizó su código para volver a lanzar la excepción, resolviendo así lo que estaba objetando.
B2K
Lo siento, comenté desde mi teléfono. Todd vuelve a lanzar la excepción, eMeL no. Debe haber algo en la captura que notifique al desarrollador o al usuario de un problema que causa una reversión. Eso podría estar escribiendo en un archivo de registro, volviendo a lanzar la excepción o devolviendo un mensaje al usuario.
B2K