我可以从 SqlConnection 对象中获取对挂起事务的引用吗?

Posted

技术标签:

【中文标题】我可以从 SqlConnection 对象中获取对挂起事务的引用吗?【英文标题】:Can I get a reference to a pending transaction from a SqlConnection object? 【发布时间】:2010-09-29 20:13:50 【问题描述】:

假设某人(除了我)编写以下代码并将其编译成程序集:

using (SqlConnection conn = new SqlConnection(connString)) 

    conn.Open();
    using (var transaction = conn.BeginTransaction())
    
        /* Update something in the database */
        /* Then call any registered OnUpdate handlers */
        InvokeOnUpdate(conn);

        transaction.Commit();
    

对 InvokeOnUpdate(IDbConnection conn) 的调用调用了我可以实现和注册的事件处理程序。因此,在这个处理程序中,我将引用 IDbConnection 对象,但不会引用挂起的事务。有什么方法可以让我持有交易吗?在我的 OnUpdate 处理程序中,我想执行类似于以下内容的操作:

private void MyOnUpdateHandler(IDbConnection conn) 

    var cmd = conn.CreateCommand();
    cmd.CommandText = someSQLString;
    cmd.CommandType = CommandType.Text;

    cmd.ExecuteNonQuery();

但是,对 cmd.ExecuteNonQuery() 的调用会引发 InvalidOperationException 抱怨

"ExecuteNonQuery 需要命令 进行交易时 分配给命令的连接是 在待处理的本地事务中。这 命令的事务属性 尚未初始化”。

我可以以任何方式将我的 SqlCommand cmd 加入待处理的事务中吗?我可以从 IDbConnection 对象中检索对待处理事务的引用吗(如果需要,我很乐意使用反射)?

【问题讨论】:

【参考方案1】:

如果有人对实现此目的的反射代码感兴趣,请点击此处:

    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 可能是个好主意。 if(currentTransaction == null ) 返回 null; -1 使用这段代码我最终遇到了这样的情况:插入,插入,无声随机回滚,在我的代码中没有任何异常,在事务之外插入更多 我已经尝试了上面的代码,它只有在你有一个实际的 SqlConnection 时才有效,它在接口级别 (IDbConnection) 不起作用,并且不适用于 OleDbConnection、OracleConnection 或任何其他实施。你们认为我们可以在接口级别(IDbConnection)完成这项工作吗?如果没有必要也获取当前活动的 IDbCommand 也会很有帮助。【参考方案2】:

哇,我一开始并不相信。我很惊讶CreateCommand() 在使用本地 SQL Server 事务时没有给出它的事务命令,并且事务没有在SqlConnection 对象上公开。实际上,在反映SqlConnection 时,当前事务甚至没有存储在该对象中。在下面的编辑中,我给了你一些提示,通过它们的一些内部类来追踪对象。

我知道你不能修改方法,但你能在方法栏周围使用 TransactionScope 吗?所以如果你有:

public static void CallingFooBar()

   using (var ts=new TransactionScope())
   
      var foo=new Foo();
      foo.Bar();
      ts.Complete();
   

这将起作用,我使用与您的代码类似的代码进行了测试,一旦我添加了包装器,如果您当然可以这样做,一切都可以正常工作。正如所指出的,如果在TransactionScope 中打开了多个连接,您将升级为分布式事务,除非您的系统为它们配置,否则您将收到错误。

使用 DTC 登记也比本地事务慢几倍。

编辑

如果你真的想尝试使用反射,SqlConnection 有一个 SqlInternalConnection,它又具有一个 AvailableInternalTransaction 属性,它返回一个 SqlInternalTransaction,它有一个 Parent 属性,它返回你需要的 SqlTransaction。

【讨论】:

【参考方案3】:

对于任何对 Denis 在 VB.NET 中制作的 C# 版本的装饰器类感兴趣的人,这里是:

using System;
using System.Collections.Generic;
using System.Text;
using System.Data;

namespace DataAccessLayer

    /// <summary>
    /// Decorator for the connection class, exposing additional info like it's transaction.
    /// </summary>
    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 构造函数中创建新连接,而不是接收已经创建的连接作为参数。最好的办法是从连接对象直接访问事务,并且根本不需要这个装饰器类......【参考方案4】:

只能使用其构造函数之一为命令对象分配事务对象。您可以采用 .NET 2.0 方法并使用在 System.Transactions 命名空间中定义的 TransactionScope 对象(具有专用程序集)。

   using System.Transactions;

    class Foo
       
        void Bar()
        
            using (TransactionScope scope = new TransactionScope())
            
                // Data access
                // ...
                scope.Complete()
            
        
    

System.Transactions 方法与 SQL Server 2005 一起使用轻量级事务协调器 (LTM)。注意不要在事务范围内使用多个连接对象,否则事务将被提升,因为它被视为分布式。然后 DTC 将处理这个更占用资源的事务版本。

【讨论】:

【参考方案5】:

我是 simple 的大力支持者,那么如何在 IDBConnection(DELEGATE PATTERN)上编写一个包装器来公开 Transaction。 (对不起 VB.NET 代码,我现在正在用 VB.NET 编写此代码)像这样:

  Public class MyConnection
      Implements IDbConnection

      Private itsConnection as IDbConnection
      Private itsTransaction as IDbTransaction

      Public Sub New(ByVal conn as IDbConnection)
         itsConnection = conn
      End Sub

      //...  'All the implementations would look like
      Public Sub Dispose() Implements IDbConnection.Dispose
         itsConnection.Dispose()
      End Sub
      //...

      //     'Except BeginTransaction which would look like
       Public Overridable Function BeginTransaction() As IDbTransaction Implements IDbConnection.BeginTransaction
         itsTransaction = itsConnection.BeginTransaction()
         Return itsTransaction
       End Function  


      // 'Now you can create a property and use it everywhere without any hacks
       Public ReadOnly Property Transaction
          Get
              return itsTransaction
          End Get
       End Property

    End Class

所以你可以将它实例化为:

Dim myConn as new MyConnection(new SqlConnection(...))

然后你就可以随时使用:

 myConn.Transaction

【讨论】:

我可能弄错了,但在我看来,您的建议要求我可以让我正在使用的第三方库实例化 MyConnection 而不是普通的 SqlConnection。不幸的是,我没有源代码,它不支持任何形式的依赖注入。 不确定你的意思。在您的情况下,您的“MyConnection”类将采用 SqlConnection、OleConnection 或您作为参数。无需注射。看看“委托设计模式”是如何工作的:en.wikipedia.org/wiki/Delegation_pattern 我明白你的意思。您的情况很困难,因为您要让第三人创建您的连接,在我的情况下,我正在创建许多连接,但它们在我的应用程序中是全局的,因此我不断忘记谁在打开哪些交易,我想保持交易我的连接。在您的情况下,我猜在您的第三方创建的连接被传递到 MyConnection 之后,交易将被跟踪。您必须在获得 conn 之前确定第 3 方是否创建了交易,因此您别无选择,只能使用反射来查找初始交易。 只要你能及早获得连接并将包装器放在它周围(如果属性不是只读的),你就应该是黄金。令人惊讶的是,第三方在创建连接时或在创建连接时或在事务开始/结束时不给您任何事件的访问权限。也许您可以创建连接并将其传递给第 3 方应用程序以替换它自己的?也许您可以使用反射将第 3 方的连接属性或内部变量设置为 MyConnection? 刚刚有了另一个想法,假设您的第三方应用程序在某些配置文件中有要使用的 sql 驱动程序,也许您可​​以将其指向使用 MyConnection(添加不带参数的构造函数来实例化 SQLConnection MyConnection 内),这应该可以由第三方应用程序加载 - 从而允许您注入 MyConnection 而不是 SQLConnection【参考方案6】:

如果有人在 .Net 4.5 上遇到此问题,您可以使用 Transaction.CurrentSystem.Transactions

【讨论】:

检索当前的TransactionScope,而不是SqlTransaction。类似但不同的技术。 你在哪里看到他需要 SqlTransaction 而不是 TransactionScope 的问题? 他问题的前 5 行。 “假设某人(除了我)编写以下代码并将其编译为程序集: using (SqlConnection conn = new SqlConnection(connString)) conn.Open(); using (var transaction = conn.BeginTransaction() )"

以上是关于我可以从 SqlConnection 对象中获取对挂起事务的引用吗?的主要内容,如果未能解决你的问题,请参考以下文章

从DbConnection获取SqlConnection

DataReader相关知识点

csharp 从DbContext获取SqlConnection

获取表的架构

如何使用相同的SqlConnection对象在多个SqlCommands中声明和使用T-SQL变量来执行多个插入?

数据库学习任务二:数据库连接对象SqlConnection