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:
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....
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!
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.