30 de junho de 2009

Trabalhando com BLOBs em Delphi e C++ Builder

Um BLOB - ou Binary Large OBject - é um tipo de dado existente em gerenciadores de Bancos de Dados. Apesar do nome "grandes objetos", ele é capaz de armazenar qualquer tipo de informação, independentemente do tamanho em bytes dessa informação. Um outra característica marcante é que o gerenciador de banco de dados não faz qualquer inferência sobre o conteúdo de campos deste tipo, cabendo à aplicação tratar o tipo corretamente. Do ponto de vista do banco de dados, um campo BLOB é apenas uma sequência de bytes.

O MS SQL Server fornece 2 tipos de BLOBs : binary e varbinary. A diferença entre ambos é que no binary o tamanho é fixo; o tamanho estipulado para o campo é o mesmo em todos os registros da tabela, o que o torna apropriado para armazenar estruturas cujo tamanho é conhecido e pré fixado. Já o varbinary aceita dados de tamanho variável, isto é, o campo pode ter tamanhos diferentes em cada registro na tabela. É útil para quando não se sabe de antemão qual o tamanho da informação a ser armazenada, como por exemplo, uma imagem, um vídeo, um áudio ou outro arquivo qualquer. Observação: de acordo com o MSDN, o tipo IMAGE do SQL Server será descontinuado. Veja aqui a informação a esse respeito.

O Oracle oferece o tipo BLOB, equivalente ao varbinary. Há uma tabela aqui com os tipos de dados do Oracle e o tamanho de cada um em várias versões deste banco de dados.

Suponha que você tem um texto formatado do tipo RTF (Rich Text Format) representando um contrato qualquer. Um usuário pode editar o conteúdo através de um componente TRichEdit inserido numa tela feita em Delphi ou C++ Builder e armazenar as alterações numa tabela no banco de dados. Para mostrar aqui como fazer isso, vou criar a seguinte tabela.
-- Em SQL Server
create table INFO
(
ID int not null,
DESCRICAO varchar(255) null,
OBJETOS varbinary(max) null
)

Não inclui chave primária por questão de simplicidade mas você deveria fazer isso num projeto real. A expressão max na declaração do BLOB indica que eu não sei qual é o tamanho máximo do conteúdo e que o Banco deve estar preparado para receber mais que os 8K normais.

Se você já tem um RTF pronto em um arquivo externo e quer carregar o contéudo no TRichEdit, pode usar o comando LoadFromFile, deixando o usuário livre para modificar o conteúdo na tela. Vou usar exemplos em C++ mas para Delphi a sequência de comandos é a mesma, bastando ajustar a sintaxe para essa linguagem.
RichEdit1->Lines->LoadFromFile (_FileName);

Inclui no Form também um componente TADOConnection configurado para se conectar ao meu banco de dados e um componente TADOCommand com o comando de inserção na tabela:
INSERT INTO INFO (ID, DESCRICAO, OBJETOS)
VALUES (:ID, :DESCRICAO, :OBJETOS)

A sintaxe :ID sinaliza ao Delphi (ou C++ Builder) que crie um parâmetro com o nome ID para facilitar o acesso. Ao solicitar a gravação, então, pode ser usada a sequência de comandos abaixo:
TMemoryStream *stream = new TMemoryStream ();
// Põe em stream com o conteúdo do RichEdit,
// isto é, o texto do contrato
RichEdit1->Lines->SaveToStream (stream);

// Monta os parâmetros do comando
TParameters *params;
params = ADOCommand1->Parameters;
params->ParamByName("ID")->Value = 1;
params->ParamByName("DESCRICAO")->Value = "Descrição";
params->ParamByName("OBJETOS")->LoadFromStream (stream, ftVarBytes);

// Executa o comando de inserção
ADOCommand1->Execute ();

delete stream;

Embora não mostrado, esse trecho de código deve ser inserido numa transação do Banco de Dados. O conteúdo para o BLOB é conseguido através de streams, primeiro salvando o texto formatado do RichEdit para dentro de um stream e depois carregando esse stream para o parâmetro do tipo BLOB do meu comando de inserção. Os demais valores eu deixei fixo apenas para simplificar; numa aplicação real, estas informações também deveriam ser variáveis.

Ficou faltando apenas mostrar como ler o conteúdo a partir do banco. Montei um TADOQuery que faz um SELECT para recuperar o registro da tabela INFO que tem o ID igual a 1. Então, uso esse SELECT para obter o BLOB:
ADOQuery1->Active = true;
if (ADOQuery1->Eof == false)
{
TBlobField *field;
TMemoryStream *stream = new TMemoryStream ();

// Salva o blob no stream
field = (TBlobField *)ADOQuery1->FieldByName ("OBJETOS");
field->SaveToStream (stream);

stream->Position = 0;
RichEdit1->Lines->LoadFromStream (stream);
delete stream;
}

Ou seja, leio o valor do campo BLOB num stream e alimento de volta o RichEdit, tomando o cuidado de reposicionar o stream no começo já que o comando LoadFromStream carrega a partir da posição atual.

Embora o exemplo tenha sido com um texto, a sequência de comandos seria a mesma para trabalhar com utro tipo qualquer de conteúdo - uma imagem, vídeo, XML, etc.

18 comentários :

Anônimo disse...

Olá amigo mas gostaria de perguntar se fosse recuperar um fluxo de dados vindo de um banco de dados tipo PostgreSQL ou Mysql utilizando suas bibliotecas proprias como libpq.a

Luís Gustavo Fabbro disse...

Nunca trabalhei diretamente com as bibliotecas do PostgreSQL - na ABC71 usamos conexões ADO para todos os tipos de bancos de dados que suportamos.

Creio que você possa configurar conexões com o DBExpress (ao invés do ADO) para usar as bibliotecas que deseja pois essa tecnologia foi desenvolvida justamente para permitir esse tipo de acesso.

Andrea disse...

Olá,
Gostaria da sua ajuda.
Estou tentando ler uma imagem em um campo BLOB do sqlite e exibir essa imagem no meu form, quando ele vai para a linha:
MS := sltb.FieldAsBlob(sltb.FieldIndex['logotipo']);
Dá um erro: Not a Blob Field.
No bd foi criado o campo blob.
Não sei como resolver isso.

Luís Gustavo Fabbro disse...

Andrea

Mesmo sem ter o contexto exato de como seu problema ocorre (ex: que wrapper do sqlite está usando, se há algum TDataSet envolvido, etc.), o comando FieldAsBlob não está enxegando o campo como sendo um Blog. Certifique-se de que ele está mesmo criado como Blob e verifique coisas tais como se o TField associado é um TBlobField pois o Delphi/C++ Builder às vezes interpretam errado o tipo do campo e criam uma associação errada.

Anônimo disse...

ola pessoal!
alguem pode me dizer como faço para guardar os dados inseridos numa interface criada em builder c++ em uma tabela da base de dados mysql?

Luís Gustavo Fabbro disse...

Que tipo de problema vc está tendo? Há alguma mensagem de erro ? Em que momento ?

[]s

Anônimo disse...

Poderia postar a fonte completa para delphi com TADO, salvar e abrir o arquivo?

desde já agradeço...

Luís Gustavo Fabbro disse...

Para fazer isso em Delphi, basta compatibilizar a sintaxe:

var stream : TMemoryStream;
params : TParameters;
begin
stream := TMemoryStream.Create ();

{ Põe em stream com o conteúdo do RichEdit,
isto é, o texto do contrato}
RichEdit1.Lines.SaveToStream (stream);

{ Monta os parâmetros do comando }
params := ADOCommand1.Parameters;
params.ParamByName("ID").Value := 1;
params.ParamByName("DESCRICAO").Value := "Descrição";
params.ParamByName("OBJETOS").LoadFromStream (stream, ftVarBytes);

{ Executa o comando de inserção }
ADOCommand1.Execute ();

stream.Free();


[]s

Anônimo disse...

Bom dia, Luis Gustavo
Tenho um banco de dados mdf em sql 2008 r2 e aplicação delphi.
E estou utilizando o filestream do sql para salvar arquivos: PDF, xls,doc.

Para salvar eu utilizo TADOstored procedure.
o meu problema esta em abrir o arquivo pdf do banco de dados.

Em alguns de seus projetos já utilizou o filestream?

desde já agradeço!


Att. Fabio Roell

Luís Gustavo Fabbro disse...

Fábio

Nós não usamos esse recurso na ABC71 pois é específico do SQL Server e nosso ERP funciona tb com outros bancos.

No pouco que vi desse assunto, você consegue recuperar um "path" para o arquivo através de um SELECT na tabela que contém a coluna FILESTREAM. Com o path, acredito que você possa abrir o documento PDF através do ShellExecute da API do Windows. Dependendo da extensão do arquivo, basta usar o vervo "open" junto com o nome do arquivo. Se a extensão não estiver presente ou não for PDF, use o READER.EXE como comando e informe o path como parâmetro do executável.

[]s

Anônimo disse...

Luis Gustavo, bom dia,

Estou com um problema para abrir arquivo do campo varbinary(max)banco .MDF do sql server, utilizao o Delphi 2010 e para salvar no banco uso stored procedure:
Info:
Parte da stored procedure Salvar PDF, Doc, etc...
Parameters[17].LoadFromFile(edanexo.Text,FtBlob);funciona 100%.

Abrir:

procedure TfrmMovPneus.AbrirAnexo;
var
Stream:TFileStream;
Field:TBlobStream;
m:TADOQuery;
begin
m:=TADOQuery.Create(nil);
m.Connection:=DMsql.ADOConnection1;
m.sql.add('SELECT MOV_ID,FROM_ANEXO');
m.SQL.Add('from MOV_PNEUS');
m.SQL.Add('WHERE MOV_ID='+Quotedstr('E2ED56E9-6E29-4574-9E87-182DBACEA04B'));
try
m.Open;
Field := TBlobStream.Create(m.Fieldbyname('FROM_ANEXO')as Tblobfield,bmread);
Stream := TFileStream.Create('C:\Sistema\test\', fmCreate or fmOpenWrite);
Stream.CopyFrom(field, 0);
//stream:=TStream.Create('C:\Sistema\test\MEUDOC.PDF', fmCreate or fmOpenWrite);
ShowMessage('Documento salvo em disco. Clique em [OK] para prosseguir ...');
try
// ...
finally
field.Free;
stream.Free;
end;
except
// handle exceptions
end;
end;

poderia me ajudar, ja quebrei tanto a cabeça?

Att. Fabio Roell

Luís Gustavo Fabbro disse...

Fábio

Usando o método descrito no post não funcionou como você queria? Isto é, trocando o TMemoryStream pelo TFileStream? A única diferença é que o post traz a sintaxe em C++; convertendo para Delphi, seria como segue:

var
Stream:TFileStream;
Field:TBlobField;
begin
stream:=TFileStream.Create('C:\Sistema\test\MEUDOC.PDF', fmCreate or fmOpenWrite);

// Salva o blob no stream
field := ADOQuery1->FieldByName ('FROM_ANEXO') As TBlobField;
field->SaveToStream (stream);
stream.Free();
end;

Após o Free no stream, o arquivo estará gravado em disco e você pode exibí-lo para o usuário através do ShellExecute.

[]s

Cezar Lopes disse...

Luís Gustavo, estou com um problema. Quando gravo está tudo certo, mas na hora de carregar não está retornando o que está no banco. Segue o fonte:

Salvando os dados

mmoEsclarecimento.Lines.SaveToStream(strmEsclarecimento);

cdsPPRAESCLARECIMENTO.LoadFromStream(strmEsclarecimento);

cdsPPRA.ApplyUpdates(0);

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Recuperando as informações

cdsPPRAESCLARECIMENTO.SaveToStream(strmEsclarecimento);

mmoEsclarecimento.Lines.LoadFromStream(strmEsclarecimento);

Se puder me ajudar eu lhe agradeço.

Luís Gustavo Fabbro disse...

Cezar

Há alguma mensagem de erro ou simplesmente não carrega nada? Nesse último caso, pode ser falta de "rebobinar" o stream antes de fazer a carga no memo; algo como:

strmEsclarecimento.Position := 0;
mmoEsclarecimento.Lines.LoadFromStream(strmEsclarecimento);


[]s

Cezar Lopes disse...

Luís Gustavo, era realmente apenas a questão de rebobinar o stream.

Mais uma vez, muito obrigado pelo auxílio.

andre.ayresi disse...

Ola Luis,... voce poderia me enviar seu contato, email, skype, ou facebook ...grato....
Skype: andrewsayres
https://www.facebook.com/andre.ayresi
email: andrewsayres@hotmail.it

Luís Gustavo Fabbro disse...

André

As informações pra contato do blog estão no canto superior direito da página, junto com o quadro "Quem sou eu". Dúvidas tb podem ser postadas aqui, na área de comentários de cada post.

[]s

Unknown disse...

Esse código considera apenas que você irá receber
um único registro da tabela MySQL.
Caso queira receber mais registros é necessário
uma mudança no código.


MYSQL *sock;
MYSQL_RES *result = NULL;
unsigned long *lengths;
FILE *fp = NULL;

sock = mysql_init(0);
if(mysql_real_connect(sock,"localhost","root",
"senhamysql","testblob",0,NULL,CLIENT_MULTI_STATEMENTS)){
fprintf(stderr,"%s",mysql_error(sock));
return EXIT_FAILURE;
}

mysql_set_character_set("latin1");

const char *query = "SELECT data FROM rawdata WHERE id = 1;";

if(mysql_real_query(sock,query,strlen(query))){
fprintf(stderr,"%s",mysql_error(sock));
return EXIT_FAILURE;
}

result = mysql_store_result(sock);
if(result == NULL){
fprintf(stdout,"Sem registros.");
return EXIT_FAILURE;
}

row = mysql_fetch_row(result);
lengths = mysql_fetch_lengths(result);

fprintf(stdout,"Size Blob: %d",lengths[0]);

if((fp = fopen("foto.jpg","w")) == NULL){
fprintf(stderr,"Erro na criação do arquivo.");
return EXIT_FAILURE;
}

if(fwrite(row[0],lengths[0],1,fp) != 1){
fprintf(stderr,"Erro em salvar o arquivo.");
return EXIT_FAILURE;
}

mysql_free_result(result);
fclose(fp);

Postar um comentário

OBS: Os comentários enviados a este Blog são submetidos a moderação. Por isso, eles serão publicados somente após aprovação.

Observação: somente um membro deste blog pode postar um comentário.