sábado 28 de febrero de 2009

El DataSource extendido. Parte 1.

¡Hola a todos!

Cheché ha elaborado otro video ilustrativo de los componentes que he venido desarrollando en los últimos años. En este caso se trata del componente TMagiaDataSource, el cual forma parte de la suite Magia Data.



En términos francos, este componente viene a resolver dos antiguos problemas que durante años hemos tenido muchos programadores Delphi a la hora de crear algunas de nuestras aplicaciones de bases de datos. El video anterior muestra cómo el nuevo componente soluciona ambos problemas, pero intentaré describir éstos con detalle.

Supongamos que dentro de un módulo de datos tenemos un componente data set (en español conjunto de datos), el cual puede ser de cualquier clase (TQuery, TTable, TIBQuery, TADOQuery, TClientDataSet, TpFIBDataSet, TMDOQuery, TkbmMemTable, TOraQuery, etc.) y queremos asociar ese conjunto de datos a los controles (componentes visuales) de un formulario. Para esto, normalmente agregamos un componente TDataSource que sirve como canal de comunicación entre el conjunto de datos y los controles de datos que están presentes en el formulario. El componente TDataSource puede ser colocado en el módulo de datos, junto al data set asociado, o bien dentro del propio formulario.

Hasta ahí todo es RADmente bello, lo reconozco desde aquel glorioso otoño boreal de 1997 en que comencé a conocer Delphi. Pero analicemos las complicaciones que suelen surgir en la práctica.

Problema número 1: Los formularios tienden a “apropiarse” de los conjuntos de datos.

Si colocamos un componente conjunto de datos dentro de un data module es porque pensamos centralizar dentro de ese contenedor la tabla, grupo de tablas o acción que representa el componente. De tal manera que quede disponible al uso que diferentes formularios y procesos quieran hacer de él. En términos de programación escrita, es como declarar una nueva constante o tipo de dato para que pueda ser utilizada por diferentes rutinas del programa, sin tener que repetir la declaración o valor literal de dicho elemento global en cada una de éstas, bastando una mera referencia a su nombre (identificador).

Pero un conjunto de datos no es algo fijo, es un objeto con un montón de propiedades, métodos y eventos. Algo que puede cambiar conforme se le utiliza. Entonces diríamos que, en términos de programación escrita, más bien es como una variable global, y los formularios son como las rutinas que usan y modifican esa variable global.

Sólo que existe una gran diferencia que carboniza a esa analogía. Una rutina que modifica el valor de una variable global lo hace conscientemente, asumiendo que eso afectará a las demás rutinas que la empleen. Hay cierta armonía y respeto hacia la esencia de una variable global, ésta existe para ser un contenedor global de datos. Pero con frecuencia colocamos data sets en el interior de un data module, no para hacer de ellos unas especies de variables globales, sino para definirlos en un solo punto, estableciendo en tiempo de diseño las propiedades y eventos que deberán observar los conjuntos de datos cuando vayan a ser utilizados por algún proceso o formulario.

El código asociado a un formulario suele parametrizar, abrir y cerrar conjuntos de datos, cambiando varias de sus propiedades, como Active, Params, ReadOnly, IndexFieldNames, Filter, Filtered, etc. Es un formulario particular que altera a un objeto globalmente definido. ¿Qué debe hacer un formulario así antes de cerrar, cuando ya ha terminado de trabajar con el conjunto de datos? ¿Debe dejarlo tal cual está? Deberá cerrarlo, dirán algunos. Correcto, ¿pero que hay de todas aquellas propiedades y parámetros que el formulario ya le estableció o modificó al componente? ¿Abandonamos la playa cual spring breaker?

En muchas ocasiones, si hay libertad de abrir por segunda vez el formulario (creando una nueva instancia o simplemente haciéndolo visible), éste o su código debe contemplar que el conjunto de datos (que se encuentra en un data module) pudo sufrir modificaciones anteriormente. ¿Acaso el formulario debe deshacer esas modificaciones antes de empezar a usar el conjunto de datos, o debería deshacerlas al terminar de usarlo, digamos, al destruirse la instancia del formulario? De ahí surge la idea de que a veces sería conveniente que cada vez que un formulario se creara o abriera, los conjuntos de datos que utiliza se pusieran en automático tal como fueron definidos en tiempo de diseño.

Una salida falsa es remover el data set del módulo de datos y colocarlo dentro del propio formulario. Así cada vez que se cree una instancia del formulario obtendremos también una nueva instancia del conjunto de datos, tal como fue definido en tiempo de diseño. Pero ésta no es una solución efectiva porque a la larga terminamos “involucionando” (en Delphi 1 no existían los data modules). Al hacer eso, nuestro conjunto de datos deja de ser un objeto globalmente definido, ya no es algo que esté elegantemente disponible para toda la aplicación. Si después agregamos otros formularios que necesiten del mismo conjunto de datos, terminaremos con varias copias de éste regadas por todos lados. Y un mal sueño se materializará cuando haya que hacerles algún cambio, por modificaciones a la estructura de una tabla, al formato de despliegue de algunos de sus campos, etc.

No nos perdamos, los conjuntos de datos viven mejor en los módulos de datos.

Pero la responsabilidad inherente a que un formulario modifique algunos parámetros o propiedades del conjunto de datos es sólo la arista de un gran cubo de hielo. Hay algo todavía más gordo: a veces necesitamos usar la misma tabla o consulta en dos puntos distintos de la aplicación, pero simultáneamente. Eso es común con formularios múltiples no modales que presentan información extraída con la misma consulta SQL, pero estableciendo parámetros o filtros distintos. Naturalmente, esto no es viable mediante el uso de un solo data set. Y aún en casos más sencillos, donde se quiera o se tolere que dos o más formularios abiertos muestren el mismo cursor, resulta que la navegación en uno de los formularios afectará a lo que el otro esté mostrando (imagine, por ejemplo, que hay una rejilla en cada uno, y el usuario cambia de fila en una de ellas; la otra rejilla también cambiará).

Ante esto, otra salida falsa es acompañar a cada instancia de un formulario con una instancia particular de un módulo de datos que contenga solo los data sets que el formulario requiera. A mediano plazo, la aplicación terminará con una cantidad de módulos de datos difícil de administrar y mantener, duplicando en tiempo de diseño muchos conjuntos de datos (algo que se buscaba evitar) y complicando las relaciones entre ellos. O habrá quien sacrifique una buena cantidad de memoria creando nuevas instancias de un gran módulo de datos, para formularios que solamente utilizan un par de data sets.

Estas dos salidas falsas van en dirección a lo que sería un modelo ideal, pero no lo consiguen. Ese modelo consiste en que un conjunto de datos pueda ser definido en tiempo de diseño en un solo punto global, pero que en tiempo de ejecución puedan crearse tantas copias locales de él como resulte necesario. Es decir, que el conjunto de datos definido en tiempo de diseño sea un objeto patrón, del cual puedan crearse clones en tiempo de ejecución.

Pues, señores, eso es lo que obtenemos con la propiedad DataSetCloned del componente TMagiaDataSource. Me cito a mi mismo con este texto de la referencia técnica:

En tiempo de ejecución, cuando esta propiedad tiene un valor de True, el componente toma el conjunto de datos que tenga o sea asignado a la propiedad DataSet, y crea una copia del mismo. Todas las propiedades, eventos y campos definidos del primer conjunto de datos serán establecidos de igual manera en el objeto clon. Y éste será el nuevo conjunto de datos asignado al componente. Esto permite establecer en tiempo de diseño varias fuentes de datos apuntando al mismo conjunto de datos, pero logrando que en tiempo de ejecución cada una use un objeto distinto para evitar conflictos entre formularios o procesos. Es una excelente alternativa a la tradicional y molesta solución de establecer dos o más conjuntos de datos similares en tiempo de diseño, para satisfacer necesidades que podrían tener conflicto si emplearan el mismo componente de datos. Su valor predeterminado es False.

Como se observa en el video, hay dos formularios que muestran datos, cada uno con un componente TMagiaDataSource en su interior, los cuales están enlazados al mismo conjunto de datos patrón en tiempo de diseño (sus propiedades DataSet apuntan al mismo objeto). Pero se les ha puesto la propiedad DataSetCloned en True para que en tiempo de ejecución cada formulario opere con su propia consulta. O sea que cada DataSource usará realmente un clon del objeto asignado en la propiedad DataSet.

En la siguiente entrada describiré el segundo problema y cómo es resuelto con la otra nueva propiedad del componente (DataSetEvents). Por lo pronto el video de arriba les habrá dado una idea de las grandes ventajas logradas con ambas.

Un abrazo extendiendo a Delphi.

Al González. :)

8 comentarios:

Anónimo dijo...

