Transacciones con .NET
Desde Base de Datos hasta D.T.C.

Fecha: 12/Jul/04 (08 de Julio de 2004)
Autor: José G. García [email protected]


Introducción

Concepto de transacción: Es una secuencia de operaciones realizadas como una sola unidad de trabajo.

Las propiedades de las transacciones se las conoce como ACID: Atomicidad, Coherencia, Aislamiento, Durabilidad.

Atomicidad: Una transacción debe ser una unidad atómica de trabajo, o se hace todo o no se hace nada.

Coherencia: Debe dejar los datos en un estado coherente luego de realizada la transacción.

Aislamiento: Las modificaciones realizadas por transacciones son tratadas en forma independiente, como si fueran un solo y único usuario de la base de datos.

Durabilidad: Una vez concluida la transacción sus efectos son permanentes y no hay forma de deshacerlos.

Ahora, que tiene que ver esto con mis datos?? Pues bien, todas las instrucciones que normalmente escribo en los procedimientos son update, insert, delete de una o más tablas y si no uso procedimientos almacenados uso comandos desde mi programa; pues bien, entonces sería algo así:

	Update Persona set Sueldo=sueldo * 1.5 
	Update Grupos set estado=1 where estado=0 

Pero nos encontramos con un problema, que las dos sentencias deben hacerse siempre unidas, como si fueran una sola pues perteneces a una actualización de sueldos. Pero que pasa si se realiza la primera operación y no la segunda. ¡Huy! Que rabia, o que pasa si solo se realiza parte de la primera, más ¡Huy! Pues no se sabe hasta que punto se hizo o no se hizo nada, pues nada hay que me garantice esto. Aquí surgen las transacciones.

	Begin Tran 
		Update Persona set Sueldo=sueldo * 1.5 
		Update Grupos set estado=1 where estado=0 
	Commit Tran

Al encerrar en una transacción decimos que se realice todo o no se realice nada (atomicidad) pues si surge algún error se deshace todo lo realizado anteriormente en la transacción.

 

Ahora desde vb.NET
(Comandos independientes)

Teniendo una base de datos llamada Ejemplo y una tabla de nombre persona que tiene los siguientes campos

Codigo varchar(10)
Nombres varchar(50)
Sueldo int
Estado varchar(1)

En un formulario que posee un botón escribimos lo siguiente en el evento clic del botón, sin olvidarse hacer antes el imports a system.data.sqlClient:

Dim Conn As  SqlConnection = New  SqlConnection("Data Source=INFORM77;Initial Catalog=EJEMPLO;User Id=sa") 

Conn.Open()

Try

Dim Comando As New SqlClient.SqlCommand("INSERT INTO PERSONA (codigo, nombres, sueldo, estado) Values('JG1','JOSE',200,'A')", Conn )

Comando.ExecuteNonQuery()

Comando = New SqlClient.SqlCommand("INSERT INTO PERSONA (codigo, nombres, sueldo, estado) Values('JG2','LUIS',180,'B')", Conn)

Comando.ExecuteNonQuery()

Comando = New SqlClient.SqlCommand("INSERT INTO PERSONA (codigo, nombres, sueldo, estado) Values('JG3','PEDRO',400,'A')", Conn)

Comando.ExecuteNonQuery()

Catch ex As Exception

MsgBox(ex.Message)

End Try

Conn.Close()

MsgBox("Datos Ingresados")

Como vemos añadiremos en la tabla persona 3 registro, pero si quisiéramos convertirle a transacción para que se ejecuten todos o ninguno si encuentra un error deberíamos hacer lo siguiente.

Fíjese en los datos para el campo código pues ahora son “tr1”, “tr2”, “tr3”.

Dim Conn As        SqlConnection = New        SqlConnection("Data Source=INFORM77;Initial Catalog=EJEMPLO;User Id=sa") 

Conn.Open()

Dim myTrans As SqlTransaction

Dim Comando As SqlClient.SqlCommand

myTrans = Conn.BeginTransaction()

Try

Comando = New SqlClient.SqlCommand("INSERT INTO PERSONA (codigo, nombres, sueldo, estado) Values('tr1','JOSE',200,'A')", Conn)

Comando.Transaction = myTrans

Comando.ExecuteNonQuery()

