7

Betrachten Sie diese erfundene Entitätsobjekten:Entfernen wählen N + 1 ohne .INCLUDE

public class Consumer 
{ 
    public int Id { get; set; } 
    public string Name { get; set; } 
    public bool NeedsProcessed { get; set; } 
    public virtual IList<Purchase> Purchases { get; set; } //virtual so EF can lazy-load 
} 

public class Purchase 
{ 
    public int Id { get; set; } 
    public decimal TotalCost { get; set; } 
    public int ConsumerId { get; set; } 
} 

Lassen Sie uns jetzt sagen, dass ich diesen Code ausführen möchten:

var consumers = Consumers.Where(consumer => consumer.NeedsProcessed); 

//assume that ProcessConsumers accesses the Consumer.Purchases property 
SomeExternalServiceICannotModify.ProcessConsumers(consumers); 

standardmäßig diese von Select N leiden + 1 in der ProcessConsumers-Methode. Es wird eine Abfrage ausgelöst werden, wenn sie die Verbraucher aufzählt, dann wird es jede Einkauf Sammlung 1 von 1. Die Standardlösung für dieses Problem einer Include hinzuzufügen wäre zu greifen:

var consumers = Consumers.Include("Purchases").Where(consumer => consumer.NeedsProcessed); 

//assume that ProcessConsumers accesses the Consumer.Purchases property 
SomeExternalServiceICannotModify.ProcessConsumers(consumers); 

dass in vielen Fällen gut funktioniert, aber in einigen komplexen Fällen kann ein Include die Leistung um Größenordnungen zerstören. Ist es möglich, so etwas zu tun.

  1. Schnappen meine Verbraucher, var Verbraucher = _entityContext.Consumers.Where (...) ToList()
  2. meine Einkäufe Schnappen, var Käufe = _entityContext.Purchases. Wo (...). ToList()
  3. Hydratisieren Sie den Consumer.Purchases Sammlungen manuell von den Einkäufen, die ich bereits in den Speicher geladen. Wenn ich es dann an ProcessConsumers übergebe, wird es nicht mehr Datenbankabfragen auslösen.

Ich bin mir nicht sicher, wie man # 3 macht. Wenn Sie versuchen, auf eine Consumer-Sammlung zuzugreifen, die die Lazy Load (und damit die Select N + 1) auslöst. Vielleicht muss ich die Consumers in den richtigen Typ (anstelle des EF-Proxy-Typs) und laden Sie dann die Sammlung? Etwas wie folgt aus:

foreach (var consumer in Consumers) 
{ 
    //since the EF proxy overrides the Purchases property, this doesn't really work, I'm trying to figure out what would 
    ((Consumer)consumer).Purchases = purchases.Where(x => x.ConsumerId = consumer.ConsumerId).ToList(); 
} 

EDIT: Ich habe das Beispiel ein wenig neu geschrieben hoffentlich, um das Problem deutlicher zu offenbaren.

+1

IIRC EF wird automatisch die Sammlungen hydratisieren, so # 3 muss nicht manuell getan werden. – jeroenh

+1

Ihre erste Abfrage sollte als einzelne SQL-Anweisung ausgeführt werden. Sehen Sie mehrere db-Aufrufe? –

+0

@Nicholas, du hast recht, ich habe das Beispiel aktualisiert, um Select N + 1 zu machen. Dies ist ein sehr einfaches erfundenes Beispiel, lies die ganze Frage und versuche zu verstehen, was ich wirklich frage. Tatsächliche Beispiele, bei denen .Include nicht ausreicht, sind dramatisch komplexer und nicht sinnvoll, um eine SO-Frage zu stellen. – manu08

Antwort

0

EF wird bevölkern die consumer.Purchases Kollektionen für Sie, wenn Sie den gleichen Kontext verwenden, um beide Sammlungen zu holen:

List<Consumer> consumers = null; 
using (var ctx = new XXXEntities()) 
{ 
    consumers = ctx.Consumers.Where(...).ToList(); 

    // EF will populate consumers.Purchases when it loads these objects 
    ctx.Purchases.Where(...).ToList(); 
} 

// the Purchase objects are now in the consumer.Purchases collections 
var sum = consumers.Sum(c => c.Purchases.Sum(p => p.TotalCost)); 

EDIT:

Diese in nur 2 db Anrufe Ergebnisse: 1 zu erhalten die Sammlung von Consumers und 1, um die Sammlung von Purchases zu erhalten.

EF wird sich jeden Purchase Datensatz ansehen und den entsprechenden Consumer Datensatz von Purchase.ConsumerId nachschlagen. Es fügt dann das Purchase Objekt der Consumer.Purchases Sammlung für Sie hinzu.


Option 2:

Wenn es aus irgendeinem Grund Sie zwei Listen aus verschiedenen Kontexten holen wollen, ist und diese miteinander verknüpfen, würde ich eine andere Eigenschaft auf die Consumer Klasse hinzufügen:

partial class Consumer 
{ 
    public List<Purchase> UI_Purchases { get; set; } 
} 

Sie können diese Eigenschaft dann aus der Purchases-Auflistung festlegen und in Ihrer Benutzeroberfläche verwenden.

+0

Option 1 ist eine Neuformulierung dessen, was ich oben habe, oder? Es wird immer noch n + 1 auswählen. Option 2 ist vernünftig, aber für meinen Fall wird es nicht wirklich funktionieren. Ich werde meine ursprüngliche Frage aktualisieren. – manu08

