ЧАСТЬ 1

В данной статье речь пойдет не о том, как расставить пару-тройку
компонентов,
для того, что бы вывести на экран пользователя содержимое таблицы.
Напротив, я
попытаюсь рассказать о том, как самостоятельно создать такие компоненты.

Изначально Delphi позиционировался как среда разработки, обеспечивающая
максимальное удобство для создания приложений, работающих именно с базами
данных. Собственно, и сейчас БД-ориентированные приложения являются
одной из
приоритетных областей применения Delphi. Действительно, еще в средине
90-х был
создан удобный и достаточно эффективный, для своего времени, механизм
доступа к
данным. Простейшие приложения можно было буквально "писать мышкой". Я
имею в
виду, не технологию BDE, как таковую, а набор абстрактных классов, для
доступа к
данным, ключевым элементом которого является TDataSet. Со временем этот
механизм
развивался, но и по сей день подавляющее большинство компонентов доступа к
источникам данных наследуют именно TDataSet.

TDataSet - абстрактный класс, в котором описывается (но не реализуется)
большинство методов, необходимых для доступа к СУБД. Реализация этих
методов
зависит от конкретной СУБД и фантазии разработчика. Компоненты отображения
данных, которые мы привыкли использовать (например, TDBGrid или TDBMemo)
используют только те методы, которые описаны в TDataSet. При этом их работа
никак не зависит от конкретной реализации компонента доступа к данным.

Недавно мне пришлось столкнуться с проблемой самостоятельной реализации
такого набора компонентов
. Замечу, что эта задача достаточно интересна и
одновременно сложна. Положение усугубляется практически полным отсутствием
"родной" документации по этой теме. В Интернете, мне удалось найти лишь три
источника адекватно иллюстрирующих данный процесс. При этом, каждый из
них, не
смотря на то, что описывает общие принципы написания пользовательского
DataSet'а,
предлагает собственный подход к решению задачи. В своем коде я реализовал
комбинированное решение.

Подобный пример может быть полезным с разных точек зрения. Во-первых,
для
того, что бы писать действительно надежные и производительные приложения,
следует четко представлять внутренние механизмы работы компонентов
доступа к
базам данных. Во-вторых, не исключено, что рано или поздно вы столкнетесь с
необходимостью создать собственный набор компонентов доступа к БД, или
модифицировать существующий. Наконец, приемы, используемые для организации
доступа к данным, могут стать полезными и при решении других задач,
напрямую не
связанных с обработкой данных, хранящихся в базах.

В одной из своих предыдущих статей я сделал небольшой (и, как
выяснилось не
совсем полный) обзор
Embedded СУБД
, предоставляющих механизмы доступа к данным с помощью
.Net
провайдеров. Там я посетовал на отсутствие поддержки Delphi в СУБД
ScimoreDB.
Именно эту СУБД я и собрался "подружить" с этой популярной средой
разработки.
Задачу можно разделить на два этапа. На первом этапе потребуется
организовать
прямой доступ к ScimoreDB. На втором нужно будет создать компонент-
наследник
TDataSet, в котором будут переопределены абстрактные методы, отвечающие за
доступ к данным. И при написании кода этих методов мы используем приемы,
разработанные на первом этапе.

Прежде всего, давайте посмотрим, какие механизмы доступа к SimoreDB
предоставляют сами разработчики. Кроме .Net провайдера (о котором шла
речь в
предыдущей статье) в комплект поставки входит реализация C++ классов для
доступа
к базе из Visual Studio. Классы, о которых идет речь, собранны в dll.
Соответственно, нашей первой задачей становится получение доступа к ним из
Delphi.

Механизм создания объектов -- экземпляров классов, реализованных в C++ dll,
подробно описан в статье "Об
использовании в Delphi классов, созданных в MS VC++ (Экспорт открытых
методов
чужого класса в свою программу)
". Вкратце поясню его суть. Нам
необходимо
создать паскалевский модуль описания классов. В нем описываются только
public
методы. При этом в коде самой dll должны четко соблюдаться соглашения о
вызовах.
В противном случае придется написать еще одну библиотеку -- оболочку, в
которой
описание классов будет приведено к необходимому виду. Сами объекты будут
создаваться не при помощи привычного Delphi-синтаксиса (Object1:=
TClass.Create()),
а с помощью вызова специально описанных в той же dll функций (Object1:=
NewObject). Не буду детально останавливаться на реализации заголовочного
модуля,
специальную сборку dll и сам заголовочный модуль был предоставлен
разработчиками
ScimoreDB. Код модуля содержится в файле ScdriverDelphi.pas.

Теперь, когда получен доступ к Scimore API, давайте разберемся с тем,
как это
работает. Сделаем небольшой пример. Создадим новый проект. В каталог, в
котором
он будет сохранен, скопируем 2 файла -- scdriver.dll и ScdriverDelphi.pas.
Последний из них сразу следует включить в проект (в IDE жмем Shift+F11). На
главной форме разместим три компонента TEdit. Из них мы будем считывать
параметры соединения с сервером БД.

edServer: TEdit;
edInstID: TEdit;
edPort: TEdit;

Само соединение будем осуществлять по нажатию соответствующей кнопки:

btnConnect: TButton;

Затем добавим модуль ScdriverDelphi.pas в секцию Uses модуля главной
формы
программы. В секции private формы объявим три переменные -- экземпляры
классов
TConnection, TFieldStream и TRecordset:

private
{ Private declarations }
smConn: TConnection;
smFStream: TFieldStream;
smRecordSet: TRecordset;

Попытаемся установить соединение с базой данных. Код подключения будет
выглядеть примерно так:

smConn:=NewConnection;
smConn.Connect(PChar(edServer.Text),
StrToIntDef(edPort.Text, 999), StrToIntDef(edInstID.Text,-1));

Вписываем его в обработчик события OnClick нашей кнопки.

Если вы все сделали правильно, то соединение с сервером должно
устанавливаться при нажатии на кнопку. Естественно, предварительно
необходимо
указать значения сервера, порта и InstId. Если при установке СУБД не
изменялись
настройки сервера по умолчанию, то значения Server, Port и InstId следует
указать следующие: localhost, 999 и -1, соответственно. В случае если
соединение
по каким-то причинам не было установлено, будет выведено сообщение об
ошибке. В
принципе, здесь не лишним было бы обработать исключение.

Следующим шагом будет создание самой базы и тестовых таблиц. Здесь мы
могли
бы воспользоваться Scimore Database Manager'ом, но для чистоты эксперимента
сделаем это программно, выполнив на сервере SQL код. Сайт разработчика и
техническая документация предоставляет достаточно широкий набор
примеров. Мы
просто модифицируем один из них. Таким образом, нам предстоит выполнить
следующий код:

CREATE DATABASE IF NOT EXISTS example;
GO;
USE example;
DROP TABLE IF EXISTS author;
DROP TABLE IF EXISTS book;
CREATE TABLE book (
book_id UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
author_id INT PARTITION REFERENCES author (author_id),
book_ TEXT
);
GO;
CREATE TABLE author (
author_id UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
Name_ varchar)
GO;

Код, выполняющий данный запрос в Delphi будет выглядеть примерно
следующим
образом:

procedure TfDirectMain.btnCreateDBClick(Sender: TObject);
var
smCommand: TCommand;
comText: PANSIChar;
comm: TStrings;
begin
try
smCommand:=NewCommand;
if FileExists(ExtractFilePath(Application.ExeName)+'\SQL\createDB.SQL')
then
begin
try
comm:=TStringList.Create;
comm.LoadFromFile(ExtractFilePath(Application.ExeName)+'\SQL\createDB.SQL');
comText:=comm.GetText;
finally
comm.Free;
end;
smCommand.PrepareText(smConn,comText);
smCommand.Execute;
smConn.GetCompletionStatus;
end;
finally
smCommand.Free;
end;
end;

Здесь мы, по аналогии с TConnection, создаем новый объект TCommand.
Текст
самой SQL команды загружаем из файла (это несколько удобнее для отладки).
Инициируем команду и выполняем ее (методы PrepareText и Execute). Обратите
внимание на то, что после выполнения любой активной SQL команды мы должны
вызвать метод GetCompletionStatus нашего объекта TConnection. По завершении
выполнения процедуры освобождаем объект TCommand. Поскольку в нашей
программе
еще не реализована функциональность отображения данных из таблицы --
результат
исполнения приведенного выше кода мы сможем наблюдать в Scimore Database
Manager'е.

