Usando TransactionScope con Enterprise Library 3.0

TransactionScope es una de las nuevas clases que aparecieron en .NET Framework 2.0 y que, entre otras cosas, logró simplificar enormemente la forma como programamos las transacciones en aplicaciones .NET. Por otro lado, Enterprise Library es un conjunto de librerías reutilizables que brindan diversas funcionalidades que el desarrollador utiliza a diario, como por ejemplo las tareas de acceso a datos. En este post me gustaría describir brevemente la forma como Enterprise Library 3.0 aprovecha el poder del TransactionScope para realizar transacciones implícitas de una manera sencilla.

El cómo configurar el Data Access Application Block queda fuera del alcance de este post. Dirígete a este link para obtener más detalles al respecto.

Para empezar, veamos un ejemplo de cómo podríamos realizar una sencilla operación de transferencia bancaria utilizando un par de métodos, Debitar y Acreditar, programados en una clase llamada Banco. Por supuesto, utilizaremos el Data Access Application Block para simplificar las líneas de código:

    public class Banco
    {
        public static int Acreditar(int monto, int idCuenta, DbTransaction transaccion)
        {
            Database db = DatabaseFactory.CreateDatabase();

            DbCommand cmd = db.GetStoredProcCommand("Acreditar");

            db.AddInParameter(cmd, "IdCuenta", DbType.Int32, idCuenta);
            db.AddInParameter(cmd, "Monto", DbType.Int32, monto);

            int regsAfectados = db.ExecuteNonQuery(cmd, transaccion);
            return regsAfectados;
        }

        public static int Debitar(int monto, int idCuenta, DbTransaction transaccion)
        {
            Database db = DatabaseFactory.CreateDatabase();

            DbCommand cmd = db.GetStoredProcCommand("Debitar");

            db.AddInParameter(cmd, "IdCuenta", DbType.Int32, idCuenta);
            db.AddInParameter(cmd, "Monto", DbType.Int32, monto);

            int regsAfectados = db.ExecuteNonQuery(cmd, transaccion);
            return regsAfectados;
        }
    }
 

Teniendo esta clase lista, la transferencia en sí podríamos realizarla de esta forma:

    class Program
    {
        static void Main(string[] args)
        {
            int monto = 500;
            int idCuentaOrigen = 1200;
            int idCuentaDestino = 2235;

            Console.WriteLine("Iniciando transferencia...");

            Database db = DatabaseFactory.CreateDatabase();
            using (DbConnection conexion = db.CreateConnection())
            {
                conexion.Open();
                DbTransaction transaccion = conexion.BeginTransaction();

                try
                {
                    // Hacer el crédito
                    Banco.Acreditar(monto, idCuentaDestino, transaccion);
                    // Hacer el débito
                    Banco.Debitar(monto, idCuentaOrigen, transaccion);

                    // Completar la transacción
                    transaccion.Commit();
                }
                catch (Exception)
                {
                    // Hacer Rollback
                    transaccion.Rollback();
                    Console.WriteLine("No se pudo realizar la transferencia.");
                    throw;
                }
                conexion.Close();
            }

            Console.WriteLine("Transferencia realizada con exito.");
        }
    }
 

De este ejemplo podemos observar claramente cómo cada método de la clase Banco que necesite participar en la transacción requiere pues justamente recibir como parámetro el objeto DBTransaction. Esto ocasiona varios problemas:

  • Se fuerza a los métodos Acreditar y Debitar a tener conciencia de que existe una transacción en la cual deben participar, cuando en realidad, para la clase Banco debería ser transparente el hecho de participar en dicha transacción.
  • Se fuerza a la clase Caller (Program en este caso) a que entre a detalles que no le corresponden, como el hecho de tener que crear y abrir una conexión a la BD. Obviamente, esto debería ser de exclusivo conocimiento de la capa de negocio ó capa de datos (si existiera).
  • Qué pasaría si uno de los dos métodos, Acreditar ó Debitar, estuviera programado en un componente del cual yo no tengo control (un componente de terceros por ejemplo) y dicho método no estuviera recibiendo actualmente ningún objeto DbTransaction? En el peor de los casos, yo no tengo acceso al fuente y, por ende, no puedo modificar al componente. Pero aún si pudiera pedir que lo modifiquen, el cambiar la firma de alguno de los métodos ocasionaría tremendo problema para todas las clases que hagan uso de dicho método.

No diré que estos son todos los problemas que se presentan, pero son realmente importantes cuando nuestras clases empiezan a multiplicarse y el mantenimiento del sistema se vuelve un gran desafío.

Ok, cómo lo arreglamos? Ahí es donde entra TransactionScope. TransactionScope es un objeto que, desde el momento en que es instanciado, rastrea todas las conexiones que se abran a la BD y las enlista en una sola transacción, sin que nosotros debamos hacer ningún tipo de manipulación con dicha transacción.

