jueves, 18 de diciembre de 2008

Un ejemplo práctico II Bis.

Todo modelo que se precie debe implementar mecanismos que permitan tanto su expansión como su trazabilidad. Es muy conveniente poder saber, por ejemplo, por qué esta fallando nuestra aplicación al intentar realizar una operación en la base de datos en un entorno de producción. Para conseguir este objetivo es requisito indispensable que cualquier operación de acceso a base de datos se realice desde un punto común. De esta forma, por tanto, podremos trazar dichas operaciones e informar de los posibles errores que estas puedan producir.

En el modelo esto es responsabilidad de la clase Dal.DataServer. A continuación el fragmento de código que lo realiza:

.
.
Public Shared Function ExecuteReader(ByVal cmd As IDbCommand) As IDataReader
  Return ExecuteCommand(Of IDataReader)(cmd, AddressOf _ExecuteReader)
End Function
Public Shared Function ExecuteNonQuery(ByVal cmd As IDbCommand) As Integer
  Return ExecuteCommand(Of Integer)(cmd, AddressOf _ExecuteNonQuery)
End Function
Public Shared Function ExecuteScalar(Of T)(ByVal cmd As IDbCommand) As T
  Return ExecuteCommand(Of T)(cmd, AddressOf _ExecuteScalar)
End Function
' Para centralizar los mensajes de error de la Capa de Acceso a Datos
' de forma que se puedan: Escribir la sentencia SQL, escribir en un log, tratar el texto de error, etc
Private Shared Function ExecuteCommand(Of T)(ByVal cmd As IDbCommand, ByVal handler As CommandHandler(Of T)) As T
  Try
    If _Trazas.Enabled And DisableLog = False Then
      Trace.WriteLine(IIf(cmd.Transaction IsNot Nothing, "Dal --> Transaction.", "DAL.") & "ExecuteCommand : " & cmd.CommandText)
    End If
    DisableLog = False
    Return handler(cmd)
  Catch e As Exception
    ' Trace the exception into the same log.
    Trace.WriteLine("--- Exception : " & e.Message)
    ' Aqui se podría inntentar controlar los mensajes de error de clave duplicada, etc...
    Throw New Exception("Data Access Layer Exception ", e)
  End Try
End Function
Private Shared Function _ExecuteReader(ByVal Cmd As IDbCommand) As IDataReader
  Return Cmd.ExecuteReader()
End Function
Private Shared Function _ExecuteNonQuery(ByVal Cmd As IDbCommand) As Integer
  Dim Res As Integer = Cmd.ExecuteNonQuery()
  If _Trazas.Enabled Then
    Trace.WriteLine(IIf(Cmd.Transaction IsNot Nothing, "Dal --> Transaction ", "DAL ") & Res & " Filas")
  End If
  Return Res
End Function
Private Shared Function _ExecuteScalar(Of T)(ByVal Cmd As IDbCommand) As T
  Return Cmd.ExecuteScalar
End Function
.
.
.

Es una traducción-adaptación a vb, allá por febrero del 2006, de un articulo lo publicado en www.codeproject.com titulado: “Templating via Generics and delegates” de Oren Ellenbogen.

Configuración del Debug.

Image1Vamos a modificar el formulario del proyecto de pruebas añadiéndole algunos botones de comando, cambiaremos el control de texto por un RichTextBox y adecentamos un poco el formulario.

Además vamos a necesitar una clase que nos permita visualizar las trazas que nuestro modelo va dejando. Esta clase va a heredar de TraceListener y enviará todo mensaje que escribamos en el objeto Trace al control RichTextBox dándole un aspecto más vistoso. Una buena presentación siempre es de  agradecer.

Este es el código de nuestro, bueno mío, TraceLister:

Friend Class MyListener : Inherits TraceListener
  Private _theRichTextBox As RichTextBox
  Private _theSetValueDelegate As SetValueDelegate

  Public Sub New(ByVal control As RichTextBox)
    MyBase.new()
    _theRichTextBox = control
    _theRichTextBox.Multiline = True
    _theSetValueDelegate = New SetValueDelegate(AddressOf SetValueProc)
  End Sub

  Public Overloads Overrides Sub Write(ByVal message As String)
    _theRichTextBox.BeginInvoke(_theSetValueDelegate, New Object() {message})
  End Sub

  Public Overloads Overrides Sub WriteLine(ByVal message As String)
    _theRichTextBox.BeginInvoke(_theSetValueDelegate, New Object() {message & Environment.NewLine})
  End Sub

  Private Delegate Sub SetValueDelegate(ByVal value As String)
  Private Sub SetValueProc(ByVal value As String)
    _theRichTextBox.SelectionColor = Color.Blue
    If value.IndexOf("Objetos/Segundo") > -1 Then _theRichTextBox.SelectionColor = Color.Gray
    If System.Text.RegularExpressions.Regex.Match(value, _
                    "(Commit)|(Rollback)|(BeginTransaction)|(Transaction.)").Success Then _theRichTextBox.SelectionColor = Color.Green    
    _theRichTextBox.AppendText(New String(" "c, 2 * Trace.IndentLevel) & value)
    _theRichTextBox.ScrollToCaret()
  End Sub

End Class

El siguiente paso es inicializar el mecanismo de notificación de carga de objetos que nos ofrece el Dal y agregar nuestro listener a la lista para que reciba los mensajes de depuración

Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
  .
  ' Resto del código omitido intencionadamente
  .       
  Dal.Loader.OnBeginLoad = New Dal.BeginLoadDelegate(AddressOf OnBeginLoad)
  Dal.Loader.OnEndLoad = New Dal.EndLoadDelegate(AddressOf OnEndLoad)
  Dal.DXTimer.Init()
    
  With Trace.Listeners
    .Add(New MyListener(Me.TextBox1))
  End With

End Sub

Private Shared Sub OnBeginLoad()
  DAL.DXTimer.GetElapsedMilliseconds()
End Sub

Private Shared Sub OnEndLoad(ByVal Properties As Integer, ByVal Objects As Integer)
  Dim time As Double = DAL.DXTimer.GetElapsedMilliseconds
  Trace.WriteLine( _
    String.Format("{0}s para {1} objetos con {2} campos.{3} Objetos/Segundo", _
                    New String() {(time / 1000.0).ToString("0.###0"), _
                                   Objects, _
                                   Properties, _
                                   (Objects / (time / 1000.0)).ToString("0.###0") _
                                   }))
End Sub
A partir de este momento tendremos completa información sobre las sentencias SQL ejecutadas así como del tiempo empleado en crear y cargar los objetos con los valores obtenidos.

Carga de objetos

Image2
Una vez que hemos realizado los cambios anteriores la pulsación del botón Cargar, como ya hacía antes, llenará lista con los ids de los envíos recuperados. Pero esta vez tenemos más cosas. Tenemos INFORMACIÓN y la información es poder.
Vemos como se van creando conexiones, como se van cerrando, que sentencias se ejecutan y cual es el tiempo que el sistema tarda en crear y cargar los objetos de la capa de Negocio.
Aquí, y seguro que alguno ya se ha dado cuenta, los datos nos muestran diferentes valores en lo referente al tiempo de carga de objetos: 0,2881 s y 0,004s. Estos valores corresponden a dos ejecuciones consecutivas y, aunque no reflejan fielmente la velocidad de carga ya que sólo son dos los registros de la tabal, si nos indican que la primera vez tarda más.
Esta tardanza es debida a la generación del delegado de carga de ese tipo de objeto. Esta generación solo se produce una vez en la “vida” del proceso que hospeda nuestros objetos. Las siguientes llamadas ejecutan código intermedio generado dinámicamente con la información de del DataBinder del objeto. Las pruebas están hechas con VS2008 en un Pentium II, no os digo más. Si es el que tengo en casa, ¡¡¡que pasa!!!!.
Entraré a fondo en este tema otro día. En www.codeproject.com  estaba la inspiración “A General Fast Method Invoker” por Luyan.

Las transacciones en el Modelo

