Integración de System.Transactions con SQL Server (ADO.NET)

Actualización: November 2007

.NET Framework versión 2.0 incorporó un nuevo marco de trabajo de transacciones al que se puede obtener acceso a través del espacio de nombres System.Transactions. Este marco de trabajo expone las transacciones de tal forma que se integra completamente en .NET Framework, incluyendo ADO.NET.

Además de las mejoras de programación, System.Transactions y ADO.NET pueden funcionar juntos para coordinar las optimizaciones al trabajar con transacciones. Una transacción promocionada es una transacción ligera (local) que, en caso necesario, se puede promover automáticamente a una transacción completamente distribuida.

A partir de ADO.NET 2.0, System.Data.SqlClient admite las transacciones promocionadas al trabajar con SQL Server 2005. Las transacciones promocionadas no invocan la sobrecarga adicional de las transacciones distribuidas a menos que sea necesario. son automáticas, es decir, no necesitan que intervenga el programador.

Las transacciones promocionadas solo están disponibles cuando se utiliza el proveedor de datos de .NET Framework para SQL Server (SqlClient) con SQL Server 2005.

Creación de transacciones promocionadas

El proveedor de .NET Framework para SQL Server ofrece compatibilidad con transacciones promocionadas, que se administran a través de las clases del espacio de nombres System.Transactions de .NET Framework. Las transacciones promocionadas optimizan las transacciones distribuidas ya que aplazan la creación de las mismas hasta que es necesario. Si sólo se necesita un administrador de recursos, no tiene lugar ninguna transacción distribuida.

Nota:

En un caso que no es de plena confianza, se requiere DistributedTransactionPermission cuando la transacción aumenta al nivel de transacción distribuida. Para obtener más información, vea Extensión de administración de transacción.

Situaciones de uso de transacciones promocionadas

Normalmente, las transacciones distribuidas consumen muchos recursos del sistema, siendo el encargado de administrarlas Microsoft DTC (Coordinador de transacciones distribuidas), que integra todos los administradores de recursos a los que se tiene acceso en la transacción. Una transacción promocionada es una forma especial de transacción de System.Transactions que delega con efectividad el trabajo en una transacción de SQL Server 2005 simple. System.Transactions, System.Data.SqlClient y SQL Server 2005 coordinan el trabajo que supone administrar la transacción y promoverla a una transacción completamente distribuida cuando es necesario.

La ventaja de utilizar transacciones promocionadas es que cuando se abre una conexión utilizando una transacción TransactionScope activa, y no hay ninguna otra conexión abierta, la transacción se confirma como una transacción ligera, en lugar de incurrir en la sobrecarga adicional de una transacción completamente distribuida.

Palabras clave de cadena de conexión

La propiedad ConnectionString admite una palabra clave, Enlist, que indica si System.Data.SqlClient detectará contextos transaccionales e inscribirá automáticamente la conexión en una transacción distribuida. Si Enlist=true, la conexión se inscribe automáticamente en el contexto de transacción actual del subproceso de apertura. Si Enlist=false, la conexión SqlClient no interactúa con una transacción distribuida. El valor predeterminado de Enlist es true. Si no se especifica Enlist en la cadena de conexión, la conexión se da de alta automáticamente en una transacción distribuida si se detecta una al abrirse la conexión.

Las palabras clave Transaction Binding en una cadena de conexión SqlConnection controlan la asociación de la conexión con una transacción System.Transactions dada de alta. También está disponible mediante la propiedad TransactionBinding de SqlConnectionStringBuilder.

La siguiente tabla describe los posibles valores.

Palabra clave

Descripción

Desenlace implícito

Es la opción predeterminada. La conexión se separa de la transacción cuando termina, y vuelve a cambiar al modo de confirmación automática.

Desenlace explícito

La conexión sigue adjuntada a la transacción hasta que ésta se cierra. La conexión producirá errores si no está activa o no coincide con Current.

Para obtener más información, vea Implementar una transacción implícita mediante el ámbito de la transacción.

Uso de TransactionScope

La clase TransactionScope crea un bloque de código transaccional al inscribir implícitamente las conexiones en una transacción distribuida. Debe llamar al método Complete al final del bloque TransactionScope antes de abandonarlo. Al salir del bloque se invoca el método Dispose. Si se ha producido una excepción que ocasiona que el código salga del ámbito, la transacción se considera anulada.

Se recomienda el uso de un bloque using para asegurarse de que se llama a Dispose en el objeto TransactionScope cuando se sale de dicho bloque. Si no se confirman ni revierten las transacciones pendientes, el rendimiento puede verse seriamente afectado ya que el tiempo de espera predeterminado de TransactionScope es un minuto. Si no utiliza una instrucción using, todo el trabajo deberá realizarlo en un bloque Try y llamar explícitamente al método Dispose en el bloque Finally.

Si se produce una excepción en TransactionScope, la transacción se marca como incoherente y se abandona. Se revertirá cuando se elimine el TransactionScope. Si no se produce ninguna excepción, las transacciones participantes se confirman.

Nota:

La clase TransactionScope crea una transacción con un IsolationLevel predeterminado de Serializable. Dependiendo de la aplicación, podría estudiar la posibilidad de reducir el nivel de aislamiento para evitar una elevada contención en la aplicación.

