Partager via


Utilisation de transactions

Les transactions permettent à plusieurs opérations de base de données d’être traitées de manière atomique. Si la transaction est validée, toutes les opérations sont appliquées avec succès à la base de données. Si la transaction est restaurée, aucune des opérations n’est appliquée à la base de données.

Conseil

Vous pouvez afficher cet exemple sur GitHub.

Comportement de transaction par défaut

Par défaut, si le fournisseur de base de données prend en charge les transactions, toutes les modifications dans un seul appel à SaveChanges sont appliquées à une transaction. Si certaines des modifications échouent, la transaction est annulée et aucune des modifications n’est appliquée à la base de données. Cela signifie que SaveChanges réussit complètement ou laisse la base de données non modifiée si une erreur se produit.

Pour la plupart des applications, ce comportement par défaut est suffisant. Vous devez uniquement contrôler manuellement les transactions si les exigences de votre application le jugent nécessaire.

Contrôle des transactions

Vous pouvez utiliser l’API DbContext.Database pour commencer, valider et annuler les transactions. L’exemple suivant montre deux opérations SaveChanges et une requête LINQ en cours d’exécution dans une seule transaction :

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
}

Bien que tous les fournisseurs de base de données relationnelle prennent en charge les transactions, les autres types de fournisseurs peuvent lever ou ne pas être opérationnels lorsque des API de transaction sont appelées.

Remarque

Le contrôle manuel de transactions de cette manière est incompatible avec des stratégies d’exécution de nouvelles tentatives appelées. Pour obtenir plus d’informations, consultez Résilience de connexion.

Points de sauvegarde

Lorsque SaveChanges est appelé et qu’une transaction est déjà en cours sur le contexte, EF crée automatiquement un point d’enregistrement avant d’enregistrer des données. Les points d’enregistrement sont des points au sein d’une transaction de base de données vers lesquels une restauration est possible ultérieurement, si une erreur se produit ou pour toute autre raison. Si SaveChanges rencontre une erreur, il restaure automatiquement la transaction jusqu’au point de sauvegarde, ce qui laisse la transaction dans le même état que si elle n’avait jamais démarré. Cela vous permet de potentiellement corriger des problèmes et de retenter l’enregistrement, en particulier quand des problèmes d’accès concurrentiel optimiste se produisent.

Avertissement

Les points d’enregistrement sont incompatibles avec des MARS (Multiple Active Result Sets) de SQL Server. Les points d’enregistrement ne sont pas créés par EF quand un MARS (Multiple Active Result Set) est activé sur la connexion, même lorsqu’un MARS n’est pas activement utilisé. Si une erreur se produit pendant SaveChanges, il est possible que la transaction soit laissée dans un état inconnu.

Il est également possible de gérer manuellement des points d’enregistrement, comme c’est le cas avec les transactions. L’exemple suivant crée un point d’enregistrement dans une transaction, puis le restaure en cas d’échec :

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
}

Transaction de contexte croisé

Vous pouvez également partager une transaction sur plusieurs instances de contexte. Cette fonctionnalité est disponible uniquement lorsque vous utilisez un fournisseur de base de données relationnelle, car elle requiert l’utilisation de DbTransaction et DbConnection, qui sont propres aux bases de données relationnelles.

Pour partager une transaction, les contextes doivent partager une DbConnection et une DbTransaction.

Autoriser la fourniture externe de la connexion

Le partage d’une DbConnection nécessite la possibilité de passer une connexion dans un contexte lors de la construction.

Le moyen le plus simple pour autoriser la DbConnection à être fournie en externe, arrêtez d’utiliser la méthode DbContext.OnConfiguring pour configurer le contexte et créez les DbContextOptions en externe avant de les passer au constructeur de contexte.

Conseil

DbContextOptionsBuilder est l’API que vous avez utilisée dans DbContext.OnConfiguring pour configurer le contexte. Vous allez maintenant l’utiliser en externe pour créer DbContextOptions.

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

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

Une alternative consiste à continuer à utiliser DbContext.OnConfiguring, mais accepte une DbConnection qui est enregistrée et ensuite utilisée dans 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);
    }
}

Partage de connexions et transactions

Vous pouvez désormais créer plusieurs instances de contexte qui partagent la même connexion. Utilisez ensuite l’API DbContext.Database.UseTransaction(DbTransaction) pour inscrire les deux contextes dans la même transaction.

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();

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

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

Utilisation de DbTransactions externes (bases de données relationnelles uniquement)

Si vous utilisez plusieurs technologies d’accès aux données pour accéder à une base de données relationnelle, vous souhaiterez partager une transaction entre les opérations effectuées par ces différentes technologies.

L’exemple suivant montre comment effectuer une opération ADO.NET SqlClient et une opération Entity Framework Core dans la même transaction.

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
}

Utilisation de System.Transactions

Il est possible d’utiliser les transactions ambiantes si vous avez besoin de coordonner sur une plus grande portée.

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

Il est également possible de s’inscrire dans une transaction explicite.

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

Remarque

Si vous utilisez des API asynchrones, soyez certain de spécifier TransactionScopeAsyncFlowOption.Enabled dans le constructeur TransactionScope afin de veiller au flux de transactions ambiantes dans des appels asynchrones.

Pour obtenir plus d’informations sur TransactionScope et les transactions ambiantes, consultez cette documentation.

Limitations de System.Transactions

  1. EF Core s’appuie sur les fournisseurs de base de données pour implémenter la prise en charge de System.Transactions. Si un fournisseur n’implémente pas la prise en charge de System.Transactions, il est possible que les appels à ces API soient complètement ignorés. SqlClient le prend en charge.

    Important

    Il est recommandé de vérifier que l’API se comporte correctement avec votre fournisseur avant de l’utiliser pour la gestion des transactions. Nous vous invitons à contacter le chargé de maintenance du fournisseur de base de données si ce n’est pas le cas.

  2. La prise en charge des transactions distribuées dans System.Transactions a été ajoutée pour .NET 7.0 pour Windows uniquement. Toute tentative d’utiliser des transactions distribuées sur des versions .NET plus ancienne ou des plateformes non Windows échouera.

  3. TransactionScope ne prend pas en charge la validation/restauration asynchrone, ce qui signifie que sa disposition synchrone bloque le thread d’exécution jusqu’à la fin de l’opération.