2012-01-23 12 views
5

Per un progetto MVC asp.Net, avrò bisogno di gestire file di grandi dimensioni (principalmente 200-300 mo, a volte 1 go).Applicazione sovrapposta: Archivia file in filestream nel database

Li memorizzerò nel database (per motivi di backup/motivo di coerenza).

Sono preoccupato per il problema di prestazioni, quindi voglio evitare tutto il possibile per avere una serie di byte in qualsiasi punto del programma, l'obiettivo è quindi quello di lavorare con streaming ovunque.

Ho un'applicazione a livelli, che significa principalmente che ho diversi "DataStore", che sono responsabili per la connessione e il recupero/inserimento/aggiornamento dei dati dal database.

Poiché EF non supporta Filestream per ora, sto gestendo la "parte del file" tramite semplici richieste Sql. Ho letto un buon articolo su utilizzo filestream qui: http://blog.tallan.com/2011/08/22/using-sqlfilestream-with-c-to-access-sql-server-filestream-data/

E ho alcune domande supplementari, che spero che mi può aiutare/punto me il buon senso:

  • dal momento che ho un'applicazione a livelli, una volta che ho istanziato il mio oggetto SQLFileStream, potrei disporre di SqlCommand/Sql Connection/Transaction scope?
  • In caso contrario, come dovrei chiuderli?
  • Nel collegamento precedente, c'è un esempio che mostra come usarlo con ASP. Ma dal momento che sto usando ASP.Net MVC, non c'è un helper che sia direttamente in grado di trasmettere un file al browser? Perché ho trovato molti esempi di dati binari di ritorno al browser, ma per ora, tutti gli esempi che ho trovato rendono fondamentalmente qualcosa come Stream.ToArray() per riempire un array di byte e restituirlo al browser. Ho scoperto che posso restituire un FileStreamResult che può contenere un parametro a Stream. È la giusta direzione?

(io non sto attualmente preoccupazione per il caricamento di file di grandi dimensioni, dal momento che vengono inseriti da un client pesante nel database)

EDIT

(Ci scusiamo per il codice sporca, è solo per non avere qui 50 metodi diversi. Ho fatto qualche altro tentativo e attualmente sono bloccato con la parte "letta", a causa della parte separata (dove generiamo il livello e dove lo consumiamo):

 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); 

Ma ottengo un'eccezione a rdr.GetSqlBinary(1).Value perché GET_FILESTREAM_TRANSACTION_CONTEXT restituisce null. Ho trovato here che questo è dovuto alla transazione mancante.

Ho provato con un "TransactionScope" + la sua chiamata .Complete();. Non cambia nulla

ho cercato di fare un BEGIN TRANSACTION, come mostrato nel link precedente:

 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(); 

Ma si blocca sul primo "ExecuteNonQuery" con il eccezione "A transaction that was started in a MARS batch is still active at the end of the batch. The transaction is rolled back." Ma è la prima query eseguita!

+0

Direi che questo è un compito più adatto ai file system, dal momento che si tratta di file. Supponendo che il sito/database sia distribuito su una macchina Windows, un'alternativa da considerare potrebbe essere Transactional NTFS. Dovrebbe anche integrarsi con altre transazioni usando il DTM. Ovviamente ci sono alcuni gotcha lì (ad esempio, supporto per la condivisione di file non valido). +1 per non utilizzare il byte [] :) –

risposta

7

Facciamo un esempio. Potremmo iniziare con la definizione di un contratto che descriverà l'operazione siamo disposti a svolgere:

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

vedremo l'implementazione in seguito.

Ora potremmo definire di conseguenza un'azione personalizzata:

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); 
    } 
} 

poi un controllore che ci permetterà di mostrare 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"); 
    } 
} 

e una corrispondente vista in cui ci accingiamo a mostrare la foto in un tag <img> utilizzando il Photo azione:

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

Ora l'ultima parte è, naturalmente, l'attuazione del repository che potrebbe apparire qualcosa sulla falsariga di:

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(); 
     } 
    }  
} 

Tutto ciò che resta ora è quello di istruire il quadro DI preferita da usare PhotosRepositorySql.

Questa tecnica consente di lavorare in modo efficiente con file di grandi dimensioni arbitrari poiché non carica mai l'intero flusso in memoria.

+0

Oh! Buona idea usare un gestore HTTP personalizzato. Non lo sapevo prima dell'azione <...> delegato, buona idea anche a me! Ci proverò adesso! – J4N

+0

@ J4N, non sto utilizzando un gestore HTTP personalizzato. Sto usando un risultato di azione personalizzato. –

+0

Sì, mi scusi per la confusione. Ho finito di provarlo e funziona benissimo! Ho appena aggiunto il "Nome file" al risultato dell'azione. Grazie mille per questo, hai salvato la mia giornata! – J4N

-1

Un database SQL funziona molto male con file enormi e anche il limite della dimensione del registro. Si consiglia di utilizzare un databse NoSQL (come MongoDB) per archiviare i file di grandi dimensioni. è possibile utilizzare due database, (SQL per dati semplici, NoSQL per file) senza problemi.

So che questa non è la risposta che si desidera, ma è il modo migliore per avere buone prestazioni.

+2

Chiede informazioni sul tipo di dati FILESREAM di SQL Server 2008 che è specificamente progettato per questo scopo. –

+0

Mi dispiace, ma Sql Server 2008 è un vincolo, non posso usare nessun altro tipo di database. E Filestream è progettato per archiviare file enormi, gestisce solo "puntatore" su file reali – J4N

0

Controllare questa risposta per un esempio di download con successo file fino a 4 GB: https://stackoverflow.com/a/3363015/234415

Un paio di punti interessanti da ricordare sull'utilizzo Filestream:

  • I file non contano come parte del limite di dimensioni (quindi funziona bene con SQLEXPRESS che ha un limite di 10 GB per la versione SQL 2008 R2. Così è possibile avere 10 GB di dati e terabyte di file, tutto gratis, che è cool.
  • Devi utilizzare s integrato ecurity (MSDN), non supporta l'accesso SQL. Questo è fastidioso se si desidera utilizzare l'hosting condiviso!
+0

Grazie per la risposta, ma in realtà non mi aiuta a stratificare la mia applicazione. Per altri vincoli, avremo una versione standard di SQL Server a proposito di – J4N