2009-01-06 12 views
34

Supongamos que alguien (aparte de mí), escribe el siguiente código y compila en un ensamblado:¿Puedo obtener una referencia a una transacción pendiente desde un objeto SqlConnection?

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

La llamada a InvokeOnUpdate (IDbConnection conn) llama a un controlador de eventos que pueda implementar y registrar. Por lo tanto, en este controlador tendré una referencia al objeto IDbConnection, pero no tendré una referencia a la transacción pendiente. ¿Hay alguna manera en que pueda obtener la transacción? En mi manejador OnUpdate Quiero ejecutar algo similar a lo siguiente:

private void MyOnUpdateHandler(IDbConnection conn) 
{ 
    var cmd = conn.CreateCommand(); 
    cmd.CommandText = someSQLString; 
    cmd.CommandType = CommandType.Text; 

    cmd.ExecuteNonQuery(); 
} 

Sin embargo, la llamada a cmd.ExecuteNonQuery() lanza un InvalidOperationException quejándose de que

"ExecuteNonQuery requiere el comando tener una transacción cuando la conexión asignada al comando es en una transacción local pendiente. La propiedad Transaction del comando no se ha inicializado ".

¿Puedo de alguna manera enlistar mi cmd de SqlCommand con la transacción pendiente? ¿Puedo recuperar una referencia a la transacción pendiente desde el objeto IDbConnection (me gustaría usar el reflejo si es necesario)?

Respuesta

7

Wow No creí esto al principio. Me sorprende que CreateCommand() no dé al comando su transacción cuando usa transacciones locales de SQL Server, y que la transacción no está expuesta en el objeto SqlConnection. En realidad, al reflexionar sobre SqlConnection, la transacción actual ni siquiera está almacenada en ese objeto. En la edición de abajo, te di algunos consejos para rastrear el objeto a través de algunas de sus clases internas.

Sé que no puede modificar el método, pero podría utilizar un TransactionScope alrededor de la barra de métodos? Así que si usted tiene:

public static void CallingFooBar() 
{ 
    using (var ts=new TransactionScope()) 
    { 
     var foo=new Foo(); 
     foo.Bar(); 
     ts.Complete(); 
    } 
} 

Esto funciona, he probado el uso de código simillar a la suya y una vez que agrego el envoltorio todo funciona bien si se puede hacer esto, por supuesto. Como se indicó, tenga cuidado de que si se abre más de una conexión dentro de TransactionScope, se le escalará a una transacción distribuida, a menos que su sistema esté configurado para ellos, obtendrá un error.

Alistar con el DTC también es varias veces más lento que una transacción local.

Editar

si realmente quiere tratar de utilizar la reflexión, SqlConnection tiene un SqlInternalConnection esto a su vez tiene una propiedad de AvailableInternalTransaction que devuelve un SqlInternalTransaction, esto tiene una propiedad de matriz que devuelve el SqlTransaction que había necesidad .

3

Al objeto de comando solo se le puede asignar un objeto de transacción utilizando uno de sus constructores. Puede optar por el enfoque .NET 2.0 y utilizar un objeto TransactionScope que se define en el espacio de nombres System.Transactions (tiene un ensamblaje dedicado).

using System.Transactions; 

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

El enfoque System.Transactions utiliza en conjunción con SQL Server 2005, un coordinador de transacciones ligero (LTM). Tenga cuidado de no utilizar múltiples objetos de conexión en su alcance de transacción o la transacción se promocionará ya que se ve como distribuida. DTC manejará esta versión más intensiva de recursos de la transacción.

0

Soy un gran defensor de lo simple así que ¿qué tal escribir un contenedor sobre IDBConnection (PATRÓN DELEGADO) que expone Transacción. (Lo siento por código VB.NET, estoy escribiendo esto en VB.NET en este momento) Algo como esto:

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 

por lo que sería una instancia como esta:

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

y entonces se puede obtener la transacción en cualquier momento mediante el uso de:

myConn.Transaction 
+0

Puedo estar equivocado, pero me parece que su sugerencia requiere que pueda hacer que la biblioteca de terceros que estoy utilizando crea una instancia de MyConnection en lugar de una simple SqlConnection. Desafortunadamente, no tengo el código fuente y no admite la inyección de dependencia en ninguna forma. – Rune

+0

No estoy seguro de lo que quiere decir. En su caso, su clase "MyConnection" tomaría SqlConnection, OleConnection o lo que tiene como argumento. No se necesita inyección. Eche un vistazo a cómo funciona el "Patrón de diseño de delegación", como aquí: http://en.wikipedia.org/wiki/Delegation_pattern – Denis

+0

Veo lo que quiere decir. Tu caso es difícil porque estás dejando que un tercero cree tu conexión, en mi caso estoy creando muchas conexiones, pero son globales en mi aplicación, así que estoy perdiendo constantemente la pista de quién está abriendo qué transacciones y me gustaría mantener las transacciones con mi conexión. En su caso, supongo que después de que su conexión creada por un tercero se transfiera a MyConnection, se hará un seguimiento de las transacciones. Debe averiguar si el tercero creó una transacción antes de obtener un contacto, por lo que no tendría más remedio que utilizar el reflejo para encontrar la transacción inicial. – Denis

4

Para cualquiera que esté interesado en la versión C# de la clase decoradora que Denis hizo en VB.NET, aquí está:

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 
    } 
} 
+0

No utilizaría esto. Puede crear una instancia de uno de estos después de que ya tenga una transacción activa en la conexión, y en ese caso esto no funcionaría. – AyCabron

+0

Tiene razón, esto funciona solo si pasa una conexión que no tiene una transacción activa ... para su caso una mejor solución es crear la nueva conexión en el constructor ConnectionWithExtraInfo, en lugar de recibir una conexión ya creada como un parámetro. Lo mejor sería tener acceso directo a la transacción desde el objeto de conexión, y esta clase de decorador no se necesitaría en absoluto ... – victorvartan

16

En caso de que alguien se interesa por el código de reflexión para lograr esto, aquí va:

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; 
    } 

Notas:

  • Los tipos son internas y las propiedades privadas por lo que no puede use los tipos internos
  • que también le impiden declarar los tipos intermedios como lo hice con el primer ConnectionInfo. Tengo que use gettype en los objetos
+2

Comprobar la actualTransacción antes de continuar puede ser una buena idea. if (currentTransaction == null) return null; – kerem

-2

En caso de que alguien se enfrentó a este problema en .Net 4.5 se puede utilizar en Transaction.Current System.Transactions.

+0

Recupera el TransactionScope actual, no el SqlTransaction. Similar, pero diferente tecnología. –

+0

¿Dónde ves en la pregunta que necesita SqlTransaction y no TransactionScope? – A77

+0

Las primeras 5 líneas de su pregunta. "Supongamos que alguien (aparte de mí), escribe el siguiente código y compila en un ensamblado: usando (SqlConnection conn = new SqlConnection (connString)) { conn.Open(); usando (transacción var = ** conn .BeginTransaction()) ** " –

Cuestiones relacionadas