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.

 

.

Un ejemplo práctico II.

En esta segunda entrega del ejemplo práctico pretendo mostrar:

  1. Las posibilidades de obtener las trazas de toda consulta realizada en la capa DAL.
  2. Las diferentes formas de inicialización, carga y utilización de los objetos.
  3. La implementación de transacciones en el modelo.
  4. Utilización de las conexiones.

Como es interesante conocer en todo momento los tiempos de carga de los objetos, el modelo dispone de un mecanismo de notificación de comienzo y fin de carga (OnBeginLoad y OnEndLoad). Pero esto lo veremos más adelante. Ahora solo agregar el código para un Timer mas preciso.

No recuerdo muy bien dónde encontré el código en C# pero creo que fue en www.kalme.de.

Este el código de Dal.Core.DXTimer.vb, archivo que hay que añadir al proyecto DAL aunque, ahora tras algún tiempo, estoy empezando a cuestionarme si no debiera estar en otra Capa. No obstante el cambio de localización de esta clase no compromete en nada el modelo.

Imports System
Imports System.Runtime.InteropServices

Namespace Dal
  Public Class DXTimer

#Region " imported functions "
    <System.Security.SuppressUnmanagedCodeSecurity()> _
    Private Declare Function QueryPerformanceFrequency Lib "kernel32" (ByRef PerformanceFrequency As Long) As Boolean
    <System.Security.SuppressUnmanagedCodeSecurity()> _
    Private Declare Function QueryPerformanceCounter _
    Lib "kernel32" (ByRef PerformanceCount As Long) As Boolean
#End Region

    ' The last number of ticks.
    Private Shared lLastTime As Long = 0
    ' The current number of ticks.
    Private Shared lCurrentTime As Long = 0
    ' The number of ticks per second for this system.
    ' This will be a constant value.
    Private Shared lTicksPerSecond As Long = 0
    ' Indicates if the Timer is initialized
    Private Shared bInitialized As Boolean = False
    ' The elapsed seconds since the last GetElapsedSeconds() call.
    Private Shared dElapsedSeconds As Double = 0.0
    ' The elapsed milliseconds since the last   GetElapsedMilliseconds() call.
    Private Shared dElapsedMilliseconds As Double = 0.0

    ' Property to query the ticks per second  for this system (Timer has to be initialized).
    Public Shared ReadOnly Property TicksPerSecond() As Long
      Get
        Return lTicksPerSecond
      End Get
    End Property

    ' The initialization of the timer. Tries to query
    ' performance frequency and determines if performance
    ' counters are supported by this system.
    Public Shared Sub Init()
      ' Try to read frequency. 
      ' If this fails, performance counters are not supported.
      If QueryPerformanceFrequency(lTicksPerSecond) = False Then
        Throw New Exception("Performance Counter not supported on this system!")
      End If
      ' Initialization successful
      bInitialized = True
    End Sub

    ' Starts the Timer. This set the initial time value.
    ' Timer has to be initialized for this.
    Public Shared Sub Start()
      ' Check if initialized
      If bInitialized = False Then
        Throw New Exception("Timer no initializado!")
      End If

      ' Initialize time value
      QueryPerformanceCounter(lLastTime)

    End Sub


    ' Gets the elapsed milliseconds since the last
    ' call to this function. Timer has to be initialized
    ' for this.
    ' Returns The number of milliseconds.
    Public Shared Function GetElapsedMilliseconds() As Double

      ' Check if initialized
      If bInitialized = False Then
        Throw New Exception("Timer no initializado!")
      End If

      ' Get current number of ticks
      QueryPerformanceCounter(lCurrentTime)

      ' Calculate number of milliseconds since last call
      dElapsedMilliseconds = (Convert.ToDouble(lCurrentTime - lLastTime) / Convert.ToDouble(lTicksPerSecond)) * 1000.0

      ' Store current number of ticks for next call
      lLastTime = lCurrentTime

      ' Return milliseconds
      Return dElapsedMilliseconds
    End Function


    ' Gets the elapsed seconds since the last call
    ' to this function. Timer has to be initialized for this.

    ' Returns The number of seconds.
    Public Shared Function GetElapsedSeconds() As Double
      ' Check if initialized
      If bInitialized = False Then
        Throw New Exception("Timer no initializedo!")
      End If
      ' Get current number of ticks
      QueryPerformanceCounter(lCurrentTime)

      ' Calculate elapsed seconds
      dElapsedSeconds = Convert.ToDouble(lCurrentTime - lLastTime) / Convert.ToDouble(lTicksPerSecond)

      ' Store current number of ticks for next call
      lLastTime = lCurrentTime

      ' Return number of seconds
      Return dElapsedSeconds
    End Function

    Public Shared Rnd As New Random(DateTime.Now.Millisecond)
  End Class
