Jump to content


Photo

Capturar Indices Unique Keys


  • Please log in to reply
4 replies to this topic

#1 coruxito

coruxito

    Advanced Member

  • Miembros
  • PipPipPip
  • 55 posts

Posted 14 May 2012 - 03:44 AM

Hola amigos,

Tengo varios campos que los cree como Unique Keys en mi base de datos.
Lo que ocurre es que me gustaría hacer el tratamiento de cual de ellos salta de una manera más centralizada, no hacer un select cada vez que voy insertar un valor para saber si existe o no.
Por ejemplo tengo índices como IND_EMPLEADO_NOMBRE E IND_EMPRESA_RAZONSOCIAL. Las dos no son PRIMARY KEYS, únicamente son campos que no quiero que se repitan en la base de datos. Cuando em ambas tablas intento meter uno de esos campos duplicado me salta el típico:

[attempt to store duplicate value(visible to active transactions) in unique index "IND_EMPLEADO_NOMBRE"]
o
[attempt to store duplicate value(visible to active transactions) in unique index "IND_EMPRESA_RAZONSOCIAL"]

Miro que es un patrón que se repite y me gustaria poder capturar cual es exactamente, así haria algo un poco más "elegante" y mejor aprovechando lo que la Firebird ya está haciendo sin meter más carga al SGBD.

Sabéis alguna manera de capturar cual indices está saltando ?
  • 0

#2 Delphius

Delphius

    Advanced Member

  • Administrador
  • 6301 posts
  • LocationArgentina

Posted 14 May 2012 - 07:30 AM

