Mostrando las entradas con la etiqueta datos. Mostrar todas las entradas
Mostrando las entradas con la etiqueta datos. Mostrar todas las entradas

miércoles, 26 de agosto de 2009

Los problemas de XML en el manejo de grandes cantidades de datos.

No es el uso, es el abuso, que puede transformarse en delirio.

Mi opinión sobre el uso (resumen de los links anteriores) está muy bien condensada en uno de los puntos del artículo: XML es perfecto para para crear documentos estructurados que puedan ser manipulados “como cajas negras” por aplicaciones o, más apropiado todavía, interpretados fácilmente por seres humanos. Por ejemplo:

tatuaje-disenador-web xmlmodelo_2

…pero si tenemos que compartir una gran cantidad de datos entre aplicaciones con una estructura preestablecida… ¿Para qué especificarla junto a los datos mismos si está, justamente, preestablecida?

Tal el caso que se ilustra en how xml threatens big data, un artículo reciente de Dataspora Blog (en inglés) donde el autor comenta brevemente su fracasado intento (enderezado a tiempo) de usar XML como formato para compartir grandes cantidades (ahí el problema) de datos entre aplicaciones.

En resumen, tres razones para el fracaso:

  1. XML aumenta la burocracia: creación, parseo, tipado, conversión…

  2. El tamaño importa. XML encaja bien para documentos (cantidades de información manejable por seres humanos), pero no para datos (cantidades de información manejable por aplicaciones).

  3. La complejidad tiene su costo. XML es mucho más complejo que otros formatos (CSV, JSON).

… y una propuesta de tres reglas para “Rebeldes del XML”:

  1. ¡Basta de inventar nuevos formatos XML!

  2. Obedece la regla de los 50 15 minutos: no debería llevar más de 50 15 (¡ay! mi inglés) minutos aprender el formato.

  3. Adoptar modelado de datos tardío (Lazy Data Modeling), un concepto comparable a la “evaluación tardía”: grabar los datos como son y dejar su interpretación (tipado) para el momento en el que se los requiera, implementando esa interpretación de acuerdo a las necesidades del caso. Es un un punto que tal vez merece más reflexión y que da para mayor discusión.

Ése es el resumen, mucho más detalle por aquí, en el artículo original.

viernes, 12 de junio de 2009

NUTS-QL (Negate/Unite/Test Standard Query Language) [DRAFT].

cucú


Nota para lectores del feed: el código SQL es más legible si lo ven formateado en la entrada original.


¿Qué es NUTS-QL? Para hacer honor a su nombre, comencemos por la negativa: NUTS-QL no es un dialecto SQL. NUTS-QL es una técnica de creación y mantenimiento de consultas SQL, un conjunto de prácticas y principios, y por lo tanto puede aplicarse en conjunción con cualquiera de los dialectos de SQL conocidos.

¿Cuáles son sus orígenes? NUTS-QL es una etiqueta bajo la cual se agrupa un conjunto coherente de técnicas que -por separado- son conocidas y utilizadas por innumerables desarrolladores a lo largo y ancho del globo. Esta compilación y su organización es obra, humildemente, de mi autoría.

¿Cuáles son sus ventajas? La principal ventaja de NUTS-QL es que va más allá de asegurar un código SQL legible: hace que la legibilidad sea irrelevante.

Vamos directamente a un ejemplo. El requerimiento será desarrollar el “Reporte de movimientos diarios”.

El primer paso es escribir lo primero que se nos venga a la cabeza, probar que compile y mandarlo a pruebas:

SELECT C.ID, C.DESCRIPCION, D.DEBE, D.HABER
FROM MOVIMIENTOS_CABECERA AS C 
INNER JOIN MOVIMIENTOS_DETALLE AS D ON C.ID=D.ID
WHERE C.FECHA = @FECHA
ORDER BY C.FECHA

¡Y eso es todo! No hace falta documentar nada. En el improbable caso de que luego de un par de días, semanas o meses, a alguien le toque modificar la consulta, aplicará los principios de NUTS-QL.

Digamos que ahora se necesita que “en los movimientos de emisión de cheques (MOVIMIENTOS_CABECERA.TIPO=1) se presente el número de cheque emitido”.

Lo primero que tenemos que hacer es implementar el requerimiento con una consulta aparte que satisfaga sólo el caso mencionado. Si bien puede utilizarse la consulta anterior como base, usualmente es más fácil no preocuparse por lo que ya está hecho, que probablemente no sirva para nada. Otra vez escribimos lo primero que se nos viene a la cabeza:

SELECT C.ID, C.DESCRIPCION, CHQ.NUMERO, D.DEBE, D.HABER
FROM MOVIMIENTOS_CABECERA AS C 
INNER JOIN MOVIMIENTOS_DETALLE AS D ON C.ID=D.ID
INNER JOIN CHEQUES AS CHQ ON CHQ.MOVIMIENTO_EMISION_ID = C.ID
WHERE C.FECHA = @FECHA AND C.TIPO = 1
ORDER BY C.FECHA

Ahora hay que integrar las dos consultas. Son tres fases. La primera es la de negación (Negative), en la que excluimos de las cláusulas anteriores los registros que incorporamos en la nueva. Esto es fácil porque podemos tomar el WHERE de la nueva y agregarlo en la primera precedido por los operadores AND NOT (ver que la línea 5 del ejemplo siguiente es la negación de la línea 5 del ejemplo anterior). La primera consulta nos queda:

