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?
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;
}
}
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.
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
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;
}
}
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();
}
}
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.
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