C# - Trabalhando com dados binários no SQL Server (DataReader)


Neste artigo eu vou mostrar como podemos trabalhar com dados binários no SQL Server usando um DataReader com a linguagem C#.

O comportamento padrão do DataReader é obter os dados a partir da fonte linha por linha. Quando vamos trabalhar com dados binários(BLOB) usando o objeto DataReader o tratamento deve ser um pouco diferente pois eles podem conter um grande volume de dados que não podem estar contidos em uma única linha.

O método Command.ExecuteReader possui uma sobrecarga que usa um argumento CommandBehavior para modificar este comportamento padrão do DataReader.

Você pode passar um CommandBehavior.SequentialAcces para o método ExecuteReader para modificar o comportamento padrão do DataReader de forma que ao invés de carregar linhas de dados ele irá carregar os dados de forma sequencial conforme forem recebidos. Este é o tratamento ideal quando formos usar dados do tipo binário como os BLOB.

Usando SequentialAccess

A utilização do SequentialAccess permite que o DataReader carregue os dados como um fluxo de bytes (stream).

Ao definir a utilização do SequentialAccess é importante notar a sequência na qual você irá acessar os campos que forem retornados.

O comportamento padrão do DataReader permite que você acesse os campos retornados em qualquer ordem até que a próxima linha seja lida. Quando você for usar o SequentialAccess você precisa acessar os diferentes campos retornados pelo DataReader na ordem correta.

Ao usar um DataReader, se você for acessar os dados de forma sequencial (1o campo, 2o campo, etc...) , poderá aumentar o desempenho usando um commandBeheavior.SequentialAccess quando for utilizar o ExecuteReader. Fazendo assim o DataReader é avisado de que o acesso é sequencial e isso otimiza o acesso.
Os valores de CommandBehavior são usados pelo método ExecuteReader da interface IDbCommand e qualquer classe derivada dela. Exemplo de uso:


dr = cmd.ExecuteReader(CommandBehavior.SequentialAccess)

Assim se a sua consulta retorna 3 colunas e a terceira é um campo do tipo BLOB você deve retornar os valores do primeiro e do segundo campos antes de acessar os dados binários da terceira coluna. De outra forma os valores dos campos se tornam indisponíveis. Este comportamento é devido a alteração que o SequentialAcess realizou no DataReader para retornar os dados na sequência.

Quando acessar os dados em um campo do tipo BLOB você deve usar os tipos de acessos GetBytes ou GetChars que preenchem um array com dados.

Você até pode usar um GetString para dados do tipo caractere porém para conservar os recursos do sistema você não pode carregar um valor BLOB em um variável string. Você pode definir um buffer de dados com um determinado tamanho a ser retornado e iniciar a leitura partir do primeiro byte ou caractere a ser lido. GetBytes e GetChars retornam um valor do tipo long que representa o número de bytes ou caracteres retornados.Você também pode definir um índice no array indicando a posição a ordem dos dados a serem lidos.

Escrevendo BLOBS

Para escrever dados binários para o banco de dados você deve usar o comando SQL apropriado INSERT ou UPDATE e passar o valor do BLOB com um parâmetro de entrada. Se o seu BLOB esta armazenado como um texto(documento) você pode passar o BLOB como um parâmetro String. Se o BLOB esta armazenado no formato binário como uma imagem você pode passar um array de bytes como um parâmetro binário.

Abaixo temos um esquema simplificado do fluxo de atividades realizado para ler/gravar dados binário do tipo Image;

Vejamos a seguir um exemplo que inclui informação sobre funcionários na tabela Funcionarios do banco de dados que iremos criar. (Baseada na tabela Employee do banco de dados Northwind)

A estrutura da tabela é data a seguir:(No menu Data Selecione Add New Data Source)

Uma foto do funcionário é lida a partir do arquivo e incluída no campo Foto na tabela que é um campo do tipo Image.

O campo Foto é então lido usando GetBybtes.  Note que o código do funcionário é acessado para a linha atual antes da foto pois os campos precisam ser acessados de forma sequencial e na ordem.

Criando uma aplicação Windows Forms

Vamos criar uma aplicação Windows Forms usando o Visual C#  Express Edition 2008 para incluir e acessar dados de uma tabela do SQL Server Express Edition.

Abra o Visual C# 2008 Express e crie uma nova aplicação Windows Application com o nome de usandoDadosBinarios;

A seguir inclua a partir da ToolBox os controles :

O leiaute do formulário deve ser parecido com o da imagem abaixo:

Antes de iniciar devemos declarar os namespaces :

using System.Data.SqlClient;
using System.IO;

Devemos também definir as variáveis globais usadas no projeto; foto armazena o caminho da foto e conSQL a string de conexão obtida no construtor.

private string foto = null;
private SqlConnection
conSQL;

O construtor do formulário esta definido da seguinte forma:

public Form1()
{

InitializeComponent();

conSQL = new SqlConnection("Data Source=.\\SQLEXPRESS;AttachDbFilename='C:\\Dados\\Cadastro.mdf';Integrated Security=True;Connect Timeout=30;User Instance=True");

atualizaGrid();

}

Vejamos agora o código relacionado a cada evento Click dos botões de comando:

1- Botão Carrega Foto:

void TbnCarregaFotoClick(object sender, EventArgs e)
{
            OpenFileDialog dlg = new OpenFileDialog();

            dlg.Title = "Abrir Foto";
            dlg.Filter = "JPG (*.jpg)|*.jpg"
                + "|All files (*.*)|*.*";

            if (dlg.ShowDialog() == DialogResult.OK)
            {
                try
                {
                    picFoto.Image = new Bitmap(dlg.OpenFile());
                    foto = dlg.FileName;
                }
                catch (Exception ex)
                {
                    MessageBox.Show("Não foi possivel carregar a foto: " + ex.Message);
                }
            }

            dlg.Dispose();
}

Neste código abrimos uma caixa de diálogo OpenFileDialog para selecionar o arquivo a ser exibido no controle PictureBox - picFoto, conforme a figura acima mostra. Essa rotina é usada para escolher uma foto do funcionário e em seguida informar seus dados para inclusão na base de dados.

2- Botão Incluir Dados

void BtnIncluirDadosClick(object sender, EventArgs e)
{

               String nome = txtNome.Text;
              
String endereco = txtEndereco.Text;
              
String cargo = txtCargo.Text;
              
DateTime admissao = DateTime.
Now;

                IncluiFuncionario(nome,endereco,cargo,admissao,foto);

                barraStatus.Text = "Funcionário incluído no banco de dados";
}

Este código obtém os valores informados no formulário e chama a rotina para incluir um novo funcionário na tabela;

3- Botão Selecionar

void BtnSelecionarClick(object sender, EventArgs e)
{
	if (txtNome.Text.Equals(""))
	{
	    MessageBox.Show("Informe o nome do funcionário.");
	    return;
	}
	else
	{
	    GetFuncionario(txtNome.Text);
	}
			
            barraStatus.Text = "Arquivo Salvo no sistema";
}

Neste código vamos selecionar um funcionário através de seu nome  usando a rotina getFuncionario();

Primeiro vamos incluir um botão com o texto Limpar para limpar os campos do formulário, desta forma o usuário poderá informar  o nome do funcionário que deseja obter.

A rotina getFuncionario e dada a seguir:

 // **** Le o BLOB a partir do banco de dados e salva no sistema de arquivos
        public void getFuncionario(string nome)
        {
            SqlCommand getFunci = new SqlCommand(
                                   "SELECT funciID, foto " +
                                   "FROM Funcionarios " +
                                   "WHERE nome = @Nome ", conSQL);

            getFunci.Parameters.Add("@Nome", SqlDbType.NVarChar, 50).Value = nome;

            FileStream fs;                                  // Escreve o BLOB para o arquivo (*.bmp).
            BinaryWriter bw;                                // Define um Streams para o objeto 
            int tamanhoBuffer = 100;                        // Tamanho do buffer do BLOB
            byte[] byteSaida = new byte[tamanhoBuffer];     // o buffer BLOB byte[] para ser preenchido com GetBytes.
            long retorno;                                   // Os bytes retornados de GetBytes.
            long inicioIndice = 0;                          // A posicao inicial no BLOB de saida
            string funci_id = "";                           // O codigo do funcionario em uso no arquivo

            // Abre a conexão e le os dados no DataReader.
            conSQL.Open();
            SqlDataReader mReader = getFunci.ExecuteReader(CommandBehavior.SequentialAccess);

            while (mReader.Read())
            {
                // Obtem o codigo que precisa existir antes de pegar o funcionario
                funci_id = mReader.GetInt32(0).ToString();

                // Cria o arquivo para tratar a saida dos dados
                fs = new FileStream("funcionario" + funci_id + ".jpg", FileMode.OpenOrCreate, FileAccess.Write);
                bw = new BinaryWriter(fs);

                // Reseta o byte de inicio para o novo BLOB.
                inicioIndice = 0;

                // Le os bytes no byteSaida[] e retem o numero de bytes retornados
                retorno = mReader.GetBytes(1, inicioIndice, byteSaida, 0, tamanhoBuffer);

                // Continua lendo e escrevendo enquanto existir bytes ate completar o tamanho do buffer
                while (retorno == tamanhoBuffer)
                {
                    bw.Write(byteSaida);
                    bw.Flush();

                    //Reposiciona o inidice de inicio para o fim ultimo buffer e preenche o buffer
                    inicioIndice += tamanhoBuffer;
                    retorno = mReader.GetBytes(1, inicioIndice, byteSaida, 0, tamanhoBuffer);
                }
                // Escreve o restante do buffer
                bw.Write(byteSaida, 0, (int)retorno);
                bw.Flush();

               // fecha o arquivo de saida
                bw.Close();
                fs.Close();
            }

            // fecha o datareader e a conexao
            mReader.Close();
            conSQL.Close();
        }

 

Para incluir os dados de um funcionário primeiro você deve escolher a sua foto clicando no botão Carregar Foto e em seguida clicar em Incluir Dados:

 

  // Le a imagem do arquivo de sistema e inclui no banco de dados
        public void IncluiFuncionario(
            string pnome,
            string pendereco,
            string pcargo,
            DateTime padmissao,
            string pcaminhoFoto)
        {

            // Le a imagem em um array de bytes do arquivo de sistemas
            byte[] fotoImagem = GetFoto(pcaminhoFoto);

            // MOnta a instrucao SQL
            SqlCommand incFunci = new SqlCommand(
                "INSERT INTO Funcionarios (" +
                "nome,endereco,cargo,admissao,foto) " +
                "VALUES(@nome,@endereco,@cargo,@admissao,@foto)", conSQL);

            incFunci.Parameters.Add("@nome", SqlDbType.NVarChar, 50).Value = pnome;
            incFunci.Parameters.Add("@endereco", SqlDbType.NVarChar, 50).Value = pendereco;
            incFunci.Parameters.Add("@cargo", SqlDbType.NVarChar, 50).Value = pcargo;
            incFunci.Parameters.Add("@admissao", SqlDbType.DateTime).Value = padmissao;
            incFunci.Parameters.Add("@foto", SqlDbType.Image, foto.Length).Value = fotoImagem;
            // Abre a conexao e inclui o BLOB na tabela
            try
            {
                conSQL.Open();
                incFunci.ExecuteNonQuery();
            }
            catch (Exception ex)
            {
                MessageBox.Show("Erro ao incluir dados : " + ex.Message);
            }
            finally
            {
                conSQL.Close();
            }
        }
 

Temos que definir a rotina auxiliar GetFoto() conforme abaixo; ela retorna um array de bytes;

// **** Le a imagem em um array de bytes a partir do sistema de arquivos

public static byte[] GetFoto(string caminhoArquivoFoto)

{

    FileStream fs = new FileStream(caminhoArquivoFoto, FileMode.Open, FileAccess.Read);

   BinaryReader br = new BinaryReader(fs);

    byte[] foto = br.ReadBytes((int)fs.Length);

   br.Close();

   fs.Close();

   return foto;

}

 

Note que a rotina getFuncionario apenas gera uma imagem em disco a partir do banco de dados . Como exercício que tal obter a imagem e exibir no controle PictureBox. (dica : use a propriedade Image do controle. Ex: picFoto.Image = Bitmap.FromStream(arquivo))

Se você desejar pode colocar um DataGridView no projeto para exibir os funcionários já cadastrados. A rotina para ler os dados e exibir no Grid é dada a seguir:

public void atualizaGrid()
        {
            //define a instrução SQL
            string strSql = "SELECT nome, endereco, cargo FROM Funcionarios";

            //cria o objeto command para executar a instruçao sql
            SqlCommand cmd = new SqlCommand(strSql, conSQL);

            //abre a conexao
            conSQL.Open();

            //define o tipo do comando 
            cmd.CommandType = CommandType.Text;
            //cria um dataadapter
            SqlDataAdapter da = new SqlDataAdapter(cmd);

            //cria um objeto datatable
            DataTable funci = new DataTable();
            try
            {
                //preenche o datatable via dataadapter
                da.Fill(funci);
                //atribui o datatable ao datagridview para exibir o resultado
                gdvFunci.DataSource = funci;
            }
            catch (Exception ex)
            {
                MessageBox.Show("Erro ao exibir dados " + ex.Message);
            }
            finally
            {
                conSQL.Close();
            }
        }

Pegue o projeto completo aqui: usandoDadosBinarios.zip (Sem a base de dados e com código de teste comentado que você pode excluir ou aproveitar)

Eu pretendo voltar a este assunto com mais detalhes em um exemplo usando ASP .NET.

Eu sei é apenas C# mas eu gosto...


José Carlos Macoratti