Автоматизация бизнес процессов

  Банников Н.А. www.stikriz.narod.ru Почта На главную страницу  

Рейтинг@Mail.ru

Продолжение первой части

Как писать DataSet. Часть вторая.

В прошлой статье мы познакомились с архитектурой TDataSet и написали потомка от него в виде электронной таблицы. Теперь, стоит задача связать наш TDataSet  с базой данных. Рассмотрим это на примере InterBase SQL Server и компонентов доступа FIBPlus фирмы Devrice : http://www.fibplus.com.ua/rus/index.shtml. Выбор именно этой библиотеки доступа не случаен. Дело в том, что InterBase фирмы Borland и клоны: Yaffil и FireBird, по видимому, в ближайшем времени будут расходится по архитектуре, возможно, API, но уверенность, что Devrice будет поддерживать все серверы есть, а вот, насчет IBX – нет. К тому же стоимость этих компонентов приемлема для российского разработчика. FIBPlus является более производительной и удобной библиотекой доступа. Вы можете использовать её компоненты совместно с теми, которые мы сейчас рассмотрим. Кроме того, никто не мешает Вам наследовать для другой библиотеки. Например, в качестве бонуса, в исходных текстах, прилагаемых к этой статье, Вы найдете компоненты для доступа через интерфейсы ADO. Это полезно, например, для любителей Delphi 3 и Delphi 4 или тем, кто устал от компонентов ADO в более старших версиях. Еще раз повторюсь, что тексты писались для Delphi 3. Кое-что для портирования в более старшие версии, по крайней мере, до Delphi 5 уже сделано, но не обольщайтесь, что все абсолютно.

Общие принципы.

Посмотрите реализацию методов InternalPost и InternalDelete класса TUnDataSet в модуле UnCustomDataSet.pas. В этих методах есть обращение к FUpdater. FUpdater в потомках – это и есть фетчер, отнаследованный от TUnCustomUpdater. Т.е. когда мы делаем Post, то проверяем, была ли вставка или это было редактирование ( по State датасета ), и выполняем соответственно либо DoInsert, либо DoUpdate фетчера. Аналогично, когда происходит удаление в методе InternalDelete датасета, выполняется метод DoDelete фетчера.

В методе TUnDataSet._CheckPositionCursor проверяется все ли строки были сфетчены из БД. Для этого вызывается метод _CheckNextFech. У TUnDataSet этот метод, практически ничего не делает. Он будет переопределен в потомках. И тогда будет вызван метод GetNextData, если у фетчера EOF <> true, т.е. еще не все данные сфетчены.

Фетчер.

Начнем с модуля UnFetchTable.pas. В этом модуле объявлен абстрактный класс TUnCustomFetcher  для фетча данных с SQL сервера и TunFetchDataSet, который может уже брать данные от фетчера. Надо сказать, что сам TunCustomFetcher является потомком от TUnSQLUpdater. Этот класс является потомком от TunCustomUpdater. Рассмотрим, пока TunCustomUpdater.

TunCustomUpdater (UnCustomDataSet.pas ) содержит абстрактные методы DoInsert, DoUpdate, DoDelete, DoRefresh, DoLock, которые делают соответственно вставку, обновление, удаление, перечитывание только что отредактированной или вставленной строки и блокировку перед редактированием записи. Как это будет реализовано, пока, нам не важно. Главное то, что этот компонент содержит BlobsCollection, в котором будет инкапсулироваться работа с блобами. Пока, он только умеет записывать информацию о блобах датасета в DFM. Абстрактные методы DoGetBlob и DoSetBlob будут реализованы для возвращения данных блоба в Tstream и зписи из Tstream в блоб. Класс TunCustomUpdater нужен нам для того, чтобы создавать фетчеры не только для SQL серверов, но и для любых способов доступа. Например, вы бы могли сделать свой сервер приложений на основе DCOM технологии или как сервис и фетчить данные с них, соответственно, через вариантный массив байтов или по своему протоколу поверх TCPIP.

Как уже говорилось TUnSQLUpdater является потомком от TunCustomUpdater. У этого класса определены свойства RefreshSQL, InsertSQL, UpdateSQL, DeleteSQL, LockSQL типа Tstrings. Здесь уже видно, что все действия с данными в датасете так или иначе выливаются в выполнение SQL запроса. Это очень удобно и одновременно трудоемко. Никто за Вас не будет решать как именно выполнять то или иное действие, поэтому Вы сможете легко редактировать любые запросы по любому количеству таблиц. Более того, во всех этих свойствах предусмотрена возможность выполнения сразу нескольких запросов по порядку. Эти запросы должны быть отделены знаком ^, который должен стоять в отдельной строке. Т.е. после первого запроса должна быть одна строка с единственным символом ^. На следующей строке может начинаться следующий запрос и т.д. В этом же модуле ( UnSQLUpdater.pas ) появился TUnSQLBlobItem – потомок от TunBlobItem, который уже содержит два запроса: GetSQL и SetSQL. Т.е. запрос на получение блоба и запрос на update блоба. Да. Блобы будут запрашиваться с сервера и писаться на сервер отдельным запросом. Это и хорошо и плохо одновременно. Хорошо, потому, что Вы теперь вправе самостоятельно определять какие блобы куда и как писать и откуда и как читать. Вы можете хранить блобы в отдельных таблицах или в нескольких разных таблицах. И никаких проблем с update или select. Однако, это требует от программиста дополнительных телодвижений по составлению этих запросов. Но, я думал не о трудоемкости, а о гибкости компонентов, поэтому думаю, что положительных моментов в таком подходе значительно больше.

TunCustomFetcher – потомок от TUnSQLUpdater инкапсулирует еше несколько абстрактных методов, которые будут рассмотрены позже. TunFetchDataSet уже содержит фетчер, а так же буфер FfechBuffer, который будет использоваться для сфетчивания данных от SQL сервера. Здесь же определены параметры, которые можно использовать в SQL запросах. Единственное, на что следует обратить внимание – это procedure GetNextData(AData: PChar); virtual; abstract; Этот метод возвращает данные одной следующей сфетченой строки, и в TunFetchDataSet уже реализована проверка _CheckNextFech на то, что все ли данные сфетчены.

В этом же модуле (UnFetchTable.pas ) есть один интересный компонент: TunDataLink, который служит для создания связки матер – деталь. Его особенность в том, что деталь обновляется при скролировании мастера не сразу, а с задержкой. Более того, пока мастер скролируется, деталь не обновляется. Это позволяет разгрузить сеть и вообще, ускорить работу пользовательского интерфейса. Однако, будьте осторожны при использовании в программе такой связки. Если Вы делаете что-то во время скролирования мастера в его детали, то лучше не связывать их, а просто переопределять параметры и делать детали Refresh. Посмотрите как он реализован самостоятельно.

Следующий модуль – UnFibClasses.pas. Рассмотрим класс TunCustomFibFetcher – потомок от TunCustomFetcher.

 

procedure TUnCustomFibFetcher.Open;

var I, J: Integer;

    TempName: string;

begin

 if not FQuery.Open then

  begin

   FIsFieldsChecked:=false;  // Нужно проверить все Fields и связать их с Fields FQuery

   if not FQuery.Prepared then

    FQuery.Prepare;

   for I:=0 to FQuery.Params.Count-1 do  // Копируем значения параметров в FQuery

    begin

     TempName:=FQuery.Params[I].Name;

     for J:=0 to FetchDataSet.Params.Count-1 do

      begin

       if CompareText(FQuery.Params[I].Name, FetchDataSet.Params[J].Name) = 0 then

        begin

         FQuery.Params[I].Value:=FetchDataSet.Params[J].Value;

        end;

      end;

    end;

   FQuery.ExecQuery;  // Открываем запрос

  end;

end;

 

Здесь подготавливается и открывается FQuery: TpFIBQuery. Открытие фетчера происходит во время открытия датасета. Закрытие – во время закрытия датасета. В программе открывать и закрывать фетчер не рекомендуется. Следующий метод – метод инициализации списка описаний полей датасета. Он вызывается или при открытии датасета или когда Вы в Delphi нажимаете кнопку Add fields в редакторе полей датасета. Сами поля будут созданы при настоящем открытии датасета в методе InternalOpen.

 

procedure TUnCustomFibFetcher.InitializeFields(ADataSet: TDataSet);

var I: Integer;

    Def: TFieldDef;

    J: Integer;

    Fld: TField;

    IsBoolean: boolean;

begin

 if InitializeFromFieldDefData and (FieldDefData.Size > 0) then

  begin   // Поля датасета инициализируются из TStream.

   FieldDefData.Position:=0;

   LoadFieldDefsFromStream(DataSet.FieldDefs, FieldDefData);

   FIsFieldsChecked:=false;

  end

 else

  begin

   if not FQuery.Prepared then

    FQuery.Prepare; // Подготовили запрос

   for I:=0 to FQuery.Current.Count-1 do

    begin // Пройдемся по всем полям

     // case по типу поля

     case (FQuery.Current[I].Data^.sqltype and not 1) of

       SQL_VARYING, SQL_TEXT: begin // строка

         TFieldDef.Create(ADataSet.FieldDefs, FQuery.Current.Vars[I].Name,

                 ftString, FQuery.Current[I].Data^.sqllen, false, I);

        end;

       SQL_DOUBLE, SQL_D_FLOAT, SQL_FLOAT: begin  // Дробное число

         TFieldDef.Create(ADataSet.FieldDefs, FQuery.Current.Vars[I].Name,

                 ftFloat, 0, false, I);

        end;

       SQL_INT64: begin   // Int64

         if (FQuery.Current.Vars[I].Scale = 4) or (FQuery.Current.Vars[I].Scale = 2) then

          begin

           Def:=TFieldDef.Create(ADataSet.FieldDefs, FQuery.Current.Vars[I].Name,

                 ftCurrency, 0, false, I);

           Def.Precision:=FQuery.Current.Vars[I].Scale;

          end

         else

          begin

           TFieldDef.Create(ADataSet.FieldDefs, FQuery.Current.Vars[I].Name,

                 ftFloat, 0, false, I);

          end;

        end;

       SQL_LONG: begin // Целое

         IsBoolean:=false;

         for J:=0 to DataSet.FieldCount-1 do

          begin

           Fld:=DataSet.Fields[J];

           if CompareText(Fld.FieldName, FQuery.Current.Vars[I].Name) = 0 then

            begin

             IsBoolean:=Fld.DataType = ftBoolean;

             break;

            end

          end;

         if IsBoolean then

          TFieldDef.Create(ADataSet.FieldDefs, FQuery.Current.Vars[I].Name,

                 ftBoolean, 0, false, I)

         else

          TFieldDef.Create(ADataSet.FieldDefs, FQuery.Current.Vars[I].Name,

                 ftInteger, 0, false, I);

        end;

       SQL_SHORT: begin  // Короткое целое

         TFieldDef.Create(ADataSet.FieldDefs, FQuery.Current.Vars[I].Name,

                 ftSmallint, 0, false, I);

        end;

       SQL_TIMESTAMP: begin  // Дата и время

         TFieldDef.Create(ADataSet.FieldDefs, FQuery.Current.Vars[I].Name,

                 ftDateTime, 0, false, I);

        end;

       SQL_BLOB: begin  // Некий блоб

         if (FQuery.Current[I].Data^.sqlsubtype = 0) then

          begin

           TFieldDef.Create(ADataSet.FieldDefs, FQuery.Current.Vars[I].Name,

                 ftMemo, 0, true, I);

          end

         else

          begin

           TFieldDef.Create(ADataSet.FieldDefs, FQuery.Current.Vars[I].Name,

                 ftBlob, 0, true, I);

          end;

        end;

       SQL_TYPE_TIME: begin // Время

         TFieldDef.Create(ADataSet.FieldDefs, FQuery.Current.Vars[I].Name,

                 ftTime, 0, false, I);

        end;

       SQL_TYPE_DATE: begin  // Дата

         TFieldDef.Create(ADataSet.FieldDefs, FQuery.Current.Vars[I].Name,

                 ftDate, 0, false, I);

        end;

       else FIBError(feInvalidDataConversion, [nil]);

     end;

     FIsFieldsChecked:=false;

    end;

 end;

end;

 

Фактически, в этом методе мы просто создаем описание полей датасета. И последний интересный метод – сам фетчь с Ib & Clones:

 

procedure TUnCustomFibFetcher.GetNextData(AData: PChar);

var I: Integer;

    Fld: TField;

    IbField: TFIBXSQLVAR;

    IdFld: Integer;

    IbRecord: TFIBXSQLDA;

begin

 if not FIsFieldsChecked then

  begin // Если поля не определялись

   if FieldFechIndex <> nil then

    FreeMem(FieldFechIndex);

   GetMem(FieldFechIndex, DataSet.FieldCount*Sizeof(Word));

   for I:=0 to DataSet.FieldCount-1 do

    begin

     Fld:=DataSet.Fields[I];

     IdFld:=FQuery.FieldIndex[Fld.FieldName];

     FieldFechIndex[I]:=IdFld;

     if (IdFld > -1) then

      begin

       IbField:=FQuery.Fields[IdFld];

       case IbField.Data^.sqltype and notof

        SQL_VARYING: // varchar

         if Fld.DataType <> ftString then

          FIBError(feInvalidDataConversion, [nil]);

        SQL_TEXT: // char

         if Fld.DataType <> ftString then

          FIBError(feInvalidDataConversion, [nil]);

        SQL_DOUBLE, SQL_FLOAT, SQL_D_FLOAT, SQL_INT64:  // double precition, numeric, decimal

         if not ((Fld.DataType = ftFloat) or (Fld.DataType =  ftCurrency)) then

          FIBError(feInvalidDataConversion, [nil]);

        SQL_LONG:  // integer

         if not ((Fld.DataType = ftInteger) or (Fld.DataType = ftBoolean)) then

          FIBError(feInvalidDataConversion, [nil]);

        SQL_SHORT: // short

         if Fld.DataType <> ftSmallint then

          FIBError(feInvalidDataConversion, [nil]);

        SQL_BLOB:  // blob

         if not Fld.IsBlob then

          FIBError(feInvalidDataConversion, [nil]);

        SQL_TYPE_TIME: // time

         if Fld.DataType <>  ftTime then

          FIBError(feInvalidDataConversion, [nil]);

        SQL_TYPE_DATE:  // date

         if Fld.DataType <> ftDate then

          FIBError(feInvalidDataConversion, [nil]);

        SQL_TIMESTAMP:  // timestamp

         if Fld.DataType <>  ftDateTime then

          FIBError(feInvalidDataConversion, [nil]);

        else FIBError(feInvalidDataConversion, [nil]);

       end;

      end;

    end;

   FIsFieldsChecked:=true;

  end;

 IbRecord:=FQuery.Current;

 for I:=0 to DataSet.FieldCount-1 do

  begin // Пройдемся по полям

   Fld:=DataSet.Fields[I];

   IdFld:=FieldFechIndex[I];

   if (Fld.FieldKind = fkData) and (IdFld > -1) then

    begin

     IbField:=IbRecord.Vars[IdFld];

     // Вызовем процедуру через указатель в списке по индексу - типу поля для реального феча данных

     if IbField <> nil then

      AFechFuncList[IbField.Data^.sqltype](Fld, IbField.Data, AData + FetchDataSet.FieldOffset[Fld.Index]);

    end;

  end;

 FQuery.Next;

end;

 

В этом методе один раз делается проверка на совместимость полей датасета и TpFIBQuery, через который, собственно фетчь и производится, и сам фетчь. А так же заполняется array FieldFechIndex, который служит для быстрого поиска полей датасета. Вызов метода фетча происходит через указатель на процедуру, который берется из array AfechFuncList. Индексом для которого служит тип поля TpFIBQuery. Посмотрите инициализацию модуля – там происходит инициализация AfechFuncList указателями на методы фетча.

Хотелось бы остановится еще на одной проблеме:

 

procedure FechSQL_BLOB(Fld: TField; IfField: PXSQLVAR; Buffer: PChar);       

begin                                                                        

 PUnBlobQuad(Buffer)^.Blob:=-1;                                              

end;                                                                          

 

Вот здесь мы теряем Handle на блоб. По этому Handle можно через API вытащить и сам блоб. Вместо этого, мы записываем туда –1. Это значит, что блоб не сфетчен. В первой версии копонентов этот Handle использовался для получения блоба. Но, я отказался в дальнейшем от такого метода, т.к. мне нужна была совместимость с другими фетчерами. К тому же, это не сказалось заметно на производительности. Блоб теперь вытаскивается отдельным SQL запросом.

Наконец, сам TUnFIBFetcher – потомок от TunCustomFibFetcher, который и будет выбираться в свойство датасета TunFetchDataSet. Fetcher. Здесь интерес представляют только методы:

 

procedure DoGetBlob(Field: TBlobField; var Data: TStream); override;

procedure DoSetBlob(Field: TBlobField; var Data: TStream); override;

 

Их реализацию Вы можете посмотреть самостоятельно – там нет сложностей.

Заключение.

 

Давайте подведем итог. Что же мы получили? Мы получили некий универсальный датасет, который может подключаться к фетчерам, созданным для различных серверов баз данных. А это значит, что для переноса приложения на другую серверную платформу, Вы можете просто поменять фетчеры, не затрагивая датасеты и его поля, а значит, не затрагивая до 90 % кода программы. Все наши датасеты хранят данные в Tstream, который может быть, например, TfileStream, а значит, мы экономим оперативную память компьютера, а значит, мы можем открывать очень большие наборы данных и нормально с ними работать. Это пригодится нам для построения больших отчетов. Кроме того, при проектировании серверов приложений, с большим количеством подключаемых пользователей, хранение данных вне оперативной памяти тоже сильно пригодится. Еще одно свойство таких датасетов – это полная совместимость формата хранения данных TunFetchDataSet и TunMemoryTable. А значит, мы можем с сервера приложений передать либо часть данных, либо весь файл и открыть его на клиенте в TunMemoryTable очень быстро.

 

P.S.

 

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

Если кому-то понравятся компоненты в общем и целом, и он пожелает участвовать в дальнейшей разработке этих компонентов, то обращайтесь Сюда

Если Вы уже скачивали исходные тексты к первой статье, то скачайте их еще раз – там есть изменения в старом коде и дополнительные модули с фетчерами.

 

Благодарности:

 

           Трухин Алексей 

           dundich 

            Дмитрий Коннов 

            NAVIY  

Дмитрий Шумко

 

     Банников Н.А. www.stikriz.narod.ru Почта 2003 г.

Сайт создан в системе uCoz