End Namespace

miércoles, 17 de diciembre de 2008

Un ejemplo práctico

image

Con esta entrada pretendo dar una visión generar del modelo y mostrar algunos ejemplos de su utilización.

Quiero hacer hincapié en que lo que se pretende es acortar el tiempo de desarrollo, homogeneizar el código fuente y ALGO MUY IMPORTANTE: reducirlo . Ya lo dijo el sabio “menos líneas menos errores”.

Lo primero que tenemos que hacer es configurar una solución con el aspecto de la imagen de la derecha.

Todos los ficheros están como antiguas entradas. La tabla que vamos a utilizar tiene el siguiente definición:

CREATE TABLE [dbo].[T_Envio](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [IdEnvio] [nvarchar](250) COLLATE Modern_Spanish_CI_AS NOT NULL,
    [Observaciones] [nvarchar](200) COLLATE Modern_Spanish_CI_AS NULL,
    [NumeroDeRegistro] [nvarchar](250) COLLATE Modern_Spanish_CI_AS NOT NULL,
    [FechaDeRegistro] [datetime] NOT NULL,
    [Fecha] [datetime] NOT NULL CONSTRAINT [DF_T_Envio_Fecha]  DEFAULT (getdate()),
    [Usuario] [nvarchar](250) COLLATE Modern_Spanish_CI_AS NOT NULL,
 CONSTRAINT [PK_T_Envio] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]

A continuación deberemos crear un nuevo proyecto para la capa que nos queda, la capa de presentación. En este proyecto es en el que veremos las distintas formas de utilizar los objetos.

De dónde se obtienen los datos.

Una de las primeras cosas que, cuando empecé en esto, me sorprendía, es que en todos los ejemplos de acceso a datos que encontraba por ahí, las conexiones con la base de datos se establecían al ladito de donde se cargaban los objetos de la capa de negocio. Esto, a mi modo de ver, no encaja en un enfoque orientado a objetos donde estos TIENEN que tener la posibilidad de no conocer cual es origen de los datos, pero también TIENEN que tener la posibilidad de especificarlo.

¿Como se resuelve esta aparente paradoja? Pues dotándoles de dos propiedades que son delegados que retornaran, si están establecidos, la conexión y la transacción asignados a ese objeto. En el caso que no establezcamos dichos valores será la capa de acceso a datos la que tenga que decidir que conexión utilizar. Esta decisión se basa en si el proyecto es WEB o WindowsForm (supongo que la conexión está abierta durante la ejecución de la aplicación).

Para simplificar las cosas aquí van dos ejemplos:

Aplicación ASP.NET

  Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
    'Dal.DataServer.SetWebConnection(System.Configuration.ConfigurationManager.AppSettings("ConnectionString"))
    Dal.DataServer.SetWebConnection("server=.;database=TEST;uid=user;pwd=xxx")
  End Sub

Aplicación WindowsForm:

Dal.DataServer.Open("server=.;database=TEST;uid=user;pwd=xxx")

La diferencia entre los dos “Modelos” es que en la primera se especifica la cadena de conexión y en la otra se abre realmente y queda a disposición de la aplicación.

Veamos cual es el camino que sigue una petición de carga:

1. Se solicita la carga de un objeto:

 Dim __Envios As Negocio.Envios = New Negocio.Envios().Carga()

2. La capa de negocio pasa la petición a la de Datos y le informa de lo que sabe (sus “provider”)

    Public Function Carga(ByVal id As Integer) As Envio
      Using D__ As New Dal.Envios(ConnectionProvider, TransactionProvider)
        Dal.Loader.LoadObject(Me, D__.GetItem(id))
      End Using
      Return Me ' El que los métodos devuelvan la instancia permite muuuuchas cosas.
    End Function

En este enlace se puede ver algún ejemplo de lo que se puede conseguir con la implementación de funciones que devuelven la instancia en lugar de la implementación de simples métodos. El que lo dice no es un cualquiera.

3. El DAL realiza una serie de comprobaciones para determinar los parámetros( conexión y transacción) que debe utilizar:

