Implementar una transacción implícita mediante el ámbito de la transacción

La clase TransactionScope proporciona una manera simple de marcar un bloque de código como participar en una transacción, sin exigirle que interactuara con la propia transacción. Un ámbito de la transacción puede seleccionar y administrar automáticamente la transacción ambiente. Debido a su facilidad de uso y eficacia, se recomienda que utilice la clase TransactionScope al desarrollar una aplicación de transacción.

Además, no necesita dar de alta explícitamente los recursos con la transacción. Cualquier administrador de recursos System.Transactions (como SQL Server 2005) puede detectar la existencia de una transacción ambiente creada por el ámbito y automáticamente darse de alta.

Crear un ámbito de la transacción

En el ejemplo siguiente se muestra un uso sencillo de la clase TransactionScope.

// This function takes arguments for 2 connection strings and commands to create a transaction
// involving two SQL Servers. It returns a value > 0 if the transaction is committed, 0 if the
// transaction is rolled back. To test this code, you can connect to two different databases
// on the same server by altering the connection string, or to another 3rd party RDBMS by
// altering the code in the connection2 code block.
static public int CreateTransactionScope(
    string connectString1, string connectString2,
    string commandText1, string commandText2)
{
    // Initialize the return value to zero and create a StringWriter to display results.
    int returnValue = 0;
    System.IO.StringWriter writer = new System.IO.StringWriter();

    try
    {
        // Create the TransactionScope to execute the commands, guaranteeing
        // that both commands can commit or roll back as a single unit of work.
        using (TransactionScope scope = new TransactionScope())
        {
            using (SqlConnection connection1 = new SqlConnection(connectString1))
            {
                // Opening the connection automatically enlists it in the
                // TransactionScope as a lightweight transaction.
                connection1.Open();

                // Create the SqlCommand object and execute the first command.
                SqlCommand command1 = new SqlCommand(commandText1, connection1);
                returnValue = command1.ExecuteNonQuery();
                writer.WriteLine("Rows to be affected by command1: {0}", returnValue);

                // If you get here, this means that command1 succeeded. By nesting
                // the using block for connection2 inside that of connection1, you
                // conserve server and network resources as connection2 is opened
                // only when there is a chance that the transaction can commit.
                using (SqlConnection connection2 = new SqlConnection(connectString2))
                {
                    // The transaction is escalated to a full distributed
                    // transaction when connection2 is opened.
                    connection2.Open();

                    // Execute the second command in the second database.
                    returnValue = 0;
                    SqlCommand command2 = new SqlCommand(commandText2, connection2);
                    returnValue = command2.ExecuteNonQuery();
                    writer.WriteLine("Rows to be affected by command2: {0}", returnValue);
                }
            }

            // The Complete method commits the transaction. If an exception has been thrown,
            // Complete is not  called and the transaction is rolled back.
            scope.Complete();
        }
    }
    catch (TransactionAbortedException ex)
    {
        writer.WriteLine("TransactionAbortedException Message: {0}", ex.Message);
    }

    // Display messages.
    Console.WriteLine(writer.ToString());

    return returnValue;
}
'  This function takes arguments for 2 connection strings and commands to create a transaction 
'  involving two SQL Servers. It returns a value > 0 if the transaction is committed, 0 if the 
'  transaction is rolled back. To test this code, you can connect to two different databases 
'  on the same server by altering the connection string, or to another 3rd party RDBMS  
'  by altering the code in the connection2 code block.
Public Function CreateTransactionScope( _
  ByVal connectString1 As String, ByVal connectString2 As String, _
  ByVal commandText1 As String, ByVal commandText2 As String) As Integer

    ' Initialize the return value to zero and create a StringWriter to display results.
    Dim returnValue As Integer = 0
    Dim writer As System.IO.StringWriter = New System.IO.StringWriter

    Try
        ' Create the TransactionScope to execute the commands, guaranteeing
        '  that both commands can commit or roll back as a single unit of work.
        Using scope As New TransactionScope()
            Using connection1 As New SqlConnection(connectString1)
                ' Opening the connection automatically enlists it in the 
                ' TransactionScope as a lightweight transaction.
                connection1.Open()

                ' Create the SqlCommand object and execute the first command.
                Dim command1 As SqlCommand = New SqlCommand(commandText1, connection1)
                returnValue = command1.ExecuteNonQuery()
                writer.WriteLine("Rows to be affected by command1: {0}", returnValue)

                ' If you get here, this means that command1 succeeded. By nesting
                ' the using block for connection2 inside that of connection1, you
                ' conserve server and network resources as connection2 is opened
                ' only when there is a chance that the transaction can commit.   
                Using connection2 As New SqlConnection(connectString2)
                    ' The transaction is escalated to a full distributed
                    ' transaction when connection2 is opened.
                    connection2.Open()

                    ' Execute the second command in the second database.
                    returnValue = 0
                    Dim command2 As SqlCommand = New SqlCommand(commandText2, connection2)
                    returnValue = command2.ExecuteNonQuery()
                    writer.WriteLine("Rows to be affected by command2: {0}", returnValue)
                End Using
            End Using

            ' The Complete method commits the transaction. If an exception has been thrown,
            ' Complete is called and the transaction is rolled back.
            scope.Complete()
        End Using
    Catch ex As TransactionAbortedException
        writer.WriteLine("TransactionAbortedException Message: {0}", ex.Message)
    End Try

    ' Display messages.
    Console.WriteLine(writer.ToString())

    Return returnValue
