|
Основной компонент, используемый в обработке ошибок, — TpFibErrorHandler. Он позволяет централизованно обрабатывать ошибки базы данных.
Для иллюстрации возьмем базу данных FIBSAMPLE.GDB, используемую в большинстве примеров по FIBPlus. В нашем примере мы будем использовать две таблицы из этой базы данных: TREFCOUNTRY и PERSON. Они имеют следующее объявление:
CREATE TABLE TREFCOUNTRY (
NAME DNAME30,
FULLNAME DNAME60,
CODCTR DCODCTR NOT NULL,
CAPITAL DNAME30,
REGION DNAME30,
DESCRIPTION DDESCR
);
CREATE TABLE PERSON (
CODPERS INTEGER NOT NULL,
FIRST_NAME DNAME20,
LAST_NAME DNAME20,
COUNTRY DCODCTR
);
ALTER TABLE PERSON ADD CONSTRAINT "PERS CHECK LASTNAME NOTNULL"
check (last_name is not null);
ALTER TABLE PERSON ADD CONSTRAINT "PERS CHECK LASTNAME VALUE"
check (last_name not containing '***');
ALTER TABLE PERSON ADD CONSTRAINT "PERS PRIMARYKEY"
PRIMARY KEY (CODPERS);
ALTER TABLE TREFCOUNTRY ADD CONSTRAINT "Country PRIMARY KEY"
PRIMARY KEY (CODCTR);
ALTER TABLE PERSON ADD CONSTRAINT "PERS FOREIGN KEY"
FOREIGN KEY (COUNTRY) REFERENCES TREFCOUNTRY (CODCTR);
Создайте в Delphi или C++Builder новый проект ErrorHandler. Положите на форму следующие компоненты:
StatusBar1: TStatusBar;
Panel1: TPanel;
Panel2: TPanel;
Splitter1: TSplitter;
Splitter2: TSplitter;
Memo1: TMemo;
DBGrid1: TDBGrid;
DBGrid2: TDBGrid;
BExit: TSpeedButton;
BRefresh: TSpeedButton;
BSRollback: TButton;
BSCommit: TButton;
Database1: TpFIBDatabase;
WriteTransaction: TpFIBTransaction;
CountryData: TpFIBDataSet;
DSCountry: TDataSource;
PersData: TpFIBDataSet;
DSPerson: TDataSource;
ErrorHandler1: TpFibErrorHandler;
Примечание. Поскольку вы используете компонент TpFibErrorHandler, вы должны явно указать модуль fib в uses для Delphi или в операторе include для C++Builder:
Delphi
uses fib;
C++
#include <fib>;
Свойство DBName компонента Database1 ссылается на базу данных FIBSAMPLE.GDB. Компонент CountryData типа TpFIBDataSet ссылается на таблицу TREFCOUNTRY. Компонент PersData ссылается на таблицу PERSON. Используя SQL Generator, сгенерируйте обычным образом все операторы SQL для этих компонентов.
Компонент DSCountry свяжите с CountryData. Свойство DataSource у DBGrid1 установите в DSCountry. Компонент DBGrid1 отображает содержимое набора данных CountryData. Аналогичным образом свяжите компонент DSPerson с PersData. Свойство DataSource у DBGrid2 установите в DSPerson. Компонент DBGrid2 отображает содержимое PersData.
В поле Memo1 будет отображаться информация об ошибках.
Рис. 1. Проект ErrorHandler. Обработка ошибок базы данных
Напишите следующие обработчики событий щелчка по кнопкам Rollback и Commit:
Delphi
procedure TFormMain.BSRollbackClick(Sender: TObject);begin WriteTransaction.RollbackRetaining; CountryData.FullRefresh; StatusBar1.Panels.Items[1].Text := IntToStr(CountryData.RecordCount); DBGrid1.SetFocus;end;procedure TFormMain.BSCommitClick(Sender: TObject);begin WriteTransaction.CommitRetaining; CountryData.FullRefresh; PersData.FullRefresh; DBGrid1.SetFocus;end;
C++
void __fastcall TFormMain::BSRollbackClick(TObject *Sender){WriteTransaction->RollbackRetaining();
CountryData->FullRefresh();
StatusBar1->Panels->Items[1]->Text =
IntToStr(CountryData->RecordCount);DBGrid1->SetFocus();
}
void __fastcall TFormMain::BSCommitClick(TObject *Sender){WriteTransaction->CommitRetaining();
CountryData->FullRefresh();
PersData->FullRefresh();
DBGrid1->SetFocus();
}
Напишите следующий обработчик события щелчка по кнопке Refresh:
Delphi
procedure TFormMain.BRefreshClick(Sender: TObject);begin CountryData.FullRefresh; PersData.FullRefresh; StatusBar1.Panels.Items[1].Text := IntToStr(CountryData.RecordCount);end;
C++
void __fastcall TFormMain::BRefreshClick(TObject *Sender){CountryData->FullRefresh();
PersData->FullRefresh();
StatusBar1->Panels->Items[1]->Text =
IntToStr(CountryData->RecordCount);
}
Все эти обработчики понадобятся только для того, чтобы подтверждать или откатывать транзакцию, а также для обновления содержимого наборов данных.
Центральная часть этой программы — компонент ErrorHandler1 и обработчик его события OnFIBEventError.
Установите в True все подсвойства свойства Options этого компонента: oeException, oeForeignKey, oeLostConnect, oeCheck, oeUniqueViolation. Это позволит перехватывать и обрабатывать в программе все основные типы исключений при работе с базой данных.
Рис. 2. Характеристики компонента ErrorHandler1.
Компонент TpFibErrorHandler также содержит следующие свойства (только для чтения):
Таблица 1. Свойства только для чтения компонента TpFibErrorHandler.
|
Свойство |
Значение |
|
ConstraintName |
Имя ограничения, вызвавшего ошибку. |
|
ExceptionNumber |
Номер пользовательского исключения (exception), вызвавшего ошибку. Если ошибка вызвана не исключением, то значение будет –1. |
|
LastError |
Тип последнего исключения — объект класса TKindIBError: |
Следующая таблица содержит описание значения подсвойств свойства Options.
Таблица 2. Список подсвойств свойства Options компонента TpFibErrorHandler.
|
Подсвойство |
Значение |
|
oeException |
Обрабатываются пользовательские исключения. Текст ошибки не выводится, номер пользовательского исключения передается в свойстве компонента ExceptionNumber. |
|
oeForeignKey |
Нарушение значения внешнего ключа (foreign key). |
|
oeLostConnect |
Потеря связи с базой данных. |
|
oeCheck |
Нарушение ограничения CHECK. |
|
oeUniqueViolation |
Нарушение ограничения UNIQUE. |
Вернемся к основной части нашей программы. Напишите следующий обработчик события OnFIBEventError для компонента ErrorHandler1:
Delphi
procedure TFormMain.ErrorHandler1FIBErrorEvent(Sender: TObject;
ErrorValue: EFIBError; KindIBError: TKindIBError; var DoRaise: Boolean);var Lasterror: string;
FKindIBError: string;begin Memo1.Lines.Add(#13#10 + '===== ErrorHandler FIBErrorEvent ====='); Memo1.Lines.Add('Sender.ClassName = ' + Sender.ClassName); Memo1.Lines.Add('Sender.Name = ' + (Sender as TComponent).Name); if Sender is TFIBQuery then Memo1.Lines.Add('Owner.Name = ' + (Sender as TFIBQuery).Owner.Name); if Sender is TpFIBStoredProc then Memo1.Lines.Add('Sender.StoredProcName = ' + (Sender as TpFIBStoredProc).StoredProcName); Memo1.Lines.Add('ConstraintName = ' + ErrorHandler1.ConstraintName); Memo1.Lines.Add('ExceptionNumber = ' + IntToStr(ErrorHandler1.ExceptionNumber)); case ErrorHandler1.LastError of keNoError: Lasterror := 'keNoError'; keException: Lasterror := 'keException'; keForeignKey: Lasterror := 'keForeignKey'; keSecurity: Lasterror := 'keSecurity'; keLostConnect: Lasterror := 'keLostConnect'; keCheck: Lasterror := 'keCheck'; keUniqueViolation: Lasterror := 'keUniqueViolation'; keOther: Lasterror := 'keOther'; else Lasterror := 'Undefined'; end; Memo1.Lines.Add('Lasterror = ' + Lasterror); Memo1.Lines.Add('SQLCode = ' + IntToStr(ErrorValue.SQLCode)); Memo1.Lines.Add('IBErrorCode = ' + IntToStr(ErrorValue.IBErrorCode)); Memo1.Lines.Add('Message = ' + ErrorValue.Message); Memo1.Lines.Add('IBMessage = ' + ErrorValue.IBMessage); Memo1.Lines.Add('SQLMessage = ' + ErrorValue.SQLMessage); case KindIBError of keNoError: FKindIBError := 'keNoError'; keException: FKindIBError := 'keException'; keForeignKey: FKindIBError := 'keForeignKey'; keSecurity: FKindIBError := 'keSecurity'; keLostConnect: Lasterror := 'keLostConnect'; keCheck: FKindIBError := 'keCheck'; keUniqueViolation: FKindIBError := 'keUniqueViolation'; keOther: FKindIBError := 'keOther'; else FKindIBError := 'Undefined'; end; Memo1.Lines.Add('KindIBError = ' + FKindIBError);// DoRaise := False;end;
C++
void __fastcall TFormMain::ErrorHandler1FIBErrorEvent(TObject
*Sender, EFIBError *ErrorValue, TKindIBError KindIBError, bool &DoRaise){AnsiString Lasterror;AnsiString FKindIBError;Memo1->Lines->Add("");Memo1->Lines->Add("========= ErrorHandler FIBErrorEvent =========");Memo1->Lines->Add("Sender.ClassName = " + Sender->ClassName());Memo1->Lines->Add("Sender.Name = " + dynamic_cast(Sender)->Name); if (Sender->ClassNameIs("TFIBQuery"))
Memo1->Lines->Add("Owner.Name = " + dynamic_cast(Sender)->Owner->Name); if (Sender->ClassNameIs("TpFIBStoredProc"))
Memo1->Lines->Add("Sender.StoredProcName = " + dynamic_cast(Sender)->StoredProcName); Memo1->Lines->Add("ConstraintName = " + ErrorHandler1->ConstraintName);Memo1->Lines->Add("ExceptionNumber = " + IntToStr(ErrorHandler1->ExceptionNumber));switch (ErrorHandler1->LastError)
{ case keNoError: Lasterror = "keNoError"; break; case keException: Lasterror = "keException"; break; case keForeignKey: Lasterror = "keForeignKey"; break; case keSecurity: Lasterror = "keSecurity"; break; case keLostConnect: Lasterror = "keLostConnect"; break; case keCheck: Lasterror = "keCheck"; break; case keUniqueViolation: Lasterror = "keUniqueViolation"; break; case keOther: Lasterror = "keOther"; break; default: Lasterror = "Undefined";}Memo1->Lines->Add("Lasterror = " + Lasterror);Memo1->Lines->Add("SQLCode = " + IntToStr(ErrorValue->SQLCode));Memo1->Lines->Add("IBErrorCode = " + IntToStr(ErrorValue->IBErrorCode));Memo1->Lines->Add("Message = " + ErrorValue->Message);Memo1->Lines->Add("IBMessage = " + ErrorValue->IBMessage);Memo1->Lines->Add("SQLMessage = " + ErrorValue->SQLMessage);switch (KindIBError)
{ case keNoError: FKindIBError = "keNoError"; break; case keException: FKindIBError = "keException"; break; case keForeignKey: FKindIBError = "keForeignKey"; break; case keSecurity: FKindIBError = "keSecurity"; break; case keLostConnect: Lasterror = "keLostConnect"; break; case keCheck: FKindIBError = "keCheck"; break; case keUniqueViolation: FKindIBError = "keUniqueViolation"; break; case keOther: FKindIBError = "keOther"; break; default: FKindIBError = "Undefined";}Memo1->Lines->Add("KindIBError = " + FKindIBError);// DoRaise = false;
}
Обработчику передаются следующие параметры: 1. ErrorValue — объект класса EFIBError. Класс содержит следующие свойства:
· IBErrorCode — содержит код ошибки InterBase. Является наиболее информативным описателем ошибки. Существует около 400 различных кодов. Список кодов приведен в документе InterBase Language Reference в 5 главе Error Codes and Messages (таб. 5.5).
· IBMessage — содержит текст сообщения об ошибке.
· SQLCode — содержит код ошибки SQLCODE.
· SQLMessage — содержит сообщение об ошибке SQL.
2. KindIBError — объект класса TKindIBError. Может иметь значения, описанные в таблице 1, в свойстве LastError.
3. DoRaise — переменная логического типа. Позволяет указать, следует ли после обработки ошибки в данном обработчике вызывать повторно исключение. Если DoRaise присваивается значение True (по умолчанию), то после выполнения действий в обработчике ошибок будет вызвано стандартное исключение с выдачей соответствующих сообщений. Если же DoRaise имеет значение False, то действие, вызвавшее ошибку, отменяется, никаких сообщений не выдается. В нашем обработчике ошибок базы данных вся возможная информация об ошибке — с использованием свойств компонента TpFibErrorHandler и параметров, передаваемых в процедуру, — выводится в поле Memo1.
Внимание. Компонент TpFibErrorHandler позволяет обрабатывать множество ошибок базы данных. При этом ошибки подключения к базе данных в нем не обрабатываются (не путать с ошибкой при потере подключения и попытках возобновления подключения). Для этого следует использовать стандартные средства Delphi или C++Builder — блок try…except (Delphi) или try…catch (C++Builder). Не обрабатываются также такие ситуации, когда для компонента, работающего с базой данных (DataSet, Query, SoredProc и некоторые другие), не указана база данных или транзакция.
Пример перехвата ошибки подключения к базе данных.
Delphi
try Database1.Connected := True;except ShowMessage(’Ошибки при подключении к базе данных’);Application.Terminate;
end;
C++
try{ Database->Connected = true; }catch(...){ ShowMessage("Ошибки при подключении к базе данных"); Application->Terminate();}
Теперь мы можем посмотреть, как работает наш обработчик ошибок.
Запустите программу на выполнение. Удалите значение первичного ключа (CODCTR) в любой строке в таблице TREFCOUNTRY (левый DBGrid). Это приводит к тому, что значение поля становится NULL, что недопустимо для первичного ключа. В поле Memo1 появится следующий текст:
========= ErrorHandler FIBErrorEvent =========Sender.ClassName = TFIBQuerySender.Name = UpdateQueryOwner.Name = CountryDataConstraintName = ExceptionNumber = -1Lasterror = keOtherSQLCode = -625IBErrorCode = 335544347Message = FormMain.CountryData.UpdateQuery:The insert failed because a column definition includes validation constraints.validation errorfor column CODCTR, value "*** null ***". IBMessage = validation error for column CODCTR, value "*** null ***". SQLMessage = The insert failed because a column definition includes validation constraints.KindIBError = keOther
Обратите внимание на первые три строчки сообщения после заголовка:
Sender.ClassName = TFIBQuerySender.Name = UpdateQueryOwner.Name = CountryData
Здесь Sender.ClassName содержит имя класса объекта, вызвавшего исключение (TFIBQuery). Sender.Name — имя объекта (UpdateQuery), Owner.Name — имя владельца объекта: имя компонента TpFIBDataSet (CountryData). Если мы в этот же набор данных попытаемся добавить новую запись с пустым значением первичного ключа, то получим такое же сообщение, только значением Sender.Name будет InsertQuery.
Аналогичный результат будет получен, если вы попытаетесь установить в NULL значение первичного ключа (CODPERS) таблицы PERSON.
Попытка ввести дублированное значение первичного ключа в любую строку в таблице PERSON приводит к выдаче сообщения:
========= ErrorHandler FIBErrorEvent =========Sender.ClassName = TFIBQuerySender.Name = UpdateQueryOwner.Name = PersDataConstraintName = PERSExceptionNumber = -1Lasterror = keUniqueViolationSQLCode = -803IBErrorCode = 335544665Message = violation of PRIMARY or UNIQUE KEY constraint "PERS PRIMARYKEY" on table "PERSON". IBMessage = violation of PRIMARY or UNIQUE KEY constraint "PERS PRIMARYKEY" on table "PERSON". SQLMessage = Invalid insert or update value(s): object columns areconstrained - no 2 table rows can have duplicate column values.KindIBError = keUniqueViolation
Аналогичным образом можно проверить реакцию на нарушение других ограничений базы данных. Проверим результат обработки ошибки «потеря соединения». Для моделирования потери соединения завершите выполнение сервера InterBase/Firebird. Если запущена программа Guardian, следует завершить ее до завершения сервера. После этого нужно щелкнуть по кнопке Refresh. Это вызовет желаемую ошибку. В результате обработчик выдаст следующие сообщения:
========= ErrorHandler FIBErrorEvent =========
Sender.ClassName = TFIBQuery
Sender.Name = RefreshQueryOwner.Name = CountryDataConstraintName = ExceptionNumber = -1Lasterror = keLostConnectSQLCode = -901IBErrorCode = 335544741Message = FormMain.CountryData.RefreshQuery:Unsuccessful execution caused by system error that does not preclude successful execution ofsubsequent statements.connection lost to database. IBMessage = connection lost to database. SQLMessage = Unsuccessful execution caused by system error that does not preclude successfulexecution of subsequent statements.KindIBError =
Обратите внимание на значения SQLCode и IBErrorCode. SQLCode = -901. Если посмотреть значения SQLCode и IBErrorCode в документации по InterBase (Language Reference), то можно увидеть, что этому значению соответствует ровно 60 вариантов ошибок. Значение же IBErrorCode = 335544741 имеет только один тип ошибки: «connection lost to database», то есть, потеря соединения с базой данных. Еще раз следует подчеркнуть, что с целью обработки ошибок базы данных в программе наиболее полезным является именно параметр IBErrorCode, передаваемый обработчику ошибок.
Отдельно стоит посмотреть, как работает обработчик ошибок при вызове из хранимой процедуры или из триггера пользовательского исключения. Нам нужно добавить в нашу базу данных FIBSAMPLE.GDB исключение и простенькую хранимую процедуру, вызывающую это исключение.
Создать исключение можно следующим образом, используя файл скрипта:
CONNECT 'D:\Указываем конкретный путь к базе данных\FIBSAMPLE.GDB'
USER 'SYSDBA' PASSWORD 'masterkey';
CREATE EXCEPTION EXCEPT1 'Exception 1';
commit;
Аналогично создается хранимая процедура после создания исключения:
CONNECT 'D:\ Указываем конкретный путь к базе данных\FIBSAMPLE.GDB'
USER 'SYSDBA' PASSWORD 'masterkey';
CREATE PROCEDURE EXEEXCEPT1
AS
begin
EXCEPTION EXCEPT1;end;
commit;
Для вызова исключения нужно также внести изменения в наш проект. Положите на форму кнопку с именем Exception и компонент TpFIBStoredProc. Для компонента TpFIBStoredProc задайте следующие характеристики:
Рис. 3. Характеристики компонента TpFIBStoredProc обращения к хранимой процедуре
Напишите следующий обработчик щелчка по кнопке Exception:
Delphi
procedure TFormMain.ExceptionClick(Sender: Tobject);
begin FIBStoredProc1.ExecProc;end;
C++
void __fastcall TformMain::ExceptionClick(Tobject *Sender)
{FIBStoredProc1->ExecProc();
}
Запустите программу на выполнение, щелкните по кнопке Exception. В поле Memo1 отобразится следующее:
========= ErrorHandler FIBErrorEvent =========
Sender.ClassName = TpFIBStoredProc
Sender.Name = FIBStoredProc1
Sender.StoredProcName = EXEEXCEPT1ConstraintName = ExceptionNumber = 1Lasterror = keExceptionSQLCode = -836IBErrorCode = 335544517Message = Exception 1.IBMessage = exception 1.Exception 1. SQLMessage = exception 268785020.KindIBError = keException
Здесь Sender.ClassName имеет значение (TpFIBStoredProc), поскольку исключение мы получили при вызове хранимой процедуры. Sender.Name содержит имя компонента, через который была вызвана хранимая процедура (FIBStoredProc1). Sender.StoredProcName содержит имя хранимой процедуры.
Здесь следует напомнить, что пользовательское исключение влияет только на программу, вызвавшую это исключение через хранимую процедуру или триггер. Другим приложениям оно не передается.
Последний пример на конфликт одновременного изменения одной и той же записи разными клиентами. Запустите два экземпляра программы. Это будут два разных клиента. Измените любую запись в одном процессе и ту же запись в другом процессе. При переходе на следующую запись в DBGrid (при этом для изменяемой записи выдается Post) будет вызвано исключение:
========= ErrorHandler FIBErrorEvent =========Sender.ClassName = TFIBQuerySender.Name = UpdateQueryOwner.Name = CountryDataConstraintName = ExceptionNumber = -1Lasterror = keOtherSQLCode = -901IBErrorCode = 335544345Message = FormMain.CountryData.UpdateQuery:Unsuccessful execution caused by system error that does not preclude successful execution ofsubsequent statements.lock conflict on no wait transaction.deadlock.update conflicts with concurrent update. IBMessage = lock conflict on no wait transaction.deadlock.update conflicts with concurrent update. SQLMessage = Unsuccessful execution caused by system error that does not preclude successfulexecution of subsequent statements.KindIBError = keOtherМы рассмотрели механизм использования TpFIBErrorHandler и продемонстрировали перехват всех основных видов ошибок, которые могут возникнуть при работе приложения с реальной базой данных. Способ и смысл обработки этих исключительных ситуаций зависят от программы, которую вы разрабатываете, а FIBPlus предлагает вам достаточно гибкий и удобный инструмент для перехвата и анализа.

До приобретения FIBPlus работал с FB1.5.3 при помощи IBX. Чтоб хоть как-то облегчить себе жизнь разработал компонент, вобравший в себя большинство функциональности, используемой мной в работе с БД: подсчет количества записей без feth all (отдельным запросом), выполнение коротких update с помощью одной функции, пользовательскую сортировку (по клику на колонке таблицы), модификацию данных короткими отдельными транзакциями, функцию получения значения генератора по имени и еще кучу всего. Представьте, как мне стало обидно, когда я просмотрел список функций компонентов FIBPlus и обнаружил все это уже готовым, причем выполненным гораздо более грамотно и универсально. Механизм разделенных транзакций позволил мне не отказываться от использования Db-aware компонентов (что я все же успел воплотить в 2-х проектах, поэтому знаю об этом ужасе не понаслышке ;)).
Теперь все новые проекты создаю ТОЛЬКО на основе FIBPlus, ну и по возможности перевожу на них старые. Команде разработчиков большой респект и пожелание удачи!
>>