Comando = New SqlClient.SqlCommand("INSERT INTO PERSONA (codigo, nombres, sueldo, estado) Values('tr2','LUIS',180,'B')", Conn)

Comando.Transaction = myTrans

Comando.ExecuteNonQuery()

Comando = New SqlClient.SqlCommand("INSERT INTO PERSONA (codigo, nombres, sueldo, estado) Values('tr3','PEDRO',400,'A')", Conn)

Comando.Transaction = myTrans

Comando.ExecuteNonQuery()

myTrans.Commit()

MsgBox("Datos Ingresados")

Catch ex As Exception

myTrans.Rollback()

MsgBox(ex.Message)

End Try

Conn.Close()

Ahora al ejecutar insertará los nuevos 3 registros sin novedad, pero lo hará usando transacciones para lo cual iniciamos la transacción con myTrans = Conn.BeginTransaction() y finalizamos con myTrans.Commit() y en caso de algún error ejecutamos myTrans.Rollback() para cancelar todo lo realizado en la transacción. Para comprobar que la transacción se cancela al encontrar un error cambiamos los valores del campo código por tr1 por ab1 y tr2 por ab2, dejando tr3 en el tercer registro pues así dará un error de clave duplicada porque ya existe un registro con esta clave previamente grabada.

Al ejecutar nos saldrá un mensaje que indica que existió una violación de primary key. Si verificamos los datos, no habrá ningún registro añadido pues como está en una transacción o se agregan todos o no se agrega ninguno.

 

Ahora con DataSets

En un formulario tenemos 2 botones Grabar y LeerDatos además de un DataGrid asi:

Ahora vamos a crear una clase llamada persona que ejecute las dos acciones de los botones, aquí está el código:
































































 Imports        System.Data.SqlClient 

Public Class Persona

Public Function Recuperar() As DataSet

   'definimos la coneccion

Dim Conn As SqlConnection = New SqlConnection("Data Source=INFORM77;Initial Catalog=EJEMPLO;User Id=sa")

   'creamos el data adapter con la instruccion select a recuperar

Dim adapter As New SqlDataAdapter("Select * from PERSONA", Conn )

   'abrimos la coneccion

Conn.Open()

   'creamos el dataset donde recuperaremos los datos de la base de datos

Dim ds As DataSet = New DataSet

   'recuperamos los datos a travez del adapter

adapter.Fill(ds, "PERSONA")

   'retornamos los datos

Return ds

End Function

 

Public Sub Grabar( ByVal ds As DataSet)

'definimos la coneccion

Dim Conn As SqlConnection = New SqlConnection("Data Source=INFORM77;Initial Catalog=EJEMPLO;User Id=sa")

'abrimos la coneccion

Conn.Open()

'creamos el data adapter con la instruccion select a recuperar

Dim adapter As New SqlDataAdapter("Select * from PERSONA", Conn )

'Creamos e inicimos la transaccion

Dim Tran As SqlTransaction = Conn.BeginTransaction

'asignamos la transaccion al comando Select del adapter

adapter.SelectCommand.Transaction = Tran

'construimos los demas comandos del adapter (DELETE, INSERT, UPDATE)

Dim X As New SqlCommandBuilder(adapter)

Try

'Actualizamos el DataSet

adapter.Update(ds, "PERSONA")

'Confirmamos la transaccion

Tran.Commit()

'mensaje final

MsgBox("Datos grabados con éxito")

Catch Ex As SqlException

'variable para el mensaje

Dim men As String

'configuracion del mensaje de acuerdo al numero de error devuelto por la MRDB

If ex.Number = 8152 Then

men = "Existen datos demasiados extensos, corrija el problema y vuelva a intentar"

ElseIf ex.Number = 2627 Then

If ex.Message.IndexOf("PRIMARY") <> -1 Then

men = "Error por intentar grabar valores duplicados en campos clave, corrija el problema y vuelva a intentar" ElseIf ex.Message.IndexOf("UNIQUE") <> -1 Then

men = "Error por intentar grabar valores duplicados en campos de valores únicos, corrija el problema y vuelva a intentar"

Else

men = "Error general en la base de datos"

End If

ElseIf ex.Number = 515 Then

men = "Algunos datos no han sido ingresados y son necesario para completar la operación, corrija el problema y vuelva a intentar"

Else

