2012-01-23 10 views
5

Para un proyecto asp.Net MVC, necesitaré manejar archivos grandes (principalmente 200-300Mo, a veces 1Go).Aplicación en capas: Almacene el archivo en filestream en la base de datos

Los almacenaré en la base de datos (por razones de seguridad/razón de coherencia).

Estoy preocupado por el problema de rendimiento, por lo que quiero evitar todo lo que pueda tener una matriz de bytes en cualquier parte del programa, el objetivo es trabajar con la transmisión en todas partes.

Tengo una aplicación en capas, que en su mayoría significa que tengo varios "DataStore", que son responsables de conectar y recuperar/insertar/actualizar los datos de la base de datos.

Dado que EF no es compatible con Filestream por ahora, estoy manejando la "parte del archivo" a través de simples solicitudes Sql. He leído un buen artículo sobre el uso filestream aquí: http://blog.tallan.com/2011/08/22/using-sqlfilestream-with-c-to-access-sql-server-filestream-data/

Y tengo algunas preguntas adicionales, que espero que me puedan ayudar/señalarme a la buena dirección:

  • desde que he una aplicación en capas, una vez que haya instanciado mi objeto SQLFileStream, ¿podría disponer de SqlCommand/Sql Connection/Transaction scope?
  • Si no, ¿cómo se supone que debo cerrarlos?
  • En el enlace anterior, hay un ejemplo que muestra cómo usarlo con ASP. Pero dado que estoy usando ASP.Net MVC, ¿no hay un ayudante que pueda transmitir directamente un archivo al navegador? Porque encontré muchos ejemplos de devolución de datos binarios al navegador, pero por ahora, todos los ejemplos que encontré hacen básicamente algo como Stream.ToArray() para llenar una matriz de bytes y devolverlos al navegador. Descubrí que puedo devolver un FileStreamResult que puede incluir el parámetro a Stream. ¿Es esa la dirección correcta?

(No estoy preocupado actualmente por la posibilidad de subir archivos de gran tamaño, ya que se insertan por una pesada cliente en la base de datos)

EDITAR

(Lo siento por el código sucio, es sólo para no tener 50 métodos diferentes aquí. He intentado un poco más, y actualmente estoy atascado con la parte "leer", debido a la parte separada (donde generamos la capa y donde la consumimos):

 SqlConnection conn = GetConnection(); 
     conn.Open(); 
     SqlCommand cmd = new SqlCommand(_selectMetaDataRequest, conn); 
     cmd.Parameters.Add(_idFile, SqlDbType.Int).Value = idFile; 
     SqlDataReader rdr = cmd.ExecuteReader(); 
     rdr.Read(); 
     string serverPath = rdr.GetSqlString(0).Value; 
     byte[] serverTxn = rdr.GetSqlBinary(1).Value; 
     rdr.Close(); 
     return new SqlFileStream(serverPath, serverTxn, FileAccess.Read); 

Pero recibo una excepción en rdr.GetSqlBinary(1).Value porque GET_FILESTREAM_TRANSACTION_CONTEXT devuelve nulo. Encontré here que esto se debe a la transacción faltante.

Intenté con un "TransactionScope" + su llamada .Complete();. No cambia nada

Se ha intentado realizar un BEGIN TRANSACTION que observamos en el enlace anterior:

 SqlConnection connection = GetConnection(); 
     connection.Open(); 
     SqlCommand cmd = new SqlCommand(); 
 cmd.CommandText = "BEGIN TRANSACTION"; 
     cmd.CommandType = CommandType.Text; 
     cmd.Connection = connection; 
     cmd.ExecuteNonQuery(); 

     cmd = new SqlCommand(_selectMetaDataRequest, connection); 
     cmd.Parameters.Add(_idFile, SqlDbType.Int).Value = idFile; 
     SqlDataReader rdr = cmd.ExecuteReader(); 
     rdr.Read(); 
     string serverPath = rdr.GetSqlString(0).Value; 
     byte[] serverTxn = rdr.GetSqlBinary(1).Value; 
     rdr.Close(); 
     SqlFileStream sqlFileStream = new SqlFileStream(serverPath, serverTxn, FileAccess.Read); 
 cmd = new SqlCommand(); 
     cmd.CommandText = "COMMIT TRANSACTION"; 
     cmd.CommandType = CommandType.Text; 
     cmd.Connection = connection; 
     cmd.ExecuteNonQuery(); 

Pero se estrella en el primer "ExecuteNonQuery" con el excepción "A transaction that was started in a MARS batch is still active at the end of the batch. The transaction is rolled back." ¡Pero es la PRIMERA consulta ejecutada!

+0

Yo diría que esta es una tarea más adecuada para los sistemas de archivos, ya que se trata de archivos. Suponiendo que el sitio/base de datos se implementa en una máquina de Windows, una alternativa para mirar podría ser Transactional NTFS. Incluso debería integrarse con otras transacciones utilizando el DTM. Es cierto que hay algunos gotcha allí (por ejemplo, mal soporte de archivos compartidos). +1 por no usar byte [] :) –

Respuesta

7

Veamos un ejemplo. Podríamos comenzar por definir un contrato que describa la operación que estamos dispuestos a realizar:

public interface IPhotosRepository 
{ 
    void GetPhoto(int photoId, Stream output); 
} 

Veremos la implementación más adelante.

Ahora podemos definir el resultado de acciones personalizadas:

public class PhotoResult : FileResult 
{ 
    private readonly Action<int, Stream> _fetchPhoto; 
    private readonly int _photoId; 
    public PhotoResult(int photoId, Action<int, Stream> fetchPhoto, string contentType): base(contentType) 
    { 
     _photoId = photoId; 
     _fetchPhoto = fetchPhoto; 
    } 

    protected override void WriteFile(HttpResponseBase response) 
    { 
     _fetchPhoto(_photoId, response.OutputStream); 
    } 
} 

continuación, un controlador que nos permitirá mostrar la foto:

public class HomeController : Controller 
{ 
    private readonly IPhotosRepository _repository; 

    public HomeController(IPhotosRepository repository) 
    { 
     _repository = repository; 
    } 

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

    public ActionResult Photo(int photoId) 
    { 
     return new PhotoResult(photoId, _repository.GetPhoto, "image/jpg"); 
    } 
} 

y una vista correspondiente en el que vamos a mostrar la foto en una etiqueta <img> utilizando el Photo acción:

<img src="@Url.Action("photo", new { photoid = 123 })" alt="" /> 

Ahora la última parte es, por supuesto, la implementación del repositorio, que podría ser algo a lo largo de las líneas de:

public class PhotosRepositorySql : IPhotosRepository 
{ 
    private readonly string _connectionString; 
    public PhotosRepositorySql(string connectionString) 
    { 
     _connectionString = connectionString; 
    } 

    public void GetPhoto(int photoId, Stream output) 
    { 
     using (var ts = new TransactionScope()) 
     using (var conn = new SqlConnection(_connectionString)) 
     using (var cmd = conn.CreateCommand()) 
     { 
      conn.Open(); 
      cmd.CommandText = 
      @" 
       SELECT 
        Photo.PathName() as path, 
        GET_FILESTREAM_TRANSACTION_CONTEXT() as txnToken 
       FROM 
        PhotoAlbum 
       WHERE 
        PhotoId = @PhotoId 
      "; 
      cmd.Parameters.AddWithValue("@PhotoId", photoId); 
      using (var reader = cmd.ExecuteReader()) 
      { 
       if (reader.Read()) 
       { 
        var path = reader.GetString(reader.GetOrdinal("path")); 
        var txnToken = reader.GetSqlBinary(reader.GetOrdinal("txnToken")).Value; 
        using (var stream = new SqlFileStream(path, txnToken, FileAccess.Read)) 
        { 
         stream.CopyTo(output); 
        } 
       } 
      } 
      ts.Complete(); 
     } 
    }  
} 

Todo lo que queda ahora es dar instrucciones a su marco DI favorito para utilizar PhotosRepositorySql.

Esta técnica le permite trabajar eficientemente con archivos grandes arbitrarios ya que nunca carga toda la secuencia en la memoria.

+0

¡Oh! Buena idea usar un controlador HTTP personalizado. No sabía antes del delegado de Acción <...>, ¡también es una buena idea! ¡Voy a intentarlo ahora mismo! – J4N

+0

@ J4N, no estoy usando un controlador HTTP personalizado. Estoy usando un resultado de acción personalizado. –

+0

Sí, discúlpeme por la confusión. ¡Terminé de probarlo y funciona bien! Acabo de agregar el "Nombre de archivo" al resultado de la acción. ¡Muchas gracias por esto, salvaste mi día! – J4N

-1

Una base de datos SQL funciona muy mal con archivos de gran tamaño, y también hay un límite de tamaño de registro. Recomiendo usar un databse NoSQL (como MongoDB) para almacenar esos archivos enormes. puede usar dos bases de datos, (SQL para datos simples, NoSQL para archivos) no hay problema.

Sé que esta no es la respuesta que desea, pero es la mejor manera de tener un buen rendimiento.

+2

Pregunta sobre el tipo de datos FILESREAM de SQL Server 2008 que está específicamente diseñado para este propósito. –

+0

Lo siento, pero Sql Server 2008 es una restricción, no puedo usar ningún otro tipo de base de datos. Y Filestream está diseñado para almacenar archivos de gran tamaño, solo administra "punteros" a archivos reales – J4N

0

Comprobar esta respuesta para un ejemplo de la descarga con éxito archivos de hasta 4 GB: https://stackoverflow.com/a/3363015/234415

Un par de puntos interesantes para recordar sobre el uso de Filestream:

  • Los archivos no cuentan como parte de la límite de tamaño (por lo que funciona muy bien con SQLEXPRESS que tiene un límite de 10 GB para la versión SQL 2008 R2. Así que puede tener 10 GB de datos y terabytes de archivos, todo gratis, lo cual es genial.
  • TIENE que usar s integrado ecurity (MSDN), no es compatible con el inicio de sesión de SQL. ¡Esto es molesto si quieres usar el alojamiento compartido!
+0

Gracias por su respuesta, pero realmente no me ayuda a aplicar capas a mi aplicación. Para otras restricciones, tendremos una edición estándar de SQL Server por cierto – J4N

Cuestiones relacionadas