Nota:

Se recomienda que sólo realice actualizaciones, inserciones y eliminaciones en transacciones distribuidas, ya que consumen una cantidad considerable de recursos de base de datos. Las instrucciones SELECT pueden bloquear los recursos de base de datos de forma innecesaria y, en algunas situaciones, es posible que tengan que utilizarse transacciones para las selecciones. Todo el trabajo que no sea de base de datos debe realizarse fuera del ámbito de la transacción, a menos que estén implicados otros administradores de recursos de transacción. Aunque una excepción en el ámbito de la transacción impide que se confirme la misma, la clase TransactionScope no deja revertir los cambios que haya realizado el código fuera del ámbito de la propia transacción. Si es necesario realizar alguna acción cuando se revierta la transacción, deberá escribir su propia implementación de la interfaz IEnlistmentNotification y darla de alta explícitamente en la transacción.

Ejemplo

Trabajar con System.Transactions requiere disponer de una referencia a System.Transactions.dll.

La siguiente función muestra cómo crear una transacción promocionada en dos instancias de SQL Server diferentes, representadas por dos objetos SqlConnection diferentes, que se incluyen en un bloque TransactionScope. El código crea el bloque TransactionScope con una instrucción using y abre la primera conexión, que automáticamente se da de alta en TransactionScope. La transacción se inscribe inicialmente como una transacción ligera y no como una completamente distribuida. La segunda conexión se inscribe en TransactionScope únicamente si el comando de la primera conexión no produce una excepción. Cuando se abre la segunda conexión, la transacción se promociona automáticamente a una transacción completamente distribuida. Se invoca el método Complete, que confirma la transacción únicamente si no se han producido excepciones. Si en algún punto del bloque TransactionScope se ha producido una excepción, no se llamará a Complete y, cuando se elimine TransactionScope al final de su bloque using, se revertirá la transacción distribuida.

' This function takes arguments for the 2 connection strings and commands in order
' to create a transaction involving two SQL Servers. It returns a value > 0 if the
' transaction committed, 0 if the transaction 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 RDBMS such as Oracle 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

    ' Create the TransactionScope in which to execute the commands, guaranteeing
    ' that both commands will commit or roll back as a single unit of work.
    Using scope As New TransactionScope()
        Using connection1 As New SqlConnection(connectString1)
            Try
                ' 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 by opening connection2 
                ' only when there is a chance that the transaction can commit.   
                Using connection2 As New SqlConnection(connectString2)
                    Try
                        ' The transaction is promoted 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)

                    Catch ex As Exception
                        ' Display information that command2 failed.
                        writer.WriteLine("returnValue for command2: {0}", returnValue)
                        writer.WriteLine("Exception Message2: {0}", ex.Message)
                    End Try
                End Using

            Catch ex As Exception
                ' Display information that command1 failed.
                writer.WriteLine("returnValue for command1: {0}", returnValue)
                writer.WriteLine("Exception Message1: {0}", ex.Message)
            End Try
        End Using

        ' If an exception has been thrown, Complete will 
        ' not be called and the transaction is rolled back.
        scope.Complete()
    End Using

    ' The returnValue is greater than 0 if the transaction committed.
    If returnValue > 0 Then
        writer.WriteLine("Transaction was committed.")
    Else
        ' You could write additional business logic here, notify the caller by
        ' throwing a TransactionAbortedException, or log the failure.
       writer.WriteLine("Transaction rolled back.")
     End If

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

    Return returnValue
End Function
// This function takes arguments for the 2 connection strings and commands in order
// to create a transaction involving two SQL Servers. It returns a value > 0 if the
// transaction committed, 0 if the transaction 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 RDBMS such as Oracle 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();

    // Create the TransactionScope in which to execute the commands, guaranteeing
    // that both commands will commit or roll back as a single unit of work.
    using (TransactionScope scope = new TransactionScope())
    {
        using (SqlConnection connection1 = new SqlConnection(connectString1))
        {
            try
            {
                // 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 by opening connection2 
                // only when there is a chance that the transaction can commit.   
                using (SqlConnection connection2 = new SqlConnection(connectString2))
                    try
                    {
                        // The transaction is promoted 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);
                    }
                    catch (Exception ex)
                    {
                        // Display information that command2 failed.
                        writer.WriteLine("returnValue for command2: {0}", returnValue);
                        writer.WriteLine("Exception Message2: {0}", ex.Message);
                    }
            }
            catch (Exception ex)
            {
                // Display information that command1 failed.
                writer.WriteLine("returnValue for command1: {0}", returnValue);
                writer.WriteLine("Exception Message1: {0}", ex.Message);
            }
        }

        // If an exception has been thrown, Complete will not 
        // be called and the transaction is rolled back.
        scope.Complete();
    }

    // The returnValue is greater than 0 if the transaction committed.
    if (returnValue > 0)
    {
        writer.WriteLine("Transaction was committed.");
    }
    else
    {
        // You could write additional business logic here, notify the caller by
        // throwing a TransactionAbortedException, or log the failure.
        writer.WriteLine("Transaction rolled back.");
    }

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

    return returnValue;
}

Vea también

Conceptos

Características proporcionadas por System.Transactions

Otros recursos

Transacciones y simultaneidad (ADO.NET)