Para probar el funcionamiento de las transacciones hemos añadido un botón con el título Insertar y establecido un manejador para el mimo. Este procedimiento simplemente insertará dos registro en la base de datos. En una primera ejecución vamos a olvidar un campo requerido por la tabla para observar que es lo que ocurre.
Este es el código del procedimiento:
 
Private Sub btnInsertar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnInsertar.Click    
    Dim TH As New Negocio.TransactionHelper(Dal.DataServer.GetNewConection())
    Try
      TH.BeginTransaction()
      ' Insertar el primer elemento (sintaxis 1)
      With New Negocio.Envio()
        .ConnectionProvider = TH.ConnectionProvider
        .TransactionProvider = TH.TransactionProvider
        .Id = 0 ' Segun nuestro diseño esto provocará una inserción.
        .IdEnvio = "Envio_02"
        .NumeroDeRegistro = "REG002"
        .FechaDeRegistro = "17/12/2008"
        .Observaciones = "Con método 2"
        .Usuario = "Test"
        .Save()
      End With
      ' Insertar el segundo elemento (Sintaxis 2)
      With Negocio.Factory.Crear(Of Negocio.Envio)(TH)
        .Id = 0
        .IdEnvio = "Envio_03"
        '.NumeroDeRegistro = "REG003"
        .FechaDeRegistro = "17/12/2008"
        .Observaciones = "Con la sintaxis 2 ahorramos lineas"
        .Usuario = "Test"
        .Save()
      End With
      TH.Commit()
      Trace.WriteLine("Inserción realizada")
    Catch ex As Exception
      TH.RollBack()
      Trace.WriteLine(ex.Message)
      MsgBox(ex.Message)
    Finally
      TH.Dispose()
    End Try
End Sub
 
En la imagen se puede ver cual ha sido el resultado de esta ejecución accidentada.

Image3

Gracias a nuestro mecanismo centralizado de consultas y trazas podemos fácilmente ver como el error que se ha producido en la segunda inserción es debido a la omisión de un valor requerido. Además observamos como: se abre y cierra correctamente la conexión; las operaciones se ejecutaban dentro de una transacción y que esta ha sido cancelada.

La siguiente imagen muestra una ejecución correcta una vez quitado el comentario del NumeroDeRegistro.
 
Image4

El código superior nos muestra las dos formas de especificar que conexión y que transacción usan los objetos. En la primera se establecen explícitamente, en la segunda se hace a través de un artilugio software (una clase vamos) que nos crea el objeto y establece estos valores obteniéndolos de TranscationHelper, pero, y esto es lo importante, con una sola línea de código. “Menos líneas menos errores”

Trabajar con conexiones

La forma en que el modelo hace uso de las conexiones es muy ¿simpe?: Si el objeto de la capa de datos tiene una conexión asignada la usa, si no y si tiene un ConnectionProvider lo invocará almacenando el resultado. En el caso de que las dos anteriores condiciones no se cumplan buscará primero una conexión compartida que usar. Si no la encuentra creará una nueva conexión y la almacenará por si el objeto la necesitara posteriormente. Es muy importante llamas al metodo Dispose de los objetos del DAL y que de no hacerlo las conexiones siguen abiertas. por lo tanto es “obligatorio” utilizar el patrón Using que implicitamente llamará a Dispose por nosotros:

Public Function Carga() As Negocio.Envios
  Using D__ As New Dal.Envios(ConnectionProvider, TransactionProvider)
    Return Dal.Loader.LoadObjects(Of Envio)(Me, D__.GetItems())
  End Using
End Function
 
En el formulario de pruebas hemos agregado un nuevo botón y un manejador para realizar, ahora, unas pruebas de modificación de objetos.
 
  Private Sub btnModificar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnModificar.Click
    Dim TH As New Negocio.TransactionHelper(Dal.DataServer.GetNewConection())
    Try
      With Negocio.Factory.Crear(Of Negocio.Envios)(TH).Carga()
        For Each env In .this.Where(Function(envio) envio.Usuario = "Test")
          env.Usuario = "OldTest"
          env.Save()
        Next
      End With
    Catch ex As Exception
      Trace.WriteLine(ex.Message)
      MsgBox(ex.Message)
    Finally
      TH.Dispose()
    End Try
  End Sub
 