End Function

El ambiente de transacción ha comenzado una vez que se ha creado un nuevo objeto TransactionScope . Tal y como se ilustra en la muestra del código se recomienda la creación de ambientes con una instrucción using. La instrucción using está disponible tanto en C# como en Visual Basic y funciona como un bloque try...finally para asegurarse de que el ámbito se elimina correctamente.

Al crear una instancia de TransactionScope, el administrador de transacciones determina en qué transacción participar. Una vez determinado, el ámbito siempre participa en esa transacción. La decisión se basa en dos factores: si está presente una transacción de ambiente y el valor del parámetro TransactionScopeOption del constructor. La transacción ambiente es la transacción dentro de la que su código se ejecuta. Puede obtener una referencia a la transacción ambiente llamando a la propiedad estática Transaction.Current de la clase Transaction. Para obtener más información sobre cómo se utiliza esta enumeración, vea el apartado Administración de flujo de transacciones con TransactionScopeOption del tema.

Completar un ámbito de la transacción

Cuando la aplicación termina todo el trabajo que tiene que llevar a cabo en una transacción, debe llamar al método TransactionScope.Complete solo una vez para notificar al administrador de transacciones que la transacción se puede confirmar. Es muy recomendable colocar la llamada Complete como la última instrucción del bloque using.

Si no se puede llamar a este método se anula la transacción, dado que el administrador de transacciones interpreta esto como un error del sistema o es equivalente a cuando se producen excepciones dentro del ámbito de la transacción. Sin embargo, llamar a este método no garantiza que se vaya a confirmar la transacción. Es solo una manera de informar al administrador de transacciones de su estado. Después de llamar a este métodoComplete ya no podrá obtener acceso a la transacción de ambiente mediante la propiedad Current y, si intenta hacerlo, se producirá una excepción.

Si el objeto TransactionScope creara inicialmente la transacción, el trabajo real de confirmar la transacción por el administrador de transacciones se produce después de la última línea de código en el bloque using. Si no ha creado la transacción, se produce la confirmación cada vez que el propietario del objeto Commit llama al método CommittableTransaction. En ese punto el administrador de transacciones llama a los administradores de recursos y les informa de si se va a confirmar o deshacer la transacción, basándose en si se ha llamado al método Complete en el objeto TransactionScope.

La instrucción using asegura que el método Dispose de TransactionScope se llama aún cuando se produzca una excepción. El método Dispose marca el fin del ámbito de la transacción. Las excepciones que se producen después de llamar a este método quizá no afecten a la transacción. Este método también restaura la transacción de ambiente a su estado previo.

Se inicia TransactionAbortedException si el ámbito crea la transacción, y ésta se anula. Se inicia TransactionInDoubtException si el administrador de transacciones no puede llegar a una decisión de la confirmación. No se produce ninguna excepción si se confirma la transacción.

Revertir una transacción

Si desea revertir una transacción, no debería llamar al método Complete dentro del ámbito de la transacción. Por ejemplo, puede producir una excepción dentro del ámbito. Se deshará la transacción en la que participa.

Flujo de la transacción de administración utilizando TransactionScopeOption

El ámbito de la transacción puede estar anidado al llamar a un método que utiliza desde dentro TransactionScope un método que utiliza su propio ámbito, como es el caso con el método RootMethod en el ejemplo siguiente,

void RootMethod()
{
    using(TransactionScope scope = new TransactionScope())
    {
        /* Perform transactional work here */
        SomeMethod();
        scope.Complete();
    }
}