SELECT C.ID, C.DESCRIPCION, D.DEBE, D.HABER
FROM MOVIMIENTOS_CABECERA AS C 
INNER JOIN MOVIMIENTOS_DETALLE AS D ON C.ID=D.ID
WHERE C.FECHA = @FECHA
	AND NOT (C.FECHA = @FECHA AND C.TIPO = 1)
ORDER BY C.FECHA

La segunda es la de unión (Unite), en donde unimos las dos consultas con la cláusula UNION:

SELECT C.ID, C.DESCRIPCION, D.DEBE, D.HABER
FROM MOVIMIENTOS_CABECERA AS C 
INNER JOIN MOVIMIENTOS_DETALLE AS D ON C.ID=D.ID
WHERE C.FECHA = @FECHA
	AND NOT (C.FECHA = @FECHA AND C.TIPO = 1)
ORDER BY C.FECHA

UNION

SELECT C.ID, C.DESCRIPCION, CHQ.NUMERO, D.DEBE, D.HABER
FROM MOVIMIENTOS_CABECERA AS C 
INNER JOIN MOVIMIENTOS_DETALLE AS D ON C.ID=D.ID
INNER JOIN CHEQUES AS CHQ ON CHQ.MOVIMIENTO_EMISION_ID = C.ID
WHERE C.FECHA = @FECHA AND C.TIPO = 1
ORDER BY C.FECHA

La tercera es la de prueba (Test), que consiste en ejecutar y corregir hasta que funcione. En este caso hay dos problemas: el ORDER BY está repetido (va uno sólo al final) y la cantidad de campos en la sección SELECT no coinciden entre las dos partes (rellenamos con nulos donde sea necesario). Necesitaremos al menos dos intentos. El resultado será:

SELECT C.ID, C.DESCRIPCION, NULL AS NUMERO, D.DEBE, D.HABER
FROM MOVIMIENTOS_CABECERA AS C 
INNER JOIN MOVIMIENTOS_DETALLE AS D ON C.ID=D.ID
WHERE C.FECHA = @FECHA
	AND NOT (C.FECHA = @FECHA AND C.TIPO = 1)

UNION

SELECT C.ID, C.DESCRIPCION, CHQ.NUMERO, D.DEBE, D.HABER
FROM MOVIMIENTOS_CABECERA AS C 
INNER JOIN MOVIMIENTOS_DETALLE AS D ON C.ID=D.ID
INNER JOIN CHEQUES AS CHQ 
	ON CHQ.MOVIMIENTO_EMISION_ID = C.ID
WHERE C.FECHA = @FECHA AND C.TIPO = 1

ORDER BY C.FECHA

Es muy, muy improbable que surja una tercera modificación o error, pero agreguemos una a modo de ejemplo. Supongamos que nos enteramos de que “los importes en moneda extranjera (registros en los que MOVIMIENTOS_DETALLE.IDMONEDA <> NULL) no se están convirtiendo a la moneda corriente de acuerdo al tipo de cambio indicado en MOVIMIENTOS_DETALLE.COTIZACION”.

Apliquemos NUTS-QL. La consulta para los registros en moneda extranjera será:

SELECT C.ID, C.DESCRIPCION, 
	D.DEBE * D.COTIZACION, 
	D.HABER * D.COTIZACION
FROM MOVIMIENTOS_CABECERA AS C 
INNER JOIN MOVIMIENTOS_DETALLE AS D ON C.ID=D.ID
WHERE C.FECHA = @FECHA AND NOT D.IDMONEDA IS NULL
ORDER BY C.FECHA

Ahora, en la etapa de negación, debemos modificar la sección WHERE de las consultas anteriores negando la condición WHERE de la nueva sección (ok, son 2, pero es sólo copiar y pegar).

La primera queda:

WHERE C.FECHA = @FECHA
	AND NOT (C.FECHA = @FECHA AND C.TIPO = 1)
	AND NOT (C.FECHA = @FECHA AND NOT D.IDMONEDA IS NULL)

y la segunda:

WHERE C.FECHA = @FECHA AND C.TIPO = 1
	AND NOT (C.FECHA = @FECHA AND NOT D.IDMONEDA IS NULL)

Luego, en la etapa de unión nos queda:

SELECT C.ID, C.DESCRIPCION, NULL AS NUMERO, D.DEBE, D.HABER
FROM MOVIMIENTOS_CABECERA AS C 
INNER JOIN MOVIMIENTOS_DETALLE AS D ON C.ID=D.ID
WHERE C.FECHA = @FECHA
	AND NOT (C.FECHA = @FECHA AND C.TIPO = 1)
	AND NOT (C.FECHA = @FECHA AND NOT D.IDMONEDA IS NULL)

UNION

SELECT C.ID, C.DESCRIPCION, CHQ.NUMERO, D.DEBE, D.HABER
FROM MOVIMIENTOS_CABECERA AS C 
INNER JOIN MOVIMIENTOS_DETALLE AS D ON C.ID=D.ID
INNER JOIN CHEQUES AS CHQ 
	ON CHQ.MOVIMIENTO_EMISION_ID = C.ID
WHERE C.FECHA = @FECHA AND C.TIPO = 1
	AND NOT (C.FECHA = @FECHA AND NOT D.IDMONEDA IS NULL)
ORDER BY C.FECHA

UNION 

SELECT C.ID, C.DESCRIPCION, 
	D.DEBE * D.COTIZACION, 
	D.HABER * D.COTIZACION