Este es su aspecto después de ejecutar el código superior
 
Image5

Aquí podemos ver como creamos un objeto TransactionHelper para pasárselo al “Creador de objetos” Factory e inmediatamente llamar al método Carga() del objeto devuelto. Con este método, y aprovechando la sentecia With, nos ahorramos la posible variable local, creamos y cargamos en la misma línea. Después vemos la utilidad de la propiedad This de “nuestros” objetos al permitirnos referenciar la colección dentro de un With y utilizando LINQ obtener solo los envios hechos por “Test”.

Cambiamos el valor de alguna propiedad y grabamos. ESTO SON OBJETOS. Creo que este tipo de flexibilidad es la que hay que ir buscando a la hora de diseñar los objeto ya que, con la actual capacidad de proceso de las maquinas con las que nos encontramos hoy en día, el recuperar 30 objetos con una simple sentencia “Select * from tabla” filtrarlos con LINQ y actualizar finalmente 4 es preferible a aplicaciones con decenas de “Optimizadas” y  “es un solo sitio usadas” operaciones de actualizacion y/o borrado. Por otro lado el utilizar estructuras simples y poco acopladas unas con otras reduce sustancialmente los tiempos de desarrollo. Posteriormente siempre es posible “Optimizar” el procedimiento que hemos detectado problemático.

En resumén si lo que quiero es realizar “una sola” operación de carga no es necesario especificar la conexión a utilizar bastaría lo siguiente:

With New Negocio.Envios().Carga()
  For Each env In .this.Where(Function(envio) envio.Usuario = "Test")
    Trace.WriteLine(env.Id)
  Next
End With
 
   Si lo que queremos es realizar varias operaciones, por ejemplo: cargar un grupo de clientes y sus facturas sería conveniente aprovechar la misma conexión con el fin de evitar una constante creación de conexiones. Utilizaríamos el siguiente patrón:
 
Dim TH As New Negocio.TransactionHelper(Dal.DataServer.GetNewConection())
With Negocio.Factory.Crear(Of Negocio.Envios)(TH).Carga()
  For Each env In .this.Where(Function(envio) envio.Usuario = "Test")
    With Negocio.Factory.Crear(Of Negocio.Anexo)(TH).Carga(env.Id)
        .
        .  
    End With
    With Negocio.Factory.Crear(Of Negocio.Historico)(TH).Carga(env.Id)
        .
        .
    End With
  Next
End with

 

Saludos a todos.

 

.

4 comentarios:

Anónimo dijo...

Realmente fabuloso !!! Gracias

Anónimo dijo...

Muy instructivo !!!

Aquí el debate puede centrarse si los objetos o entidades que se utilizan en presentación tienen esas propiedades que enlazan con la conexión y la transacción.

Saludos.

Rafael Castro dijo...

Estoy de acuerdo en que se podría habilitar un nivel más donde los objetos solamente tuvieran las propiedades de la entidad. Ahora si realizamos un "databind" de los objetos de negocio con algun control que lo posibilite ( dataGrid o como se llame ahora), veremos que muestra estas dos propiedades.

Para este escenario este funcionamiento no es el optimo. Tengo que decir que hace mucho tiempo que no utilizo el "DataBind" y sus muchos problemas al editar, borrar,etc.

El debate está ahí.

Lo importante sería encontrar el método para poder indicar a los objetos de negocio la conexión y la transacción. Cosa que estimo imprescindible.

Saludos y gracias por los comentarios

Anónimo dijo...

No sé qué opinarán los gurús...

Utilizando DAtaBinding con colecciones de entidades se producen muchos problemas, no tanto para Dataset. Pero Dataset mucho más pesado y más si tienes servicios WCF.

El debate...pues muchos no estarían a favor de indicar a los objetos de negocio la conexión y la transacción.
Aunque en la práctica sea muy útil...

Es una discusión eterna...los puristas de Microsoft qué opinarán ?? pues en MSDN Video no había estos casos...

Saludos, feliz año !!!