Trabajar con Transacciones

Nota:

Solo EF6 y versiones posteriores: las características, las API, etc. que se tratan en esta página se han incluido a partir de Entity Framework 6. Si usa una versión anterior, no se aplica parte o la totalidad de la información.

Este documento describirá el uso de transacciones en EF6, incluidas las mejoras que hemos agregado desde EF5 para facilitar el trabajo con transacciones.

¿Qué hace EF de forma predeterminada?

En todas las versiones de Entity Framework, siempre que ejecute SaveChanges() para insertar, actualizar o eliminar en la base de datos, el marco ajustará esa operación en una transacción. Esta transacción solo dura lo suficiente para ejecutar la operación y, a continuación, se completa. Cuando se ejecuta otra operación de este tipo, se inicia una nueva transacción.

A partir de EF6 Database.ExecuteSqlCommand() de forma predeterminada, ajustará el comando en una transacción si aún no estaba presente. Hay sobrecargas de este método que le permiten invalidar este comportamiento si lo desea. También en EF6, la ejecución de procedimientos almacenados incluidos en el modelo a través de API como ObjectContext.ExecuteFunction() hace lo mismo (excepto que el comportamiento predeterminado no se puede invalidar en este momento).

En cualquier caso, el nivel de aislamiento de la transacción es el nivel de aislamiento que el proveedor de base de datos considera su configuración predeterminada. De forma predeterminada, por ejemplo, en SQL Server, se trata de READ COMMITTED.

Entity Framework no encapsula las consultas en una transacción.

Esta funcionalidad predeterminada es adecuada para muchos usuarios y, si es así, no es necesario hacer nada diferente en EF6; simplemente escriba el código como siempre hizo.

Sin embargo, algunos usuarios requieren un mayor control sobre sus transacciones: esto se trata en las secciones siguientes.

Funcionamiento de las API

Antes de EF6, Entity Framework insistió en abrir la propia conexión de base de datos (produjo una excepción si se pasó una conexión que ya estaba abierta). Dado que una transacción solo se puede iniciar en una conexión abierta, esto significaba que la única manera en que un usuario podía encapsular varias operaciones en una transacción era usar TransactionScope o usar la propiedad ObjectContext.Connection y empezar a llamar a Open() y BeginTransaction() directamente en el objeto EntityConnection devuelto. Además, las llamadas API que se han puesto en contacto con la base de datos producirán un error si hubiera iniciado una transacción en la conexión de base de datos subyacente por su cuenta.

Nota:

La limitación de aceptar solo las conexiones cerradas se quitó en Entity Framework 6. Para obtener más información, consulte Administración de conexiones.

A partir de EF6, el marco ahora proporciona:

  1. Database.BeginTransaction() : un método más sencillo para que un usuario inicie y complete las transacciones en sí mismas dentro de DbContext existente, lo que permite combinar varias operaciones dentro de la misma transacción y, por tanto, todas son confirmadas o todas se revierten como una. También permite al usuario especificar más fácilmente el nivel de aislamiento de la transacción.
  2. Database.UseTransaction() : que permite que DbContext use una transacción que se inició fuera de Entity Framework.

Combinación de varias operaciones en una transacción dentro del mismo contexto

Database.BeginTransaction() tiene dos invalidaciones: una que toma un IsolationLevel explícito y otra que no toma ningún argumento y usa el IsolationLevel predeterminado del proveedor de base de datos subyacente. Ambas invalidaciones devuelven un objeto DbContextTransaction que proporciona métodos Commit() y Rollback() que realizan la confirmación y reversión en la transacción del almacén subyacente.

DbContextTransaction está diseñado para eliminarse una vez confirmado o revertido. Una manera fácil de lograrlo es con la sintaxis using(...) {...} que llamará automáticamente a Dispose() cuando se complete el bloque using:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        static void StartOwnTransactionWithinContext()
        {
            using (var context = new BloggingContext())
            {
                using (var dbContextTransaction = context.Database.BeginTransaction())
                {
                    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();
                }
            }
        }
    }
}

Nota:

El inicio de una transacción requiere que la conexión del almacén subyacente esté abierta. Por lo tanto, al llamar a Database.BeginTransaction() se abrirá la conexión si aún no está abierta. Si DbContextTransaction abrió la conexión, se cerrará cuando se llame a Dispose().

Pasar una transacción existente al contexto

A veces desea una transacción que sea aún más amplia en el ámbito y que incluya operaciones en la misma base de datos, pero fuera de EF completamente. Para ello, debe abrir la conexión e iniciar la transacción usted mismo y, a continuación, indicar a EF que a) use la conexión de base de datos ya abierta y b) use la transacción existente en esa conexión.

Para ello, debe definir y usar un constructor en la clase de contexto que hereda de uno de los constructores DbContext que toman i) un parámetro de conexión existente y ii) el valor booleano contextOwnsConnection.

Nota:

La marca contextOwnsConnection debe establecerse en false cuando se llama a en este escenario. Esto es importante, ya que informa a Entity Framework que no debe cerrar la conexión cuando haya terminado con ella (por ejemplo, consulte la línea 4 a continuación):

using (var conn = new SqlConnection("..."))
{
    conn.Open();
    using (var context = new BloggingContext(conn, contextOwnsConnection: false))
    {
    }
}

Además, debe iniciar la transacción usted mismo (incluido IsolationLevel si desea evitar la configuración predeterminada) e informar a Entity Framework que ya hay una transacción existente iniciada en la conexión (consulte la línea 33 siguiente).

A continuación, puede ejecutar operaciones de base de datos directamente en SqlConnection o en DbContext. Todas estas operaciones se ejecutan dentro de una transacción. Se hace responsable de confirmar o revertir la transacción y de llamar a Dispose() en ella, así como de cerrar y eliminar la conexión de la base de datos. Por ejemplo:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
     class TransactionsExample
     {
        static void UsingExternalTransaction()
        {
            using (var conn = new SqlConnection("..."))
            {
               conn.Open();

               using (var sqlTxn = conn.BeginTransaction(System.Data.IsolationLevel.Snapshot))
               {
                   var sqlCommand = new SqlCommand();
                   sqlCommand.Connection = conn;
                   sqlCommand.Transaction = sqlTxn;
                   sqlCommand.CommandText =
                       @"UPDATE Blogs SET Rating = 5" +
                        " WHERE Name LIKE '%Entity Framework%'";
                   sqlCommand.ExecuteNonQuery();

                   using (var context =  
                     new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        context.Database.UseTransaction(sqlTxn);

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

                    sqlTxn.Commit();
                }
            }
        }
    }
}

Borrar la transacción

Puede pasar null a Database.UseTransaction() para borrar el conocimiento de Entity Framework de la transacción actual. Entity Framework no confirmará ni revertirá la transacción existente cuando lo haga, por lo que debe usarse con cuidado y solo si está seguro de que esto es lo que desea hacer.

Errores en UseTransaction

Verá una excepción de Database.UseTransaction() si pasa una transacción cuando:

  • Entity Framework ya tiene una transacción existente
  • Entity Framework ya funciona dentro de TransactionScope
  • El objeto de conexión de la transacción pasada es null. Es decir, la transacción no está asociada a una conexión: normalmente se trata de un signo de que esa transacción ya se ha completado
  • El objeto de conexión de la transacción pasada no coincide con la conexión de Entity Framework.

Uso de transacciones con otras características

En esta sección se detalla cómo interactúan las transacciones anteriores:

  • Resistencia de la conexión
  • Métodos asincrónicos
  • Transacciones TransactionScope

Resistencia de conexión

La nueva característica de Resistencia de conexión no funciona con transacciones iniciadas por el usuario. Para obtener más información, consulte Estrategias de reintento de ejecución.

Programación asincrónica

El enfoque descrito en las secciones anteriores no necesita más opciones ni configuraciones para trabajar con la consulta asincrónica y guardar métodos. Pero tenga en cuenta que, dependiendo de lo que haga dentro de los métodos asincrónicos, esto puede dar lugar a transacciones de larga duración, lo que puede provocar interbloqueos o bloqueos que son incorrectos para el rendimiento de la aplicación general.

Transacciones TransactionScope

Antes de EF6, la forma recomendada de proporcionar transacciones de ámbito más grandes era usar un objeto TransactionScope:

using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        static void UsingTransactionScope()
        {
            using (var scope = new TransactionScope(TransactionScopeOption.Required))
            {
                using (var conn = new SqlConnection("..."))
                {
                    conn.Open();

                    var sqlCommand = new SqlCommand();
                    sqlCommand.Connection = conn;
                    sqlCommand.CommandText =
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'";
                    sqlCommand.ExecuteNonQuery();

                    using (var context =
                        new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        var query = context.Posts.Where(p => p.Blog.Rating > 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }
                        context.SaveChanges();
                    }
                }

                scope.Complete();
            }
        }
    }
}

SqlConnection y Entity Framework usarían la transacción TransactionScope ambiental y, por tanto, se confirmarían conjuntamente.

A partir de .NET 4.5.1 TransactionScope se ha actualizado para que también funcione con métodos asincrónicos mediante el uso de la enumeración TransactionScopeAsyncFlowOption:

using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        public static void AsyncTransactionScope()
        {
            using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
            {
                using (var conn = new SqlConnection("..."))
                {
                    await conn.OpenAsync();

                    var sqlCommand = new SqlCommand();
                    sqlCommand.Connection = conn;
                    sqlCommand.CommandText =
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'";
                    await sqlCommand.ExecuteNonQueryAsync();

                    using (var context = new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        var query = context.Posts.Where(p => p.Blog.Rating > 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }

                        await context.SaveChangesAsync();
                    }
                }
                
                scope.Complete();
            }
        }
    }
}

Todavía hay algunas limitaciones en el enfoque TransactionScope:

  • Requiere .NET 4.5.1 o posterior para trabajar con métodos asincrónicos.
  • No se puede usar en escenarios en la nube a menos que esté seguro de que tiene una y solo una conexión (los escenarios en la nube no admiten transacciones distribuidas).
  • No se puede combinar con el enfoque Database.UseTransaction() de las secciones anteriores.
  • Se producirán excepciones si emite algún DDL y no ha habilitado transacciones distribuidas a través del servicio MSDTC.

Ventajas del enfoque TransactionScope:

  • Actualizará automáticamente una transacción local a una transacción distribuida si realiza más de una conexión a una base de datos determinada o combina una conexión a una base de datos con una conexión a una base de datos diferente dentro de la misma transacción (nota: debe tener configurado el servicio MSDTC para permitir que las transacciones distribuidas funcionen).
  • Facilidad de codificación. Si prefiere que la transacción sea ambiental y se trate implícitamente en segundo plano en lugar de controlarla explícitamente, el enfoque TransactionScope puede adaptarse mejor.

En resumen, con las nuevas API Database.BeginTransaction() y Database.UseTransaction() anteriores, el enfoque TransactionScope ya no es necesario para la mayoría de los usuarios. Si sigue usando TransactionScope, tenga en cuenta las limitaciones anteriores. Se recomienda usar el enfoque descrito en las secciones anteriores en su lugar siempre que sea posible.