2015-04-15 4 views
5

È molto comune nelle nostre applicazioni Web avere bisogno di dati da una varietà di tabelle nel nostro database. Oggi è possibile trovare 5 o 6 query di database eseguite in serie per una singola richiesta. Nessuna di queste query dipende dai dati dell'altra, quindi sono candidati perfetti per essere eseguiti in parallelo. Il problema è il noto DbConcurrencyException che viene generato quando più query vengono eseguite sullo stesso contesto.Parallelismo e Entity Framework

Generalmente utilizziamo un singolo contesto per richiesta e quindi abbiamo una classe di repository in modo da poter riutilizzare le query tra vari progetti. Quindi smaltiamo il contesto alla fine della richiesta quando il controller viene smaltito.

Di seguito è riportato un esempio che utilizza il parallelismo, ma c'è ancora un problema!

var fileTask = new Repository().GetFile(id); 
var filesTask = new Repository().GetAllFiles(); 
var productsTask = AllProducts(); 
var versionsTask = new Repository().GetVersions(); 
var termsTask = new Repository().GetTerms(); 

await Task.WhenAll(fileTask, filesTask, productsTask, versionsTask, termsTask); 

Ogni repository crea internamente proprio contesto, ma come è ora, essi non vengono smaltiti. Questo é un problema. So che potrei chiamare Dispose su ogni repository che creo, ma che inizia a ingombrare il codice rapidamente. Potrei creare una funzione wrapper per ogni query che utilizza il proprio contesto, ma che si sente disordinato e non è una soluzione a lungo termine per il problema.

Quale sarebbe il modo migliore per risolvere questo problema? Vorrei che il cliente/consumatore non dovesse preoccuparsi di smaltire ogni repository/contesto nel caso in cui più query fossero eseguite in parallelo.

L'unica idea che ho adesso è quella di seguire un approccio simile a un modello di fabbrica, tranne che la mia fabbrica avrebbe tenuto traccia di tutti gli oggetti creati. Potrei quindi disporre della fabbrica una volta che avrò finito di sapere che le mie domande sono finite e che la fabbrica potrebbe disporre internamente di ogni repository/contesto.

Sono sorpreso di vedere così poco discussione intorno parallelismo e Entity Framework, quindi speriamo che qualche idea in più da parte della comunità verranno in.

Modifica

Ecco un semplice esempio di ciò che il nostro repository appare come:

public class Repository : IDisposable { 
    public Repository() { 
     this.context = new Context(); 
     this.context.Configuration.LazyLoadingEnabled = false; 
    } 

    public async Task<File> GetFile(int id) { 
     return await this.context.Files.FirstOrDefaultAsync(f => f.Id == id); 
    } 

    private bool disposed = false; 

    protected virtual void Dispose(bool disposing) { 
     if (!this.disposed) { 
      if (disposing) { 
       context.Dispose(); 
      } 
     } 
     this.disposed = true; 
    } 

    public void Dispose() { 
     Dispose(true); 
     GC.SuppressFinalize(this); 
    } 
} 

Come si può vedere, ogni repository ha il proprio contesto. Ciò significa che ogni repository deve essere eliminato. Nell'esempio che ho dato sopra, ciò significa che avrei bisogno di 4 chiamate allo Dispose().

I miei pensieri per un approccio fabbrica per il problema era simile al seguente:

public class RepositoryFactory : IDisposable { 
    private List<IRepository> repositories; 

    public RepositoryFactory() { 
     this.repositories = new List<IRepository>(); 
    } 

    public IRepository CreateRepository() { 
     var repo = new Repository(); 
     this.repositories.Add(repo); 
     return repo;    
    } 

    #region Dispose 
    private bool disposed = false; 

    protected virtual void Dispose(bool disposing) { 
     if (!this.disposed) { 
      if (disposing) { 
       foreach (var repo in repositories) { 
        repo.Dispose(); 
       } 
      } 
     } 
     this.disposed = true; 
    } 

    public void Dispose() { 
     Dispose(true); 
     GC.SuppressFinalize(this); 
    } 
    #endregion 
} 

Questa fabbrica sarebbe responsabile della creazione di istanze di mio repository, ma sarebbe anche tenere traccia di tutte le istanze che ha creato. Una volta che questa singola classe factory è stata eliminata, sarebbe internamente responsabile dello smaltimento di ogni repository creato.

+0

credo un contesto EF non ha bisogno di essere smaltito se non si gestisce la connessione manualmente. Dovrebbe aprire e chiudere per ogni richiesta. Tuttavia, i contesti non disposti mi sembrano un approccio sporco. – usr

+0

@usr Abbiamo codice scritto da qualcun altro in produzione che non elimina tutti i contesti. :) Funziona, ma non sono sicuro su quali saranno o saranno le conseguenze. Dal momento che il contesto implementa 'IDisposable', mi piacerebbe sviluppare un approccio che rimuova il mistero di ciò che potrebbe accadere. –

+1

_ "Quale sarebbe il modo migliore per affrontare questo problema?" _ - Penso che dovrai essere più specifico riguardo alle tue obiezioni ai possibili approcci che hai già identificato. Qualcosa di meglio di "disordinato" e "disordine". Il fatto è che l'incapsulamento è una tecnica comune e valida per nascondere "disordinato" e "ingombrante", e un involucro di qualche tipo è una forma di incapsulamento. Senza ulteriori dettagli, tutto quello che otterrete sono risposte vaghe e supponenti. –

risposta

1

Si potrebbe consentire ai clienti di configurare il comportamento smaltimento dei Repository passando una sorta di optional (false di default) autodispose po 'al costruttore. Un'implementazione sarebbe simile a questa:

public class Repository : IDisposable 
{ 
    private readonly bool _autodispose = false; 
    private readonly Lazy<Context> _context = new Lazy<Context>(CreateContext); 

    public Repository(bool autodispose = false) { 
     _autodispose = autodispose; 
    } 

    public Task<File> GetFile(int id) { 
     // public query methods are still one-liners 
     return WithContext(c => c.Files.FirstOrDefaultAsync(f => f.Id == id)); 
    } 

    private async Task<T> WithContext<T>(Func<Context, Task<T>> func) { 
     if (_autodispose) { 
      using (var c = CreateContext()) { 
       return await func(c); 
      } 
     } 
     else { 
      return await func(_context.Value); 
     } 
    } 

    private static Context CreateContext() { 
     var c = new Context(); 
     c.Configuration.LazyLoadingEnabled = false; 
     return c; 
    } 

    public void Dispose() { 
     if (_context.IsValueCreated) 
      _context.Value.Dispose(); 
    } 
} 

Nota: ho mantenuto la logica di smaltimento semplice per l'illustrazione; potrebbe essere necessario rielaborare i tuoi bit disposed.

I suoi metodi di query sono ancora semplici battute, e il cliente può facilmente configurare il comportamento di smaltimento, se necessario, e persino ri-utilizzare un'istanza repository in situazioni di auto-smaltimento:

var repo = new Repository(autodispose: true); 
var fileTask = repo.GetFile(id); 
var filesTask = repo.GetAllFiles(); 
var productsTask = AllProducts(); 
var versionsTask = repo.GetVersions(); 
var termsTask = repo.GetTerms(); 

await Task.WhenAll(fileTask, filesTask, productsTask, versionsTask, termsTask); 
+0

Questa è una buona idea ed è stato inizialmente come pensavo di poter risolvere questo problema, ma ci sono dei momenti in cui non posso disporre del contesto perché il record deve essere aggiornato. Esistono molte situazioni in cui interroghiamo il record esistente dal database, aggiorniamo l'oggetto con i nuovi dati dell'utente e trasferiamo tali modifiche al database. Se il contesto è disposto, l'aggiornamento fallirebbe. –

+0

Ok, sembra che tu abbia bisogno che il comportamento sia configurabile, e sulla base di quello ho riscritto la mia risposta. Continuo a non pensare che ci sia bisogno di una classe di fabbrica qui. –

+0

Questo sembra buono! Non sono sicuro del motivo per cui non ho pensato di spostare quel pezzo verso il costruttore. Ti ho dato un +1 e spero di fare un tentativo più avanti nel pomeriggio o la prossima settimana. –