Protected Function CreateCommand() As System.Data.IDbCommand
  Dim cmd As IDbCommand = ConexionEnUso.CreateCommand()
  cmd.Connection = ConexionEnUso
  cmd.Transaction = TransaccionEnUso
  Return cmd
End Function
Private _ConexionActual As IDbConnection = Nothing
Protected ReadOnly Property ConexionEnUso() As IDbConnection
  Get
    ' Primero se intenta usar la conexion asignada al objeto
    If _ConexionActual IsNot Nothing Then Return _ConexionActual
    ' Luego se intenta usar el proveedor de conexion
    If Me.ConnectionProvider IsNot Nothing Then
      _ConexionActual = ConnectionProvider.Invoke()
      Return _ConexionActual
    End If
    ' Si la conexion compartida no existe se crea una nueva (normalmente para uso con WebForm)
    If DataServer.Connection Is Nothing Then
      _ConexionActual = DataServer.GetNewConection()
      Return _ConexionActual
    End If
    Return DataServer.Connection() ' Se devuelve la connexion compartida (Esta ser� la usada normalmente por WindowsForm)
  End Get
End Property
Public Function GetItem(ByVal id As Integer) As IDataReader
  Using cmd As IDbCommand = CreateCommand()   
    cmd.CommandText = String.Format("Select * from [TELMA_Envio] where Id={0}", id)
    Return DataServer.ExecuteReader(cmd)
  End Using
End Function

Otro día entraré a fondo en la clase DataServer y sus métodos y como nos permite controlar, trazar TODAS las consultas que hacemos a la base de datos. También debería volver a buscar las páginas de las que aprendí todo lo que estoy poniendo aquí y enlazarlas. Prometo hacerlo.

Cómo deben comportarse los objetos

Los objetos deben comportarse bieen y ser bueeeeeeeeenos. Además, y algo muy importante, TODOS deben comportarse de forma parecida a fin de que el conocimiento por parte de un componente del equipo del código de un objeto u objetos, le haga sentirse a gusto con el código desarrollado por los otros componentes.

Yo propongo que:

  1. Los elementos (no colecciones) deben poder grabarse, borrarse y cargarse.
  2. La grabación: Inserciones y actualizaciones se facilita si adoptamos Id únicos por registro. Yo siempre que puedo uso auto numéricos.
  3. Con autonumericos el establecimiento de objeto.Id=0 implica una inserción en la base de datos al llamar a objeto.Save(). Posteriores llamadas as Save() provocan la actualización.
  4. Los constructores de objetos NUNCA cargan el objeto. Utilizo New Negocio.Envio().Carga(25).
  5. Los Elementos No cargan colecciones, eso lo dejo para las coleccionesDeObjetos
  6. Las Colecciones de objetos SON LAS RESPONSABLES de cargar grupos de objetos. Para ello deberán tener metodos con nombres significativos como: CargaPorId(codigo), CargaPendientes(), etc.  Por favor evitar esto: CarIElePend(), No hay quien se entere luego.
  7. Siempre que se pueda, y aquí entran cuestiones de rendimiento, el primer enfoque que doy a la carga de entidades relacionada es la Lazy Load o Carga Perezosa con algun mecanismo de caheo. De esta manera los objetos no pierden “su integridad” y siguen funcionando como se espera tras implementar diferentes modos de carga: en un escenario haciendo un inner join con Provincias y en otro escenario con Facturas donde no interesa en absoluto la información de las anteriores. Esto también es tema de grandes discusiones y de otro posible post.

El ejemplo

Bueno todo esto empezó con que iba a poner un ejemplo. Al final me he liado.

Una vez que tenemos una ventana de prueba en un proyecto de prueba en el que hemos referenciado Dal.dll y Negocio.dll

image

escribimos es siguiente código en el formulario:

Public Class Form1
  Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
    ' Si quisieramos una sola conexión por aplicación descomentariar la linea siguiente
    'Dal.DataServer.Open("server=.;database=TRACTORES;uid=xxx;pwd=xxx")
    ' Para simular un entorno WEB donde las conexiones se deben crear y cerrar mu rapido :-)
    Dal.DataServer.SetWebConnection("server=.;database=TRACTORES;uid=xx;pwd=xx")
  End Sub
  Private Sub btnEjecutar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnEjecutar.Click
    Me.ListBox1.DataSource = Nothing
    With New Negocio.Envios().Carga()
      Me.ListBox1.DisplayMember = "IdEnvio"
      Me.ListBox1.DataSource = .this
    End With
  End Sub