Hola Al,
Soy Delphius.
Recuerdo que en ClubDelphi se han tocado estas cuestiones técnicas que mencionas sobre "Modulo vs Forms"... es más en algunas participé yo.

Nunca me había quedado del todo claras las ideas que exponían, pero ahora leyendo tus fantásticas explicaciones y junto con el video comprendo mejor lo que decías tu, roman y otros. Y de paso se me aclaró la duda del hilo que abriste preguntando sobre la propiedad.

tu si que eres un buen maestro.
Estaré esperando las nuevas clases :)

Saludos,

Andrés dijo...

Hola Al.

Me ha parecido muy interesante la explicación pero sobre todo la iniciativa de clonar "al vuelo" el DataSet origen. Un buen programador, a la hora de sortear un problema no escatima en tiempo / trabajo / quebraderos de cabeza si la solución final es la más satisfactoria, y ésta parece serlo para el problema que se plantea con el uso compartido de DataSets. Mi enhorabuena por el planteamiento y por la solución.

Ahora bien, tengo una serie de interrogantes:

Dices que "Todas las propiedades, eventos y campos definidos del primer conjunto de datos serán establecidos de igual manera en el objeto clon." Supongo que por campos te refieres a los campos establecidos como persistentes en diseño, pero supongo que esto sólo sirve para clonar sus propiedades, no para acceder a ellos directamente, evitando la fórmula DataSet.FieldByName('NomCampo'). Una de las razones por las que muchas veces he declarado un campo como persistente es para ganar en eficiencia al acceder a él, aparte de evitar llenar el código de cadenas literales que pueden fallar sin que el compilador te avise. Supongo que la solución pasaría por clonarlos en tiempo de diseño para poderlos referenciar desde código, y ya en ejecución se buscaría la manera de que cada instancia de formulario accediera sólo a "su copia" de dichos campos. Parece complicado. ¿Algo de esto te ha quitado el sueño?

Bueno, lo anterior es algo tal vez prescindible. Otra cosa, en caso de manejar DataSets en relación Maestro-Detalle, ¿se establecen esas relaciones entre los DataSets clonados en el formulario? Me imagino que lo tendrás previsto, pero como no he visto los componentes más que en la demo, ignoro si hay una propiedad en cada TMagiaDataSource donde indicarle el DataSource maestro, u otra metodología que "inspeccione" los DataSets clonados y revise las relaciones existentes entre sus DataSets origen.

Otra más, si realizamos modificaciones en un DataSet clonado, ¿se refleja inmediatamente ese cambio en el DataSet origen y por ende en sus posibles otros clones? ¿Está este comportamiento de sincronización controlado de alguna manera?

Lo de clonar los eventos del DataSet también me parece muy buena iniciativa, he tenido más de un quebradero de cabeza cuando he necesitado interceptar uno de esos eventos para aspectos visuales, y siempre me he visto abocado a colocar un DataSource en el formulario y descifrar su evento OnChange, aunque éste no es siempre suficiente.

Buena idea lo de colgar estos videos en YouTube, una imagen vale que más que muchas explicaciones y espero te ayuden a extender el producto.

Saludos,

Andrés.

Al González dijo...

Hola Al,
Soy Delphius.
Recuerdo que en ClubDelphi se han tocado estas cuestiones técnicas que mencionas sobre "Modulo vs Forms"... es más en algunas participé yo.

Nunca me había quedado del todo claras las ideas que exponían, pero ahora leyendo tus fantásticas explicaciones y junto con el video comprendo mejor lo que decías tu, roman y otros. Y de paso se me aclaró la duda del hilo que abriste preguntando sobre la propiedad.


¡Hola Marcelo! Me alegra mucho saber que mi artículo te ayudó a comprender un poco más estos aspectos de Delphi.



Andrés dijo...

Dices que "Todas las propiedades, eventos y campos definidos del primer conjunto de datos serán establecidos de igual manera en el objeto clon." Supongo que por campos te refieres a los campos establecidos como persistentes en diseño, pero supongo que esto sólo sirve para clonar sus propiedades, no para acceder a ellos directamente, evitando la fórmula DataSet.FieldByName('NomCampo'). Una de las razones por las que muchas veces he declarado un campo como persistente es para ganar en eficiencia al acceder a él, aparte de evitar llenar el código de cadenas literales que pueden fallar sin que el compilador te avise.

