当我在 2018 年底第一次发现这个问题时,我并不认为当时投票最多的答案中可能存在错误,但事实就是如此。我首先考虑简单地评论答案,但后来我想用我自己的参考来支持我的主张。以及我所做的测试(基于 .Net Framework 4.6.1 和 .Net Core 2.1。)
鉴于 OP 的约束,事务应在连接中声明,这使我们只能使用其他答案中已经提到的 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();
//TESTING: throw new System.InvalidOperationException("Something bad happened.");
}
ts.Complete();
}
}
catch (Exception)
{
throw;
}
}
使用 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;
}
}
您应该知道,在 SqlConnection 中声明 TransactionScope 时,连接对象不会自动登记到 Transaction 中,而是必须使用显式登记它conn.EnlistTransaction(Transaction.Current);
测试并证明
我在 SQL Server 数据库中准备了一个简单的表:
SELECT * FROM [staging].[TestTable]
Column1
-----------
1
.NET 中的更新查询如下:
string query = @"UPDATE staging.TestTable
SET Column1 = 2";
在 command.ExecuteNonQuery() 之后立即引发异常:
command.ExecuteNonQuery();
throw new System.InvalidOperationException("Something bad happened.");
这是供您参考的完整示例:
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;
}
}
如果执行测试,它会在 TransactionScope 完成之前引发异常,并且更新不会应用于表(事务回滚)并且值保持不变。这是每个人都期望的预期行为。
Column1
-----------
1
如果我们忘记在事务中登记连接会发生什么conn.EnlistTransaction(Transaction.Current);
?
重新运行示例再次引发异常,执行流程立即跳转到 catch 块。尽管ts.Complete();
从未被称为表值已更改:
Column1
-----------
2
由于事务范围是在 SqlConnection 之后声明的,因此连接不知道范围并且不会隐式登记在所谓的环境事务中。
对数据库书呆子的深入分析
为了更深入地挖掘,如果command.ExecuteNonQuery();
在引发异常之后和之前执行暂停,我们可以查询数据库(SQL Server)上的事务,如下所示:
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')
请注意,可以通过连接字符串中的应用程序名称属性设置会话程序名称: Application Name=TransactionScopeTest;
目前现有的交易正在以下展开:
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
如果conn.EnlistTransaction(Transaction.Current);
没有事务绑定到活动连接,因此更改不会在事务上下文下发生:
session_id transaction_id is_local open_transaction_count transaction_begin_time dtc_state dtc_status
----------- -------------------- -------- ---------------------- ----------------------- ----------- -----------
备注 .NET Framework 与 .NET Core
在我使用 .NET Core 进行测试期间,我遇到了以下异常:
System.NotSupportedException: 'Enlisting in Ambient transactions is not supported.'
看来.NET Core (2.1.0) 目前不支持 TransactionScope 方法,无论 Scope 是在 SqlConnection 之前还是之后初始化的。