引言
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 WIN1251collate WIN1251,
COMMON_NAME VARCHAR (30) character setWIN1251 collate 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过滤器不会生效.