Transacciones en Workflow Foundation

Muy frecuentemente en nuestras aplicaciones necesitamos incorporar algún tipo de mecanismo que nos asegure que los datos que estemos usando hayan sido grabados de manera correcta y consistente. Bienvenidos a las Transacciones, las cuáles -en el contexto de WF- las podemos utilizar como medio para asegurarnos que efectivamente la información haya sido actualizada tal y como lo esperamos y en todo caso de alguna falla la información no quede incompleta o inconsistente.

Pero ¿qué es una Transacción?

Una transacción la podemos definir como una unidad de trabajo en donde se ejecutan exitosamente todas las tareas que se incluyen en la transacción, o no se ejecuta ninguna.

En WF podemos incorporar en nuestros flujos de trabajo dos tipos de transacciones: Transacciones de tipo 2PC (Two Phase Commit) y Transacciones Compensables. En este capítulo explicaremos y mostraremos el uso de cada una de ellas para nuestras aplicaciones.

Transacciones Two Pase Commit

Este tipo de transacción es aquella que está coordinada a través de un administrador el cual su objetivo es determinar si las acciones y datos relacionados con la transacción se deben aplicar en un momento específico. Ese momento lo determina el administrador tomando en cuenta si todos los recursos relacionados en la transacción "votan" para que la transacción continúe o se deshaga en su totalidad.

Las transacciones de esta categoría deben tener las propiedades ACID, las cuales son por sus siglas en inglés: Atomicity, Consistency, Isolation y Durability.

La prueba del "Ácido" para las transacciones

Como describimos en el párrafo anterior, las transacciones de tipo Two Phase Commit deben tener ciertas propiedades para que su comportamiento sea el esperado y funcionen correctamente. Estas propiedades las explicaremos a continuación:

Atomicidad (Atomicity). Esta propiedad indica que todas las operaciones relacionadas con la transacción se han ejecutado en su totalidad o no se han ejecutado, es decir, esta es la propiedad que nos asegura que un proceso de actualización de datos no puede quedar a medias provocando que nuestros datos queden en un estado incorrecto. Un ejemplo en donde podemos apreciar esta propiedad es cuando realizamos un retiro de dinero en un cajero automático: solicitamos cierta cantidad y esa cantidad nos es dada en efectivo. Ahora bien, imaginemos el caso en el que la transacción no fuera atómica y que ocurriese alguna falla en la transacción, digamos que el cajero automático no tenga dinero suficiente para nuestro retiro. Terminaríamos con un saldo menor e incorrecto en nuestra cuenta de ahorros pero con nada de dinero en efectivo en las manos. O tal vez el pago de un servicio en línea por medio de una tarjeta de crédito. En fin, ejemplos podríamos decir muchos. Gracias a la propiedad de atomicidad en las transacciones nos aseguramos que en algún caso de error en los ejemplos anteriores los datos relacionados no se vean afectados.

La Atomicidad nos asegura que todas las operaciones en una transacción se realizan todas o no se realiza ninguna.

Consistencia (Consistency). Esta propiedad indica que los datos envueltos en la transacción se deben mantener consistentes. Si tomamos como ejemplo una base de datos relacional esto indica que la transacción no debe romper ninguna restricción que haya sido declarada. Tal es el caso de una transacción que requiere insertar un registro en dos tablas con una relación padre-hijo por ejemplo las tablas Cliente y Factura. Si la transacción inserta exitosamente un registro en la tabla Factura pero falla al insertar el registro correspondiente en la tabla Cliente, la integridad de la base de datos se vería afectada y la transacción –gracias a la propiedad de Consistencia- es echada para atrás.

La Consistencia nos asegura que la integridad de los datos se mantendrá una vez realizada la transacción y que no se romperá ninguna restricción.

Aislamiento (Isolation). Esta propiedad nos permite que las operaciones en una transacción no afecten a otras. Existen diferentes tipos de nivel de aislamiento para las transacciones que podemos utilizar según el contexto y las necesidades propias de la aplicación y/o flujo de trabajo que estemos desarrollando.

La propiedad de Aislamiento nos asegura que las operaciones de una transacción no pueden afectar a otras.

Durabilidad (Durability). Esta propiedad nos asegura que una vez realizada la transacción los datos modificados o agregados quedarán guardados de una manera no-volátil y estarán disponibles aún si ocurriese una falla en el sistema. Esto es cierto aún si la falla ocurriera 1 milisegundo después de haber sido realizada la transacción.

La Durabilidad nos asegura que una vez realizada la transacción los datos quedarán guardados aunque falle el sistema.

Ejemplo

