April 13, 2005.

Click.. click.. tap.. RUN.. CRASH!!! Un mejor RAD!

La mayor debilidad se encuentra en su fortaleza

Y eso en cierto de la programación RAD. Click.. click.. tap.. RUN y !presto! una aplicación de base de datos completa con interfaz de usuario y edición de datos en minutos, gracias al poder de Delphi.

Click.. click.. tap.. RUN.. CRASH!!! es el resultado de programar una aplicación real al estilo de los tutoriales que muestran como se conectan los controles... como si realmente enseñaran arquitectura...

El diseño del acceso a datos en Delphi es el MEJOR equilibrado que he visto en todas las herramientas de desarrollo que he manejado (Visual FoxPro, Visual basic (aquí ni había un diseño), Java, .NET). Aunque cada herramienta/plataforma tiene puntos muy fuertes (por ejemplo, en .NET es más simple hacer un acceso a datos válido para un ambiente Web) la arquitectura de datos (el combo de TDatabase/TDataSource/TDataSet) permite:

  1. Una interfaz sencilla para navegar datos... Next, Prior, First, Last. Funciones de búsqueda simples como Locate y sofisticadas como Filter.
  2. Una forma simple de editar. Edit, Post, Add. Hermoso.
  3. Un enlace a los controles que no apesta, como en Visual Basic. Con buen desempeño, visual (incluso se ven los datos en tiempo de diseño) que se puede "desconectar" temporalmente para soportar algún proceso y que simplemente, es tan bueno que no hay que hacerle nada mas.
  4. Lo más importante: En lo que respecta a los controles, solo entienden una clase abstracta (TDataSource) y se puede intercambiar entre diversas versiones de TDataSet que conectan con distintas tecnologías como BDE, ADO, ODBC, Acceso directo o bases de datos como Sql Server, Firebird, Oracle, Access... Así que al contrario que en otras herramientas no hay que alterar la interfaz gráfica ni hacerle ningún código especial cuando se cambia la tecnología que accede a los datos (algo desagradablemente común).

Lo que la mayoría de nosotros no captamos es que la arquitectura de Delphi SEPARA el acceso a datos de la interfaz gráfica y de cómo se conectan AMBAS. Pero igual el uso/código que se ve normalmente NO explota esta característica.

Un ejemplo es cuando alguien decide (MUY inteligenteme) dejar de usar Paradox y volcarse a una base de datos como Firebird. Ahora el caudal de cambios que toca hacerle al código es impresionante....

  1. Toca cambiar los TDatabase de BDE a Interbase/Firebird. Este es simple... pero...
  2. Toca cambiar todos los TDataSet. Mucha gente mete los TDataSet en los formularios/reportes lo que es MUY mala idea. Otros son un poco mas listos y usan los TDataModules para ello, PERO, igual pueden ser muchos.
  3. Al pasar de un acceso basado en tablas como en paradox a uno por Sql o al cambiar DIALECTOS de Sql (como el que usa Sql Server a uno que usa Oracle) toca alterar las consultas y eso esta POR TODAS partes con codigo que encadena texto aqui y alla. Caos total.

Y la razón de todo esto, digo yo, es que nos emocionamos demasiado la primera vez que vimos lo simple que era el ENLACE de los datos y pensamos, hey! asi debo hacer toda mi aplicación!

Tecnicas de acceso a datos: La forma sencilla

Cuando uno aprende a programar conoce una de las cosas mas simples, las constantes. El ejemplo tipico es el del numero PI, el cual es algo como 3,1416. Y se nos enseña que es mas lindo hacer un

    const PI = 3.1416;

Porque en fin, algun día cambiara PI?... no! realmente porque asi es el código más claro. Ahora bien, la mayoría de la gente mete a lo bestia SQL asi:

    lcSql := 'SELECT Id FROM Clientes WHERE Id=' + QuoteStr(Id) + ' ORDER BY '+ CampoOrden;

Y parnafernalia como esa, que esta regada aqui y alla, mezclada entro los eventos de validación de los controles, los reportes, funciones.. es como una invasión marciana en una pelicula cuando el soldado a punto de morir grita !estan en todas partes! y si, este tipo de cosas tambien me asuntan.

Recuerdan las constantes? Por si acaso, el valor de PI puede cambiar pero en fin, las constantes hacen mas legible el código.

Lo que propongo es muy simple: En vez de meter Sql a lo bestia, lo dejamos encerrado como buen cachorro que es en una lista de constantes, como:

  unit SqlConst

  interface

  const SqlListaClientesPorId = 'SELECT Id FROM Clientes WHERE Id=%s ORDER BY %s';

  implementation end.

Y ahora si, existe un UNICO lugar donde alterar las definiciones de los sql. Un nuevo campo? Se paso de una tabla a una vista? No hay problema... bueno al menos ahora es claro DONDE buscar.

Pero si toda esta carreta fuera para decir que la solución es usar constantes... no!!! porque ser programador implica más!

La verdad, ese asunto de meter SQL como constantes tiene un uso limitado.