+0

Nein, es ergibt nur 2 db Anrufe. Ich habe meiner Antwort mehr Erklärung gegeben. –

0

Schnappen meine Verbraucher

var consumers = _entityContext.Consumers 
           .Where(consumer => consumer.Id > 1000) 
           .ToList(); 

meine Einkäufe Schnappen

var purchases = consumers.Select(x => new { 
             Id = x.Id, 
             IList<Purchases> Purchases = x.Purchases   
             }) 
         .ToList() 
         .GroupBy(x => x.Id) 
         .Select(x => x.Aggregate((merged, next) => merged.Merge(next))) 
         .ToList(); 

Hydratisieren Sie den Consumer.Purchases Sammlungen von den Käufen, die ich bereits in den Speicher geladen.

for(int i = 0; i < costumers.Lenght; i++) 
    costumers[i].Purchases = purchases[i]; 
+0

Ich glaube, die linke Hälfte von, Verbraucher [i] .Käufe = Käufe [i], wird EF auslösen, um zu versuchen, Käufe für Sie zu laden. Sie werden also die Lazy Load auslösen und dann überschreiben. – manu08

+0

Sie können das Objektdiagramm einfach vom Kontext trennen oder die Funktion zum verzögerten Laden deaktivieren, bevor Sie Schritt 3 ausführen. –

+0

@MortenMertner In meinem Fall kann ich das verzögerte Laden nicht vollständig deaktivieren, da ich andere Dinge später laden muss. Ich werde herumspielen, um das Objekt zu lösen, manuell zu laden und dann das Objekt wieder anzubringen. Danke für die Idee. – manu08

0

Wäre es nicht möglich sein, für Sie, indem Sie die Arbeit an der Datenbank rund um die vielen-Rundreisen-oder-ineffizient-Abfrage-Generation Problem arbeiten - im Wesentlichen durch einen Vorsprung anstelle eines bestimmten Rückkehr Einheit, wie unten gezeigt:

var query = from c in db.Consumers 
      where c.Id > 1000 
      select new { Consumer = c, Total = c.Purchases.Sum(p => p.TotalCost) }; 
var total = query.Sum(cp => cp.Total); 

ich bin kein EF-Experte mit allen Mitteln, so verzeiht mir, wenn diese Technik nicht geeignet ist.

+0

Diese Technik ist gut für das erfundene Beispiel, aber ich bitte Sie, anzunehmen, dass es viel komplizierter ist (z. B. müssen Sie viele andere Dinge mit Konsumenten und ihren Käufen tun, so dass Sie das gesamte Objektdiagramm im Speicher haben wollen). – manu08

+0

Nehmen Sie an, dass mein Beispiel gleichermaßen erfunden ist, indem ich nur den Gesamtbetrag aus den Käufen zurückgebe. Sie können leicht Sammlungen von verwandten Objekten zurückgeben, die für die Dinge, die Sie tun müssen, ausgewählt wurden. Wählen Sie die Daten in einer benutzerdefinierten Projektionsklasse (d. H. ConsumerPurchasesForYearlyReportData oder einige solche) und verwenden Sie diese als Ihren Ausgangspunkt für die folgende Arbeit. ORMs im Allgemeinen (und insbesondere EF) sind nicht großartig, um gefilterte Assoziationen zurückzugeben. –

1

Wenn ich richtig verstehe, möchten Sie sowohl eine gefilterte Teilmenge der Verbraucher jeweils mit einer gefilterten Teilmenge ihrer Einkäufe in 1 Abfrage laden. Wenn das nicht stimmt, verzeih mir bitte, dass ich deine Absicht verstanden habe. Wenn das richtig ist, könnten Sie so etwas wie:

var consumersAndPurchases = db.Consumers.Where(...) 
    .Select(c => new { 
     Consumer = c, 
     RelevantPurchases = c.Purchases.Where(...) 
    }) 
    .AsNoTracking() 
    .ToList(); // loads in 1 query 

// this should be OK because we did AsNoTracking() 
consumersAndPurchases.ForEach(t => t.Consumer.Purchases = t.RelevantPurchases); 

CannotModify.Process(consumersAndPurchases.Select(t => t.Consumer)); 

Beachten Sie, dass diese Arbeit nicht, wenn die Prozessfunktion erwartet, dass die Verbraucher Objekt zu ändern und diese Änderungen dann in die Datenbank zurück begehen.

+0

Dies verhindert Änderungsverfolgung, aber ich glaube, t.Consumer.Purchases wird immer noch eine Lazy Load auslösen, bevor t.RelevantPurchases darüber speichern. – manu08

+0

Ich bin überrascht, dass das passieren würde. Wenn dies jedoch geschieht, können Sie (nach dem Aufruf von ToList()) hinzufügen: .Wählen Sie (t => neuer Verbraucher {/ * kopieren Sie alle relevanten Felder aus t.Consumer * /, Einkäufe = t.RelevantePurchases})(). Dies sollte effektiv alles in Nicht-EF-Proxy-Objekte abbilden, die kein lazy loading ausführen sollten. Eine andere Option besteht darin, die Käufe-Eigenschaft nicht virtuell zu machen, obwohl dies auch ein Lazy-Laden an anderer Stelle verhindert. – ChaseMedallion