* При перепечатке материалов ссылка на www.SeoLiga.ru обязательна! RSS



ПРОСТЕЙШИЙ DATASET
31 марта 2009

Есть такое хорошее правило – прежде чем переходить к программированию, четко определить круг задач, которые обязательно должны быть решены. Давайте ему последуем. Я хочу создать потомка TDataSet, работающего с набором данных, содержащим два столбца: целое число и его квадрат. Для простоты этот набор данных будет содержать лишь 20 записей – для чисел от 1 до 20. Borland выдвигает еще одно требование: каждый потомок TDataSet должен поддерживать работу с закладками. Однако я позволю себе пока его проигнорировать – тем более, что и без этого получается практически вполне работоспособный класс. Желающие могут добавить в этот пример поддержку закладок, предварительно ознакомившись с механизмом закладок в следующем разделе.
ПРИМЕЧАНИЕ
О терминологии: в дальнейшем я буду использовать название TDataSet для обозначения программного кода и классов, тогда как под набором данных буду понимать сам набор данных, доступ к которому обеспечивает конкретный потомок TDataSet.
Чтобы создать потомка TDataSet, нужно понять, как TDataSet выполняет доступ к данным. Общедоступный (public) интерфейс полностью реализуется средствами самого TDataSet; на плечи конкретных потомков ложится лишь обеспечение средств коммуникации общедоступных методов с набором данных. В основном, коммуникация осуществляется методами InternalXXX, часть из которых в TDataSet объявлена пустыми, а часть – и вовсе абстрактными.
Эти методы можно разделить по их назначению на три группы: навигация (перемещение) по набору и чтение записи из БД в кэш-буфер (а также запись из кэш-буфера в набор данных), вставка/удаление записей, чтение/изменение значений полей записи, прочие методы (открытие и закрытие TDataSet, закладки, выделение и освобождение памяти).

