Está en la página 1de 4

TransactionScope, ¡ha venido para quedarse!

Todas las aplicaciones que desarrollo en mi empresa están orientadas a datos. De este modo, una de mis grandes preocupaciones es
como gestionar de forma eficiente las transacciones en el acceso a datos. Hasta ahora, siempre había utilizado el
objeto SqlTransaction para llevar a cabo esta tarea, pero lo cierto es que no estoy del todo satisfecho con su uso. Para intentar
argumentar porque no quiero SqlTransaction, primero veamos un ejemplo de cómo utilizar este objeto.

Sub Main()
Using cnn As New SqlConnection("Data Source=SERGIO-VAIO;Initial Catalog=Pedidos;Integrated Security=True")
cnn.Open()
Dim tran As SqlTransaction = Nothing
Try
tran = cnn.BeginTransaction
InsertarPrimerCliente(cnn, tran)
InsertarSegundoCliente(cnn, tran)
tran.Commit()
Catch ex As Exception
If Not tran Is Nothing Then
tran.Rollback()
End If
Console.WriteLine(ex.Message)
Finally
Console.ReadLine()
End Try
End Using
End Sub

Sub InsertarPrimerCliente(ByVal cnn As SqlConnection, ByVal tran As SqlTransaction)


Dim cmdText As String = "INSERT INTO Clientes VALUES (1, 'Cliente 1')"
Using cmd As New SqlCommand(cmdText, cnn, tran)
cmd.ExecuteNonQuery()
End Using
End Sub

Sub InsertarSegundoCliente(ByVal cnn As SqlConnection, ByVal tran As SqlTransaction)


Dim cmdText As String = "INSERT INTO Clientes VALUES (2, 'Cliente 2')"
Using cmd As New SqlCommand(cmdText, cnn, tran)
cmd.ExecuteNonQuery()
End Using
End Sub

Realmente este código no me gusta porque los métodos “InsertarPrimerCliente” e “InsertarSegundoCliente” tienen que recibir como
parámetros tanto el objeto SqlConnection como el objeto SqlTransaction. Esto es así porque si quiero que los comandos ejecutados
desde estos métodos y contra la base de datos participen todos en la misma transacción, tiene que compartir tanto la misma conexión
como la misma transacción.

Creo que es fácil entender que la dependencia de este modelo transaccional en ADO.NET implica mantener siempre viva la referencia
tanto al objeto SqlConnection como al objeto SqlTransaction. Ahora imagina, no 2 métodos, sino X métodos en distintas clases,
distintos ensamblados, etc. No sé, en mi opinión, en el momento que tu código transaccional ya no es sólo una sencilla llamada a un
único método con un principio y fin concreto, este modelo en vez de ayudarme, me la hace la vida más difícil.

De hecho, si por ejemplo sólo pasamos a los métodos el objeto SqlConnection, pero no el objeto SqlTransaction, obtendremos este
bonito error que para mí ya es todo un clásico:“ExecuteNonQuery requiere que el comando tenga una transacción cuando la conexión
asignada al mismo está en una transacción local pendiente. No se ha inicializado la propiedad Transaction del comando.”.Por otro lado,
si directamente no pasamos ni la conexión ni la transacción, nuestros comandos en métodos como los expuestos serán simple y
llanamente una nueva conexión sin intervenir en el contexto de ninguna transacción.

Para solucionar esto, hemos ideado en nuestro empresa un contexto ficticio de conexión y transacción que mejor o peor funciona y
evita tener que pasar constantemente estos parámetros (SqlConnection y SqlTransaction), pero aún así es una carga extra de diseño
en cualquier programa que hay que soportar y por supuesto no está exenta de errores ni de posibles refactorizaciones.

En este punto creí que todo estaba perdido hasta que descubrí el objeto TransactionScope.

Ahora, el anterior código pasaría a ser este otro:

Sub Main()
Using tran As New Transactions.TransactionScope
Try
InsertarPrimerCliente()
InsertarSegundoCliente()
tran.Complete()
Catch ex As Exception
Console.WriteLine(ex.Message)
End Try
End Using
Console.ReadLine()
End Sub

Sub InsertarPrimerCliente()
Using cnn As New SqlConnection("Data Source=SERGIO-VAIO;Initial Catalog=Pedidos;Integrated Security=True")
cnn.Open()
Dim cmdText As String = "INSERT INTO Clientes VALUES (1, 'Cliente 1')"
Using cmd As New SqlCommand(cmdText, cnn)
cmd.ExecuteNonQuery()
End Using
End Using
End Sub