FROM MOVIMIENTOS_CABECERA AS C 
INNER JOIN MOVIMIENTOS_DETALLE AS D ON C.ID=D.ID
WHERE C.FECHA = @FECHA AND NOT D.IDMONEDA IS NULL
ORDER BY C.FECHA

Ya en la etapa de test, nos damos cuenta de que (otra vez) el ORDER BY está repetido y de que tenemos que rellenar el SELECT de la nueva consulta con campos nulos para que coincida con el de las anteriores. El resultado final será:

SELECT C.ID, C.DESCRIPCION, NULL AS NUMERO, D.DEBE, D.HABER
FROM MOVIMIENTOS_CABECERA AS C 
INNER JOIN MOVIMIENTOS_DETALLE AS D ON C.ID=D.ID
WHERE C.FECHA = @FECHA
	AND NOT (C.FECHA = @FECHA AND C.TIPO = 1)
	AND NOT (C.FECHA = @FECHA AND NOT D.IDMONEDA IS NULL)

UNION

SELECT C.ID, C.DESCRIPCION, CHQ.NUMERO, D.DEBE, D.HABER
FROM MOVIMIENTOS_CABECERA AS C 
INNER JOIN MOVIMIENTOS_DETALLE AS D ON C.ID=D.ID
INNER JOIN CHEQUES AS CHQ 
	ON CHQ.MOVIMIENTO_EMISION_ID = C.ID
WHERE C.FECHA = @FECHA AND C.TIPO = 1
	AND NOT (C.FECHA = @FECHA AND NOT D.IDMONEDA IS NULL)

UNION 

SELECT C.ID, C.DESCRIPCION, NULL, 
	D.DEBE * D.COTIZACION, 
	D.HABER * D.COTIZACION
FROM MOVIMIENTOS_CABECERA AS C 
INNER JOIN MOVIMIENTOS_DETALLE AS D ON C.ID=D.ID
WHERE C.FECHA = @FECHA AND NOT D.IDMONEDA IS NULL

ORDER BY C.FECHA

Y listo, nuevamente a pruebas y producción.

Los puristas encontrarán que en aquellos registros que corresponden a cheques nominados en moneda extranjera no se está mostrando el número. En el cuasi-imposible caso de que esto se detecte nos llegará el requerimiento correspondiente. Normalmente deberíamos entender toda la consulta y buscar el error. Gracias a NUTS-QL simplemente hacemos lo de siempre.

Para abreviar les dejo sólo el resultado final de aplicar la técnica a este último caso (es un buen ejercicio que el lector lo haga por su cuenta y compare los resultados):

SELECT C.ID, C.DESCRIPCION, NULL AS NUMERO, D.DEBE, D.HABER
FROM MOVIMIENTOS_CABECERA AS C 
INNER JOIN MOVIMIENTOS_DETALLE AS D ON C.ID=D.ID
WHERE C.FECHA = @FECHA
	AND NOT (C.FECHA = @FECHA AND C.TIPO = 1)
	AND NOT (C.FECHA = @FECHA AND NOT D.IDMONEDA IS NULL)
	AND NOT (C.FECHA = @FECHA AND C.TIPO = 1 
		AND NOT D.IDMONEDA IS NULL)

UNION

SELECT C.ID, C.DESCRIPCION, CHQ.NUMERO, D.DEBE, D.HABER
FROM MOVIMIENTOS_CABECERA AS C 
INNER JOIN MOVIMIENTOS_DETALLE AS D ON C.ID=D.ID
INNER JOIN CHEQUES AS CHQ 
	ON CHQ.MOVIMIENTO_EMISION_ID = C.ID
WHERE C.FECHA = @FECHA AND C.TIPO = 1
	AND NOT (C.FECHA = @FECHA AND NOT D.IDMONEDA IS NULL)
	AND NOT (C.FECHA = @FECHA AND C.TIPO = 1 
		AND NOT D.IDMONEDA IS NULL)

UNION 

SELECT C.ID, C.DESCRIPCION, NULL, 
	D.DEBE * D.COTIZACION, 
	D.HABER * D.COTIZACION
FROM MOVIMIENTOS_CABECERA AS C 
INNER JOIN MOVIMIENTOS_DETALLE AS D ON C.ID=D.ID
WHERE C.FECHA = @FECHA AND NOT D.IDMONEDA IS NULL
	AND NOT (C.FECHA = @FECHA AND C.TIPO = 1 
		AND NOT D.IDMONEDA IS NULL)

UNION

SELECT C.ID, C.DESCRIPCION, CHQ.NUMERO, 
	D.DEBE * D.COTIZACION, 
	D.HABER * D.COTIZACION
FROM MOVIMIENTOS_CABECERA AS C 
INNER JOIN MOVIMIENTOS_DETALLE AS D ON C.ID=D.ID
INNER JOIN CHEQUES AS CHQ 
	ON CHQ.MOVIMIENTO_EMISION_ID = C.ID
WHERE C.FECHA = @FECHA AND C.TIPO = 1 
	AND NOT D.IDMONEDA IS NULL

ORDER BY C.FECHA

Comentarios: se le achaca a este método producir sentencias cada vez más ilegibles e ineficientes. Pero hemos visto que no es necesario leerlas, por lo que el primer punto es irrelevante. En cuanto al segundo… bueno, eso es un problema de hardware ¿no?

Lo mejor de todo es que lograr la adopción de NUTS-QL es fácil: una vez que un desarrollador comienza los demás están obligados a seguirlo, basta con respetar el punto de no documentación de requerimientos (si éstos estuviesen documentados, un programador inexperto podría estar tentado a reescribir toda la sentencia). Así, es un camino de ida.


