Utilisation des transactions

Remarque

EF6 et versions ultérieures uniquement : Les fonctionnalités, les API, etc. décrites dans cette page ont été introduites dans Entity Framework 6. Si vous utilisez une version antérieure, certaines ou toutes les informations ne s’appliquent pas.

Ce document décrit l’utilisation de transactions dans EF6, notamment les améliorations que nous avons ajoutées depuis EF5 pour faciliter l’utilisation des transactions.

Ce qu’EF fait par défaut

Dans toutes les versions d’Entity Framework, chaque fois que vous exécutez SaveChanges() pour insérer, mettre à jour ou supprimer sur la base de données, l’infrastructure encapsule cette opération dans une transaction. Cette transaction ne dure que suffisamment longtemps pour exécuter l’opération, puis se termine. Lorsque vous exécutez une autre opération de ce type, une nouvelle transaction est démarrée.

À compter d’EF6 Database.ExecuteSqlCommand() par défaut, la commande est encapsulée dans une transaction si une transaction n’était pas déjà présente. Il existe des surcharges de cette méthode qui vous permettent de remplacer ce comportement si vous le souhaitez. En outre, dans l’exécution EF6 des procédures stockées incluses dans le modèle par le biais d’API telles que ObjectContext.ExecuteFunction() fait de même (sauf que le comportement par défaut ne peut pas être substitué au moment).

Dans les deux cas, le niveau d’isolation de la transaction est quel que soit le niveau d’isolation que le fournisseur de base de données considère son paramètre par défaut. Par défaut, par exemple, sur SQL Server, il s’agit de READ COMMITTED.

Entity Framework n’encapsule pas les requêtes dans une transaction.

Cette fonctionnalité par défaut convient à un grand nombre d’utilisateurs et, si c’est le cas, il n’est pas nécessaire de faire quelque chose de différent dans EF6 ; écrivez simplement le code comme vous l’avez toujours fait.

Toutefois, certains utilisateurs ont besoin d’un plus grand contrôle sur leurs transactions , ce qui est abordé dans les sections suivantes.

Fonctionnement des API

Avant EF6 Entity Framework a insisté sur l’ouverture de la connexion de base de données elle-même (elle a levé une exception s’il a été passé une connexion déjà ouverte). Étant donné qu’une transaction ne peut être démarrée que sur une connexion ouverte, cela signifie que la seule façon dont un utilisateur pouvait encapsuler plusieurs opérations dans une transaction était d’utiliser une TransactionScope ou d’utiliser la propriété ObjectContext.Connection et commencer à appeler Open() et BeginTransaction() directement sur l’objet EntityConnection retourné. En outre, les appels d’API qui ont contacté la base de données échouent si vous aviez démarré une transaction sur la connexion de base de données sous-jacente vous-même.

Remarque

La limitation de l’acceptation des connexions fermées uniquement a été supprimée dans Entity Framework 6. Pour plus d’informations, consultez Gestion des connexions.

À compter d’EF6, le framework fournit désormais les éléments suivants :

  1. Database.BeginTransaction() : méthode plus simple pour qu’un utilisateur démarre et termine les transactions elles-mêmes dans un DbContext existant, ce qui permet à plusieurs opérations d’être combinées au sein de la même transaction et donc toutes validées ou toutes restaurées en tant qu’une seule. Il permet également à l’utilisateur de spécifier plus facilement le niveau d’isolation de la transaction.
  2. Database.UseTransaction() : qui permet à DbContext d’utiliser une transaction démarrée en dehors de Entity Framework.

Combinaison de plusieurs opérations dans une transaction dans le même contexte

Database.BeginTransaction() a deux remplacements : l’un qui prend un isolationLevel explicite et l’autre qui ne prend aucun argument et utilise l’isolationLevel par défaut du fournisseur de base de données sous-jacent. Les deux remplacements retournent un objet DbContextTransaction qui fournit des méthodesCommit() et Rollback() qui effectuent la validation et la restauration sur la transaction de magasin sous-jacente.

Le DbContextTransaction est destiné à être supprimé une fois qu’il a été validé ou restauré. Pour ce faire, il s’agit de la syntaxe using(...) {...} qui appelle automatiquement Dispose() lorsque le bloc d’utilisation se termine :

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        static void StartOwnTransactionWithinContext()
        {
            using (var context = new BloggingContext())
            {
                using (var dbContextTransaction = context.Database.BeginTransaction())
                {
                    context.Database.ExecuteSqlCommand(
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'"
                        );

                    var query = context.Posts.Where(p => p.Blog.Rating >= 5);
                    foreach (var post in query)
                    {
                        post.Title += "[Cool Blog]";
                    }

                    context.SaveChanges();

                    dbContextTransaction.Commit();
                }
            }
        }
    }
}

Remarque

Le début d’une transaction nécessite que la connexion de magasin sous-jacente soit ouverte. L’appel de Database.BeginTransaction() ouvre donc la connexion s’il n’est pas déjà ouvert. Si DbContextTransaction a ouvert la connexion, elle la ferme lorsque Dispose() est appelé.

Passage d’une transaction existante au contexte

Parfois, vous souhaitez une transaction qui est encore plus étendue et qui inclut des opérations sur la même base de données, mais en dehors d’EF complètement. Pour ce faire, vous devez ouvrir la connexion et démarrer la transaction vous-même, puis indiquer à EF a) d’utiliser la connexion de base de données déjà ouverte, et b) pour utiliser la transaction existante sur cette connexion.

Pour ce faire, vous devez définir et utiliser un constructeur sur votre classe de contexte qui hérite de l’un des constructeurs DbContext qui prennent i) un paramètre de connexion existant et ii) le booléen contextOwnsConnection.

Remarque

L’indicateur contextOwnsConnection doit être défini sur false lorsqu’il est appelé dans ce scénario. Cela est important, car il informe Entity Framework qu’il ne doit pas fermer la connexion quand elle est effectuée avec elle (par exemple, voir la ligne 4 ci-dessous) :

using (var conn = new SqlConnection("..."))
{
    conn.Open();
    using (var context = new BloggingContext(conn, contextOwnsConnection: false))
    {
    }
}

En outre, vous devez démarrer la transaction vous-même (y compris isolationLevel si vous souhaitez éviter le paramètre par défaut) et indiquer à Entity Framework qu’il existe une transaction existante déjà démarrée sur la connexion (voir la ligne 33 ci-dessous).

Ensuite, vous êtes libre d’exécuter des opérations de base de données directement sur SqlConnection elle-même ou sur DbContext. Toutes ces opérations sont exécutées dans une transaction. Vous êtes responsable de la validation ou de la restauration de la transaction et de l’appel de Dispose(), ainsi que de la fermeture et de la suppression de la connexion de base de données. Par exemple :

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
     class TransactionsExample
     {
        static void UsingExternalTransaction()
        {
            using (var conn = new SqlConnection("..."))
            {
               conn.Open();

               using (var sqlTxn = conn.BeginTransaction(System.Data.IsolationLevel.Snapshot))
               {
                   var sqlCommand = new SqlCommand();
                   sqlCommand.Connection = conn;
                   sqlCommand.Transaction = sqlTxn;
                   sqlCommand.CommandText =
                       @"UPDATE Blogs SET Rating = 5" +
                        " WHERE Name LIKE '%Entity Framework%'";
                   sqlCommand.ExecuteNonQuery();

                   using (var context =  
                     new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        context.Database.UseTransaction(sqlTxn);

                        var query =  context.Posts.Where(p => p.Blog.Rating >= 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }
                       context.SaveChanges();
                    }

                    sqlTxn.Commit();
                }
            }
        }
    }
}

Effacement de la transaction

Vous pouvez passer null à Database.UseTransaction() pour effacer les connaissances d’Entity Framework sur la transaction actuelle. Entity Framework ne valide ni ne restaure la transaction existante lorsque vous effectuez cette opération. Utilisez donc avec soin et uniquement si vous êtes sûr que c’est ce que vous voulez faire.

Erreurs dans UseTransaction

Vous verrez une exception de Database.UseTransaction() si vous transmettez une transaction lorsque :

  • Entity Framework a déjà une transaction existante
  • Entity Framework fonctionne déjà dans un TransactionScope
  • L’objet de connexion dans la transaction passée est null. Autrement dit, la transaction n’est pas associée à une connexion . en général, il s’agit d’un signe que cette transaction a déjà été effectuée
  • L’objet de connexion dans la transaction passée ne correspond pas à la connexion d’Entity Framework.

Utilisation de transactions avec d’autres fonctionnalités

