Чтение данных из BLOB
В качестве примера чтения BLOB рассмотрим процедуру, которая разбиваетстроку по разделителю (обратная процедура для встроенной агрегатнойфункции LIST). Она объявлена следующим образом
create procedure split (
txt blob sub_type text character set utf8,
delimiter char(1) character set utf8 = ','
)
returns (
id integer
)
external name 'myudr!split'
engine udr;
Зарегистрируем фабрику нашей процедуры:
function firebird_udr_plugin(AStatus: IStatus; AUnloadFlagLocal: BooleanPtr;
AUdrPlugin: IUdrPlugin): BooleanPtr; cdecl;
begin
// регистрируем нашу процедуру
AUdrPlugin.registerProcedure(AStatus, 'split', TProcedureSimpleFactory<TSplitProcedure>.Create());
theirUnloadFlag := AUnloadFlagLocal;
Result := @myUnloadFlag;
end;
Здесь я применил обобщённую фабрику процедур для простых случаев, когдафабрика просто создаёт экземпляр процедуры без использования метаданных.Такая фабрика объявлена следующим образом:
...
interface
uses SysUtils, Firebird;
type
TProcedureSimpleFactory<T: IExternalProcedureImpl, constructor> =
class(IUdrProcedureFactoryImpl)
procedure dispose(); override;
procedure setup(AStatus: IStatus; AContext: IExternalContext;
AMetadata: IRoutineMetadata; AInBuilder: IMetadataBuilder;
AOutBuilder: IMetadataBuilder); override;
function newItem(AStatus: IStatus; AContext: IExternalContext;
AMetadata: IRoutineMetadata): IExternalProcedure; override;
end;
...
implementation
{ TProcedureSimpleFactory<T> }
procedure TProcedureSimpleFactory<T>.dispose;
begin
Destroy;
end;
function TProcedureSimpleFactory<T>.newItem(AStatus: IStatus;
AContext: IExternalContext; AMetadata: IRoutineMetadata): IExternalProcedure;
begin
Result := T.Create;
end;
procedure TProcedureSimpleFactory<T>.setup(AStatus: IStatus;
AContext: IExternalContext; AMetadata: IRoutineMetadata; AInBuilder,
AOutBuilder: IMetadataBuilder);
begin
...
Теперь перейдём к реализации процедуры. Сначала объявим структуры длявходного и выходного сообщения.
TInput = record
txt: ISC_QUAD;
txtNull: WordBool;
delimiter: array [0 .. 3] of AnsiChar;
delimiterNull: WordBool;
end;
TInputPtr = ^TInput;
TOutput = record
Id: Integer;
Null: WordBool;
end;
TOutputPtr = ^TOutput;
Как видите вместо значения BLOB передаётся идентификатор BLOB, которыйописывается структурой ISC_QUAD
.
Теперь опишем класс процедуры и возвращаемого набора данных:
TSplitProcedure = class(IExternalProcedureImpl)
private
procedure SaveBlobToStream(AStatus: IStatus; AContext: IExternalContext;
ABlobId: ISC_QUADPtr; AStream: TStream);
function readBlob(AStatus: IStatus; AContext: IExternalContext;
ABlobId: ISC_QUADPtr): string;
public
// Вызывается при уничтожении экземпляра процедуры
procedure dispose(); override;
procedure getCharSet(AStatus: IStatus; AContext: IExternalContext;
AName: PAnsiChar; ANameSize: Cardinal); override;
function open(AStatus: IStatus; AContext: IExternalContext; AInMsg: Pointer;
AOutMsg: Pointer): IExternalResultSet; override;
end;
TSplitResultSet = class(IExternalResultSetImpl)
{$IFDEF FPC}
OutputArray: TStringArray;
{$ELSE}
OutputArray: TArray<string>;
{$ENDIF}
Counter: Integer;
Output: TOutputPtr;
procedure dispose(); override;
function fetch(AStatus: IStatus): Boolean; override;
end;
Дополнительные методы SaveBlobToStream
и readBlob
предназначены длячтения BLOB. Первая читает BLOB в поток, вторая — основана на первой ивыполняет преобразование прочтённого потока в строку Delphi. В наборданных передаётся массив строк OutputArray
и счётчик возвращённыхзаписей Counter
.
В методе open
читается BLOB и преобразуется в строку. Полученная строкаразбивается по разделителю с помощью встроенного метода Split
из хелперадля строк. Полученный массив строк передаётся в результирующий наборданных.
function TSplitProcedure.open(AStatus: IStatus; AContext: IExternalContext;
AInMsg, AOutMsg: Pointer): IExternalResultSet;
var
xInput: TInputPtr;
xText: string;
xDelimiter: string;
begin
xInput := AInMsg;
Result := TSplitResultSet.Create;
TSplitResultSet(Result).Output := AOutMsg;
if xInput.txtNull or xInput.delimiterNull then
begin
with TSplitResultSet(Result) do
begin
// создаём пустой массив
OutputArray := [];
Counter := 1;
end;
Exit;
end;
xText := readBlob(AStatus, AContext, @xInput.txt);
xDelimiter := TFBCharSet.CS_UTF8.GetString(TBytes(@xInput.delimiter), 0, 4);
// автоматически не правильно определяется потому что строки
// не завершены нулём
// ставим кол-во байт/4
SetLength(xDelimiter, 1);
with TSplitResultSet(Result) do
begin
OutputArray := xText.Split([xDelimiter], TStringSplitOptions.ExcludeEmpty);
Counter := 0;
end;
end;
Note
|
Замечание
Тип перечисление |
Теперь опишем процедуру чтения данных из BLOB в поток. Для того чтобыпрочитать данные из BLOB, его необходимо его открыть. Это можно сделатьвызвав метод openBlob
интерфейса IAttachment
. Поскольку мы читаем BLOBиз своей базы данных, то будем открывать его в контексте текущегоподключения. Контекст текущего подключения и контекст текущей транзакциимы можем получить из контекста внешней процедуры, функции или триггера(интерфейс IExternalContext
).
BLOB читается порциями (сегментами), максимальный размер сегмента равен64 Кб. Чтение сегмента осуществляется методом getSegment
интерфейсаIBlob
.
procedure TSplitProcedure.SaveBlobToStream(AStatus: IStatus;
AContext: IExternalContext; ABlobId: ISC_QUADPtr; AStream: TStream);
var
att: IAttachment;
trx: ITransaction;
blob: IBlob;
buffer: array [0 .. 32767] of AnsiChar;
l: Integer;
begin
try
att := AContext.getAttachment(AStatus);
trx := AContext.getTransaction(AStatus);
blob := att.openBlob(AStatus, trx, ABlobId, 0, nil);
while True do
begin
case blob.getSegment(AStatus, SizeOf(buffer), @buffer, @l) of
IStatus.RESULT_OK:
AStream.WriteBuffer(buffer, l);
IStatus.RESULT_SEGMENT:
AStream.WriteBuffer(buffer, l);
else
break;
end;
end;
AStream.Position := 0;
// метод close в случае успеха совобождает интерфейс IBlob
// поэтому последующий вызов release не нужен
blob.close(AStatus);
blob := nil;
finally
if Assigned(blob) then
blob.release;
if Assigned(trx) then
trx.release;
if Assigned(att) then
att.release;
end;
end;
Note
|
Замечание
Обратите внимание, интерфейсы |
Important
|
Важно
Метод В примере переменной |
На основе метода SaveBlobToStream
написана процедура чтения BLOB встроку:
function TSplitProcedure.readBlob(AStatus: IStatus; AContext: IExternalContext;
ABlobId: ISC_QUADPtr): string;
var
{$IFDEF FPC}
xStream: TBytesStream;
{$ELSE}
xStream: TStringStream;
{$ENDIF}
begin
{$IFDEF FPC}
xStream := TBytesStream.Create(nil);
{$ELSE}
xStream := TStringStream.Create('', 65001);
{$ENDIF}
try
SaveBlobToStream(AStatus, AContext, ABlobId, xStream);
{$IFDEF FPC}
Result := TEncoding.UTF8.GetString(xStream.Bytes, 0, xStream.Size);
{$ELSE}
Result := xStream.DataString;
{$ENDIF}
finally
xStream.Free;
end;
end;
Note
|
Замечание
К сожалению Free Pascal не обеспечивает полную обратную совместимость сDelphi для класса |
Метод fetch
выходного набора данных извлекает из массива строк элемент синдексом Counter
и увеличивает его до тех пор, пока не будет извлечёнпоследний элемент массива. Каждая извлечённая строка преобразуется кцелому. Если это невозможно сделать, то будет возбуждено исключение скодом isc_convert_error
.
procedure TSplitResultSet.dispose;
begin
SetLength(OutputArray, 0);
Destroy;
end;
function TSplitResultSet.fetch(AStatus: IStatus): Boolean;
var
statusVector: array [0 .. 4] of NativeIntPtr;
begin
if Counter <= High(OutputArray) then
begin
Output.Null := False;
// исключение будут перехвачены в любом случае с кодом isc_random
// здесь же мы будем выбрасывать стандартную для Firebird
// ошибку isc_convert_error
try
Output.Id := OutputArray[Counter].ToInteger();
except
on e: EConvertError do
begin
statusVector[0] := NativeIntPtr(isc_arg_gds);
statusVector[1] := NativeIntPtr(isc_convert_error);
statusVector[2] := NativeIntPtr(isc_arg_string);
statusVector[3] := NativeIntPtr(PAnsiChar('Cannot convert string to integer'));
statusVector[4] := NativeIntPtr(isc_arg_end);
AStatus.setErrors(@statusVector);
end;
end;
inc(Counter);
Result := True;
end
else
Result := False;
end;
Note
|
Замечание
На самом деле обработка любых ошибок кроме |
Работоспособность процедуры можно проверить следующим образом:
SELECT ids.ID
FROM SPLIT((SELECT LIST(ID) FROM MYTABLE), ',') ids
Note
|
Замечание
Главным недостатком такой реализации состоит в том, что BLOB будетвсегда прочитан целиком, даже если вы хотите досрочно прерватьизвлечение записей из процедуры. При желании вы можете изменить кодпроцедуры таким образом, чтобы разбиение на подстроки осуществлялосьболее маленькими порциями. Для этого чтение этих порций необходимоосуществлять в методе |