End Class

Nuestra ventana se ve así

image

Bueno pues esto es todo por hoy.
No he pensado nunca en hacer esto, y mira lo he hecho he escrito un articulo en un blog.
CONTINUARÁ
Saludos.

martes, 16 de diciembre de 2008

Explicación del Proyecto Base

Todos estos archivos forman parte del “Proyecto base”. Exceptuando los que hacen referencia a Envíos que es una entidad de ejemplo para que se pueda ver como interactúan las dos “Capas”.

La idea es generar el código de estas dos capas: la de Negocio y la de Acceso a Datos que se repite en todas las entidades. Para esto he creado una herramienta de uso interno. Le he mandado el código tanto del generador como del proyecto base a El blog de Quique. No se si lo pondrá en geeks  no obstante existen multitud de generadores que se podrían utilizar.

Es necesario decir que muchas de las cosas que este modelo tiene(buenas se entiende) no se me han ocurrido a mi solo. Lo que he hecho es solo coger algo de aquí y algo de allí y unirlo. Muchas de las ideas y de la información están en CodeProject.

Negocio.EnviosCollection.vb

' Fichero generado con PolCodeGen
'        __          __               _     ___             _                   ___  
'       /__\  __ _  / _|  __ _   ___ | |   / __\ __ _  ___ | |_  _ __  ___     / _ \ ___   _ __ ___    ___  ____
'      / \// / _` || |_  / _` | / _ \| |  / /   / _` |/ __|| __|| '__|/ _ \   / /_\// _ \ | '_ ` _ \  / _ \|_  /
'     / _  \| (_| ||  _|| (_| ||  __/| | / /___| (_| |\__ \| |_ | |  | (_) | / /_\\| (_) || | | | | ||  __/ / / 
'     \/ \_/ \__,_||_|   \__,_| \___||_| \____/ \__,_||___/ \__||_|   \___/  \____/ \___/ |_| |_| |_| \___|/___|                                                                   
'

Namespace Negocio
  ''' <summary>
  ''' Envios es una slase de contiene elementos del tipo Envio
  ''' </summary>
  <Xml.Serialization.XmlRoot("Envios")> _ 
  Public Class Envios : Inherits ColeccionBase(Of Envio)
    Public Const ClassName As String = "Coleccion de Envios"
    
    ''' <summary>
    ''' Referencia a la instancia.
    ''' </summary>
    '''<example>
    ''' <c>
    ''' With New Envios()
    '''   DoSomething(.this())
    '''   .DoPrint()
    ''' End With
    ''' </c>
    ''' </example>
    Public Overloads Function this() As Envios
      Return Me
    End Function
    
    ''' <summary>
    ''' Constructor de instancias de proporcionando en <param ref="conexion" /> la conexión
    ''' </summary>
    Public Shared Function Values(ByVal fieldName As String, ByVal th As TransactionHelper) As String()    
      Using D__ As New DAL.Envios()
        D__.ConnectionProvider = th.ConnectionProvider
        D__.TransactionProvider = th.TransactionProvider      
        With D__.GetDistinctItem(fieldName)
          Dim Cadenas__ As New Generic.List(Of String)
          Do Until .Read() = False
            If Not TypeOf (.GetValue(0)) Is System.DBNull Then Cadenas__.Add(.GetValue(0))
          Loop
          .Close()
          Return Cadenas__.ToArray()
        End With        
      End Using      
    End Function


#Region " Recuperación desde la Base de Datos "
    ''' <summary>
    ''' Carga todos los elementos del tipo Envio desde la base de datos en la colección
    ''' </summary>
    Public Function Carga() As Envios
      Using D__ As New Dal.Envios(ConnectionProvider, TransactionProvider)
        Return Dal.Loader.LoadObjects(Of Envio)(Me, D__.GetItems())
      End Using
    End Function
    'Public Function CargaByXXX(ByVal IdXXX As Integer) As Envios
    '  Using D__ As New DAL.Envios(ConnectionProvider, TransactionProvider)
    '    Return DAL.Loader.LoadObjects(Of Envio)(Me, D__.GetItemsByXXX(IdXXX))
    '  End Using      
    'End Function    
#End Region
  

  End Class
End Namespace

Negocio.Envio.vb

