2015-04-15 16 views
5

Es ist sehr üblich in unseren Webanwendungen, Daten aus einer Vielzahl von Tabellen in unserer Datenbank zu benötigen. Heute können 5 oder 6 Datenbankabfragen seriell für eine einzelne Anfrage ausgeführt werden. Keine dieser Abfragen hängt von den Daten der anderen ab, so dass sie perfekt geeignet sind, parallel ausgeführt zu werden. Das Problem ist das bekannte DbConcurrencyException, das ausgelöst wird, wenn mehrere Abfragen für denselben Kontext ausgeführt werden.Parallelität und das Entity Framework

Wir verwenden normalerweise einen einzelnen Kontext pro Anfrage und haben dann eine Repository-Klasse, so dass wir Abfragen über verschiedene Projekte hinweg wiederverwenden können. Wir verfügen dann über den Kontext am Ende der Anfrage, wenn der Controller entsorgt wird.

Unten ist ein Beispiel, das Parallelität verwendet, aber es gibt immer noch ein Problem!

Jedes Repository erstellt intern seinen eigenen Kontext, aber wie es jetzt ist, werden sie nicht entsorgt. Das ist ein Problem. Ich weiß, dass ich Dispose für jedes Repository, das ich erstelle, aufrufen kann, aber das fängt an, den Code schnell zu verwirren. Ich könnte eine Wrapper-Funktion für jede Abfrage erstellen, die ihren eigenen Kontext verwendet, aber das fühlt sich chaotisch an und ist keine langfristige Lösung für das Problem.

Was wäre der beste Weg, um dieses Problem anzugehen? Ich möchte, dass sich der Kunde/Konsument nicht darum kümmern muss, jedes Repository/jeden Kontext zu entsorgen, wenn mehrere Abfragen parallel ausgeführt werden.

Die einzige Idee, die ich gerade habe, ist eine Herangehensweise ähnlich einem Fabrikmuster, nur dass meine Fabrik alle Objekte, die sie erstellt hat, im Auge behält. Ich könnte dann die Fabrik entsorgen, sobald ich weiß, dass meine Abfragen beendet sind und die Fabrik jedes Repository/jeden Kontext intern entsorgen könnte.

Ich bin überrascht, dass so wenig Diskussion um Parallelität und der Entity Framework, um zu sehen, so hoffentlich einige weitere Ideen aus der Community in kommen.

bearbeiten

Hier ist ein einfaches Beispiel dafür, was Unser Repository sieht folgendermaßen aus:

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

Wie Sie sehen können, erhält jedes Repository seinen eigenen Kontext. Dies bedeutet, dass jedes Repository entsorgt werden muss. In dem Beispiel, das ich oben angegeben habe, würde ich 4 Anrufe zu Dispose() benötigen.

Meine Gedanken für eine Fabrik Annäherung an das Problem war so etwas wie die folgenden:

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 
} 

Diese Fabrik würde für die Erstellung von Instanzen von meinem Repository verantwortlich sein, aber es würde auch den Überblick über alle Instanzen halten es hat erstellt. Sobald diese einzelne Fabrikklasse entsorgt wird, ist sie intern für die Entsorgung jedes von ihr erstellten Repositories verantwortlich.

+0

Ich glaube, Ein EF-Kontext muss nicht entsorgt werden, wenn Sie die Verbindung nicht manuell verwalten. Es sollte für jede Anfrage geöffnet und geschlossen werden. Kontexte nicht anzuordnen, erscheint mir jedoch als dreckiger Ansatz. – usr

+0

@usr Wir haben Code von jemand anderem in der Produktion geschrieben, der nicht über alle Kontexte verfügt. :) Es funktioniert, aber ich bin mir nicht sicher, was die Konsequenzen sind oder sein werden. Da der Kontext "IDisposable" implementiert, möchte ich einen Ansatz entwickeln, der das Mysterium dessen, was passieren könnte, entfernt. –

+1

_ "Was wäre der beste Weg, um dieses Problem anzugehen?" _ - Ich denke, dass Sie Ihre Einwände gegen mögliche Ansätze, die Sie bereits identifiziert haben, genauer definieren müssen. Etwas besser als "chaotisch" und "Krempel". Tatsache ist, dass Kapselung eine übliche und gültige Technik zum Verbergen von "Unordentlichem" und "Durcheinander" ist, und ein Wrapper irgendeiner Art ist eine Form der Kapselung. Ohne weitere Details werden Sie nur vage, eigensinnige Antworten erhalten. –

Antwort

1

Sie können Clients das Verhalten zum Verwerfen von Repository konfigurieren, indem Sie eine Art optionales (false standardmäßig) autodispose-Bit an den Konstruktor übergeben. etwa wie folgt aussehen Eine Implementierung würde:

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

Hinweis: Ich hielt die Entsorgung Logik einfach zur Veranschaulichung; Möglicherweise müssen Sie Ihre disposed Bits zurück in arbeiten.

Ihre Abfragemethoden sind immer noch einfache Einzeiler, und der Kunde kann sehr leicht das Entsorgungsverhalten nach Bedarf konfiguriert und sogar eine Repository-Instanz in Auto-Entsorgung Situationen wiederverwenden:

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

Dies ist eine gute Idee und es war zunächst, wie ich dachte, ich könnte dieses Problem lösen, aber es gibt Zeiten, wo ich nicht den Kontext haben kann, weil der Datensatz aktualisiert werden muss. Es gibt viele Situationen, in denen wir den vorhandenen Datensatz von der Datenbank abfragen, das Objekt mit den neuen Daten vom Benutzer aktualisieren und diese Änderungen in die Datenbank übernehmen. Wenn der Kontext entsorgt wird, schlägt die Aktualisierung fehl. –

+0

Ok, es klingt so, als müsste das Verhalten konfigurierbar sein, und basierend darauf habe ich meine Antwort neu geschrieben. Ich denke immer noch nicht, dass hier eine Fabrikklasse benötigt wird. –

+0

Das sieht gut aus! Ich bin mir nicht sicher, warum ich nicht daran gedacht habe, dieses Bit zum Konstruktor zu bewegen. Ich habe dir +1 gegeben und werde es hoffentlich später am Nachmittag oder in der nächsten Woche versuchen. –