Entity Framework 核心 select 导致查询过多

Entity Framework core select causes too many queries

我有以下方法,用于构建单个对象实例,其中它的属性是通过递归调用相同方法构建的:

public ChannelObjectModel GetChannelObject(Guid id, Guid crmId)
    {
        var result = (from channelObject in _channelObjectRepository.Get(x => x.Id == id)
                      select new ChannelObjectModel
                      {
                          Id = channelObject.Id,
                          Name = channelObject.Name,
                          ChannelId = channelObject.ChannelId,
                          ParentObjectId = channelObject.ParentObjectId,
                          TypeId = channelObject.TypeId,
                          ChannelObjectType = channelObject.ChannelObjectTypeId.HasValue ? GetChannelObject(channelObject.ChannelObjectTypeId.Value, crmId) : null,
                          ChannelObjectSearchType = channelObject.ChannelObjectSearchTypeId.HasValue ? GetChannelObject(channelObject.ChannelObjectSearchTypeId.Value, crmId) : null,
                          ChannelObjectSupportingObject = channelObject.ChannelObjectSupportingObjectId.HasValue ? GetChannelObject(channelObject.ChannelObjectSupportingObjectId.Value, crmId) : null,
                          Mapping = _channelObjectMappingRepository.Get().Where(mapping => mapping.ChannelObjectId == channelObject.Id && mapping.CrmId == crmId).Select(mapping => new ChannelObjectMappingModel
                          {
                              CrmObjectId = mapping.CrmObjectId
                          }).ToList(),
                          Fields = _channelObjectRepository.Get().Where(x => x.ParentObjectId == id).Select(field => GetChannelObject(field.Id, crmId)).ToList()
                      }
                     );
        return result.First();
    }

public class ChannelObjectModel
{
    public ChannelObjectModel()
    {
        Mapping = new List<ChannelObjectMappingModel>();
        Fields = new List<ChannelObjectModel>();
    }
    public Guid Id { get; set; }
    public Guid ChannelId { get; set; }
    public string Name { get; set; }
    public List<ChannelObjectMappingModel> Mapping { get; set; }
    public int TypeId { get; set; }
    public Guid? ParentObjectId { get; set; }
    public ChannelObjectModel ParentObject { get; set; }
    public List<ChannelObjectModel> Fields { get; set; }
    public Guid? ChannelObjectTypeId { get; set; }
    public ChannelObjectModel ChannelObjectType { get; set; }
    public Guid? ChannelObjectSearchTypeId { get; set; }
    public ChannelObjectModel ChannelObjectSearchType { get; set; }
    public Guid? ChannelObjectSupportingObjectId { get; set; }
    public ChannelObjectModel ChannelObjectSupportingObject { get; set; }
}

这是使用 Entity Framework Core 2.1.1

连接到 SQL 数据库

虽然它在技术上可行,但它会导致进行大量数据库查询 - 我意识到这是因为 ToList() 和 First() 等调用。

然而,由于对象的性质,我可以用 from.... select new {...} 制作一个巨大的 IQueryable<anonymous> 对象并在其上调用 First,但代码超过 300 行在层次结构中只深入 5 层,所以我试图用上面的代码替换它,它更干净,尽管速度慢得多..

ChannelObjectType, ChannelObjectSearchType, ChannelObjectSupportingObject

都是 ChannelObjectModel 个实例,字段是 ChannelObjectModel 个实例的列表。

查询目前执行大约需要 30 秒,这太慢了,而且它也在一个小的本地主机数据库上,所以随着 db 记录数量的增加,它只会变得更糟,并生成大量数据库当我 运行 它时调用它。

300 多行代码生成的查询少得多,而且相当快,但显然是可怕的代码(我没有写!)

任何人都可以建议一种方法,我可以用与上述方法类似的方式递归构建对象,但大大减少了数据库调用的次数,因此速度更快吗?

我使用 EF6,而不是 Core,但据我所知,同样的事情也适用于此。

首先,将此函数移至您的存储库,以便所有调用共享 DbContext 实例。

其次,在您的 DbSet 属性上使用 Include 来预先加载它们:

ctx.DbSet<ChannelObjectModel>()
     .Include(x => x.Fields)
     .Include(x => x.Mapping)
     .Include(x => x.ParentObject) 
     ...

好的做法是使它成为调用例如 BuildChannelObject() 的上下文(或扩展方法)的函数,它应该 return IQueryable - 只是包含。

然后就可以开始递归部分了:

public ChannelObjectModel GetChannelObjectModel(Guid id)
{
    var set = ctx.BuildChannelObject(); // ctx is this

    var channelModel = set.FirstOrDefault(x => x.Id == id); // this loads the first level

    LoadRecursive(channelModel, set);

    return channelModel;
}

private void LoadRecursive(ChannelObjectModel c, IQueryable<ChannelObjectModel> set)
{
     if(c == null)
         return; // recursion end condition

     c.ParentObject = set.FirstOrDefault(x => x.Id == c?.ParentObject.Id);
    // all other properties

     LoadRecursive(c.ParentObject, set);
    // all other properties
}

如果所有这些代码都使用同一个 DbContext 实例,它应该会很快。如果没有,你可以使用另一个技巧:

ctx.DbSet<ChannelObjectModel>().BuildChannelObjectModel().Load();

这会将所有对象加载到 DbContext 的内存缓存中。不幸的是,它随着上下文实例而死,但它使那些递归调用更快,因为没有进行数据库访问。

如果这仍然很慢,您可以添加 AsNoTracking() 作为 BuildChannelObjectModel() 的最后一条指令。

如果这仍然很慢,只需实现这些对象的应用程序范围的内存缓存并使用它而不是每次都查询数据库 - 如果您的应用程序是一项可以长时间启动但随后运行速度很快的服务,这将非常有用。

另一种方法是通过将导航属性标记为虚拟来启用延迟加载 - 但请记住,returned 类型将派生类型匿名代理,而不是您的原始 ChannelObjectModel!此外,只有在您不处理上下文的情况下,属性才会加载 - 之后您会遇到异常。使用上下文加载所有属性然后 return 完整对象也有点棘手 - 最简单(但不是最好!)的方法是将对象序列化为 JSON(记住循环引用) 在 return 之前。

如果这不能满足您的要求,请切换到 nHibernate,我听说它默认具有应用程序范围的缓存。