无法访问 SqlTransaction 对象以在 catch 块中回滚
Posted
技术标签:
【中文标题】无法访问 SqlTransaction 对象以在 catch 块中回滚【英文标题】:Cannot access SqlTransaction object to rollback in catch block 【发布时间】:2011-02-24 02:56:19 【问题描述】:我遇到了一个问题,我找到的所有文章或示例似乎都不关心它。
我想在事务中执行一些数据库操作。我想做的与大多数示例非常相似:
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;
但问题是SqlTransaction Trans
是在try
块内声明的。所以它在catch()
块中是不可访问的。大多数示例只是在try
块之前执行Conn.Open()
和Conn.BeginTransaction()
,但我认为这有点冒险,因为两者都可能引发多个异常。
我错了,还是大多数人都忽略了这种风险? 如果发生异常,能够回滚的最佳解决方案是什么?
【问题讨论】:
P.S.您确定要返回 -1(错误代码)而不是抛出异常吗? 【参考方案1】: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;
或者你可以更简洁、更轻松地使用它:
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;
【讨论】:
第二个版本真的是在抛出异常的时候回滚吗?编辑:好的,在阅读文档后我已经看到了。 在您的第一个示例中,您不需要指定sqlcommand与事务相关联吗?比如using (SqlCommand Com = new SqlCommand(ComText, Conn, **trans**))
?或者那是不必要的?它是隐式关联的吗?
好的,谢谢。使用 TransactionScope,您不需要,但我在第一个示例中省略了它。相应地进行了编辑。
在第二个示例中,我认为您需要使用块关闭 TransactionScope 内的连接,否则当它离开块时会出现异常,说明连接未关闭。
只需将此行 trans = Conn.BeginTransaction();
包装在 using
语句中并阻塞,然后如果在调用提交之前发生异常,则将在处理之前为您调用 Rollback()
。
【参考方案2】:
我不喜欢键入类型并将变量设置为 null,所以:
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 */
如果你想切换到 mysql 或其他提供程序,你只需要修改 1 行。
【讨论】:
【参考方案3】:使用这个
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;
顺便说一句 - 如果处理成功,您没有提交它
【讨论】:
【参考方案4】: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;
【讨论】:
虽然还有更多的代码需要编写,但这提供了最好的粒度来确定每个步骤为什么会失败。但是,请注意 SqlCommand 必须与事务相关联。【参考方案5】:当我在 2018 年底第一次发现这个问题时,我没想到当时投票最多的答案中可能存在错误,但事实就是如此。我首先考虑简单地评论答案,但后来我想用我自己的参考来支持我的主张。以及我所做的测试(基于 .Net Framework 4.6.1 和 .Net Core 2.1。)
鉴于 OP 的约束,事务应在连接中声明,这使我们只能使用其他答案中已经提到的 2 个不同的实现:
使用 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;
使用 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 之后声明的,因此连接不知道范围,也不会隐式加入所谓的ambient transaction。
对数据库书呆子的深入分析
为了更深入地挖掘,如果在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.'
seems.NET Core (2.1.0) 目前不支持 TransactionScope 方式,无论 Scope 是在 SqlConnection 之前还是之后初始化。
【讨论】:
【参考方案6】:Microsoft 示例,将 begin trans 放在 try/catch see this msdn link 之外。我假设 BeginTransaction 方法应该要么抛出异常,要么开始事务,但绝不应该两者都发生(尽管文档没有说这是不可能的)。
但是,您最好使用TransactionScope,它可以为您管理很多(不是那么)繁重的工作:this link
【讨论】:
【参考方案7】: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();
【讨论】:
以上是关于无法访问 SqlTransaction 对象以在 catch 块中回滚的主要内容,如果未能解决你的问题,请参考以下文章
CommandBuilder 和 SqlTransaction 插入/更新行
为啥使用带有 SqlTransaction 的 using 语句?
将 SqlTransaction 与 SqlDataReader 一起使用