Página principal
Artículos y trucos
Catálogo de productos
Ejemplos y descargas
Mis libros
Cursos de formación
Investigación y desarrollo
Libros recomendados
Mis páginas favoritas
Acerca del autor
 
En colaboración con Amazon
 
Intuitive Sight

Tablas en memoria

A veces olvido que existen dos versiones de Delphi además de la cliente/servidor. Comienzo a explicar técnicas para acelerar el trabajo con campos de búsquedas, o para hacer más robusta la entrada de datos master/detail, y propongo el uso de conjuntos de datos clientes (TClientDataSet). Pero Delphi Standard y Professional no tienen este componente.

LAS TABLAS EN MEMORIA DEL BDE

Por fortuna, el BDE ofrece una técnica relativamente sencilla para suplir esta carencia: las tablas en memoria. Una tabla en memoria se crea mediante la siguiente función del API del BDE:

function DbiCreateInMemTable(hDb: hDBIDb; pszName: PChar; iFields: Word;
   pfldDesc: pFLDDesc; var hCursor: hDBICu): DBIResult;

El primer parámetro es un handle a una base de datos; hace falta un alias para ubicar dentro del mismo a la tabla. pszName indica el nombre que le daremos a esta tabla; aunque, en realidad, no sé para qué es necesario este nombre. iFields es la cantidad de columnas que va a tener la tabla, y pfldDesc apunta a un array de valores del tipo FLDDesc, que contiene la descripción de las columnas. Por último, hCursor recibirá el handle del cursor de la recién nacida.

Hay varias limitaciones aplicables a este tipo de tablas. La más importante es que no se pueden definir índices sobre las mismas. Además, tampoco el BDE soporta directamente restricciones, ni valores por omisión, sobre sus columnas. En este sentido, TClientDataSet es mucho más potente. Además, no existe un mecanismo directo de guardar su contenido en un fichero plano, a no ser que utilicemos la función DbiBatchMove y un fichero ASCII como destino.

ENCAPSULANDO LA TABLA

Conozco pocos kamikazes dispuestos a trabajar directa y exclusivamente con el API del BDE. Si queremos aprovechar las tablas en memoria del BDE, debemos crear un componente que las haga "digeribles". Aquí explicaré los pasos básicos de esta técnica, y el código fuente completo puede encontrarse en la página de ejemplos.

Crearemos una nueva clase, TMemoryTable, cuyo ancestro será TDBDataSet. ¿Por qué esta clase? Pues porque necesitamos un alias abierto para crear el cursor de la tabla, y nos ahorraremos bastante código si aprovechamos la clase mencionada, que implementa las propiedades DatabaseName, Database y DBHandle; esta última propiedad es la encargada, además, de realizar la conexión con el alias cuando tratamos de recuperar su valor1.


Como consecuencia indeseada de la elección de este ancestro, la clase TMemoryTable publicará las propiedades Filter, Filtered y FilterOptions, aunque no estén correctamente implementadas.

Tenemos que redefinir la función CreateHandle, para obtener un cursor a la tabla en memoria; será en esta función donde llamaremos a DbiCreateInMemTable. Pero antes tenemos que decidir cómo configuraremos las columnas de la nueva tabla. Utilizaremos dos técnicas:

  1. La nueva propiedad CloneDataSet puede apuntar a una tabla o consulta, de la cual se extraerá las definiciones pertinentes.
  2. Aunque el tipo TDataSet ya contiene la propiedad FieldDefs, no la publica en el Inspector de Objetos. De esto nos encargaremos nosotros, además de implementar la función IsFieldDefsStored que utilizaremos en una cláusula stored para almacenar el valor de FieldDefs en el fichero DFM cuando el número de definiciones de campos sea distinto de cero.

Entre estos dos mecanismos, CloneDataSet tendrá mayor prioridad.

El núcleo de todo el componente es, como he mencionado, la función CreateHandle, cuya implementación incluyo a continuación:

function TMemoryTable.CreateHandle: HDBICur;
var
  OldActive: Boolean;
  DSProps: CurProps;
  FieldDesc: TFieldDescList;
  I, FieldCount: Word;
begin
  FieldCount := FieldDefs.Count;
  if FCloneDataSet <> nil then
  begin
    OldActive := FCloneDataSet.Active;
    try
      FCloneDataSet.Open;
      Check(DbiGetCursorProps(FCloneDataSet.Handle, DSProps));
      FieldCount := DSProps.iFields;
      SetLength(FieldDesc, FieldCount);
      Check(DbiGetFieldDescs(FCloneDataSet.Handle, pFLDDesc(FieldDesc)));
    finally
      FCloneDataSet.Active := OldActive;
    end;
  end
  else if FieldCount > 0 then
  begin
    SetLength(FieldDesc, FieldCount);
    for I := 0 to FieldCount - 1 do
      with FieldDefs[I] do
        EncodeFieldDesc(FieldDesc[I], Name, DataType, Size, Precision);
  end
  else
    DatabaseError('Dataset required');
  Check(DbiCreateInMemTable(DBHandle, PChar(FTableName), FieldCount,
    pFLDDesc(FieldDesc), Result));
end;

El método auxiliar EncodeFieldDesc ha sido copiado de la clase TTable, y transforma un TFieldDef en un valor de tipo FLDDesc, del API del BDE, haciendo uso de algunas constantes globales de la unidad DBCommon:

procedure TMemoryTable.EncodeFieldDesc(var FieldDesc: FLDDesc;
  const Name: string; DataType: TFieldType; Size, Precision: Word);
begin
  with FieldDesc do
  begin
    StrPLCopy(szName, Name, SizeOf(szName) - 1);
    iFldType := FldTypeMap[DataType];
    iSubType := FldSubTypeMap[DataType];
    case DataType of
      ftString, ftFixedChar, ftBytes, ftVarBytes, ftBlob..ftTypedBinary:
        iUnits1 := Size;
      ftBCD:
        begin
          { Default precision is 32, Size = Scale }
          if (Precision > 0) and (Precision <= 32) then
            iUnits1 := Precision
          else
            iUnits1 := 32;
          iUnits2 := Size;  {Scale}
        end;
    end;
  end;
end;

UN MUNDO DE POSIBILIDADES

Esta misma técnica (crear descendientes de TBDEDataSet redefiniendo simplemente CreateHandle) puede aplicarse a muchos otros tipos de cursores especiales del BDE. Por ejemplo, existe una larga lista de funciones, que puede buscar en la ayuda del BDE tecleando "DbiOpenList functions", y que devuelven información sobre tablas disponibles, la estructura de las mismas, los alias y controladores registrados, etc. Lo más interesante consiste en que, una vez que redefinimos CreateHandle, el resto de los métodos heredados desde TBDEDataSet y TDataSet se encargan de la configuración del buffer del conjunto de datos, de la creación de componentes TFields y todo lo demás. Quizás más adelante muestre ejemplos de clases que utilicen estas otras funciones.


¿Ha escuchado o leído, alguna vez, el vocablo procrastination? Consulte la página 252 del libro "Delphi Component Design", de Danny Thorpe (ISBN 0-201-46136-6).