Cambiar los mensajes de error de Interbase
Cambiar los mensajes de error de Interbase
Cuando se produce un error en una operación contra el
motor de Bases de Datos de Interbase, éste responde
enviando un código de error y el mensaje
correspondiente. Estos datos se muestran normalmente en un
cuadro de diálogo como el siguiente:
Este mensaje no nos dice mucho sobre la causa del error;
peor todavía en el caso del usuario final.
Conocí un caso en que el usuario vió un
mensaje como este y apagó el equipo pensando que era
un virus!
El problema es que estos mensajes son enviados por
Interbase, por lo que cambiar el texto es cuando menos muy
difícil. Claro que el cuadro de diálogo es
mostrado por nuestra aplicación cliente; esto
implicaría que en algún punto se está
recibiendo y procesando el error… y ahí es
donde podemos 'meter la cuchara' para cambiar el mensaje
por uno más humano.
Los errores en Interbase pueden deberse a
-
Errores de datos (por ej: corrupción de tablas)
-
Falta de datos (se encontró el fin del archivo de
datos antes de terminar la instrucción)
-
Incumplimiento de restricciones impuestas a los datos
(Check, Foreign Key, Primary Key, triggers)
Cada comando SQL que se ejecuta en el servidor resulta en
un código de estado y un array de enteros con
más información. En IBX, las llamadas a la
API son 'envueltas' en un procedimiento llamado Call
que en caso de recibir un mensaje de error llama al
procedimiento IBDatabaseError definido en la unit IB.
Este procedimiento toma el código de error y pide a
Interbase que genere el mensaje de error estándar
asociado, y con esas dos piezas de información crea
una excepción de tipo EIBInterbaseError o
EIBInterbaseRoleError y la eleva al sistema de
gestión de excepciones usando raise.
Si capturamos esas excepciones, ganamos acceso al
código de error SQL y al código de error de
Interbase. Por ejemplo, el error de la figura anterior
(violación de una restricción Check) tiene un
código de Interbase igual a 335544558, y un
código de error SQL igual a -297. Una primera
aproximación a un método de tratamiento
genérico de excepciones de Interbase sería el
siguiente:
procedureTForm1.Excepciones(Sender: TObject; E: Exception);
begin
if E.ClassNameIs('EIBInterbaseError') then
ShowMessage(Format('IBErrorCode: %d - SQLErrorCode: %d',
[EIBInterbaseError(e).IBErrorCode,EIBInterbaseError(e).SQLCode])+#13+
'Mensaje: '+e.Message)
else
ShowMessage(E.Message);
end;
Si asociamos este procedimiento al evento TApplication.OnException,
veremos que ante cualquier error de Interbase aparece un
mensaje con más información que lo normal: el
código de error de Interbase y el código de
error SQL. Ambos están disponibles fácilmente
como propiedades de la clase EIBInterbaseError.
Hagamos una pequeña aplicación para ver el
comportamiento del procedimiento anterior. Creamos una
nueva aplicación, y en el formulario principal
ponemos los siguientes componentes:
-
IBDatabase
-
IBTransaction
-
IBTable
-
Datasource
-
DBGrid
-
DBNavigator
como se ve en la siguiente figura, usaremos la
archiconocida base de datos Employee.gdb que viene
con Delphi y se encuentra normalmente en el directorio
archivos de programaarchivos
comunesborland shareddata. La tabla a
usar es la de países (COUNTRY):
Para que la aplicación utilice nuestro
procedimiento para mostrar los errores, podemos
especificarlo en el evento OnCreate del formulario
principal:
procedure TForm1.FormCreate(Sender: TObject);
begin
Application.OnException:= Excepciones;
ibtable1.Open;
end;
Ejecutemos el programa e intentemos introducir un nuevo
registro, pero con condiciones de error; por ejemplo, al
querer introducir un registro dando valor solamente al
campo COUNTRY (y encima, un
valor repetido) obtenemos el siguiente mensaje:
¿Qué pasó con el código de
error y todo eso? Pues que en este caso, el error no
salió del servidor IB sino de la misma
aplicación; Delphi sabe que el campo CURRENCY debe tener un
valor (está declarado como NO
NULL en la base de datos) y no envía
el registro si no se cumple con esa restricción.
Damos entonces valor al campo CURRENCY, y aceptamos los
cambios… y ahora si, obtenemos el mensaje de error
extendido debido a la violación de la unicidad de
la clave primaria
Recapitulemos: podemos dividir los errores de Bases de
Datos en dos tipos, los generados por Delphi antes
de enviar los datos al servidor SQL, y los devueltos por
el servidor. En este artículo desarrollaremos una
técnica para lidiar con el último tipo de
errores.
La idea es la siguiente. Cada restricción que
definimos en la Base de Datos se almacena con un nombre
único en las tablas de sistema del servidor. Este
nombre es el que se presenta en el mensaje
estándar luego de la palabra 'constraint'.
Así, si producimos otro error de violación
de clave primaria en una tabla diferente a 'COUNTRY', el
código de error será el mismo pero
el nombre de la restricción no. Entonces, si
podemos generar y almacenar en algún lugar un
'diccionario' que asocie a cada restricción un
mensaje de error escrito por nosotros, podemos mostrar
ese mensaje en lugar del estándar.
La idea en sí no es difícil de entender
¿verdad? Ahora veremos como lograrlo, para lo cual
tendremos que bucear un poco en las profundidades de las
tablas de sistema del servidor, la interacción con
IBX, etc.
Las dos tareas a realizar son las siguientes:
-
Detectar los errores de Interbase con detalle
suficiente para saber cuál es la
restricción que se ha violado
-
Crear y administrar un 'diccionario' de equivalencias
entre nombres de restricciones y mensajes de error
En esta primera parte agregaremos al ejemplo anterior los
procedimientos necesarios para detectar los errores,
obtener el nombre de la restricción violada y, si
existe un mensaje asociado a la misma, mostrar ese
mensaje. Las asociaciones entre nombres de restricciones
y mensajes –el diccionario- habrá que
escribirlas manualmente.
En la segunda parte crearemos una aplicación que
muestre las restricciones definidas en una base de datos,
y permita asociar a cada una un mensaje, administrando el
diccionario.
Para la primera parte, creamos un archivo INI con las
siguientes líneas:
[Constraints]
INTEG_2=Ya existe un país con ese nombre
Y una unit con los procedimientos necesarios para
capturar el error y mostrar el mensaje:
unit uMensajesError;
interface
uses Classes, SysUtils;
const
secConstraints = 'Constraints';
procedure CargarDiccionario(const Nombre:string);
function GetConstraintName: string;
function MostrarMensajeIB(E:Exception): Boolean;
implementation
uses IniFiles, IB, Dialogs;
var
Restricciones: TStrings;
procedure CargarDiccionario(const Nombre:string);
var
Diccio: TIniFile;
begin
Diccio:= TIniFile.Create(Nombre);
Diccio.ReadSectionValues(secConstraints,Restricciones);
Diccio.Free;
end;
functionGetConstraintName: string;
var
p: PStatusVector;
i: Integer;
begin
p:= StatusVectorArray;
Result:= '';
i:= 0;
while p[i]<>0 do
begin
if p[i] = 2 then
begin
Result:= PChar(p[i+1]);
Break;
end
else
if p[i]=3 then Inc(i);
Inc(i,2);
end; //while
end;
functionMostrarMensajeIB(E:Exception): Boolean;
var
ConstrName,ConstrValue: string;
begin
if E.ClassNameIs('EIBInterbaseError') then
begin
ConstrName:= GetConstraintName;
ConstrValue:= Restricciones.Values[ConstrName];
if ConstrValue='' then
ShowMessage(Format('IBErrorCode: %d - SQLErrorCode: %d',
[EIBInterbaseError(e).IBErrorCode,EIBInterbaseError(e).SQLCode])+#13+
'Mensaje: '+e.Message)
else
ShowMessage(ConstrValue);
Result:= True;
end
else
Result:= False;
end;
initialization
Restricciones:= TStringList.Create;
finalization
if Assigned(Restricciones) then Restricciones.Free;
end.
En el programa principal solamente tenemos que llamar a
las funciones de esta nueva unidad. La unidad del form
principal completa queda como sigue:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, DB, ExtCtrls, DBCtrls, Grids, DBGrids, IBCustomDataSet, IBTable,
IBDatabase, StdCtrls;
const
NombreDiccio = 'diccio.ini';
type
TForm1 = class(TForm)
IBDatabase1: TIBDatabase;
IBTransaction1: TIBTransaction;
IBTable1: TIBTable;
DBGrid1: TDBGrid;
DBNavigator1: TDBNavigator;
DataSource1: TDataSource;
procedure FormCreate(Sender: TObject);
private
public
procedure Excepciones(Sender: TObject; E: Exception);
end;
var
Form1: TForm1;
implementation
usesIBExternals, uMensajesError;
{$R *.dfm}
procedureTForm1.Excepciones(Sender: TObject; E: Exception);
begin
if not MostrarMensajeIB(E) then
Application.ShowException(E);
end;
procedureTForm1.FormCreate(Sender: TObject);
begin
Application.OnException:= Excepciones;
CargarDiccionario(ExtractFilePath(Application.ExeName)+NombreDiccio);
ibTable1.Open;
end;
end.
Probemos primero si el comportamiento es el deseado.
Generamos un error de violación de la clave
primaria (el único que está tratado en el
archivo de diccionario, por ahora) como hicimos
más arriba; el mensaje que se nos presenta es el
que nosotros definimos:
Mucho mejor! Al final del artículo hay un resumen
de los pasos a realizar en una aplicación para
agregar este proceso de errores.
Veamos ahora por qué el programa anterior
hace lo que hace. No voy a repetir el código,
simplemente referiré a los procedimientos o
funciones por su nombre.
El paso crucial de la técnica es la
recuperación del nombre de la restricción
violada; este dato no está presente en la clase
IBDatabaseError, por lo
que debemos investigar un poco más profundamente
la generación de este objeto por IBX. El resultado
de este estudio es el procedimiento GetConstraintName,
que devuelve el nombre de la restricción
correspondiente al último error detectado.
Como dijimos, después de cada operación
Interbase llena un array con elementos numéricos
enteros que llevan información del resultado al
cliente. Este vector se puede acceder (en IBX) a
través de la función StatusVectorArray de la
unit IB, que nos devuelve un
puntero al primer elemento.
Para encontrar el significado de cada elemento del array
debemos remitirnos a la documentación de
Interbase. En ApiGuide.pdf encontramos
la composición del vector de estado. Este vector
se divide en bloques de elementos (llamados
clusters en la documentación) de distinta
longitud y significado; se identifican por el primer
elemento del bloque según la siguiente tabla:
|
Valor 1er
elemento
|
Cant. de
elementos
|
Significado
|
|
0
|
|
Fin de la información
|
|
1
|
2
|
El elemento siguiente es el código de error
de Interbase
|
|
2
|
2
|
El elemento siguiente es la dirección de un
string provisto por Interbase para reemplazar en el
mensaje estándar de error
|
|
3
|
3
|
El elemento siguiente es el tamaño en bytes
de una cadena de longitud variable pasado por el
servidor; el elemento siguiente a ese es la
dirección de la cadena
|
|
4
|
2
|
El elemento siguiente es un número para
reemplazar en el mensaje estándar de error
|
|
5
|
2
|
El elemento siguiente es la dirección de una
cadena conteniendo el mensaje estándar de
error ya formateado, listo para mostrar
|
|
6
|
2
|
El elemento siguiente es un código de error
de VAX/VMS
|
|
7
|
2
|
El elemento siguiente es un código de error
de UNIX
|
|
8
|
2
|
El elemento siguiente es un código de error
de un dominio Apollo
|
|
9
|
2
|
El elemento siguiente es un código de error
de DOS / OS/2
|
La lista sigue algunos números más; pero ni
siquiera en el manual se ponen de acuerdo, ya que por
ejemplo en la tabla dice que el código 12 indica
que el elemento siguiente es un código de error de
NeXT/Mach, y en las constantes listadas un poco
más abajo aparece como número 15… De
todas maneras, la lista se irá modificando a
medida que Interbase se adapte a distintas plataformas, y
solamente estamos interesados por ahora en los primeros
tipos de bloques:
El número (tipo de bloque) 0 indica que este es el
último elemento del array con información
útil. No se deben interpretar los elementos
siguientes.
El
tipo 1 indica que lo sigue un elemento con el
código numérico de error de Interbase, por
ejemplo para la violación de clave primaria
sería el
335544665.
El
tipo 2 nos dice que el número que sigue en el
array es una dirección de memoria, el comienzo de
una cadena de caracteres que será colocada en el
lugar del parámetro del mensaje de error
estándar del servidor. Por ejemplo, el mensaje
estándar para el error de Violación de
Clave Primaria es (tal como aparece en la
documentación)
Violation of PRIMARY or UNIQUE KEY constraint: "<string>"
Cuando el servidor arma el mensaje de error
estándar, reemplaza la cadena <string> por
la cadena indicada en el vector de estado en el bloque
tipo 2. En el ejemplo que venimos manejando, esta cadena
sería INTEG_2.
El
tipo 3 también especifica la dirección de
una cadena de caracteres, pero esta vez el puntero es
precedido por el número de bytes que ocupa la
misma. Es el único tipo de bloque compuesto por 3
elementos.
El
tipo 4 indica que el segundo elemento del bloque es un
número para ser reemplazado en el mensaje
estándar (como en el caso del bloque tipo 2).
Dado que el procedimiento GetConstraintName
es llamado cuando se produce un error, sabemos que
el vector de estado contiene información del
mismo; entonces solamente tenemos que buscar el primer
bloque tipo 2, y recuperar la cadena referenciada por
él. Ésta cadena es el nombre de la
restricción.
El resto del código se encarga de manejar el
diccionario, es decir el archivo INI que contiene
las equivalencias entre los nombres de restricciones y
los mensajes a mostrar. He optado por una variable de
módulo (sólo válida para la unidad
donde se define) para almacenar la lista de cadenas que
componen el diccionario. Esta lista se crea en la
sección de inicialización de la unit, y se
destruye en la parte de finalización.
El archivo Ini es leído y almacenadas todas sus
cadenas en el procedimiento CargarDiccionario, al que
se le pasa como parámetro el nombre del archivo.
Finalmente, la función MostrarMensajeIB debe ser
llamada desde el manejador global de excepciones. En esta
función buscamos la restricción en la lista
de cadenas; si se encuentra se muestra el mensaje
asociado. Si no, se muestra el mensaje estándar
acompañado por los códigos de error, tanto
de SQL como de Interbase. Si se muestra alguno de los dos
mensajes, se devuelve True para indicar al programa
principal que ya fue visualizado.
Para finalizar, extractemos los pasos necesarios para
incorporar este proceso de errores a una
aplicación existente:
Esto es todo. Huelga decir que la unit uMensajesError debe estar
mencionada en la cláusula uses.
Este programa sencillo muestra las posibilidades; en la
próxima oportunidad armaremos una
aplicación para hacer más sencilla la
gestión del diccionario de asociación entre
los nombres de las restricciones –que muchas veces
son generados por Interbase y tienen nombres muy
difíciles de recordar- y los mensajes a mostrar al
usuario final.
¡Hasta entonces!