删除不带 的选定 N+1.包括

本文关键字:N+1 包括 删除 | 更新日期: 2023-09-27 18:36:44

考虑这些人为的实体对象:

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

现在假设我想运行这段代码:

var consumers = Consumers.Where(consumer => consumer.NeedsProcessed);
//assume that ProcessConsumers accesses the Consumer.Purchases property
SomeExternalServiceICannotModify.ProcessConsumers(consumers);

默认情况下,这将受到 ProcessConsumers 方法中的 Select N+1 的影响。 它将在枚举消费者时触发查询,然后逐一获取每个购买集合。 此问题的标准解决方案是添加包含:

var consumers = Consumers.Include("Purchases").Where(consumer => consumer.NeedsProcessed);
//assume that ProcessConsumers accesses the Consumer.Purchases property
SomeExternalServiceICannotModify.ProcessConsumers(consumers);

这在许多情况下都很好用,但在某些复杂的情况下,包含可能会完全破坏性能几个数量级。 是否可以做这样的事情:

  1. 抓住我的消费者,var 消费者 = _entityContext.Consumer.Where(...)。到列表()
  2. 抓住我的购买,var 购买 = _entityContext.Purchases.Where(...)。到列表()
  3. 为消费者补充水分。从已加载到内存中的购买中手动购买集合。 然后,当我将其传递给ProcessConsumer时,它不会触发更多的数据库查询。

我不确定如何做#3。 如果您尝试访问任何消费者。购买将触发延迟加载的集合(因此选择 N+1)。 也许我需要将使用者强制转换为正确的类型(而不是 EF 代理类型),然后加载集合? 像这样:

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

编辑:我已经重写了这个例子,希望能更清楚地揭示这个问题。

删除不带 的选定 N+1.包括

如果我

理解正确,您希望在 1 个查询中加载两个过滤的消费者子集,每个子集都有一个过滤的购买子集。如果这不正确,请原谅我对你的意图的理解。如果这是正确的,您可以执行以下操作:

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

请注意,如果 Process 函数希望修改使用者对象,然后将这些更改提交回数据库,则此操作不起作用。

抓住我的消费者

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

抢购商品

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

为消费者补充水分。手动购买集合 我已经加载到内存中的购买。

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

您是否不可能通过在数据库上执行工作来解决多次往返或低效查询生成问题 - 基本上是通过返回投影而不是特定实体,如下所示:

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 );
无论如何,我

都不是 EF 专家,所以如果这种技术不合适,请原谅我。

如果您使用相同的上下文提取这两个集合,EF 将为您填充consumer.Purchases集合:

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

编辑:

这只导致 2 个数据库调用:1 个用于获取Consumers集合,1 个用于获取Purchases集合。

EF 将查看返回的每个Purchase记录,并从Purchase.ConsumerId中查找相应的Consumer记录。然后,它会将Purchase对象添加到Consumer.Purchases集合中。


选项 2:

如果出于某种原因,您想从不同的上下文中获取两个列表,然后链接它们,我会向Consumer类添加另一个属性:

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

然后,可以从 Purchases 集合中设置此属性,并在 UI 中使用它。