Delphi-Help

  • Increase font size
  • Default font size
  • Decrease font size
Главная

Работа с BLOB-полями в клиентских приложениях InterBase и Firebird на основе компонентов FIBPlus

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

Работа с BLOB-полями в клиентских приложениях InterBase и Firebird на основе компонентов FIBPlus

Достаточно часто желательно хранить в базе данных разнообразные неструктурированные данные: изображения, OLE-объекты, звук и т.д. Специально для этих целей существует специальный тип данных - BLOB. Прежде чем рассматривать работу FIBPlus с полями этого типа на примерах, вспомним о том, как сам сервер реализовывает работу с ними. Важно знать и помнить следующее: В отличие от всех других полей, данные BLOB поля не хранятся непосредственно в записи таблицы. В записи таблицы хранится лишь идентификатор BLOB (BLOB_ID), а само тело BLOB хранится на отдельных страницах базы данных и доступ к ним осуществляется специальными функциями IB API. Эта особенность позволяет хранить в BLOB полях данные нефиксированного размера. FIBPlus максимально скрывает эти нюансы непосредственно от разработчика, от вас не требуется самостоятельно вызывать вышеупомянутые функции IB API, но, тем не менее, полезно знать, что происходит «за кулисами» .

Итак, продемонстрируем использование BLOB-полей на примере следующей таблицы:

CREATE TABLE BIOLIFE (
    ID INTEGER NOT NULL,
    CATEGORY VARCHAR (15) character set WIN1251 collate WIN1251,
    COMMON_NAME VARCHAR (30) character set WIN1251 collate WIN1251,
    SPECIES_NAME VARCHAR (40) character set WIN1251 collate WIN1251,
    LENGTH__CM_ DOUBLE PRECISION,
    LENGTH_IN DOUBLE PRECISION,
    NOTES BLOB sub_type 1 segment size 80,
    GRAPHIC BLOB sub_type 0 segment size 80);

Использование TpFIBDataSet при работе с BLOB-полями

0012_01

Рис. 1. Внешний вид формы приложения для работы с BLOB-полями.

Для вывода изображений, сохраненных в поле GRAPHIC, мы будем использовать стандартный компонент DBIMage1: TDBImage. Очевидно также, что запросы при работе с BLOB-полями внешне ничем не отличаются от запросов со стандартными типами полей:

SelectSQL:
  SELECT * FROM BIOLIFE
  
UpdateSQL:
  UPDATE BIOLIFE Set
        ID=?NEW_ID,
        CATEGORY=?NEW_CATEGORY,
        COMMON_NAME=?NEW_COMMON_NAME,
        SPECIES_NAME=?NEW_SPECIES_NAME,
        LENGTH__CM_=?NEW_LENGTH__CM_,
        LENGTH_IN=?NEW_LENGTH_IN,
        NOTES=?NEW_NOTES,
        GRAPHIC=?NEW_GRAPHIC
  WHERE ID=?OLD_ID
  
InsertSQL:
  INSERT INTO BIOLIFE(
        ID,
        CATEGORY,
        COMMON_NAME,
        SPECIES_NAME,
        LENGTH__CM_,
        LENGTH_IN,
        NOTES,
        GRAPHIC
  )
  VALUES (
        ?NEW_ID,
        ?NEW_CATEGORY,
        ?NEW_COMMON_NAME,
        ?NEW_SPECIES_NAME,
        ?NEW_LENGTH__CM_,
        ?NEW_LENGTH_IN,
        ?NEW_NOTES,
        ?NEW_GRAPHIC
  )
  
DeleteSQL:
  DELETE FROM BIOLIFE
  WHERE ID=?OLD_ID
  
RefreshSQL:
  SELECT * FROM BIOLIFE
  WHERE 
  ID=?OLD_ID

Нюансы при чтении:

Сразу же акцентируем внимание на первом скрытом нюансе. Выполнение «SELECT * FROM BIOLIFE» не вычитывает данные из BLOB полей на клиента. В поле «GRAPHIC» возвращается идентификатор BLOB'a. Далее «за кулисами» происходит следующее: Компонент DBImage1 хочет отобразить содержимое поля из первой записи. Он обращается к pFIBDataSet1 за этим содержимым, и тот «втайне» от нас обращается к серверу через специальные функции IB API непосредственно за телом BLOB поля, пользуясь Blob_ID поля из ПЕРВОЙ записи. Таким образом, мы должны понимать, что в отображенной на иллюстрации ситуации мы «вытащили» на клиента содержимое BLOB поля только первой записи. По мере скроллирования по записям в TpFIBDataSet, DBImage1 будет обращаться за данными других записей, и эти обращения будут транслироваться к серверу.

Нюансы при модификации:

BLOB- поля в TFIBDataSet представлены потомками от TBlobField , и как следствие, наследуют четыре специальных метода модификации таких полей: методы LoadFromFile, LoadFromStream, SaveToFile и SaveToStream.

Первый метод (LoadFromFile) используется для сохранения в поле данных из внешнего файла, второй (LoadFromStream) - для сохранения из любого объекта типа TStream.

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

procedure TMainForm.OpenBClick(Sender: TObject);
  begin
        if not OpenD.Execute then exit;
        pFIBDataSet1.Edit;
        TBlobField(pFIBDataSet1.FieldByName('GRAPHIC')).LoadFromFile(OpenD.FileName);
        pFIBDataSet1.Post;
  end;

Обратите внимание на важный момент: перед тем, как присваивать значение BLOB-полю, необходимо перевести pFIBDataSet в состояние редактирования данных. В данном случае это делается безусловным вызовом метода Edit. После загрузки данных остается только сохранить изменения вызовом метода Post.

Второй важный момент - необходимо прописать явное приведение поля к типу TBlobField. Дело в том, что FieldByName возвращает объект типа TField, а он не имеет нужных нам методов.

Помимо специальных методов LoadFromXXX, для модификации BLOB полей можно использовать и простые методы типа FieldByName(…).AsString:='asfdsafsadfsad';

Методами SaveToFile, SaveToStream мы можем сохранить значение BLOB-поля в некоторый внешний файл или TStream.

Пример сохранения в файл:

procedure TMainForm.SaveBClick(Sender: TObject);
  begin
        if not SaveD.Execute then exit;
        if not pFIBDataset1.FieldByName('GRAPHIC').IsNull then 
               begin
                       TBlobField(pFIBDataSet1.FieldByName('GRAPHIC')).SaveToFile(SaveD.FileName);
               end;
  end;

Пример очистки поля.

procedure TMainForm.Button1Click(Sender: TObject);
  begin
        pFIBDataSet1.Edit;
        pFIBDataSet1.FieldByName('GRAPHIC').Clear;
        pFIBDataSet1.Post;
  end;

Иногда также нужно знать, является ли BLOB-поле пустым или нет. При использовании визуальных компонентов типа TDBImage мы не можем быть в этом уверены. Согласитесь, что никто не мешает нам «нарисовать» пустую картинку и сохранить ее в BLOB-поле. В этом случае, мы не сможем отличить «на глаз»: есть ли какое-то изображение в BLOB-поле, или нет. Однако мы можем написать обработчик события OnDataChange компонента DataSource1: TDataSource:

procedure TMainForm.DataSource1DataChange(Sender: TObject; Field: 
  TField);
  begin
        CheckBox1.Checked := pFIBDataSet1.FieldByName('GRAPHIC').IsNull;
  end;

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

Итак, с внешней стороной дела мы разобрались, давайте теперь заглянем «за кулисы». Что происходит при модификациях записи, содержащей BLOB поле?

Вариант 1. Если BLOB поле не изменялось в процессе редактирования, то в соответствующий параметр UPDATE SQL попадет прежний BLOB_ID. Само содержимое BLOB поля на сервер не передается.

Вариант 2. BLOB поле подверглось модификации. Фактически для записи нового содержимого производится несколько действий. Во-первых, с помощью IB API функций isc_create_blob2, isc_put_segment, isc_close_blob в базе данных сохраняется тело НОВОГО BLOB. Так же при этом действии клиент узнает и запоминает BLOB_ID для нового поля. Во-вторых, в UPDATE SQL передается этот новый BLOB_ID, и UPDATE запускается на выполнение. В-третьих (ОЧЕНЬ НЕОЧЕВИДНЫЙ НЮАНС), сервер при фиксации изменений записи ПРЕОБРАЗУЕТ полученный BLOB_ID. То есть, тот BLOB_ID, который передавался клиентом, становится негодным к повторному использованию.

