【问题标题】:Entity Framework Include directive not getting all expected related rows实体框架包含指令未获取所有预期的相关行
【发布时间】:2018-10-10 17:59:33
【问题描述】:

在调试一些性能问题时,我发现 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 属性只有 Name == "Author" 的 MetaInfo 对象

如果我将 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

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

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

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

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

【问题讨论】:

    标签: c# .net entity-framework entity-framework-6


    【解决方案1】:

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

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

    所以虽然var docs = context.Documents.Include(d => d.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();
    

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

    .Select() 人口可以由 Automapper 使用 .ProjectTo&lt;TViewModel&gt;() 处理。

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

    【讨论】:

    • 这确实会导致“正确”的结果,但在可能有数千行并且我们正在尝试通过 skip().take() 进行分页的实际情况下,这不是一个可行的解决方案.到目前为止,我想出的最好办法是只获取文档的 ID,然后进行第二个查询,通过 ID in 子句使用 MetaInfo 提取文档。
    • 分页意味着这些实体将在其上下文范围之外返回。我建议使用视图模型,而不是返回以避免问题,可能还有其他问题。我在上面的答案中详细说明了这种情况。
    • 是的,这正是我所缺少的,而且我之前也从未在 automapper 中看到过 ProjectTo,作为参考,我已经在存储库中实现了 ProjectTo 版本。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多