Nota para lectores del feed: el código SQL es más legible si lo ven formateado en la entrada original.

lunes, 10 de noviembre de 2008

Codificación: datos de la aplicación como recursos XML embebidos (parte IV).

Parte I: Resumen.

Parte II: La definción del archivo XML.

Parte III: Encapsulamiento.

Parte IV: Que funcione.

IV. Que funcione.

Ya tenemos nuestras clases de definición de menú alimentadas por un recurso incrustado en el ensamblado, ahora vamos desde el front-end.

Dejemos Form1 para pruebas. Agregamos un nuevo formulario: frmPrincipal, y en él un TreeView, trvMenu. El código del formulario será:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace EjemploRecursosXMLEmbebido
{
    public partial class frmPrincipal : Form
    {
        public frmPrincipal()
        {
            InitializeComponent();            
            CargarMenu(Menues.Raiz, this.trvMenu.Nodes);
        }

        private void CargarMenu(MenuItem definicionMenu, TreeNodeCollection contenedor)
        {
            TreeNode menuNodo = CrearNodo(definicionMenu);
            foreach (MenuItem definicionSubmenu in definicionMenu.Submenues())
                CargarMenu(definicionSubmenu, menuNodo.Nodes);

            contenedor.Add(menuNodo);
        }

        private TreeNode CrearNodo(MenuItem menu)
        {
            TreeNode nodo = new TreeNode();
            nodo.Text = menu.Titulo;
            nodo.Name = menu.Id;

            return nodo;
        }


    }
}

Son solamente dos procedimientos: CargarMenu, que recorre recursivamente la estructura de definiciones de menú y CrearNodo, que es una función que crea el nodo correspondiente a cada definición de ítem de menú.

Dejé para este momento el tema "que haga algo" para ejemplificar las bondades del encapsulamiento del XML en esta manera.

¿Qué sucede cuando seleccionamos alguna opción en el menú? Por lo pronto, nada. Lo lógico sería que de alguna manera se visualice el front-end de la funcionalidad correspondiente. Necesitaremos algunas definiciones extra para avanzar.

Sólo a modo de ejemplo (no me gusta mucho cómo queda), implementaremos el front-end de cada funcionalidad como un UserControl. Así que colocamos un SplitContainer (splitContainer1) en el formulario. Éste lo divide visualmente en dos controles Panel (splitContainer1.Panel1 y splitContainer1.Panel2). En splitContainer1.Panel1 colocamos trvMenu. En splitContainer1.Panel2 cargaremos un UserControl creado dinámicamente cuando el usuario haga click en un ítem del menú.

Comencemos con sólo dos funcionalidades del menú: Compras/Presupuestos y Compras/Ordenes.

Primero, agregamos dos nuevos UserControl al proyecto: uscComprasPresupuestos y uscComprasOrdenes. Pongamos cualquier cosa dentro de ellos. Yo he puesto simplemente un Label con el nombre de la funcionalidad.

Segundo, agregamos código al evento NodeMouseDoubleClick en frmPrincipal para que cargue el control correspondiente de acuerdo al menú que ha recibido el evento:

private void trvMenu_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
{
    UserControl frontendUC = null;
    switch (e.Node.Name)
    {
        case "compras_presupuestos":
            frontendUC = new uscComprasPresupuestos();
            break;

        case "compras_ordenes":
            frontendUC = new uscComprasOrdenes();
            break;
    }

    //si no tiene front end asociado no hace nada
    if (frontendUC == null)
        return;

    //si ya hay un front-end cargado, lo eliminamos.
    if (this.splitContainer1.Panel2.Controls.Count > 0)
        this.splitContainer1.Panel2.Controls.RemoveAt(0);

    frontendUC.Dock = DockStyle.Fill;
    this.splitContainer1.Panel2.Controls.Add(frontendUC);

}

Este código tiene un switch en el que se selecciona el control correspondiente de acuerdo al nombre del nodo (recordemos que habíamos asignado a cada nodo un nombre igual al id del menú en su definición en el archivo XML). Si no hay un UserControl asociado al menú no hace nada. Si lo hay, elimina el correspondiente a la opción anteriormente seleccionada y crea el nuevo, colocándolo en splitContainer1.Panel2.

Probemos... ok, funciona (por lo menos en mi máquina funciona).

A algunos la solución les parecerá buenísima... ¡manos a la obra! ¡Ya tenemos la estructura de la aplicación, ahora a programar funcionalidad! Hay como 200 funcionalidades que implementar, son 200 controles por crear, así que ¡rápido!

A otros la solución les hará un poco de ruido... "qué incómodo ese switch... por más que tengo los datos del menú en XML, sigo teniendo que agregar la línea que crea el UserControl... no es como crear el nodo a mano en diseño, pero... bueno, empecemos y veremos qué pasa".

Otros con más experiencia verán que ese switch va a convertirse en un problema, y ni hablar de cuando los menúes empiecen a "moverse" por los cambios estéticos y de funcionalidad... ¿quién mantendrá eso ordenado y legible dentro de un par de meses, con 200 o más entradas case, y probablemente con muchas "de basura"?

En realidad, en este momento no es demasiado importante la actitud a tomar. Pero en un par de meses será fundamental. Créanme, ese switch va a ser un problema. Si alguen lo detecta ahora, bien. Si lo corrige, mejor. Pero dentro de un tiempo, si no se corrige va a crecer transformándose en algo muy feo.

En la próxima entrada de la serie veremos una de las soluciones posibles para aislar al front-end de los problemas del crecimiento y los cambios en el menú.

Parte I: Resumen.

Parte II: La definción del archivo XML.

Parte III: Encapsulamiento.

Parte IV: Que funcione.

jueves, 6 de noviembre de 2008

Codificación: datos de la aplicación como recursos XML embebidos (parte III).

Parte I: Resumen.

Parte II: La definción del archivo XML.

Parte III: Encapsulamiento.

Parte IV: Que funcione.

III. Encapsulamiento.

Con lo que tenemos hasta aquí ya podríamos armar nuestro menú de ejemplo copiando y pegando el código de prueba que tenemos y modificándolo para recorrer los datos de acuerdo a las necedidades del caso.

Pero hay un tema extremadamente importante que aclarar antes de seguir: cuidado con XML. Podemos definirlo y utilizarlo muy fácilmente, y así de fácil se nos puede ir de las manos. Ya he comentado algunos ejemplos en este blog:

  • XML * Inconciencia.
  • XML (donde comenzaba con la muy acertada frase: "XML is like Violence, if it doesn't solve the problem, use some more").
  • Pobre XML.

XML es una muy buena herramienta para guardar los datos, pero nunca, nunca jamás (créanme) accedan directamente a ellos desde toda la aplicación.

En este sentido son asimilables a una base de datos: no accedemos a ellos creando una conexión y los objetos necesarios para ejecutar un comando SQL en cada módulo, clase o procedimiento en el que los necesitamos. Siempre creamos una capa de datos que puede ser más o menos compleja y con más o menos funcionalidades de acuerdo al caso, pero nunca, nunca accedemos a ellos directamente.

Las razones principales son:

  • Claridad del código.
  • Si cada vez que deseamos obtener información de un menú hacemos

    
    //......
    
                string resourceName = "EjemploRecursosXMLEmbebido.Menu.xml";
                
                XmlDocument menuesXML = new XmlDocument();
                using (Stream s = this.GetType().Assembly.GetManifestResourceStream(resourceName))
                {
                    menuesXML.Load(s);
                }
    
    //.... (etcétera)...
            
    

    Estaremos ensuciando el procedimiento con todo este montón de cuestiones acerca de los recursos, los streams y demás que dificultan la lectura.

  • Mantenimiento.
  • Imaginemos que utilizamos el código anterior en muchas clases de nuestro proyecto. El simple hecho de cambiar el nombre del recurso implicaría buscar la cadena "EjemploRecursosXMLEmbebido.Menu.xml" por todos lados y reemplazarla.

    Es fácil cometer errores al utilizar XPath. Tal vez no todos los programadores del equipo están familiarizados con él. En cada acceso podría cometerse un error diferente.

  • Mejoras, nuevas funcionalidades, reutilización.
  • Veremos que hay mucho para mejorar en la utilización de recursos XML. Las posibilidades son muchas y usualmente iremos aprendiendo sobre la marcha, a medida que surjan las necesidades. Por ello es que necesitamos tener encapsulado el acceso, para poder modificarlo sin que el resto del sistema se vea afectado.

Espero haberlos convencido. Si es así, estarán dispuestos a crear un par de clases que encapsulen el recurso y lo hagan invisible al resto de la aplicación.

Examinemos la estructura de cada nodo que representa un ítem del menú:

<menu id="compras">
  <titulo>Compras</titulo>
  <submenues>
    <!-- lista de elementos menu con la misma estructura-->
  </submenues>
</menu>

Podemos crear una clase (MenuItem) que encapsule ese nodo y exponga los datos que contiene como propiedades:

using System;
using System.Collections.Generic;
using System.Xml;

namespace EjemploRecursosXMLEmbebido
{
    public class MenuItem
    {
        XmlNode _nodo;

        public MenuItem(XmlNode nodo)
        {
            _nodo = nodo;
        }

        public string Id
        {
            get { return _nodo.SelectSingleNode("@id").InnerText; }
        }

        public string Titulo
        {
            get { return _nodo.SelectSingleNode("titulo").InnerText; }
        }

        public IEnumerable<MenuItem> Submenues()
        {
            XmlNodeList submenues = _nodo.SelectNodes("submenues/menu");

            foreach (XmlNode menuNodo in submenues)
                yield return new MenuItem(menuNodo);

        }

    }
}

Y otra clase que nos dé acceso al recurso (Menues):

using System;
using System.Xml;
using System.IO;
using System.Collections.Generic;

namespace EjemploRecursosXMLEmbebido
{
    public static class Menues
    {
        private static XmlDocument _menuDocument;
        private const string _resourceName = "EjemploRecursosXMLEmbebido.Menu.xml";
        
        static Menues()
        {
            _menuDocument = new XmlDocument();
            using (Stream s = typeof(Menues).Assembly.GetManifestResourceStream(_resourceName))
                _menuDocument.Load(s);
        }

        public static MenuItem Raiz
        {
            get { return new MenuItem(_menuDocument.SelectSingleNode("menu")); }
        }

    }
}

Ya estamos listos para otra prueba. Colocamos un nuevo botón sobre el formulario, cuyo código será:

        private void button2_Click(object sender, EventArgs e)
        {
            foreach (MenuItem menuItem in Menues.Raiz.Submenues())
            {
                MessageBox.Show(menuItem.Titulo);
            }
        }

