Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
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
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.
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.
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.