martes, 23 de febrero de 2010

Transacciones con .NET

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:

  • 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 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:
  • 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.

No hay comentarios:

Publicar un comentario