Supongo que la solución pasaría por clonarlos en tiempo de diseño para poderlos referenciar desde código, y ya en ejecución se buscaría la manera de que cada instancia de formulario accediera sólo a "su copia" de dichos campos. Parece complicado. ¿Algo de esto te ha quitado el sueño?


Te entiendo perfectamente. Durante años también le he dado preferencia al uso de campos persistentes, en parte para establecerles propiedades en tiempo de diseño pero también para no tener que referirme a ellos con una cadena literal, sino mediante un identificador de programa (algo que ya no aprecio tanto). En tiempo de ejecución, al clonarse el conjunto de datos también se clonan sus objetos TField (e incluso sus “Params”), pero es de esperarse que durante la compilación no hay ningún identificador de programa relacionado con posibles objetos clones que serán creados en tiempo de ejecución.

Cuando programaba esta característica tuve la misma inquietud, pero después de ver los resultados me dije «¡qué diablos! Vale mucho la pena empezar a usar FieldByName». Incluso ahora me parece más legible el código. ;)

Aunque aquí debo decir que ya ni siquiera FieldByName utilizo, ya que me he implementado un “pequeño” framework en el que manejo los registros (filas) de un conjunto de datos como objetos. Luego por ahí me dijeron que eso se llamaba “ORM”, así que busqué ORM en Wikipedia y Google para terminar descubriendo que no soy el único que se ha inventado una máquina objetivizadora de registros. La imagen actual en la cabecera de esta bitácora es parte de ese juguete mío. :)



Bueno, lo anterior es algo tal vez prescindible. Otra cosa, en caso de manejar DataSets en relación Maestro-Detalle, ¿se establecen esas relaciones entre los DataSets clonados en el formulario? Me imagino que lo tendrás previsto, pero como no he visto los componentes más que en la demo, ignoro si hay una propiedad en cada TMagiaDataSource donde indicarle el DataSource maestro, u otra metodología que "inspeccione" los DataSets clonados y revise las relaciones existentes entre sus DataSets origen.

Previsto en términos generales. Implementarlo no sería complicado, triangulando la relación de los objetos se lograría, pero quisiera esperar un tiempo y considerar la experiencia y comentarios de los propios usuarios del componente. Por ahora basta una sencilla instrucción en el evento OnCreate del formulario, como estas:

(DataSourceDetalle.DataSet As TClientDataSet).MasterSource := DataSourceMaestro;

(DataSourceDetalle.DataSet As TIBQuery).DataSource := DataSourceMaestro;



Otra más, si realizamos modificaciones en un DataSet clonado, ¿se refleja inmediatamente ese cambio en el DataSet origen y por ende en sus posibles otros clones? ¿Está este comportamiento de sincronización controlado de alguna manera?


Es una buena pregunta, y más porque ya existe con cierto arraigo el concepto de “clonación”, pero en términos de cursores. Para evitar confusiones, hay que aclarar que aquí hablamos de clonar una instancia de objeto. Y cuando TMagiaDataSource clona el conjunto de datos lo hace excluyendo una propiedad en particular: Active. De esta forma se asegura que el nuevo clon estará de entrada cerrado. Recae en el programador cuándo y cómo abrirlo (lo mismo que si no usara un data set clonado). Un conjunto de datos clon no guarda relación con su objeto patrón, ni con sus hermanos clones (al menos no en esta versión de Magia Data).

Este componente en particular no tiene como objetivo implementar mecanismos para sincronía de cursores. Pero si hay interés en lograr también esto último, recomendaría el uso de conjuntos de datos TClientDataSet, ya que esta clase posee un útil método llamado CloneCursor cuyo propósito va en la dirección que señalas. De hecho recomiendo TClientDataSet en cualquier caso, es una chulada, como decimos en México. :)



Lo de clonar los eventos del DataSet también me parece muy buena iniciativa, he tenido más de un quebradero de cabeza cuando he necesitado interceptar uno de esos eventos para aspectos visuales, y siempre me he visto abocado a colocar un DataSource en el formulario y descifrar su evento OnChange, aunque éste no es siempre suficiente.


Has escrito en 56 palabras el quid del problema #2. ;)

Andrés dijo...

Hola de nuevo, Al:

Antes que nada una leve matización, el evento del DataSource al que me refería no se llama OnChange sino OnDataChange, éste permite saber si se ha efectuado un Scroll (parámetro Field = nil) o si lo que ha cambiado ha sido un campo (el indicado en Field). Aún así, es insuficiente esta información para muchas necesidades que suelen surgir en un formulario, principalmente aquellas relacionadas con eventos BeforeXXX en que queremos controlar cierta acción antes de que suceda, por ejemplo preguntando al usuario, ya que el DataSource sólo nos informa de "hechos consumados".

