【问题标题】:Entity Framework 6: virtual collections lazy loaded even explicitly loaded on a queryEntity Framework 6:延迟加载的虚拟集合,甚至在查询中显式加载
【发布时间】:2016-12-27 08:38:11
【问题描述】:

我在尝试优化查询时遇到了 EF6 问题。考虑这个类与一个集合:

public class Client
{
    ... a lot of properties
    public virtual List<Country> Countries { get; set; }
}

您可能知道,当 EF 尝试为每个客户端获取所有国家/地区时,我遇到了这个 n+1 问题。

我尝试使用 Linq 投影;例如:

        return _dbContext.Clients
            .Select(client => new
            {
                client,
                client.Countries
            }).ToList().Select(data =>
            {
                data.client.Countries = data.Countries; // Here is the problem
                return data.client;
            }).ToList();

这里我使用了两个选择:第一个用于 Linq 投影,因此 EF 可以创建 SQL,第二个用于将结果映射到客户端类。原因是我使用的是存储库接口,它返回List&lt;Client&gt;

尽管查询是使用其中的国家生成的,但当我尝试呈现整个信息时,EF 仍然使用延迟加载(同样的 n+1 问题)。避免这种情况的唯一方法是删除虚拟访问器:

public class Client
{
    ... a lot of properties
    public List<Country> Countries { get; set; } 
}

我对这个解决方案的问题是我们仍然希望这个属性是虚拟的。这种优化只对应用程序的特定部分是必要的,而在其他部分我们希望有这个延迟加载功能。

我不知道如何“通知” EF 这个属性,它已经通过这个 Linq 投影延迟加载。那可能吗?如果没有,我们还有其他选择吗? n+1 问题使应用程序需要几秒钟才能加载大约 1000 行。

编辑

感谢您的回复。我知道我可以使用Include() 扩展来获取集合,但我的问题是我需要添加一些额外的优化(很抱歉没有发布完整的示例,我认为集合问题就足够了):

public class Client
{
    ... a lot of properties
    public virtual List<Country> Countries { get; set; }
    public virtual List<Action> Actions { get; set; }
    public virtual List<Investment> Investments { get; set; }
    public User LastUpdatedBy {
        get {
          if(Actions != null) {
              return Actions.Last();
          }       
        }
    }
}

如果我需要渲染客户端,关于上次更新和投资数量的信息 (Count()),使用 Include() 我实际上需要从数据库中获取所有信息。但是,如果我使用像

这样的投影
        return _dbContext.Clients
              .Select(client => new
              {
                    client,
                    client.Countries,
                    NumberOfInvestments = client.Investments.Count() // this is translated to an SQL query
                    LastUpdatedBy = client.Audits.OrderByDescending(m => m.Id).FirstOrDefault(),
              }).ToList().Select(data =>
              {
                    // here I map back the data
                    return data.client;
              }).ToList();

我可以减少查询,只获取所需的信息(在 LastUpdatedBy 的情况下,我需要将属性更改为 getter/setter 属性,这不是大问题,因为它仅用于此特定应用程序的一部分)。

如果我通过这种方法(投影然后映射)使用 Select(),则 EF 不会考虑 Include() 部分。

【问题讨论】:

    标签: c# entity-framework linq


    【解决方案1】:

    如果我理解正确,你可以试试这个

    _dbContext.LazyLoading = false;
    
    var clientWithCountres =  _dbContext.Clients
                                        .Include(c=>c.Countries)
                                        .ToList();
    

    这将获取Client,并且只包含Countries。如果您禁用延迟加载,则不会从查询中加载其他集合。除非您指定包含或投影。

    仅供参考:Projection 和 Include() 不能一起使用,请参阅此答案 如果您是投影,它将绕过包含。 https://stackoverflow.com/a/7168225/1876572

    【讨论】:

    • 我编辑了问题以添加更多数据。 Include() 工作正常,但是这个 Clients 对象与几个集合有连接,我不想带整个数据库
    • @jparaya 我已经更新了答案,请看一下
    • 谢谢。如果我们可以使用包含包含的投影,或者以某种方式通知 EF Include() 已经通过投影生成,那就太好了,但我认为这是一个有助于防止延迟加载同步错误的功能。
    • 如果您使用投影,这些集合将自动包含在查询中。 EF smart做到了,只有包含的东西才能被查询。在投影中,您可以包含您的 where 过滤器,其中包括获取所有数据而无需任何位置
    【解决方案2】:

    不知道你想做什么,你使用的是 lambda 表达式而不是 linq,你的第二次选择它是不必要的。

    data.clientclientdata.Countriesclient.Countries,所以 data.client.Countries = data.Countries 总是正确的。

    如果您不想延迟加载Countries,请使用_dbContext.Clients.Include("Countries").Where() or select ()

    【讨论】:

      【解决方案3】:

      为了强制预先加载虚拟属性,您应该使用 Include 扩展方法。 这是 MSDN https://msdn.microsoft.com/en-us/library/jj574232(v=vs.113).aspx 的链接。

      所以这样的事情应该可以工作:

       return _dbContext.Clients.Include(c=>c.Countries).ToList();
      

      【讨论】:

      • 嗯...不起作用。没有 Select 的 Include() 工作正常,但没有这两个选择...
      • 这是错误的。投影中不保留包含,因此上述内容无效。
      • 实际上你不再需要投影以及 2 个后续的 ToList() 了,因为它是错误的触发预加载的方式。
      【解决方案4】:

      我不是 100% 确定,但我认为您的问题是您仍在为您的内部集合维护一个可查询的对象,直到查询结束。

      这个可查询是惰性的(因为在模型中它是惰性的),并且您没有做任何事情来解释这不应该是这种情况,您只是将相同的惰性可查询投影到结果集中。

      我无法告诉你这里的正确答案是什么,但我会尝试以下方法:

      1 在内部可查询对象上也使用投影,例如

      return _dbContext.Clients
                  .Select(client => new
                  {
                      client,
                      Countries = client.Countries.Select(c=>c)// or a new Country
                  })
      

      2 将包含放在查询的末尾(我很确定包含适用于结果而不是输入。如果你把它放在投影之前它肯定不起作用)例如:

      _dbContext.Clients
                  .Select(client => new
                  {
                      client,
                      client.Countries
                  }.Include(c=>c.Countries)`
      

      3 尝试在投影中指定枚举,例如:

      _dbContext.Clients
                  .Select(client => new
                  {
                      client,
                      Countries = client.Countries.AsEnumerable() //perhaps tolist if it works
                  }`
      

      我确实想通过说我没有尝试过上述任何方法来解决这个问题,但我认为这会让你走上正确的道路。

      关于延迟加载的说明

      IMO 很少有延迟加载的好用例。它几乎总是会导致生成太多查询,除非您的用户直接在模型上遵循惰性路径。仅在极其谨慎的情况下使用它,IMO 根本不在请求响应(例如 Web)应用程序中使用它。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-02-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多