使用Blob

来源:互联网 发布:中国第一个程序员 编辑:程序博客网 时间:2024/05/16 12:38
 基于FIBPlus组件在InterBase/Firebird 客户端应用程序中使用BLOB字段 2006年7月作者Sergey Vostrikov和Serge Buzadzhy

引言

BLOB字段可以在数据库中存储非结构数据,如图像,OLE对象,声音等,我们将阐述BLOB字段如何在服务端工作.了解与其他字段的区别很重要,BLOB数据没有直接存储在表的记录中.表记录只存储一个BLOB_ID,而BLOB体存储在一个独立的数据库表中. IB API提供可特定函数来存取BLOB体.这个特性使开发者可以存取未知大小的BLOB字段.使用FIBPlus 不需要调用这些函数,FIBPlus提供了这些功能的封装.但是知道幕后的原理是很有必要的.

现在使用如下表来阐述如何使用BLOB字段:

CREATE TABLE BIOLIFE (

   ID INTEGER NOT NULL,

   CATEGORY VARCHAR (15) character set WIN1251collateWIN1251,

   COMMON_NAME VARCHAR (30) character setWIN1251collate WIN1251,

   SPECIES_NAME VARCHAR (40) character set WIN1251collate 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-fields

图1.操作BLOB字段的应用程序.
这个范例使用标准组件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字段内容读取到客户端.只读取了BLOB_ID.幕后的行为是:当DBImage1组件要显示第一个记录的字段内容时,其通知pFIBDataSet1要获取字段内容.而后控件调用IB API从服务端获取BLOB数据体.这时使用第一条记录的Blob_ID字段值.可见这时本例只将第一条记录对应的BLOB字段抓取到客户端.切换TpFIBDataSet的记录,DBImage1将从其他记录中获取BLOB_ID值并发送回服务端.

修改的差别:

TFIBDataSet 中的BLOB字段由TBlobField 子类描述,继承了四个特定方法: LoadFromFile, LoadFromStream, SaveToFile和SaveToStream.

LoadFromFile用来将数据从特定文件加载到字段中,LoadFromStream从TStream对象中加载数据.

例如如果想将文件中的数据加载到BLOB字段,代码如下:

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为编辑模式.这里调用pFIBDataSet1.Edit.加载数据后调用Post方法保存修改.
第二个 重要的地方是设置TBlobField字段类型.不做类型转换FieldByName 返回TField对象实例,将无法调用必要的方法.

除了LoadFromXXX方法,也可能用到如下简单方法如FieldByName(…).AsString:='asfdsafsadfsad'; 来修改BLOB字段.

使用SaveToFile或SaveToStream方法将BLOB字段内容保存到文件或TStream对象:

procedure TMainForm.SaveBClick(Sender: TObject);

begin

 if not SaveD.Execute then

  exit;

 if not pFIBDataset1.FieldByName('GRAPHIC').IsNullthen

 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字段中是否是一个图片.可以在DataSource组件的OnDataChange事件中写如下代码:

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的BLOB_ID.而后UPDATE SQL 获取新的BLOB_ID,并执行更新语句.最后(很微妙的差异)服务端修改记录的BLOB_ID,而且客户端应用程序发送的BLOB_ID不会再次被分配.

从上面提到的差异可得到几个结论.如要修改BLOB字段必须将TpDataSet的poRefreshAfterPost设置为True(如果有两个事务而且没有设置AutoCommit,设置数据集的RefreshTransactionKind属性为tkUpdateTransaction).这样FIBPlus就可以获取到被服务端修改的BLOB_ID并替换掉无效的BLOB_ID.第二BLOB字段数据体在记录修改前发生会服务端.如果并发修改记录引起阻塞,则每个修改都要重新向服务端发送BLOB数据体.这将增加网络流量和数据库大小.这就是为什么我们推荐分为两步骤:在一个查询中修改非BLOB 字段,保存成功后再独立修改BLOB字段.

注意: FIBPlus的TpFIBDataSet对于自动生成更新语句有个特殊选项.使开发者可以隔离这两个过程: AutoUpdateOptions.SeparateBlobUpdate.

使用TpFIBQuery 操作BLOBs

如果使用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 表中所有记录,并进行遍历,调用SaveToFile函数将GRAPHIC字段中的内容保存到文件中,调用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-fields

已经讨论了BLOB字段的读取和修改.现在阐述如何查找BLOB字段.你需要理解的是如果BLOB参数出现在WHERE 子句中,服务端将字段的BLOB_ID与参数的BIOB_ID进行比较(而不是BLOB数据体的比较).这也是为什么要避免BLOB参数而且不能使用LoadFromFile 或 LoadFromStream 参数.
如果从TStream中价值参数值,事实上在服务端又创建了一个新的BLOB_ID及BLOB数据体. BLOB_ID是临时存在的,而不是做比较用的.这就是为什么在当比较BLOB字段是服务端抛出内部异常.如果必须对BLOB字段的内容进行比较,有两种变通方式:

将记录的BLOB字段与小于32KB的字符串进行比较:

使用AsString设置参数的必要值.服务端获取SQL_TEXT参数然后对这个值进行转换后比较. 如:

select

  ID

from

  BIOLIFE

where

  NOTES = :NOTES


代码:

begin

with DataSet1 do

 begin

  ParamByName('NOTES').asString:='Sample';

  Open;

 end;

end;

高于32KB的BLOB字段比较,使用特殊的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函数.
两个函数完全相同--接受同样的输入数据返回一样的值.

最后注意 (很明显): 魔术数字32KB是CHAR 和VARCHAR的最大尺寸.

FIBPlus特性: 客户端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参数将使用默认的子类型0,客户端程序调用存储过程时BLOB过滤器不会生效.