Vemos que ahora, para nuestro front-end de pruebas, el XML simplemente no existe, ni siquiera "sabe" que es un recurso XML. Simplemente solicita a la clase Menues el menú raíz y recorre sus submenúes mostrando el título.

Pero seguimos sin tener un menú como la gente. Paciencia...

Parte I: Resumen.

Parte II: La definción del archivo XML.

Parte III: Encapsulamiento.

Parte IV: Que funcione.

martes, 4 de noviembre de 2008

Codificación: datos de la aplicación como recursos XML embebidos (parte II).

Parte I: Resumen.

Parte II: La definción del archivo XML.

Parte III: Encapsulamiento.

Parte IV: Que funcione.

II. La definción del archivo XML.

Para empezar con algo realmente simple, supongamos la siguiente estructura de menú definida en XML:

<?xml version="1.0" encoding="utf-8" ?>
<menu id="inicio">
  <titulo>Mi ERP 0.0.1</titulo>
  <submenues>
    <menu id="compras">
      <titulo>Compras</titulo>
      <submenues>
        <menu id="compras_presupuestos">
          <titulo>Presupuestos</titulo>
        </menu>
        <menu id="compras_ordenes">
          <titulo>Ordenes</titulo>
        </menu>
        <menu id="compras_seguimiento">
          <titulo>Seguimiento</titulo>
        </menu>
      </submenues>
    </menu>
    <menu id="ventas">
      <titulo>Ventas</titulo>
      <submenues>
        <menu id="ventas_catalogo">
          <titulo>Catálogo</titulo>
        </menu>
        <menu id="ventas_pedidos">
          <titulo>Pedidos</titulo>
        </menu>        
      </submenues>
    </menu>    
  </submenues>
</menu>

Como verán, es una organización recursiva de elementos menu. Cada uno de ellos tiene un atributo id que lo identifica unívocamente, un elemento título que corresponde al texto a mostrar al usuario y un elemento submenues que contiene una lista de elementos menu, cada uno de ellos con las mismas características.

En el ejemplo estamos definiendo la siguiente estructura de menú:

Mi ERP 0.0.1
 Compras
  Presupuestos
  Ordenes
  Seguimiento
 Ventas
  Catálogo
  Pedidos

Es lo mínimo indispensable para sacar algo por pantalla. Así que lo primero que tenemos que hacer es agregar un nuevo archivo xml al proyecto y establecerlo como recurso embebido. Esto último le indica al compilador que debe incrustar el archivo xml en el ensamblado.

Ya estamos listos para escribir la definción (o mejor, copiar y pegar el ejemplo anterior) y hacer una pequeña prueba: colocamos un botón sobre el formulario del proyecto. El código para el botón será:

        private void button1_Click(object sender, EventArgs e)
        {
            string resourceName = "EjemploRecursosXMLEmbebido.Menu.xml";
            
            XmlDocument menuesXML = new XmlDocument();
            using (Stream s = this.GetType().Assembly.GetManifestResourceStream(resourceName))
            {
                menuesXML.Load(s);
            }

            XmlNodeList titulos = menuesXML.SelectNodes("descendant::titulo");
            foreach (XmlNode titulo in titulos)
                MessageBox.Show(titulo.InnerText);

        }

(Nota: hay que agregar los using a los namespaces System.IO y System.Xml para que compile.)

La idea es cargar el archivo en un objeto XmlDocument para poder acceder a los datos por código utilizando XPath, que es el lenguaje de consulta para XML. Como prueba presentamos un MessageBox para cada título de menú en el archivo.

Aquí ya tenemos un par de líneas para resaltar. Por un lado, noten el nombre que se le asigna al recurso embebido: [nombre del ensamblado].[nombre del archivo (con extensión)]. En nuestro caso: "EjemploRecursosXMLEmbebido.Menu.xml".

El archivo se encuentra incrustado como recurso en el ensamblado. Para recuperarlo tenemos que obtener una referencia al ensamblado que lo contiene (clase Assembly) y a través del método GetManifestResourceStream obtener un Stream. Todo es lo hace la instrucción

using (Stream s = this.GetType().Assembly.GetManifestResourceStream(resourceName))

que coloca la referencia al Stream en la variable s.

Luego pasamos esa referencia como parámetro al método Load de un objeto XmlDocument. En nuestro caso, la variable menuesXML contiene al documento, por lo que hacemos:

menuesXML.Load(s);

El resto del código obtiene todos los nodos "titulo" del documento y muestra el texto que contiene.

La magia se termina aquí (no era mucha). Investigando un poco de XPath ya se podría recorrer el documento e ir cargando los datos en, por ejemplo, un TreeView. Lo que haremos de aquí en más será brindarle estructura a la solución de manera de hacerla funcional (todo muy lindo, pero esto no hace nada) y más sólida, mantenible y reutilizable.

Parte I: Resumen.

Parte II: La definción del archivo XML.

Parte III: Encapsulamiento.

Parte IV: Que funcione.

lunes, 3 de noviembre de 2008

Codificación: datos de la aplicación como recursos XML embebidos (parte I).

Parte I: Resumen.

Parte II: La definción del archivo XML.

Parte III: Encapsulamiento.

Parte IV: Que funcione.

Hoy tengo ganas de meter las manos en el barro y jugar un poco. Esta es la primera entrega de un artículo en el quiero ejemplificar algunas técnicas de manejo de recursos en C# con Visual Studio que hemos ido perfeccionando en el trabajo y que nos han resultado de muchísima utilidad.

I. Resumen.