Sub InsertarSegundoCliente()
Using cnn As New SqlConnection("Data Source=SERGIO-VAIO;Initial Catalog=Pedidos;Integrated Security=True")
cnn.Open()
Dim cmdText As String = "INSERT INTO Clientes VALUES (2, 'Cliente 2')"
Using cmd As New SqlCommand(cmdText, cnn)
cmd.ExecuteNonQuery()
End Using
End Using
End Sub

Si nos fijamos, ahora simplemente “iniciamos un contexto transaccional” y a partir de aquí cualquier comando ejecutado contra esa
misma conexión o contra cualquier otra nueva conexión será ejecutado de forma automática en el contexto de la misma “transacción
de ambiente” (más adelante veremos que se puede configurar esto, pero en principio este es el comportamiento predeterminado).
Por otro lado, sólo hay que controlar el éxito de la operación para llamar al método Complete, mientras que para anular, revertir,
deshacer o rechazar la transacción (hoy estoy que lo tiro con los sinónimos), simplemente se hará automáticamente si al salir de la
instrucción Using donde se declaró el objeto TransactionScope no se llamó de forma explícita al método Complete.

Para la utilización de TransactionScope es necesario agregar una referencia a System.Transactions.Esta referencia sólo es necesario en
los proyectos que hacen referencia explícita al objeto TransactionScope. Si por ejemplo la función “InsertarPrimerCliente” estuviera en
una biblioteca de clases, participaría de la transacción pero no sería necesario agregar la referencia a System.Transactions porque no
la utiliza de forma explícita.

Realmente, TransactionScope utiliza MSDTC (Microsoft Data Transaction Coordinator, coordinador de transaciones distribuidas) para
llevar a cabo su tarea. De hecho y donde radica principalmente la potencia de TransactionScope (más allá de su facilidad de uso frente
a SqlTransaction) es que es capaz de incluir en misma transacción, operaciones contra distintos orígenes de base de datos o recursos
transaccionales (por ejemplo las colas también son transaccionables). Eso significa que en nuestro ejemplo anterior,
“InsertarPrimerCliente” podría haber insertado en una base de datos A, mientras que “InsertarSegundoCliente” podría haberlo hecho
en una base de datos B, todo ello en una misma transacción que se completaría o rechazaría como una operación ACID.

Algo importante es que si estamos utilizando una sola base de datos en nuestra transacción, sólo tenemos una conexión abierta a la
vez y además no tenemos transacciones anidadas (todos estos requisitos se cumplen en el ejemplo anterior), TransactionScope utilizará
una “transacción ligera” gestionada por LTM (Lightweight Transaction Coordinator), pero en el momento en que utilicemos 2 o más
base de datos, las cadenas de conexión sean distintas (aunque finalmente apunten a la misma base de datos y servidor) o anidemos
transacciones, la transacción ligera será promocionada a MSDTC. Además, todo lo anterior se cumple en SQL Server 2008, pero en
versiones anteriores (2005) incluso abrir 2 conexiones simultaneas a la misma base de datos y con la misma cadena de conexión,
también promocionará la transacción, ver más en Avoid unwanted Escalation to Distributed Transactions

Cuando decía que hay que tener cuidado con las cadenas de conexión me refería a que si accedemos a nuestra única base de datos
(en la que suponemos será gestionada por LTM) pero con distintas cadenas de conexión, entonces TransactionScope pensará que son
distintas base de datos y pasará a ser una transacción gestionada por MSDTC.

Para ver esto lo mejor será un ejemplo. Simplemente cambiaremos la cadena de conexión de “InsertarPrimerCliente” a otra cadena
de conexión válida para la misma base de datos y observaremos como ahora pasamos de LTM a MSDTC.

InsertarPrimerCliente Data Source=SERGIO-VAIO;Initial Catalog=Pedidos;User


Id=sa;Password=XX
InsertarSegundoCliente Data Source=SERGIO-VAIO;Initial Catalog=Pedidos;Integrated
Security=True
Sobra decir que está claro que la transacción ha sido promovida a MSDTC, ¡si es un perro nos muerde!, pero además vemos como
hemos recibido un error que nos informa de que MSDTC no está disponible en nuestro equipo. Aunque podría ser más complicado,
inicialmente para configurar MSDTC tienes que abrir “Servicios de componentes” y en las propiedades de DTC local, activar el
checkbox “Permitir clientes remotos”.

Una vez ya tenemos configurado MSDTC, las transacciones promovidas podemos visualizarlas también en “Servicios de
componentes”. Vamos a poner un Console.ReadLine() antes de llamar a Complete() para poder ver esto.

Lo que hemos hecho hasta ahora ha sido romper el hielo con el tema de TransactionScope, pero ahora debemos ver unas cuantas
opciones más en lo relativo a su uso que cerrarán el círculo y nos permitirán tener mayor control sobre cómo y cuándo suceden las
cosas.

Todos los ejemplos vistos han utilizando la instrucción Using, diciendo que si al salir de ella aún no se ha llamado al
método Complete() los cambios serán rechazados. Esto es totalmente cierto, pero también es totalmente cierto que no es necesario
utilizar Using (aunque si recomendable) puesto que Using lo único que asegura es que se llama siempre al método Dispose(). Pues bien,
es en ese método donde se haya toda la lógica de rechazo de la transacción, así que si tienes la necesidad de
usar TransactionScope sin Using, simplemente llama a Dispose() y obtendrás la misma funcionalidad que con Using.
La clase TransactionOptions permite especificar el comportamiento de la transacción. Por ejemplo, la propiedad Timeout especifica el
tiempo de espera máximo de la transacción antes de que sea abortada automáticamente, mientras que la
propiedad IsolationLevel especifica el nivel aislamiento de la transacción.

Y ahora viene en mi opinión la parte más importante y que es precio entender para manejar correctamente TransactionScope y es la
relación entre la transacción de ambiente con siguientes nuevas transacciones declaradas en el código. Veamos esto con un ejemplo:

Using cnn As New SqlConnection("Data Source=SERGIO-VAIO;Initial Catalog=Pedidos;Integrated Security=True")


cnn.Open()
Using tran1 As New TransactionScope
InsertarPrimerCliente()
Using tran2 As New TransactionScope
InsertarSegundoCliente()
tran2.Complete()
End Using
tran1.Complete()
End Using
End Using

En este ejemplo podemos ver como hay 2 TransactionScope, una de ellas anidada dentro de otra (haz esto extensible a llamadas entre
métodos, etc.). ¿Cómo se comporta entonces la segunda transacción?

Primero decir que la siguientes instrucciones son equivalentes:

Using tran As New TransactionScope()

Using tran As New TransactionScope(TransactionScopeOption.Required)

Cuando declaramos un nuevo objeto TransactionScope podemos definir su comportamiento respecto a una posible transacción de
ambiente existente. El valor predeterminado es Required, pero veamos que valores puede tomar esta opción:

Required El ámbito requiere una transacción.


Utiliza una transacción de ambiente si ya existe una.
De lo contrario, crea una nueva transacción antes de introducir el ámbito. Éste es el valor predeterminado.
RequiresNew Siempre se crea una nueva transacción para el ámbito.
Supress Se suprime el contexto de transacción de ambiente al crear el ámbito.
Todas las operaciones dentro del ámbito se realizan sin un contexto de transacción de ambiente.

Esto traducido al español es:

Required Si hay una transacción de ambiente la utilizaré sino crearé una nueva.
RequiresNew Me da igual si hay o no hay una transacción de ambiente, yo a mi bola en una nueva transacción.
Supress Paso de transacciones, soy un machote y no quiero ejecutarme en el contexto de ninguna transacción.

Cabe mencionar que todos estos ejemplos lo estoy realizando con SQL Server 2008 y al menos esta versión soporta perfectamente
transacciones anidadas, por lo que por ejemplo si la transacción externa es Required y la interna RequiresNew se crearán 2
transacciones (esto es 2 BEGIN TRANSACTION en SQL Server).

Por último, un par de apuntes sobre el método Complete y sobre como averiguar si nuestro código está en el contexto de una
transacción y en qué transacción.

 Si se llama a Complete para la misma instancia de TransactionScope 2 o más veces, excepto la primera, las siguientes
fallarán con System.InvalidOperationException.
 Si se llama a Complete desde una transacción anidada especificada como Required (ámbito predeterminado), realmente no
se está confirmando la transacción y no tiene por ahora ningún efecto en la misma, sólo la llamada a Complete de la
transacción de ambiente (la más externa) será la que confirma o rechace los cambios.
 La propiedad Transaction.Current nos devuelve (también sirve para establecer) la transacción de ambiente. Además
también nos devuelve información sobre la transacción ¿Recuerdas ese maravillo GUID que salía en “Servicios de
componentes” como identificador de la transacción activa?.

Bueno, la verdad es que este post podría continuar y continuar, pero por hoy ya es suficiente.

Lo dicho, TransactionScope ha venido para quedarse.

También podría gustarte