Cambiar los mensajes de error de Interbase-1

By: Ernesto Cullen

Abstract: Tecnica para cambiar asignar un mensaje propio a cada restriccisn de una base de datos de Interbase, en clientes realizados con IBX

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:

  • Agregar a la unit del form principal un procedimiento de manejo de errores compatible con el tipo TExceptionEvent, si no existe.
  • En este procedimiento se debe llamar a MostrarMensajeIB de la unit uMensajesError, por ejemplo:
    
      if not MostrarMensajeIB(E) then
        Application.ShowException(E);
    
  • En algún momento antes que se produzcan los errores (típicamente, en el evento OnCreate del form principal) hay que direccionar el evento OnException de la aplicación al nuevo procedimiento. Por ejemplo, si el procedimiento se llama Excepciones entonces pondríamos
    
      Application.OnException:= Excepciones;
    
  • También antes de producirse las excepciones debemos cargar los mensajes llamando al procedimiento CargarDiccionario de la unit uMensajesError, enviando como parámetro el nombre del archivo INI a usar.

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!

Server Response from: SC3