men = "Error general en la base de datos"

End If

'cancelamos la transaccion

Tran.Rollback()

'Indicamos el mensaje

Throw New Exception(men)

Catch Ex As DBConcurrencyException

'cancelamos la transaccion

Tran.Rollback()

'Indicamos el mensaje

Throw New Exception("Lo siento, los datos fueron actualizados por otro usuario")

Catch Ex As Exception

'Indicamos el mensaje

Throw New Exception("Error: " & EX.Message)

End Try

End Sub

Luego en el formulario creado con los botones y el grid escribimos el siguiente código

Botón Leer

     Dim        dt As New        Persona 

Grid.DataSource = dt.Recuperar

Botón Grabar

               Dim        dt As New   Persona 

Try

   dt.Grabar(Grid.DataSource)

     Grid.DataSource = dt.Recuperar

Catch ex As Exception

MsgBox(ex.Message, MsgBoxStyle.Critical)

End Try

Load_Form (Carga del formulario)

               Dim        dt As New   Persona 

   Grid.DataSource = dt.Recuperar

De esta forma podemos grabar y leer los datos del grid en forma transaccional, es decir que si por algún motivo existiese un error se cancelarán todas las actualizaciones. Además esto nos es bien recomendable cuando existe concurrencia de varios usuarios sobre el mismo grupo de registros pues graba solo los registros actualizados del primer usuario que ejecuta el grabar y para los demás usuarios no graba ningún registro, que si no se lo hace en forma transaccional grabaría una parte de los registros y otros no.

Para esto hay que tomar en cuenta que el manejo de transacciones debe hacerse en capa de reglas de negocio (nunca en la capa UI o en la de Datos).

 

Usando D.T.C.

Hasta aquí hemos formado transacciones a nivel de base de datos y desde vb.NET su manejo cuando usamos comandos o cuando usamos Datasets. Ahora vamos un paso más allá, usaremos DTC (Coordinador de Transacciones Distribuidas), aprovechando el uso de COM+ y aprenderemos a interactuar desde .NET.

Veamos un escenario. En un banco yo tengo un Depósito configurado como una transacción pues al depositar yo actualizo y creo registros en alrededor de 6 tablas. Así mismo yo tengo un Retiro configurado como otra transacción y también actualizo y creo registros en 6 tablas más. Pero que pasa cuando deseo hacer otro objeto llamado Traspaso de Dinero en donde está involucrado un retiro y un depósito. Pues, se complica la cosa porque son dos transacciones diferentes y una transacción no puede estar embebida en otra transacción y pero aún si complicamos el escenario cuando el depósito se realiza en SQL y el retiro el Oracle. ¡Huy! Como hago una transacción que al hacer Rollback deje en su esto inicial, pues esto si que está difícil, pues cuando se hacer una transacción que encierra mas transacciones lo visto hasta ahora casi casi no podemos emplear al menos que seamos muy bueno para el uso de bandera de estado y para interoperar entre sistemas.

Para esto hay que crear una clase especial que cumpla ciertas características:

		< Assembly : ApplicationName("Ejemplo_COM_NET")>
				< Assembly : AssemblyKeyFile("c:\key\demo.snk")> 































































		<Transaction(TransactionOption.Required)> Public class Persona

Nota: Todos los métodos de la clase se vuelven transaccionales, para ello hay que escribir <AutoComplete()> antes de la declaración de cada uno. Esto significa que cuando el método termina sin novedad envía un mensaje de terminación y cuando termina por algún error éste envía automáticamente al DTC un mensaje de error para que cancele las operaciones.

Luego de todo hay que registrar la clase en COM+ usando Regsvcs.exe , o al usar la primera vez lo hace automáticamente pero si se tiene permisos de administrador.

Pasemos a transformar nuestra código de actualización del DataSet en una clase COM+.

Nos creamos un proyecto nuevo dentro de la solución que tenemos.

Luego nos creamos un par de llaves (pública y privada para hacer de nuestro nuevo assembly uno con nombre seguro), para esto salimos al símbolo del sistema de visual studio y nos creamos un directorio en la raiz de C:\ (solo por facilidad)

		Md key 			‘crea un directorio llamado key
		Cd key 			‘cambia al Nuevo directorio
		Sn –k demo.snk 		‘crea el archive que contiene las klaves
	

