transacciones

Transacciones en ADO.NET (II): Activando las Transacciones Distribuidas mediante MSDTC


Anteriormente hemos aprendido a utilizar entornos transaccionales o Transaction Scopes. El ejemplo anterior mostraba lo que se conoce como una ‘transacción ligera’ que afecta a varias tablas de una misma fuente de datos. Sin embargo, en numerosas ocasiones se nos presentará la dificultad añadida de trabajar con múltiples fuentes de datos localizadas incluso en puntos geográficos diversos.

Para solventar esta dificultad, el propio Windows nos ofrece la posibilidad de utilizar las llamadas Transacciones distribuidas. Por defecto aparecen deshabilitadas, por lo que a continuación, mostraremos cómo activarlas. Una vez activadas, utilizaremos la clase TransactionScope como si se tratara de una transacción ligera, dejando en manos del sistema operativoel control de la propia transacción.

(más…)

Transacciones en ADO.NET (I): La clase TransactionScope


Hay determinadas ocasiones en las que se nos presenta la obligación de realizar inserciones, eliminaciones o actualizaciones en una base de datos en la cual los datos que se van a tocar están relacionados entre sí. Imaginemos la siguiente situación: nuestro cliente regenta un concesionario, y un cliente desea adquirir un vehículo. Tras rellenar el papeleo pertinente, la venta toca a su fin, y necesitamos realizar una serie de operaciones, tales como:

  • Enviar la orden de cobro al banco.
  • Enviar a Tráfico los datos de nuestro cliente para el registro del mismo.
  • Almacenar los datos de nuestro cliente en la base de datos de la empresa.

Imaginemos por un momento que una de estas operaciones falla, por ejemplo, la segunda: la orden al banco ya habrá sido cursada, mientras que tráfico y el concesionario no tendrán notificación alguna de la titularidad del vehículo. Los datos se encontrarían, pues, en un estado inconsistente. Para evitar este tipo de situaciones recurrimos a las transacciones. Una transacción es un conjunto de operaciones sobre una o varias fuentes de datos, con la peculiaridad de que o se ejecutan todas correctamente, o no se ejecuta ninguna. Una transacción se caracteriza por cumplir las llamadas propiedades ACID, que identifican los requisitos para que una transacción se realice. Estas propiedades son: atomicidad (Atomicity), coherencia (Consistency), aislamiento (Isolation) y permanencia (Durability). Así, en el ejemplo anterior, si la conexión con la base de datos de Tráfico devuelve un error, se revertirán los cambios realizados en la conexión con el banco, dejando la transacción en el estado inicial. Es lo que llamamos un ROLLBACK o «vuelta atrás». En el caso de que todo haya ido correctamente, procederíamos a «comprometer» la transacción, ejecutando todas las operaciones por las que ésta se componga. Es lo que llamamos COMMIT. Vista la introducción teórica ¿cómo implementamos una transacción en ADO.NET? Personalmente me gusta implementar las transacciones mediante entornos transaccionales o transactional scopes, implementados mediante la clase System.Transactions.TransactionScope. Lo primero que deberemos hacer será añadir una referencia a la biblioteca System.Transactions.

090701st

Hecho esto, mediante la cláusula using crearemos un entorno transaccional en el que encerraremos un bloque try/catch. En el interior del try será donde implementemos la lógica de acceso a datos, realizando múltiples consultas/inserciones/eliminaciones/actualizaciones. Si se produce un error, el flujo de control saltará al bloque catch, donde se interrumpirá la transacción y se realizará un ROLLBACK automático. Si no se produce ningún error, se llegará a la cláusula Complete() de la transacción, la cual comprometerá la misma y dejará la fuente de datos en un estado consistente. En C# sería algo como lo que sigue, suponiendo que la lógica de acceso a datos está encapsulada en los métodos del objeto DAO y que ‘listaElementos’ es un array de enteros que contiene identificadores de la tabla a modificar:

using (System.Transactions.TransactionScope scope = new System.Transactions.TransactionScope())
{
try
{
foreach (int id in listaElementos)
{
dao.OrdenarEnvioCuentaBancaria(id, numeroCuenta, datosCliente);
dao.RegistrarVehiculoEnTrafico(id, matricula, datosCliente);
dao.GuardarDatosCompraCliente(id, matricula, datosCliente, fechaActual);
}
scope.Complete();
}
catch (Exception ex)
{
throw (ex);
}
}