Veamos ahora cómo implementar transacciones de tipo Two Phase Commit en nuestros flujos de trabajo utilizando Workflow Foundation. Cabe mencionar que el siguiente ejemplo supone la existencia de la base de datos Northwind implementada en SQL Server Express de manera local y supone también que se ha configurado el servicio SqlWorkflowPersistenceService usando una base de datos también local llamada WorkflowStore usando los scripts que se incluyen con el .NET Framework 3.0 presentes en [Carpeta de Windows]\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\EN. Para mayor información acerca de SqlWorkflowPersistenceService consulta el capítulo "Persistencia" en esta obra.

Con Visual Studio .NET abierto, hagamos un nuevo proyecto de tipo Sequential Workflow Console Application. Con el diseñador de WF en la pantalla arrastremos y coloquemos una actividad de tipo TransactionScope tal y como lo muestra la siguiente figura:

La actividad TransactionScope es una actividad compuesta, es decir, que puede contener una o más actividades dentro de ella. Todas las actividades que definamos dentro de la actividad TransactionScope participarán en la misma transacción. Una de las propiedades más importantes de esta actividad es TransactionOptions.IsolationLevel la cual indica el nivel de aislamiento que esta transacción tendrá al ejecutar todas sus operaciones siendo su valor predeterminado Serializable (el nivel de aislamiento más restrictivo). A continuación en la tabla 1 se enlistan los diferentes niveles de aislamiento disponibles:

Nombre

Descripción

Serializable

Los datos no aplicados pueden ser leídos pero no modificados, y no se pueden insertar datos durante la transacción

RepeatableRead

Los datos no aplicados pueden ser leídos pero no modificados. Se pueden insertar datos durante la transacción

ReadCommitted

Los datos no aplicados no pueden ser leídos durante la transacción pero sí pueden ser modificados

ReadUncommitted

Los datos no aplicados pueden ser leídos y modificados

Snapshot

Los datos no aplicados pueden ser leídos. Antes de modificar los datos verifica si otra transacción ha modificado los datos después de que se leyeron los datos, si es así arroja una excepción

Chaos

Los cambios pendientes de transacciones que tengan un nivel de aislamiento superior no pueden ser reemplazados

Unspecified

Otro. Sin embargo este nivel no puede ser asignado de manera manual

Tabla 1. Niveles de aislamiento para las transacciones

No podemos colocar una actividad TransactionScope dentro de otra

Para continuar con el ejemplo usaremos una actividad de tipo Code y la colocaremos dentro de la actividad transactionScopeActivity1. Es en esta actividad en donde declararemos el código que deseamos que participe en la transacción así que asignemos un método llamado "Transaccion" en la propiedad ExecuteCode de la actividad codeActivity1. En este método definamos el siguiente código:

private void Transaccion(object sender, EventArgs e)
{
 
 using (SqlConnection conn = new SqlConnection())
 {
 
 conn.ConnectionString = @"Data Source=.\sqlexpress;Initial Catalog=Northwind;Integrated Security=Yes";
 
 SqlCommand cmd = new SqlCommand("INSERT INTO Region VALUES (@RegionID, @Description)", conn);
 
 cmd.Parameters.AddWithValue("@RegionID", this.RegionID);
 
 cmd.Parameters.AddWithValue("@Description", this.RegionID.ToString());
 
 conn.Open();
 
 cmd.ExecuteNonQuery();
 
 }
 
} 

Asimismo, en nuestra clase del flujo de trabajo definamos una propiedad pública de tipo int llamada RegionID. Esta propiedad nos servirá como mecanismo para enviar parámetros a nuestro flujo de trabajo específicamente al comando de SQL que vamos a ejecutar.

Ya que vamos a demostrar el poder y comportamiento de las transacciones es buena idea agregar un Fault Handler a nuestro flujo de trabajo para que capture cualquier tipo de excepción, es por eso que vamos a usar la clase System.Exception. En el fault handler agregaremos una actividad de tipo Code la cual ejecutará el método ErrorHandler para mostrar un mensaje amigable al usuario. La siguiente figura muestra el diseñador de Workflow Foundation en la vista de Fault Handlers.

El código del método ErrorHandler será el siguiente:

private void ErrorHandler(object sender, EventArgs e)
{
 Console.WriteLine("Ha ocurrido un error. La transacción será echada para atrás.");
}

Una vez hecho lo anterior, asignemos un método manejador para el evento Completed de nuestro flujo de trabajo. Esto lo podemos lograr haciendo clic en el botón de eventos en la ventana de propiedades del flujo de trabajo tal y como lo muestra la siguiente figura:

El nombre del método será WorkflowCompleto y tendrá el siguiente código:

private void WorkflowCompleto(object sender, EventArgs e)
{
 Console.WriteLine("El workflow ha completado");
} 

Por último debemos modificar el código que inicia la instancia del flujo de trabajo en la aplicación de consola. Para esto modifiquemos el código incluido en Program.cs para que tenga el siguiente código antes de invocar instance.Start().

Console.Write("Escriba la nueva región a insertar: ");
 
int regionID = int.Parse(Console.ReadLine());
 
Dictionary<string, object> parametros = new Dictionary<string, object>();
 
parametros.Add("RegionID", regionID);
 
 
 
//Agregamos el servicio al runtime 
 
workflowRuntime.AddService(new SqlWorkflowPersistenceService(
 
@"Data Source=.\sqlexpress;Initial Catalog=WorkflowStore;Integrated Security=Yes"));
 
 
 
WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(WorkflowConsoleApplication1.Workflow1), parametros); 

Muy bien! Hemos preparado nuestro flujo de trabajo para que agregue una nueva región a la table Region de la base de datos Northwind dentro de una transacción. Probemos nuestro proyecto ejecutándolo con Ctrl + F5 y pasemos la región número 5 como parámetro. En este caso la operación de inserción es exitosa y nuestro flujo de trabajo nos despliega el siguiente mensaje en la consola:

Finalmente debemos comentar que en caso de alguna falla la actividad TransactionScope se encargará de echar para atrás todos los cambios dejando los datos en un estado consistente.

Transacciones Compensables

Este tipo de transacciones nos permiten definir una serie de operaciones a ejecutar cuando la ejecución de la transacción falla, es decir, la compensan. A diferencia de las transacciones de tipo Two Phase Commit en donde las operaciones se ejecutan o no se ejecutan, en una transacción compensable las operaciones sí son ejecutadas y en todo caso de una falla esas operaciones no son echadas para atrás sino que se ejecutan algunas otras acciones que complementan o "compensan" esa falla. Para dar un ejemplo de esto podemos mencionar el caso cuando pagamos un excedente de algún servicio, en donde la operación compensatoria es emitir una nota de crédito o cupón para hacer uso de ese beneficio posteriormente. Cabe mencionar que este tipo de transacciones son útiles cuando ya no podemos echar para atrás los cambios producidos por la misma transacción o por otra.

Ejemplo

Para demostrar este tipo de transacciones retomemos el proyecto de ejemplo anterior, sin embargo en vez de usar una actividad de tipo TransactionScope utilizaremos la actividad CompensatableTransactionScope la cual nos permite definir una serie de actividades a ejecutar como parte de esa transacción tal y como ocurre con su contraparte explicada en la sección anterior.

Asimismo, para esta demostración provocaremos un error a propósito después de ejecutar la transacción que da de alta regiones en la base de datos Northwind. La actividad que arrojaremos será de tipo System.Exception. La siguiente figura muestra el diseñador de Workflow Foundation con los cambios necesarios:

Debemos notar que el tipo de transacción que ejecuta la actividad codeActivity1 ahora es de tipo CompensateTransactionScope y que agregamos una actividad de tipo Throw después de la transacción para provocar el error mencionado.

Para indicar la serie de actividades compensatorias a ejecutar debemos definirlas en la vista de la actividad compensationHandlerActivity1. Esto lo podemos lograr haciendo clic la opción apropiada del menú desplegable de la transacción compensatableTransactionScopeActivity1 tal y como lo muestra la siguiente figura:

En la actividad compensationHandlerActivity1 añadiremos una actividad de tipo Code. Esta actividad nos servirá para ejemplificar la compensación de una transacción cuando esta falla. A esta actividad le relacionaremos el método Compensa y en este método solamente agregaremos el siguiente sencillo código:

private void Compensa(object sender, EventArgs e)
{
 Console.WriteLine("Compensando...");
} 

Finalmente, en la vista de Fault Handlers agregaremos una actividad de tipo Compensate, la cual indica por medio de su propiedad TargetActivityName la actividad que se quiere compensar, en nuestro caso será la actividad compensatableTransactionScopeActivity1. Esta actividad nos servirá para indicar de manera manual que queremos realizar la compensación correspondiente. La figura 7 muestra el diseñador de Workflow Foundation con la vista de Fault Handlers:

Resumen

En este capítulo vimos los dos tipos de transacciones que podemos incorporar en nuestros flujos de trabajo. Asimismo dimos ejemplos y escenarios en donde pueden ser implementados y la manera sencilla y amigable que disponemos en Workflow Foundation para hacer uso de ellos.

Este artículo fue creado para el curso Desarrollador .NET de la revista Users.  http://desarrollador.redusers.com/