Exceptions and databases

by Mark Cashman

While exceptions in normal code are fairly easy to understand and control, the handling of database exceptions can be more confusing. Part of the confusion comes from the nature of database operations in C++Builder, where many normal database events occur without direct control from code.

The proper management of exceptions falls into a few categories, some of which are related to the architecture of your application, and some of which are more incidental.

This article will explain some of the database exceptions that can occur and how to handle them properly.

 

Databases and C++Builder

As you probably know, C++Builder database programs can be written without a single line of code. (See my article at http://www.temporaldoorway.com/

programming/cbuilder/basics/simpledbprogram.htm.) This presents a number of problems that can be formulated as questions typically asked by the developer:

 

 

The following sections will address these questions.

 

Architectural issues

As always, the structure of your program has a great influence on the ease with which these questions can be answered. In addition, the VCL offers specialized features that can help you develop solutions to exception handling problems, or to avoid having to handle exceptions entirely.

The project source contains the catch-all (no pun intended) exception handler for the entire application. To see the project source, choose Project|View Source from the C++Builder main menu. The project source looks like this:

WINAPI WinMain(
  HINSTANCE, HINSTANCE, LPSTR, int)
{
  try
  {
    Application->Initialize();
    Application->CreateForm(
      __classid(TForm1), &Form1);
    Application->Run();
  }
  catch (Exception &exception)
  {
    Application->
      ShowException(&exception);
  }
  return 0;
}

If an exception occurs at application startup, the exception will be reported and the program will terminate. You can place code in the catch block of the project source, but about all you can do is gracefully report the error and allow the application to terminate. In theory, you might even restart the application, for instance by correcting the error condition and issuing another Application->Run(). In practice, this is rarely useful. However, this is the place to deal with errors that occur as a result of the creation of forms and data modules. Types of errors that might occur in a database application include:

 

 

For more graceful error management, including the possibility of recovery, close all datasets at design time, and open the dataset within a try/catch block. You can do this either in the constructor (with appropriate recovery actions) or in a method to be called by another form. Here’s how the code might look:

for (int i=0;i<ComponentCount;i++)
{
  try
  {
    TQuery* Qry = dynamic_cast<TQuery*>
      (Components[i]);
    if (Qry)
    {
      if (Qry->SQL->Text.Length() > 0)
      {
        Qry->Open();
      };
    }

    TTable* Tbl = dynamic_cast<TTable*>
      (Components[i]);
    if (Tbl)
    {
      if (Tbl->DatabaseName.Length() > 0
          && Tbl->TableName.Length() > 0)
        Tbl->Open();
    }
  }
  catch (Exception &E)
  {
    Application->ShowException(&E);
    // Or other code to handle exception
  };
}

This code attempts to open tables or queries when the application starts. If an exception is thrown, the application attempts to continue. In addition, this code avoids attempting to open components that have not been properly initialized (due to a bad table name, missing SQL statement, and so on). You can also arrange for the exception handling to terminate the application, either by directly calling Application->Terminate(), or if this code is in a constructor, by re-throwing the exception. In that case, it is best to indicate the lack of recovery by placing the try/catch outside the loop.

Once the application has started, you can use the TApplication class to catch exceptions at a global level. This is done by providing an event handler for the OnException event. In C++Builder 5, this handler can be easily implemented via the TApplicationEvents component. This component allows you to assign event handlers to TApplication events using the Object Inspector just as you do with other components.

In previous versions of C++Builder, it is necessary to implement the event handler and then to assign its address to the OnException property of the Application object. Here’s an example event handler for the OnException event:

void __fastcall 
TExceptionForm::HandleOnException(
  System::TObject* Sender, 
  Sysutils::Exception* E)
{
  ReportException(E.Message);
};

After defining the event handler, simply assign its address to the OnException event:

Application->OnException = 
  ExceptionForm->HandleOnException;

Obviously, the OnException handler can be used to deal with a variety of database errors that might occur once the program has started. However, it is better to first consider some architectural alternatives.

 

Reporting database errors during normal operation

A properly structured C++Builder program separates the database operations from the user interface, primarily through the use of data modules. This allows the data to be linked to a variety of user interfaces. However, for this scheme to work, the data module must not be dependent on any aspect of a particular user interface. In particular, it must not use any controls from a specific form to report errors.

So how do you report errors that occur during the database operations of the data module? Exceptions, of course, are the obvious answer.

If the database error occurs during a request from a form to a data module, that request can be placed within a try/catch block. Take, for example, this code that accesses a data module called Account:

Account->Database->StartTransaction();
try
{
  Account->Table->Open();
  Account->Table->Append();
  Account->TableID->AsString = 
    AccountEdit->Text;
  Account->Table->Post();
  Account->Database->Commit();
}
catch (EDatabaseError &DatabaseError)
{
  Account->Database->Rollback();
  ReportError(DatabaseError.Message);
};

In this example, one or more TTable methods may throw an exception. Further the Commit() method may throw an exception if the changes cannot be written to the database. The catch block handles this possibility by rolling back the transaction and reporting the error to the user.

But what about an error that occurs as a result of an operation from a data-aware control, an equivalent post from a TDBGrid for instance? The only place those can be caught will be in the TApplication class’s OnException handler. This means that the data module needs to provide sufficient information for the application to direct the error to the appropriate form, or to allow the application to present the message directly from the OnException handler.

 

Validation and error messages

Many developers who learned programming in languages without events—and without the specialized database support provided by C++Builder—tend to bind database operations to the visual control. This includes database field entry validation. However, data modules provide a variety of facilities to avoid this dependence of validation on visual presentation.

When it comes to validating entry to a database field, you need first to construct persistent fields for all of your datasets. For those fields which will be presented in a user interface, and for which validation must therefore be performed, you need to use the OnValidate event handler provided by all TField descendants. Logic in this event handler can check the data against a mask, against other data in the row, or against literal data encoded in the event handler. If the data is in error, the field can ensure that the record is not posted by either throwing an exception to be caught by the OnException event handler, or by producing a message itself and calling the special, silent exception function Abort() (note that the first letter must be capitalized or you will end up calling the abort() function in the RTL). Abort() throws a silent exception that bypasses all handlers, including TApplication’s OnException event handler.

Naturally, throwing an exception to the application is preferable to presenting a message. Remember that the data module, and even the validation facilities, may be reused in non-interactive contexts. It may even be used as part of a distributed system and may run on a machine without a user interface or a screen. If you can avoid having the data module present errors, you have more possibilities for reuse.

 

Conclusion

Exceptions are just as powerful for handling and providing information on errors arising from database operations as they are in handling and providing information on other errors in processing. Properly combined with the other features of C++Builder, they can allow you to decouple your database operations and error management from the user interface, thus improving the degree to which you can reuse your database objects.