' Fichero autogenerado con PolCodeGen
'        __          __               _     ___             _                   ___  
'       /__\  __ _  / _|  __ _   ___ | |   / __\ __ _  ___ | |_  _ __  ___     / _ \ ___   _ __ ___    ___  ____
'      / \// / _` || |_  / _` | / _ \| |  / /   / _` |/ __|| __|| '__|/ _ \   / /_\// _ \ | '_ ` _ \  / _ \|_  /
'     / _  \| (_| ||  _|| (_| ||  __/| | / /___| (_| |\__ \| |_ | |  | (_) | / /_\\| (_) || | | | | ||  __/ / / 
'     \/ \_/ \__,_||_|   \__,_| \___||_| \____/ \__,_||___/ \__||_|   \___/  \____/ \___/ |_| |_| |_| \___|/___|                                                                   

Namespace Negocio
  ''' <summary>
  ''' Clase de Envio
  ''' </summary>
  <Serializable()> _
  Public Class Envio : Inherits ObjetoBase
    Public Const ClassName As String = "Envio"
    
#Region " Recuperación desde la Base de Datos "
    ''' <summary>
    ''' Carga el objeto con los valores del registro de la base de datos con el id especificado.
    ''' </summary>
    ''' <param name="conexion"> Conexión que utilizará el objeto. </param>
    Public Function Carga(ByVal id As Integer) As Envio
      Using D__ As New Dal.Envios(ConnectionProvider, TransactionProvider)
        Dal.Loader.LoadObject(Me, D__.GetItem(id))
      End Using
      Return Me
    End Function
#End Region 

#Region " Grabar y Borrar "
    ''' <summary>
    ''' Constructor de instancias de proporcionando en <param ref="conexion" /> la conexión
    ''' </summary>
    ''' <param name="conexion"> Conexión que utilizará el objeto. </param>
    Public Function Save() As Envio
      Using D__ As New Dal.Envios(ConnectionProvider, TransactionProvider)        
        If _id = 0 Then
          _id = D__.Insert(IdEnvio, Observaciones, NumeroDeRegistro, FechaDeRegistro, Usuario)
        Else
          D__.Update(Id, IdEnvio, Observaciones, NumeroDeRegistro, FechaDeRegistro, Usuario)
        End If
        Return Me
      End Using
    End Function
    
    ''' <summary>
    ''' Constructor de instancias de proporcionando en <param ref="conexion" /> la conexión
    ''' </summary>
    Public Overrides Function DeleteString() As String
      Return "Confirma la eliminación del Envio: {0}"
    End Function
    
    ''' <summary>
    ''' Constructor de instancias de proporcionando en <param ref="conexion" /> la conexión
    ''' </summary>
    Public Sub Delete()
      Using D__ As New Dal.Envios(ConnectionProvider, TransactionProvider)
        D__.Delete(_id)        
      End Using  
    End Sub
#End Region   
  


#Region " Propiedades enlazadas a Datos "

    Private _id As Integer
    ''' <summary>
    ''' Obtiene o establece el Id del Envio
    ''' </summary>
    ''' <value>
    ''' La propiedad Id obtiene o establece el valor de la variable privada _id.
    ''' </value>
    Public Overrides Property Id() As Integer
      Get
        Return _id
      End Get
      Set(ByVal value As Integer)      
        _id = value
      End Set
    End Property

    Private _idEnvio As String
    ''' <summary>
    ''' Obtiene o establece el IdEnvio del Envio
    ''' </summary>
    ''' <value>
    ''' La propiedad IdEnvio obtiene o establece el valor de la variable privada _idEnvio.
    ''' </value>
    Public Property IdEnvio() As String
      Get
        Return _idEnvio
      End Get
      Set(ByVal value As String)      
        _idEnvio = value
      End Set
    End Property

    Private _observaciones As String
    ''' <summary>
    ''' Obtiene o establece el Observaciones del Envio
    ''' </summary>
    ''' <value>
    ''' La propiedad Observaciones obtiene o establece el valor de la variable privada _observaciones.
    ''' </value>
    Public Property Observaciones() As String
      Get
        Return _observaciones
      End Get
      Set(ByVal value As String)      
        _observaciones = value
      End Set
    End Property

    Private _numeroDeRegistro As String
    ''' <summary>
    ''' Obtiene o establece el NumeroDeRegistro del Envio
    ''' </summary>
    ''' <value>
    ''' La propiedad NumeroDeRegistro obtiene o establece el valor de la variable privada _numeroDeRegistro.
    ''' </value>
    Public Property NumeroDeRegistro() As String
      Get
        Return _numeroDeRegistro
      End Get
      Set(ByVal value As String)      
        _numeroDeRegistro = value
      End Set
    End Property

    Private _fechaDeRegistro As String
    ''' <summary>
    ''' Obtiene o establece el FechaDeRegistro del Envio
    ''' </summary>
    ''' <value>
    ''' La propiedad FechaDeRegistro obtiene o establece el valor de la variable privada _fechaDeRegistro.
    ''' </value>
    Public Property FechaDeRegistro() As String
      Get
        Return _fechaDeRegistro
      End Get
      Set(ByVal value As String)      
        Try
      _fechaDeRegistro = DateTime.Parse(value).ToString("dd/MM/yyyy")
    Catch e As Exception
      _fechaDeRegistro = ""
    End Try
      End Set
    End Property

    Private _fecha As String
    ''' <summary>
    ''' Obtiene o establece el Fecha del Envio
    ''' </summary>
    ''' <value>
    ''' La propiedad Fecha obtiene o establece el valor de la variable privada _fecha.
    ''' </value>
    Public Property Fecha() As String
      Get
        Return _fecha
      End Get
      Set(ByVal value As String)      
        Try
      _fecha = DateTime.Parse(value).ToString("dd/MM/yyyy")
    Catch e As Exception
      _fecha = ""
    End Try
      End Set
    End Property

    Private _usuario As String
    ''' <summary>
    ''' Obtiene o establece el Usuario del Envio
    ''' </summary>
    ''' <value>
    ''' La propiedad Usuario obtiene o establece el valor de la variable privada _usuario.
    ''' </value>
    Public Property Usuario() As String
      Get
        Return _usuario
      End Get
      Set(ByVal value As String)      
        _usuario = value
      End Set
    End Property

#End Region    

    ''' <summary>
    ''' Devuelve una cadena que representa al objeto
    ''' </summary>
    Public Overrides Function ToString() as String
      Return id.ToString() & " " & idEnvio.ToString() & " " & observaciones.ToString() & " " & numeroDeRegistro.ToString() & " " & fechaDeRegistro.ToString() & " " & fecha.ToString() & " " & usuario.ToString()
    End Function
    
    ''' <summary>
    ''' Devuelve una cadena que representa al objeto
    ''' </summary>
    ''' <param name="customFormat"> Indica se se realizará el formato personalizado </param>
    Public Overrides Function ToString(ByVal customFormat As Boolean) As String      
      Return Me.ToString()
    End Function
    
    ''' <summary>
    ''' Constructor de instancias de proporcionando en <param ref="conexion" /> la conexión
    ''' </summary>
    '''<example>
    ''' <c>
    ''' With New Envio()
    '''   DoSomething(.this())
    '''   .DoPrint()
    ''' End With
    ''' </c>
    ''' </example>
    Public Shadows Function this() As Envio
      Return Me
    End Function

#Region " Cargador del objeto "
    ''' <summary>
    ''' Constructor de instancias de proporcionando en <param ref="conexion" /> la conexión
    ''' </summary>
    Private Shared Sub InitDataBinder()
      If _DataBinder Is Nothing Then
        With New Dal.ObjectLoader()
          _DataBinder = .this
          .Add(New Dal.BindItem("id", "Id", 0,GetType(Integer)))
          .Add(New Dal.BindItem("idEnvio", "IdEnvio", 1,GetType(String)))
          .Add(New Dal.BindItem("observaciones", "Observaciones", 2,GetType(String)))
          .Add(New Dal.BindItem("numeroDeRegistro", "NumeroDeRegistro", 3,GetType(String)))
          .Add(New Dal.BindItem("fechaDeRegistro", "FechaDeRegistro", 4,GetType(Date)))
          .Add(New Dal.BindItem("fecha", "Fecha", 5,GetType(Date)))
          .Add(New Dal.BindItem("usuario", "Usuario", 6,GetType(String)))       
        End with
      End If
    End Sub

    Shared _DataBinder As Dal.ObjectLoader = Nothing
    
    ''' <summary>
    ''' Constructor de instancias de proporcionando en <param ref="conexion" /> la conexión
    ''' </summary>
    ''' <param name="conexion"> Conexión que utilizará el objeto. </param>
    Public Overrides Function GetBinder() As Dal.ObjectLoader
      InitDataBinder()
      Return _DataBinder
    End Function