Esto nos permitirá realizar transacciones de forma segura. Pero ¡ojo! ¿y si lo que queremos realizar son operaciones sobre distintas fuentes de datos? Para ello necesitaremos activar las transacciones distribuidas, que por defecto aparecen deshabilitadas. En un artículo posterior aprenderemos cómo activarlas.

Realizando una transacción en SQL Server


Por norma general, me gusta codificar la lógica de acceso a datos en programa, en lugar de en procedimientos almacenados en base de datos. Esta aproximación no es muy óptima (como se suele decir, al César lo que es del César y a Dios lo que es de Dios), ya que el entorno transaccional estará mejor gestionado por el SGBD que por el código .NET.
Si queremos trasladar la responsabilidad de gestionar la transacción al SGBD, deberemos saber en primer lugar que se declara del siguiente modo:

BEGIN TRAN
 -- Ejecución de sentencias
COMMIT TRAN

Como imaginaremos, la primera sentencia «arranca» la transacción, mientras que la última la compromete. Pero ¿y si ocurre un error en alguna de las sentencias? ¿Cómo lo detectamos?
En el siguiente ejemplo, utilizaremos variables enteras para trazar los fallos y realizar un ROLLBACK en el caso de que la transacción falle.

Se declarará una variable de tipo INT por cada sentencia a ejecutar, y se inician a 0. Se comprueba, después de cada sentencia ejecutada, la variable @@Error, y se almacena en las variables anteriormente creadas. Si es distinta de 0, hay un error, por lo que se añade información al mensaje de error.
Al final de la transacción, si todas las variables son 0, se hace el COMMIT. Si una de ellas es distinta de cero, significará que existe un error y se hace el Rollback, mostrando el mensaje de error.


DECLARE @CadenaError varchar(1000);
 DECLARE @CodigosError varchar(1000);
 DECLARE @ErrorSentencia1 int;
 DECLARE @ErrorSentencia2 int;
 DECLARE @ErrorSentencia3 int;

 SET @ErrorSentencia1 = 0;
 SET @ErrorSentencia2 = 0;
 SET @ErrorSentencia3 = 0;

 SET @CadenaError = 'Error al realizar actualización: ';
 SET @CodigosError = 'Códigos de error: ';

 -- Inicio de la transacción
 BEGIN TRAN

 -- Primera actualización
 UPDATE Usuario SET Nombre = 'Elena' WHERE Nombre = "Helena";

 -- Almacenamos el posible código de error
 SET @ErrorSentencia1 = @@Error;
 IF (@ErrorSentencia1  0)
 BEGIN
     SET @CadenaError = @CadenaError + 'Actualización 1; '
     SET @CodigosError = @CodigosError + CAST(@ErrorSentencia1 AS VARCHAR(10)) + '; ';
 END

 -- Segunda actualización
 UPDATE Perfil SET Nombre = 'Administrador' WHERE Nombre = "Admin";

 -- Almacenamos el posible código de error
 SET @ErrorSentencia2 = @@Error;
 IF (@ErrorSentencia2  0)
 BEGIN
     SET @CadenaError = @CadenaError + 'Actualización 2; '
     SET @CodigosError = @CodigosError + CAST(@ErrorSentencia2 AS VARCHAR(10)) + '; ';
 END

 -- Tercera actualización
 UPDATE UsuarioPerfil SET CodigoUsuario = 1 WHERE CodigoPerfil < 3;

 -- Almacenamos el posible código de error
 SET @ErrorSentencia3 = @@Error;
 IF (@ErrorSentencia3  0)
 BEGIN
     SET @CadenaError = @CadenaError + 'Actualización 3; '
     SET @CodigosError = @CodigosError + CAST(@ErrorSentencia3 AS VARCHAR(10)) + '; ';
 END

 -- En ausencia de errores, comprometemos la transacción
 IF (@ErrorSentencia1 = 0) AND (@ErrorSentencia2 = 0) AND (@ErrorSentencia3 = 0)

BEGIN
     COMMIT TRAN;
     PRINT('Transacción realizada con éxito');
 END
 ELSE
 BEGIN
     ROLLBACK TRAN;
     PRINT('Transacción abortada');
     PRINT(@CadenaError);
     PRINT(@CodigosError);
 END

Con todo esto, obtendremos suficiente información (esperemos) como para detectar donde está el fallo.