Utilizamos recursos cuando se trata de almacenar datos propios de la aplicación que no deben ser modificados durante la ejecución y que conviene tener en un repositorio propio más que hardcodeados por todo el código.

El ejemplo típico, el texto que corresponde a etiquetas y mensajes con la posibilidad de que se requieran varios juegos por idioma, tiene un tratamiento muy específico (ver Recursos en aplicaciones (MSDN)).

Pero aquí pretendo ejemplificar el tratamiento de recursos que a) no dependen del idioma en el que el usuario esté utilizando la aplicación y b) son más complejos que un diccionario de cadenas de texto.

Para desarrollar el código, utilizaré como ejemplo el menú de la aplicación: supongamos que tenemos una aplicación con una estructura de funcionalidades organizada en menúes y submenúes, y que no queremos que ésta sea configurable por el usuario ni que pueda ser modificada de manera alguna luego de la compilación y el pasaje a producción.

Con estas especificaciones podríamos crear un menú directamente sobre el formulario principal del sistema utilizando el diseñador. Sin embargo, esto es muy incómodo cuando la aplicación es grande, y difícil de modificar y mantener a futuro. Por otro lado, estaríamos mezclando conceptualmente la estructura de funcionalidades (propia del negocio) con la forma específica de presentarlo al usuario. Si un día en vez de un menú queremos presentarla en un Treeview, o de cualquier otra manera, estaremos en problemas.

Empezaremos con algo simple y "que funcione" y luego lo iremos refinando.

Parte I: Resumen.

Parte II: La definción del archivo XML.

Parte III: Encapsulamiento.

Parte IV: Que funcione.

martes, 26 de agosto de 2008

La capa de acceso a datos.

A partir de los comentarios de la entrada "Jaque" me quedé dando vueltas alrededor de la arquitectura de la capa de datos en una aplicación.

El primer punto a definir es qué entendemos por capa de datos. Me gusta tomar Wikipedia porque siento que representa el mínimo común denominador entre todas las acepciones posibles. Una base sobre la que (creo) estaríamos todos de acuerdo. En la correspondiente entrada (Programación por capas) encontramos:

[...] Capa de datos: es donde residen los datos y es la encargada de acceder a los mismos. Está formada por uno o más gestores de bases de datos que realizan todo el almacenamiento de datos, reciben solicitudes de almacenamiento o recuperación de información desde la capa de negocio.

Lo que sigue a continuación es toda la secuencia de experiencia previa, análisis, pruebas y errores que nos llevó a la capa de acceso a datos que tenemos armada ahora, con la que estoy bastante conforme. Está claro que dependiendo del proyecto las atribuciones y responsabilidades de esta capa podrán variar sutilmente. Para el caso, necesito agregar una responsabilidad más, que puede o no asignarse a esta capa, pero que en todo caso quiero incluir en la discusión:

Transformar objetos que representan la estructura de la base de datos en otros que representan la lógica del negocio y viceversa.

Como decía, puede que se lo asignemos o no a la capa de datos, pero en lo que estaremos todos de acuerdo es en que los objetos que maneje la capa de negocios no deberían estar absolutamente condicionados por la estructura de los datos en la base, por más que tengan alguna relación con ellos o que utilicemos las tablas como punto de partida. Por ejemplo:

  • El módulo de facturación puede definir un objeto "Cliente" de acuerdo a sus necesidades, por ejemplo con una propiedad "Facturación" (una lista de facturas). Digamos que se llama "Facturacion.Cliente".
  • El módulo de CRM podría definir un objeto "Cliente" diferente ("CRM.Cliente"), más acorde a sus propias necesidades. Por ejemplo con una propiedad "Incidentes" que contiene una lista de las comunicaciones que tenemos con él.

Traducido a la capa de acceso a datos, estamos haciendo un JOIN entre la tabla de clientes y la de facturas por un lado y la de incidentes por el otro, más todas las tablas satélite que se puedan requerir en cada caso.

En el framework de trabajo existente hasta ese momento, cada objeto de negocio utilizaba una capa de datos para acceder a la base, pero definía su propia consulta utilizando el lenguaje específico, SQL, que se iba armando en el mismo objeto de negocio. La capa de datos devolvía objetos genéricos (DataTable, DataReader, etc.) que eran utilizados directamente. En este caso provee la persistencia y la conexión, pero se desentiende de la consulta en sí misma.

Los problemas más notorios son que el código de consulta ensucia la clase de negocio, no permite reutilización alguna y se producen infinitos puntos de acceso a datos. Por otro lado la utilización de objetos de datos genéricos como el DataTable hacen que el código sea difícil de seguir, sobre todo si se utilizan subíndices para acceder a los datos en particular (ej.: saldo=dataRow[1] + dataRow[3]).

Con este punto de partida, los primeros esfuerzos se orientaron a optimizar y ordenar el armado de esas consultas. En principio se adoptó una filosofía estricta de utilizar siempre procedimientos almacenados, para separar el código SQL del resto de la lógica.

Eso emprolija un poco el código, pero no resuelve los demás problemas. Era una tarea necesaria, pero que no atacaba el centro de la cuestión, representado por el ejemplo anterior: necesitamos manejar objetos de negocio específicos, no genéricos ("Cliente" en vez de "DataTable") y necesitamos acceder a propiedades evitando el uso de subíndices, entre otros.

Entonces comenzamos a armar una capa de acceso a datos que asumiera esa tarea de transformación de datos en conceptos de negocio, montado sobre el "proveedor de persistencia y conexión" (por llamarlo de alguna manera) existente.

