web-dev-qa-db-fra.com

Impossible d'accéder à l'objet SqlTransaction pour restaurer dans le bloc catch

J'ai un problème et tous les articles ou exemples que j'ai trouvés semblent ne pas s'en soucier.

Je souhaite effectuer certaines actions de base de données dans une transaction. Ce que je veux faire est très similaire à la plupart des exemples:

using (SqlConnection Conn = new SqlConnection(_ConnectionString))
{
    try
    {
        Conn.Open();
        SqlTransaction Trans = Conn.BeginTransaction();

        using (SqlCommand Com = new SqlCommand(ComText, Conn))
        {
            /* DB work */
        }
    }
    catch (Exception Ex)
    {
        Trans.Rollback();
        return -1;
    }
}

Mais le problème est que le SqlTransaction Trans Est déclaré à l'intérieur du bloc try. Il n'est donc pas accessible dans le bloc catch(). La plupart des exemples ne font que Conn.Open() et Conn.BeginTransaction() avant le bloc try, mais je pense que c'est un peu risqué, car les deux peuvent lever plusieurs exceptions.

Ai-je tort ou la plupart des gens ignorent-ils simplement ce risque? Quelle est la meilleure solution pour pouvoir annuler, si une exception se produit?

33
Marks
using (var Conn = new SqlConnection(_ConnectionString))
{
    SqlTransaction trans = null;
    try
    {
        Conn.Open();
        trans = Conn.BeginTransaction();

        using (SqlCommand Com = new SqlCommand(ComText, Conn, trans))
        {
            /* DB work */
        }
        trans.Commit();
    }
    catch (Exception Ex)
    {
        if (trans != null) trans.Rollback();
        return -1;
    }
}

ou vous pourriez devenir encore plus propre et plus facile et utiliser ceci:

using (var Conn = new SqlConnection(_ConnectionString))
{
    try
    {
        Conn.Open();
        using (var ts = new System.Transactions.TransactionScope())
        {
            using (SqlCommand Com = new SqlCommand(ComText, Conn))
            {
                /* DB work */
            }
            ts.Complete();
        }
    }
    catch (Exception Ex)
    {     
        return -1;
    }
}
56
Dave Markle

Je n'aime pas taper les types et mettre les variables à null, donc:

try
{
    using (var conn = new SqlConnection(/* connection string or whatever */))
    {
        conn.Open();

        using (var trans = conn.BeginTransaction())
        {
            try
            {
                using (var cmd = conn.CreateCommand())
                {
                    cmd.Transaction = trans;
                    /* setup command type, text */
                    /* execute command */
                }

                trans.Commit();
            }
            catch (Exception ex)
            {
                trans.Rollback();
                /* log exception and the fact that rollback succeeded */
            }
        }
    }
}
catch (Exception ex)
{
    /* log or whatever */
}

Et si vous vouliez passer à MySql ou à un autre fournisseur, vous n'auriez qu'à modifier 1 ligne.

8
Mike Trusov

utilisez ceci

using (SqlConnection Conn = new SqlConnection(_ConnectionString))
{
    SqlTransaction Trans = null;
    try
    {
        Conn.Open();
        Trans = Conn.BeginTransaction();

        using (SqlCommand Com = new SqlCommand(ComText, Conn))
        {
            /* DB work */
        }
    }
    catch (Exception Ex)
    {
        if (Trans != null)
            Trans.Rollback();
        return -1;
    }
}

BTW - Vous ne l'avez pas validé en cas de traitement réussi

6
Itay Karo
using (SqlConnection Conn = new SqlConnection(_ConnectionString))
{
    try
    {
        Conn.Open();
        SqlTransaction Trans = Conn.BeginTransaction();

        try 
        {
            using (SqlCommand Com = new SqlCommand(ComText, Conn))
            {
                /* DB work */
            }
        }
        catch (Exception TransEx)
        {
            Trans.Rollback();
            return -1;
        }
    }
    catch (Exception Ex)
    {
        return -1;
    }
}
3
Paul Talbot
SqlConnection conn = null;
SqlTransaction trans = null;

try
{
   conn = new SqlConnection(_ConnectionString);
   conn.Open();
   trans = conn.BeginTransaction();
   /*
    * DB WORK
    */
   trans.Commit();
}
catch (Exception ex)
{
   if (trans != null)
   {
      trans.Rollback();
   }
   return -1;
}
finally
{
   if (conn != null)
   {
      conn.Close();
   }
}
1
Ibki

Lorsque j'ai trouvé cette question pour la première fois fin 2018, je ne pensais pas qu'il pouvait y avoir un bug dans la réponse alors votée, mais c'est parti. J'ai d'abord pensé à simplement commenter la réponse, mais encore une fois, je voulais sauvegarder ma demande avec mes propres références. Et les tests que j'ai effectués (basés sur .Net Framework 4.6.1 et .Net Core 2.1.)

Compte tenu de la contrainte de l'OP, la transaction doit être déclarée au sein de la connexion ce qui nous laisse aux 2 implémentations différentes déjà mentionnées dans d'autres réponses:

Utilisation de TransactionScope