Из упомянутых нюансов, можно сделать несколько практических выводов. Во-первых, для TpDataSet, в которых вы будете модифицировать BLOB поля, просто необходимо использовать опцию poRefreshAfterPost (если используются две транзакции и не используется AutoCommit, то свойство датасета "RefreshTransactionKind" должно иметь значение "tkUpdateTransaction"). При этом действии FIBPlus получит преобразованный сервером BLOB_ID и подменит им тот, который уже стал невалидным. Во-вторых, мы видим, что тело BLOB поля передается на сервер ДО выполнения модификации записи. Если по каким-либо причинам последующая модификация записи будет отвергнута сервером (например, через constraints), то при повторной попытке модификации нам придется передавать тело BLOB поля заново. Это может быть накладно и с точки зрения сетевого трафика, и с точки зрения «разбухания» базы. Поэтому иногда имеет смысл разделить два процесса: отдельным запросом делать модификации всех не BLOB полей, a после успешного завершения этих модификацй отдельно отправлять изменения BLOB полей. (для TpFIBDataSet с автогенерацией модифицирующих запросов в FIBPlus есть специальная опция, регулирующая такое поведение: AutoUpdateOptions. SeparateBlobUpdate).

Использование TpFIBQuery при работе с BLOB-полями

Если вы используете TpFIBQuery для работы с BLOB-полями, то общий принцип остается тем же - можно использовать либо файлы, либо методы работы с TStream. Например, мы можем написать следующую процедуру для сохранения всех изображений из нашей таблицы в файлы:

pFIBQuery.SQL: SELECT * FROM BIOLIFE

procedure TMainForm.Button2Click(Sender: TObject);
var 
  Index: Integer;
begin
  with pFIBQuery1 do begin
        ExecQuery;
        Index := 1;
        while not Eof do begin
               FN('GRAPHIC').SaveToFile(IntToStr(Index) + '.bmp');
               Next;
               inc(Index);
        end;
  Close;
  end;
end;

Примечание: Метод FN является аналогом метода FieldByName.

Смысл кода, приведенного выше, совершенно очевиден: мы получаем все записи из таблицы BIOLIFE, в цикле берем от сервера очередную запись из запроса, сохраняем в файл значение поля GRAPHIC при помощи метода SaveToFile и запрашиваем следующую запись при помощи метода Next. Аналогичным образом мы могли бы присваивать значение BLOB-параметру:

pFIBQuery.SQL: INSERT INTO BIOLIFE (GRAPHIC) VALUES (?GRAPHIC)
procedure TMainForm.Button2Click(Sender: TObject);
var
  Index: Integer;
begin
  with pFIBQuery1 do begin
        Prepare;
        for Index := 1 to 3 do begin
               Params[0].LoadFromFile(IntToStr(Index) + '.bmp');
               ExecQuery;
        end;
        Transaction.Commit;
  end;
end;

Данный пример вставляет три новые записи в таблицу BIOLIFE и сохраняет в них изображения из некоторых файлов “1.bmp”, “2.bmp” и “3.bmp”.

Примечание: поскольку в данном примере для сохранения изменений использовался метод Commit, то необходимо перезапустить приложение, чтобы увидеть вставленные записи в DBGrid1.

Поиск по BLOB полям

До сих пор мы рассматривали только операции чтения/модификации BLOB полей. Рассмотрим ситуацию, когда BLOB поля присутствуют в условиях выборки. Если BLOB параметр присутствует в where клаузе, то нужно понимать, что при выполнении запроса сравниваются не содержимое BLOB поля и параметра, а BLOB_ID поля и BLOB_ID параметра. Поэтому нам следует избегать появления BLOB параметров и не пользоваться методами LoadFromFile или LoadFromStream.
Поскольку если вы загружаете в параметр значения через TStream, в реальности вы создаете НОВЫЙ BLOB на сервере с НОВЫМ BLOB_ID. BLOB_ID получается ВРЕМЕННЫЙ, и не предназначен для сравнения с чем-либо. Поэтому сервер в ответ на такую попытку, выдает сообщение о внутренней ошибке. Если вам крайне необходимо сравнивать BLOB поле с неким содержимым, то у вас есть две возможности:

Ситуация, когда нужно найти записи, в которых BLOB поле сравнивается со строкой менее 32 Kб.

Вам необходимо присвоить искомое значение в параметр через свойство AsString. В этом случае на сервер уйдет параметр типа SQL_TEXT и сервер сам произведет необходимую конвертацию значения, необходимую для сравнения.
Например, так::

select 

ID
from 
   BIOLIFE 
where 
   NOTES = :NOTES

Код вызова:

   begin
     with DataSet1 do
     begin
        ParamByName('NOTES').asString:='Sample';
        Open;
     end;
   end;

Если нужно сравнить BLOB поле со значением большим, чем 32 Кб, то придется использовать специальные udf.
Например, так::

select 
   ID 
from 
   BIOLIFE 
where 
   blobCRC(NOTES) = :NOTES

Код вызова:

      TempStream := TMemoryStream.Create; 
      Try 

TempStream.LoadFromFile('MyFile');
            with DataSet1 do 

begin

ParamByName('NOTES') .asInteger:= blobCRCPas(MyStream);

Open;
            end; 
        finally 

FreeAndNil(TempStream);
      end; 

В этом примере blobCRC -это udf, а blobCRCPas - функция Pascal.
Обе функции должны быть идентичными, то есть возвращать один и тот же результат для одних и тех же входных данных.

И последнее (почти очевидное) замечание: "Волшебная" цифра 32 Кб - это максимальный размер значений типа CHAR и VARCHAR.

Уникальные возможности FIBPlus: Client BLOB-filters. "Прозрачное" сжатие BLOB-полей..

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

В FIBPlus есть механизм клиентских BLOB фильтров, очень схожий с аналогичным механизмом на серерве. Но преимущество локальных BLOB фильтров FIBPlus в том, что они значительно снижают сетевой трафик приложения, если BLOB поле сжимается до отправки на клиентское приложение и распаковывается после того, как оно получено на клиенте. Вы можете сделать это путем регистрации двух процедур: чтения и записи BLOB полей в TpFIBDatabase. После этого FIBPlus будет автоматически использовать эти процедуры для того, чтобы обрабатывать все BLOB поля заданного типа во всех TpFIBDataSet'ах, используя один экземпляр TpFIBDatabase. Проиллюстрируем этот механизм примером:

Сначала мы создадим таблицу с 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 должен иметь отрицательное значение!

Примечание: «Существует несколько предопределенных подтипов BLOB, которые встроены в InterBase. Все эти подтипы имеют неотрицательные номера, например subtype 0 - это данные неопределенного типа, subtype 1 - текст, subtype 2 - BLR (Binary Language Representation, см. глоссарий и главу "Структура базы данных InterBase") и т. д. Пользователь также может определять свои подтипы BLOB, которые могут иметь отрицательные значения.». Мир InterBase, А. Ковязин, С. Востриков.

Теперь положите следующие компоненты на форму:

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

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

0012_02

Рис.2. Приложение с использованием BLOB-фильтров FIBPlus

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

procedure TForm1.Button1Click(Sender: TObject);
  begin
        if not OpenDialog1.Execute then exit;
        pFIBDataSet1.Append;
        TBlobField(pFIBDataSet1.FieldByName('BlobText')).LoadFromFile(OpenDialog1.FileName);
        pFIBDataSet1.Post;
  end;

Теперь создадим функции сжатия/распаковки 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 поле, станет ясно, что данные были заархивированы:

0012_03

Рис. 3. Данные в BLOB-поле, упакованные при помощи локального фильтра FIBPlus.

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

Примечание:

Если запись "фильтрованных" BLOB осуществляется через хранимые процедуры, то в процедуре обязательно должен быть указан подтип входного параметра. Например:

CREATE PROCEDURE "BlobTable_U"(
   "Id" INTEGER,
   "BlobText" BLOB SUB_TYPE -15)
    AS
       BEGIN
         UPDATE "BlobTable"
           SET "BlobText" = :"BlobText"
         WHERE ("Id" = :"Id");
       END;

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

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

Авторизация



Счетчики