Написание собственных движков баз данных |
Top Previous Next |
FastReport позволяет строить отчеты не только на основе данных, определенных в приложении. Вы также можете определить свои собственные источники данных (подключения к БД, таблицы, запросы) прямо в отчете. В комплекте с FastReport идут движки для ADO, BDE, IBX, DBX, FIB. Вы можете создать собственный движок и подключить его к FastReport.
На рисунке показана иерархия классов, предназначенных для создания движков баз данных. Зеленым цветом выделены компоненты нового движка.
Как видно, стандартный набор компонентов движка БД включает в себя Database, Table и Query. Вы можете реализовать все эти компоненты, или только некоторые из них (например, многие БД не имеют компонента типа Table). Также вы можете реализовать компоненты, не входящие в стандартный набор (например, аналог StoredProc).
Рассмотрим подробнее базовые классы.
TfrxDialogComponent - базовый класс для всех невизуальных компонентов, которые могут быть помещены в отчет FastReport. В нем не определено каких-либо важных свойств и методов.
Класс TfrxCustomDatabase является базовым для написания компонент типа TDatabase.
TfrxCustomDatabase = class(TfrxDialogComponent) protected procedure SetConnected(Value: Boolean); virtual; procedure SetDatabaseName(const Value: String); virtual; procedure SetLoginPrompt(Value: Boolean); virtual; procedure SetParams(Value: TStrings); virtual; function GetConnected: Boolean; virtual; function GetDatabaseName: String; virtual; function GetLoginPrompt: Boolean; virtual; function GetParams: TStrings; virtual; public procedure SetLogin(const Login, Password: String); virtual; property Connected: Boolean read GetConnected write SetConnected default False; property DatabaseName: String read GetDatabaseName write SetDatabaseName; property LoginPrompt: Boolean read GetLoginPrompt write SetLoginPrompt default True; property Params: TStrings read GetParams write SetParams; end;
В классе определены следующие свойства:
- Connected – является ли подключение к БД активным; - DatabaseName – имя БД; - LoginPrompt – надо ли запрашивать пароль при поключении к БД; - Params – параметры подключения.
От данного класса наследуется компонент типа TDatabase. Для его реализации необходимо перекрыть все виртуальные методы и вынести нужные свойства в секцию published. Также необходимо добавить свойства, специфичные для вашего компонента.
Классы TfrxDataset, TfrxCustomDBDataset, TfrxDBDataset обеспечивают функции доступа к данным. Ядро FastReport использует эти компоненты для навигации и обращения к полям набора данных. В данном случае они являются частью общей иерархии и не представляют интереса.
TfrxCustomDataSet - базовый класс для компонентов БД, производных от TDataSet. От этого класса наследуются компоненты - аналоги Query, Table, StoredProc. По сути, класс представляет собой обертку над TDataSet.
TfrxCustomDataset = class(TfrxDBDataSet) protected procedure SetMaster(const Value: TDataSource); virtual; procedure SetMasterFields(const Value: String); virtual; public property DataSet: TDataSet; property Fields: TFields readonly; property MasterFields: String; property Active: Boolean; property DBConnected: Boolean; published property Filter: String; property Filtered: Boolean; property Master: TfrxDBDataSet; end;
В классе определены следующие свойства:
- DataSet - ссылка на внутренний объект типа TDataSet; - Fields - ссылка на DataSet.Fields; - Active - активен ли набор данных; - DBConnected – подключен ли набор данных к компоненту TfrxXXXDatabase; - Filter - выражение для фильтрации; - Filtered - активна ли фильтрация; - Master – ссылка на источник данных, являющийся основным. Применяется для связей типа master-detail. - MasterFields – список пар полей вида field1=field2. Применяется для связей типа master-detail.
TfrxCustomTable - базовый класс для компонентов БД типа Table. Класс является оберткой над компонентом типа Table.
TfrxCustomTable = class(TfrxCustomDataset) protected function GetIndexFieldNames: String; virtual; function GetIndexName: String; virtual; function GetTableName: String; virtual; procedure SetIndexFieldNames(const Value: String); virtual; procedure SetIndexName(const Value: String); virtual; procedure SetTableName(const Value: String); virtual; published property MasterFields; property TableName: String read GetTableName write SetTableName; property IndexName: String read GetIndexName write SetIndexName; property IndexFieldNames: String read GetIndexFieldNames write SetIndexFieldNames; end;
В классе определены следующие свойства:
- TableName – имя таблицы; - IndexName – имя индекса; - IndexFieldNames – имена индексных полей.
От данного класса наследуется компонент типа Table. Для его реализации необходимо определить недостающие свойства, как правило, Database. Также необходимо перекрыть виртуальные методы из классов TfrxCustomDataset, TfrxCustomTable.
TfrxCustomQuery - базовый класс для компонентов БД типа Query. Класс является оберткой над компонентом типа Query.
TfrxCustomQuery = class(TfrxCustomDataset) protected procedure SetSQL(Value: TStrings); virtual; abstract; function GetSQL: TStrings; virtual; abstract; public procedure UpdateParams; virtual; abstract; published property Params: TfrxParams; property SQL: TStrings; end;
В классе определены свойства, общие для всех компонентов Query - SQL и Params. Т.к. разные компоненты Query имеют разную реализацию параметров (например, TParams, TParameters), свойство Params имеет тип TfrxParams и является оберткой над конкретным типом параметров.
В данном классе определены следующие методы:
- SetSQL - должен установить свойство SQL компонента типа Query; - GetSQL - должен вернуть свойство SQL компонента типа Query; - UpdateParams - должен скопировать значения параметров в компонент типа Query. Если компонент Query имеет параметры типа TParams, то копирование осуществляется стандартной процедурой frxParamsToTParams.
Рассмотрим создание движка БД на примере IBX. Полный исходный текст движка можно найти в каталоге SOURCE\IBX. Здесь мы будем приводить выдержки из исходного текста с комментариями и некоторыми поправками.
Компоненты IBX, вокруг которых мы будем строить обертку - TIBDatabase, TIBTable, TIBQuery. Соответственно, наши компоненты будут называться TfrxIBXDatabase, TfrxIBXTable, TfrxIBXQuery.
Еще один компонент, который мы должны создать - TfrxIBXComponents, он будет помещен в палитру компонент FastReport при регистрации движка в среде Delphi. При помещении этого компонента в проект Delphi автоматически добавит ссылку на модуль нашего движка в список uses. На этот компонент удобно возложить еще одну задачу - определить у него свойство DefaultDatabase, которое ссылается на уже имеющееся в проекте подключение к БД. По умолчанию все компоненты TfrxIBXTable и TfrxIBXQuery будут ссылаться на это подключение. Компонент необходимо наследовать от класса TfrxDBComponents:
TfrxDBComponents = class(TComponent) public function GetDescription: String; virtual; abstract; end;
Единственная функция должна возвращать описание, например 'IBX Components'. Реализация компонента TfrxIBXComponents следующая:
type TfrxIBXComponents = class(TfrxDBComponents) private FDefaultDatabase: TIBDatabase; FOldComponents: TfrxIBXComponents; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; function GetDescription: String; override; published property DefaultDatabase: TIBDatabase read FDefaultDatabase write FDefaultDatabase; end;
var IBXComponents: TfrxIBXComponents;
constructor TfrxIBXComponents.Create(AOwner: TComponent); begin inherited; FOldComponents := IBXComponents; IBXComponents := Self; end;
destructor TfrxIBXComponents.Destroy; begin if IBXComponents = Self then IBXComponents := FOldComponents; inherited; end;
function TfrxIBXComponents.GetDescription: String; begin Result := 'IBX'; end;
Мы определяем глобальную переменную IBXComponents, которая будет ссылаться на экземпляр компонента TfrxIBXComponents. На случай, если вы несколько раз поместили компонент в проект (хотя это и не имеет смысла), предусмотрено сохранение ссылки на предыдущий компонент и восстановление ее после удаления компонента.
В свойство DefaultDatabase можно поместить ссылку на уже имеющееся в проекте подключение к БД. Мы напишем компоненты TfrxIBXTable, TfrxIBXQuery таким образом, чтобы они могли использовать это подключение по умолчанию (собственно, ради этого нам нужна глобальная переменная IBXComponents).
Следующий компонент - TfrxIBXDatabase. Он представляет собой обертку над TIBDatabase.
TfrxIBXDatabase = class(TfrxCustomDatabase) private FDatabase: TIBDatabase; FTransaction: TIBTransaction; function GetSQLDialect: Integer; procedure SetSQLDialect(const Value: Integer); protected procedure SetConnected(Value: Boolean); override; procedure SetDatabaseName(const Value: String); override; procedure SetLoginPrompt(Value: Boolean); override; procedure SetParams(Value: TStrings); override; function GetConnected: Boolean; override; function GetDatabaseName: String; override; function GetLoginPrompt: Boolean; override; function GetParams: TStrings; override; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; class function GetDescription: String; override; procedure SetLogin(const Login, Password: String); override; property Database: TIBDatabase read FDatabase; published { определяем свойства, имеющиеся у TIBDatabase. Обратите внимание, что многие св-ва уже есть в родительском классе. } property DatabaseName; property LoginPrompt; property Params; property SQLDialect: Integer read GetSQLDialect write SetSQLDialect; { свойство Connected надо располагать последним! } property Connected; end;
constructor TfrxIBXDatabase.Create(AOwner: TComponent); begin inherited; { создаем компонент - подключение } FDatabase := TIBDatabase.Create(nil); { создаем компонент - транзакцию (специфика IBX) } FTransaction := TIBTransaction.Create(nil); FDatabase.DefaultTransaction := FTransaction; { не забудьте эту строку! } Component := FDatabase; end;
destructor TfrxIBXDatabase.Destroy; begin { удаляем транзакцию } FTransaction.Free; { а подключение удалится автоматически в родительском классе } inherited; end;
{ описание компонента - оно будет показано рядом с иконкой в панели объектов } class function TfrxIBXDatabase.GetDescription: String; begin Result := 'IBX Database'; end;
{ перенаправляем свойства компонента на свойства обертки и наоборот } function TfrxIBXDatabase.GetConnected: Boolean; begin Result := FDatabase.Connected; end;
function TfrxIBXDatabase.GetDatabaseName: String; begin Result := FDatabase.DatabaseName; end;
function TfrxIBXDatabase.GetLoginPrompt: Boolean; begin Result := FDatabase.LoginPrompt; end;
function TfrxIBXDatabase.GetParams: TStrings; begin Result := FDatabase.Params; end;
function TfrxIBXDatabase.GetSQLDialect: Integer; begin Result := FDatabase.SQLDialect; end;
procedure TfrxIBXDatabase.SetConnected(Value: Boolean); begin FDatabase.Connected := Value; FTransaction.Active := Value; end;
procedure TfrxIBXDatabase.SetDatabaseName(const Value: String); begin FDatabase.DatabaseName := Value; end;
procedure TfrxIBXDatabase.SetLoginPrompt(Value: Boolean); begin FDatabase.LoginPrompt := Value; end;
procedure TfrxIBXDatabase.SetParams(Value: TStrings); begin FDatabase.Params := Value; end;
procedure TfrxIBXDatabase.SetSQLDialect(const Value: Integer); begin FDatabase.SQLDialect := Value; end;
{ этот метод нужен для работы с мастером подключения к БД } procedure TfrxIBXDatabase.SetLogin(const Login, Password: String); begin Params.Text := 'user_name=' + Login + #13#10 + 'password=' + Password; end;
Как видим, ничего сложного. Мы создаем объект FDatabase: TIBDatabase, и определяем свойства, которые мы хотели бы видеть в дизайнере. Для каждого свойства пишутся методы Get и Set. Аналогично поступаем с остальными классами, которые также являются обертками над соответствующими компонентами БД.
Следующий класс - TfrxIBXTable. Он, как говорилось выше, наследуется от стандартного класса TfrxCustomDataSet. Вся базовая функциональность (работа со списком полей, базовыми свойствами) уже реализована в базовом классе. Нам необходимо определить только те свойства, которые являются специфичными для данного компонента. Также необходимо перекрыть методы SetMaster, SetMasterFields для работы механизма master-detail.
TfrxIBXTable = class(TfrxCustomTable) private FDatabase: TfrxIBXDatabase; FTable: TIBTable; procedure SetDatabase(const Value: TfrxIBXDatabase); protected procedure Notification(AComponent: TComponent; Operation: TOperation); override; procedure SetMaster(const Value: TDataSource); override; procedure SetMasterFields(const Value: String); override; procedure SetIndexFieldNames(const Value: String); override; procedure SetIndexName(const Value: String); override; procedure SetTableName(const Value: String); override; function GetIndexFieldNames: String; override; function GetIndexName: String; override; function GetTableName: String; override; public constructor Create(AOwner: TComponent); override; constructor DesignCreate(AOwner: TComponent; Flags: Word); override; class function GetDescription: String; override; procedure BeforeStartReport; override; property Table: TIBTable read FTable; published property Database: TfrxIBXDatabase read FDatabase write SetDatabase; end;
constructor TfrxIBXTable.Create(AOwner: TComponent); begin { создаем компонент - таблицу } FTable := TIBTable.Create(nil); { присваиваем ссылку на него свойству DataSet из базового класса - не забудьте эту строку! } DataSet := FTable; { присваиваем ссылку на подключение к БД по умолчанию } SetDatabase(nil); { после этого можно вызывать базовый конструктор } inherited; end;
{ этот конструктор вызывается в момент, когда вы добавляете компонент в отчет. Он автоматически подключает таблицу к компоненту TfrxIBXDatabase, если он уже есть. } constructor TfrxIBXTable.DesignCreate(AOwner: TComponent; Flags: Word); var i: Integer; l: TList; begin inherited; l := Report.AllObjects; for i := 0 to l.Count - 1 do if TObject(l[i]) is TfrxIBXDatabase then begin SetDatabase(TfrxIBXDatabase(l[i])); break; end; end;
class function TfrxIBXTable.GetDescription: String; begin Result := 'IBX Table'; end;
{ отслеживаем удаление компонента TfrxIBXDatabase, на который мы ссылаемся в св-ве FDatabase. Иначе можем получить ошибку. } procedure TfrxIBXTable.Notification(AComponent: TComponent; Operation: TOperation); begin inherited; if (Operation = opRemove) and (AComponent = FDatabase) then SetDatabase(nil); end;
procedure TfrxIBXTable.SetDatabase(const Value: TfrxIBXDatabase); begin { обратите внимание - свойство Database типа TfrxIBXDatabase, а не TIBDatabase! } FDatabase := Value; { если значение <> nil, подключаем таблицу к выбранному компоненту } if Value <> nil then FTable.Database := Value.Database { иначе пробуем подключить к БД по умолчанию, определенной в компоненте TfrxIBXComponents } else if IBXComponents <> nil then FTable.Database := IBXComponents.DefaultDatabase { если по каким-то причинам TfrxIBXComponents не существует, сбрасываем в nil } else FTable.Database := nil; { если подключились успешно, надо установить флаг DBConnected } DBConnected := FTable.Database <> nil; end;
function TfrxIBXTable.GetIndexFieldNames: String; begin Result := FTable.IndexFieldNames; end;
function TfrxIBXTable.GetIndexName: String; begin Result := FTable.IndexName; end;
function TfrxIBXTable.GetTableName: String; begin Result := FTable.TableName; end;
procedure TfrxIBXTable.SetIndexFieldNames(const Value: String); begin FTable.IndexFieldNames := Value; end;
procedure TfrxIBXTable.SetIndexName(const Value: String); begin FTable.IndexName := Value; end;
procedure TfrxIBXTable.SetTableName(const Value: String); begin FTable.TableName := Value; end;
procedure TfrxIBXTable.SetMaster(const Value: TDataSource); begin FTable.MasterSource := Value; end;
procedure TfrxIBXTable.SetMasterFields(const Value: String); begin FTable.MasterFields := Value; FTable.IndexFieldNames := Value; end;
{ этот метод необходим в некоторых случаях } procedure TfrxIBXTable.BeforeStartReport; begin SetDatabase(FDatabase); end;
Наконец, последний компонент - TfrxIBXQuery. Он наследуется от базового класса TfrxCustomQuery, в котором уже определены все необходимые свойства. Нам остается только определить свойство Database и перекрыть метод SetMaster (SetMasterFields в случае с Query перекрывать не надо). Реализация остальных методов аналогична компоненту TfrxIBXTable.
TfrxIBXQuery = class(TfrxCustomQuery) private FDatabase: TfrxIBXDatabase; FQuery: TIBQuery; procedure SetDatabase(const Value: TfrxIBXDatabase); protected procedure Notification(AComponent: TComponent; Operation: TOperation); override; procedure SetMaster(const Value: TDataSource); override; procedure SetSQL(Value: TStrings); override; function GetSQL: TStrings; override; public constructor Create(AOwner: TComponent); override; constructor DesignCreate(AOwner: TComponent; Flags: Word); override; class function GetDescription: String; override; procedure BeforeStartReport; override; procedure UpdateParams; override; property Query: TIBQuery read FQuery; published property Database: TfrxIBXDatabase read FDatabase write SetDatabase; end;
constructor TfrxIBXQuery.Create(AOwner: TComponent); begin { создаем компонент - запрос } FQuery := TIBQuery.Create(nil); { присваиваем ссылку на него свойству DataSet из базового класса - не забудьте эту строку! } Dataset := FQuery; { присваиваем ссылку на подключение к БД по умолчанию } SetDatabase(nil); { после этого можно вызывать базовый конструктор } inherited; end;
constructor TfrxIBXQuery.DesignCreate(AOwner: TComponent; Flags: Word); var i: Integer; l: TList; begin inherited; l := Report.AllObjects; for i := 0 to l.Count - 1 do if TObject(l[i]) is TfrxIBXDatabase then begin SetDatabase(TfrxIBXDatabase(l[i])); break; end; end;
class function TfrxIBXQuery.GetDescription: String; begin Result := 'IBX Query'; end;
procedure TfrxIBXQuery.Notification(AComponent: TComponent; Operation: TOperation); begin inherited; if (Operation = opRemove) and (AComponent = FDatabase) then SetDatabase(nil); end;
procedure TfrxIBXQuery.SetDatabase(const Value: TfrxIBXDatabase); begin FDatabase := Value; if Value <> nil then FQuery.Database := Value.Database else if IBXComponents <> nil then FQuery.Database := IBXComponents.DefaultDatabase else FQuery.Database := nil; DBConnected := FQuery.Database <> nil; end;
procedure TfrxIBXQuery.SetMaster(const Value: TDataSource); begin FQuery.DataSource := Value; end;
function TfrxIBXQuery.GetSQL: TStrings; begin Result := FQuery.SQL; end;
procedure TfrxIBXQuery.SetSQL(Value: TStrings); begin FQuery.SQL := Value; end;
procedure TfrxIBXQuery.UpdateParams; begin { в этом методе необходимо присвоить значения из Params в FQuery.Params } { это делается стандартной процедурой } frxParamsToTParams(Self, FQuery.Params); end;
procedure TfrxIBXQuery.BeforeStartReport; begin SetDatabase(FDatabase); end;
Регистрация всех компонент движка выполняется в секции initialization.
initialization { вместо картинок используем индексы стандартных картинок 37,38,39 } frxObjects.RegisterObject1(TfrxIBXDataBase, nil, '', '', 0, 37); frxObjects.RegisterObject1(TfrxIBXTable, nil, '', '', 0, 38); frxObjects.RegisterObject1(TfrxIBXQuery, nil, '', '', 0, 39);
finalization CatBmp.Free; frxObjects.Unregister(TfrxIBXDataBase); frxObjects.Unregister(TfrxIBXTable); frxObjects.Unregister(TfrxIBXQuery);
end.
Этого достаточно для того, чтобы использовать движок в отчетах. Осталось две вещи: зарегистрировать классы движка в скриптовой системе для того, чтобы к ним можно было обращаться из скрипта, и зарегистрировать редакторы некоторых свойств (например, TfrxIBXTable.TableName), чтобы с компонентом было удобно работать.
Код регистрации движка в скриптовой системе лучше вынести в отдельный файл с суффиксом RTTI. Подробнее о регистрации классов в скриптовой системе можно прочитать в соответствующей главе. Вот пример такого файла:
unit frxIBXRTTI;
interface
{$I frx.inc}
implementation
uses Windows, Classes, fs_iinterpreter, frxIBXComponents {$IFDEF Delphi6} , Variants {$ENDIF};
type TFunctions = class(TfsRTTIModule) public constructor Create(AScript: TfsScript); override; end;
{ TFunctions }
constructor TFunctions.Create; begin inherited Create(AScript); with AScript do begin AddClass(TfrxIBXDatabase, 'TfrxComponent'); AddClass(TfrxIBXTable, 'TfrxCustomDataset'); AddClass(TfrxIBXQuery, 'TfrxCustomQuery'); end; end;
initialization fsRTTIModules.Add(TFunctions);
end.
Код редакторов свойств также лучше поместить в отдельный файл с суффиксом Editor. В нашем случае надо написать редакторы для свойств TfrxIBXDatabase.DatabaseName, TfrxIBXTable.IndexName, TfrxIBXTable.TableName. Подробнее о написании редакторов свойств можно прочитать в соответствующей главе. Вот пример такого файла:
unit frxIBXEditor;
interface
{$I frx.inc}
implementation
uses Windows, Classes, SysUtils, Forms, Dialogs, frxIBXComponents, frxCustomDB, frxDsgnIntf, frxRes, IBDatabase, IBTable {$IFDEF Delphi6} , Variants {$ENDIF};
type TfrxDatabaseNameProperty = class(TfrxStringProperty) public function GetAttributes: TfrxPropertyAttributes; override; function Edit: Boolean; override; end;
TfrxTableNameProperty = class(TfrxStringProperty) public function GetAttributes: TfrxPropertyAttributes; override; procedure GetValues; override; end;
TfrxIndexNameProperty = class(TfrxStringProperty) public function GetAttributes: TfrxPropertyAttributes; override; procedure GetValues; override; end;
{ TfrxDatabaseNameProperty }
function TfrxDatabaseNameProperty.GetAttributes: TfrxPropertyAttributes; begin { это свойство имеет редактор } Result := [paDialog]; end;
function TfrxDatabaseNameProperty.Edit: Boolean; var SaveConnected: Bool; db: TIBDatabase; begin { получаем ссылку на TfrxIBXDatabase.Database } db := TfrxIBXDatabase(Component).Database; { создаем стандартный OpenDialog } with TOpenDialog.Create(nil) do begin InitialDir := GetCurrentDir; { нас интересуют файлы *.gdb } Filter := frxResources.Get('ftDB') + ' (*.gdb)|*.gdb|' + frxResources.Get('ftAllFiles') + ' (*.*)|*.*'; Result := Execute; if Result then begin SaveConnected := db.Connected; db.Connected := False; { если диалог завершен успешно, присваиваем новое имя БД } db.DatabaseName := FileName; db.Connected := SaveConnected; end; Free; end; end;
{ TfrxTableNameProperty }
function TfrxTableNameProperty.GetAttributes: TfrxPropertyAttributes; begin { свойство представляет собой список значений } Result := [paMultiSelect, paValueList]; end;
procedure TfrxTableNameProperty.GetValues; var t: TIBTable; begin inherited; { получаем ссылку на компонент TIBTable } t := TfrxIBXTable(Component).Table; { заполняем список доступных таблиц } if t.Database <> nil then t.DataBase.GetTableNames(Values, False); end;
{ TfrxIndexProperty }
function TfrxIndexNameProperty.GetAttributes: TfrxPropertyAttributes; begin { свойство представляет собой список значений } Result := [paMultiSelect, paValueList]; end;
procedure TfrxIndexNameProperty.GetValues; var i: Integer; begin inherited; try { получаем ссылку на компонент TIBTable } with TfrxIBXTable(Component).Table do if (TableName <> '') and (IndexDefs <> nil) then begin { обновляем индексы } IndexDefs.Update; { заполняем список доступных индексов } for i := 0 to IndexDefs.Count - 1 do if IndexDefs[i].Name <> '' then Values.Add(IndexDefs[i].Name); end; except end; end;
initialization frxPropertyEditors.Register(TypeInfo(String), TfrxIBXDataBase, 'DatabaseName', TfrxDataBaseNameProperty); frxPropertyEditors.Register(TypeInfo(String), TfrxIBXTable, 'TableName', TfrxTableNameProperty); frxPropertyEditors.Register(TypeInfo(String), TfrxIBXTable, 'IndexName', TfrxIndexNameProperty);
end.
|