Otro problema era lograr cierta reutilización. Estaba claro que si se creaba un procedimiento almacenado para cada necesidad estaríamos simplemente poniendo el muerto bajo otra alfombra. Todas la consultas que antes ensuciaban los objetos de negocio pasarían a formar un inmenso (e inmanejable) repositorio de procedimientos almacenados.

Aquí es donde entra en juego el problema de los JOIN en los procedimientos almacenados que se menciona al pasar en los comentarios de Jaque. En nuestro ejemplo, si hago un JOIN en la base de datos necesito dos consultas o procedimientos almacenados: una para Cliente-Facturas y otra para Cliente-Incidentes.

Algo similar sucede con las transacciones. Necesito grabar Clientes-Facturas en una única transacción. Si la implemento en un procedimiento almacenado, ese procedimiento sólo podrá grabar esa combinación de datos.

Era claro que por ese camino y sólo tomando las dos o tres tablas principales del sistema tendríamos una miríada de consultas diferentes, cada una respondiendo a una combinación cabecera-detalle diferente. Se descartó por ser a todas luces poco práctica la posibilidad de una súper-consulta que recupere (por ejemplo) un cliente y todos sus datos devolviendo una especie de súper-entidad. Está claro que la tabla cliente podría tener más de 20 tablas de detalle independientes (ej.: facturas, remitos, incidentes, pedidos, contactos, pagos, cheques, movimientos, y todo lo que se nos ocurra), y que rara vez se utilice más de un par en cada módulo de negocio (es claro que el módulo que maneje la chequera poco tiene que hacer con los incidentes). Por otro lado también se descartó la posibilidad de cargar esa súper-entidad "on demand" por presentarse demasiado compleja.

Es cuando surge como una posibilidad más sencilla el tener un conjunto de consultas estándar, las clásicas SELECT, INSERT, UPDATE y DELETE para cada tabla y con ellas llenar las estructuras de datos más complejas en el código. En la gran mayoría de los casos esto no representa problemas de rendimiento. Pero de todas maneras nada impide tener una consulta especial sólo para aquellas situaciones que sí lo justifiquen.

Surgieron así las clases de acceso a datos creadas automáticamente a partir de la estructura de cada tabla. Para nuestro ejemplo, tendríamos tres clases ya predefinidas: ClientesDatos, FacturasDatos, IncidentesDatos. El objeto de negocio las utiliza para llenar Facturación.Cliente.Facturas y CRM.Cliente.Incidentes en cada caso. Estas clases devuelven objetos tipados, también creados a partir de la estructura de las tablas.

Con las transacciones sucede lo mismo. Al momento de grabar un cliente y sus incidentes la clase de negocio abre una transacción, llama a las clases de datos correspondientes y la cierra. No puede abrirse una transacción en la misma clase de datos, ya que esto impediría la reutilización. Conceptualmente tiene sentido. Una transacción queda definida en la operación de negocio que se realice. No es lo mismo facturar que insertar un incidente, pero a la capa de datos no tiene por qué interesarle esto.

Pero estoy haciendo trampa, porque en mi ejemplo omití una situación común. Se puede argumentar, con razón, que IncidentesDatos devuelve, según mi ejemplo, incidentes para un cliente, y que probablemente otro módulo requiera, por ejemplo, "incidentes por responsable interno" o "incidentes sin responsable" o "incidentes no resueltos", etc. Para cada uno de estos casos deberíamos armar un procedimiento almacenado y estaríamos de vuelta con el problema anterior.

Esto era justamente lo que nos estaba comenzando a suceder. Si bien era fácil tener clasificados los procedimientos almacenados (ya que siempre se referían a una única tabla), la realidad era que por ese camino iban a ser demasiados.

Hubo que romper el mito de "todo procedimientos almacenados" (con cierta reticencia inicial por mi parte, pero por suerte la mejora se notó rápidamente, despejando las dudas). De vuelta a generar código automático, esta vez para crear objetos que encapsularan un criterio WHERE generado dinámicamente. Un especie de filtro extremadamente configurable para cada tabla.

La clave fue el encapsulamiento. Si la lógica de armar consultas "on the fly" se hubiese expandido sin control por todo el sistema estaríamos de vuelta en la situación inicial. Estos objetos (uno específico para cada tabla) se volvieron internos de la capa de datos.

Para resumir, en el cuerpo de un método de la capa de acceso a datos se crea uno de estos objetos, se lo configura con un par de líneas de código y al ejecutarlo nos devuelve un objeto tipado que representa los datos de una tabla de la base. Un objeto de la capa de negocio puede llamar a varios métodos de la capa de acceso a datos para crear objetos complejos según su necesidad.

Cambiamos procedimientos almacenados por métodos, que resultaron más fáciles de clasificar, mantener y controlar. Un ejemplo en este sentido: si cambio la estructura de una tabla y genero automáticamente el código de la clase de filtro para esa tabla, se provoca automáticamente un error de compilación en todos los métodos de datos que se vean afectados por el cambio (y sólo en ellos). Una vez arreglados estos errores de compilación puedo estar seguro de fue implementado sin mayores consecuencias. Es decir, si compila funciona, si no funciona no compila.

¿Y dónde quedaron los JOIN y los procedimientos almacenados en T-SQL? Reservados sólo para algunos casos especiales, donde la consulta se realiza sobre tablas muy grandes o donde es necesario alguna lógica adicional u optimización relacionada con los datos. Pero resultaron ser apenas un par de casos excepcionales.