Cette section explique comment les transactions ci-dessus interagissent avec :

  • Résilience de connexion
  • Méthodes asynchrones
  • Transactions TransactionScope

Résilience des connexions

La nouvelle fonctionnalité de résilience de connexion ne fonctionne pas avec les transactions initiées par l’utilisateur. Pour plus d’informations, consultez Nouvelles tentatives de stratégies d’exécution.

Programmation asynchrone

L’approche décrite dans les sections précédentes n’a pas besoin d’autres options ou paramètres pour fonctionner avec la requête asynchrone et enregistrer les méthodes. Toutefois, sachez que, selon ce que vous faites dans les méthodes asynchrones, cela peut entraîner des transactions de longue durée, ce qui peut à son tour entraîner des blocages ou des blocages qui sont incorrects pour les performances de l’application globale.

TransactionScope Transactions

Avant EF6, la façon recommandée de fournir des transactions d’étendue plus volumineuses était d’utiliser un objet TransactionScope :

using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        static void UsingTransactionScope()
        {
            using (var scope = new TransactionScope(TransactionScopeOption.Required))
            {
                using (var conn = new SqlConnection("..."))
                {
                    conn.Open();

                    var sqlCommand = new SqlCommand();
                    sqlCommand.Connection = conn;
                    sqlCommand.CommandText =
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'";
                    sqlCommand.ExecuteNonQuery();

                    using (var context =
                        new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        var query = context.Posts.Where(p => p.Blog.Rating > 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }
                        context.SaveChanges();
                    }
                }

                scope.Complete();
            }
        }
    }
}

SqlConnection et Entity Framework utilisent la transaction TransactionScope ambiante et sont donc validées ensemble.

À compter de .NET 4.5.1 TransactionScope a été mis à jour pour fonctionner avec des méthodes asynchrones via l’utilisation de l’énumération TransactionScopeAsyncFlowOption :

using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;

namespace TransactionsExamples
{
    class TransactionsExample
    {
        public static void AsyncTransactionScope()
        {
            using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
            {
                using (var conn = new SqlConnection("..."))
                {
                    await conn.OpenAsync();

                    var sqlCommand = new SqlCommand();
                    sqlCommand.Connection = conn;
                    sqlCommand.CommandText =
                        @"UPDATE Blogs SET Rating = 5" +
                            " WHERE Name LIKE '%Entity Framework%'";
                    await sqlCommand.ExecuteNonQueryAsync();

                    using (var context = new BloggingContext(conn, contextOwnsConnection: false))
                    {
                        var query = context.Posts.Where(p => p.Blog.Rating > 5);
                        foreach (var post in query)
                        {
                            post.Title += "[Cool Blog]";
                        }

                        await context.SaveChangesAsync();
                    }
                }
                
                scope.Complete();
            }
        }
    }
}

Il existe toujours des limitations à l’approche TransactionScope :

  • Nécessite .NET 4.5.1 ou version ultérieure pour fonctionner avec des méthodes asynchrones.
  • Il ne peut pas être utilisé dans les scénarios cloud, sauf si vous êtes sûr d’avoir une seule connexion (les scénarios cloud ne prennent pas en charge les transactions distribuées).
  • Elle ne peut pas être combinée avec l’approche Database.UseTransaction() des sections précédentes.
  • Elle lève des exceptions si vous émettez un DDL et n’avez pas activé les transactions distribuées via le service MSDTC.

Avantages de l’approche TransactionScope :

  • Il met automatiquement à niveau une transaction locale vers une transaction distribuée si vous effectuez plusieurs connexions à une base de données donnée ou combinez une connexion à une base de données avec une connexion à une autre base de données au sein de la même transaction (notez que le service MSDTC doit être configuré pour autoriser les transactions distribuées pour que cela fonctionne).
  • Facilité de codage. Si vous préférez que la transaction soit ambiante et traitée implicitement en arrière-plan plutôt que explicitement sous votre contrôle, l’approche TransactionScope peut vous convenir mieux.

En résumé, avec les nouvelles API Database.BeginTransaction() et Database.UseTransaction() ci-dessus, l’approche TransactionScope n’est plus nécessaire pour la plupart des utilisateurs. Si vous continuez à utiliser TransactionScope, tenez compte des limitations ci-dessus. Nous vous recommandons d’utiliser l’approche décrite dans les sections précédentes au lieu de cela.