Acceso a bases de datos


Acceso a bases de datos relacionales



La mayoría de las aplicaciones de gestión requieren el acceso a bases de datos. C++ Builder incluye componentes que facilitan enormemente el desarrollo de aplicaciones que interactúan con bases de datos. Usualmente trabajaremos con bases de datos relacionales (bases de datos constituidas por tablas).

Antes de seguir, definamos dos conceptos básicos:

Bases de Datos Locales

El modelo más sencillo de aplicación que accede a bases de datos es aquél en el cual se accede a bases de datos locales, también denominadas bases de datos de escritorio La base de datos reside en una sola máquina (generalmente la misma en la que se ejecuta la aplicación), y sólo puede tener acceso a la misma un único usuario. Ejemplos de este modelo son dBase, Paradox, Approach o Access.

Bases de Datos Cliente/Servidor

En este caso, un gestor de bases de datos (DBMS) mantiene la base de datos. El almacenamiento y gestión del acceso a los datos los realiza el gestor (el servidor). Los gestores de bases de datos relacionales también se conocen como servidores SQL, por ser éste el lenguaje que se utiliza para acceder a los datos. Oracle, IBM DB2, InterBase y SQL Server entran dentro de esta categoría.

Las aplicaciones de los usuarios (los clientes) hacen peticiones al servidor utilizando algún protocolo predefinido, CLI [Call-Level-Interface] en inglés, que puede ser un estándar (ODBC, JDBC o BDE) o específico de la base de datos que utilicemos. Como es lógico, varios usuarios situados en distintos puestos de trabajo de una red pueden compartir el mismo servidor SQL, que será el responsable de mantener la "acidez" de las transacciones [Atomicity-Consistency-Isolation-Durability].

Arquitecturas multicapa


BDE

El Motor de Bases de Datos de Borland



Para permitir el acceso a bases de datos locales y servidores SQL, C++ Builder proporciona el Borland Database Engine (BDE), que no es más que una colección de DLLs. El BDE es un CLI [Call-Level Interface] que hace de intermediario entre las aplicaciones y las bases de datos a las que se accede, de forma que éstas sean independientes de la base de datos que se utilice.

BDE

El BDE necesita un "alias" para acceder a una base de datos particular. Un alias de BDE establece el conjunto de parámetros que se requieren para establecer una conexión con una base de datos, tales como el controlador [driver] empleado y la localización de la base de datos. Los alias se definen con ayuda del BDE Administrator (que se encuentra en el Panel de Control de Windows).

