Sdílet prostřednictvím


Použití transakcí

Transakce umožňují zpracování několika databázových operací atomovým způsobem. Pokud je transakce potvrzena, všechny operace jsou úspěšně použity na databázi. Pokud je transakce vrácena zpět, žádná z operací se nepoužije na databázi.

Tip

Ukázku pro tento článek najdete na GitHubu.

Výchozí chování transakcí

Pokud poskytovatel databáze ve výchozím nastavení podporuje transakce, všechny změny v jednom volání SaveChanges se použijí v transakci. Pokud některé ze změn selžou, transakce se vrátí zpět a žádná z těchto změn se nepoužije v databázi. To znamená, že je zaručeno, že SaveChanges buď je databáze zcela úspěšná, nebo nechat databázi nezměněnou, pokud dojde k chybě.

U většiny aplikací je toto výchozí chování dostatečné. Transakce byste měli řídit pouze ručně, pokud vaše požadavky na aplikaci považují za nezbytné.

Řízení transakcí

Rozhraní API můžete použít DbContext.Database k zahájení, potvrzení a vrácení transakcí zpět. Následující příklad ukazuje dvě SaveChanges operace a dotaz LINQ, který se spouští v jedné transakci:

using var context = new BloggingContext();
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
}

Zatímco všichni poskytovatelé relačních databází podporují transakce, ostatní typy poskytovatelů mohou vyvolat nebo bez operace při zavolání rozhraní API transakcí.

Poznámka

Ruční řízení transakcí tímto způsobem není kompatibilní s implicitně vyvolanými strategiemi provádění. Další informace najdete v tématu Připojení o odolnosti.

Savepoints

Při SaveChanges vyvolání a transakce již probíhá v kontextu, EF automaticky vytvoří bod uložení před uložením dat. Body uložení jsou body v rámci databázové transakce, která se může později vrátit zpět, pokud dojde k chybě nebo z jakéhokoli jiného důvodu. Pokud SaveChanges dojde k nějaké chybě, automaticky vrátí transakci zpět do savepointu, takže transakce zůstane ve stejném stavu, jako kdyby se nikdy nezačala. To vám umožní opravit problémy a opakovat ukládání, zejména v případě, že dojde k problémům s optimistickou souběžností .

Upozorňující

Body ukládání nejsou kompatibilní s několika aktivními sadami výsledků (MARS) SQL Serveru. Funkce Savepoints se nevytvoří ef, pokud je u připojení povolená služba MARS, i když se mars aktivně nepoužívá. Pokud dojde k chybě během SaveChanges, transakce může být ponechána v neznámém stavu.

Je také možné ručně spravovat body ukládání, stejně jako u transakcí. Následující příklad vytvoří bod uložení v rámci transakce a vrátí se k němu při selhání:

using var context = new BloggingContext();
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
}

Transakce napříč kontexty

Transakci můžete také sdílet napříč několika kontextovými instancemi. Tato funkce je k dispozici pouze při použití zprostředkovatele relační databáze, protože vyžaduje použití DbTransaction a DbConnection, které jsou specifické pro relační databáze.

Chcete-li sdílet transakci, kontexty musí sdílet jak a DbConnection a DbTransaction.

Povolit externí poskytnutí připojení

DbConnection Sdílení vyžaduje možnost předat připojení do kontextu při jeho vytváření.

Nejjednodušší způsob, jak umožnit DbConnection externí poskytnutí, je zastavit použití DbContext.OnConfiguring metody ke konfiguraci kontextu a externě vytvořit DbContextOptions a předat je do konstruktoru kontextu.

Tip

DbContextOptionsBuilder je rozhraní API, které DbContext.OnConfiguring jste použili ke konfiguraci kontextu, a teď ho použijete externě k vytvoření DbContextOptions.

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

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

Alternativou je pokračovat v používání DbContext.OnConfiguring, ale přijmout DbConnection , který je uložen a pak použit v 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);
    }
}

Sdílení připojení a transakce

Teď můžete vytvořit více kontextových instancí, které sdílejí stejné připojení. Pak použijte DbContext.Database.UseTransaction(DbTransaction) rozhraní API k zařazení obou kontextů do stejné transakce.

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

using var context1 = new BloggingContext(options);
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
}

Použití externích dbTransactions (pouze relační databáze)

Pokud pro přístup k relační databázi používáte více technologií přístupu k datům, můžete chtít sdílet transakci mezi operacemi prováděnými těmito různými technologiemi.

Následující příklad ukazuje, jak provést operaci ADO.NET SqlClient a operaci Entity Framework Core ve stejné transakci.

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

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
}

Použití System.Transactions

Je možné použít okolí transakce, pokud potřebujete koordinovat napříč větším rozsahem.

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
    }
}

Je také možné zahrnout do explicitní transakce.

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
    }
}

Poznámka

Pokud používáte asynchronní rozhraní API, nezapomeňte v konstruktoru TransactionScope zadat TransactionScopeAsyncFlowOption.Enabled, aby se zajistilo, že okolí transakce proudí přes asynchronní volání.

Další informace o TransactionScope a okolí transakcí naleznete v této dokumentaci.

Omezení System.Transactions

  1. EF Core využívá poskytovatele databází k implementaci podpory pro System.Transactions. Pokud poskytovatel neimplementuje podporu pro System.Transactions, je možné, že volání těchto rozhraní API budou zcela ignorována. SqlClient ho podporuje.

    Důležité

    Před tím, než na něj spoléháte při správě transakcí, doporučujeme otestovat, že se rozhraní API chová správně s vaším poskytovatelem. Doporučujeme kontaktovat správce poskytovatele databáze, pokud ne.

  2. Podpora distribuovaných transakcí v system.Transactions byla přidána pouze do .NET 7.0 pro Windows. Všechny pokusy o použití distribuovaných transakcí ve starších verzích .NET nebo na platformách jiných než Windows selžou.

  3. TransactionScope nepodporuje asynchronní potvrzení nebo vrácení zpět; to znamená, že jeho synchronní odstranění blokuje spuštěné vlákno, dokud nebude operace dokončena.