Hola coruxito,
No recuerdo bien el texto de todas las excepciones posibles de capturar pero si ese es el texto que te muestra el cuadro de diálogo de la excepción es posible entonces capturarla y extraer de éste el nombre del índice.
Como toda excepción, en su propiedad Message tiene el texto a mostrar. Aprovechando la cláusula try-except capturas la excepción y evalúas el Message para ver si contiene dicho texto, luego resta localizar en éste la primera comilla doble ("), lo que sigue del texto es justamente el nombre del índice.

Para lograr eso te puedes valer de funciones de tratamiento para cadenas como Pos(), Copy(), etc.

¿Me explico?



delphi
  1. try
  2. // hacer algo
  3. except
  4. // capturamos la excepción y buscamos el texto
  5.   on E: Exception do
  6.   begin
  7.     // primero nos aseguramos que se trate efectivamente de la excepción correcta
  8.     PosExp := Pos('[attempt to store duplicate value(visible to active transactions) in unique index', E.Exception);
  9.     if PosExp = 1
  10.       then begin
  11.                   // entonces buscamos la comilla
  12.                   PosExp := Pos('"',E.Message);
  13.                   // copiamos el texto solamente desde la pos +1 hasta justo antes de "] del final
  14.                   NombreIndice := Copy(E.Message, PosExp + 1, Length(E.Message) - PosExp - 2);
  15.               end;
  16.   end;
  17. end;



Ojo: está escrito al vuelo, es sólo una orientación. Desconozco si existe una excepción propia para este tipo de error. De ser así entonces directamente en vez de evaluar con Exception utilizamos la clase definida y no se necesitaría de estar leyendo en Message si el texto es del error por índice.

Saludos,
  • 0

#3 cadetill

cadetill

    Advanced Member

  • Moderadores
  • PipPipPip
  • 994 posts
  • LocationEspaña

Posted 14 May 2012 - 09:01 AM

Buenas,

Yo te propongo una solución algo más elaborada. No está terminado (a ver si tengo tiempo y lo termino para publicarlo en el blog), pero para hacerte una idea de por donde tirar quizás pueda servirte. El ejemplo está hecho usando FIBPlus, dependiendo de los componentes que uses puede cambiar algo el código (pero la idea debería ser la misma)

Primero el bloque de código que puede dar error, en este caso al hacer el post



delphi
  1.     try
  2.       dsFIBMaster.Append;
  3.       dsFIBMasterID.AsInteger := 1;
  4.       dsFIBMasterCADENA.AsString := '2';
  5.       dsFIBMasterUNICO.AsInteger := 2;
  6.       dsFIBMasterREQUERIDO.AsInteger := 2;
  7.       dsFIBMaster.Post;
  8.     except // aquí empieza el control de error. Fíjate que se capturan diferentes tipos de errores, aunque básicamente siempre se llama a la misma función
  9.       on E: EDatabaseError do
  10.         ShowMessage(GetFIBMessageError(E));
  11.       on E: EFIBClientError do
  12.         ShowMessage(GetFIBMessageError(E));
  13.       on E: Exception do
  14.         ShowMessage(E.ClassName);
  15.     end;



Y ahora el código que gestiona el error y devuelve la cadena resultante



delphi
  1. function GetFIBMessageError(E: Exception): string;
  2.   function GetValidationFieldMsg(BDMsg, Raiser: string): string;
  3.   var
  4.     Column,
  5.     ErrorValue: string;
  6.   begin
  7.     Column := Copy(BDMsg, Pos('for column', BDMsg)+10, Pos(', value', BDMsg)-(Pos('for column', BDMsg)+10));
  8.     ErrorValue := Copy(BDMsg,
  9.                       PosEx('"', BDMsg, Pos(', value', BDMsg)),
  10.                       PosEx('"', BDMsg, PosEx('"', BDMsg, Pos(', value', BDMsg))));
  11.     Result := Format(errValidationField, ['Validación de campo incorrecta',
  12.                                           Raiser,
  13.                                           Format(errValidation, [Trim(Column), Trim(ErrorValue)])]);
  14.   end;
  15.  
  16.   function GetConstraintFields(aConstraint: string): string;
  17.   const
  18.     SQL = 'select i.rdb$field_name, i.rdb$field_position from RDB$INDEX_SEGMENTS i where i.rdb$index_name = :constraint order by 2';
  19.   var
  20.     Q: TpFIBQuery;
  21.   begin
  22.     Q := TpFIBQuery.Create(nil);
  23.     try
  24.       Result := '';
  25.       Q.Database := dbFIBBase;
  26.       Q.Transaction := trFIBTrans;
  27.       Q.Options := [qoStartTransaction];
  28.       Q.SQL.Text := SQL;
  29.       Q.ParamByName('constraint').AsString := aConstraint;
  30.       Q.ExecQuery;
  31.       while not Q.Eof do
  32.       begin
  33.         if Result = '' then Result := Trim(Q.Fields[0].AsString)
  34.         else Result := Result + ',' + Trim(Q.Fields[0].AsString);
  35.         Q.Next;
  36.       end;
  37.     finally
  38.       if Assigned(Q) then FreeAndNil(Q);
  39.     end;
  40.   end;
  41.  
  42.   function GetDuplicatePrimUnqMsg(BDMsg, Raiser: string): string;
  43.   var
  44.     Constraint,
  45.     Table,
  46.     Cols: string;
  47.     A, B: Integer;
  48.   begin
  49.     A := PosEx('"', BDMsg)+1;
  50.     B := PosEx('"', BDMsg, A+1);
  51.     Constraint := Trim(Copy(BDMsg, A, B-A));
  52.  
  53.     A := PosEx('"', BDMsg, B+1)+1;
  54.     B := PosEx('"', BDMsg, A+1);
  55.     Table := Trim(Copy(BDMsg, A, B-A));
  56.  
  57.     Cols := GetConstraintFields(Constraint);
  58.  
  59.     Result := Format(errPrimaryKey, ['Clave primaria o índice único duplicado',
  60.                                     Raiser,
  61.                                     Format(errDuplicatePriUniq, [Constraint, Cols, Table])]);
  62.   end;
  63.  
  64.   function GetForeingKey(BDMsg, Raiser: string): string;
  65.   var
  66.     Constraint,
  67.     Table,
  68.     Cols: string;
  69.     A, B: Integer;
  70.   begin
  71.     A := PosEx('"', BDMsg)+1;
  72.     B := PosEx('"', BDMsg, A+1);
  73.     Constraint := Trim(Copy(BDMsg, A, B-A));
  74.  
  75.     A := PosEx('"', BDMsg, B+1)+1;
  76.     B := PosEx('"', BDMsg, A+1);
  77.     Table := Trim(Copy(BDMsg, A, B-A));
  78.  
  79.     Cols := GetConstraintFields(Constraint);
  80.  
  81.     Result := Format(errForeingKey, ['Clave foránea inexistente',
  82.                                     Raiser,
  83.                                     Format(errDuplicatePriUniq, [Constraint, Cols, Table])]);
  84.   end;
  85.  
  86.   function GetGeneralMsg(BDMsg, Raiser, Msg: string; IBError, SQLCode: Integer): string;
  87.   begin
  88.     Result := Format(errGeneralText, [BDMsg,
  89.                                       raiser,
  90.                                       Msg,
  91.                                       IBError,
  92.                                       SQLCode]);
  93.   end;
  94. begin
  95.   Result := E.Message;
  96.  
  97.   // si la excepción recibida no es de la clase EFIBError, mostramos un error genérico
  98.   if not(E is EFIBError) then
  99.   begin
  100.     if not(E is EDatabaseError) then
  101.       Result := GetGeneralMsg('Error no codificado', '', E.Message, -1, -1)
  102.     else
  103.       Result := GetGeneralMsg('Error no codificado', '', EDatabaseError(E).Message, -1, -1);
  104.     Exit;
  105.   end;
  106.  
  107.   // aquí personalizamos los diferentes errores
  108.   case EFIBError(E).IBErrorCode of
  109.     isc_not_valid:
  110.       Result := GetValidationFieldMsg(EFIBError(E).IBMessage, EFIBError(E).RaiserName); // validation column;
  111.     isc_unique_key_violation:
  112.       Result := GetDuplicatePrimUnqMsg(EFIBError(E).IBMessage, EFIBError(E).RaiserName); // duplicate primary or unique
  113.     isc_foreign_key: Result :=
  114.       GetForeingKey(EFIBError(E).IBMessage, EFIBError(E).RaiserName); // clave foránea inexistente
  115.  
  116.     // aquí se podría ir añadiendo tipos de errores y así personalizarlos todos. En FIBPlus están todos definidos en la unidad IB_ErrorCodes
  117.  
  118.     else
  119.       Result := GetGeneralMsg(EFIBError(E).IBMessage, EFIBError(E).RaiserName, EFIBError(E).Msg, EFIBError(E).IBErrorCode, EFIBError(E).SQLCode);
  120.   end;
  121. end;



y los mensajes de error (para ya tenerlo todo



delphi
  1. resourcestring
  2.   errConnectingDB = 'Se ha producido un error conectando con la base de datos.'#13#13'Base de datos: %s'#13'Mensaje de error: %s';
  3.  
  4.   // general data
  5.   qInEditInsert = 'Está añadiendo o modificando datos. Si cancela la pantalla sin aceptar los cambios, éstos no se guardarán. ¿Está seguro de querer continuar?';
  6.  
  7.   // Post Errors
  8.   errGeneralText =
  9.     '  Se ha producido un error en la grabación del registro. '#13#13 +
  10.     '  Causa: %s '#13 +
  11.     '  Generado por: %s '#13#13 +
  12.     '  Información extra: %s '#13#13 +
  13.     '  IBErrorCode: %d '#13#13 +
  14.     '  SQLCode: %d';
  15.  
  16.   errValidationField =
  17.     '  Se ha producido un error en la grabación del registro. '#13#13 +
  18.     '  Causa: %s '#13 +
  19.     '  Generado por: %s '#13#13 +
  20.     '  Información extra: %s ';
  21.  
  22.   errPrimaryKey =
  23.     '  Se ha producido un error en la grabación del registro. '#13#13 +
  24.     '  Causa: %s '#13 +
  25.     '  Generado por: %s '#13#13 +
  26.     '  Información extra: %s ';
  27.  
  28.   errForeingKey =
  29.     '  Se ha producido un error en la grabación del registro. '#13#13 +
  30.     '  Causa: %s '#13 +
  31.     '  Generado por: %s '#13#13 +
  32.     '  Información extra: %s ';
  33.  
  34.   errValidation =
  35.     #13'  - Campo: %s '#13 +
  36.       '  - Valor no válido: %s '#13#13 +
  37.       'Modifique el valor e inténtelo de nuevo. Si el problema persiste, contacte con el distribuidor.';
  38.  
  39.   errDuplicatePriUniq =
  40.     #13'  - Índice: %s '#13 +
  41.       '  - Campos afectados: %s '#13 +
  42.       '  - Tabla: %s '#13#13 +
  43.       'Modifique el/los valor/es e inténtelo de nuevo. Si el problema persiste, contacte con el distribuidor.';



Entiendo que si lo hubiera puesto todo en una unit hubiera quedado mejor, pero bueno, espero se entienda jejejeje

Nos leemos

  • 0

#4 coruxito

coruxito

    Advanced Member

  • Miembros
  • PipPipPip
  • 55 posts

Posted 15 May 2012 - 08:03 AM

Hola amigos, agradezco a Delphius y a cadetill. La verdad que al final pillé la idea de Delphis pq la de cadetill sencillamente no la entendí muy bien.
Pero lo hize algo distinto, cree una procedure así:


delphi
  1. procedure ErroresClaveDuplicada(Texto : String);
  2. var
  3.   Error : String;
  4.   Pos_Ini: Integer;
  5. begin
  6.   Pos_Ini:=Pos('IND_',Texto);
  7.   Error:=copy(Texto,Pos_Ini,length(Texto));
  8.   Error:=copy(Error,1,length(Error)-1);
  9.  
  10.   if Error = 'IND_TIP_NOMBRE' then
  11.   begin
  12.     ShowMessage('Tipo ya existe');
  13.   end
  14.   else if Error = 'IND_AGE_NIF' then
  15.   begin
  16.     ShowMessage('NIF del Agente ya existe');
  17.   end
  18.   else if Error = 'IND_CLI_CUENTA' then
  19.   begin
  20.     ShowMessage('Numero de la cuenta ya existe');
  21.   end
  22.   else if Error = 'IND_EMP_NIF' then
  23.   begin
  24.     ShowMessage('NIF del Empleado ya existe');
  25.   end
  26.   else if Error = 'IND_EMP_SS' then
  27.   begin
  28.     ShowMessage('Numero de Seguridad Social del Empleado ya existe');
  29.   end
  30.   else if Error = 'IND_EPR_RAZON_SOCIAL' then
  31.   begin
  32.     ShowMessage('Razón Social de la Empresa ya existe');
  33.   end
  34.   else if Error = 'IND_FOR_NIF' then
  35.   begin
  36.     ShowMessage('NIF del Formador ya existe');
  37.   end
  38.   else if Error = 'IND_MOD_DESCRIPCION' then
  39.   begin
  40.     ShowMessage('Modalidad ya existe');
  41.   end
  42.   else if Error = 'IND_STA_NOMBRE' then
  43.   begin
  44.     ShowMessage('Estado ya existe');
  45.   end
  46.   else if Error = 'IND_STU_DESCRIPCION' then
  47.   begin
  48.     ShowMessage('Formación ya existe');
  49.   end
  50.   else
  51.   begin
  52.     ShowMessage('Error Desconocido');
  53.   end;
  54. end;



La instrucción que ejecuta la Query la pongo dentro deu un bloque como ese:



delphi
  1. procedure TFormPrincipal.BGuardarTipoClick(Sender: TObject);
  2. begin
  3.   IBQueryInsertarTipos.ParamByName('tip_denominacion').Value:= ETipDenominacion.Text;
  4.   IBQueryInsertarTipos.ParamByName('tip_horas').Value:= METipHoras.Text;
  5.   IBQueryInsertarTipos.ParamByName('tip_idmodalidad').Value:= IBQueryCargaModalidades.FieldByName('mod_id').Value;
  6.  
  7.   try
  8.     begin
  9.       IBQueryInsertarTipos.ExecSQL;
  10.       IBTransactionINSERT.Commit;
  11.       Principal.RecargarDatos;
  12.       Principal.TipoValorDefecto;
  13.  
  14.       ShowMessage('Tipo Ingresado con Exito');
  15.     end
  16.   except
  17.     on E: Exception do
  18.     begin
  19.       Principal.ErroresClaveDuplicada(E.Message);
  20.     end;
  21.   end;
  22. end;



Hago el mismo para todos formularios donde tenga la necesidad de hacer cambios en la base, de momento sólo funciona para los índices de las claves de tipo UNIQUE, pero me gustó tanto la solución que pretendo mejorarla para abarcar todos errores que sean tratados por los índices de Firebird(cada vez me va gustando más el SGBD).
Espero que sea útil para algunos ese código y que tb pueda ser criticado/mejorado.

Cuando cadetill tenga tiempo creo que seria muy interesante si pudiera explicar si solución para un novato como yo porque la verdad que tiene pinta de algo que abarca muchos tipos de fallos distintos.

Gracias como siempre sois unos cracks !!!
  • 0

#5 Wilson

Wilson

    Advanced Member

  • Moderadores
  • PipPipPip
  • 2137 posts

Posted 15 May 2012 - 09:43 AM

Me alegra que hayas resuelto el asunto.

A continuación te coloco un ejemplo de como centralizar los mensajes de error de todo tipo, provenientes de una base de datos FIREBIRD, para el ejemplo uso DbExpress y obviamente TClientDataset, es un procedimiento único  que sirve para todas las tablas de la BD, que  intercepta el evento OnRenconcileError de todos los TClientDatasets de un módulo y un arcivo de texto de nombre Mensajes.txt que contiene los mensajes de error (incluidas las excepciones, constraints, etc. de toda la DB) y que se encuentra en la misma carpeta del ejecutable.

Pongamos como ejemplo  la siguiente BD:

CREATE EXCEPTION CODIGO_VACIO 'EL CODIGO NO DEBE ESTAR EN BLANCO';
CREATE EXCEPTION NOMBRE_VACIO 'DEBE DE INGRESAR UN NOMBRE DE ENTIDAD';

CREATE TABLE NOMBRES (
    ID_NOMBRE      INTEGER NOT NULL,
    NOMBRE          VARCHAR(30) NOT NULL ,
    CODIGO          VARCHAR(10) NOT NULL ,

PRIMARY KEY (ID_NOMBRE),
CONSTRAINT UQ_CODIGO_REPETIDO UNIQUE (CODIGO)
);

CREATE TABLE OTRATABLA (
    ID_TABLA      INTEGER NOT NULL,
    TABLA          VARCHAR(30) NOT NULL ,

PRIMARY KEY (ID_TABLA),
CONSTRAINT UQ_TABLA_REPETIDA UNIQUE (TABLA)
);


Agregamos en la sección private del módulo de datos (en Delphi) el siguiente campo:



delphi
  1. private
  2.   FErrores: TStringList;



Y luego intreceptar el evento OnReconcileError de los TClientDatset así­: (Si usas otros componentes de conexión, basta interceptar el evento apropiado de los Datasets y compartir el procedimiento)



delphi
  1. procedure TMiModulo.Conciliacion(DataSet: TCustomClientDataSet;
  2.   E: EReconcileError; UpdateKind: TUpdateKind; var Action: TReconcileAction);
  3. const
  4.   Archivo = 'Mensajes.txt';
  5. var
  6.   K: Integer;
  7.   S, N: string;
  8. begin
  9.   if not Assigned(FErrores) then
  10.   begin
  11.     FErrores := TStringList.Create;
  12.     S := ExtractFilePath(GetModuleName(HInstance)) + Archivo;
  13.     if FileExists(S) then
  14.       FErrores.LoadFromFile(S);
  15.   end;
  16.   S := E.Message;
  17.   for K := 0 to FErrores.Count - 1 do
  18.   begin
  19.     N := FErrores.Names[K];
  20.     if Pos(N, S) <> 0 then
  21.     begin
  22.       S := FErrores.Values[N];
  23.       Break;
  24.     end;
  25.   end;
  26.   MessageDlg(S, mtError, [mbOk], 0);
  27. end;



A continuación escribo como deberian ir algunas lineas del archivo de texto:

CODIGO_VACIO=El código no debe estar en blanco//o lo que tu quieras
NOMBRE_VACIO=Debe ingresar un nombre de entidad
UQ_CODIGO_REPETIDO=Este codigo ya existe
UQ_TABLA_REPETIDA=Esta tabla ya existe


Saludos
  • 0




IP.Board spam blocked by CleanTalk.