Usar transacciones

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

Sugerencia

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

Comportamiento predeterminado de las transacciones

De manera predeterminada, si el proveedor de base de datos admite las transacciones, todos los cambios de una llamada sencilla a SaveChanges se aplican a una transacción. Si cualquiera de los cambios presenta un error, la transacción se revertirá y no se aplicará ninguno de los cambios a la base de datos. Esto significa que se garantiza que SaveChanges se complete correctamente o deje sin modificaciones la base de datos si se produce un error.

Este comportamiento predeterminado es suficiente para la mayoría de las aplicaciones. Solo debe controlar manualmente las transacciones si los requisitos de la aplicación lo consideran necesario.

Control de las transacciones

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

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

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

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

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

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

Si bien 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.

Puntos de retorno

Nota

Esta característica se incluyó por primera vez en EF Core 5.0.

Cuando se invoca a SaveChanges y ya hay una transacción en curso en el contexto, EF crea automáticamente un SaveChanges 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 encuentra algún error, revierte automáticamente la transacción al punto de retorno, y la transacción se mantiene en el mismo estado que si nunca se hubiera iniciado. Esto le permite posiblemente corregir problemas y volver a intentar guardar, en particular cuando ocurren problemas de simultaneidad optimista.

Advertencia

Los puntos de retorno son incompatibles con los conjuntos de resultados activos múltiples de SQL Server y no se usan. Si se produce un error durante SaveChanges, es posible que la transacción se quede en un estado desconocido.

También es posible administrar los puntos de retorno de forma manual, del mismo modo que 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();
using var transaction = context.Database.BeginTransaction();

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

    transaction.CreateSavepoint("BeforeMoreBlogs");

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

    transaction.Commit();
}
catch (Exception)
{
    // If a failure occurred, we rollback to the savepoint and can continue the transaction
    transaction.RollbackToSavepoint("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 base de datos relacionales porque requiere el uso de DbTransaction y DbConnection, específicos para las bases de datos relacionales.

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

Permitir conexiones proporcionadas externamente

Compartir una DbConnection requiere la capacidad de pasar una conexión a un contexto cuando se construya.

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

Sugerencia

DbContextOptionsBuilder es la API que usó en DbContext.OnConfiguring para configurar el contexto y ahora va a usarla para crear externamente 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 una DbConnection que se guarda y luego 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 comparten la misma conexión. Luego use la API DbContext.Database.UseTransaction(DbTransaction) 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);
using var transaction = context1.Database.BeginTransaction();
try
{
    context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
    context1.SaveChanges();

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

        var blogs = context2.Blogs
            .OrderBy(b => b.Url)
            .ToList();
    }

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    transaction.Commit();
}
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, es posible que quiera compartir una transacción entre las operaciones que estas distintas tecnologías realizan.

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);
connection.Open();

using var transaction = connection.BeginTransaction();
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))
    {
        context.Database.UseTransaction(transaction);
        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        context.SaveChanges();
    }

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

Utilizar System.Transactions

Es posible usar transacciones de ambiente si necesita coordinar en un ámbito mayor.

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

    try
    {
        // Run raw ADO.NET command in the transaction
        var command = connection.CreateCommand();
        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))
        {
            context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
            context.SaveChanges();
        }

        // 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 inscribir 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))
        {
            context.Database.OpenConnection();
            context.Database.EnlistTransaction(transaction);

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

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

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

Limitaciones de System.Transactions

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

    Importante

    Se recomienda probar que la API se comporte correctamente con el proveedor antes de usarla para administrar las transacciones. Si no es así, recomendamos que se ponga en contacto con el mantenedor del proveedor de base de datos.

  2. La implementación de .NET Core de System.Transactions actualmente no incluye compatibilidad con transacciones distribuidas, por lo que no puede usar TransactionScope ni CommittableTransaction para coordinar las transacciones entre varios administradores de recursos. El seguimiento del soporte técnico se realiza en este error.