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.
Vamos 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 SubA 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
Las transacciones en el Modelo
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
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.
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
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
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
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:
Realmente fabuloso !!! Gracias
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.
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
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 !!!
Publicar un comentario