using (SqlConnection conn = new SqlConnection(conn2))
{
    try
    {
        conn.Open();
        using (TransactionScope ts = new TransactionScope())
        {
            conn.EnlistTransaction(Transaction.Current);
            using (SqlCommand command = new SqlCommand(query, conn))
            {
                command.ExecuteNonQuery();
                //TESTING: throw new System.InvalidOperationException("Something bad happened.");
            }
            ts.Complete();
        }
    }
    catch (Exception)
    {
        throw;
    }
}

Utilisation de SqlTransaction

using (SqlConnection conn = new SqlConnection(conn3))
{
    try
    {
        conn.Open();
        using (SqlTransaction ts = conn.BeginTransaction())
        {
            using (SqlCommand command = new SqlCommand(query, conn, ts))
            {
                command.ExecuteNonQuery();
                //TESTING: throw new System.InvalidOperationException("Something bad happened.");
            }
            ts.Commit();
        }
    }
    catch (Exception)
    {
        throw;
    }
}

Vous devez savoir que lorsque vous déclarez un TransactionScope dans un SqlConnection, cet objet de connexion n'est pas pas automatiquement enrôlé dans la transaction, mais à la place, vous devez l'enrôler explicitement avec conn.EnlistTransaction(Transaction.Current);

Tester et prouver
J'ai préparé un tableau simple dans une base de données SQL Server:

SELECT * FROM [staging].[TestTable]

Column1
-----------
1

La requête de mise à jour dans .NET est la suivante:

string query = @"UPDATE staging.TestTable
                    SET Column1 = 2";

Et juste après command.ExecuteNonQuery () une exception est levée:

command.ExecuteNonQuery();
throw new System.InvalidOperationException("Something bad happened.");

Voici l'exemple complet pour votre référence:

string query = @"UPDATE staging.TestTable
                    SET Column1 = 2";

using (SqlConnection conn = new SqlConnection(conn2))
{
    try
    {
        conn.Open();
        using (TransactionScope ts = new TransactionScope())
        {
            conn.EnlistTransaction(Transaction.Current);
            using (SqlCommand command = new SqlCommand(query, conn))
            {
                command.ExecuteNonQuery();
                throw new System.InvalidOperationException("Something bad happened.");
            }
            ts.Complete();
        }
    }
    catch (Exception)
    {
        throw;
    }
}

Si le test est exécuté, il déclenche une exception avant la fin de TransactionScope et la mise à jour n'est pas appliquée à la table (restauration transactionnelle) et la valeur reste inchangée. C'est le comportement souhaité, comme tout le monde s'y attend.

Column1
-----------
1

Que se passe-t-il maintenant si nous avons oublié d'enregistrer la connexion dans la transaction avec conn.EnlistTransaction(Transaction.Current);?

La réexécution de l'exemple provoque à nouveau l'exception et le flux d'exécution passe immédiatement au bloc catch. Bien que ts.Complete(); ne soit jamais appelée, la valeur de la table a changé:

Column1
-----------
2

Comme la portée de la transaction est déclarée après la SqlConnection, la connexion n'est pas consciente de la portée et ne s'inscrit pas implicitement dans le soi-disant transaction ambiante .

Analyse plus approfondie des nerds de la base de données

Pour creuser encore plus, si l'exécution s'interrompt après command.ExecuteNonQuery(); et avant que l'exception ne soit levée, nous pouvons interroger la transaction sur la base de données (SQL Server) comme suit:

SELECT tst.session_id, tat.transaction_id, is_local, open_transaction_count, transaction_begin_time, dtc_state, dtc_status
  FROM sys.dm_tran_session_transactions tst
  LEFT JOIN sys.dm_tran_active_transactions tat
  ON tst.transaction_id = tat.transaction_id
  WHERE tst.session_id IN (SELECT session_id FROM sys.dm_exec_sessions WHERE program_name = 'TransactionScopeTest')

Notez qu'il est possible de définir la session program_name via la propriété Application Name dans la chaîne de connexion:Application Name=TransactionScopeTest;

La transaction actuellement en cours se déroule ci-dessous:

session_id  transaction_id       is_local open_transaction_count transaction_begin_time  dtc_state   dtc_status
----------- -------------------- -------- ---------------------- ----------------------- ----------- -----------
113         6321722              1        1                      2018-11-30 09:09:06.013 0           0

Sans la conn.EnlistTransaction(Transaction.Current); aucune transaction n'est liée à la connexion active et donc les modifications ne se produisent pas dans un contexte transactionnel:

session_id  transaction_id       is_local open_transaction_count transaction_begin_time  dtc_state   dtc_status
----------- -------------------- -------- ---------------------- ----------------------- ----------- -----------

Remarques .NET Framework vs .NET Core
Lors de mes tests avec .NET Core, je suis tombé sur l'exception suivante:

System.NotSupportedException: 'Enlisting in Ambient transactions is not supported.'

Il semble .NET Core (2.1.0) ne prend actuellement pas en charge l'approche TransactionScope, que l'étendue soit initialisée avant ou après SqlConnection.

1
Hans

Exemples Microsoft, placez le trans de début en dehors du try/catch voir ce lien msdn . Je suppose que la méthode BeginTransaction doit soit lever une exception OR commencer une transaction mais jamais les deux (bien que la documentation ne dise pas que cela est impossible).

Cependant, il vaut peut-être mieux utiliser TransactionScope qui gère une grande partie (pas si) du poids lourd pour vous: ce lien

1
Daniel Renshaw