Unless you elect to put all your error handling inline, you will find it necessary to design one or more error handlers for your DBTools.h++ applications. So far, we have deliberately kept our example error handling routines extremely simple, in order to concentrate on presenting an overview of the error model. In this section, we go into the subject in more detail.
The first thing a useful error handler needs to know is what kind of error it is dealing with. The Tools.h++ approach is to classify errors as internal or external, depending on whether or not they can reasonably be predicted in advance by the programmer. In Tools.h++, an internal error throws an exception of type RWInternalError, and an external error throws an exception of type RWExternalError. In DBTools.h++, however, database errors are also possible. To simplify the situation, we have adopted the policy that all errors are external errors. This means that:
The type of exception thrown by RWDBStatus::raise() is RWExternalErr. Note that some of the Tools.h++ classes used by DBTools.h++ could still throw an RWInternalError.
Your application logic needs to distinguish between errors from databases and errors from DBTools.h++.
The errorCode() member function of RWDBStatus tells you the kind of error you're dealing with; see Appendix A for a list of error codes. Of these, the codes vendorLib, serverError, and serverMessage indicate database errors or events, while the remaining error messages are generated by DBTools.h++ itself. But even though there are many more error codes generated by DBTools.h++, our experience indicates that most errors in database applications are database errors.
In order to distinguish database errors from other errors, the first part of our error handler might look like this:
void errorHandler(const RWDBStatus& status) { RWDBStatus::ErrorCode code = status.errorCode(); if (code == RWDBStatus::vendorLib || || code == RWDBStatus::serverError || || code == RWDBStatus::serverMessage) { // handle database error } else { // raise an exception status.raise(); } }
Once your application receives a database error, you must first interpret it before you can handle it. The main problem here is that each database vendor has its own ways of representing and categorizing errors. DBTools.h++ solves this problem by providing class RWDBStatus with four variables reserved for database-dependent information. These are accessed using the member functions vendorError1(), vendorError2(), vendorMessage1(),and vendorMessage2(). The first two return long int, and the second two return RWCString.
Each DBTools.h++ access library defines the contents of RWDBStatus for its particular database. Each access library guide provides details. As comparative examples, Table 8, Table 9, and Table 10 list the mappings used for three popular databases, Oracle, Sybase, and MS SQL Server.
Field | Contents |
errorCode |
serverError |
message |
Server error:%s, where %s is the text from the call to oerhms(). |
vendorMessage1 |
Oracle server name |
vendorMessage2 |
Not used |
vendorError1 |
Return code as reported by OCI Library (cda.rc or lda.rc) |
vendorError2 |
OS error code as reported by OCI Library (lda.ose or cda.ose) |
Field | Contents |
errorCode |
vendorLib |
message |
Vendor Library Error:%s, where %s is the error text from Client-Library or CS-Library (CS_CLIENT_MSG.msgstring) |
vendorMessage1 |
Operating system error text, as reported by Client-Library or CS-Library (CS_CLIENT_MSG.osstring) |
vendorMessage2 |
Text that describes the error (CS_CLIENT_MSG.sqlstate) |
vendorError1 |
Client-Library or CS-Library error number (CS_CLIENT_MSG.msgno) |
vendorError2 |
Severity level, as reported by Client-Library or CS-Library (CS_CLIENT_MSG.severity) |
Field | Contents |
errorCode |
serverError |
message |
Server error:%s, where %s is the text of the SQL Server message (msgtxt) |
vendorMessage1 |
SQL Server name (srvname) |
vendorMessage2 |
Not used |
vendorError1 |
SQL Server message number (msgno). |
vendorError2 |
Severity level, as reported by SQL Server (severity) |
You can see in these tables that the layouts are similar but not identical. This is not surprising, since error reporting is one of the least standardized aspects of database APIs. Let us now return and modify our error handler to try to cope with this problem:
void errorHandler(const RWDBStatus s) { switch(s.errorCode()) { case RWDBStatus::serverError: cout << s.message() << endl << "Error number: " << s.vendorError1() << endl << "Server: " << s.vendorMessage1() << endl << "Severity: " << s.vendorError2() << endl; break; case RWDBStatus::serverMessage: case RWDBStatus::vendorLib: // handle other database errors default: // raise an exception s.raise(); } }
Now let's create an error condition. The following fragment attempts to drop a database table that doesn't exist:
RWDBTable junk = myDbase.table("junk");//table doesn't exist RWDBStatus s = junk.drop();
On our system, this is the output when the example is run against an Oracle database and a Sybase database, respectively:
(Oracle output)
[SERVERERROR] Error from Server: ORA-00942: table or view does not exist Error number: 942 Server: T:crest Severity: 0
(Sybase output)
[SERVERERROR] Error from Server: Cannot drop the table 'junk', because it doesn't exist in the system catalogs. Error number: 3701 Server: SYBASE100 Severity: 11
Now suppose that your application needs to take some special action if a table doesn't exist. Evidently your application must be able to interpret database errors, not just report them, and to interpret them it must recognize the database. Perhaps the most straightforward way for your application to handle different databases, then, is to have separate error handlers for each database type:
const long OraNoTableError = 942; const long SybNoTableError = 3701; void oraErrorHandler(const RWDBStatus& s) { RWDBStatus::ErrorCode code = s.errorCode(); if (code == RWDBStatus::serverError && s.vendorError1() == OraNoTableError) { // take special action } else { s.raise(); } } void sybErrorHandler(const RWDBStatus& s) { RWDBStatus::ErrorCode code = s.errorCode(); if (code == RWDBStatus::serverError && s.vendorError1() == SybNoTableError) { // take special action } else { s.raise(); } } . . . RWDBDatabase oracle = RWDBManager::database ( /*args for Oracle*/ ); RWDBDatabase sybase = RWDBManager::database ( /*args for Sybase*/ ); oracle.setErrorHandler(oraErrorHandler); sybase.setErrorHandler(sybErrorHandler);
When the database-dependent table does not exist error comes up, it is trapped explicitly and handled by the take special action case in the appropriate error handler. The tradeoff here is between portability and specificity. The more portable your application, the less database-dependent intelligence it can use, and vice versa.
To summarize, once you make the decision to interpret information in a database-dependent way, you can do it several different ways. In addition to the examples shown here, you may consider deriving from class RWDBStatus in order to obtain a database-dependent interface to it. Or you might design a database exception class with derived database-dependent variants, and explicitly throw() instances of your class instead of using the raise() method of RWDBStatus.
©Copyright 2000, Rogue Wave Software, Inc.
Contact Rogue Wave about documentation or support issues.