La mayoría de los sql son generados al vuelo y deben ser más flexibles... además igual no se ha solucionado el asunto de tener que cambiar de componentes de acceso a datos.

De vuelta al comienzo. Recuerdan las ventajas del modelo de acceso a datos de Delphi? Principalmente, TDataSet es una clase abstracta de la cual derivan las diversas implementaciones.

Asi que mejorando un poco, la idea es:

    unit AccesoDatos

    interface

    type
        TGeneradorSql = class(TObject)

    public function GetSelect(Tablas:String;Campos:String;Filtro:String;Orden:String=''):String     end;

    type TAccesoDatos = class(TObject)
        FCon:TAdoConnection;
        function Conectar:Boolean;public function ObtenerSql( lcSql : String ) : TDataSet;
        function EjecutarSql( lcSql : String ) : Integer;
        function ActualizarDatos ( DataSets:array of TDataSet);

        implementation

        function TAccesoDatos .Conectar:Boolean
        begin
            // Crear aqui la coneccion con la respectiva cadena de conexion...
        end;

        function TAccesoDatos .ObtenerSql( lcSql : String ) : TDataSet;
        begin
            // Llamar a Conectar. De esa manera se accese a los datos y Conectardetermina
            //si se soporta el modo StateFull o StateLess
            // Crear el respectivo TDataSet con los diferentes parametros. Ejecutar la consulta, y retornar
        end;

    function TAccesoDatos .EjecutarSql( lcSql : String ) : Integer;
    begin
            // Para comandos como INSERT, DELETE y UPDATE. Retornar el numero de registros afectados y automaticamente ejecutar en el contexto de una transaccion
    end;

    function TAccesoDatos .ActualizarDatos ( DataSets:array of TDataSet);
    begin
            // Dentro de una transaccion realizar los post a los TDataSet enviados.
        end;

    function TGeneradorSql.GetSelect(Tablas:String;Campos:String;Filtro:String;Orden:String=''):String
    begin
        // Hacer la concatenacion, como:Result := 'SELECT '+ Campos + ' FROM ' + Tablas + ' WHERE '+ Filtro + ' ORDER BY '+ Orden
    end;
    end.

Obviamente, el código que pongo es solo para exponer la idea. Entremos a detallar:

1- Existe un UNICO lugar donde se administra el acceso a datos. Lo que implica un unico lugar donde cambiar los componentes. Por ejemplo, la conexion es un TAdoConection, solo pocos metodos lo tocan asi que hacer el cambio es cuestion de minutos.

2- Metodos bien definidos para hacer selects, inserts, updates, que automatizan la logica de las transacciones, y virtualizan la creación de componentes permitiendo mayor reutilización y menor acople.

Hacerle una llamado es muy simple, algo como:

    DataSet := AccesoDatos.ObtenerSql(Sql_Clientes);

Noten que nada de tocar la conexión ni de crear objetos TAdo esto TDBE aquello....

3- Una clase que se encarga de hacer las concatenacions de las tablas, campos, filtros, orders, etc... Eso significa la posibilidad de adaptarse a los diferentes dialectos y MENOS errores por causa de concatenar mal las cosas.

Entonces es mas simple: lcSql := GeneradorSql.GetSelect("Clientes","Id","Id=1","Id") y teneros la certeza que se devuelve la cadena SQL correcta. Y podemos expandir la idea para llamar procedimientos almacenados o INSERTS/UPDATE/DELETE.

La verdad, hacer este código no demora menos de 1 semana, incluyendo un parser más sofisticado de Sql... pero es una libreria reusable y de fácil mantenimiento. Incluso haciendo esto me di el lujo de hacer una interface orientada a objetos con código así:

    oSql.Tablas.Add("Clientes")
    oSql.Campos.Add("Id","N")
    oSql.Filtro.Add("Id=1")
    oSql.Filtro.Add("Activo=1", AND) 
    lcSql := oSql.Sql;

Y se puede reversar, o sea, a partir de una cadena SQL se obtiene una defincion de las tablas, campos, filtros, etc...!!!

Ahora, el asunto es que se debería hacer subclases de acuerdo a cada aplicación. Piensen en algo como:

    type
        TAccesoDatosFacturacion = class(TAccesoDatos)
        public function ObtenerClientes(Filtro:String;Orden:String) : TDataSet;
        function ActualizarFactura(DataSets:array of TDataSets)
    end;

El asunto es mantener un unico lugar que se encarge de tocar la base de datos y sus tablas. Es increiblemente compacto el código resultante y es maravilloso la facilidad de uso que se logra y las ventajas de poder alterar el acceso a datos, no solo sin afectar la interface gráfica que Delphi se encarga de ello, sino sin alterar la LOGICA de nuestras aplicaciones!

Ahora bien, si quieren un producto más sofisticado, RemObjects tiene el framework DataAbstract, lo cual es una versión mucho mejor de la idea que presento aquí y que es ideal para un desarrollo más fuerte... darle una mirada también les puede ayudar a entender más el concepto.