Теперь приступим к реализации наиболее трудоемкой части приложения. Нам
необходимо обеспечить механизмы модификации данных: insert, delete и
update, а
также организовать данных графическое отображение содержимого таблиц.

На данном этапе записи таблицы мы можем показывать, используя
TstringGrid.
Естественно, что заполнять ячейки нам придется с помощью собственного кода.
Такой код, на первый взгляд, может показаться несколько путанным, но ничего
сложного он, в принципе, не содержит. Прежде всего, мы создаем новые
объекты
RecordSet и TCommand и подготавливаем текст оператора Select. В отличие от
предыдущего примера, вместо Execute для TCommand , мы используем метод
Open, а в
качестве параметра передадим объект RecordSet. С помощью последнего и будет
осуществляться доступ к набору данных, возвращаемому в результате
выполнения
запроса.

procedure TfDirectMain.btnOpenDBClick(Sender: TObject);
var
cmd2, cmd: TCommand;
pb: integer;
begin
StringGrid1.RowCount:=1;
try
smRecordSet:=NewRecordSet;
cmd:=NewCommand;
cmd.PrepareText(smConn, 'use example select * from author');
cmd.Open(smRecordSet);
smConn.GetCompletionStatus;
ClearGrid(StringGrid1);
SetGridVal(smRecordSet, StringGrid1);
finally
cmd.Free;
cmd:=nil;
smRecordSet.Free;
smRecordSet:=nil;
end;
end;

Здесь мы видим вызов еще двух процедур. Первая из них (ClearGrid) просто
очищает StringGrid и устанавливает минимально возможное количество строк и
столбцов, подготавливая его к отображению данных из RecordSet.

procedure TfDirectMain.ClearGrid(sg: TStringGrid);
var
i,j: integer;
begin
for I := 1 to sg.RowCount do
for j := 1 to sg.ColCount do
begin
sg.Cells[j,i]:='';
end;
sg.RowCount:=2;
sg.FixedRows:=1;
end;

Вторая (SetGridVal) непосредственно осуществляет отображение данных.
Рассмотрим ее детальнее. Экземпляр класса TRecordSet представляет собою
однонаправленный курсор. Доступ к записям производится последовательно.
Перед
тем как поочередно перебрать все записи и занести их значения в ячейки
StringGrid'а, мы определяем количество полей таблицы (метод GetFieldCount).
Затем производим перебор записей до тех пор, пока не будет достигнут конец
набора данных (IsEOF). При этом в цикле осуществляется считывание значения
каждого из полей текущей записи. Они считываются в поток TFieldStream с
помощью
метода GetFieldStreamByIndex, а информация о поле (тип данных, имя поля
и т.д.)
извлекается путем вызова метода GetFieldInfoByIndex, возвращающего
значение типа
FIELD_INFO. Далее эти значения переносятся из потока в переменные
требуемого
типа (для этого используются указатели), преобразовываются в строку и
заносятся
в соответствующую ячейку StringGrid.

function TfDirectMain.SetGridVal(smRS: TRecordSet; sg:
TStringGrid):
integer;
Type
PLargeInt = ^Int64;
var
fs: TFieldStream;
i, j: integer;
ftp: data_type;
fieldData: PChar;
fSize: Integer;
LIBuf: PLargeInt;
IBuf: PInteger;
begin
j:=0;
sg.ColCount:= smRS.GetFieldCount+1;
while not smRS.IsEOF do
begin
Inc(j);
for I := 0 to smRS.GetFieldCount - 1 do
begin
fs:=smRS.GetFieldStreamByIndex(i);
ftp:= smRS.GetFieldInfoByIndex(i).typ;
fSize:= fs.GetFieldSize;
GetMem(fieldData, fSize);

if j>1 then
begin
if i=0 then
sg.RowCount:=StringGrid1.RowCount+1
end
else
sg.Cells[i+1,0]:= smRS.GetFieldInfoByIndex(i).name;