#End Region
  
  End Class
End Namespace

Negocio.Core.TransactionHelper.vb

Namespace Negocio

  Public Class TransactionHelper : Implements IDisposable

    Public Sub New(ByVal connection As IDbConnection)
      _Connection = connection
    End Sub

    Public Sub BeginTransaction()
      _Transaction = Dal.DataServer.CreateTransaction(_Connection)
    End Sub

    Public Sub Commit()
      Dal.DataServer.Commit(_Transaction)
    End Sub
    Public Sub RollBack()
      Dal.DataServer.Rollback(_Transaction)
    End Sub

    Private _Connection As IDbConnection
    Public Function Connection() As IDbConnection
      Return _Connection
    End Function

    Private _Transaction As IDbTransaction
    Public Function Transaction() As IDbTransaction
      Return _Transaction
    End Function

    Public ReadOnly Property ConnectionProvider() As Dal.ConnectionProviderDelegate
      Get
        Return AddressOf Connection
      End Get
    End Property

    Public ReadOnly Property TransactionProvider() As Dal.TransactionProviderDelegate
      Get
        Return AddressOf Transaction
      End Get
    End Property

#Region " IDisposable "
    Private _disposedValue As Boolean = False
    Protected Overridable Sub Dispose(ByVal disposing As Boolean)
      If Not Me._disposedValue Then
        If disposing Then

        End If
        Connection.Close()
        Connection.Dispose()
      End If
      Me._disposedValue = True
    End Sub
    Public Sub Dispose() Implements IDisposable.Dispose
      Dispose(True)
      GC.SuppressFinalize(Me)
    End Sub
