首页 > 解决方案 > 实体框架包含指令未获取所有预期的相关行

问题描述

在调试一些性能问题时,我发现 Entity 框架正在通过延迟加载加载大量记录(900 个额外的查询调用并不快!)但我确信我有正确的包含。我已经设法把它归结为一个很小的测试用例来证明我遇到的困惑,实际用例更复杂,所以我没有很大的空间来重新设计我的签名我在做,但希望这是我遇到的问题的一个明显例子。

文档有很多相关的 MetaInfo 行。我想获取按具有特定值的 MetaInfo 行分组的所有文档,但我希望包含所有 MetaInfo 行,因此我不必为所有 Documents MetaInfo 发起新请求。

所以我有以下查询。

ctx.Configuration.LazyLoadingEnabled = false;

var DocsByCreator = ctx.Documents
    .Include(d => d.MetaInfo) // Load all the metaInfo for each object
    .SelectMany(d => d.MetaInfo.Where(m => m.Name == "Author") // For each Author
        .Select(m => new { Doc = d, Creator = m })) // Create an object with the Author and the Document they authored.
    .ToList(); // Actualize the collection

我希望这将包含所有 Document / Author 对,并填充所有 Document MetatInfo 属性。

这不是发生的情况,我得到了 Document 对象,而 Authors 就好了,但是 Documents MetaInfo 属性只有 MetaInfo 对象的 Name == "Author"

如果我将 where 子句从 select many 中移出,它会做同样的事情,除非我在实现之后将它移到(虽然这里可能没什么大不了,但它在实际应用程序中,因为这意味着我们得到了一个巨大的数据量比我们想要处理的要多。)

在使用了很多不同的方法来做到这一点之后,我认为问题在于当你执行 select(...new...) 以及 where 和 include 时。在实现之后执行 select 或 Where 子句会使数据以我期望的方式出现。

我认为这是 Document 的 MetaInfo 属性被过滤的问题,因此我将其重写如下以测试该理论,并惊讶地发现这也给出了相同的(我认为是错误的)结果。

ctx.Configuration.LazyLoadingEnabled = false;

var DocsByCreator = ctx.Meta
    .Where(m => m.Name == "Author")
    .Include(m => m.Document.MetaInfo) // Load all the metaInfo for Document
    .Select(m => new { Doc = m.Document, Creator = m })
    .ToList(); // Actualize the collection

由于我们没有将位置放在 Document.MetaInfo 属性上,我希望这可以绕过问题,但奇怪的是,文档似乎仍然只有“作者”MetaInfo 对象。

我创建了一个简单的测试项目并将其上传到 github,其中包含一堆测试用例,据我所知,它们都应该通过,只有那些过早实现通过的错误。

https://github.com/Robert-Laverick/EFIncludeIssue

有人有任何理论吗?我是否以某种我想念的方式滥用 EF / SQL?有什么我可以做不同的事情来获得相同的结果吗?这是 EF 中的一个错误,它只是被默认情况下打开的 LazyLoad 隐藏在视图之外,并且它有点奇怪的组类型操作?

标签: c#.netentity-frameworkentity-framework-6

解决方案


这是 EF 中的一个限制,如果返回的实体的范围从引入包含的位置更改,则包含将被忽略。

我找不到对 EF6 的引用,但它记录在 EF Core 中。(https://docs.microsoft.com/en-us/ef/core/querying/related-data)(请参阅“忽略包含”)我怀疑这是阻止 EF 的 SQL 生成完全退出的限制某些场景。

因此,whilevar docs = context.Documents.Include(d => d.Metas)将返回对文档加载的 metas;一旦.SelectMany()你改变了 EF 应该返回的内容,那么 Include 语句就会被忽略。

如果您想返回所有文档,并包含作为其作者的属性:

var DocsByCreator = ctx.Documents
    .Include(d => d.MetaInfo)
    .ToList() // Materialize the documents and their Metas.
    .SelectMany(d => d.MetaInfo.Where(m => m.Name == "Author") // For each Author
        .Select(m => new { Doc = d, Creator = m })) // Create an object with the Author and the Document they authored.
    .ToList(); // grab your collection of Doc and Author.

如果您只想要有作者的文档:

var DocsByCreator = ctx.Documents
    .Include(d => d.MetaInfo)
    .Where(d => d.MetaInfo.Any(m => m.Name == "Author")
    .ToList() // Materialize the documents and their Metas.
    .SelectMany(d => d.MetaInfo.Where(m => m.Name == "Author") // For each Author
        .Select(m => new { Doc = d, Creator = m })) // Create an object with the Author and the Document they authored.
    .ToList(); // grab your collection of Doc and Author.

这意味着您需要确保所有过滤逻辑都在第一次'ToList()调用之上完成。或者,您可以考虑在查询后解析 Author 元数据,例如在填充视图模型时,或者解决它的 Document 上未映射的“Author”属性。尽管我通常避免使用未映射的属性,因为如果它们的使用滑入 EF 查询中,则会在运行时出现严重错误。

编辑:基于跳过和采取的要求,我建议使用视图模型来返回数据而不是返回实体。使用视图模型,您可以指示 EF 只返回您需要的原始数据,使用简单的填充代码或使用 Automapper 组合视图模型,它可以很好地与 IQueryable 和 EF 配合使用,并且可以处理大多数这样的延迟情况。

例如:

public class DocumentViewModel
{
    public int DocumentId { get; set; }
    public string Name { get; set; }
    public ICollection<MetaViewModel> Metas { get; set; } = new List<MetaViewModel>();
    [NotMapped]
    public string Author // This could be update to be a Meta, or specialized view model.
    {
        get { return Metas.SingleOrDefault(x => x.Name == "Author")?.Value; }
    }
}

public class MetaViewModel
{
    public int MetaId { get; set; }
    public string Name { get; set; }
    public string Value { get; set; }
}

然后查询:

var viewModels = context.Documents
    .Select(x => new DocumentViewModel
    {
        DocumentId = x.DocumentId,
        Name = x.Name,
        Metas = x.Metas.Select(m => new MetaViewModel
        {
            MetaId = m.MetaId,
            Name = m.Name,
            Value = m.Value
         }).ToList()
    }).Skip(pageNumber*pageSize)
    .Take(PageSize)
    .ToList();

“作者”与文档的关系在数据级别是隐含的,而不是强制的。该解决方案使实体模型对数据表示保持“纯粹”,并让代码处理将隐含关系转换为公开文档作者的处理。

Automapper.Select()可以使用.ProjectTo<TViewModel>().

通过返回视图模型而不是实体,您可以避免此类 .Include() 操作无效的问题,以及由于在不同上下文之间分离和重新附加实体的诱惑而导致的问题,以及通过仅选择和传输来提高性能和资源使用如果您忘记禁用延迟加载或意外的#null 数据,则可以避免延迟加载序列化问题。


推荐阅读