Ejemplo
  • Crear una aplicación nueva. Guardar el proyecto con el nombre BDEje, y el formulario con el nombre Ppal.cpp.
  • Añadir un componente TTable (en la página Data Access) al formulario.
  • Ver qué valores se pueden seleccionar para la propiedad DataBaseName, los alias BDE disponibles en la máquina.
  • Ejemplo: Configuración del driver para InterBase


    Componentes de acceso a Bases de Datos



    Los componentes de la VCL para el acceso a bases de datos se dividen en categorías siguiendo el modelo MVC (Model-View-Controller):

    El asistente

    Aunque podemos crear nuestras aplicaciones de bases de datos directamente a partir de los componentes de bases de datos, C++ Builder nos ofrece un "potente" asistente para generar formularios sencillos que accedan a bases de datos. Para utilizar el asistente hay que seguir los siguientes pasos:

    1. Llamar al asistente como si fuésemos a añadir a nuestro proyecto un formulario más (con "File/New..."), seleccionando "Database Form Wizard" de la pestaña "Business":

      New... Database Form Wizard

      También se puede llamar al asistente directamente desde la opción Form Wizard del menú Database.

    2. Seleccionar el tipo de formulario que deseemos (simple o maestro/detalle) y cómo queremos generarlo (mediante tablas o consultas). Para comenzar, optaremos por un formulario simple construido con una tabla (TTable):

      Type of form

    3. Seleccionamos la tabla a la cual deseamos acceder. En este caso, seleccionamos la base de datos de demostración que aparece bajo el alias de BCDEMOS y, dentro de ella, la tabla BIOLIFE.DB:

      Table

    4. Una vez seleccionada la tabla, hemos de indicar qué campos incluiremos en nuestro formulario. Seleccionamos todos (>>) menos la longitud en pulgadas, LENGTH_IN (<):

      Fields

    5. Ahora podemos escoger la disposición de los controles en el formulario: horizontal, vertical o en una rejilla. Probaremos inicialmente con la disposición horizontal de los controles en el formulario.

      Layout

    6. Finalmente, le indicamos al asistente que cree un formulario acompañado por un módulo de datos independiente (para separar los objetos que modelizan los conjuntos de datos de los controles que constituyen la vista de esos conjuntos de datos):

      Finish

    7. El resultado obtenido es el siguiente:

      Form

      El formulario de la imagen está bien aunque aún necesita algunos retoques. Por ejemplo, los campos de tipo MEMO (textos) o BLOB (imagen en el ejemplo) no deben mostrarse en una simple caja de edición. Por desgracia, al indicarle al asistente que pusiese los campos horizontalmente, ha puesto todos los campos de la tabla como si se pudiesen editar con un simple TDBEdit.

    Disposición vertical de los controles

    1. Para evitar el error que se producía anteriormente (el uso indiscriminado de cajas de edición incluso para las imágenes), podemos optar por disponer los campos verticalmente (tal como se hace desde tiempos del dBase):

      Layout

    2. Hemos de indicar, además, si queremos que las etiquetas que indican los nombres de los campos aparezcan a la izquierda o encima de los controles que nos permiten editarlos. Seleccionamos la primera de las opciones:

      Layout

    3. Como en el ejemplo anterior, le indicamos al asistente que cree un formulario acompañado por un módulo de datos independiente:

      Finish

    4. ... y el resultado que obtenemos es el siguiente:

      Form

      Este formulario está algo mejor que el inicial y, para que se vea correctamente la imagen, sólo hemos de poner a true la propiedad Stretch del control que nos la muestra (el único TDBImage del formulario) y ajustar un poco el tamaño de los distintos controles.

    Formularios maestro/detalle

    Utilicemos ahora el asistente para algo un poco más complejo:

    1. Creamos ahora un formulario maestro/detalle (master/detail) con objetos de tipo TTable para modelizar los conjuntos de datos:

      Master/Detail Form

    2. Seleccionamos la tabla CLIENTS.DBF de la base de datos BCDEMOS como tabla maestra del formulario, una tabla que contiene información acerca de los clientes de una empresa:

      Master Table

    3. Escogemos los campos que nos interese mostrar y los ordenamos utilizando los botones que aparecen debajo de la lista de campos seleccionados:

      Master Fields

    4. Indicamos que los campo de la tabla maestro los dispondremos verticalmente (para evitar que nos pase lo de antes con las imágenes y los textos):

      Master Layout

      Master Layout

    5. Seleccionamos la tabla detalle, que será la tabla HOLDINGS.DBF de la base de datos BCDEMOS (tabla que contiene las inversiones en bolsa de nuestros clientes):

      Detail Table

    6. Escogemos todos sus campos:

      Detail Fields

    7. Y seleccionamos una rejilla para visualizarlos ya que, usualmente, las tablas detalle siempre se muestran en rejillas:

      Detail Layout

    8. Para completar nuestro formulario maestro/detalle hemos de especificar cómo se reliza la reunión entre las tablas CLIENTS.DBF y HOLDINGS.DBF, a través del índice existente sobre la columna ACCT_NBR:

      Join

      Join

    9. Como siempre, le indicamos al asistente que cree un formulario acompañado por un módulo de datos independiente:

      Finish

    10. Voilà! Ya tenemos listo un formulario maestro/detalle y, con unos cuantos retoques, la presentación será la adecuada para incluirlo en alguna de nuestras aplicaciones:

      Master/Detail Form

    Ejercicios prácticos
    • Tipo del Formulario: maestro/detalle con objetos TTable.
    • Tabla maestra: Alias BCDEMOS, tabla customer.db.
    • Campos de la tabla maestra: CustNo, Company, Addr1, City, State, Country, Phone (en ese orden).
    • Disposición de los campos de la tabla maestra: Horizontally.
    • Tabla detalle: Alias BCDEMOS, tabla orders.db.
    • Campos de la tabla detalle: CustNo, OrderNo, SaleDate, TaxRate, ItemsTotal (en ese orden).
    • Disposición de los campos de la tabla detalle: In a Grid.
    • Definir la reunión (join) seleccionando el índice CustNo: join CustNo -> CustNo.
    • Tipo del Formulario: maestro/detalle con objetos TTable.
    • Tabla maestra: Alias BCDEMOS, tabla employee.db.
    • Tabla detalle: Alias BCDEMOS, tabla orders.db.
    • Tipo del Formulario: maestro/detalle con objetos TTable.
    • Tabla maestra: Alias BCDEMOS, tabla orders.db.
    • Tabla detalle: Alias BCDEMOS, tabla items.db.
    • Tipo del Formulario: maestro/detalle con objetos TTable.
    • Tabla maestra: Alias BCDEMOS, tabla vendors.db.
    • Tabla detalle: Alias BCDEMOS, tabla parts.db.
    • Tipo del Formulario: maestro/detalle con objetos TTable.
    • Tabla maestra: Alias BCDEMOS, tabla event.db.
    • Tabla detalle: Alias BCDEMOS, tabla reservat.db.
    • Tipo del Formulario: maestro/detalle con objetos TTable.
    • Tabla maestra: Alias BCDEMOS, tabla industry.db.
    • Tabla detalle: Alias BCDEMOS, tabla master.db.
    • Tipo del Formulario: maestro/detalle con objetos TTable.
    • Tabla maestra: Alias BCDEMOS, tabla master.db.
    • Tabla detalle: Alias BCDEMOS, tabla holdings.db.

    Limitaciones del asistente

    Como hemos visto, el asistente nos permite crear rápidamente formularios que nos permiten gestionar conjuntos de datos. No obstante, cualquier aplicación real requerirá formularios más complejos que los vistos hasta ahora y tendremos que olvidarnos del asistente y crearlos nosotros mismos.

    Ejercicios para los cuales el asistente se queda corto
    Mostrar la relación entre los lugares en que se celebran actividades (venues.db) y los clientes que acuden a cada recinto (custoly.db). Cada evento (event.db) tiene un lugar de celebración (venues.db) y una serie de reservas (reservat.db) realizadas por clientes (custoly.db).
    Mostrar los pedidos de cada cliente (orders.db como detalle de customer.db) indicando los detalles de los pedidos (items.db) y el proveedor de cada pìeza (relación entre items.db y parts.db, que además está relacionada con vendors.db).
    Mostrar las inversiones de los clientes (relación entre holdings.dbf y clients.dbf) indicando el nombre de las empresas en las que se invierte (almacenado en master.dbf), no sólo su símbolo.
    Mostrar el total de inversiones (holdings.dbf) de cada cliente (clients.dbf) o el importe total de lo que ha ganado cada proveedor (vendors.db) con los pedidos realizados hasta el momento (orders.db).

    Bases de datos de demostración

    Base de datos de inversiones en bolsa

    Base de datos de reserva de entradas

    Base de datos de pedidos

    NOTA: Las tablas ANIMALS.DBF, BIOLIFE.DB y COUNTRY.DB completan el conjunto de bases de datos incluidas como ejemplo en la distribución del C++Builder.


    Controles de acceso a datos



    Todos ellos tienen un control homólogo estándar en la paleta de componentes. Se diferencian de los controles estándar en que están enlazados a datos (son componentes data-aware). Dicho enlace se establece mediante un DataSource.

    Estos componentes trabajan con los datos seleccionados por las propiedades DataSource y, en su caso, DataField:

    Propiedad Descripción
    DataSource Indica el datasource del que se obtienen y al que se envían los datos.
    DataField Es el campo o columna del datasource al que accede cada control.

    A continuación, presentaremos los distintos controles de datos del C++ Builder

    Controles orientados a conjuntos de datos
    Control Descripción
    TDBGrid Rejilla de datos: Muestra un DataSource con aspecto de tabla. Mediante la propiedad Columns se pueden especificar qué campos del conjunto de datos se pueden ver en la rejilla y cuáles son sus propiedades.
    TDBCtrlGrid Rejilla que permite incluir controles: Como un TDBGrid, aunque cada fila es en realidad un panel en el que se pueden colocar controles de datos "replicables".
    TDBNavigator Control de navegación: Sirve para controlar el cursor, así como la inserción, edición y borrado de datos. La propiedad VisibleButtons se utiliza para seleccionar qué botones podrá pulsar el usuario. Por su parte, la propiedad Hints permite especificar mensajes de ayuda para cada botón (que habrá que traducir).
    TDBChart Permite construir gráficos a partir de un conjunto de datos. La propiedad SeriesList se emplea para seleccionar qué datos deseamos representar y cómo
     
    Controles orientados a campos
    Control Descripción
    TDBText Como el componente estándar TLabel, pero enlazado a datos. Muestra el valor de un campo, el cual no se puede editar. Igual que las etiquetas normales, no consume recursos en Windows.
    TDBEdit Presenta un cuadro de edición para un campo concreto. Análogo a TEdit.
    TDBMemo Caja de edición con múltiples líneas. Ideal para textos sin formato.
    TDBCheckBox Se emplea en campos que sólo permiten dos valores diferentes. Con las propiedades ValueChecked y ValueUnchecked se establecen listas de valores para que el TDBCheckBox esté activado o desactivado. Dichas listas se especifican mediante una serie de valores separados por punto y coma.
    TDBRadioGroup Se utiliza para campos que sólo permiten varios valores. En la propiedad Items se fijan los identificadores que el usuario ve en el grupo de botones. En la propiedad Values se indican los valores que en realidad se almacenan en la base de datos.
    TDBListBox Establece una lista de posibles valores para un campo. En la propiedad Items se fijan los valores permitidos, que son fijos.
    TDBComboBox TComboBox enlazado a datos. Igual que en el control TDBListBox, las distintas posibilidades se colocan en la propiedad Items.
    TDBLookupListBox Ideal para claves externas: Como TDBListBox, aunque la lista de valores permitidos no la establece el programador. Los valores de la lista se obtienen a partir de los valores de un campo de otro conjunto de datos. El DataSource que se consulta se establece con la propiedad ListSource. El campo que se muestra en la lista de valores viene dado por la propiedad ListField. La propiedad KeyField indica el campo cuyos valores se almacenan en la base de datos.
    TDBLookupComboBox Igual que TDBLookupListBox, aunque la lista aparece como un TComboBox.
    TDBRichEdit Igual que TDBMemo, con la capacidad adicional de dar formato al texto del campo (en formato RTF).
    TDBImage Para mostrar imágenes contenidas en una base de datos (en formato BMP o WMF).

    El comportamiento de todos los controles anteriores ante modificaciones se puede alterar con las propiedades AutoEdit y ReadOnly.

    Bases de datos con fechas

    Se puede adaptar TDateTimePicker (página Win32 de la paleta de componentes) para que funcione con un TDataSource especificando sus eventos OnStateChange, OnDataChange y OnUpdateData. También es necesario personalizar el evento OnExit del componente TDateTimePicker.

    Ejemplo
    class TFormDate : public TForm
    {
      ...
      private:
        bool FCambiando;
      ...
    };
    
    void __fastcall TFormDate::DataSourceDataChange(TObject *Sender, TField *Field)
    {
      if (! FCambiando)
         try {
           FCambiando = True;
           DateTimePicker->DateTime = Table->FieldValues["HireDate"];
         } __finally {
           FCambiando = False;
         }
    }
    
    void __fastcall TFormDate::DataSourceUpdateData(TObject *Sender)
    {
      Table->FieldValues["HireDate"] = DateTimePicker->DateTime;
    }
    
    
    void __fastcall TFormDate::DateTimePickerChange(TObject *Sender)
    {
      if (! FCambiando)
         try {
           FCambiando = True;
           DataSource->Edit();
         } __finally {
           FCambiando = False;
         }
    }
    
    void __fastcall TFormDate::DateTimePickerExit(TObject *Sender)
    {
      Table->UpdateRecord();
    }
    

    Uso de rejillas

    La tecla 'Insert' permite añadir tuplas, mientras que la combinación 'Control+Supr' se utiliza para eliminarlas. Por defectro, aparece un mensaje en inglés que se puede personalizar interceptando el evento BeforeDelete del conjunto de datos asociado:

    Ejemplo de personalización de los mensajes mostrados al usuario
    void __fastcall ...::TableBeforeDelete(TDataSet *DataSet)
    {
      if ( MessageDlg ( "¿Desea eliminar la tupla actual?",
                        mtConfirmation, 
                        TMsgDlgButtons()<<mbYes<<mbNo, 0) != mrYes )
         Abort();
    }
    

    Interceptando el evento OnDrawColumnCell se puede personalizar la presentación de las celdas: cambiar el color, el tipo de letra e incluso mostrar imágenes o checkboxes sin tener que recurrir al componente TDBCtrlGrid.

    Personalización del color de un campo determinado (según el cumplimiento de alguna condición)
    void __fastcall TFormXXX::DBGridDrawColumnCell
        (TObject *Sender, const TRect Rect, int DataCol, TColumn *TColumn, TGridDrawState State)
    {
      TDBGrid *grid = static_cast<TDBGrid*>(Sender);
    
      if (tbParts->FieldValues["OnOrder"] >= tbParts->FieldValues["OnHand"])
         grid->Canvas->Font->Color = clRed;
    
      grid->DefaultDrawColumnCell(Rect, DataCol, Column, State);
    }
    
     
    Propiedades del tipo de letra
    void __fastcall TFormXXX::DBGridDrawColumnCell
        (TObject *Sender, const TRect& Rect, int DataCol, TColumn *Column, TGridDrawState State)
    {
      TDBGrid *grid = static_cast<TDBGrid*>(Sender);
      TFontStyles FS;
    
      switch (grid->DataSource->DataSet->UpdateStatus()) {
        case usModified: FS << fsBold; break;
        case usInserted: FS << fsItalic; break;
        case usDeleted: FS << fsStrikeOut; break;
      }
    
      grid->Canvas->Font->Style = FS;
      grid->DefaultDrawColumnCell(Rect, DataCol, Column, State);
    }
    
     
    Imágenes en vez de texto
    void __fastcall TFormXXX::DBGridDrawColumnCell
        (TObject *Sender, const TRect Rect, int DataCol, TColumn *TColumn, TGridDrawState State)
    {
      TDBGrid *grid = static_cast<TDBGrid*>(Sender);
    
      if (Column->FieldName != "")
         grid->DefaultDrawColumnCell(Rect, DataCol, Column, State);
      else if (tbEmpleados->FieldValues["Salary"] >= 150000)
         grid->Canvas->StretchDraw(Rect, CaraAlegre->Picture->Graphic);
      else
         grid->Canvas->StretchDraw(Rect, CaraTriste->Picture->Graphic);
    }
    
     
    CheckBoxes (aunque es más cómodo y flexible utilizar TDBCtrlGrid)
    void __fastcall TFormXXX::DBGridDrawColumnCell
        (TObject *Sender, const TRect Rect, int DataCol, TColumn *TColumn, TGridDrawState State)
    {
      if (CompareText(Column->FieldName, "ACTIVO") == 0) {
         UINT check = 0;
     
         if (Table1->FieldValues["ACTIVO"])
            check = DFCS_CHECKED;
     
         DBGrid->Canvas->FillRect(Rect);
         DrawFrameControl(DBGrid1->Canvas->Handle, (RECT*) &Rect, DFC_BUTTON, DFCS_BUTTONCHECK | check);
    
      } else 
         DBGrid->DefaultDrawColumnCell(Rect, DataCol, Column, State);
    }
    

    Podemos seleccionar la columna por la que se ordenan los datos usando la propiedad IndexFieldNames del conjunto de datos. Esta propiedad la podemos establecer dinámicamente como respuesta al evento OnTitleClick.

    Ordenación dinámica de un conjunto de datos
    void __fastcall TFormXXX::DBGridTitleClick(TColumn *Column)
    {
      try {
        if (Column->Field->FieldKind == fkLookup)
           Table->IndexFieldNames = Column->Field->KeyFields;
        else
           Table->IndexFieldNames = Column->FieldName;
      } catch(Exception&) {
      }
    }
    

    Pistas y trucos


    El componente TDataSource



    TDataSource

    El componente TDataSource ofrece un mecanismo para enganchar los conjuntos de datos (Table, TQuery y TStoredProc, por ejemplo) con los controles visuales que muestran los datos (TDBGrid, TDBEdit, TDBListBox, etc.). TDataSource se encarga de toda la comunicación necesaria entre ellos.

    Para cambiar el conjunto de datos enlazado a un componente de tipo TDataSource (ya sea éste una tabla o una consulta), solamente hay que modificar su propiedad DataSet.

    La propiedad AutoEdit del componente TDataSource determina si se invoca automáticamente al método Edit del conjunto de datos asociado cuando un control (un TDBEdit, por ejemplo) recibe el foco.

    Implementación de componentes "data-aware"

    Para crear nuestros propios componentes de forma que se puedan enlazar a un conjunto de datos hemos de automatizar el intercambio de mensajes con los componentes TDataSource. El código común a todos los controles de datos está recogido en TDataLink (y en su descendiente TFieldDataLink), de forma que podemos utilizarlo para hacer nuestros componentes "data-aware".

    TDataLink


    TDataSet



    TDataSet

    Conjuntos de datos

    TDataSet es la clase superior de la jerarquía de clases definida en C++Builder para definir conjuntos de datos. TTable, TQuery y TStoredProc son casos particulares de conjuntos de datos. De hecho, la mayoría de las propiedades, métodos y eventos que se utilizan en estas clases están definidas realmente en TDataSet, en TBDEDataSet o en TDBDataSet:

    Propiedades

    Propiedad Descripción
    Active Abre el conjunto de datos cuando se establece a true y lo cierra cuando se fija a false.
    Bof Devuelve true si el cursor está en el primer registro del conjunto de datos.
    CachedUpdates Cuando está a true, las actualizaciones se mantienen en la caché de la máquina del cliente hasta que el programador los envíe al servidor. Cuando está a false, todos los cambios realizador en los conjuntos de datos se pasan automáticamente a la base de datos.
    CanModify Determina si el usuario puede editar el conjunto de datos o no.
    DataSource El componente TDataSource asociado.
    DatabaseName El nombre de la base de datos con la que se está trabajando.
    Eof Devuelve true cuando el cursor está llega al final del conjunto de datos.
    FieldCount Es el número de columnas o campos del conjunto de datos.
    Fields Columnas del conjunto de datos: array de objetos de tipo TField.
    FieldValues Devuelve el valor de un campo especificado en el registro actual.
    Found Indica si una operación de búsqueda ha tenido éxito o no.
    Modified Indica si el registro actual ha sido modificado.
    RecNo El número de registro actual en el dataset.
    RecordCount Devuelve el número de registros en el dataset.
    State Devuelve el estado actual del conjunto de datos.
    UpdatesPending Cuando está a true, el buffer del conjunto de datos contiene modificaciones de los datos que aún no han sido enviadas a la base de datos.

    Estados

    Estados de TDataSet

    vg: Post graba una nueva fila si el estado es dsInsert, actualiza la actual si es dsEdit

    Métodos

    Método Descripción
    Append() Crea un registro vacío y lo añade al final del conjunto de datos.
    AppendRecord() Añade un registro especificado al final del conjunto de datos.
    ApplyUpdates() Envía a la base de datos cualquier actualización en caché que estuviese pendiente. La escritura no se realiza realmente hasta que se invoca al método CommitUpdates().
    Cancel() Cancela cualquier modificación en el registro actual si no ha sido enviada con Post().
    CancelUpdates() Cancela cualquier actualización en caché que estuviese pendiente.
    ClearFields() Vacía el contenido de todos los campos del registro actual.
    CommitUpdates() Se indica a la base de datos que realice las actualizaciones que estaban en caché y se vacía el buffer de actualizaciones en caché.
    Close() Cierra el conjunto de datos.
    Delete() Elimina el registro actual.
    DisableControls() Desactiva los controles de datos asociados al conjunto de datos (muy útil para evitar el parpadeo de los controles visuales).
    Edit() Permite la edición del registro actual.
    EnableControls() Activa los controles de datos asociados al conjunto de datos.
    FetchAll() Obtiene todos los registros desde la posición actual del cursor hasta el final del conjunto de datos y los almacena localmente.
    FieldByName() Devuelve un campo TField dado su nombre.
    First() Mueve el cursor al primer registro.
    GetFieldNames() Recupera la lista con los nombres de los campos del conjunto de datos.
    Insert() Inserta un registro en blanco y pone el conjunto de datos en modo de edición.
    InsertRecord() Inserta un registro en el conjunto de datos con los datos que se le indiquen y los envía con Post().
    Last() Sitúa el cursor en el último registro.
    Locate() Búsqueda de un registro en particular.
    Lookup() Localiza un registro por el medio más rápido posible y devuelve el dato contenido en el mismo (se usan índices si es posible).
    MoveBy() Mueve el cursor un número determinado de filas.
    Next() Mueve el cursor al siguiente registro.
    Open() Abre el conjunto de datos.
    Post() Escribe los datos modificados de un registro en el conjunto de datos (los envía a la base de datos o al buffer de actualizaciones en caché).
    Prior() Mueve el cursor al registro anterior.
    Refresh() Actualiza el contenido del registro actual leyéndolo de la base de datos.
    RevertRecord() Cuando se utiliza la caché, este método ignora los cambios hechos previamente que todavía no han sido enviados a la base de datos.
    SetFields() Establece los valores para todos los campos de un registro.
    UpdateStatus() Devuelve el estado actual cuando las actualizaciones en caché se activan.

    Eventos

    Evento Descripción (¿cuándo se genera?)
    AfterCancel Después de cancelar las modificaciones.
    AfterClose Cuando se cierra un conjunto de datos
    AfterDelete Después de que un registro se haya eliminado.
    AfterEdit Después de que se ha modificado un registro.
    AfterInsert Después de insertar un registro.
    AfterOpen Después de abrir un conjunto de datos.
    AfterPost Después de enviar los cambios de un registro.
    BeforeCancel Antes de que se cancelen los cambios.
    BeforeClose Antes de que se cierre un conjunto de datos.
    BeforeDelete Antes de eliminar un registro.
    BeforeEdit Antes de entrar en modo edición.
    BeforeInsert Antes de que se inserte un registro.
    BeforeOpen Antes de abrir el conjunto de datos.
    BeforePost Antes de enviar las modificaciones.
    OnDeleteError Cuando ocurre algún error al eliminar una tupla.
    OnEditError Cuando ocurre algún error al editar una tupla.
    OnNewRecord Siempre que se añade un nuevo registro.
    OnPostError Cuando ocurre un error al enviar los cambios.
    OnUpdateError Cuando ocurre un error al actualizar los datos.
    OnUpdateRecord Cuando se actualiza el registro en la BD.

    EJEMPLOS DE USO DE EVENTOS

    Introducción de datos

    Asignación de valores por omisión en el evento OnNewRecord utilizando FieldValues

    void __fastcall TDataModuleXXX::TableNewRecord(TDataSet *DataSet)
    {
      DataSet->FieldValues["Date"] = Date();
    }
    

    Validaciones a nivel de registros

    Comprobaciones como respuesta al evento BeforePost, con la posibilidad de generar una excepción (DatabaseError)

    void __fastcall TDataModulePersonal::tbEmpleadosBeforePost(TDataSet *DataSet)
    {
      if (Date() - tbEmpleadosHireDate->Value < 365
         && tbEmpleadosSalary->Value > TopeSalarial)
         DatabaseError("¡Este es un enchufado!", 0);
    }
    

    Eliminación en cascada

    Eliminar las líneas detalle de una relación master/detail interceptando el evento BeforeDelete

    void __fastcall TDataModulePedidos::tbPedidosBeforeDelete(TDataSet *DataSet)
    {
      tbLineas->First();
     
      if (! tbLineas->Eof)
         if (MessageDlg("¿Eliminar detalles?",mtConfirmation, TMsgDlgButtons()<<mbYes<<mbNo, 0)==mrYes) {
            do {
               tbLineas->Delete();
            } while (! tbLineas->Eof);
         } else {
            Abort();
    	 }
    }
    

    Gestión de errores

    Eventos de detección de errores: OnEditError, OnPostError, OnDeleteError

    Errores

    Posible solución ante un error de bloqueo no concedido
    void __fastcall TDataModuleXXX::TableEditError
        (TDataSet *DataSet, EDatabaseError *E, TDataAction &Action)
    {
      // Consultar al usuario
    
      if ( MessageDlg ( E->Message, mtWarning, 
                        TMsgDlgButton() << mbRetry << mbAbort, 0) == mrRetry ) {
    
         // Esperar entre 1 y 2 segundos antes de reintentar
    
         Sleep(1000 + random(1000));
         Action = daRetry;
    
      } else {
    
         // Abortar 
    
         Action = daAbort;
      }
    
    }
    
     
    Códigos de error BDE
    void __fastcall TDataModuleXXX::TablePostError
         (TDataSet *TDataSet, EDatabaseError *E, TDataAction &Action)
    {
      AnsiString S;
      EDBEngineError *Err = dynamic_cast<EDBEngineError*>(E);
    
      if (Err) {
         for (int i = 0; i < Err->ErrorCount; i++) {
             if (i > 0) AppendStr(S, '\n');
             TDBError *E = Err->Errors[i];
             AppendStr(S, Format( "%.4x (%d): %s",
                                  ARRAYOFCONST((E->ErrorCode, E->NativeError,E->Message))));
         }
         DatabaseError(S, 0);
      }
    }
    
     
    Mensajes de error personalizados
    int GetBDEError(EDatabaseError *E)
    {
      EDBEngineError *Err = dynamic_cast<EDBEngineError*>(E);
    
      if (Err)
         for (int I = 0; I < Err->ErrorCount; I++) {
             TDBError *dbe = Err->Errors[I];
             if (dbe->NativeError == 0)
                return dbe->ErrorCode;
         }
    
      return -1;
    }
    
    // Relaciones maestro/detalle
    
    void __fastcall TDataModulePedidos::tbPedidosDeleteError
        (TDataSet *TDataSet, EDatabaseError *E, TDataAction &Action)
    {
      if (GetBDEError(E) == DBIERR_DETAILRECORDSEXIST)
         if ( MessageDlg( "¿Eliminar también las líneas de detalles?",
                          mtConfirmation, TMsgDlgButtons() << mbYes << mbNo, 0) == mrYes) {
    
            tbLineas->First();
    
            while (! tbLineas->Eof)
                  tbLineas->Delete();
    
            // Reintentar el borrado en la tabla de pedidos
    
            Action = daRetry;
    
         } else // Fallar sin mostrar otro mensaje
            Action = daAbort;
    }
    
    
    // Evento OnPostError para cualquier tabla
    
    void __fastcall TDataModuleXXX::PostError 
        (TDataSet *DataSet, EDatabaseError *E, TDataAction &Action)
    {
      TTable *T = static_cast<TTable*>(DataSet);
    
      switch (GetBDEError(E)) {
    
        case DBIERR_KEYVIOL:
             DatabaseErrorFmt("Clave repetida en la tabla %s", ARRAYOFCONST((T->TableName)), 0);
             break;
    
        case DBIERR_FOREIGNKEYERR:
             DatabaseErrorFmt("Error en clave externa. Tabla: %s", ARRAYOFCONST((T->TableName)), 0);
             break;
      }
    }
    


    Cursores



    Ejercicios
  • Navegar por un conjunto de datos utilizando First, Prior, Next, Last, MoveBy, BOF, EOF, IsEmpty y RecordCount.
  • Comprobar el efecto de DisableControls y EnableControls.
  • Marcadores (TBookmarkStr)

    TBookmarkStr BM = Table->Bookmark;
    
    try {
      // Mover la fila activa
    } __finally {
      // Regresar a la posición inicial
      Table->Bookmark = BM;
    }
    


    Actualizaciones en caché



    Las actualizaciones en caché son un recurso del BDE para mejorar el rendimiento de las transacciones en entornos cliente/servidor. Los conjuntos de datos de C++ Builder vienen equipados con una propiedad, CachedUpdates, que decide si los cambios efectuados en el conjunto de datos son grabados inmediatamente en la base de datos o si se almacenan en la memoria del ordenador cliente y se envían en bloque al servidor a petición del programa cliente.

    Cuando la propiedad CachedUpdates está a true, los cambios en los registros no se escriben en la base de datos. En su lugar, se escriben en un buffer de la máquina local. Los registros se mantienen en la caché hasta que se llama al método ApplyUpdates(). Para confirmar los cambios se ha de llamar a CommitUpdates(). Para anular los cambios almacenados en caché se puede usar el método CancelUpdates(). También se pueden anular las modificaciones hechas en un registro con el método RevertRecord().

    
    StartTransaction(); // this = la base de datos sobre la que estemos trabajando
    
    try {
        for (int i = 0; i <= DataSets_size; i++)
            DataSets[i]->ApplyUpdates(); // Pueden fallar
        Commit();
    } catch(Exception&) {
        Rollback();
        throw; // Propagar la excepción
    }
    
    for (int i = 0; i <= DataSets_size; i++)
        DataSets[i]->CommitUpdates(); // Nunca fallan
    
    

    Una alternativa es forzar el vaciado de los buffers escribiendo:

    void __fastcall TDataModuleXXX::TableAfterPost(TObject *Sender)
    {
      static_cast<TDBDataSet*>(Sender)->FlushBuffers();
    }
    

    Ventajas

    Desventajas


    TTable



    El componente TTable facilita el acceso más simple y rápido a una tabla. Entre sus múltiples propiedades y métodos destacan:

    Relaciones maestro/detalle

    Para construir una relación maestro/detalle en C++ Builder basta con indicar el DataSource de la tabla maestro en la propiedad MasterSource de la tabla detalle y establecer la condición de la reunión (join) mediante la propiedad MasterFields. Además, hay que especificar IndexName o IndexFieldNames para establecer un orden en la tabla detalle (y ¡para que C++Builder lo haga eficientemente!).

    Ejercicio

  • Crear un módulo de datos (DataModule) que contenga dos tablas (clientes y pedidos, por ejemplo) y sus correspondientes DataSources.

  • Establecer la propiedad MasterSource en la tabla detalle para que apunte al DataSource de la tabla maestra. Fijar las propiedades necesarias para especificar correctamente la relación maestro/detalle (vg: reunión CustNoÞCustNo usando como índice CustNo).

  • Crear un formulario que contenga dos componentes TDBGrid enlazados a las tablas del módulo de datos (a sus DataSources para ser más precisos). Para ello es necesario realizar el #include correspondiente.
  • Búsquedas

    Una necesidad bastante común en cualquier aplicación de bases de datos es la de seleccionar un conjunto de registros que cumplan una condición.

    Filtros

    Para realizar búsquedas se pueden utilizar tablas con filtros. Un filtro se puede construir usando la propiedad Filter (con FilterOptions) o el evento OnFilterRecord. La propiedad Filtered indica si el filtro está activo. Un conjunto de datos filtrado puede recorrerse usando los métodos FindFirst(), FindNext(), FindPrior() y FindLast().

    Medite antes de decidirse a utilizar OnFilterRecord. Este tipo de filtro se aplica en el cliente, lo cual implica que la aplicación debe bajarse a través de la red incluso los registros que no satisfacen el filtro.

    void __fastcall TFormX::Filtrar(TObject *Sender)
    {
      Set<TFieldType, ftUnknown, ftDataSet> TiposConComillas;
      AnsiString Operador, Valor;
      TiposConComillas < ftString < ftDate < ftTime < ftDateTime;
    
      Operador = Sender == miIgual ? "=" : Sender == miMayorIgual ? ">=" :
                 Sender == miMenorIgual ? "<=" : "<>";
    
      // Extraer el nombre del campo
    
      AnsiString Campo = DBGrid->SelectedField->FieldName;
    
      // Extraer y dar formato al valor seleccionado
    
      if (TiposConComillas.Contains(DBGrid->SelectedField->DataType))
         Valor = QuotedStr(DBGrid->SelectedField->AsString);
      else {
         Valor = DBGrid->SelectedField->AsString;
         for (int i = 1; i <= Valor.Length(); i++)
             if (Valor[i] == DecimalSeparator) Valor[i] = '.'; 
      }
    
      // Combinar la nueva condición con las anteriores
    
      if (Table->Filter == "")
        Table->Filter = Format("[%s] %s %s", ARRAYOFCONST((Campo, Operador, Valor)));
      else
        Table->Filter = Format("%s AND [%s] %s %s",
                               ARRAYOFCONST((Table1->Filter, Campo, Operador, Valor)));
    
      // Activar directamente el filtro
      miActivarFiltro->Checked = True;
      Table->Filtered = True;
      Table->Refresh();
    }
    
    void __fastcall TFormX::miActivarFiltroClick(TObject *Sender)
    {
      miActivarFiltro->Checked = ! miActivarFiltro->Checked;
      // Activar o desactivar en dependencia del estado de la opción del menú.
      Table->Filtered = miActivarFiltro->Checked;
      Table->Refresh();
    }
    
    void __fastcall TFormX::miEliminarFiltroClick(TObject *Sender)
    {
      miActivarFiltro->Checked = False;
      Table->Filtered = False;
      Table->Filter = "";
      Table->Refresh();
    }
    
    NB: Los filtros también son aplicables a componentes de tipo TQuery

    Índices

    Se pueden hacer búsquedas usando el índice activo de una tabla (establecido por IndexName o IndexFieldNames) usando los métodos FindKey y FindNearest.

    Alternativamente se pueden utilizan las combinaciones SetKey+GotoKey (en vez de FindKey) y SetKey+GotoNearest (sustituyendo a FindNearest)

    void __fastcall TFormDatos.ButtonClick(TObject *Sender)
    {
      Table->SetKey();
    
      // FormSearch => diálogo del tipo OK/Cancel con TDBEdits
    
      if (FormSearch->ShowModal() == mrOk)
         Table->GotoNearest();
      else
         Table->Cancel();
    }
    

    Rangos

    También se puede limitar el conjunto de registros de un conjunto de datos utilizando rangos mediante los métodos SetRange y CancelRange. Un rango es una restricción de las filas visibles de una tabla en la que se muestran sólo aquéllas tuplas en las cuales los valores de ciertas columnas se encuentran entre dos valores dados. Debe existir un índice sobre esas columnas.

    void __fasctcall TFormAgenda::TabControlChange(TObject *Sender)
    {
      AnsiString Letra;
    
      Letra = TabControl->Tabs->Strings[TabControl1->TabIndex];
    
      if (TabControl->TabIndex == 0)
         // Si es la pestaña con el asterisco mostramos todas las filas.
         Table->CancelRange();
      else
         // Activamos el rango correspondiente
         Table->SetRange(ARRAYOFCONST((Letra)),ARRAYOFCONST((Letra + "zzz")));
    
      // Actualizamos los controles asociados
      Table->Refresh();
    }
    

    Locate & Lookup

    Los métodos Locate y Lookup amplían las posibilidades de los métodos de búsqueda con índices. El primero intenta encontrar un registro con los valores deseados (usando índices si los hay); mientras que el segundo se utiliza para buscar el valor de una columna utilizando como clave una diferente (por ejemplo, para buscar el nombre de un empleado conociendo su código personal o, al revés, para encontrar el código de un empleado del que conocemos su nombre).

    AnsiString TDataModulePersonal::ObtenerNombre(int Codigo)
    {
      return VarToStr(tbClientes->Lookup("Código", Codigo, "Nombre"));
    }
    
    
    int TDataModulePersonal::ObtenerCodigo(const AnsiString Apellido, const AnsiString Nombre)
    {
      Variant V = tbEmpleados->Lookup( "Apellido;Nombre",
                                       VarArrayOf(ARRAYOFCONST((Apellido, Nombre))), "Código");
    
      if (VarIsNull(V))
         DatabaseError("Empleado no encontrado", 0);
    
      return V;
    }
    
    
    AnsiString TDataModulePersonal::NombreDeEmpleado(int Codigo)
    {
      Variant V = tbEmpleados->Lookup("Codigo", Codigo, "Nombre;Apellido");
    
      if (VarIsNull(V))
         DatabaseError("Empleado no encontrado", 0);
    
      return V.GetElement(0) + " " + V.GetElement(1);
    }
    

    Actualizaciones

    Actualización programada

    TableDetail->Edit();
    
    try {
        // Asignaciones a campos...
    
        TableDetailFecha->Value = Date(); 
          // == TableDetail->FieldByName("Fecha")->AsDateTime = Date();
          // == TableDetail->FieldValues["Fecha"] = Date();
    
    	TableDetailConcepto->Clear();
    
        TableDetail->Fields->Fields[x]->Assign(TableMaster->Fields->Fields[x]);
       
        // Post
    
        TableDetail->Post();
    
    } catch (Exception&) {
    
        TableDetail->Cancel();
        throw;
    }
    

    Inserción de datos

    AnsiString RandomString (int Longitud)
    {
      char*      Vocales = "AEIOU";
      char       LastChar = 'A';
      AnsiString Rslt;
    
      Rslt.SetLength(Longitud);
    
      for (int i = 1; i <= Longitud; i++) {
          LastChar = strchr("AEIOUNS", LastChar) ? random(26) + 'A' : Vocales[random(5)];
          Rslt[i] = LastChar;
      }
    
      return Rslt;
    }
    
    
    void CrearDatosSinteticos (TTable *Tabla, int registros)
    {
      int intentos = 3;
      int longitud = Tabla->FieldByName("Cadena")->Size;
    
      randomize();
    
      while (registros > 0)
            try {
    
                1) Tabla->Append();
                   Tabla->FieldValues["Cadena"] = RandomString(longitud);
                   Tabla->FieldValues["Entero"] = random(MAXINT);
    
                2) Tabla->AppendRecord( ARRAYOFCONST (
                                         RandomString(longitud),
                                         random(MAXINT) ) ));
    
                Tabla->Post();
    			
                // NOTA: Sería más eficiente si se agrupasen
                // distintas inserciones en una única transacción
    
                intentos = 3;
                registros--;
    
            } catch(Exception&) {
    
                intentos--;
    
                if (intentos == 0)
                   throw;
            }
    }
    

    Refresco de los datos

    El contenido actual de la fila activa se puede obtener llamando de forma consecutiva a Edit y Cancel (lo que es más eficiente que llamar a Refresh sobre el conjunto completo de datos)

    Table->Edit();
    Table->Cancel();
    


    TQuery



    El componente TQuery permite realizar consultas en SQL. En los siguientes apartados se describen sus propiedades y métodos básicos:

    La consulta SQL

    La propiedad SQL es de tipo TStringList (un lista de cadenas de caracteres) y contiene la sentencia SQL que ha de ejecutarse sobre la base de datos indicada mediante la propiedad DatabaseName.

    Ejemplo:
    • Cambiar la tabla de clientes por una consulta.
    • Enlazarla con la base de datos BCDEMOS (propiedad DatabaseName).
    • Enlazar el DataSource que tenía la tabla a la nueva consulta.
    • En la propiedad SQL de la consulta, teclear:   select * from customer where CustNo > 1300

    Otra propiedad bastante interesante del componente TQuery es LiveRequest, que permite que una consulta sea actualizable (es decir, que podamos editar su conjunto de datos sin tener que preocuparnos de cómo almacenar los cambios en la base de datos). Por desgracia, no todas las consultas son actualizables.

    Ejecución de sentencias SQL

    Para ejecutar en tiempo de diseño la sentencia SQL correspondiente a una consulta (un SELECT) basta con poner su propiedad Active a true (lo que equivale a invocar al método Open()).

    En tiempo de ejecución, se puede usar Open() para ejecutar una consulta (SELECT), o ExecSQL() para ejecutar una sentencia SQL de tipo INSERT, UPDATE o DELETE.

    Sentencias SQL con parámetros

    En las sentencias SQL se pueden utilizar parámetros. Los parámetros son variables que se escriben en la sentencia SQL precedidos de : (dos puntos). El valor de tales parámetros se puede establecer accediente a la propiedad Params del componente TQuery o mediante el método ParamByName.

    En tiempo de diseño:

    Preparar una consulta SQL con:

      select * from customer
      where CustNo > :ValorMinimo
    

    Editar la propiedad Params estableciendo el parámetro ValorMinimo como un integer con valor 1300.

     
    En tiempo de ejecución:
    {
      query->SQL->Clear();
      query->SQL->Add(
        "select * from customer where CustNo > :ValorMinimo");
      query->ParamByName("ValorMinimo")->AsInteger = 1300;
      query->Open();
    }
    

    En los parámetros también se pueden utilizar comodines:

    Query->ParamByName("id")->AsString = Edit->Text + "%";

    Los métodos Prepare() y UnPrepare() sirven para ejecutar consultas preparadas, que serán más eficientes si se ejecutan múltiples veces siempre que nuestra base de datos permita este tipo de consultas optimizadas. La propiedad Prepared nos indica si una consulta ha sido preparada o no.

    Preparación inicial de una consulta
    if (! Query->Prepared)
       Query->Prepare();
    
    Query->Open();
    
    Liberación de recursos
    Query->Close();
    
    if (Query->Prepared)
       Query->UnPrepare();
    

    Consultas dependientes [linked queries]

    Mediante consultas también se pueden establecer relaciones maestro/detalle. Para ello hemos de dejar un parámetro de la consulta detalle asociado a una columna del conjunto de datos que hace de maestro, SIN especificar su tipo. Para enlazar la consulta basta entonces establecer su propiedad DataSource para que apunte a la tabla maestra. Este tipo de consultas NO son actualizables, por lo que usualmente preferiremos utilizar tablas (TTables).

    Consultas heterogéneas

    El motor de bases de datos de Borland (BDE) permite incluso realizar consultas que involucren tablas almacenadas en distintas bases de datos. Para ello, en la consulta SQL hay que indicar las tablas utilizando la notación :ALIAS:tabla, donde ALIAS es el alias BDE de la base de datos donde se encuentra la tabla tabla.

    Actualizaciones (insert, update & delete)

    int __fastcall ejecutarSQL(const AnsiString ADatabase, const AnsiString Instruccion)
    {
      std::auto_ptr query(new TQuery(NULL));
      query->DatabaseName = ADatabase;
      query->SQL->Text = Instruccion;
      query->ExecSQL();
      return query->RowsAffected;
    }
    

    Datos sintéticos

    insert into TablaAleatoria(Entero, Cadena)
    values (:Ent, :Cad)
    
    void RellenarTabla(TQuery *Query, int CantRegistros)
    {
      randomize();
      Query->Prepare();
    
      try {
          int Intentos = 3;
          
          while (CantRegistros > 0)
                try {
                    Query->ParamByName("ENT")->AsInteger = random(MAXINT);
                    Query->ParamByName("CAD")->AsString = RandomString(35);
                    Query->ExecSQL();
                    Intentos = 3;
                    CantRegistros--;
                } catch(Exception&) {
                     if (--Intentos == 0) 
    		 throw;
                }
      } __finally {
        Query->UnPrepare();
      }
    }
    


    TStoredProc



    El componente TStoredProc representa un procedimiento almacenado en un servidor de bases de datos. El esquema de utilización de procedimientos almacenados es:

    Procedimientos almacenados que devuelven valores

    create procedure EstadisticasProducto (CodProd int)
                     returns ( TotalPedidos int, CantidadTotal int,
                               TotalVentas int,  TotalClientes int )
    as
      declare variable Precio int;
    
    begin
    
      select Precio
      from Articulos
      where Codigo = :CodProd
      into :Precio;
    
      select count(Numero), count(distinct RefCliente)
      from Pedidos
      where :CodProd 
            in ( select RefArticulo
                 from Detalles
                 where RefPedido = Numero )
      into :TotalPedidos, :TotalClientes;
    
      select sum(Cantidad), sum(Cantidad*:Precio*(100-Descuento)/100)
      from Detalles
      where Detalles.RefArticulo = :CodProd
      into :CantidadTotal, :TotalVentas;
    
    end ^
    
    
    void __fastcall ...::EstadisticasProducto(TObject *Sender)
    {
      AnsiString S;
    
      if ( InputQuery("Información", "Código del producto", S) 
           && Trim(S) != "" ) {
    
         TStoredProc *sp = DataModuleProductos->spEstadisticas;
    
         sp->ParamByName("CodProd")->AsString = S;
    
         sp->ExecProc();
    
         ShowMessage( Format( "Pedidos: %d\nClientes: %d\nCantidad: %d\nTotal: %m",
                      ARRAYOFCONST((sp->ParamByName("TotalPedidos")->AsInteger,
                                    sp->ParamByName("TotalClientes")->AsInteger,
                                    sp->ParamByName("CantidadTotal")->AsInteger,
                                    sp->ParamByName("TotalVentas")->AsFloat))));
      }
    }
    

    Procedimientos almacenados que devuelven conjuntos de datos

    Definimos una base de datos (en InterBase) para almacenar una jerarquía de conceptos (como las categorías de un índice en Internet tipo Yahoo!):

    create domain TID as INTEGER;
    create domain TDESCRIPTION as VARCHAR(128);
    
    
    create table CATEGORY (
      ID          TID not null,
      description TDESCRIPTION,
      primary key (ID) );
    
    create table HIERARCHY (
      SUPER   TID not null references CATEGORY, 
      SUB     TID not null references CATEGORY,
      primary key (SUPER,SUB) );
    

    Creamos sendos procedimientos almacenado recursivos que nos devuelven todas las categorías por debajo (o por encima) de una categoría dada en la jerarquia:

    create procedure DESCENDANTS (super integer)
      returns (sub integer)
    as
    begin
    
     FOR SELECT sub
         FROM HIERARCHY
         WHERE SUPER = :super
         INTO :sub
     DO BEGIN
    
         if (sub is not null)
            then begin
                   SUSPEND;
    	       for select *
    	           from DESCENDANTS(:sub)
    		   into :sub
    	       do begin
    		   if (sub is not null)
    		      then SUSPEND;
    	       end
    	     end
        END
    
    end ^
    
    
    create procedure ANCESTORS (sub integer)
      returns (super integer)
    as
    begin
    
     FOR SELECT id
         FROM HIERARCHY
         WHERE SUB = :sub
         INTO :super
     DO BEGIN
    
         if (super is not null)
            then begin
                   SUSPEND;
    	       for select *
    	           from ANCESTORS(:super)
    		   into :super
    	       do begin
    		   if (super is not null)
    		      then SUSPEND;
    	       end
    	     end
        END
    
    end ^
    
    set term ;^
    

    Para utilizar los procedimientos almacenados anteriores, se puede escribir alguna de las siguientes consultas parametrizadas en la propiedad SQL de un TQuery:

    select distinct * from descendants(:id)
    
    select distinct * from ancestors(:id)
    

    Ejecución de un procedimiento almacenado en una hebra independiente

    class TSPThread : public TThread
    {
     protected:
      void __fastcall Execute();
     public:
      __fastcall TSPThread();
    };
    
    __fastcall TSPThread::TSPThread():TThread(True) // Crear hebra "suspendida"
    {
      FreeOnTerminate = True;
      Resume(); // Ejecutar la hebra
    }
    
    // DataModuleSPThread es un módulo de datos con tres componentes:
    // - SessionSPThread (de tipo TSession),
    // - DatabaseSPThread (de tipo TDatabase), y
    // - StoredProcSPThread (de tipo TStoredProc).
    
    void __fastcall TSPThread::Execute()
    {
      DataModuleSPThread->DatabaseSPThread->Open();
    
      try {
          DataModuleSPThread->StoredProcSPThread->ExecProc();
      } __finally {
          DataModuleSPThread->SessionSPThread->Close();
      }
    }
    
    // Ejecución
    
    void __fastcall TForm1::Btn1Click(TObject *Sender)
    {
      new TSPThread;
    }
    


    TField: Acceso a las columnas de un conjunto de datos



    La clase TField representa un campo (una columna) en un conjunto de datos. Por medio de esta clase pueden establecer los atributos de un campo (tipo, tamaño, índice, si es un campo calculado o de búsqueda, si es obligatorio...), y también se puede acceder a su valor a través de propiedades como AsString o AsInteger.

    TField es una clase base de la que se derivan otras muchas. Sus descendientes incluyen a TStringField, TIntegerField, TFloatField y TDateField, entre otros muchos.

    TField
    Nuevos en C++Builder 4:
    TField

    Acceso a los valores de los campos

    Para recuperar o establecer el valor de un campo podemos usar:

    Ejemplo
    TablaApellidos->Value = "Pérez Martín";
    
    Tabla->Fields->Fields[0]->Value = "Pérez Martín";
    
    Tabla->FieldByName("Apellidos")->Value = "Pérez Martín";
    

    El Editor de Campos [Fields Editor]

    Desde el menú contextual de cualquier descendiente de TDataSet (vg: TTable o TQuery) se permite el acceso al "Fields Editor" en tiempo de diseño. De esta forma se pueden especificar aquellos campos que se quieren incluir en el conjunto de datos y sus propiedades.

    Ejemplo
  • Seleccionar del menú contextual (botón derecho del ratón) de cualquier TDataSet la opción Fields Editor.
  • Seleccionar Add Fields en el menú del Fields Editor.
  • Añadir los campos que deseemos utilizar.
  • Ver las propiedades de los campos en el Inspector de Objetos (p.ej. seleccionándolos en el Fields Editor).
  • Propiedades interesantes de TField

    Propiedad Descripción
    FieldName Nombre de la columna en la BD.
    DisplayLabel Texto descriptivo empleado por TDBGrid.
    Value Valor en la fila activa.
    IsNull Indica si el valor es nulo.
    DisplayFormat Personalización de la visualización.
    EditMask Control de la edición del campo.
    CustomConstraint Restricción en el valor del campo.
    ConstraintErrorMessage Mensaje de error asociado.
    ReadOnly Campo de sólo lectura

    Los eventos de TField

    Evento Descripción
    OnChange Cuando cambia el valor del campo.
    OnValidate Validación a nivel de campos.
    OnGetText Al visualizar valores.
    OnSetText Al editar valores.

    Uso de TField

    Visualizacion personalizada

    Definiendo la respuesta al evento GetText

    Seleccionar un campo de tipo entero y hacer que se visualice con números romanos.
    void __fastcall ...::tbFieldGetText
                         (TField *Sender, AnsiString &Text, bool DisplayText)
    {
     static AnsiString Unidades[10] = {"", "I", "II", "III", "IV",
                                       "V", "VI", "VII", "VIII", "IX"};
     static AnsiString Decenas[10] = {"", "X", "XX", "XXX", "XL",
                                      "L", "LX", "LXX", "LXXX", "XC"};
     static AnsiString Centenas[10] = {"", "C", "CC", "CCC", "CD",
                                       "D", "DC", "DCC", "DCCC", "CM"};
     static AnsiString Miles[4] = {"", "M", "MM", "MMM"};
     
     if (Sender->AsInteger > 3999) {
        Text = "Infinitum";  // Hay que ser consecuentes con el lenguaje
     } else {
        int i = Sender->AsInteger;
        Text = Miles[i / 1000] 
             + Centenas[i / 100 % 10]
    	 + Decenas[i / 10 % 10]
    	 + Unidades[i % 10];
     }
    }
    

    Restricciones a nivel de campos

    1. Definiendo la respuesta al evento SetText:

    Forzar que las iniciales de los nombres sean siempre letras mayúsculas
    void __fastcall ...::tbFieldSetText(TField *Sender, const AnsiString Text)
    {
     int        i;
     AnsiString S = Text;
    
     for ( i=1; i<=S.Length(); i++)
         if ( i==1 || S[i-1]==' ')
            CharUpperBuff(&S[i], 1);
    
     Sender->AsString = S;
    }
    

    2. Utilizando el evento OnValidate:

    Un nombre propio no puede incluir dígitos ni otros simbolos
    void __fastcall ...::tbFieldValidate(TField *Sender)
    {
      AnsiString S = Sender->AsString;
    
      for (int i = S.Length(); i > 0; i--)
          if (S[i] != ' ' && !IsCharAlpha(S[i]))
             DatabaseError("Carácter no permitido en nombre propio", 0);
    }
    

    3. Controlando el valor que puede tomar un campo mediante sus propiedades EditMask y CustomConstraint. Cuando se utiliza esta última, lo normal es definir también ConstraintErrorMessage para que el usuario sepa por qué no puede darle determinados valores a un campo.

    Valores no nulos
    CustomConstraint = value is not null
    
    Código postal
    EditMask = !99999;1;_
    
    Teléfono o fax
    EditMask = !999 99 99 99;0;_
    
    Dirección de correo electrónico
    CustomConstraint = value is null or value='' or value like '%_@%_.%_'
    

    Campos calculados

    Se crean con New field (Calculated) desde el menú contextual del editor de campos y se manejan a través del evento OnCalcFields, que se produce cada vez que se cambia o se modifica la fila activa del conjunto de datos (si está a true la propiedad AutoCalcFields del conjunto de datos).

    Campos de búsqueda [lookup fields]

    Se pueden crear con la opción New field (Lookup) del menú contextual del editor de campos. Se definen mediante las propiedades KeyFields, LookupDataset, LookupKeyFields y LookupResultField.

    Mediante LookupCache se pueden cargar los valores permitidos en la propiedad LookupList, la cual se puede actualizar llamando a RefreshLookupList.

    BLOBs: TBlobField

    Los campos de tipo BLOB [Binary Large OBject] en una base de datos se pueden utilizar para almacenar ficheros completos, imágenes, sonidos o cualquier otro tipo de información digitalizada. El contenido de este tipo de campos se puede modificar invocando al método LoadFromFile, mientras que su complementario (SaveToFile) nos permite recuperar la información almacenada en la base de datos.

    Imágenes en formato JPG (jpeg.hpp)
    void __fastcall ...::DataSourceImagenDataChange(TObject *Sender, TField *Field)
    {
      if (tbImagenesFoto->IsNull)
         Image->Picture->Graphic = NULL;
      else {
         std::auto_ptr BS(new TBlobStream(tbImagenesFoto, bmRead));
         auto_ptr Graphic(new TJPEGImage);
         Graphic->LoadFromStream(BS.get());
         Image->Picture->Graphic = Graphic.get();
      }
    }
    


    TSession



    El componente TSession gestiona una sesión de la base de datos (un conjunto de conexiones desde una misma aplicación). Cada vez que se inicia una aplicación que accede a bases de datos a través del motor de bases de datos de Borland, el BDE establece un objeto de tipo TSession global llamado Session con el cual se puede acceder a las propiedades de la sesión actual. En principio, no hay que crear un objeto TSession propio salvo que se esté construyendo una aplicación multihebra.

    Este componente incluye algunos métodos de interés:

    Gestión de alias BDE

    AddAlias(), AddStandardAlias(), ModifyAlias(), DeleteAlias() y SaveConfigFile() son métodos que pueden usarse para gestionar alias BDE en tiempo de ejecución.

    void __fastcall TSession::AddAlias
         (const AnsiString Nombre, const AnsiString Ctrldor, TStrings *Lista);
    void __fastcall TSession::AddStandardAlias
         (const AnsiString Nombre, const AnsiString Dir, const AnsiString Ctrldor);
    void __fastcall TSession::ModifyAlias
         (const AnsiString Alias, TStrings *Parametros);
    void __fastcall TSession::DeleteAlias
         (const AnsiString Alias);
    void __fastcall TSession::SaveConfigFile();
    

    Acceso al catálogo

    GetAliasNames(), GetDatabaseNames(), GetDriverNames(), GetTableNames() y GetStoredProcNames() se pueden utilizar para conseguir información acerca de las bases de datos disponibles en el sistema.

    void __fastcall TSession::GetDriverNames
         (TStrings *Lista);
    void __fastcall TSession::GetDriverParams
         (const AnsiString Controlador, TStrings *Lista);
    
    void __fastcall TSession::GetDatabaseNames
         (TStrings *Lista);
    
    void __fastcall TSession::GetAliasNames
         (TStrings *Lista);
    AnsiString __fastcall TSession::GetAliasDriverName
         (const AnsiString Alias);
    void __fastcall TSession::GetAliasParams
         (const AnsiString Alias, TStrings *Lista);
    
    void __fastcall TSession::GetTableNames
         (const AnsiString Alias, const AnsiString Patron,
         bool Extensiones, bool TablasDeSistema, TStrings *Lista);
    
    void __fastcall TSession::GetStoredProcNames
         (const AnsiString Alias, TStrings *Lista);
    
    Ejemplo:
    Session->GetDatabaseNames(DBNamesComboBox->Items);
    


    TDatabase



    El componente TDatabase permite controlar las conexiones a una base de datos. Aunque su uso no es estrictamente necesario, hay ciertas operaciones para las que resulta imprescindible:

    Conexiones persistentes con bases de datos

    La propiedad KeepConnections de TDatabase se utiliza para controlar cómo se manejan las conexiones con las bases de datos cuando se cierra un dataset. Si KeepConnections está a false, cuando se cierra el último dataset, la conexión con la base de datos se pierde y habrá que reiniciarla la próxima vez que se abra otro dataset, lo que consumirá una cantidad considerable de tiempo y, si no hacemos nada para evitarlo, volverá a solicitar la clave de acceso del usuario.

    Establecimiento de la conexión [Login]

    El componente TDatabase también permite controlar el login de una forma automática:

  • Estableciendo la propiedad LoginPrompt a false y fijando explícitamente los parámetros de la conexión.

    Desde el código
    Database->AliasName = "Oracle";
    Database->DatabaseName = "MiDB";
    Database->Params->Values["user name"] = "scott";
    Database->Params->Values["password"]  = "tiger";
    Table->DatabaseName = Database->DatabaseName;
    Table->TableName = "CUSTOMER";
    Table->Open();
    
    Desde el inspector de objetos
    En el editor de listas asociado a la propiedad Params:
    USER NAME=scott
    PASSWORD=tiger
    

  • Interceptando el evento OnLogin, el cual se genera cada vez que se ha de establecer una conexión cuando la propiedad LoginPrompt está a true.

    void __fastcall ...::DatabaseLogin
        (TDatabase *Database, TStrings *LoginParams)
    {
      LoginParams->Values["user name"] = "scott";
      LoginParams->Values["password"]  = "tiger";
    }
    

    Schema cache

    void __fastcall ...::DatabaseLogin
        (TDatabase *Database, TStrings *LoginParams)
    {
      ...
    
      // Schema cache
    
      AnsiString S;
      S.SetLength(255);
      
      int L = GetTempPath(255, S.c_str());
      
      if (S.IsPathDelimiter(L)) L--;
    
      S.SetLength(L);
      Database1->Params->Values["ENABLE SCHEMA CACHE"] = "TRUE";
      Database1->Params->Values["SCHEMA CACHE DIR"] = S;
    }
    

    Control de Transacciones

    Otro uso más del componente TDatabase (puede que el más importante) es el control explícito de las transacciones con la base de datos.

    Una transacción es una colección de operaciones sobre la base de datos (inserciones, modificaciones o eliminaciones de registros) que ha de realizarse de forma atómica.

    Una transacción con la base de datos se inicia explícitamente llamando al método StartTransaction(). Las modificaciones realizadas sobre la base de datos no se harán efectivas hasta que se invoque al método Commit(). Si se quiere cancelar la transacción en curso hay que recurrir al método Rollback(), lo cual hará que las operaciones efectuadas en la transacción sean ignoradas. Mediante la propiedad TransIsolation se puede establecer la el nivel de aislamiento entre distintas transacciones. El nivel tiRepeatableRead (lecturas repetibles) será siempre el deseable, aunque con determinados servidores SQL sólo se llega a tiCommitedRead ("lecturas comprometidas") y las bases de datos de escritorio (tipo Paradox, dBase o Access) sólo permiten tiDirtyRead.

    Una transferencia bancaria
    TLocateOptions Opt;
    
    // Iniciar la transacción
    
    Database->StartTransaction();
    
    try {
    
        if (! Table->Locate("Cliente", cliente1, Opt))
           DatabaseError("No existe el primer cliente", 0);
    
        Table->Edit();
        Table->FieldValues["Saldo"] -= importe;
        Table->Post();
    
        if (! Table->Locate("Cliente", cliente2, Opt))
           DatabaseError("No existe el segundo cliente", 0);
    
        Table->Edit();
        Table->FieldValues["Saldo"] += importe;
        Table->Post();
    
        // Confirmar la transacción
    
        Database->Commit();
    
    } catch(Exception&) {
    
        // Cancelar la transacción
    
        Database->Rollback();
        Table->Refresh();
        throw;
    }
    

    Cancelar los cambios realizados desde el ultimo commit/rollback

    void CancelarCambios (TDatabase* ADatabase)
    {
      ADatabase->Rollback();
    
      for (int i = ADatabase->DataSetCount - 1; i >= 0; i--)
          ADatabase->DataSets[i]->Refresh();
    }