#End Region


  End Class

End Namespace

Negocio.Core.ObjetoBase.vb

Namespace Negocio

  <Serializable()> _
  Public MustInherit Class ObjetoBase : Implements IExportable : Implements Dal.ILoader
    Public MustOverride Property Id() As Integer
    Public MustOverride Overloads Function ToString(ByVal CustomFormat As Boolean) As String

    Public Sub New()
    End Sub

    Public Overridable Function DeleteString() As String
      Return "Confirma la eliminacion del elemento: {0}"
    End Function


#Region " Interface ILoader "
    <NonSerialized()> _
    Private _ConnectionProvider As Dal.ConnectionProviderDelegate = Nothing
    <Xml.Serialization.XmlIgnore()> _
    Public Property ConnectionProvider() As Dal.ConnectionProviderDelegate Implements Dal.ILoader.ConnectionProvider
      Get
        Return _ConnectionProvider
      End Get
      Set(ByVal value As Dal.ConnectionProviderDelegate)
        _ConnectionProvider = value
      End Set
    End Property

    <NonSerialized()> _
    Private _TransactionProvider As Dal.TransactionProviderDelegate = Nothing
    <Xml.Serialization.XmlIgnore()> _
    Public Property TransactionProvider() As Dal.TransactionProviderDelegate Implements Dal.ILoader.transactionProvider
      Get
        Return _TransactionProvider
      End Get
      Set(ByVal value As Dal.TransactionProviderDelegate)
        _TransactionProvider = value
      End Set
    End Property

    Public Overridable Function GetBinder() As Dal.ObjectLoader Implements Dal.ILoader.GetObjectLoader
      Return Nothing
    End Function
