Salvando e visualizando arquivos em banco de dados - MVC C#

Código para realizar upload de arquivo e salvá-lo em banco de dados, e para visualizar arquivo salvo em banco de dados

Neste post, deixarei bem explícito um trecho de código para realizar upload de arquivo para ser salvo em banco de dados, e  outro trecho para o usuário posteriormente fazer o download do arquivo. Depois, apresentarei sugestões de melhorias. Ao final, farei minhas considerações a respeito de salvar arquivos em banco.

Primeiramente, vou considerar que o arquivo será salvo em uma tabela chamada Arquivo, como mostra a figura abaixo.
 

Figura 1 - tabela Arquivo (MySQL)
 
  • Sabemos que um arquivo é nada mais que uma cadeia de bytes. Na tabela, essa cadeia será armazenado no campo Conteudo (no MySQL, uma sequência de bytes trata-se do tipo BLOB e variações; no SQL Server, usa-se o tipo VARBINARY).
  • O campo Tipo armazena o MIME type do arquivo, necessário na hora de fazer o download do arquivo pelo usuário.
  • O campo Nome é o nome do arquivo com extensão; não é obrigatório, mas acredito que é útil armazenar esta informação. Por exemplo, para evitar problemas de requisição, eu retiro os caracteres especiais do nome do arquivo ("Delação Premiada.pdf" vira "delacao-premiada.pdf") - porque eu já tive esse problema.

Agora, vamos tratar da parte de upload e salvamento do arquivo. Considerando a tabela acima, abaixo exibo o código da View de upload do arquivo, e da Controller que salva o arquivo no banco:
 
@model Arquivo
@{
    ViewBag.Title = "Criar arquivo";
}

@using (Html.BeginForm("Criar", "Arquivo", FormMethod.Post, new { enctype = "multipart/form-data", @class = "form" }))
{
    @Html.ValidationSummary(true)

    <div>
        @Html.LabelFor(model => model.Conteudo)
        @Html.TextBox("ArquivoSelecionado", "", new { type = "file"})
    </div> 

    <input type="submit" value="Salvar" />
}
Código da View para criar arquivo
  

 
        public ActionResult Criar()
        {
            return View();
        } 

        [HttpPost]
        public ActionResult Criar([Bind(Exclude="Conteudo")] Arquivo form, HttpPostedFileBase ArquivoSelecionado)
        {
            try
            {
                if(ModelState.IsValid)
                {
                    // Dados do arquivo upado pelo usuário
                    using (var binaryReader = new BinaryReader(ArquivoSelecionado.InputStream))
                    {
                        form.Conteudo = binaryReader.ReadBytes(ArquivoSelecionado.ContentLength);
                    }
                    form.Tipo = ArquivoSelecionado.ContentType;
                    form.Nome = ArquivoSelecionado.FileName;
                    
                    // Repositorio personalizado acessa o banco, inclue o registro e commita
                    this.Repositorio.Incluir(form);
                    this.Repositorio.Salvar();

                    this.AdicionarMensagemSucesso("Arquivo salvo com sucesso."); // Metodo personalizado para mensagens
                    return RedirectToAction("Index");
                }
            }
            catch(Exception e)
            {
                this.AdicionarMensagemErro("Erro ao salvar: " + e.Message);
            }
            return View();
        }
Código da Controller para salvar o arquivo
 
  • Apesar de haver meu campo Conteudo, eu utilizo um campo ArquivoSelecionado para receber o arquivo do usuário. É que o campo da minha tabela é somente uma cadeia de bytes, enquanto ArquivoSelecionado receberá várias informações que não quero perder e com as quais vou preencher meu registro.
  • Faço um Bind Exclude do meu campo Conteudo porque ele é obrigatório na minha Model; se eu não fizer isso, serei obrigada a passar um valor para Conteudo no formulário - e neste caso eu vou preenchê-lo na Controller.
  • Não esqueça de verificar o tamanho máximo de upload que sua aplicação pode receber e configurar isso no seu Web.config. Se seu usuário enviar um arquivo maior que o permitido, a requisição irá devolver a exceção "Maximum request length exceeded".
(...)

 <system.web>

    <!-- Tamanho máximo de upload aceito (20MB no exemplo) -->
    <httpRuntime targetFramework="4.5" maxRequestLength="20480" />

(...)


Simples assim. E para depois exibir o arquivo para o usuário, basta eu criar uma Controller para recuperar o arquivo do banco e retornar um File contendo a cadeia de bytes, MIME type e nome do arquivo:
 
        public ActionResult Download(int id)
        {
                Arquivo arquivo = this.Repository.Obter(id);
                return File(arquivo.Conteudo, arquivo.Tipo, arquivo.Nome);
        }
Código para visualização/download do arquivo


Melhorias

1. Tamanho máximo de arquivo para upload - Como existe um limite máximo de tamanho de arquivo para upload na aplicação, é bom avisar seu usuário se ele estiver enviando um arquivo maior que este limite. Isto é possível usando jquery:
 
var maximoMB = 10; // tem que estar igual a  WebConfig.MaxUploadSizeMB

$(document).ready(function () {
    $('#ArquivoSelecionado').change(function (e) {
        verificaTamanhoMB(this);
    });
});

function verificaTamanhoMB(file) {
    bytes = $(file).prop("files")[0].size;
    mb = (bytes / (1024 * 1000)).toFixed(2); // .toFixed(2) para arredondar duas casas
    if (mb > maximoMB) {
        alert("Este arquivo não poderá ser adicionado:\nUpload máximo é de " + maximoMB + " MB,\ne este arquivo possui " + mb + " MB.");
        $(file).val('');
    }
}

Você pode também fazer com que maximoMB receba seu valor diretamente do Web.config, o que é o certo a se fazer. O trecho de código abaixo, retirado de https://stackoverflow.com/questions/14248515/how-to-read-system-value-from-web-config-and-use-in-asp-net-mvc-c-sharp-method faz isso pra você:
 
int maxRequestLength = 0;
HttpRuntimeSection section =
ConfigurationManager.GetSection("system.web/httpRuntime") as HttpRuntimeSection;
if (section != null) 
    maxRequestLength = section.MaxRequestLength;

2. Miniatura de imagem ao sleecionar arquivo - Caso seu usuário esteja enviando uma imagem, é interessante exibir uma miniatura da mesma quando o arquivo for selecionado. Acrescentamos então um local para exibir a miniatura no formulário, e no jquery tratamos para exibir a miniatura caso o arquivo seja uma imagem:
 
    <!-- Adicionado abaixo de Html.TextBox("ArquivoSelecionado", "", new { type = "file"}): -->   
    <div style="border:1px solid #F7E9E9; margin-top:10px; margin-bottom:10px">
        <i>Prévia (para imagens):</i>
        <img id="previa" src="" style="max-width:100px; max-height:100px; display:block; margin:10px" />
    </div>
$(document).ready(function () {
    $('#ArquivoSelecionado').change(function (e) {
        exibirMiniatura(this);
    });
});

function exibirMiniatura(input) {

    // Apenas se for imagem
    var mime = $(input).prop("files")[0].type;
    if (mime.match("^image")) { // se o MIME começa com "image"
        var reader = new FileReader();
        reader.onload = function (e) {
            $('img#previa').attr('src', e.target.result);
        }
        reader.readAsDataURL(input.files[0]);

        $('img#previa').show();
    }
    else {
        $('img#previa').attr('src', "");
        $('img#previa').hide();
    }
}


Considerações finais

Sobre salvar arquivos em banco

A vantagem é ter todos os dados do seu sistema centralizados, facilitando consultas, backups e transferências.

A desvantagem é que arquivos são cadeias de bytes e por isso são pesados se comparados com outros tipos de dados. Não vai demorar pro seu banco de dados ganhar tamanho. “Select *” com arquivos no meio nem pensar, ou você irá sentar o seu banco. É preciso sabedoria para saber quais arquivos salvar, como armazenar e como consulta-los depois.

Quando eu preciso buscar um conjunto de arquivos para listar, por exemplo, eu costumo usar um POCO (uma versão simplificada do objeto) de Arquivo e preenche-lo somente com os dados que eu vou precisar, evitar acessar a coluna dos bytes e melhorando consideravelmente meu tempo de resposta.
 
        var registros = this.Repositorio.Select(r =>
                new ArquivoPoco
                    {
                        IdArquivo = r.IdArquivo,
                        Titulo = r.Titulo,
                        DtCriacao = r.DtCriacao,
                        ArqNome = r.ArqNome,
                        ArqTipo = r.ArqTipo,
                        IsImagem = r.IsImagem
                    }
                ).ToList();

Então, na minha opinião, é bom salvar arquivos em banco apenas se for algo esporádico ou para poucos registros finais na tabela.
 
Arquivos em tabela exclusiva

Particularmente, eu prefiro colocar os arquivos do sistema em uma tabela separada (que eu chamo de Arquivo), e depois eu construo as relações com outras tabelas do meu banco. Exemplo: se eu tenho uma tabela Usuario e quero guardar uma foto de cada usuário, as fotos ficam em Arquivo, e eu coloco um campo IdArquivoFoto na tabela Usuario. Motivos pra isso: (1) trabalhar livremente na minha tabela de usuários, sem o peso dos bytes de dados de arquivos atrapalhando minhas consultas, e (2) centralização dos meus arquivos na tabela Arquivo.

 
Obrigada pela visita!


 

0 comentários

Envie seu comentário