我能从一个 SqlConnection 对象中获取一个挂起事务的引用吗?
我能从一个 SqlConnection 对象中获取一个挂起事务的引用吗?
假设有人(不是我)编写了以下代码并将其编译为汇编:\n
using (SqlConnection conn = new SqlConnection(connString)) { conn.Open(); using (var transaction = conn.BeginTransaction()) { /* 在数据库中更新一些内容 */ /* 然后调用任何注册的 OnUpdate 处理程序 */ InvokeOnUpdate(conn); transaction.Commit(); } }
\n对 InvokeOnUpdate(IDbConnection conn) 的调用会调用一个我可以实现和注册的事件处理程序。因此,在这个处理程序中,我将拥有一个对 IDbConnection 对象的引用,但我将没有对未决事务的引用。是否有任何方法可以获取事务的引用?在我的 OnUpdate 处理程序中,我想执行类似以下的操作:\n
private void MyOnUpdateHandler(IDbConnection conn) { var cmd = conn.CreateCommand(); cmd.CommandText = someSQLString; cmd.CommandType = CommandType.Text; cmd.ExecuteNonQuery(); }
\n然而,对 cmd.ExecuteNonQuery() 的调用会引发一个 InvalidOperationException,指出:\n
\n\"当连接分配给命令处于挂起的本地事务中时,ExecuteNonQuery 需要命令具有事务。命令的 Transaction 属性尚未初始化。\"\n
\n我是否可以以任何方式将我的 SqlCommand cmd 登记到未决事务中?是否可以从 IDbConnection 对象中检索到未决事务的引用(如果需要,我可以使用反射来实现)?
问题的原因是在C#中,SqlConnection类没有直接提供获取挂起事务的方法。解决方法是创建一个装饰类ConnectionWithExtraInfo,该类实现了IDbConnection接口,并包含一个私有的IDbConnection对象和一个私有的IDbTransaction对象,通过该装饰类可以间接访问到连接对象的事务。
具体代码如下:
using System; using System.Collections.Generic; using System.Text; using System.Data; namespace DataAccessLayer { ////// Decorator for the connection class, exposing additional info like it's transaction. /// public class ConnectionWithExtraInfo : IDbConnection { private IDbConnection connection = null; private IDbTransaction transaction = null; public IDbConnection Connection { get { return connection; } } public IDbTransaction Transaction { get { return transaction; } } public ConnectionWithExtraInfo(IDbConnection connection) { this.connection = connection; } #region IDbConnection Members public IDbTransaction BeginTransaction(IsolationLevel il) { transaction = connection.BeginTransaction(il); return transaction; } public IDbTransaction BeginTransaction() { transaction = connection.BeginTransaction(); return transaction; } public void ChangeDatabase(string databaseName) { connection.ChangeDatabase(databaseName); } public void Close() { connection.Close(); } public string ConnectionString { get { return connection.ConnectionString; } set { connection.ConnectionString = value; } } public int ConnectionTimeout { get { return connection.ConnectionTimeout; } } public IDbCommand CreateCommand() { return connection.CreateCommand(); } public string Database { get { return connection.Database; } } public void Open() { connection.Open(); } public ConnectionState State { get { return connection.State; } } #endregion #region IDisposable Members public void Dispose() { connection.Dispose(); } #endregion } }
然后,有人指出这种方法的缺点是只能在连接没有活动事务的情况下使用。对于已经存在活动事务的情况,这种方法不适用。建议在ConnectionWithExtraInfo的构造函数中创建新的连接,而不是接收一个已存在的连接作为参数。最好的解决方法是直接从连接对象中获取事务,这样就不需要使用装饰类ConnectionWithExtraInfo了。
问题的出现的原因是SqlConnection
对象没有直接提供对挂起事务的引用。在反射SqlConnection
对象时,当前事务甚至没有存储在该对象中。
解决方法是使用TransactionScope
将方法bar
包装起来。在TransactionScope
中创建新的事务,并在方法bar
执行完毕后调用Complete()
方法提交事务。需要注意的是,如果在TransactionScope
中打开了多个连接,将会升级为分布式事务,如果系统没有配置分布式事务,将会出错。而且与本地事务相比,与DTC的协同需要更多的时间。
如果真的想要尝试使用反射,可以通过SqlConnection
对象的SqlInternalConnection
属性获取SqlInternalTransaction
对象,然后通过Parent
属性获取SqlTransaction
对象。
问题的出现原因是:需要从一个SqlConnection对象中获取一个待处理事务的引用。
解决方法是:使用反射来实现这个功能。首先,通过反射获取SqlConnection对象的InnerConnection属性,再通过InnerConnection对象获取CurrentTransaction属性和Parent属性,最后将Parent属性转换为SqlTransaction类型并返回。
代码如下:
private static readonly PropertyInfo ConnectionInfo = typeof(SqlConnection).GetProperty("InnerConnection", BindingFlags.NonPublic | BindingFlags.Instance); private static SqlTransaction GetTransaction(IDbConnection conn) { var internalConn = ConnectionInfo.GetValue(conn, null); var currentTransactionProperty = internalConn.GetType().GetProperty("CurrentTransaction", BindingFlags.NonPublic | BindingFlags.Instance); var currentTransaction = currentTransactionProperty.GetValue(internalConn, null); var realTransactionProperty = currentTransaction.GetType().GetProperty("Parent", BindingFlags.NonPublic | BindingFlags.Instance); var realTransaction = realTransactionProperty.GetValue(currentTransaction, null); return (SqlTransaction) realTransaction; }
需要注意的是:
- 这些类型和属性都是内部的,且属性都是私有的,所以无法使用动态类型。
- 由于类型是内部的,无法像ConnectionInfo那样声明中间类型,而需要使用对象的GetType方法。
- 在继续操作之前,最好先检查currentTransaction的值。如果currentTransaction为null,则返回null。
还有一位用户提到了使用上述代码后遇到的问题,即在代码中没有任何异常的情况下,出现了插入、插入和随机回滚的情况。另一位用户则提出了是否能够在接口级别(IDbConnection)上实现这个功能的问题,并且还希望能够获取当前活动的IDbCommand。
文章完整内容如上。