Supongamos que queremos controlar un borrado, pidiendo autorización expresa al usuario, lo lógico es que éste código vaya en el evento BeforeDelete, pero esto supone "ensuciar" el DataModule con la llamada a un diálogo de confirmación. Y si, a la vez, quiero borrar "por código" dicho registro sin pedir autorización, tengo que meter aquí un "flag" que indique quién requiere la acción para que no me salte el diálogo ... etc; o bien crearme dos DataSets uno para cada ocasión. De aquí que la idea de clonar tantos DataSets al vuelo como hagan falta, con sus eventos asociados, y aislando el tratamiento para cada uno, me parezca muy acertada. Y este comportamiento obviamente no lo puede suplir un ClientDataSet.

Bueno no me extiendo más, que te toca a tí explicar esa segunda parte del TMagiaDataSource :---)), sólo una última pregunta capciosa que quizás quieras responder en tu próxima entrada, ¿pueden heredar estos clon-eventos el comportamiento del DataSet origen? Esto lo veo fundamental para, por ejemplo, el evento OnNewRecord.

En cuanto al ejemplo de Morelia Framework que aparece actualmente (a la fecha de este post) en la cabecera del blog, me ha suscitado una serie de misterios: primero, me ha llamado la atención la expresión AField.Named('Importe;_Rec'), el _Rec aún no sé por dónde cogerlo ni a qué se refiere ... luego veo que hay una función Sum que es un método de un Dataset ... misterioso, ¿a quién pertenece este señor DataSet? ¿de qué clase es este Dataset? ¿Y de donde sale el Master? No parecen nombres asignados a componentes DataSets sino más bien referencias a propiedades o parámetros accesibles al mismo evento, ... uhm!!.

Creo que tarde o temprano tendrás que dar explicaciones ... :-))

Un clon-abrazo

Andrés.

Al González dijo...

Todo a su tiempo, Andrés. Todo a su tiempo. ;)

{ Entity class for ORDER table }
TRecOrder = Class (TDataSetRec)
Protected
Procedure AfterChange (AField :TField; PrevValue :Variant; Direct :Boolean); Override;
End;

:)

Andrés dijo...

Todo a su tiempo, Andrés. Todo a su tiempo. ;)

Esto te pasa por enseñar la golosina antes de hora ... jeje

Vale. Con ese escueto código me ha quedado más claro, me confundía pensar que estabas interceptando un evento cuando en realidad es un método heredado de la aún misteriosa clase TDataSetRec, de la que el resto de mortales sólo sabemos que debe implementar métodos sustitutivos de funciones agregadas de SQL como sum, average ...etc, ¿eh? y que sabe entenderse con un DataSet Maestro.

Y dejo de hacer de Sherlock Holmes, como parece que está en fase Beta, no te pregunto más. Suerte con ese ORM, yo tampoco sabía qué significaban estas siglas hasta que lo leí en el blog de Marteens, es un viejo anhelo lo de objetivizar el tratamiento de bases de datos que obtenemos del modelo relacional, hay quien incluso se crea una clase diferente para gestionar cada tabla (¡animalada padre!), en mi opinión es mejor un enfoque más genérico y menos ambicioso, al estilo de lo que estás dejando entrever en esas lineas.

Un abrazo,

Andrés.

Carlos Enrique Velez Farak dijo...

Bueno yo he pasado y tengo aplicaciones con datamodule y he tenido el problema que se ponen lenta la aplicaciones cuando se entra a diferentes formularios y he tratado de corregir sin mucha suerte Trabajo en delphi desde la primera version del delphi uno pasando por delphi dos delphi tres delphi 5 delphi 7 y RAD STUDIO 2007 DE LOS CUALES EL MEJOR PARA MI ES EL DELPHI 7

Al González dijo...

tengo aplicaciones con datamodule y he tenido el problema que se ponen lenta la aplicaciones cuando se entra a diferentes formularios y he tratado de corregir sin mucha suerte

La lentitud de un proceso es uno de los problemas que más causas posibles tiene en el desarrollo de software. Habría que indagar en los detalles de cada caso. :)

Publicar un comentario en la entrada

Te invito a expresar tu opinión abierta y libremente (no es necesario que estés registrado). Vale el anonimato mientras no lo uses como trinchera.