Veamos, primero ya no necesitaremos recibir ningún objeto DBTransaction en nuestras clases de negocio:

    public class Banco
    {
        public static int Acreditar(int monto, int idCuenta)
        {
            Database db = DatabaseFactory.CreateDatabase();

            DbCommand cmd = db.GetStoredProcCommand("Acreditar");

            db.AddInParameter(cmd, "IdCuenta", DbType.Int32, idCuenta);
            db.AddInParameter(cmd, "Monto", DbType.Int32, monto);

            int regsAfectados = db.ExecuteNonQuery(cmd);
            return regsAfectados;
        }     

        public static int Debitar(int monto, int idCuenta)
        {
            Database db = DatabaseFactory.CreateDatabase();

            DbCommand cmd = db.GetStoredProcCommand("Debitar");

            db.AddInParameter(cmd, "IdCuenta", DbType.Int32, idCuenta);
            db.AddInParameter(cmd, "Monto", DbType.Int32, monto);

            int regsAfectados = db.ExecuteNonQuery(cmd);
            return regsAfectados;
        }
    }
 

Y ahora, entra TransactionScope, al cual lo instanciaremos en la clase Caller:

    class Program
    {
        static void Main(string[] args)
        {
            int monto = 500;
            int idCuentaOrigen = 1200;
            int idCuentaDestino = 2235;

            Console.WriteLine("Iniciando transferencia...");

            using (TransactionScope ambito = new TransactionScope())
            {
                // Hacer el crédito
                Banco.Acreditar(monto, idCuentaDestino);
                // Hacer el débito
                Banco.Debitar(monto, idCuentaOrigen);

                // Completar la transacción
                ambito.Complete();
            }

            Console.WriteLine("Transferencia realizada con exito.");
        }
    }
 

Se nota claramente cómo luego de instanciar a TransactionScope nos olvidamos por completo de la transacción y simplemente nos dedicamos a invocar a cada método que hará sus operaciones de base de datos. Una vez que todos los métodos han hecho su trabajo, simplemente invocamos al método Complete y listo, TransactionScope hará Commit de todas las transacciones pendientes.

Creo que es obvio la forma como este enfoque ayuda enormemente a resolver los problemas antes mencionados. Claro está que si alguno de los componentes participantes no ha sido programado para enlistarse en transacciones, pues el mismo simplemente no participará de la misma, pero el implementarle esa funcionalidad adicional no implicará el añadirle parámetros adicionales a los métodos, lo cual es un gran alivio.

Ustedes dirán: Ok, pero no podía hacer ya todo esto con Enterprise Library 2.0? La respuesta es sí, pero el costo era mayor. Enterprise Library 2.0 normalmente abre y cierra una conexión cada vez que se invocaba algún método para interactuar con la BD (ExecuteNonQuery, ExecuteReader, LoadDataset, etc). Esto no es compatible con la forma como opera TransactionScope, puesto que, al haber más de una conexión, él considerará que esta es una transacción distribuida (MSDTC) y ese tipo de transacciones tienen un costo en rendimiento realmente significativo, comparado con una simple transacción local.

La genialidad del nuevo Enterprise Library 3.0 está en que ahora los métodos de la clase Database reconocen la existencia de un TransactionScope (de haber alguno) y automáticamente enlista las llamadas a la BD en dicha transacción. Con ello, la clase Database sabrá que debe usar una misma conexión a la BD, lo cual evita la subida hacia transacciones distribuidas y mantiene las transacciones locales.

Esta es solo una pequeña parte de las grandes ventajas que ofrece el trabajar con la Enterprise Library. Para más detalles, revisa este link. Así mismo, más detalles sobre el uso del TransactionScope, por acá.

Julio.

Published 07 May 2007 04:48 PM por Julio Casal

Comentarios

# rochoa_ec said on 08 May, 2007 09:37 AM

Hola, esta muy interesante usar esta clase TransactionScope la verdad es que yo utilizaba siempre el objeto DBTransaction para manejar la integridad de mis transacciones...pero ya con esto voy a olvidarme por completo de esto....

# elperucho said on 20 July, 2007 01:18 PM
Realmente interesante Julio, no sabia esa forma de trabajar del TRANSACTIONSCOPE del EntLib 3.0.
# Julio Casal said on 20 July, 2007 02:01 PM

Pues sí, simplifica bastante el código y aisla código de infraestructura del código de negocio.

Julio.

# Roberto said on 13 December, 2007 07:44 AM
Que sucede si ocurriese un error en la llamada a alguno de los metodos Acreditar o Debitar. como se enteraría el usuario? Existe un rollback con TransactionScope? Roberto.
# Julio Casal said on 13 December, 2007 09:40 AM

Pues dado que no le he agregado a este ejemplo el manejo de excepciones apropiado, la excepción estaría no controlada y, por ende, el usuario inmediatamente recibiría una pantalla con el detalle del error ocurrido. El rollback es implícito, puesto que al no llegar a la linea "ambito.Complete()" la transacción se deshace automáticamente.

Julio.