Логика TDataSet никогда не работает со значениями полей записи прямо в наборе. Вместо этого вызовы GetRecord читают целиком записи, причем каждая из них заносится в свой кэш-буфер, и дальнейшие операции с полями каждой считанной записи выполняются в кэш-буфере. В TDataSet одновременно в памяти присутствуют, в общем случае, несколько буферов (свойство-массив Buffers). Помимо данных, считанных из БД, потомок TDataSet может размещать в буфере произвольную дополнительную служебную информацию.
Прежде, чем переходить к рассмотрению методов, я поясню одно из центральных для TDataSet понятий - понятие курсора.
Стандартный способ работы с БД таков, что целевая запись для таких воздействий, как удаление записи, изменение записи, вставка новой записи перед данной и т.п. не указывается явно при вызове соответствующей операции, а определяется предыдущими операциями. Соответственно, для хранения описания того места в наборе данных, которое будет являться целевым для следующей операции, необходимо ввести некую сущность – курсор.
Курсор - это произвольная информация, придающая смысл в каждый момент времени таким понятиям, как “текущая”, “следующая” и (для двунаправленных наборов данных) “предыдущая” запись. Позволю себе вольность пользоваться в отношении курсора фразой “перемещение курсора к некоторой записи”, подразумевая изменение составляющей курсор информации так, чтобы курсор указывал на эту запись.
Тот курсор, который определяет целевую позицию для действий, запрашиваемых у TDataSet сторонним кодом, я в дальнейшем буду называть логическим курсором. С логическим курсором в TDataSet связан буфер активной записи (свойство ActiveBuffer хранит адрес этого буфера).
Логический курсор никак не учитывает структуру набора данных, а потому сам по себе в общем случае недостаточен для эффективного указания на запись реального набора. Например, набор может представлять собой дерево, а коду TDataSet потомок, обеспечивающий работу с этим деревом, представляет его как последовательность вершин дерева, получающуюся при его обходе по некоторому правилу. В таком случае самое большее, что можно выяснить, зная значение логического курсора – это номер, под которым встретится данная вершина при таком обходе. Согласитесь, что не слишком удобно основывать работу с деревом на такой информации, ведь ее придется все время пересчитывать, например, в путь от корня дерева к данной вершине.
Поэтому потомкам TDataSet предлагается пользоваться своим собственным курсором, осведомленным о структуре набора. Назовем этот курсор физическим курсором. В простейшем случае, когда набор данных является простым массивом, на роль физического курсора может вполне сгодиться индекс записи в массиве.
ПРИМЕЧАНИЕ
В дальнейшем под курсором следует понимать логический курсор, если не оговорено обратное. Запись, на которую указывает логический курсор, я буду называть текущей записью.
Для поддержания согласованности логического курсора с физическим используется следующая техника: после вызова метода, от которого ожидается перемещение физического курсора на непредсказуемое расстояние от его текущего положение (например, при переходе на первую запись или поиске по закладке), вызывается метод Resync, считывающий в кэш-буферы запись, на которую теперь указывает физический курсор, а также несколько окружающих ее записей. Если же перемещение предсказуемо (как в случае метода MoveBy, вызываемого из Prior и Next), то читается лишь часть окружающих записей. Понятно, что в случае, когда вообще не ожидается никаких перемещений, никакие записи и не перечитываются.
Если потомку требуется переместить физический курсор вопреки ожиданиям TDataSet, необходимо оповестить его об этом вызовомCursorPosChanged. Такое оповещение обрабатывается не сразу, и, говоря откровенно, меня до сих пор гложут сомнения, не надежнее ли сразу вызывать Resync (хотя в таком случае можно проиграть в скорости, если после этого Resync будет вызван повторно).
Таким образом, в момент вызова методов InternalXXX логический курсор совпадает с физическим (от возможных сбоев этого соответствия в ходе выполнения закрытым кодом каких-либо внутренних операций спасает выполняющийся перед входом в InternalXXX вызов методаUpdateCursorPos), а после выхода из InternalXXX логический курсор опять-таки приводится в соответствие физическому.
UpdateCursorPos работает с буфером текущей записи: для помещения физического курсора на ту же запись, на которую указывает логический, он вызывает InternalSetToRecord, указав в качестве аргумента (буфера записи, к которой нужно перейти) буфер текущей записи (ActiveBuffer).
Единственное обнаруженное мной исключение из этого правила – метод InternalInsert (вызывающийся при вставке записи), перед вызовом которого не выполняется UpdateCursorPos, из-за чего приходится искать способ переместить физический курсор на ту же запись, на которую указывает логический, при этом не пользуясь private-членами TDataSet). Подробнее об этом – в следующем разделе.
При открытии (инициализации) логического курсора (TDataSet.OpenCursor, вызывается при TDataSet.Active:=true и по выходу из режима дизайнера) вызывается метод InternalOpen. Он должен сформировать список определений полей FieldDefs и, если свойство DefaultFields равно True, создать на их базе список полей Fields. Он также должен связать поля из списка Fields с полями набора данных и установить физический курсор в позицию перед первой записью. Кроме того, если до начала манипулирования содержащимися в наборе данными нужно предпринять какие-либо действия (например, установить соединение с удаленным сервером), их также следует выполнить в этом методе.
InternalOpen может вызываться не только из OpenCursor. Запрос на формирование списка FieldDefs может поступить и отдельно: в этом случае вызывается метод InitFieldDefs, который может вызывать InternalInitFieldDefs (последний должен инициализировать FieldDefs).
Для проверки того, что логический курсор открыт (т.е. что набор готов предоставить доступ к своим данным), вызывается функция IsCursorOpen, возвращающая True или False. Ясно, что до выполнения Open она должна возвращать False, а после Open и до последующего Close – True.
Для закрытия набора данных (TDataSet.Close) вызывается метод InternalClose, назначение которого симметрично InternalOpen: если DefaultFields установлено в True, то уничтожить список полей Fields (в этом случае они были созданы самим потомком в ходе выполненияInternalOpen); также, в случае необходимости, выполнить такие действия, как разрыв соединения с сервером БД.
Для навигации (перемещения логического курсора) по набору применяются общедоступные методы First, Prior, Next и Last.
Метод First вызывает метод InternalFirst, который должен установить физический курсор на позицию перед первой записью в наборе. После этого First выполняет серию вызовов GetNextRecord (а те, в конечном счете, GetRecord) для чтения в кэш-буферы следующей (т.е. первой) записи набора и нескольких последующих записей. Затем при помощи вызова DataEvent генерируется уведомление об изменении данных в кэш-буферах.
Зачем нужно устанавливать курсор на позицию перед первой записью, а потом читать следующую запись? Почему бы просто не поместить курсор сразу на первую запись? Дело, возможно, в том, что если для позиционирования на первую запись необходимо переоткрытие набора (например, в однонаправленных наборах), то эта обязанность возлагается на InternalFirst. То есть для помещения курсора на первую запись InternalFirst должен был бы переоткрыть набор и выполнить переход к следующей записи. По-видимому, команда разработчиков Borland решила, что будет лучше, если все команды навигации по записям будут подаваться из находящегося в базовом классе TDataSet слоя абстрактной логики, а не из методов, отвечающих за конкретную реализацию доступа к данным (таких, какInternalFirst). Таким образом, работа IntrnalFirst должна в такой ситуации завершаться переоткрытием (а после этой операции курсор, как уже говорилось, находится перед первой записью).
Аналогично, InternalLast должен устанавливать физический курсор в позицию, находящуюся за последней записью БД (это уже, видимо, просто для единообразия).
Методы Prior и Next апеллируют к одному и тому же методу GetRecord. Последний многофункционален: он должен проверить, не выйдет ли позиция курсора при выполнении операции за пределы БД, изменить позицию физического курсора, считать из набора в буфер записи, переданный в качестве аргумента, данные записи, ставшей текущей, и (в зависимости от параметров вызова) в случае ошибки возбудить исключение.
Когда необходимо прочитать значение некоторого поля текущей записи, вызывается GetFieldData, в который передается ссылка на соответствующий нужному полю объект TField и адрес буфера, содержащего запись. GetFieldData – перегруженный метод, но фактическое извлечение значения поля из буфера записи в так называемом Native-формате (его мы рассмотрим далее) осуществляет лишь одна из его версий (остальные версии в конечном счете просто вызывают именно ее).
Кстати, существует и еще один способ позиционирования физического курсора: метод InternalSetToRecord предназначен для установки курсора на запись, содержимое которой находится в переданном в качестве аргумента буфере (ранее оно было считано в этот буфер методом GetRecord).
Для выделения памяти под новый буфер вызывается метод AllocRecordBuffer, для освобождения занятой под буфер памяти –FreeRecordBuffer. Надо заметить, что TDataSet может повторно использовать уже выделенный однажды буфер для хранения другой записи. Обработку вытеснения записи из буфера при таком повторном использовании можно поместить туда же, куда и действия по подготовке буфера перед помещением туда записи – в InitRecordBuffer. Этот метод вызывается перед каждым помещением в буфер какой-либо записи.
Редактирование данных, в т.ч. добавление и удаление записей, становится возможно, когда метод GetCanModify возвращает True. Процессы, происходящие при редактировании, будут рассмотрены в следующем разделе, поэтому пока мы считаем, что GetCanModifyвсегда возвращает False.
Метод InternalHandleException служит для обработки возможных исключений, возникающих при чтении объекта из потока. Насколько мне удалось понять из исходных текстов потомков TDataSet, предлагаемых Borland, хорошим тоном является такая его реализация: Application.HandleException(Self).
Метод IsSequenced должен возвращать True, если в наборе каждую запись можно однозначно сопоставить с ее порядковым номером (т.е. числом, показывающим, какой по счету окажется запись при последовательном переборе всех записей, начиная с первой).
Помимо этих методов, в работе с данными может участвовать еще несколько семейств методов, предназначенных для расширения возможностей TDataSet. Одно из них - работа с закладками - как я уже говорил, будет рассмотрено в следующем разделе. Поддержка остальных не является обязательной (см. также перечень не рассматриваемых здесь функций в конце раздела “Область применения”). Однако три из них стоит реализовать, иначе довольно некрасиво смотрится полоса прокрутки в DBGrid и подобных ему элементах управления, а клиенты теряют возможность простого позиционирования на запись по ее номеру.
 GetRecordCount должен возвращать общее число содержащихся в наборе записей.
 GetRecNo должен возвращать номер (начиная с 1) текущей записи.
 SetRecNo должен устанавливать физический и логический курсоры на запись с указанным номером.
Последние два метода фактически являются реализацией свойства RecNo.
Два слова о Native-формате данных: детально мы его обсудим в следующем разделе, пока же – основное. Извлеченные из записи данные следует возвращать в особом формате (Native), своем для каждого типа поля. В нашем примере используются только поля типа ftInteger, для которых Native-формат представляет собой 4-байтовое целое число со знаком, т.е. значение типа integer. (см. чтение в методеGetFieldData).
Листинг 1 TMyDataSet (read-only)
unit DataSet0;
//Простой Read-Only BiDirectional DataSet в виде таблицы
// квадратов целых чисел (два поля – число и его квадрат)
interface
Uses DB,Classes {для TComponent};

Type
TMyDataSet=class(TDataset)
protected
procedure InternalHandleException; override;
// Инициализация/деинициализация курсора
procedure InternalInitFieldDefs; override;
procedure InternalOpen; override;
function IsCursorOpen: Boolean; override;
procedure InternalClose; override;
// Выборка
function GetRecord(Buffer: PChar; GetMode: TGetMode; DoCheck: Boolean):
TGetResult; override;
function AllocRecordBuffer: PChar; override;
procedure FreeRecordBuffer(var Buffer: PChar); override;
procedure InternalInitRecord(Buffer: PChar); override;
// Навигация
procedure InternalFirst; override;
procedure InternalLast; override;
procedure InternalSetToRecord(Buffer: PChar); override;
// Разрешение доступа
function GetCanModify: Boolean; override;
// Необязательные методы
function GetRecordCount: Integer; override;
//Везде в данном коде, где используется RecordCount, можно было бы просто
//написать List.Count. Реализовывать GetRecordCount не обязательно.
procedure SetRecNo(Value: Integer); override;
function GetRecNo: Integer; override;
//Здесь номер записи считается от 1
public
// Чтение поля
function GetFieldData(Field: TField; Buffer: Pointer): Boolean; override;
// Создание/уничтожение
constructor Create(AOwner:TComponent); override;
private
FIsOpen:boolean;
FCursor:integer;
end;

TRecordData=record //Собственно копия записи – по замыслу, она должна считываться из БД
Number, Square:integer;
end;

TRecordBuffer=record //Формат буфера, хранящего запись: данные записи +
//служебная информация
RecordData:TRecordData;
RecordIndex:integer; //Номер записи, считая от 1
end;
PRecordBuffer=^TRecordBuffer;

implementation

procedure TMyDataSet.InternalHandleException;
begin Application.HandleException(Self) end;

procedure TMyDataSet.InternalInitFieldDefs;
begin
FieldDefs.Clear;
with FieldDefs.AddFieldDef do
begin
DataType:=ftInteger;
FieldNo:=1;
Name:='Число';
end;
with FieldDefs.AddFieldDef do
begin
DataType:=ftInteger;
FieldNo:=2;
Name:='Квадрат';
end;
end;

procedure TMyDataSet.InternalOpen;
begin
InternalInitFieldDefs;
if DefaultFields then CreateFields;
BindFields(true); //Привязываем поля к БД
FIsOpen:=true;
FCursor:=0;
end;

function TMyDataSet.IsCursorOpen: Boolean;
begin result:=FIsOpen end;

procedure TMyDataSet.InternalClose;
begin
BindFields (False); //Отвязываем поля
if DefaultFields then DestroyFields;
FIsOpen:=false;
end;

procedure TMyDataSet.InternalFirst;
begin FCursor:=0 end;

procedure TMyDataSet.InternalLast;
begin FCursor:=RecordCount+1 end;

function TMyDataSet.GetRecord(Buffer: PChar; GetMode: TGetMode;
DoCheck: Boolean): TGetResult;
begin
result:=grOK;
Case GetMode of
gmPrior:if FCursor<=1 then result:=grBOF else Dec(FCursor);
gmNext:if FCursor>=RecordCount then result:=grEOF else Inc(FCursor);
gmCurrent:if (FCursor < 1) or (FCursor > RecordCount) then Result := grError;
end;
if result=grOK then
with PRecordBuffer(Buffer)^ do
begin
RecordData.Number:=FCursor;
RecordData.Square:=sqr(RecordData.Number);
RecordIndex:=FCursor;
end;
if (result=grError) and DoCheck then DatabaseError('Error in GetRecord()');
end;

function TMyDataSet.GetRecordCount: Integer;
begin Result:=20 end;

procedure TMyDataSet.SetRecNo(Value: Integer);
begin if (Value<1) or (Value>=RecordCount+1) then exit; FCursor:=Value; Resync([]) end;
//Мне не пришло в голову более умного средства синхронизации логического
// курсора с физическим, чем выполнение Resync (ресинхронизация – сброс
// логического курсора и последующее чтение записей, окружающих
// физический курсор)

function TMyDataSet.GetRecNo: Integer;
//Возвращает номер записи, соответствующей активному в данный момент буферу
begin Result:=PRecordBuffer(ActiveBuffer)^.RecordIndex end;

procedure TMyDataSet.InternalSetToRecord(Buffer: PChar);
begin FCursor:=PRecordBuffer(Buffer)^.RecordIndex end;

function TMyDataSet.AllocRecordBuffer: PChar;
begin GetMem(result,sizeof(TRecordBuffer)) end;

procedure TMyDataSet.FreeRecordBuffer(var Buffer: PChar);
begin FreeMem(Buffer,sizeof(TRecordBuffer)) end;

procedure TMyDataSet.InternalInitRecord(Buffer: PChar);
begin
inherited; //Нам не нужна какая-то особая инициализация буфера записи
// до чтения туда фактической записи
end;

function TMyDataSet.GetCanModify: Boolean;
begin result:=false end;

function TMyDataSet.GetFieldData(Field: TField; Buffer: Pointer): Boolean;
begin
result:=true;
case Field.FieldNo of
1:PInteger(Buffer)^:=PRecordBuffer(ActiveBuffer)^.RecordData.Number;
2:PInteger(Buffer)^:=PRecordBuffer(ActiveBuffer)^.RecordData.Square;
else result:=false;
end;
end;

constructor TMyDataSet.Create(AOwner: TComponent);
begin
inherited;
FIsOpen:=false;
end;

end.
Для проверки работы класса TMyDataSet можно использовать следующий тестовый проект:
Листинг 2
program Dataset;
uses Forms, DB, DBGrids, DBCtrls, Controls, DataSet0;
Var F:TForm;
DS:TDataSource;
MyDataSet:TMyDataSet;
begin
Application.Initialize;
Application.CreateForm(TForm, F);
F.Width:=300;F.Height:=300;F.Position:=poDesktopCenter;
MyDataSet:=TMyDataSet.Create(F);
MyDataSet.Open;
DS:=TDataSource.Create(F);
DS.DataSet:=MyDataSet;
with TDBNavigator.Create(F) do
begin Parent:=F; Align:=alTop; DataSource:=DS end;
with TDBGrid.Create(F) do
begin Parent:=F; Align:=alClient; DataSource:=DS end;
Application.Run;
end.


Теги: программирование роботов, программирование shell Borland Delphi

Статьи по теме:

Форматирование вывода
Проектирование структуры реляционного хранилища данных
GetIndexNames
Панель Standard
Метод ResetPageFooterSize
PackTable
Гипертекстовые и мультимедийные информационные технологии
Свойство DataSet для TQRDBText
OnCopyDateTimeAsString
Расширение интерпретатора выражений
Перекрытый ввод-вывод
Панель HTML Design
Методо-ориентированные ППП
CancelRange
Редактор кода
| Borland Delphi | vitek |
 


Пн Вт Ср Чт Пт Сб Вс
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30


     



Rambler's Top100

Данный сайт или домен продается ICQ: 403-353-727

© 2009 Seoliga.ru | Borland Delphi | ПРОСТЕЙШИЙ DATASET. Регион сайта: Москва и Санкт-Петербург