Entity Framework Include指令未获取所有预期的相关行



在调试一些性能问题时,我发现Entity框架通过延迟加载加载了大量记录(900个额外的查询调用不快!),但我确信我有正确的include。我已经设法将其归结为一个相当小的测试用例,以证明我所面临的困惑,实际的用例更复杂,所以我没有太多的空间来重新处理我正在做的事情的签名,但希望这是我所面临问题的一个清楚的例子。

文档有许多相关的MetaInfo行。我想获得按具有特定值的MetaInfo行分组的所有文档,但我希望包括所有MetaInfo行,这样我就不必对所有文档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或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属性上放置where,我希望这可以绕过问题,但奇怪的是,文档似乎仍然只有"Author"MetaInfo对象。

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

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

有人有什么理论吗?我是否以某种缺失的方式滥用EF/SQL?我能做些什么不同的事情来获得同样的结果吗?这是EF中的一个错误吗?它只是被默认打开的LazyLoad从视图中隐藏了出来,而且它有点像一个奇怪的组类型操作?

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

我找不到EF6对此的引用,但它是为EF Core记录的。(https://learn.microsoft.com/en-us/ef/core/querying/related-data)(请参阅"忽略includes")我怀疑在某些情况下,阻止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查询,则在运行时会出现严重错误。

编辑:根据要求跳过&take我建议使用视图模型来返回数据,而不是返回实体。使用视图模型,您可以指示EF只返回您需要的原始数据,使用简单的填充代码或使用Automapper组成视图模型,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<TViewModel>()来处理。

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

最新更新