case ftp of
DB_UNIQUEIDENTIFIER:
begin
fs.ReadField(PByte(@fieldData^), fSize);
Move(fieldData, LIBuf, 8);
sg.Cells[i+1,j]:= IntToStr(LIBuf^);
end;

DB_CHAR:
begin
fs.ReadField(PByte(@fieldData^), fSize);
sg.Cells[i+1,j]:=fieldData;
end;
DB_INT:
begin
fs.ReadField(PByte(@fieldData^), fSize);
Move(fieldData, IBuf, 4);
sg.Cells[i+1,j]:= IntToStr(IBuf^);
end;
DB_TEXT:
begin
sg.Cells[i+1,j]:= 'Text';

end;
end;
end;
smRS.Next;
end;
result:=j;
end;

Что бы проверить работоспособность кода мы можем добавить несколько
записей
вручную (ведь наша программа пока еще не умеет этого делать), используя
все тот
же Manager. Просто выполняем следующий запрос:

Use Example Insert into Author (name_) values ('Пушкин')

Запустив программу, вы сможете убедиться в том, что данные об авторе
успешно
отображаются на форме.

Теперь попробуем реализовать добавление записи программно. Прежде всего,
создадим еще одну форму. Она будет содержать поле ввода, куда будет
заноситься
фамилия нового автора, и две кнопки btnOK и btnCancel.

Фактически нам понадобится изменить лишь надписи (Caption) и значения
свойства ModalResult для кнопок, соответственно, Caption -- 'OK' и
'Cancel', а
ModalResult - mrOK и mrCancel. Никакого кода в модуле, содержащем форму,
писать
не потребуется.

Код обработчика события OnClick для кнопки, отвечающей за добавление
записи в
таблицу Authors, будет выглядеть так.

procedure TfDirectMain.btnAddAuthorsClick(Sender: TObject);
var
smCommand: TCommand;
begin
fAuthor.ShowModal;
if fAuthor.ModalResult=mrOK then
begin
try
smCommand:=NewCommand;
smCommand.PrepareText(smConn,
'Use Example Insert into Author (name_) values (?a)');
smCommand.AddParameter('a', DB_VARCHAR,
PByte(PChar(fAuthor.edName.Text)), SizeOf(fAuthor.edName.Text));
smCommand.Execute;
finally
smCommand.Free;
end;
end;

end;

Следует обратить внимание на тот факт, что параметр в ScimoreDB SQL
обозначается не двоеточием (как мы привыкли это делать при использовании
различных Delphi компонентов), а символами ??? либо ?@?. Поэтому запрос,
передаваемый СУБД будет иметь следующий вид:

Use Example Insert into Author (name_) values (?a)

При выборе в таблице авторов какой-либо записи нам потребуется
отобразить
перечень написанных им книг. Для того, что бы реализовать эту возможность
обработаем событие OnSelectCell StringGrid1. Это событие будет возникать
каждый
раз при выборе новой ячейки в первой таблице.