Creamos primero una referencia a: System.EnterpriseServices

Luego Ingresamos al archivo AssemblyInfo de nuestro nuevo proyecto y escribimos el imports a la referencia anterior como primera línea.

	Imports System.EnterpriseServices 

Luego en las etiquetas de los atributos del ensamblado hay que escribir los 2 atributos exigidos para lo que necesitamos:

	< Assembly : ApplicationName("Ejemplo_COM_NET")>
	< Assembly : AssemblyKeyFile("c:\key\demo.snk")>


El ApplicationName es el nombre con el que se registrará en el DTC

El AssemblyKeyFile indica el nombre del archivo generado con las claves pública y privada usadas para crear un assembly con nombre seguro.

La clase quedaría tal como esta en el siguiente código:

Imports        System.EnterpriseServices 

Imports System.Data.SqlClient

 

<Transaction(TransactionOption.Required)> Public Class GrabandoCom

Inherits ServicedComponent

<AutoComplete()> Public Sub Grabando( ByVal ds As DataSet)

'definimos la coneccion

Dim Conn As SqlConnection = New SqlConnection("Data Source=INFORM77;Initial Catalog=EJEMPLO;User Id=sa;Password=''")

'abrimos la coneccion

Conn.Open()

'creamos el data adapter con la instruccion select a recuperar

Dim adapter As New SqlDataAdapter("Select * from PERSONA", Conn )

'construimos los demás comandos del adapter (DELETE, INSERT, UPDATE)

Dim X As New SqlCommandBuilder(adapter)

Try

'Actualizamos el DataSet

adapter.Update(ds, "PERSONA")

Catch Ex As SqlException

'variable para el mensaje

Dim men As String

'configuracion del mensaje de acuerdo al numero de error devuelto por la MRDB   

If ex.Number = 8152 Then

men = "Existen datos demasiados extensos, corrija el problema y vuelva a intentar"

ElseIf ex.Number = 2627 Then

If ex.Message.IndexOf("PRIMARY") <> -1 Then

men = "Error por intentar grabar valores duplicados en campos clave, corrija el problema y vuelva a intentar"

ElseIf ex.Message.IndexOf("UNIQUE") <> -1 Then

men = "Error por intentar grabar valores duplicados en campos de valores únicos, corrija el problema y vuelva a intentar"

Else

men = "Error general en la base de datos"

End If

ElseIf ex.Number = 515 Then

men = "Algunos datos no han sido ingresados y son necesario para completar la operación, corrija el problema y vuelva a intentar"

Else

men = "Error general en la base de datos"

End If

Throw New Exception(men)

Catch Ex As DBConcurrencyException

Throw New Exception("Lo siento, los datos fueron actualizados por otro usuario")

Catch Ex As Exception

Throw New Exception("Error: " & EX.Message)

End Try

End Sub

End Class

Como se puede observar, no existe movimiento ni código de transacciones pues esto se encargará automáticamente del DTC.

Luego en el proyecto de anterior donde teníamos la clase persona cambiamos la programación, creando antes una referencia al nuevo proyecto que acabamos de escribir.

El código del método grabar se reemplazo por este:

	
	Public Sub        Grabar( ByVal        ds As        DataSet) 

Dim dtc As New Datos.GrabandoCom

Try

dtc.Grabando(ds)

Catch ex As Exception

Throw New Exception(ex.Message)

End Try

End Sub

Aquí se crea una variable de tipo Datos.GrabandoCom que es el nombre del proyecto seguido del método GrabandoCom.

Eso es todo, ahora las transacciones son automáticas y quien es encargado es el DTC. Puede estar las conexiones a varias bases de datos de diferente o igual tipo más todo se hará Commit o Rollback, todo como una sola y única transacción con todas las prestaciones de COM+.

Luego de ejecutar la pantalla que contiene el Grid se puede comprobar que las transacciones si se ejecutan y es más, al ver el monitor de COM+ comprobamos que todo está Ok.

Para ver que se ha registrado nuestra dll entraremos por:

 

Y más abajo en estadísticas de transacciones se puede ver el registro de cuantas transacciones se han registrado, tanto las concluidas bien como las no concluidas.



ir al índice

Fichero con el código de ejemplo: cone_win_transacciones.zip - Tamaño 20 KB