Compartir a través de


Usar transacciones

Las transacciones permiten procesar varias operaciones de base de datos de forma atómica. Si se confirma la transacción, todas las operaciones se aplican correctamente a la base de datos. Si la transacción se revierte, ninguna de las operaciones se aplica a la base de datos.

Sugerencia

Puede ver el ejemplo de este artículo en GitHub.

Comportamiento de transacción predeterminado

De forma predeterminada, si el proveedor de bases de datos admite transacciones, todos los cambios en una sola llamada a SaveChanges se aplican en una transacción. Si se produce un error en alguno de los cambios, la transacción se revierte y ninguno de los cambios se aplica a la base de datos. Esto significa que se garantiza que SaveChanges se complete correctamente, o bien que deje sin modificaciones la base de datos si se produce un error.

Para la mayoría de las aplicaciones, este comportamiento predeterminado es suficiente. Solo debe controlar manualmente las transacciones si los requisitos de la aplicación lo considera necesario.

Control de transacciones

Puede usar la DbContext.Database API para iniciar, confirmar y revertir transacciones. En el ejemplo siguiente se muestran dos SaveChanges operaciones y una consulta LINQ que se ejecuta en una sola transacción:

using var context = new BloggingContext();
await using var transaction = await context.Database.BeginTransactionAsync();

try
{
    context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
    await context.SaveChangesAsync();

    context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
    await context.SaveChangesAsync();

    var blogs = await context.Blogs
        .OrderBy(b => b.Url)
        .ToListAsync();

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    await transaction.CommitAsync();
}
catch (Exception)
{
    // TODO: Handle failure
}

Aunque todos los proveedores de bases de datos relacionales admiten transacciones, otros tipos de proveedores pueden generar errores o no operar cuando se llama a las API de transacciones.

Nota:

Controlar manualmente las transacciones de esta manera no es compatible con las estrategias de ejecución de reintento invocadas implícitamente. Consulte Resistencia de conexión para obtener más información.

Puntos de retorno

Cuando SaveChanges se invoca y una transacción ya está en curso en el contexto, EF crea automáticamente un punto de guardado antes de guardar los datos. Los puntos de retorno son puntos dentro de una transacción de base de datos a los que se puede revertir más tarde en caso de que ocurra un error o por cualquier otro motivo. Si SaveChanges se produce algún error, revierte automáticamente la transacción al punto de guardado, dejando la transacción en el mismo estado que si nunca se hubiera iniciado. Esto le permite corregir posibles problemas y volver a intentar guardar, en particular cuando se producen problemas de simultaneidad optimista .

Advertencia

Los puntos de retorno no son compatibles con los conjuntos de resultados activos múltiples (MARS) de SQL Server. EF no creará puntos de guardado cuando MARS esté habilitado en la conexión, incluso si MARS no está en uso activamente. Si se produce un error durante SaveChanges, la transacción puede dejarse en un estado desconocido.

También es posible administrar los puntos de retorno de forma manual, como sucede con las transacciones. En el ejemplo siguiente se crea un punto de retorno dentro de una transacción y se revierte cuando se produce un error:

using var context = new BloggingContext();
await using var transaction = await context.Database.BeginTransactionAsync();

try
{
    context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/dotnet/" });
    await context.SaveChangesAsync();

    await transaction.CreateSavepointAsync("BeforeMoreBlogs");

    context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/visualstudio/" });
    context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/aspnet/" });
    await context.SaveChangesAsync();

    await transaction.CommitAsync();
}
catch (Exception)
{
    // If a failure occurred, we rollback to the savepoint and can continue the transaction
    await transaction.RollbackToSavepointAsync("BeforeMoreBlogs");

    // TODO: Handle failure, possibly retry inserting blogs
}

Transacción entre contextos

También puede compartir una transacción en varias instancias de contexto. Esta funcionalidad solo está disponible cuando se usa un proveedor de bases de datos relacionales porque requiere el uso de DbTransaction y DbConnection, que son específicos de las bases de datos relacionales.

Para compartir una transacción, los contextos deben compartir tanto un DbConnection como un DbTransaction.

Permitir que la conexión se proporcione externamente

Para el uso compartido de DbConnection se necesita la capacidad de pasar una conexión a un contexto cuando se construya.

La manera más fácil de permitir que DbConnection se proporcione externamente es dejar de usar el método DbContext.OnConfiguring para configurar el contexto, crear DbContextOptions externamente y pasarlos al constructor del contexto.

Sugerencia

DbContextOptionsBuilder es la API que usó en DbContext.OnConfiguring para configurar el contexto; ahora la usará externamente para crear DbContextOptions.

public class BloggingContext : DbContext
{
    public BloggingContext(DbContextOptions<BloggingContext> options)
        : base(options)
    {
    }

    public DbSet<Blog> Blogs { get; set; }
}

Una alternativa es seguir usando DbContext.OnConfiguring, pero aceptar que DbConnection se guarda y, a continuación, se usa en DbContext.OnConfiguring.

public class BloggingContext : DbContext
{
    private DbConnection _connection;

    public BloggingContext(DbConnection connection)
    {
      _connection = connection;
    }

    public DbSet<Blog> Blogs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(_connection);
    }
}

Compartir conexión y transacción

Ahora puede crear varias instancias de contexto que compartan la misma conexión. A continuación, use la DbContext.Database.UseTransaction(DbTransaction) API para inscribir ambos contextos en la misma transacción.

using var connection = new SqlConnection(connectionString);
var options = new DbContextOptionsBuilder<BloggingContext>()
    .UseSqlServer(connection)
    .Options;

using var context1 = new BloggingContext(options);
await using var transaction = await context1.Database.BeginTransactionAsync();
try
{
    context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
    await context1.SaveChangesAsync();

    using (var context2 = new BloggingContext(options))
    {
        await context2.Database.UseTransactionAsync(transaction.GetDbTransaction());

        var blogs = await context2.Blogs
            .OrderBy(b => b.Url)
            .ToListAsync();

        context2.Blogs.Add(new Blog { Url = "http://dot.net" });
        await context2.SaveChangesAsync();
    }

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    await transaction.CommitAsync();
}
catch (Exception)
{
    // TODO: Handle failure
}

Uso de DbTransactions externas (solo bases de datos relacionales)

Si usa varias tecnologías de acceso a datos para acceder a una base de datos relacional, puede compartir una transacción entre las operaciones realizadas por estas tecnologías diferentes.

En el ejemplo siguiente se muestra cómo realizar una operación sqlClient de ADO.NET y una operación de Entity Framework Core en la misma transacción.

using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();

await using var transaction = (SqlTransaction)await connection.BeginTransactionAsync();
try
{
    // Run raw ADO.NET command in the transaction
    var command = connection.CreateCommand();
    command.Transaction = transaction;
    command.CommandText = "DELETE FROM dbo.Blogs";
    command.ExecuteNonQuery();

    // Run an EF Core command in the transaction
    var options = new DbContextOptionsBuilder<BloggingContext>()
        .UseSqlServer(connection)
        .Options;

    using (var context = new BloggingContext(options))
    {
        await context.Database.UseTransactionAsync(transaction);
        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        await context.SaveChangesAsync();
    }

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    await transaction.CommitAsync();
}
catch (Exception)
{
    // TODO: Handle failure
}

Uso de System.Transactions

Es posible usar transacciones ambientales si necesita coordinarse en un ámbito mayor.

using (var scope = new TransactionScope(
           TransactionScopeOption.Required,
           new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
    using var connection = new SqlConnection(connectionString);
    await connection.OpenAsync();

    try
    {
        // Run raw ADO.NET command in the transaction
        var command = connection.CreateCommand();
        command.CommandText = "DELETE FROM dbo.Blogs";
        await command.ExecuteNonQueryAsync();

        // Run an EF Core command in the transaction
        var options = new DbContextOptionsBuilder<BloggingContext>()
            .UseSqlServer(connection)
            .Options;

        using (var context = new BloggingContext(options))
        {
            context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
            await context.SaveChangesAsync();
        }

        // Commit transaction if all commands succeed, transaction will auto-rollback
        // when disposed if either commands fails
        scope.Complete();
    }
    catch (Exception)
    {
        // TODO: Handle failure
    }
}

También es posible inscribirse en una transacción explícita.

using (var transaction = new CommittableTransaction(
           new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
    var connection = new SqlConnection(connectionString);

    try
    {
        var options = new DbContextOptionsBuilder<BloggingContext>()
            .UseSqlServer(connection)
            .Options;

        using (var context = new BloggingContext(options))
        {
            await context.Database.OpenConnectionAsync();
            context.Database.EnlistTransaction(transaction);

            // Run raw ADO.NET command in the transaction
            var command = connection.CreateCommand();
            command.CommandText = "DELETE FROM dbo.Blogs";
            await command.ExecuteNonQueryAsync();

            // Run an EF Core command in the transaction
            context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
            await context.SaveChangesAsync();
            await context.Database.CloseConnectionAsync();
        }

        // Commit transaction if all commands succeed, transaction will auto-rollback
        // when disposed if either commands fails
        transaction.Commit();
    }
    catch (Exception)
    {
        // TODO: Handle failure
    }
}

Nota:

Si usa api asincrónicas, asegúrese de especificar TransactionScopeAsyncFlowOption.Enabled en el TransactionScope constructor para asegurarse de que la transacción ambiente fluye a través de llamadas asincrónicas.

Para más información sobre TransactionScope y transacciones ambientales, vea esta documentación.

Limitaciones de System.Transactions

  1. EF Core se basa en proveedores de bases de datos para implementar la compatibilidad con System.Transactions. Si un proveedor no implementa compatibilidad con System.Transactions, es posible que se omitan completamente las llamadas a estas API. SqlClient lo admite.

    Importante

    Se recomienda probar que la API se comporta correctamente con el proveedor antes de confiar en ella para administrar transacciones. Si no lo hace, se recomienda ponerse en contacto con el mantenedor del proveedor de bases de datos.

  2. La compatibilidad con transacciones distribuidas en System.Transactions se agregó a .NET 7.0 solo para Windows. Se producirá un error en cualquier intento de usar transacciones distribuidas en versiones anteriores de .NET o en plataformas que no son de Windows.

  3. TransactionScope no admite la confirmación o reversión asincrónicas; esto significa que eliminarlo de forma sincrónica bloquea el subproceso en ejecución hasta que se complete la operación.