7

Considere estos entidad artificial objetos:Extracción de selección de N + 1 sin .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; } 
} 

Ahora vamos a decir que desea ejecutar este código:

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

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

Por defecto, este sufrirá de Selección N + 1 dentro del método ProcessConsumers. Que dará lugar a una consulta cuando enumera los consumidores, entonces va a agarrar cada colección compras 1 por 1. La solución estándar para este problema sería añadir un include:

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

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

Eso funciona bien en muchos casos, pero en algunos casos complejos, una inclusión puede destruir por completo el rendimiento en órdenes de magnitud. ¿Es posible hacer algo como esto:.

  1. agarrar mis consumidores, consumidores var = _entityContext.Consumers.Where (...) ToList()
  2. agarrar mis compras, compras var = _entityContext.Purchases. Dónde (...). ToList()
  3. Hidrata al consumidor. Compra colecciones manualmente de las compras que ya cargué en la memoria. Luego, cuando lo paso a ProcessConsumers, no generará más consultas de db.

No sé cómo hacer # 3. Si intenta acceder a cualquier consumidor. Compra una colección que activará la carga diferida (y, por lo tanto, la Select N + 1). Tal vez deba enviar los consumidores al tipo correcto (en lugar del tipo de proxy EF) y luego cargar la colección? Algo como esto:

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: he vuelto a escribir un poco el ejemplo de esperar para revelar el problema con mayor claridad.

+1

IIRC EF hidratará automáticamente las colecciones, por lo que el n. ° 3 no tiene que hacerse manualmente. – jeroenh

+1

Su primera consulta debería ejecutarse como una sola instrucción de SQL. ¿Estás viendo múltiples llamadas a db? –

+0

@Nicholas, tienes razón, actualicé el ejemplo para hacerlo Seleccionar N + 1. Este es un ejemplo artificial muy simple, lea toda la pregunta e intente comprender lo que realmente estoy preguntando. Los ejemplos reales donde .Include es insuficiente son dramáticamente más complejos y no es razonable incluirlos en una pregunta de SO. – manu08

Respuesta

0

EF rellenará los consumer.Purchases colecciones para usted, si se utiliza el mismo contexto para obtener ambas colecciones:

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:

Esto se traduce en sólo 2 llamadas DB: 1 para obtener la colección de Consumers y 1 para obtener la colección de Purchases.

EF observará cada registro Purchase devuelto y buscará el registro correspondiente Consumer de Purchase.ConsumerId. A continuación, agregará el objeto Purchase a la colección Consumer.Purchases.


Opción 2:

Si hay alguna razón que se desea obtener dos listas de diferentes contextos y luego vincularlos, me gustaría añadir otra propiedad a la clase Consumer:

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

A continuación, puede configurar esta propiedad desde la colección Purchases y utilizarla en su UI.

+0

La opción 1 es una reformulación de lo que tengo arriba, ¿verdad? Todavía tendrá seleccionar n + 1. La opción 2 es razonable, pero realmente no funcionará en mi caso. Actualizaré mi pregunta original para elaborar. – manu08

+0

No, resulta en solo 2 llamadas a bases de datos. He agregado más explicación a mi respuesta. –

0

Coge mis consumidores

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

Coge mis compras

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

Hidratar al consumidor.Compra colecciones manualmente de las compras que ya he cargado en la memoria.

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

Creo que la mitad izquierda de, los consumidores [i] .Compras = compras [i], hará que EF intente cargar las compras por usted. Entonces activará la carga lenta y luego la sobrescribirá. – manu08

+0

Puede separar el gráfico de objeto del contexto o deshabilitar la capacidad de carga diferida, antes de hacer el paso 3. –

+0

@MortenMertner En mi caso, no puedo deshabilitar por completo la carga diferida porque necesito cargar otras cosas perezosas más tarde. Voy a jugar con separar el objeto, cargarlo manualmente y luego volver a colocar el objeto. Gracias por la idea – manu08

0

¿No sería posible para que pueda trabajar alrededor de los muchos-ida y vuelta-o-ineficiente-consulta generación problema haciendo el trabajo en la base de datos - esencialmente mediante la devolución de una proyección en lugar de un particular, entidad, como se demuestra a continuación:

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

no soy un experto EF por cualquier medio, así que perdónenme si esta técnica no es adecuada.

+0

Esa técnica está bien para el ejemplo artificial, pero le pido que suponga que es mucho más complicado (por ejemplo, necesita hacer muchas otras cosas con los consumidores y sus compras, por lo que quiere que todo el gráfico objeto en la memoria). – manu08

+0

Supongamos que mi ejemplo es igualmente artificial, ya que simplemente devuelvo el total de las compras. Puede devolver fácilmente colecciones de objetos relacionados seleccionados a mano para las cosas que necesita hacer. Seleccione los datos en una clase de proyección personalizada (es decir, ConsumerPurchasesForYearlyReportData o algo así) y utilícelos como su semilla para el siguiente trabajo. Los ORM en general (y EF en particular) no son buenos para devolver asociaciones filtradas. –

1

Si entiendo correctamente, le gustaría cargar un subconjunto filtrado de consumidores cada uno con un subconjunto filtrado de sus compras en 1 consulta. Si eso no es correcto, perdone mi comprensión de su intención. Si eso es correcto, usted podría hacer algo como:

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

Tenga en cuenta que esto no funcionará si la función de proceso está esperando para modificar el objeto de consumo y luego confirmar esos cambios de nuevo a la base de datos.

+0

Esto evitará el seguimiento de cambios, pero creo que t.Consumer.Purchases todavía activará una carga lenta, antes de guardar t.Compras relevantes sobre ella. – manu08

+0

Estoy sorprendido de que eso suceda. Sin embargo, si eso está sucediendo, podría agregar (después de la llamada ToList()):. Seleccione (t => nuevo Consumidor {/ * copie todos los campos relevantes de t.Consumidor * /, Compras = t.Compras Importantes}).(). Eso debería efectivamente mapear todo en objetos proxy no EF, que no deberían hacer ninguna carga lenta. Otra opción es hacer que la propiedad de Compras no sea virtual, aunque esto también evitará la carga diferida en otro lugar. – ChaseMedallion

Cuestiones relacionadas