21 julio 2006

Por qué podríamos eliminar CampoNulidad y por qué va a continuar

Os enlazo un artículo muy breve pero muy interesante sobre algo que leí hace ya mucho tiempo (cuando lanzaban C# 2.0) y que después no había vuelto a recordar por lo que no había podido usarlo: la facilidad con que se crean tipos Nullables en .net a partir de otros tipos que no admiten Null (tipos de valor, como int, DateTime...), usando el tipo genérico Nullable<>.
Usando esto, no es necesario tener 2 propiedades para un campo de la base de datos que puede ser nulo. Antes creábamos dos propiedades, una DateTime (por ejemplo) y otra bool. Ahora basta con crear una Nullable, lo que nos facilita el trabajo doblemente: de la entidad a la base de datos, el ORM no necesita mapear dos propiedades, sólo una, asignado un valor si es eso lo que hay en la base de datos, o un null si encuentra un DBNull: exactamente igual que al mapear objetos: menos trabajo y más simple. Por el otro lado, al establecer el enlace entre controles y entidades (Data Binding), podemos conectar un control directamente con una propiedad Nullable, cuando antes si lo hacíamos no se consideraba la otra propiedad buleana y a veces salía un valor cuando realmente era un Null y no debía verse nada. Ahora incluso podemos definir en el DataBinding el valor a mostrar cuando nos encontremos null, de forma muy sencilla.
Total, que tendríamos que eliminar el atributo CampoNulidad, así como los parámetros IgnorarNulos y ValorSiNulo del resto de atributos Campo.
Pues no: en primer lugar, los parámetros IgnorarNulos y ValorSiNulo siguen teniendo sentido para las propiedades en las que no nos aporta significado si un valor es nulo o no, en los que aunque hayamos leído un nulo ahora vamos a guardar un valor.
Y en segundo lugar, pueden seguir existiendo casos en que la entidad quede más clara si dividimos el nulo en una segunda propiedad, por ejemplo en el patrón DeBaja, donde contamos con 2 propiedades: DeBaja y FechaBaja. Queda más claro preguntar si DeBaja que si FechaBaja == null. Por lo que las posibilidades de mapeo se mantendrán, pero es obvio que se usará mucho menos el CampoNulidad, definiendo en su lugar una única propiedad de tipo Nullable.

Etiquetas:

13 julio 2006

Pepi, Luci y Bom y otros ORMs del montón II

Pues nada volvemos al ataque con otro estudio sobre ORMs gratuitos:
Parece mentira, pero cuanto más ORMs veo más me gusta Gentle.NET, es verdad que tiene características que no me convencen pero también es verdad que el resto de ORMs me convencen bastante menos.
Paso a comentar los dos a los que le he echado un vistazo:

Nota: No espereis un profundo análisis, es un simple primer vistazo.

DomainObjects:

  1. Para cada clase hay que crear un constructor (protected) con todas las propiedades como parámetros para que el gestor pueda "reconstruir" los objetos. Supongo que además deberán estar en el mismo orden que las columnas de la abse de datos. No me gusta.
  2. Para usar una propierty y que el gestor se de cuenta de que esta ha modificado una variable mapeada hay que añadir a la property el atributo [Mutator]. Dejando aparte lo orrendo del nombre, ¿no serían mutadores todas las propiedades?
  3. El mapeado se realiza con archivos XML.
Grove:

  1. La carga de entidades se realiza en vez de usando el generic pasando el typeof. Posiblemente porque aún no estaba disponible esta posibilidad.
  2. En todas las consultas las condiciones se añaden como ¡cadena de texto!, esto es, new ObjectQuery(typeof(Customer), "this.State='WA'"). Esto no es ni muy eficiente ni muy cómodo.
  3. Distingue entre insertar un nuevo objeto (Insert) o actualizar uno ya existente (Update). Consecuencia de no exigir heredar de una clase base.
  4. Es posible realizar transacciones, indicandole al gestor cuando empieza, cuando termina y si hay que hacer roleback. Esta caracterísitca es interesante y como aún no la hemos estudiado nosotros no estoy en disposición de verle los problemas.
  5. Si se realiza un interesante intento de poder definir objetos almacenados en multiples tablas, pero con una aproximación que creo que es compleja y requiere introducir información muy cercana al nivel de acceso a datos y además redundante. Con un poco de esfuerzo se podría pulir, aunque se agradece que nos aporte una aproximación.
Con todo esto, va tomando más poder Gentle.NET, solo me queda intentar investigar si es posible realizar herencia de clases y que las propiedades de cada uan vayan a la tabla que le corresponda.

Ahh, se me olvidaba, aún me queda un ORM en al recámara. Retina.NET, que ya veremos si aporta algo nuevo.

Etiquetas:

Entidades multitabla y tablas multientidad

Voy a hacer un pequeño resumen de la reunión matutina en la que hemos abordado algunos temas pero la mayoría giran en torno al título.
Pensando en el diseño de las entidades de personas (clientes, proveedores...) en Sota y en Vesta (el programa inmobiliario de Berny), mantenemos la idea de hacer herencia (lógico) y añadimos la posibilidad de navegar, que no estaba contemplada, mediante propiedades .ComoCliente, .ComoBanco... que devuelvan una instancia de la entidad actual con el tipo pedido, si existe, y si no null. Se añadirían métodos HacerCliente(), HacerBanco()... para obtener lo mismo pero si no existe, se daría de alta; algo así como dámelo como cliente, y si no existe lo creas y me lo das. Con esto, la idea era eliminar las propiedades EsCliente, EsBanco... y reemplazar su uso por ComoCliente == null, pero al eliminarlas se nos complica el mapeo con la base de datos, ya que allí hay un buleano, así que mientras me decido vamos a hacer esa propiedad internal y vamos a quitar su NP.

Volviendo al tema de esta entrada, esa jerarquía de personas define una tabla multientidad, ya que (en Sota) con una sola tabla Personas almacenamos EPersona (abstracta), ECliente, EProveedor y EAseguradora. ¿Qué nos falta para que esto esté operativo? Como ahora se heredan las propiedades y el atributo Tabla de EPersona, Cargar y Guardar funciona ya en los objetos hijos. En cambio, las colecciones hay que implementarlas manualmente: si pedimos ECliente.GetCollection() hay que establecer un filtro para que sólo devuelva los clientes. Pensamos añadir un atributo a ECliente indicando que usa la tabla Personas pero con un criterio dado (en este caso, cuando el campo EsCliente == TRUE); habrá que pensar cómo definir el criterio, seguramente usando IFiltro. La utilización de este criterio facilitará las colecciones, y también evitará que se cargue incorrectamente un Cliente como Proveedor, por ejemplo (algo que puede suceder actualmente).
No se me ocurre otro caso más complicado donde una misma tabla se emplee para almacenar distintas entidades: aunque no hereden, en ese caso sólo habría que repetir las propiedades en las entidades, y definir la misma tabla al guardar, y el criterio por el que discriminar. El criterio separa lógicamente la tabla en varias subtablas.

Una entidad multitabla, por otro lado, es una entidad que se persiste sobre varias tablas. Vamos a comentar 2 ejemplos muy distintos:
  • En Sota, en EPersona hemos almacenado tanto los datos de la persona (física o jurídica) como su dirección. En un futuro, podría plantearse separar esto en dos tablas: Personas y Direcciones. Para no cambiar EPersona, habría que definir que dicha clase se almacena en 2 tablas, de una forma similar a como lo hace Grove pero mejor, claro. La forma de ellos es muy prolija (que bonita traducción para el inglés verbose), y se puede sintetizar bastante.
  • Por otro lado, también en Sota tenemos EOrdenBase como raíz común a ECita, EOrden y EPresupuesto. En base de datos tenemos el mismo esquema: las tablas OrdenesBase, Ordenes, Citas y Presupuestos, donde estas 3 referencian a la primera con su campo clave (que también es clave foránea, en este caso no es autonumérico). Al guardar un objeto EOrden, primero hay que guardarlo como EOrdenBase (¿llamando a un base.Guardar? Debe hacerse automáticamente en Nibi.Dal), y una vez guardado hay que guardar las propiedades que existen en EOrden (no las heredadas, ojo) en la tabla definida en EOrden (ojo con que el atributo Tabla se hereda, por lo que EOrden tendrá 2 y sólo hay que coger el propio: parece que sólo hay que coger el heredado cuando no hay un atributo Tabla no heredado; y supongo que el más cercano en la jerarquía). Además, al definir esa tabla hay que indicar que no tiene campo clave autonumérico (que sería posible), sino que su campo clave, y a la vez referencia (clave foránea) al campo clave de EOrdenBase, se llama "Id". Todo esto puede ser una nueva propiedad CampoClaveReferenciaAlPadre="Id" en el atributo Tabla. ¿Se os ocurre un nombre mejor?
En resumen hay 3 nuevos casos a considerar:
  • Que una sola entidad se persista en varias tablas (p.ej. EPersona -> Personas y Direcciones).
  • Que una jerarquía de entidades se persista en una sola tabla (p.ej. EPersona, ECliente, EProveedor, EAseguradora -> Personas).
  • Que una jerarquía de entidades se persista en varias tablas (p.ej. EOrdenBase -> OrdenesBase y EOrden -> Ordenes; o bien EPersona -> Personas y EComprador -> Compradores).
Iremos comentando la solución que damos a cada uno de ellos.

Etiquetas:

Enlace a datos de un grupo de RadioButtons

Se me ha ocurrido una cuestión que daba por resuelta: si en una entidad tengo una propiedad que representa un valor enumerado, y quiero mostrarla en la interfaz de usuario con varios RadioButtons (esos circulicos de los que _sólo pué queá uno_), resulta que no puedo usar las propiedades de DataBinding directamente. Cada RadioButton ofrece una propiedad Checked buleana (mi contribución al DRAE), pero esto podría enlazarse si tuviera 2 RadioButton y la propiedad de la entidad fuera buleana también. En el caso que me ocupa, en que la propiedad no es buleana (cómo me recreo) sino un enumerado, no puede emplearse. Tendría que crear una propiedad buleana (olé) en la entidad por cada posible valor del enumerado (algo como IsValor1, IsValor2...) y enlazar cada una de estas al control RadioButton correspondiente. Pero lógicamente esta no será mi solución. Así que a guglear (eso, quememos el diccionario).

Una propuesta interesante es construir un control que representa un grupo de RadioButtons. El .NET Framework no lo ofrece, pero no es dificil de conseguir, aquí hay un ejemplo de uno en código fuente. Al verlo, me llevo una grata sorpresa por un problema que yo no había considerado. Yo pensaba introducir los RadioButtons uno a uno manualmente en la interfaz de usuario, pero aquí el amigo Hotdog (Robert Verpalen) plantea que el su control grupo de radiobuttons tenga un DataSource de donde cargue los controles. ¡Genial! Sólo una pega: el diseño o distribución de las opciones (RadioButtons) se hace dinámicamente, automáticamente, por lo que no podemos ajustarlo manualmente. Aunque esto es lo mejor y más aconsejable para la mayoría de los casos, tenemos que dar las dos posibilidades:
( ) rellenar opciones en diseño manualmente,
(·) o automáticamente a partir de un DataSource.

Otra propuesta, mucho menos elegante pero interesante por ser más sencilla, consiste en capturar los eventos Parse y Format del DataBinding. Esto nos obliga a establecer el DataBinding mediante código propio, no estableciendo propiedades en diseño. El evento Format se dispara al tratar de asignar un valor leído de la fuente de datos al control. El evento Parse, de forma complementaria, al asignar el valor establecido en el control de vuelta a la fuente de datos. De nuevo necesitamos código específico para estos dos eventos. Y no veo una forma sencilla y bien diseñada de generalizar este código.

Dejo de guglear, ya que no encuentro nada nuevo, sólo algunas críticas a la gente de .net por no incluir de forma nativa el soporte para este tipo de enlace a datos tan frecuente.

P.D. Está vivo este blog. Espero que siga así.

Etiquetas:

10 julio 2006

Pepi, Luci, Bom y otros ORMs del montón

Pues aprovechando que me han pasado una interesante dirección sobre ORMs, mchos de ellos gratuitos. Me decido a echarles un ojo y ver como los demás se enfrentan a los mismos problemas que nosotros. El primero que he estado mirando ha sido Gentle.NET. Lo primero que me llama la atención es la calidad de la ayuda, bastante clara y con ejemplos. Pero no nos dejemos engañar por el envoltorio y pasemos al contenido;

  • En lineas generales es bastante parecido a nuestro enfoque, esto es, usar atributos para definir el mapeo para la clase y las propiedades, aunque en sus atributos permiten definir restricciones del origen de datos como campos Not null, etc. Esto en principio no lo veo muy útil, salvo el caso de definir si el campo PrimaryKey es autogenerado o no, el poder definir este comportamiento sí puede llegar a interesarnos cuando nos enfrentemos a origenes de datos que no generen automáticamente los identificadores.
  • Este ORM permite trabajar directamente con la clase que implementa la persistencia para cargar o guardar objetos, esto hace posible que las clases no tengan que heredar de ninguna clase base, a costa de guarrear el código. Aunque también dan la posibilidad deheredar de la clase Persistent, que aporta funciones de persistencia a los objetos, aunque para cargarlos del origen de datos es necesario seguir accediendo a la clase gestora. Todo esto creo que ensucia el código y dificulta establecer un patrón de trabajo, considero mucho más claro nuestro enfoque de heredar SIEMPRE de una clase base que contiene un gestor que nos provee las capacidades de cargar y guardar objetos.
  • Con respecto a las listas de objetos su solución no acaba de separar la capa de negocio y la de datos, como muestra el que para hacer una búsqueda de objetos cuyo nombre contengan la palabra "Antonio", se deba, a nivel de negocio, añadir los % al patrón. Esto claramente debería ser trabajo del gestor, ¿que pasa cuando trabajamos con access? ¿habría que generar el patrón de busqueda como "%Antonio%" o "*Antonio*" dependiendo el caso?.
  • Otra cosa que me llama la atención de las listas es que estas consulta devuelve IList, ¿no implementan IBindingList las listas devueltas por el gestor?
  • También abordan el problema de los campos nulos pero de una manera diferente y es añadiendo a los atributos de la propiedad que valores se tomarán como nulos para los tipos de datos que no aceptan nulos (DateTime, Decimal y Guid). Aquí sigo viendo más elegante nuestra solución de tener una segunda propiedad para cada uno de estos campos que nos indique si el campo es nulo o no.
  • Por último la solución aportada para el control de concurrencia, que no es más que añadir un campo más en la tabla de tipo entero que sirva para almacenar la verisón de la fila, si bien es sencilla y robusta no aporta tanto significado como usar un campo fecha. En este punto hay que adoptar una u otra solución. O nos complicampos la vida con los problemas derivados de que arios equipos tengan distinta fecha usando un TimeStamp , o simplemente controlamos la concurrencia sin almacenar la fecha de la última modificación usando un entero.

Resumiendo, despues del ladrillazo. Está claro que elegir una solución u otra a cada problema muchas veces solo es cuestión de gustos. Lo que si está claro es que para diseñar un ORM hay que superar ciertos obtaculso muy claros:

  1. Como mapear el objeto de negocio al origen de datos.
  2. Como permitir objetos almacenados en multiples tablas y tablas que alamcena multiples objetos.
  3. ¿Heredar de un objeto base que provea la funcionalidad de cargar y guardar o atacar siempre a la clase que implementa la persistencia?
  4. Como obtener listas filtrando por ciertas propiedades.
  5. Como contemplar el caso de campos nulos en el origen de datos que no pueden ser nulos en el negocio.
  6. Como controlar la concurrencia.
  7. Como forzar el cacheo o no de los distintos objetos.

Sin duda, como resolver estos 7 problemas definirá la calidad del ORM.

Etiquetas:

07 julio 2006

Utilización de recursos en .net

Aprovechando que tengo instalada la versión Arquitecto de VS2005 (sólo por 180 días) en mi Windows Vista, me he puesto hoy a probar algunas de las funcionalidades que ofrece. Me iba a poner a generar un proyecto de prueba de Nibi.Negocio, pero por el camino me he encontrado el Análisis estático de código, y me he puesto a revisar los numerosos avisos que da, y a corregir algunos de ellos. Por ahí he llegado a uno que me pedía que generara un archivo de recursos para almacenar las cadenas de texto usadas en el programa, idea que tenía en mente desde hace tiempo pero que aún no había atacado, así que me he puesto a investigar eso esta mañana (declaro instaurado el viernes como el gran día del I+D).

Lo primero que encuentro es un tutorial en el SDK de Microsoft sobre recursos y localización, pero no me aclara mucho, porque comenta la utilización del ResourceManager para acceder a los recursos, y la del CultureInfo para la cultura (idioma + pais) del usuario, pero no aclara cómo realizar la conexión entre estas dos ideas. Leo en algunos otros sitios la utilización de recursos satélites, pero no me queda muy claro.

Finalmente me quedo con una solución sencilla: al agregar un nuevo archivo de recursos, crea una clase que publica esos recursos y que es global y ofrece los recursos de forma tipada. Voy a usar eso, con una clase llamada Cadenas, pero lo que aún no sé es cómo cambiar después el idioma a la aplicación. Por cierto, había llamado a la clase es-ES, pero veo que el idioma neutral nunca sigue esa nomenclatura, así que vuelvo a Cadenas. Lo hago así porque espero tener otro recurso con las imágenes que no cambien entre diferentes idiomas (que serán la mayoría), y que llamaré Imagenes.

01 julio 2006

Almacenar enumerados en la base de datos

Investigando un poco el blog del amigo este que tanto criticaba el artículo introductorio de Custom Entities, me encuentro con una entrada muy interesante de algo que ya nos hemos planteado nosotros y donde de nuevo estoy en desacuerdo con él, aunque aporta una alternativa que nunca hemos barajado: habla de cómo guardar los valores enumerados en la base de datos, y su opción elegida consiste en guardar los textos en vez de los números. Es interesante el artículo y más aún los comentarios que aparecen. Y me termina de convencer para dar la razón a Jorge en la idea de crear una tabla para cada enumerado.

P.D.: Hoy para compensar la poca actividad del último mes, dos entradas nuevas al precio de una.

Control propio de la concurrencia en la DAL

Tras un par de meses sin apenas mover este blog, vamos a intentar darle un poco más de meneo este verano, sobre todo ahora que Carvajal tiene más tiempo para dedicarnos.
Voy a retomar la tarea comentando un repaso que ya dimos esta semana a la capa de acceso a datos para personalizar el mapeo en la clase EOrden, extendiendo la funcionalidad que ya ofrecíamos con CampoNulidad. Tuvimos que adaptar DBd para permitir esta personalización. Pero queda mucho por hacer.
Hoy me voy a centrar en una cuestión que ronda a Nibi.DAL desde que comenzamos: ¿es posible hacerlo sin DataSets? Está claro que sí, pero ¿cómo hacerlo para no perder el control de concurrecia (optimista) que nos ofrece ADO.NET? He vuelto a esta idea tras leer el artículo Introducing Custom Entity Classes, que es introductorio pero muy bueno y además argumentado, y que ofrece enlaces donde profundizar en las cuestiones más problemáticas del diseño de una capa de negocio, entre ellos el de la concurrencia ya planteado. El artículo está en ASP.NET, pero es de aplicación general, y llegué a el desde esta entrada en un blog: Article about Custom Entities vs Datasets, donde hacen una dura crítica al artículo, la cual no comparto: es cierto que puede haber entidades de negocio sobre DataSets (lo que nosotros tenemos actualmente), pero tiene bastantes contrapartidas (drawbacks):
  • Ocupas más del triple de memoria: el DataSet consume bastante, y los datos están repetidos entre él y los objetos de negocio, además de que el propio DataSet los almacena dos veces: la copia actual y la de la base de datos, para ofrecer el control de concurrencia.
  • Dificultas la genericidad, sobre todo a la hora de leer datos de varias entidades en una misma consulta y luego tratar de actualizar de vuelta la base de datos (un registro de la tabla del DataSet podría contener datos de varias tablas, por lo que no podrían actualizarse por separado... aquí recuerdo que el DataSet puede tener varias DataTables, pero ¿cómo cargas eficientemente en una tabla algunos albaranes y en otra sus clientes? Serían dos consultas, y la de los clientes debería incluir el mismo filtro de los albaranes... una movida que no controlo).
Total, que la crítica os la pongo porque hay que reconocer que encontré el artículo por ahí, y de paso para que veamos los típicos argumentos de quien no sabe sacar el mejor partido a una capa de negocio y no se le ocurre cómo automatizar el mapeo (ni tampoco buscar una implementación comercial que lo haga).

Volviendo al tema principal, el control de concurrencia, en Concurrency Control in ADO.NET encontramos los dos métodos de actualización optimista (lógicamente nos decantamos por optimista, eso no se merece ni media palabra más):
  • Comprobar una marca de versión o fecha. Se utiliza un campo de la base de datos para almacenar, pongamos, la fecha y hora de última modificación; antes de cambiar los datos en la base de datos, comprobamos que el valor del campo en la base de datos es igual al que leímos, lo que prueba que nadie lo ha modificado; entonces guardamos cambiando la marca por una actual. La principal ventaja es el uso de memoria: no hay que guardar los datos originales, sólo un dato más. El único inconveniente es que hay que añadir un campo nuevo a la tabla.
    Incluso usando DataSets, dice este artículo que tenemos que implementar nuestro propio código para emplear esta aproximación.
  • Comprobar todos los valores. Esta es la aproximación automática de los DataSets. En cada cambio a la base de datos, se comprueba que los valores que hay en la base de datos para TODOS los campos coincide con los que leímos nosotros. La ventaja es que es automático (en los DataSets, claro) y que no necesita nada en la tabla para funcionar. El problema es que duplica la memoria y que es más difícil de implementar.
Me convence bastante la primera aproximación, ya que el campo extra que hay que crear no sólo sirve para esto, sino que aporta información al usuario: fecha de la última modificación. Así que me decanto por ofrecer dos opciones en nuestra capa de acceso a datos:
  • Si indicamos un campo especial con la fecha de actualización, se usará para comprobar al escribir, ofreciendo actualización optimista.
  • Pero si para una entidad no se indica este campo, no habrá control de concurrencia, resultando en una Last In Win: el último que escribe gana, es decir, machaca los datos del anterior.
Pero la segunda opción normalmente no será recomendable, por lo que habrá que añadir el campo de actualización a todas las tablas. Habrá que crear un atributo nuevo CampoFechaActualizacion("nombrecampo") para marcar la nueva propiedad. Porque la publicaremos como propiedad, ya que puede ser significativo para el usuario.

Por si no ha quedado claro, esto significa que tenemos que generar nuestras propias consultas de actualización: UPDATE, INSERT y DELETE. Hay que ver si CommandBuilder ofrece la posibilidad de ser usado para automatizar esto, devolviendo las consultas o los comandos (creo que ya esto no lo hace, así que...) y aplicando el enfoque de actualización optimista.

Por cierto, mirando todo esto he encontrado este artículo de nuestro amigo Rocky Lotcka, en el que habla de CSLA.NET. ¿A que no sabéis lo que es? Pues su propio Framework para persistencia. ¡Otro! Lo ofrece gratis (creo) pero te vende los libros en los que te enseña a manejarlo, lo cual es una muy buena práctica en mi opinión, a ver si cunde más.
De todas formas, lo he pillado en dos faltas bastante gordas:
  • Dice que se pueden usar dos opciones de actualización optimista: Last In Win o First In Win (creo que esta segunda se entiende con lo que he escrito hasta ahora), y que en el libro ha usado la primera porque no tenía espacio para ponerse con la segunda, que es más compleja. En el artículo que os pongo dice que la primera opción es, literalmente, unacceptable. Hala. Así que te vende el libro usando la opción que no debe aplicarse nunca en la práctica. Está bien la cosa.
  • Luego te explica esa opción más compleja, First In Win, que es básicamente lo que hacía ADO, hace ADO.NET y nos proponemos hacer nosotros mediante TimeStamp. Y al comentar cómo hacerlo comparando todos los campos, en lugar de componer una SQL que lo haga y falle si no se puede, va y te dice que antes del Update hagas un Select y compares con código propio si puedes hacer el update o no. Tiene ding dongs la cosa. De todas formas, me sigue cayendo bien este Rocky, aunque ya veo que no es muy de fiar.

Etiquetas: