Technical Information Database TI177B.txt Introduction to BDE Programming Category :General Programming Platform :All Product :BDE All Description: Introduction to BDE Programming BDE v2.0 I) Project Setup This section covers the basics of what needs to be done in order to set up a Borland Database Engine project or makefile. II) Design Overview A high level overview of the steps that are required to create a simple application which retrieves fields from a table III) Design Specifics Detailed description, with code examples, of the steps described in part II. IV) Error Handler This section contains the error handler used by this example. I) Project Setup ================ This document provides a quick introduction to programming with the Borland Database Engine. By the end of this section, you should have a simple EasyWin, BDE example which gets a record from a table and displays the first two fields. The following steps must be followed when starting to write a BDE application. First, set up the project or makefile MAIN.CPP - File to contain your code DBERR.CPP - Error handling routine IDAPI.LIB - BDE Import Library MAIN.DEF - Module Definition file Select the large memory model. For this simple application, set the target to be an EasyWin application. This way we don't have to deal with any Windows UI issues. It is also suggested to install the IDAPI.TOK file so as to have syntax hilighting for BDE functions and types (directions on this are included in the \BDE\DOC\IDAPITOK.TXT file). The on-line help file, IDAPI.HLP, can also be incorporated into the BC 4.5 OpenHelp architecture, allowing context sensitive help on IDAPI types and functions. Other required setup: --------------------- The Borland Database Engine is fairly stack intensive, especially when doing batch operations and queries, so it is generally recommended that the stack be set to a minimum of 25k (for Windows C/C++ applications, the stack size is set in the module definition file) Increase the number of file handles available to the application. By default, the BDE assumes that it has access to 48 physical file handles (set by the BDE Configuration utility, System page | MAXFILEHANDLES option, 5-255). Due to this default setting, it is usually best to set the number of file handles available to an application to a minimum of 68 using the Windows API function SetHandleCount. Seemingly random errors can occur when the BDE does not have access to enough file handles. Use the Debug Layer when developing applications. The debug layer performs much stricter error checking than the regular DLL, resulting in fewer GP faults and less re-booting of your machine. It will also produce a trace output of which IDAPI functions were called by an application. Note that use of the debug layer requires the use of both the Debug DLL (Set using the DLLSwap utility), as well as a call to the DbiDebugLayerOptions function. Make certain to compile with 'Allocate enums as ints' selected (in the BC 4.x IDE, Options | Project | Compiler | Code Generation). A number of structures, such as CURProps, make use of enumerations. Within the DLL, these are allocated as two byte values. Turning off this option will result in your code passing only one byte. This error generally manifests itself with stack corruption problems, such as GP faults when calling or returning from a function. Within a module to contain BDE code, include the following header files: WINDOWS.H IDAPI.H Note that order is important. The WINDOWS.H file must be included before IDAPI.H. II) Design Overview =================== Before we start writing code, a brief overview of what needs to be done to get a record from a tables: Increase the number of file handles available to the application Initialize the Borland Database Engine Enable the debug layer Open a database object Set the database object to point to the directory containing the table Set the directory for temporary objects Open a table, creating a cursor object Get the properties of the table Using these properties, allocate memory for a record buffer Position the cursor on the desired record Get the desired record from the cursor (table) Get the desired field(s) from the record Free all resources Note that throughout the following short example unfamiliar variable types may be used. These are IDAPI variable types that are defined in the IDAPI.H header file. For example, BYTE, BOOL, CHAR, and FLOAT. III) Design Specifics ===================== Increase the number of file handles available to the application ---------------------------------------------------------------- A call to the Windows API function SetHandleCount will increase the number of file handles available to an application in the Windows environment: int HandlesAvail; int HandlesWanted; HandlesAvail = 0; HandlesWanted = 68; HandlesAvail = SetHandleCount(HandlesWanted); if (HandlesAvail < HandlesWanted) { // Display message re: not enough available // file handles return; } Note that in non-trivial cases it is recommended to determine the number of file handles that are specified in the BDE configuration file. This would be done using the DbiOpenCfgInfoList function, which will be described in a later section on configuration management. Initialize the Borland Database Engine -------------------------------------- The Borland Database Engine is initialized using the DbiInit function: CHKERR(DbiInit(NULL)); Note that the CHKERR macro is defined in the DBERR.H file, which is a part of the Error Handling module. The call to DbiInit allocates system resources for the client. Enable the Debug layer ---------------------- The following code is used to enable the debug layer, outputting trace information to a text file on disk. DbiDebugLayerOptions(DEBUGON | OUTPUTTOFILE, "MYTRACE.INF"); Note that in certain situations, you may also want to use the FLUSHEVERYOP flag, which will force output to the trace file after every operation. While this is slower, it is useful when a GP fault occurs.... Open a database object ---------------------- The next step that is needed is to open a database object. All table access must be performed within the context of a database. Local databases generally use what is referred to as the "STANDARD" database, which is what we will be using in this example. The preferred method would be to create an alias to a local directory and using that as the database. This allows for easy modification in the future if one day it is decided to move the application from using dBASE tables to using InterBase tables. The DbiOpenDatabase function is used to open a database: hDBIDb hDb; // Handle to the Database hDb = 0; // Initialize to zero for cleanup // purposes CHKERR_CLEANUP( DbiOpenDatabase(NULL, // Database name // - NULL for standard Database NULL, // Database type - // NULL for standard Database dbiREADWRITE, // Open mode - Read/Write or // Read only dbiOPENSHARED, // Share mode - Shared or // Exclusive NULL, // Password - not needed for the // STANDARD database NULL, // Number of Optional Parameters NULL, // Field Desc for Optional // Parameters NULL, // Values for the // Optional Parameters &hDb) // Handle to the database ); // At the end of the function CleanUp: // Close only if open if (hDb != 0) { DbiCloseDatabase(&hDb); } Set the database object to point to the directory containing ------------------------------------------------------------ the table --------- The working directory next needs to be set to the directory containing the table. While it is possible to open a table in other directories, specifying the absolute path, it is generally recommended to open tables in the working directory, as a number of operations, such as getting a list of available tables, work off the current directory. The DbiSetDirectory function is used to set the working directory (using the default location of the BDE example tables): CHKERR_CLEANUP( DbiSetDirectory(hDb, // Handle to the // database which is being modified "C:\\BDE\\EXAMPLES\\TABLES") // The new working directory ); Note that the full, absolute path needs to be used. Relative paths are not supported. Set the directory for temporary objects --------------------------------------- While not all BDE applications create temporary objects, but larger applications will at one time or another. For example, the result set from a query or the records which cause a key violation in a restructure will be placed in a temporary table. By default, this temporary, or private directory, as it is called, is the startup directory. This will cause a problem if the application is run off a network or a CD-ROM., as the directory cannot be shared, and it must we writable. The DbiSetPrivateDir function is used to set the private directory for a client: CHKERR_CLEANUP( DbiSetPrivateDir("c:\\") // Select a directory on a local drive that is not // used by other applications. ); Note that the full, absolute path needs to be used. Relative paths are not supported. Open a table, creating a cursor object -------------------------------------- Next, open the table. Upon opening a table, a cursor object is created and returned to the calling application. A cursor object is basically an abstraction which allows queries and tables to be accessed in the same method: hDBICur hCur; // Handle to the cursor (table) CHAR szTblName[DBIMAXNAMELEN]; // Table name - DBIMAXNAMELEN // is defined in IDAPI.H CHAR szTblType[DBIMAXNAMELEN]; // Table Type hCur = 0; // Initialize to zero for cleanup // purposes strcpy(szTblName, "customer"); // Name of the table strcpy(szTblType, szPARADOX); // Type of the tables // - szPARADOX is defined in IDAPI.H CHKERR_CLEANUP( DbiOpenTable(hDb, // Handle to the // database to which this cursor will // be related szTblName, // Name of the table szTblType, // Type of the table - only // used for local tables NULL, // Index Name - Optional NULL, // IndexTagName - Optional. // Only used by dBASE 0, // IndexId - 0 = Primary. Optional. // Paradox and SQL only dbiREADWRITE, // Open Mode - // Read/Write or Read Only dbiOPENSHARED, // Shared mode - // SHARED or EXCL xltFIELD, // Translate mode // FIELD or NONE // FIELD: Convert data from table // format to C format // NONE: Leave data in it's native // state FALSE,// Unidirectional - // False means can navigate forward // and back. NULL, // Optional Parameters. &hCur)// Handle to the cursor ); CleanUp: // Close only if open if (hCur != 0) { DbiCloseCursor(&hCur); // Note the use of DbiCloseCursor // - there is no DbiCloseTable. } Get the properties of the table ------------------------------- We next need to determine the size of the record buffer for the table. This information is gotten from the cursor via the DbiGetCursorProps function. The Cursor properties include information on the table name, size, type, number of fields, and record buffer size. More information can be found on cursor properties in the on-line help under CURProps. CURProps curProps; // Properties of the cursor CHKERR_CLEANUP( DbiGetCursorProps(hCur, // Handle to the cursor &curProps) // Properties of the cursor // (table) ); curProps.iRecBufSize contains the size of the record buffer. Using these properties, allocate memory for a record buffer pBYTE pRecBuf; // Pointer to the record buffer pRecBuf = NULL; // For cleanup purposes // Allocate memory for the record buffer pRecBuf = (pBYTE)malloc(curProps.iRecBufSize * sizeof(BYTE)); if (pRecBuf == NULL) { // Display some error message goto CleanUp; } CleanUp: if (pRecBuf) { free(pRecBuf); } Position the cursor on the desired record ----------------------------------------- The DbiSetToBegin() function is used to position the cursor to the "crack" before the first record in the table. Crack semantics basically allow for the current location of the cursor to be before the first record, between records, or after the last record. One of the reasons for having crack semantics is to allow the use of one function to access all records in a table. For example, in place of having to use DbiGetRecord the first time, and DbiGetNextRecord each subsequent time, it is now possible to use DbiGetNextRecord to get all records in a table. CHKERR_CLEANUP( DbiSetToBegin(hCur) // Position the specified cursor // to the crack before the first record ); Get the desired record from the cursor (table) ---------------------------------------------- The DbiGetNextRecord function is normally used to get a record from a table. Note that the current record of the cursor is set to the record returned by this function (the next record in the table): CHKERR_CLEANUP( DbiGetNextRecord(hCur, // Cursor from which to get the // record. dbiNOLOCK, // Lock Type pRecBuf, // Buffer to store the record NULL) // Record properties - // don't need in this case ); Get the desired field(s) from the record ---------------------------------------- The last step is to get the field values out of the record buffer and into some local variable. Note that in this case we are making assumptions about which field is at which ordinal position within the table, as well as the size of the field. In general, it is recommended to use DbiGetFieldDescs to get information about a field before retrieving it. Also note that a single function, DbiGetField, is used to get all fields, other than BLOBs, from a table. FLOAT custNum; BOOL isBlank; CHKERR_CLEANUP( DbiGetField(hCur, // Cursor which contains the record 1, // Field Number of the "Customer" field. pRecBuf, // Buffer containing the record (pBYTE)&custNum, // Variable for the Customer // Number isBlank) // Is the field blank? ); Free all resources ------------------ After all desired operations have been performed, the resources allocated on behalf of the application need to be cleaned up. In addition to any memory explicitly allocated using malloc or new, all engine objects must also be cleaned up, including the cursor, database, and engine: CleanUp: if (pRecBuf) { // Free the record buffer free(pRecBuf); } if (hCur !=0) { // Close the cursor DbiCloseCursor(&hCur); } // Close only if open if (hDb != 0) { // Close the database DbiCloseDatabase(&hDb); } // Close the BDE. DbiExit(); IV) Error handler // DBERR.H // This file contains macros to use around BDE functions // which are not expected to fail, or which would // result in a fatal error. More explicit error handling needs // to be done in the application when a non-fatal // error can occur, for example, getting an End of file error, // DBIERR_EOF, from a DbiGetNextRecord // loop. #ifndef __DBERR_H #define __DBERR_H #include // Macro to use when no cleanup is required #define CHKERR(parm) DBIError(__FILE__, __LINE__, \ #parm, parm) ; \ if (GlobalDBIErr) { \ return GlobalDBIErr ;} // Macro to use when cleanup must be done within a function // for example, to close open tables or to free allocated memory. // Note: must have a CleanUp label defined in each function // where this macro is used. The CleanUp // label should start the section of a function which frees the // resources allocated within a given function #define CHKERR_CLEANUP(parm) DBIError(__FILE__, __LINE__, \ #parm, parm) ; \ if (GlobalDBIErr) { \ goto CleanUp ;} // Global variable to contain the result code extern DBIResult GlobalDBIErr; // Prototype for the error handling function DBIResult DBIError(pCHAR, UINT16, pCHAR, DBIResult); #endif // DBERR.C #include #include "DBERR.h" // The way to use this macro is to include the DBERR.H. // Then pass an IDAPI function as a parameter to the // macro: // // #define CHKERR(parm) DBIError(__FILE__, __LINE__, \ // #parm, parm) ; \ // if ( GlobalDBIErr ) { \ // return GlobalDBIErr ;} // // You would then use it as such: // CHKERR(DbiCreateTable(hDb, bOverWrite, // &crTblDsc)) ; // Global variable to hold return value from BDE functions DBIResult GlobalDBIErr; // Global variables to contain the error messages. Defined // globally to ensure that an error message can be displayed // even if the system is out of memory (and to keep it off // the stack) // Contains just the BDE error message static char szDBIStatus[(DBIMAXMSGLEN * 7)+1]; // Contains the entire message, including file name and line // number of the offending function. static char szMessage[(DBIMAXMSGLEN * 7)+1+110]; //========================================== // Function: // DBIError(); // // Input: module name (pCHAR), line number (UINT16), // Engine function name (pCHAR), // Result (DBIResult) // // Return: A DBIResult value. // // Description: // This is a function which takes in the information of // where the error occurred and displays error // information in a message box. //============================================================== DBIResult DBIError (pCHAR module, UINT16 line, pCHAR function, DBIResult retVal) { DBIErrInfo ErrInfo; // Structure to contain the error // information if (retVal == DBIERR_NONE) { GlobalDBIErr = DBIERR_NONE; return retVal; } // Don't want to call functions if the DLL is not there.... if (retVal != DBIERR_CANTFINDODAPI) { // Get as much error information as possible DbiGetErrorInfo(TRUE, &ErrInfo); // Make certain information is returned on the correct // error (that this function was called // immediately after the function that caused the error. if (ErrInfo.iError == retVal) { strcpy(szDBIStatus, ErrInfo.szErrCode); if (strcmp(ErrInfo.szContext1, "")) { strcat(szDBIStatus, ErrInfo.szContext1); } if (strcmp(ErrInfo.szContext2, "")) { strcat(szDBIStatus, ErrInfo.szContext2); } if (strcmp(ErrInfo.szContext3, "")) { strcat(szDBIStatus, ErrInfo.szContext3); } if (strcmp(ErrInfo.szContext4, "")) { strcat(szDBIStatus, ErrInfo.szContext4); } } else { DbiGetErrorString(retVal, szDBIStatus); } sprintf(szMessage, "Module:\t\t%s\nFunction:\t%s\nLine:\t\t%d\n" "Category:\t%d\nCode:\t\t%d\nError:\r\n\r\n%s\n", module, function, line, ErrCat(retVal), ErrCode(retVal), szDBIStatus); MessageBox(NULL, szMessage, "BDE Error", MB_ICONEXCLAMATION); } else { // Display an error message if the BDE DLL's cannot be // found. MessageBox(NULL, "Cannot find Borland Database" " Engine files: Check the [IDAPI] section" " in WIN.INI.", "BDE Initialization Error", MB_ICONHAND | MB_OK); } GlobalDBIErr = retVal; return retVal; } Reference: 7/15/98 3:24:21 PM
Last Modified: 01-SEP-99