#End Region

    Public Overridable Function ToXml() As String Implements IExportable.FormatoXml
      With New Xml.Serialization.XmlSerializer(Me.GetType)
        Dim _SWriter As New IO.StringWriter()
        .Serialize(_SWriter, Me)
        Dim xml As String = _SWriter.ToString()
        With New System.Xml.XmlDocument()
          .LoadXml(xml)
          Return .DocumentElement.InnerXml
        End With
      End With
    End Function

    Public Overridable Function ToCSV(ByVal separador As String) As String Implements IExportable.FormatoCsv
      With New Xml.Serialization.XmlSerializer(Me.GetType)
        Dim _SWriter As New IO.StringWriter()
        .Serialize(_SWriter, Me)
        With New System.Xml.XmlDocument()
          .LoadXml(_SWriter.ToString())
          Dim s As New System.Text.StringBuilder
          For i As Integer = 0 To .ChildNodes(1).ChildNodes.Count - 1
            s.Append(.ChildNodes(1).ChildNodes(i).InnerText.Replace(separador, ".") & separador)
          Next
          s.Remove(s.Length - 1, 1)
          Return s.ToString
        End With
      End With
    End Function

    Public Overridable Function HeaderCSV(ByVal separador As String) As String Implements IExportable.HeaderCsv
      With New Xml.Serialization.XmlSerializer(Me.GetType)
        Dim _SWriter As New IO.StringWriter()
        .Serialize(_SWriter, Me)
        With New System.Xml.XmlDocument()
          .LoadXml(_SWriter.ToString())
          Dim s As New System.Text.StringBuilder
          For i As Integer = 0 To .ChildNodes(1).ChildNodes.Count - 1
            s.Append(.ChildNodes(1).ChildNodes(i).Name & separador)
          Next
          s.Remove(s.Length - 1, 1)
          Return s.ToString
        End With
      End With
    End Function

    Public Function this() As ObjetoBase
      Return Me
    End Function

    <NonSerialized()> _
    Private _Tag As Object = Nothing
    <Xml.Serialization.XmlIgnore()> _
    Public Property Tag() As Object
      Get
        Return _Tag
      End Get
      Set(ByVal Value As Object)
        _Tag = Value
      End Set
    End Property

    Public Shared Function ToBase64XmlString(Of T)(ByVal input As T) As String
      If input Is Nothing Then Return Nothing
      Dim writer As New IO.MemoryStream()
      Dim formatter As Xml.Serialization.XmlSerializer = New Xml.Serialization.XmlSerializer(input.GetType)
      formatter.Serialize(writer, input)
      Return Convert.ToBase64String(writer.ToArray)
    End Function

    Public Shared Function FromBase64XmlString(Of T)(ByVal input As String) As T
      If input Is Nothing Or input.Length = 0 Then Return Nothing
      Dim formatter As Xml.Serialization.XmlSerializer = New Xml.Serialization.XmlSerializer(GetType(T))
      Return formatter.Deserialize(New IO.MemoryStream(Convert.FromBase64String(input)))
    End Function


#Region " Compresion "


    Public Shared Function ToBase64XmlStringCompress(Of T)(ByVal input As T) As String
      If input Is Nothing Then Return Nothing
      Dim writer As New IO.MemoryStream()
      Dim formatter As Xml.Serialization.XmlSerializer = New Xml.Serialization.XmlSerializer(input.GetType)
      'Dim formatter As Runtime.Serialization.Formatters.Binary.BinaryFormatter = New Runtime.Serialization.Formatters.Binary.BinaryFormatter()
      formatter.Serialize(writer, input)
      writer.Position = 0
      Return Convert.ToBase64String(Compress(writer))
    End Function

    Public Shared Function FromBase64XmlStringCompress(Of T)(ByVal input As String) As T
      If input Is Nothing Or input.Length = 0 Then Return Nothing
      Dim formatter As Xml.Serialization.XmlSerializer = New Xml.Serialization.XmlSerializer(GetType(T))
      'Dim formatter As Runtime.Serialization.Formatters.Binary.BinaryFormatter = New Runtime.Serialization.Formatters.Binary.BinaryFormatter()
      Return formatter.Deserialize(New IO.MemoryStream(Decompress(New IO.MemoryStream(Convert.FromBase64String(input)))))
    End Function

    Private Shared Function Compress(ByVal stream As IO.Stream) As Byte()
      Using resultStream As IO.MemoryStream = New IO.MemoryStream()
        Using writeStream As IO.Compression.GZipStream = New IO.Compression.GZipStream(resultStream, IO.Compression.CompressionMode.Compress, True)
          CopyBuffered(stream, writeStream)
        End Using
        Return resultStream.ToArray()
      End Using
    End Function

    Private Shared ReadOnly bufferSize As Integer = 10000
    Private Shared Sub CopyBuffered(ByVal readStream As IO.Stream, ByVal writeStream As IO.Stream)
      Dim bytes(bufferSize) As Byte
      Dim byteCount As Integer = readStream.Read(bytes, 0, bytes.Length)
      While byteCount <> 0
        writeStream.Write(bytes, 0, byteCount)
        byteCount = readStream.Read(bytes, 0, bytes.Length)
      End While
    End Sub

    Private Shared Function Decompress(ByVal stream As IO.Stream) As Byte()
      Using decompressedStream As IO.Stream = New IO.Compression.GZipStream(stream, IO.Compression.CompressionMode.Decompress, False)
        Using resultStream As IO.MemoryStream = New IO.MemoryStream()
          CopyBuffered(decompressedStream, resultStream)
          Return resultStream.ToArray()
        End Using
      End Using
    End Function
#End Region


  End Class

End Namespace