Transacciones con .NET Fecha: 12/Jul/04 (08 de Julio de 2004) |
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=0Pero 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 TranAl 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 SubLuego 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.RecuperarBotó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 TryLoad_Form (Carga del formulario)
Dim dt As New Persona Grid.DataSource = dt.RecuperarDe 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:
- Debe tener nombre seguro (Strong Name)
- Debe poseer dentro del AssemblyInfo las etiquetas
< Assembly : ApplicationName("Ejemplo_COM_NET")> < Assembly : AssemblyKeyFile("c:\key\demo.snk")>
- Debe heredar de ServicedComponent
- Antes del nombre de la clase debe escribirse
<Transaction(TransactionOption.Required)> Public class PersonaNota: 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 klavesCreamos 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.EnterpriseServicesLuego 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 ClassComo 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 SubAquí 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:
- Inicio, Panel de Control, Herramientas Administrativas, Servicios de Componentes
- Raiz de Consola, Servicios de Componentes, Equipos, Mi PC, Aplicaciones COM+
- Alli debe estar nuestra aplicación registrada
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.
Fichero con el código de ejemplo: cone_win_transacciones.zip - Tamaño 20 KB