实体框架包含指令未获取所有预期的相关行

时间:2018-10-10 17:59:33

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

在调试一些性能问题时,我发现Entity Framework正在通过延迟加载来加载很多记录(900个额外的查询调用并不很快!),但是我确定我有正确的include。我已经设法将其简化为一个很小的测试用例,以证明我的困惑,实际用例更加复杂,因此我没有太多的余地来重新设计我所使用的签名。我正在做,但希望这是我遇到的问题的明确例子。

文档具有许多与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

我希望它具有所有“文档/作者”对,并具有所有“文档MetatInfo”属性。

不会发生这种情况,我得到了Document对象,而Authors也很好,但是Documents MetaInfo属性仅包含名称==“ Author”的MetaInfo对象

如果将where子句从select many中移出,它将执行相同的操作,除非在实现之后将其移至(虽然这可能没什么大不了,但它在实际的应用程序中,因为这意味着我们在得到的数据量比我们想要处理的要多。)

在尝试了许多不同的方法后,我认为问题确实出在您执行select(... new ...)以及where和include时。执行select或实现后的Where子句使数据按我期望的方式显示。

我认为这是文档的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处于打开状态,它只是从视图中隐藏了,并且有点奇怪的组类型操作?

1 个答案:

答案 0 :(得分:1)

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

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

因此,var 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仅返回所需的原始数据,使用简单的填充代码或使用与IQueryable和EF配合得很好并且可以处理大多数此类延后情况的Automapper组成视图模型。

例如:

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()处理.ProjectTo<TViewModel>()填充。

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