Interbase - tratar erros |
Top Previous Next |
Question/Problem/Abstract:
This article shows a technique used to tackle with Interbase errors on the client side, using Interbase Express (IBX). When you receive such an error, you get an ugly message (from user's perspective) such as "Violation of primary or unique key constraint 'INTEG_2' on table 'COUNTRY'". Using this technique, you can turn those messages into more ‘human’ ones, or translate them into your own language. Answer:
Changing Interbase Error Messages =================================
This article shows a technique used to tackle with Interbase errors on the client side, using Interbase Express (IBX). When you receive such an error, you get an ugly message (from user's perspective) such as "Violation of primary or unique key constraint 'INTEG_2' on table 'COUNTRY'". Using this technique, you can turn those messages into more ‘human’ ones, or translate them into your own language.
The objective is to change all non handled exceptions, so the TApplication's OnMessage event is used to peek the name of the broken constraint directly from Interbase’s Status Vector and replace the default text with our own, taken from a file. This error processing can be plugged into any project with a couple of lines; the bother part is to construct the equivalences file, so we’ll go a little further developing a simple application to construct that file, using Interbase’s System Tables.
As for the first part, we’ll put all the code in a separate unit and provides access to it through global functions. The three global functions are as in next table:
Function/Procedure | Called from | Parameters | Comments ========================================================================== LoadDict | Main program | Name: string | Dictionary file name Must be called before error processing start -------------------------------------------------------------------------- DisplayMessageIB | Main program | E: Exception | Current Exception object Returns True if succesfully processed the error, False if the exception was not EIBError -------------------------------------------------------------------------- GetConstraintName | DisplayMessageIB | - | Returns the name of last constraint broken, extracted from Status Vector. Kept as global just as a bonus. ==========================================================================
The technique is not difficult to grasp. First of all, we need a place to store the messages; it could be anything capable of keeping pairs of related strings: the name of the constraint and the associated message. We’ll stick here to simplicity, using an INI file which we will call the dictionary file.
Dictionary replacement ======================
Once we get to the name of the broken constraint (we’ll talk about that later), we have to search for the message associated with it, if exists. We will use a simple .INI file to keep pairs of the form
Constraint_Name=Message
For example,
INTEG_2=The country already exists
This file will be called ‘Dictionary’ from now on. All the equivalence pairs will be put in a single section entitled ‘Constraints’, so this file could be really the same one that your application uses to keep general settings. So, we need to have access to an entire section of an INI file; the code reads the entire section in a TStringList object so we can easily search for a name afterwards. You can see the relevant code in Figure 5: in the initialization section the string list is created (by the way, the string list is private to this unit); it is filled in function LoadDict, which should be called from the application before any processing takes place; and it is deallocated in unit’s finalization section. Once we have the messages in a string list, we can begin processing Interbase errors.
Error processing ================
When a constraint is broken in a database operation, the server assembles a special vector called the Status Vector with information such as the internal error code, the SQL error code is any, the name of the constraint broken, and a standard message ready for display. The Status Vector is divided (logically, not physically) into clusters of one or more elements; each element is a 32-bit number. The first element of each cluster identifies the type of cluster. The useful types of clusters of the Status Vector are shown in the table in Figure 2; the complete reference is in Interbase’s Api Guide document.
Cluster ID | | (first element) | Elements in cluster | Meaning ============================================================================== 0 | | End of Status Vector.
1 | 2 | The next element is Interbase’s Error Code. 2 | 2 | Next element is a string parameter for replacing in standard message. 3 | 3 | Next element is the size in bytes of a variable-length string whose address is in cluster’s third element. 4 | 2 | Next element is a number parameter for replace in standard message. 5 | 2 | Next element is the address of a string with the standard message, the parameters already replaced, ready for display. ==============================================================================
This status Vector is made available to us by Interbase Express (IBX) as an array of isc_status elements (32 bits signed integers: longint). The definitions are found in IBExternals.pas. There is a series of global functions for working with the Status Vector (StatusVector, StatusVectorArray, etc) in unit IB.pas. The one we will use is StatusVectorArray, which returns a pointer to the first element of the status vector array:
type ISC_STATUS = Long; TStatusVector = array[0..19] of ISC_STATUS; PStatusVector = ^TStatusVector;
function StatusVectorArray: PStatusVector;
When there is an error, IBX calls IBDatabaseError, which in turn raises an EIBInterBaseError or an EIBInterBaseRoleError exception (two classes that inherit from EIBError without modification). This exception object is what we get in TApplication.OnException event handler as parameter E. In fact, we don’t need that exception object to get the Status Vector since it is global and corresponds to last executed command; but we will use the error codes inside the main processing function to display a somewhat detailed message… in case we don’t have our own, of course.
The first step in processing an error is to look for the name of the broken constraint in the staus vector. The code is in function GetConstraintName, which returns the name if found, or an empty string if not. Next, we search the StringList for that name. If we found a match, we display the associated message. If we can’t find the constraint name in the list, we build a standard message showing the error codes and the server message. All this process is the task of function DisplayMessageIB, which takes the exception object as a parameter to get the error codes. This is the only function we need to call from TApplication’s OnException event handler.
Implementation ==============
We have three functions and a String List; it all could be encapsulated in a class, but for the sake of simplicity they are in a separate unit (uErrorMessages.pas, in Figure 5). To use the technique in an application, this unit has to be listed in main form’s uses clause and we have to call LoadDict before all start; tipically, main form’s OnCreate event handler. DisplayMessageIB from Application.OnException event handler. The DisplayMessageIB function returns a boolean value which will be True if the exception was processed, or False if not (because it could not be an IBError). If it was not processed, then the exception is not one of Interbase, and should follow the standard processing.
It’s easier to show the implementation using an example.
A little example ================
We will build a simple application which uses table Employee from database Employee.gdb in Delphi demos, generate the dictionary file with custom messages, and raise an exception to see the new message. Create a file named ‘employee.ini’ with the following text in it (you can use Windows’ notepad):
[Constraints] INTEG_30=Salary out of bounds
Now start a new application in Delphi, and add a form with a DBGrid and the necessary components to display table Employee from database Employee.gdb. Add the following procedure to main form’s class:
procedure TForm1.ProcessExcept(Sender: TObject; E: Exception); begin if not DisplayMessageIB(E) then ShowMessage(E.Message); end;
Now add these lines in main form’ OnCreate event handler (if your INI file is not in Windows’ directory, modify the LoadDict parameter):
LoadDict(‘employee.ini’); Application.OnException:= ProcessExcept;
Add uErrorMessages.pas to main form’s uses clause.
That’s all. Run the application, and break the constraint –it is a check constraint on Salary field- by changing Leslie Johnson’s salary to 150000. When you try to post the change you should see the new ‘Salary out of bounds’ message. Any other Interbase exception display the standard error message, including the error codes.
Dictionary file creation ========================
All of the above is fine, once we have the INI dictionary file created; but this could be a daunting task, for it involves extracting constraint’s names and associate messages to every one of interest. To help in this task we will write a program which will extract all constraint names, display their definition and let the user write the associated message… it could even create some standard messages automatically.
In brief, this application extracts all constraints names and some other information from Interbase System Tables, and puts that info in a Listview for display and temporary storage. When any one constraint is selected in the ListView, its definition is displayed in a read-only memo component. In another memo component, the user can write its own message for this constraint. When finished, all messages can be saved to an INI file of choice.
It all starts when we select a Database and open it. The information about this Database’s constraints is in its System Tables, special tables created and mantained by Interbase to keep track of all metadata information. The application extract all constraint names and other data using the following query:
SELECT RC.RDB$CONSTRAINT_NAME as Nombre, RC.RDB$CONSTRAINT_TYPE as Tipo, RC.RDB$RELATION_NAME as Tabla, CC.RDB$TRIGGER_NAME as Campo, RC.RDB$INDEX_NAME as Indice, T.RDB$TRIGGER_SOURCE as Definicion FROM RDB$CHECK_CONSTRAINTS CC RIGHT JOIN RDB$RELATION_CONSTRAINTS RC ON CC.RDB$CONSTRAINT_NAME = RC.RDB$CONSTRAINT_NAME LEFT JOIN RDB$TRIGGERS T ON CC.RDB$TRIGGER_NAME = T.RDB$TRIGGER_NAME ORDER BY RC.RDB$CONSTRAINT_TYPE, RC.RDB$CONSTRAINT_NAME
We have no room here to make a deep description of Interbase system tables, but you can find them in Interbase Language Reference document. The following table shows the different kinds of constraints that exist, and where to find the necessary information.
Constraint type | Auxiliary table | Information available =============================================================================== NOT NULL | RDB$CHECK_CONSTRAINTS | Affected field name
CHECK | RDB$TRIGGERS | Check expression
PRIMARY KEY | RDB$INDEX_SEGMENTS | Index fields
UNIQUE | RDB$INDEX_SEGMENTS | Index fields
FOREIGN KEY | RDB$INDEX_SEGMENTS | Foreign Key index fields. | RDB$REF_CONSTRAINTS | Name of referenced PRIMARY KEY o | UNIQUE constraint.
The result of this query is used to populate a ListView with
* Constraint name * Constraint type (‘CHECK’, ‘PRIMARY KEY’, etc.) * Table where constraint is defined * Constraint field, for NOT NULL type * Expression, for CHECK constraint * Index name for PRIMARY KEY, UNIQUE and FOREIGN KEY * Message to display (taken from dictionary file)
The code is in procedure ReadConstraints. This ListView serves both display and data storage purposes.
The next step is taken when the user selects an item from the ListView: complete information about the selected constraint is extracted and displayed in a read-only memo. For example, for a CHECK constraint it will show the complete text of the CHECK expression.
Some of this information is already in the ListView; the remaining data is gathered using two extra querys. The first one extracts the list of fields that make up an index, whose name is passed as a parameter:
SELECT RC.RDB$RELATION_NAME as Tabla, S.RDB$FIELD_NAME as Campo FROM RDB$RELATION_CONSTRAINTS RC INNER JOIN RDB$INDEX_SEGMENTS S ON RC.RDB$INDEX_NAME = S.RDB$INDEX_NAME WHERE RC.RDB$INDEX_NAME = :Index ORDER BY S.RDB$FIELD_POSITION
The other query gets information about a referenced master table in a Foreign Key constraint: index, table and fields.
SELECT RC.RDB$CONSTRAINT_NAME as NombreFK, RC.RDB$CONST_NAME_UQ as ConstrRef, RELC.RDB$RELATION_NAME as TablaRef, RELC.RDB$INDEX_NAME as IndiceRef, IND.RDB$FIELD_NAME as CampoRef FROM RDB$REF_CONSTRAINTS RC LEFT JOIN RDB$RELATION_CONSTRAINTS RELC ON RC.RDB$CONST_NAME_UQ = RELC.RDB$CONSTRAINT_NAME LEFT JOIN RDB$INDEX_SEGMENTS IND ON RELC.RDB$INDEX_NAME = IND.RDB$INDEX_NAME WHERE RC.RDB$CONSTRAINT_NAME = :FK ORDER BY IND.RDB$FIELD_POSITION
The parameter ‘FK’ holds the name of the Foreign Key constraint.
All this information is used in ListView’s OnSelectItem event handler, assembling a descriptive text which is displayed in a memo.
The user can write a suitable message for the selected constraint in the memo provided; when another constraint is selected, that message will be stored in a column of the ListView.
Automatic message generation ============================
Another feature of this little application is the auto-generation of messages: certainly not too diferent than Interbase’s messages, but at least they could be written in your language! The standard messages are defined as constants; the correct one is selected based on restriction’ type.
The code is pretty straightforward: procedure GenerateMessage writes a new message (if there was any, it is not overwritten) directly to one item of the ListView, whose index is passed as a parameter. This procedure is called from inside a loop when we want to generate messages for all constraints.
The rest of the code is just interface housekeeping.
Enjoy!
|