procedure TfDirectMain.StringGrid1SelectCell(Sender: TObject;
ACol,
ARow: Integer; var CanSelect: Boolean);
var
AuthorID: Integer;
cmd: TCommand;
begin
AuthorID:= StrToIntDef(StringGrid1.Cells[1,ARow],-1);
smRecordSet:=NewRecordSet;
try
cmd:=NewCommand;
cmd.PrepareText(smConn, 'Use Example select * from book where author_id=
?i');
cmd.AddParameter('i', DB_INT, @(AuthorID), 4);
cmd.Open(smRecordSet);
smConn.GetCompletionStatus;

ClearGrid(StringGrid2);
bookCount:= SetGridVal(smRecordSet, StringGrid2);
finally
smRecordSet.Free;
smRecordSet:=Nil;
cmd.Free;
cmd:=nil;
end;
end;

Фактически, считывание набора книг производится так же, как и считывание
списка авторов, с той лишь разницей, что для книг будет проверяться
принадлежность к текущему автору.

Удаление книги реализовать достаточно просто. Приведенный ниже код,
думаю, в
комментариях не нуждается. А для таблицы авторов мы попытаемся реализовать
каскадное удаление. Прежде чем удалить автора из таблицы, удалим все его
книги.

procedure TfDirectMain.btnDeleteBookClick(Sender: TObject);
var
cmd: TCommand;
BookId: INT64;

begin
BookID:= StrToIntDef(StringGrid2.Cells[1,StringGrid2.Selection.Top],-1);
try
smRecordSet:=NewRecordSet;
cmd:=newCommand;
cmd.PrepareText(smConn, 'use example delete from book where book_id =
?b');
cmd.AddParameter('b', DB_BIGINT, @BookID, 8);
cmd.Execute;
smConn.GetCompletionStatus;

finally
cmd.Free;
end;
end;

procedure TfDirectMain.btnDeleteAuthorClick(Sender: TObject);
var
cmd: TCommand;
AuthID: Int64;
begin
AuthID:= StrToIntDef(StringGrid1.Cells[1,StringGrid1.Selection.Top],-1);
if AuthID>-1 then
begin
try
smRecordSet:=NewRecordSet;
cmd:=NewCommand;
cmd.PrepareText(smConn, 'use example delete from book where author_id =
?a;
Go; use example delete from author where author_id=?a; Go;');
cmd.AddParameter('a', DB_BIGINT, @AuthID, 8);
smConn.GetCompletionStatus;
cmd.Execute;
finally
cmd.Free;
smRecordSet.Free;
smRecordSet:=nil;
end;
end;
btnOpenDB.Click;
end;

По аналогии несложно реализовать и Update.

Приложение, реализованное подобным образом, вполне работоспособно. В
ряде
случаев имеет смысл использовать прямой доступ к данным. Например, для
повышения
производительности приложения, или при необходимости осуществить
какое-нибудь
нестандартное решение. Однако, трудоемкость его реализации значительно
выше, чем
при использовании компонентов доступа к данным. Тем не менее, для создания
собственных производных от TDataSet, потребуется понимание механизмов
прямого
доступа к СУБД.

Во второй части статьи на безе кода полученного приложения, мы
попытаемся
создать собственные компоненты доступа к данным.

Код данного приложения вы сможете найти в папке DirectDemo. Не забудьте
установить ScimoreDB
Server
,
прежде чем запускать этот код.

ЧАСТЬ 2

Прежде чем приступить к разработке компонентов, обеспечивающих доступ
к базе
данных, с которыми могли бы работать элементы управления из группы Data
Controls,
нам нужно установить соединение с ScimoreDB. Создадим новый компонент -
наследник класса TСustomConnection. Как и TDataSet, TСustomConnection
является
абстрактным классом.

TsmConnection = class(TCustomConnection)
private
FConnectEvents: TList;
FConnected: boolean;
FStreamedConnected: Boolean;
FAfterConnect: TNotifyEvent;
FAfterDisconnect: TNotifyEvent;
FBeforeConnect: TNotifyEvent;
FBeforeDisconnect: TNotifyEvent;
FDBName: String;
FConnPort: integer;
FServerName: AnsiString;
FInstId: integer;
FNeedLoading: boolean;
function GetConnString: string;
procedure SetInstId(const Value: integer);
procedure EnsureInactive;
procedure SetConnPort(const Value: integer);
procedure SetDBName(const Value: String);
procedure SetServerName(const Value: Ansistring);
procedure CheckDatabaseName;
procedure CheckInactive;
function GetConnected: boolean;
procedure setConnected(const Value: boolean);
protected
FStreamedConnected;
procedure DoConnect; override;
procedure DoDisconnect; override;
procedure Loaded; override;
procedure SendConnectEvent(Connecting: Boolean);
property ConnectionString: string read GetConnString;

public
FConn: TConnection;
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;

published
property Connected: boolean read GetConnected write setConnected;
property Server: string read FServerName write SetServerName;
property DBName: string read FDBName write SetDBName;
property Port: integer read FConnPort write SetConnPort;
property InstId: integer read FInstId write SetInstId default -1;

property AfterConnect: TNotifyEvent read FAfterConnect write
FAfterConnect;
property BeforeConnect: TNotifyEvent read FBeforeConnect write
FBeforeConnect;
property AfterDisconnect: TNotifyEvent read FAfterDisconnect write
FAfterDisconnect;
property BeforeDisconnect: TNotifyEvent read FBeforeDisconnect write
FBeforeDisconnect;
end;

Реализация большинства методов не составляет особого труда, сам же
механизм
соединения с базой описан в первой части статьи. Я не стану здесь
приводить код
реализации методов, а сразу перейду к созданию компонента, производного от
TDataSet.

В первой части статьи мы разобрались с механизмом получения объекта
- набора
данных. Так же мы смогли получить информацию о полях, составляющих этого
набора
(имя, тип данных, размер). Основная проблема заключается в том, что
данный набор
является однонаправленным
. Иными словами, навигация в нем осуществляется
только
с помощью метода Next
. Вернуться к предыдущей записи или "перескочить"
через
несколько записей мы не можем. Обычные же Delphi компоненты,
используемые для
доступа к данным, реализуют такую функциональность. В связи с этим, нам
придется
полностью копировать данные из набора TRecordSet и работать с локальной
копией.
Для хранения данных, в принципе, можно использовать любую структуру.
Марко Кэнту
в своей книге Mastering Delphi 7 предлагает воспользоваться для этой
обычным
потоком и осуществлять навигацию путем динамического вычисления требуемой
позиции в потоке. Такой подход достаточно трудоемок, мы воспользуемся
наследником класса TCollection. Сами же данные (Value) конкретной записи
(TPhysRecord)
мы будем хранить в потоке.

TPhysRecord = class (TCollectionItem)
private
Value: TStream;
Blobs: array of TStream;
public
constructor Create(Collection: TCollection); override;
destructor Destroy; override;
end;

TRecords=class(TCollection)
public
Constructor Create;
function Add: TPhysRecord;
End;

В дальнейшем, во избежание путаницы, я буду называть коллекцию данных -
локальным набором данных.

Ключевым понятием для реализации пользовательского класса-наследника
TDataSet
является буфер. В не зависимости от того, каким образом организуется
хранение
записей в локальном наборе данных, буфер содержит информацию о конкретной
(текущей) записи. Обычно, буфер содержит в себе информацию, необходимую для
навигации по локальному набору данных, значения обычных полей, а также
значения
вычисляемых (Calculated) полей.

Отличительной особенностью буфера записи является то, что его размер
должен
быть одинаков для каждой записи. До тех пор, пока в таблице встречаются
поля
только фиксированного размера проблем с буфером у нас не возникнет. Но как
только в таблицу добавятся поля, размер которых может варьироваться
(Text, BLOB
и т.д.) размер буфера, соответственно, тоже будет меняться. Далее мы
детально
остановимся на этом вопросе, сейчас же просто обратите внимание на то,
что в
классе TPhysRecord наряду с полем Value описано поле Blobs (Blobs: array of
TStream). Этот массив потоков мы и будем использовать для хранения значений
полей произвольного размера.

Обычно, при реализации пользовательских наборов данных создают два
класса,
один из которых реализует методы работы с локальным набором данных, но не
реализует взаимодействия с источником данных. Второй класс - наследник
первого,
содержит в себе реализацию той части кода, в которой происходит
непосредственное
взаимодействие с базой данных.

Мы поступим точно так же и тоже создадим два класса - TSMBaseDataset
= class(TDataset)
и TsmDataset=class(TSMBaseDataset).

Все методы, которые нам предстоит написать, можно условно разделить на
несколько групп, по признаку их функциональности: методы формирования
локального
набора данных, методы навигации, методы работы с буфером и методы
модификации
данных. По понятным причинам я не буду приводить полный код методов, его вы
сможете посмотреть в исходных текстах компонента. Остановлюсь лишь на
наиболее
интересных моментах.

Для создания и уничтожения буфера записи используется два метода -
AllocRecordBuffer и FreeRecordBuffer.

Естественно, что реализация наследника класса TDataSet для работы с
конкретной СУБД потребует понимания принципов работы с этой СУБД.

Метод GetRecord также является одним из ключевых методов. В
результате его
работы присваиваются значения полей. При этом, то, каким образом
значения полей
распределяются в буфере не имеет значения. Описывается метод следующим
образом:

function TDataset.GetRecord(Buffer: PChar; GetMode: TGetMode;
DoCheck:
Boolean): TGetResult;

Функция вызывается с тремя параметрами. Параметр GetMode указывает на то
каким образом следует осуществить навигацию. Здесь возможно три
варианта:

gmCurrent -- указывает на необходимость передать в буфер текущую
запись;
gmNext - указывает на необходимость передать в буфер запись, следующую за
текущей;
gmPrior - указывает на необходимость передать в буфер запись,
предшествующую
текущей.

Результат функции может принимать четыре значения:

grEof - достигнут конец локального набора данных;
grBof -- текущая запись -- перая в локальном наборе данных;
grOK -- опреация прошла успешено;
grError - возникла ошибка.

За то, каким образом значения полей извлекаются из буфера и заносятся
в буфер
после модификации отвечают методы GetFieldData и SetFieldData.

function TDataset.GetFieldData(Field: TField; Buffer: Pointer):
Boolean;
procedure TDataset.SetFieldData(Field: TField; Buffer: Pointer);

Именно в этих методах мы реализуем преобразование типов, описанных в
структуре TField к реальным Delphi типам. Фрагмент кода, отвечающего
непосредственно за копирования значения поля в буфер будет выглядеть
примерно
так:

case Field.DataType of
ftSmallInt: Move((RecBuffer + Offset)^, LargeInt(Buffer^),
sizeof(SmallInt));
ftInteger, ftTime, ftDate: Move((RecBuffer + Offset)^, Integer(Buffer^),
sizeof(Integer));
...
End;

Метод GetRecordSize возвращает размер записи, которой получается путем
сложения размера данных, размера служебной информации и размера вычисляемых
полей.

Result := GetDataSize + sizeof(TRecordInfo) +
CalcFieldsSize;

Необходимая нам служебная информация описывается структурой
следующего вида:

TRecordInfo = record
RecordID: Pointer;
Bookmark: Pointer;
BookMarkFlag: TBookmarkFlag;
end;

Код метода InternalOpen следует рассмотреть детально. Смысл его несложно
определить из названия.

procedure TSMBaseDataset.InternalOpen;
begin
if DoOpen then
begin
BookmarkSize := GetBookMarkSize; //Bookmarks not supported
InternalInitFieldDefs;
if DefaultFields then
CreateFields;
BindFields(True);
FisOpen := True;
FillBufferMap;
end;
end;

Из кода мы видим, что прежде всего вызывается метод DoOpen. Именно
там мы
осуществляем выполнение запроса и создаем объект TRecordSet,
реализованный в
библиотеке доступа к ScimoreDB. Там же мы заполняем локальный набор
данных. В
соответствии с логикой реализации компонентов, DoOpen мы реализуем в классе
TsmDataSet, поскольку там происходит взаимодействие с физическим источником
данных. Фактически в DoOpen происходит тоже, что и в примере из первой
части
статьи при заполнении значениями компонента TStringGrid с тем лишь
отличием, что
данные из TRecordSet нам нужно записывать в локальный набор данных.
Следующий
ниже код показывает то, каким образом мы переносим значение в локальный
набор.
Соответственно код этот должен выполняться для каждого поля каждой
записи набора
TrecordSet.

fs := FrecordSet.GetFieldStreamByIndex(i);
fc := fs.GetFieldSize;
fs.ReadField(PByte(@data), fc);
PhysRec.Value.Write(data, fc);

Метод InternalInitFieldDefs отвечает за создание полей, также
используя для
этого информацию, хранящуюся в TRecordSet. Но прежде чем наш экземпляр
TDataSet
получит необходимо проверить, не были ли они заданы по умолчанию в
приложении
(программно или с использованием редактора полей).

Метод InternalClose отвечает за закрытие набора данных.
Метод InternalHandleException вызывается в том случае, когда происходит
исключение. В большинстве случаев код метода выглядит следующим
образом:Application.HandleException(Self).
InternalInitRecord вызывается тогда, когда необходимо провести
инициализацию
ранее выделенного буфера.

Кроме того, следует реализовать несколько вспомогательных функций. В
частности, в первой части статьи мы обращали внимание на то, что SQL-парсер
ScimoreDB признаком параметра считает символ ???, а не ?:?, как это
принято в
VCL. Однако, в исходном классе TDataSet реализован механизм разбора
(парсинга)
SQL кода, на основе которого автоматически генерируются параметры запроса
(свойство Parameters). Поэтому, для компонента TsmDataset лучше
использовать
двоеточие, а для того, что бы СУБД "понимала" такие запросы следует
использовать
подобную функцию преобразования:

function TsmDataset.TranslateQuery(const query: string):
PANSIChar;
var
s: string;
begin
s:= query;
s:=PANSIChar(query);
Result:=PANSIChar(ReplaceText(s, ':','?'));
end;

До сих пор мы реализовывали методы, позволяющие нам работать с
набором данных
и, непосредственно, с буфером. В классе TsmDataSet реализуются методы,
которые
обеспечивают непосредственное взаимодействие с базой. Компонент использует
четыре ключевых published свойства - DeleteSQL, InsertSQL, SelectSQL,
ModifySQL.
Они позволяют задать SQL выражения для четырех основных операций, удаления,
вставки, выбора данных и изменения данных, соответственно.

Кроме того, TsmDataSet содержит реализацию практически всех методов
навигации
и методов, необходимых для определения размеров данных. Собственно,
извлечение
самих значений производится здесь же. Код данного компонента довольно
объемный.
Я не стану детально останавливаться на реализации всех методов и свойств
компонента, обращу внимание лишь на ключевые аспекты.

Метод DoCreateFieldDefs отвечает за формирование списка полей, которые
берутся из источника данных (детально такая процедура описана в первой
части
статьи). Здесь же вычисляется размер каждого поля. Полученные данные
передаются
в набор FieldDefs, описанный в родительском классе:

FieldDefs.Add (FieldName, FieldType, testSize, False);

За обновление БД, после любых модификаций внутреннего набора данных
отвечают
методы DoDeleteRecord, InternalPost, DoBeforeEdit.

Методы GetFieldValue и SetFieldValue отвечают за извлечение значения
поля и
присвоения значения полю.

Отдельно хочу обратить внимание на организацию работы с BLOB полями.
Как я
говорил выше, размер буфера для каждой записи в локальном наборе данных
должен
быть одинаковым. Но BLOB поля не имеют фиксированного размера. Поэтому
пришлось
реализовать второй массив - массив потоков (Blobs: array of TStream;) в
который
"складываются" значения BLOB полей. А навигация и обмен данными с этим
массивом
производятся параллельно с основным набором данных.

Конечно, код, рассматриваемый в статье, далек от совершенства, многие
возможности, заложенные в базовый VCL класс TDataSet остались
нереализованными.
Тем не менее, он (код) достаточно тяжел для визуального восприятия.
Понять суть
происходящего практически не возможно без пошагового выполнения в среде
разработки. Такую операцию лучше производить на небольшом наборе данных,
так как
многие методы вызываются для каждой записи или для каждого поля.

Внимательное изучение поведения пользовательских наборов данных может
натолкнуть на ряд интересных выводов.

Прежде всего, производительность и некоторые особенности поведения
DataSet'ов
все же во многом зависят от реализации
, не смотря на то, что они могут
работать
с одной и той же СУБД и наследовать один и тот же родительский класс. Ярким
практическим примером может послужить сравнение библиотек IBExpress и
FIBPlus.
Более того, различные приемы работы с пользовательскими наборами данных
могут
давать различный практический эффект. Поэтому даже при использовании самых
широко распространенных библиотек нелишне хотя бы бегло ознакомится с их
исходным кодом, в связи с чем, преимущество OpenSource компонентов
проявляется
особенно остро.

Вторым интересным выводом может стать тот факт, что в качестве источника
данных можно использовать практически любой структурированный набор
. Это
может
быть как СУБД, так и, скажем файловая система, XML или даже HTML код.

И, наконец, пользовательские DataSet'ы позволяют не только существенно
облегчить труд прикладного программиста за счет сокращения объема кода,
но и
реализовать ряд дополнительных возможностей. Тем не менее, неграмотное их
использование может повлечь за собою потерю как производительности
конечного
приложения, так и множество других проблем.

Исходники

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии