Версия для печати

Devrace FIBPlus: Оптимизация сетевого трафика в приложениях на Delphi и C++ Builder

Оцените материал
(2 голосов)

Devrace FIBPlus: Оптимизация сетевого трафика в приложениях на Delphi и C++ Builder

В данной статье мы постараемся дать несколько рекомендаций и примеров, которые позволят разработчикам создавать более эффективные приложения для InterBase и Firebird. Эти технологии оптимизации сетевого трафика могут пригодиться для приложений, работающих в многопользовательской среде, а также для систем, которые обращаются к серверу по низкоскоростным каналам (например, ISDN).

Перед началом статьи рекомендую выбрать Офисные кресла серии Самурай

Кэширование метаданных

FIBPlus предоставляет нам возможность автоматически получать системную информацию о полях таблиц, самостоятельно настраивая такие свойства полей в TpFIBDataSet как Required (для NOT NULL полей), ReadOnly (для вычислимых полей) и DefaultExpression (для полей, у которых в базе данных задано значение по умолчанию). Это удобно и для программиста, и для пользователя, поскольку первому не нужно заботиться о ручной настройке указанных свойств во время разработки клиентского приложения, а второй получает более осмысленные сообщения при работе с программой. Например, если какое-то поле описано в базе данных как NOT NULL, то при попытке оставить его пустым пользователь получит сообщение «Field ‘…’ must have a value.», что гораздо проще для понимания, чем системная ошибка InterBase/Firebird о нарушении PRIMARY KEY. То же самое касается вычислимых полей – очевидно, что такие поля нельзя редактировать. FIBPlus автоматически задаст у таких полей свойство ReadOnly равное True, и пользователь не будет мучаться из-за непонятных ошибок при попытке исправить значения этих полей в TDBGrid.

Однако данная возможность FIBPlus имеет и свой недостаток, который очевидным образом проявляется при работе с низкоскоростными каналами связи. Чтобы получить информацию о полях компоненты FIBPlus выполняют дополнительные «невидимые» запросы, обращаясь к системным таблицам InterBase/Firebird. Разумеется, при большом количестве таблиц в приложении, а также  при большом количестве полей в этих таблицах, работа приложения может замедлиться, а трафик возрасти. Особенно это видно на стадии первоначальных открытий запросов – каждый из них сопровождается серией дополнительных. Потом, в процессе работы программы, при повторных запросах, FIBPlus использует уже полученную ранее информацию, однако при старте приложения  можно обратить внимание на некоторое замедление работы.

Кажется, что у программиста есть только два варианта решения этой проблемы – либо терпеть задержки при первоначальном открытии запросов, либо отказаться от приятной функции компонентов FIBPlus, которые самостоятельно настроят все поля таблиц. На самом деле, мы можем использовать такую возможность FIBPlus, как кэширование метаданных на клиентском компьютере.

Продемонстрируем все сказанное простым примером. Создайте новое приложение и поместите на него следующие компоненты:

pFIBDatabase1: TpFIBDatabase; 
pFIBTransaction1: TpFIBTransaction; 
pFIBDataSet1: TpFIBDataSet; 
DBGrid1: TDBGrid; 
DataSource1: TDataSource; 
FIBSQLMonitor1: TFIBSQLMonitor; 
Label1: TLabel; 
Label2: TLabel; 
Label3: TLabel; 
Memo1: TMemo; 
Button1: TButton; 
ListView1: TListView;

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

pFIBDatabase1.DefaultTransaction := pFIBTransaction1; 
pFIBTransaction1.DefaultDatabase := pFIBDatabase1; 
pFIBDataSet1.Database := pFIBDatabase1; 
pFIBDataSet1.Transaction := pFIBTransaction1; 
pFIBDataSet1.AutoCommit := True; 
DBGrid1.DataSource := DataSource1; 
DataSource1.DataSet := pFIBDataSet1;

Укажите параметры подключения к базе данных:

[Image]

И сформируйте запросы, необходимые для работы FIBDataSet1. Для этого вызовите SQL Generator, нажав правую кнопку мыши на pFIBDataSet1:

[Image]

[Image]

Выберите в списке таблиц EMPLOYEE и «перетащите» это название в редактор слева. Нажмите кнопку “Save SQL” и перейдите к закладке “Options”:

[Image]

В списке «Select main table:» укажите таблицу EMPLOYEE (она должна быть одна, поскольку мы не использовали объединение таблиц в запросе), нажмите кнопку “Get table fields”. SQL Generator сформирует два списка полей. Остается нажать «Generate SQLs», “Save all”, и компонент готов к работе. Напишем обработчики событий OnFormCreate и OnSQL (для компонента TFIBSQLMonitor):

Теперь обратим внимание на компонент ListView1. У него необходимо добавить 4-е столбца: Name, Not Null, Computed и Default. Они понадобятся нам, чтобы проверять, установлены ли свойства полей pFIBDataset1 надлежащим образом. Для этого напишем следующий обработчик события AfterOpen у pFIBDataset1:

procedure TForm1.FIBSQLMonitor1SQL(EventText: String;
EventTime: TDateTime);
begin
 Memo1.Lines.Text := Memo1.Lines.Text + EventText;
end;
 
 
procedure TForm1.FormCreate(Sender: TObject);
begin
 pFIBDatabase1.Connected := True;
 pFIBDataset1.Active := True;
end;
 
 
procedure
TForm1.pFIBDataSet1AfterOpen(DataSet: TDataSet);
var FieldInfo: TListItem;
      Index: Integer;
begin
     with pFIBDataSet1 do begin
         for Index := 0 to pred(FieldCount) do begin
              FieldInfo := ListView1.Items.Add;
              FieldInfo.Caption := Fields[Index].FieldName;
            if Fields[Index].Required then FieldInfo.SubItems.Add('+')
            else FieldInfo.SubItems.Add('-');
            if Fields[Index].ReadOnly then FieldInfo.SubItems.Add('+')
            else FieldInfo.SubItems.Add('-');
            if Fields[Index].DefaultExpression <> '' then
            FieldInfo.SubItems.Add(Fields[Index].DefaultExpression);
        end;
    end;
end;

Итак, общий вид нашего приложения должен стать таким:

[Image]

Запустите приложение и обратите внимание на содержимое ListView1:

[Image]

Из рисунка видно, что pFIBDataSet1 получил все свойства полей таблицы вплоть до значений по умолчанию. Это было достигнуто при помощи серии дополнительных запросов, которые были перехвачены FIBSQLMonitor, например:

[Application: metadata_cache]: [Execute]

SELECT R.RDB$FIELD_NAME , R.RDB$FIELD_SOURCE , F.RDB$COMPUTED_BLR , R.RDB$DEFAULT_SOURCE DS ,
 F.RDB$DEFAULT_SOURCE DS1 , F.RDB$FIELD_TYPE , F.RDB$DIMENSIONS FROM RDB$RELATION_FIELDS R 
 JOIN RDB$FIELDS F ON (R.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME ) WHERE R.RDB$RELATION_NAME = :TN
 ORDER BY R.RDB$FIELD_POSITION

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

Перейдем к основному вопросу – кэшированию метаданных на клиенте. Задайте следующие значения свойства CacheSchemeOptions у pFIBDatabase1:

[Image]

Теперь pFIBDatabase1 будет сохранять все полученные метаданные (в наше случае, это свойства полей – NOT NULL, COMPUTED и т.д.) во внешнем файле «metadata_cache.fpc» при закрытии приложения и загружать их из того же файла при запуске приложения. Таким образом, при повторном запуске приложения отпадет нужда в дополнительных запросах. Есть также еще один момент, касающийся работы со схемой кэширования метаданных: pFIBDatabase1 будет пытаться проверить актуальность текущих метаданных. Для этого он снова делает запрос к системным таблицам, в котором проверяет, не изменился ли системный идентификатор таблицы, что указывает на обновление структуры. Если обнаружено, что какая-то структура таблицы на сервере была изменена, то данные о ней перечитываются. Разумеется, это не затрагивает остальных таблиц.

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

procedure TForm1.pFIBDatabase1AcceptCacheSchema(const ObjName: String;
var Accept: Boolean);
begin
    Accept := True;
end;

Этот обработчик вызывается для метаданных каждого объекта, сохраненного в файле. Обратите внимание, что если реальные метаданные отличаются от тех, которые кэшируются нашим приложением, то в работе приложения могут появиться трудноуловимые ошибки. Запустите наше приложение. При первом запуске мы не заметим никаких изменений – приложение выполнит все запросы, которые выполнялись и ранее. Однако при повторном запуске программы мы обнаружим, что в Memo1 больше нет запросов к RDB$RELATION_FIELDS. Фактически, приложение выполнит только один запрос:

[Application: metadata_cache]

pFIBDataSet1: [Execute]

SELECT
EMP.EMP_NO,
EMP.FIRST_NAME,
EMP.LAST_NAME,
EMP.PHONE_EXT,
EMP.HIRE_DATE,
EMP.DEPT_NO, 
EMP.JOB_CODE,
EMP.JOB_GRADE,
EMP.JOB_COUNTRY,
EMP.SALARY,
EMP.FULL_NAME
FROM
EMPLOYEE EMP

Execute tick count 0
Тем не менее, несмотря на отсутствие системных запросов, все свойства полей в pFIBDataset1 (как видно в ListView1) будут правильными, что и требовалось получить. Мы смогли сократить лишние запросы, но в тоже время не отказались от приятных особенностей, заложенных в FIBPlus.

Динамические модифицирующие запросы

Прежде, чем переходить к этой части статьи, поясним, как работает модификация данных пользователем при помощи TpFIBDataSet. Запрос, который используется для получения данных, указывается в свойстве SelectSQL. При вставке записи (а точнее, когда после метода Append/Insert вызывается метод Post) выполняется запрос, указанный в InsertSQL, в который вместо параметров подставляются реальные значения полей, введенные пользователем (или сгенерированные программой). То же самое происходит при изменении записи – после вызова Post, TpFIBDataSet выполняет запрос из свойства UpdateSQL, подставив туда значения из полей записи. Чтобы увидеть это воочию, напишем небольшой пример. Сначала создадим следующую таблицу и генератор для первичного ключа:

CREATE TABLE "Simple Table" (
    "Id" INTEGER NOT NULL,
    "First Name" VARCHAR (100),
    "Last Name" VARCHAR (100),
    "Address" VARCHAR (100));
 
ALTER TABLE "Simple Table" ADD CONSTRAINT "PK_Simple Table" PRIMARY KEY ("Id");
 
CREATE GENERATOR “Simple Table_Id_GEN”;

Создадим новое приложение. В нем нужно разместить те же самые компоненты, что и в предыдущем, но без TListView (нам он теперь не нужен). При помощи SQL Generator сформируем запрос для SelectSQL:

SELECT
    Sim."Id",
    Sim."First Name",
    Sim."Last Name",
    Sim."Address"
FROM
    "Simple Table" Sim

А затем сгенерируем модифицирующие запросы. Весь процесс был описан в предыдущем примере. Например, для UpdateSQL будет получен следующий запрос:

UPDATE "Simple Table" SET 
    "Id" = ?"Id",
    "First Name" = ?"First Name",
    "Last Name" = ?"Last Name",
    "Address" = ?"Address"
 WHERE     
            "Id" = ?"OLD_Id"

Теперь необходимо настроить автогенерацию значений первичного ключа для поля “Id”. Воспользуемся свойством AutoUpdateOptions:

[Image]

В данном случае важны свойства GeneratorName (имя генератора), KeyFields (название ключевого поля) и WhenGetGenID (опция получения значения генератора). Свойство WhenGetGenID мы выставили равным wgOnNewRecord, чтобы pFIBDataSet1 получал новое значение генератора сразу при вставке записи. Запустите приложение и добавьте пару записей в нашу таблицу:

[Image]

Если теперь посмотреть в Memo1, то можно увидеть, как именно вставлялись записи:

[Application: update_only_modified]
pFIBDataSet1: [Execute]

INSERT INTO "Simple Table"(
    "Id",
    "First_Name",
    "Last_Name",
    "Address"
)
VALUES(
    ?"Id",
    ?"First_Name",
    ?"Last_Name",
    ?"Address"
)
 
  Id = 1
  First_Name = 'Name 1'
  Last_Name = 'Last Name 1'
  Address = 'Address 1'

Rows Affected:  1
Execute tick count 0

После каждой вставки пользователем pFIBDataSet1 выполнял запрос из InsertSQL, причем подставлял вместо параметров те значения, которые были набраны. Попробуйте исправить, например, запись с Id = 2. SQL Monitor перехватит следующий запрос:

pFIBDataSet1: [Execute]

UPDATE "Simple Table" SET 
    "Id" = ?"Id",
    "First_Name" = ?"First_Name",
    "Last_Name" = ?"Last_Name",
    "Address" = ?"Address"
 WHERE     
            "Id" = ?"OLD_Id"
    
  Id = 2
  First_Name = 'Name 2 - Changed'
  Last_Name = 'Last Name 2'
  Address = 'Address 2'
  OLD_Id = 2

Rows Affected:  1
Execute tick count 0

Видно, что pFIBDataSet1 отправляет на сервер все поля записи, несмотря на то, что реально было изменено только поле First_Name. Легко представить, что при работе в многопользовательской среде и таблица с большим количеством полей (особенно строковых) такой подход порождает значительный лишний сетевой трафик. Данный недостаток можно исправить, если воспользоваться генератором запросов, встроенным в TpFIBDataSet. Для этого необходимо добавить еще некоторые опции в AutoUpdateOptions: во-первых, задайте AutoReWriteSQLs и CanChangeSQLs в True, свойство UpdateTableName укажите равным "Simple Table", а свойство UpdateOnlyModifiedFields - True. Данные опции позволят pFIBDataSet1 генерировать модифицирующие запросы каждый раз после изменения записи, причем, в данный запрос будут добавляться только те поля, значения которых реально были изменены. Запустите приложение и попробуйте исправить поле “First_Name” у записи с Id = 3. В Memo1 вы увидите запрос, который был выполнен после такого изменения:

pFIBDataSet1: [Execute]

Update "Simple Table" Set
     "First_Name"=?"NEW_First_Name"
where "Simple Table"."Id"=?"OLD_Id"
 
  NEW_First_Name = 'Name 3 - Changed'
  OLD_Id = 3

Rows Affected:  1
Execute tick count 0

Экономия сетевого трафика при таком подходе очевидна.

Использование опции poRefreshAfterPost в свойстве Options у TpFIBDataSet

У компонента TpFIBDataset есть специальное свойство RefreshSQL, которое предназначено для обновления только что измененной записи. Представьте себе ситуацию, когда на нашу таблицу из примера выше наложен триггер AFTER UPDATE, изменяющий поле Last Name. Когда пользователь редактирует запись и pFIBDataSet1 выполняет соответствующий UpdateSQL, триггер также правит запись. После этого TpFIBDataset выполняет запрос из RefreshSQL, который возвращает только одну запись – текущую. Например, если посмотреть запрос, который был сгенерирован при помощи SQL Generator, то RefreshSQL выглядит следующим образом:

SELECT'
    Sim."Id",
    Sim."First_Name",
    Sim."Last_Name",
    Sim."Address"
FROM
    "Simple Table" Sim
WHERE 
    (    
            Sim."Id" = ?"OLD_Id"
    )

Очевидно, что после выполнения этого запроса, мы «увидим» изменения, сделанные триггером. Однако также очевидно, что выполнение данного запроса после любого исправления записи создает дополнительные сетевой трафик. Если вы точно уверены, что у таблицы нет триггеров, которые меняют значения полей, или вероятность одновременной правки записи несколькими пользователями в вашей программе минимальна, то вы можете отключить данный запрос, убрав ключ poRefreshAfterPost из pFIBDataSet1.Options. В этом случае запрос RefreshSQL не будет выполняться без непосредственного вызова метода pFIBDataSet1.Refresh в программе.
Несомненно, при работе с таблицами, имеющими большое количество полей, отключение RefreshSQL может сильно сократить трафик и увеличить скорость работы приложения на низкоскоростных каналах связи.

Повторное использование запросов

Для того чтобы подготовить запрос к выполнению, все клиентские библиотеки, включая FIBPlus, передают на сервер полный текст запроса. Для выполнения же подготовленного запроса достаточно передавать только Handle и значения параметров. Если в вашей программе часто используются одинаковые запросы, то вы вполне можете организовать их повторное использование при помощи методов:

function  GetQueryForUse (aTransaction: TFIBTransaction; const SQLText: string): TpFIBQuery;
procedure FreeQueryForUse (aFIBQuery: TpFIBQuery);

Вам не придется создавать экземпляры TpFIBQuery - процедура GetQueryForUse самостоятельно создаст его при первом вызове, а потом будет возвращать ссылку на существующий компонент, если вы будете выполнять этот же запрос снова и снова. Очевидно, что при каждом последующем вызове будет использоваться уже подготовленный к выполнению запрос, а значит передача текста запроса на сервер будет выполнена только один раз. После получения результатов запроса из компонента TpFIBQuery необходимо вызвать метод FreeQueryForUse. Данный механизм уже используется для внутренних целей в компонентах FIBPlus, например, при вызове генераторов для получения значений первичных ключей. Вы также можете воспользоваться этими методами в своих приложениях для оптимизации трафика.

Клиентские BLOB-фильтры. «Прозрачная» упаковка BLOB-полей.

Наверняка многие из читателей знают о существовании в InterBase/Firebird такой технологии, как blob-фильтры. Это пользовательские функции, которые позволяют обрабатывать (кодировать/декодировать, упаковывать и т.д.) blob-поля на стороне сервера прозрачно для пользовательского приложения. Это может оказаться полезным, чтобы архивировать blob-поля в базе данных, причем менять для этого клиентскую программу не нужно. Однако данный подход никак не поможет нам снизить наш сетевой трафик, поскольку обмен между сервером и приложением все равно будет производиться с распакованными полями.

В FIBPlus (спасибо, Ivan Ravin!) реализован механизм клиентских blob-фильтров очень похожий по своей сути на механизм, встроенный в InterBase/Firebird. Несомненным преимуществом локального blob-фильтра является то, что мы можем значительно снизить сетевой трафик приложения, если будем упаковывать blob-поля перед отправкой на сервер и распаковывать их при получении. Причем это делается централизованно за счет регистрации двух процедур для чтения и записи blob-поля в TpFIBDatabase. В результате FIBPlus будет автоматически использовать данные процедуры для обработки всех blob-полей заданного типа во всех TpFIBDataSet, использующих один экземпляр TpFIBDatabase. Рассмотрим данный механизм на конкретном примере.

Сначала создадим таблицу c blob-полями и триггер для генерации уникальных значений первичного ключа:

CREATE TABLE "BlobTable" (
    "Id" INTEGER NOT NULL,
    "BlobText" BLOB sub_type -15 segment size 1);
 
ALTER TABLE "BlobTable" ADD CONSTRAINT "PK_BlobTable" PRIMARY KEY ("Id");

Обратите внимание на тот факт, что sub_type должен иметь отрицательное значение! Теперь разместите на форме следующие компоненты:

pFIBDataSet1: TpFIBDataSet;
pFIBTransaction1: TpFIBTransaction;
pFIBDatabase1: TpFIBDatabase;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
DBMemo1: TDBMemo;
Button1: TButton;
OpenDialog1: TOpenDialog;

Свяжите между собой компоненты FIBPlus и сгенерируйте запросы для pFIBDataSet1 (только на этот раз для таблицы “BlobTable”), как это уже было продемонстрировано в примерах ранее. Мы получим следующую форму:

[Image]

Напишем обработчик нажатия на кнопку:

procedure TForm1.Button1Click(Sender: TObject);
var S: TStream;
        FileS: TFileStream;
begin
    if not OpenDialog1.Execute then exit;
    pFIBDataSet1.Append;
    S :=pFIBDataSet1.CreateBlobStream(pFIBDataSet1.FieldByName('BlobText'), bmReadWrite);
    FileS := TFileStream.Create(OpenDialog1.FileName, fmOpenRead);
    S.CopyFrom(FileS, FileS.Size);
    FileS.Free;
    S.Free;
    pFIBDataSet1.Post;
end;

Запустите приложение и попробуйте сохранить пару текстовых файлов в blob-полей (например, исходные тексты данного приложения). Теперь, если посмотреть содержимое данной таблицы при помощи какого-либо визуального инструмента администрирования, содержащего BLOB-Viewer, то мы обнаружим, что blob-поля хранят непосредственно текст файлов:

[Image]

Теперь создадим функции для упаковки/распаковки blob-поля:

procedure PackBuffer(var Buffer: PChar; var BufSize: LongInt);
var srcStream, dstStream: TStream;
begin
    srcStream := TMemoryStream.Create;
    dstStream := TMemoryStream.Create;
    try
        srcStream.WriteBuffer(Buffer^, BufSize);
        srcStream.Position := 0;
        GZipStream(srcStream, dstStream, 6);
        srcStream.Free;
        srcStream := nil;
        BufSize := dstStream.Size;
        dstStream.Position := 0;
        ReallocMem(Buffer, BufSize);
        dstStream.ReadBuffer(Buffer^, BufSize);
    finally
        if Assigned(srcStream) then srcStream.Free;
        dstStream.Free;
    end;
end;
 
procedure UnpackBuffer(var Buffer: PChar; var BufSize: LongInt);
var srcStream,dstStream: TStream;
begin
    srcStream := TMemoryStream.Create;
    dstStream := TMemoryStream.Create;
    try
       srcStream.WriteBuffer(Buffer^, BufSize);
       srcStream.Position := 0;
       GunZipStream(srcStream, dstStream);
       srcStream.Free;
       srcStream:=nil;
       BufSize := dstStream.Size;
       dstStream.Position := 0;
       ReallocMem(Buffer, BufSize);
       dstStream.ReadBuffer(Buffer^, BufSize);
    finally
        if assigned(srcStream) then srcStream.Free;
           dstStream.Free;
    end;
end;

Не забудьте добавить два модуля в секцию uses: zStream, IBBlobFilter. Первый предназначен для архивации данных, а второй входит в состав FIBPlus и отвечает за управление blob-фильтрами. Теперь осталось только зарегистрировать blob-фильтры. Это делается при помощи вызова функции RegisterBlobFilter. Значение первого параметра – это тип blob-поля (в нашем случае, –15), второй и третий параметры – это функции кодирования и декодирования blob-поля:

procedure TForm1.FormCreate(Sender: TObject);
begin
    pFIBDatabase1.RegisterBlobFilter(-15, @PackBuffer, @UnpackBuffer);
    pFIBDatabase1.Connected := True;
    pFIBDataset1.Active := True;
end;

Запустите наше приложение, удалите те записи, которые там уже были, и добавьте новые. Внешне ничего не изменилось, однако если мы попробуем посмотреть, что же реально сохранилось в blob-полях, то увидим, что все данные заархивированы:

[Image]

Более того, приложение отправляет на сервер (и получает с сервера) blob’ы в уже упакованном виде, что может сильно сократить сетевой трафик! Конечно, упаковка blob-полей возможна и без использования описанного механизма blob-фильтров. Например, можно просто перед сохранением поля сжать его в процедуре Button1Click и распаковать в обработчике AfterScroll или что-нибудь в этом роде.

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

Резюме

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

Прочитано 15647 раз