void SomeMethod()
{
    using(TransactionScope scope = new TransactionScope())
    {
        /* Perform transactional work here */
        scope.Complete();
    }
}

El ámbito de la transacción de nivel superior se conoce como el ámbito de la raíz.

La clase TransactionScope proporciona varios constructores sobrecargados que aceptan una enumeración del tipo TransactionScopeOption, que define el comportamiento transaccional del ámbito.

Un objeto TransactionScope tiene tres opciones:

  • Unir la transacción ambiente o crear una nueva si no existe.

  • Ser un nuevo ámbito de la raíz, eso es, iniciar una nueva transacción y tener esa transacción siendo la nueva transacción ambiente dentro de su propio ámbito.

  • No tomar parte en absoluto en una transacción. Como resultado, no hay ninguna transacción ambiente.

Si se crean instancias del ámbito con Required, y una transacción ambiente está presente, el ámbito combina dicha transacción. Si, por otro lado, no hay ninguna transacción ambiente, a continuación, el ámbito crea una nueva transacción y se vuelve el ámbito de la raíz. Este es el valor predeterminado. Cuando se utiliza Required, el código dentro del ámbito no necesita comportarse de manera diferente si es la raíz o simplemente uniendo la transacción ambiente. Debería funcionar idénticamente en ambos casos.

Si se crean instancias del ámbito con RequiresNew, siempre es el ámbito de la raíz. Inicia una nueva transacción y su transacción se vuelve la nueva transacción ambiente dentro del ámbito.

Si se crea del ámbito instancias con Suppress, nunca toma la parte en una transacción, sin tener en cuenta si una transacción ambiente está presente. Un ámbito con instancias creadas siempre tiene null con este valor como su transacción ambiente.

Las opciones anteriores se resumen en la tabla siguiente.

TransactionScopeOption Transacción ambiente El ámbito toma parte.
Obligatorio No Nueva transacción (será la raíz)
Se requiere nueva No Nueva transacción (será la raíz)
Suprimir No Sin transacción
Obligatorio Transacción ambiente
Se requiere nueva Yes Nueva transacción (será la raíz)
Suprimir Yes Sin transacción

Cuando un objeto TransactionScope une una transacción ambiente existente, al eliminar el objeto de ámbito, no se puede finalizar la transacción, a menos que el ámbito anule la transacción. Si un ámbito de la raíz creara la transacción ambiente, solo cuando el ámbito de la raíz se elimina, se llama Commit en la transacción. Si se crea la transacción manualmente, la transacción finaliza cuando se anula o cuando su creador la confirma.

El ejemplo siguiente muestra un objeto TransactionScope que crea tres objetos de ámbito anidados, cada uno con instancias con un valor TransactionScopeOption diferente.

using(TransactionScope scope1 = new TransactionScope())
//Default is Required
{
    using(TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Required))
    {
        //...
    }

    using(TransactionScope scope3 = new TransactionScope(TransactionScopeOption.RequiresNew))
    {
        //...  
    }
  
    using(TransactionScope scope4 = new TransactionScope(TransactionScopeOption.Suppress))
    {
        //...  
    }
}

El ejemplo muestra un bloque de código sin cualquier transacción ambiente que crea un nuevo ámbito (scope1) con Required. El ámbito scope1 es un ámbito raíz porque cuando crea una nueva transacción (la transacción A) y hace que la transacción A sea transacción ambiente. Scope1 a continuación, crea tres objetos más, cada uno con un valor diferente TransactionScopeOption. Por ejemplo, scope2 se crea con Requiredy hay subsecuentemente una transacción ambiente, que se une la primera transacción creada por scope1. Observe que scope3 es el ámbito de la raíz de una nueva transacción, y scope4 no tiene ninguna transacción ambiente.

Aunque el valor predeterminado y más comúnmente utilizado de TransactionScopeOption es Required, cada uno de los otros valores tiene su propósito único.

Código no transaccional dentro de un ámbito de transacción

Suppress resulta útil cuando desea conservar las operaciones realizadas por la sección de código y no desea anular la transacción ambiente si se produce un error en las operaciones. Por ejemplo, al desear realizar un registro u operaciones de la auditoría, o al desear publicar los eventos a los suscriptores sin tener en cuenta si su transacción ambiente se confirma o sufre interrupciones. Este valor le permite tener una sección de código no transaccional dentro de un ámbito de la transacción, como se muestra en el ejemplo siguiente.

using(TransactionScope scope1 = new TransactionScope())
{
    try
    {
        //Start of non-transactional section
        using(TransactionScope scope2 = new
            TransactionScope(TransactionScopeOption.Suppress))  
        {  
            //Do non-transactional work here  
        }  
        //Restores ambient transaction here
   }
   catch {}  
   //Rest of scope1
}

Votar dentro de un ámbito anidado

Aunque un ámbito anidado puede unir la transacción ambiente del ámbito de la raíz, llamar a Complete en el ámbito anidado no tiene ningún efecto en el ámbito de la raíz. La transacción solo se confirmará si todos los ámbitos, desde el ámbito raíz hasta el último ámbito anidado, votan a favor de confirmar la transacción. Si no se llama a Complete en un ámbito anidado, el ámbito de la raíz resultará afectado, ya que la transacción ambiente se anulará inmediatamente.

Establecer el tiempo de espera de TransactionScope

Algunos de los constructores sobrecargados de TransactionScope aceptan un valor de tipo TimeSpan, que se utiliza para controlar el tiempo de espera de la transacción. Un conjunto de tiempos de espera establecidos a cero significa un tiempo de espera infinito. El tiempo de espera infinito es principalmente útil para depurar, al desear aislar un problema en su lógica comercial caminando a través de su código, y no desear la transacción que depura el tiempo de espera mientras intenta buscar el problema. Sea sumamente cuidadoso utilizando el valor de tiempo de espera infinito en todos los otros casos, porque invalida las medidas de seguridad contra los interbloqueos de la transacción.

Se establece normalmente el tiempo de espera TransactionScope en los valores distintos del valor predeterminado en dos casos. El primero durante el desarrollo, al desear probar la manera en que su aplicación administra las transacciones anuladas. Estableciendo el tiempo de espera en un valor pequeño (como un milisegundo), hace que su transacción produzca un error y observa así su código de control de errores. El segundo caso en el que establece el valor para ser menor que el tiempo de espera predeterminado es al creer que el ámbito está implicado en la contención del recurso, produciendo los interbloqueos. En ese caso, desea anular lo antes posible la transacción y no esperar para que el tiempo de espera predeterminado expire.

Cuando un ámbito combina una transacción ambiente pero especifica un tiempo de espera menor que el de la transacción ambiente, el nuevo tiempo de espera más corto se exige en el objeto TransactionScope y el ámbito debe finalizar dentro de la hora anidada especificada o se anula la transacción automáticamente. Si el tiempo de espera del ámbito anidado es más que eso de la transacción ambiente, no tiene ningún efecto.

Establecer el nivel de aislamiento de TransactionScope

Algunos de los constructores sobrecargados de TransactionScope aceptan una estructura de tipo TransactionOptions para especificar un nivel de aislamiento, además de un valor de tiempo de espera. De forma predeterminada, la transacción se ejecuta con nivel de aislamiento establecido en Serializable. La selección de un nivel de aislamiento distinto de Serializable se utiliza normalmente para los sistemas de lectura-intensivos. Esto requiere un sólido entendimiento de teoría del procesamiento de transacciones y las semántica de la propia transacción, los problemas de simultaneidad implicados y las consecuencias para la coherencia del sistema.

Además, no todos los administradores de recursos admiten todos los niveles de aislamiento y pueden elegir tomar parte en la transacción en un nivel más alto que el que se configuró.

Cada nivel de aislamiento además de Serializable es susceptible a la inconsistencia que es el resultado de otras transacciones que tienen acceso a la misma información. La diferencia entre los niveles de aislamiento diferentes es la manera en que se utilizan los bloqueos de la lectura y escritura. Se puede contener un bloqueo solo cuando la transacción tiene acceso a los datos en el administrador de recursos, o se puede contener hasta que la transacción se confirme o anule. Lo primero es mejor para el rendimiento, lo último para la coherencia. Los dos tipos de bloqueos y los dos tipos de operaciones (lectura/escritura) proporcionan cuatro niveles de aislamiento básicos. Consulte IsolationLevel para obtener más información.

Al utilizar los objetos TransactionScope anidados, todos los ámbitos anidados se deben configurar para utilizar exactamente el mismo nivel de aislamiento si desean unir la transacción ambiente. Si un objeto TransactionScope anidado intenta unir la transacción ambiente todavía especifica un nivel de aislamiento diferente, se inicia ArgumentException.

Interoperabilidad con COM+

Al crear que una nueva instancia TransactionScope, se puede utilizar la enumeración EnterpriseServicesInteropOption en uno de los constructores para especificar cómo interactuar con COM+. Para obtener más información sobre esto, vea Interoperabilidad con Enterprise Services y transacciones de COM+.

Consulte también