El Blog de RCastro
Que el diseño inicial no te hipoteque
viernes, 2 de diciembre de 2011
Ha nacido GLYPY The Hero
Todo empezó como una simple prueba con XNA y C# pero al cabo de algún tiempo se ha convertido en un juego completo, de principio a fin.
No creo que me haga rico con esto de los juegos pero me estoy divirtiendo mucho en el camino. :-)
GLYPY The Hero es un típico juego de plataformas en el que se “aplastan” bicharracos y se recogen cosas. Aquí os dejo una captura de pantalla para que os hagáis una idea.
Aquí dejo el enlace por si queréis probarlo.
lunes, 12 de enero de 2009
Un ejemplo práctico y III
En esta tercera y última entrega del ejemplo práctico pretendo:
- Teorizar sobre los diferentes enfoques al problema de carga de entidades en un diseño orientado a objetos.
- Profundizar en el diseño de la clase DataLoader y en sus mecanismos de expansión.
En el día a día de un desarrollador es habitual la utilización de tablas de Código-Descripción. Con ellas se ahorra espacio ya que no se duplica la información, se normalizan los datos, etc. Lo que todos ya sabemos. Los problemas con el manejo de estas entidades comienzan cuando en nuestro sistema existen entidades con muchos "códigos" de "tablas maestras", claves externas vamos.
¿Cómo obtenemos estos valores relativos a estas claves?. ¿Utilizamos sentencias INNER JOIN?. ¿Realmente y vamos a necesitar cargar todos estos valores en todos los escenarios?. Abordar estas preguntas, a mi modo de ver, es crucial a la hora de diseñar un sistema para que sea AGIL y TOLERANTE a los cambios. Esto último es importantísimo en proyectos grandes en los que, como todos sabemos, los cambios son constantes y no hay que romper lo que ya funciona.
Voy a intentar poner un ejemplo sencillo: supongamos que tenemos la típica entidad Empleado, que tiene, además de los campos habituales, un código de departamento, un código de categoría, un código de Responsable y una lista de Proyectos en los que participa. A la hora de cargar un objeto Empleado la implementación más habitual es la de lanzar una sentencia SQL con un INNER JOIN de las diferentes tablas. Es obvio, que si un caso de uso concreto dice que solo tengo que listar el Nombre y el Departamento, estaremos solicitando y transmitiendo más datos de los que realmente necesitamos.
Con este modelo "INNER JOIN" si posteriormente se añade otra clave externa, por ejemplo TipoDeHorario, habrá que modificar la sentencia SQL para que el método Carga() llene TODAS las propiedades. Este cambio en el modelo implicará una penalización en el rendimiento de otros métodos ya implementados que funcionaban correctamente.
Si cada cambio que se realice en el modelo afecta al rendimiento de lo ya existente nuestro sistema NO ES AGIL ni TOLERANTE a cambios. Con unos cuantos cambios como estos, a posteriori, y con este enfoque, el sistema dejará rápidamente de comportarse como venía haciéndolo y lo que es peor: de como el usuario ya está acostumbrado.
Por lo tanto y como ya he escrito en otra entrada:
“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 algún mecanismo de cacheo. 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 la tabla Provincias, en otro escenario con la tabla Facturas donde no interesa en absoluto la información de las anteriores.”
No estoy en contra de la utilización del método INNER JOIN, al contrario, muchas veces es el que hay que utilizar. Lo que planteo es que, antes de nada, es necesario un análisis exhaustivo de donde se donde se va a utilizar y de como afectará esto a a posibles futuras modificaciones.
Otra decisión importante que tenemos que tomar a la hora de cargar un objeto, es "Cuando" se cargarán las entidades relacionadas y las descripciones de las propiedades de las que solo tenemos su clave externa. En nuestro ejemplo, simple el, solamente existe una: La lista de Proyectos. En una aplicación del mundo real podría haber, además, una lista de direcciones, una lista de teléfonos, etc. ¿Cómo abordamos este problema? ¿Cargamos todas las entidades en el método carga()?. De este modo estaríamos en disposición de obtener cualquier valor inmediatamente después de su invocación, pero de nuevo en la implementación del caso de uso Listar Nombre y Departamento estaríamos haciendo más cosas de las estrictamente necesarias.
Nota importante: Existen escenarios en que este enfoque es el correcto ya que SIEMPRE que se carga una entidad se van a utilizar/consultar sus entidades relacionadas. De no ser así es mas aconsejable utilizar mecanismos de Carga Perezosa (Lazy Load). De esta forma las cosas se cargan cuando realmente se necesitan.
A continuación intentaré mostrar como realizar la carga de objetos para evitar, de algún modo, los problemas expuestos anteriormente.
Carga de un valor desde una tabla maestra.
En ocasiones es necesario mostrar la descripción de una propiedad en lugar del código o clave externa. Por ejemplo, supongamos que ahora necesitamos especificar un Tipo para la entidad Envio que ya conocemos de otras entradas del blog. Los tipos pueden ser: Local = 0, Nacional = 1 e Internacional = 2. Podríamos implementarlo de la siguiente forma:
Private _idTipo As Integer Public Property IdTipo() As Integer Get Return _idTipo End Get Set(ByVal value As Integer) _idTipo = value End Set End Property Private _TipoDeEnvio As String = "" Public ReadOnly Property TipoDeEnvio() As String Get Return _TipoDeEnvio End Get End Property
Con un enfoque tradicional del problema primero realizaríamos un INNER JOIN en la sentencia SQL, cambiaríamos el cargador del objeto y lo tendríamos. Lo MALO de este enfoque es que penalizamos el código ya existente que no necesita el tipo de envio. Por lo tanto es necesario hacernos las siguientes preguntas: ¿Es en todos los escenarios estrictamente necesaria la carga del Tipo de envio? ¿Lo será en futuros escenarios o casos de uso?. Lo más normal es dar un NO como respuesta estas dos preguntas.
Tendremos, por tanto, que mejorar el mecanismo de carga de este tipo de propiedades. Una primera aproximación puede ser la de cachear el valor. Lo vemos con el siguiente código:
Public ReadOnly Property TipoDeEnvio() As String Get If _TipoDeEnvio.Length = 0 Then _TipoDeEnvio = New TipoDeEnvio().Carga(_idTipo).Descripcion End If Return _TipoDeEnvio End Get End Property
Si observamos con detenimiento esta nueva implementación nos daremos cuenta de que no interfiere con la anterior y de que en el caso de seguir cargando el objeto con INNER JOIN la creación y carga de un objeto TipoDeEnvio no será necesaria. Es muy importante, repito, que las modificaciones no rompan ni incidan en el rendimiento de lo que ya funciona.
Esta patrón puede ser interesante para la implementación de algunos casos de uso sencillos. Por ejemplo, si queremos abrir un solo envio y conocer su tipo. Necesitaremos, entonces, 2 accesos a la capa de Datos.
En el caso de que queramos, por el contrario, listar todos los envios de un mes cualquiera y mostrar su tipo, el patrón anterior no será el más adecuado, realizará una consulta a la base de datos por cada envio. No es del todo malo, ya que en una etapa inicial del desarrollo puede interesar más mostrar la futura funcionalidad, que el RENDIMIENTO.
Una versión mejorada puede hacer uso de mecanismos de cacheo globales o incrementales.
Public ReadOnly Property TipoDeEnvio() As String Get If _TipoDeEnvio.Length = 0 Then If Helper.CacheDeTipos Is Nothing Then _TipoDeEnvio = New TipoDeEnvio().Carga(_idTipo).Descripcion Else _TipoDeEnvio = Helper.CacheDeTipos.Item(_idTipo).Descripcion End If End If Return _TipoDeEnvio End Get End Property
Se puede apreciar como esta última implementación “permitirá” el correcto funcionamiento del objeto independientemente del contexto en el que se utilice y de como se haya inicializado.
Carga de Entidades relacionadas
En ocasiones me he encontrado con código heredado en el que los objetos del tipo Cliente se cargaban en el constructor junto con una colección de TODAS las facturas del mismo, para finalmente mostrar por pantalla sencillamente la Localidad y la Provincia. Inadmisible.
Hemos de preguntarnos, como hicimos anteriormente, si es estrictamente necesario cargar todas, algunas o ninguna de las entidades relacionadas. Existirán casos en los que no haya más remedio que cargar todas las entidades y otros en los que habrá que dotar al objeto de mecanismos que le permitan ser usado de una forma Flexible.
Como norma general en el diseño de los objetos se establece que los métodos de carga recuperarán solamente los valores comunes a todos los casos de uso, es decir, los de la Tabla de la Base de Datos correspondiente a la entidad. Los demás valores o entidades relacionadas deberían ser cargadas bajo petición.
Un patrón de implementación de la propiedad Documentos es la siguiente:
Private _Documentos As Anexos = Nothing Public ReadOnly Property Documentos() As Anexos Get If _Documentos Is Nothing Then _Documentos = New Anexos().Carga(Me.IdEnvio) End If Return _Documentos End Get End Property
podremos utilizar el objeto de la siguiente forma:
. . With New Envio().Carga(350) Debug.WriteLine(.this.IdEnvio) For Each a As Anexo In .this.Documentos Debug.WriteLine(a.FileName) Next End With . .
De esta forma no existirá penalización en el rendimiento cuando solamente queramos cambiar, por ejemplo la fecha de un envio ya que los Documentos SOLO son cargados cuando se solicitan.
Si requisitos posteriores indican, por ejemplo, la necesidad de incluir una lista de Destinatarios del envio, bastará con realizar la implementación de la propiedad Destinatarios de la misma forma que antes:
Private _Destinatarios As Destinatarios Public ReadOnly Property Destinatarios() As Destinatarios Get If _Destinatarios Is Nothing Then _Destinatarios = New Destinatarios().Carga(Me.IdEnvio) End If Return _Destinatarios End Get End Property
La ejecución de la nueva funcionalidad no interfiere en absoluto con la funcionalidad anterior consistente en listar los nombres de los documentos anexos.
With New Envio().Carga(350) Debug.WriteLine(.this.IdEnvio) For Each a As Destinatario In .this.Destinatario Debug.WriteLine(a.Nombre) Next End With
Lo que perseguimos con patrones como estos es que los objetos se comporten como se espera que lo hagan y sobre todo, mantener está coherencia y efectividad aun cuando los requisitos cambien (Que cambiarán).
Al principio y cuando estamos acostumbrados a la carga INNER JOIN todo lo anterior nos parece que es complicar las cosas, porque, si me puedo traer toda la información de una sola vez, ¿Para qué montar tanto lio?. La respuesta es: Porque no sabemos cuales serán los requisitos en el en futuro, porque no queremos "Que el diseño inicial nos hipoteque el sistema", porque queremos desarrollos ágiles y porque queremos a toda costa evitar sentencias SLQ de más de 30 líneas que difícilmente entenderemos pasados cinco minutos.
Pensemos en que los grandes sistemas existentes, incluso los vivos, están compuestos por la agregación de pequeñas partes: bloques, ladrillos, células ... Lo complejo aquí es el sistema no sus componentes. Diseñemos, púes, componentes pequeños, sencillos, pensados para su expansión futura y, sobre todo, fácilmente comprensibles tanto en su funcionamiento individual como cuando interactúan con otros para formar el TODO.
El DataLoader y sus mecanismos de expansión.
Como hemos visto anteriormente los objetos de un sistema deben ser cargados de forma efectiva y coherente en sus todos y cada uno de los escenarios en donde son utilizados. Para ello debemos disponer de unos mecanismos de carga lo suficientemente flexibles para realizar con éxito esta tarea.
En el Modelo que propongo esta responsabilidad se le ha asignado a los propios objetos. No obstante se podría, por ejemplo, crear una clase “BindingManager” que realizara la tarea de gestionar los “cargadores” de los objetos.
Los objeto de la capa de negocio, heredando y sobrescribiendo, implementan el siguiente interfaz:
Public Interface ILoader Function GetObjectLoader() As ObjectLoader Property ConnectionProvider() As Dal.ConnectionProviderDelegate Property transactionProvider() As Dal.TransactionProviderDelegate End Interface
Como ya he dicho anteriormente los objetos inicialmente son cargados con sus valores mas “usados” y a la vez los mas “fácilmente” accesibles. Por lo tanto para este escenario el objeto proporciona a través de la interfaz ILoader su “cargador genérico”. Este cargador, normalmente, traspasa los valores de la base de datos (DataReader) a las variables privadas de las propiedades del objeto. El siguiente código muestra el cargador generico de la clase Envio:
Public Class Envio : Inherits ObjetoBase . . Private Shared Sub InitDataBinder() If _DataBinder Is Nothing Then _DataBinder = New Dal.ObjectLoader() With _DataBinder .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 Public Overrides Function GetBinder() As Dal.ObjectLoader InitDataBinder() Return _DataBinder End Function . . End Class
Este cargador está diseñado para trabajar con un DataReader con 6 campos que son los que actualmente devuelve el siguiente método de la capa de Datos:
Public Function GetItem(ByVal id As Integer) As IDataReader Using cmd As IDbCommand = CreateCommand() cmd.CommandText = String.Format("Select * from [Envio] where Id={0}", id) Return DataServer.ExecuteReader(cmd) End Using End Function
El responsable de la carga es el método LoadObject de la clase DataLoader que devuelve el objeto cargado con los valores de un DataReader:
Public Class Envio : Inherits ObjetoBase . . 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 . .
Hasta aquí hemos visto cual es la forma, genérica, en la que los objetos son cargados. Creo que es el momento de aclarar que en la actual implementación de los métodos de carga se generan dinámicamente funciones de carga de acuerdo a la información especificada en los ObjectLoader de los objetos. Esta generación dinámica de código intermedio se realiza una sola vez por tipo de cargador y almacenada como un delegado en el mismo ObjectLoader. El mecanismo de generación supone que existe una variable privada por cada bindItem cuyo nombre va precedido de “_”. Esta variable recibirá el valor del campo del DataReader cuyo índice se indica en el BindItem. Veamos una ejemplo de lo que digo:
en el cargador del objeto
.Add(New Dal.BindItem("id", "Id", 0, GetType(Integer)))
en la propiedad
. . Private _id As Integer Public Property Id() As Integer . .
Nota: El segundo parámetro del constructor de BindItem actualmente no se usa. Es el nombre del campo en la tabla de la base de datos. Por rendimiento el acceso es por el indice de columna, el tercer parámetro.
A partir de ahora veremos como el Modelo nos permite especificar que valores cargar.
Supongamos que en un determinado caso de uso necesitamos conocer el Id, el Id del envio y el usuario que lo realizó. A la vista del código que ya tenemos utilizamos el método genérico de carga para implementar dicho caso de uso. Bueno, funciona, y en un primer estadío del desarrollo nos sirve para mostrar la funcionalidad del sistema al cliente pero sin entrar en cuestiones de rendimiento. El cliente, sin que sirva de precedente, nos indica que “Que si, que le gusta la funcionalidad” pero que estima que “el tiempo de respuesta debe ser bastante menor”. En este momento, o una vez que detectamos un punto en el que el rendimiento es importante, pasaríamos a la optimización del código de carga para este caso particular.
La primera optimización que podemos hacer es la creación en la capa de datos de un método que nos devuelva un DataReader únicamente con los columnas necesarias:
Public Function GetItemsExt() As IDataReader Using cmd As IDbCommand = CreateCommand() cmd.CommandText = "Select Id,IdEnvio,Usuario from [Envio]" Return DataServer.ExecuteReader(cmd) End Using End Funtion
A continuación deberemos crear un método que pueda ser usado para implementar el exigente Caso de Uso. Este método debe especificar cuales son las propiedades que se van a cargar. Hasta ahora hemos visto que el cargador de objetos necesita un objeto y un DataReader con los valores de las propiedades para realizar su trabajo. Lo que no vemos tan fácilmente es que necesita también un cargador del objeto ya que este es solicitado al objeto si no se proporciona. De todas formas, y este es un típico mecanismo de expansión del diseño, existen versiones sobrecargadas de los métodos de carga: LoadObject y LoadObjects que aceptan un Cargador Personalizado:
Public Shared Sub LoadObject(Of T As {Class, New, ILoader})(ByVal Target As T, ByVal dr As IDataReader, ByVal Loader_ As ObjectLoader) Public Shared Sub LoadObject(Of T As {Class, New, ILoader})(ByVal Target As T, ByVal dr As IDataReader) Public Shared Function LoadObjects(Of T As {Class, New, ILoader})(ByVal Target As Generic.List(Of T), ByVal dr As IDataReader) As Generic.List(Of T) Public Shared Function LoadObjects(Of T As {Class, New, ILoader})(ByVal Target As Generic.List(Of T), ByVal dr As IDataReader, ByVal Loader_ As ObjectLoader) As Generic.List(Of T)
En el siguiente ejemplo el cargador se crearía en cada llamada, no obstante vemos claramente que este es un punto en el que se podrían aplicar mecanismos de cacheo de cargadores específicos. No lo hago aquí por simplicidad.
Public Function CargaExt() As Envios Using D__ As New Dal.Envios(ConnectionProvider, TransactionProvider) Dim customLoader As New Dal.ObjectLoader With customLoader .Add(New Dal.BindItem("id", "Id", 0, GetType(Integer))) .Add(New Dal.BindItem("idEnvio", "IdEnvio", 1, GetType(String))) .Add(New Dal.BindItem("usuario", "Usuario", 2, GetType(String))) End With Return Dal.Loader.LoadObjects(Of Envio)(Me, D__.GetItemsExt(), customLoader) End Using End Function
Bueno pues esto es todo por hoy. Hemos visto la forma generica de cargar los objetos y como, en casos específicos, es necesaria una carga especifica y optimizada de valores para obtener unos rendimientos altos.
Espero que todo esto sirva, al menos, para daros algunas ideas.
Saludos y hasta la próxima.
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.
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.
.
Un ejemplo práctico II.
En esta segunda entrega del ejemplo práctico pretendo mostrar:
- Las posibilidades de obtener las trazas de toda consulta realizada en la capa DAL.
- Las diferentes formas de inicialización, carga y utilización de los objetos.
- La implementación de transacciones en el modelo.
- 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