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:
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.
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].
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.
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 |
|
NOTA: El nivel de lecturas repetibles no es adecuado para las aplicaciones que tienen que estar pendientes de las actualizaciones realizadas en otros puestos.
Los componentes de la VCL para el acceso a bases de datos se dividen en categorías siguiendo el modelo MVC (Model-View-Controller):
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:
También se puede llamar al asistente directamente desde la opción Form Wizard del menú Database.
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.
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.
Utilicemos ahora el asistente para algo un poco más complejo:
Ejercicios prácticos |
---|
|
|
|
|
|
|
|
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). |
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.
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.
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(); } |
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&) { } } |
Append en vez de Insert |
void __fastcall TFormXXX::DBNavigatorBeforeAction(TObject *Sender,TNavigateBtn Button) { if (Button==nbInsert) { static_cast<TDBNavigator*>(Sender)->DataSource->DataSet->Append(); SysUtils::Abort(); } } |
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.
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".
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:
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. |
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. |
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. |
Introducción de datos |
---|
void __fastcall TDataModuleXXX::TableNewRecord(TDataSet *DataSet) { DataSet->FieldValues["Date"] = Date(); } |
Validaciones a nivel de registros |
---|
void __fastcall TDataModulePersonal::tbEmpleadosBeforePost(TDataSet *DataSet) { if (Date() - tbEmpleadosHireDate->Value < 365 && tbEmpleadosSalary->Value > TopeSalarial) DatabaseError("¡Este es un enchufado!", 0); } |
Eliminación en cascada |
---|
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 |
---|
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; } } |
Ejercicios |
---|
|
TBookmarkStr BM = Table->Bookmark; try { // Mover la fila activa } __finally { // Regresar a la posición inicial Table->Bookmark = BM; } |
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(); } |
El componente TTable facilita el acceso más simple y rápido a una tabla. Entre sus múltiples propiedades y métodos destacan:
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 |
---|
|
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.
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(); } |
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(); } |
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(); } |
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); } |
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; }
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; } }
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();
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.
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.
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(); |
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).
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.
int __fastcall ejecutarSQL(const AnsiString ADatabase, const AnsiString Instruccion) { std::auto_ptr |
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(); } } |
El componente TStoredProc representa un procedimiento almacenado en un servidor de bases de datos. El esquema de utilización de procedimientos almacenados es:
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)))); } } |
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) |
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; } |
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.
Para recuperar o establecer el valor de un campo podemos usar:
__property TFields* TDataSet::Fields; __property int TFields::Count; __property TField* TFields::Fields[int Index];
TField* __fastcall TDataSet::FieldByName(const AnsiString Nombre);
Ejemplo |
TablaApellidos->Value = "Pérez Martín"; Tabla->Fields->Fields[0]->Value = "Pérez Martín"; Tabla->FieldByName("Apellidos")->Value = "Pérez Martín"; |
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 |
|
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 |
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. |
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]; } } |
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 '%_@%_.%_' |
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).
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.
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 |
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:
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();
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);
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:
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.
El componente TDatabase también permite controlar el login de una forma automática:
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 |
void __fastcall ...::DatabaseLogin (TDatabase *Database, TStrings *LoginParams) { LoginParams->Values["user name"] = "scott"; LoginParams->Values["password"] = "tiger"; } |
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; } |
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; } |
void CancelarCambios (TDatabase* ADatabase) { ADatabase->Rollback(); for (int i = ADatabase->DataSetCount - 1; i >= 0; i--) ADatabase->DataSets[i]->Refresh(); } |