EF Core-通过键值对的子级集合进行筛选非常慢

时间:2018-10-03 14:40:47

标签: c# entity-framework linq-to-sql entity-framework-core

我的主系统实体被“标记”为键值对的子集合,我想用它来过滤主要实体的列表。但是,我在下面编写的EF核心查询的速度太慢,无法接受。

简化的实体类

 public class MainEntity
 {
    public int Id { get; set; }
    public DateTimeOffset Created { get; set; }
    public string Stuff {get; set;}
    public virtual List<Tag> Tags { get; set; }
 }

 public class Tag
 {
    public int Id { get; set; }
    public string Key { get; set; }
    public string Value { get; set; }

    public int MainEntityId { get; set; }
    public virtual MainEntity MainEntity { get; set; }
 }

简化查询

//filter params passed into the query function
//String? stuffFilter
//List<Tag> tagSearchValues

var query = _dbContext.MainEntities.Where(
    me => ((!stuffFilter.HasValue || me.Stuff == stuffFilter.Value)                    
    && (tagSearchValues == null || tagSearchValues.Count == 0 ||
    (
    (me.Tags.Select(t => t.Key).Any(tk => tagSearchValues.Select(s => s.Key).Any(sk => sk == tk))) &&
    (me.Tags.Select(t => t.Value).Any(tv => tagSearchValues.Select(s => s.Value).Any(sv => sv == tv)))
    )
    ).                    
    OrderByDescending(l => me.Created).AsNoTracking();

我对EF(第一次使用EF Core)有点不满意,但是问题出在我用多个.Any()命令(子查询)通过子Tag集合过滤的方式如果未指定标签过滤器,则效果最佳。

我想不出另一种方法来针对选定的Tag过滤器对象过滤子Tag对象集合-我想象一个过滤器Tag会更简单,更快捷。

我目前唯一想到的选择是自己做一个自定义SQL查询,但是在组合我的第一个EF Core查询时似乎已经不得不诉诸于此了!

2 个答案:

答案 0 :(得分:2)

首先要注意的是,您提出的查询无法完全作为SQL进行评估,因为对于包含非原始值tagSearchValues的集合没有SQL等效项。这导致EF自动切换到client-side evaluation。也就是说,它将所有符合stuffFilter条件的实体及其所有标签拉入内存,然后应用标签谓词。显然,这是无效的。

第二,查询不正确。包含具有特定键的标签和具有特定值的标签的实体与包含特定键/值组合的标签不同。它需要一个与每个组合匹配的查询,如下所示:

db.MainEntities.Where(...)
    .Where(m => tagSearchValues
       .Any(t => m.Tags.Any(mt => mt.Key == t.Key 
                               && mt.Value == t.Value)))

但是,如果这样做,EF将再次转向效率低下的客户端评估,您甚至必须应用Include或延迟加载自己才能将标签拉入内存。 (此外,由于某种原因,EF会引发大量冗余查询。)

问题的实质是,EF(与其他ORM一样)不适用于服务器端的这种成对比较。因此,您需要谓词生成器来构建标记谓词。有几个谓词buiders,例如在Linqkit中。我使用this one是因为它很好而且很简单。诀窍是:建立谓词并将其应用于Where()

var tagPredicate = PredicateBuilder.True<MainEntity>();
if (tagSearchValues.Any())
{
    tagPredicate = PredicateBuilder.False<MainEntity>();
    foreach (var tag in tagSearchValues)
    {
        tagPredicate = tagPredicate.Or(m => m.Tags
                           .Any(t => t.Key == tag.Key
                                  && t.Value == tag.Value));
    }
}

var query = _dbContext.MainEntities
    .Where(m => string.IsNullOrWhiteSpace(stuff) || m.Stuff == stuff)
    .Where(tagPredicate);
... // Use query

我之所以使用Or,是因为(从您的查询中)我假设您想要在搜索标签中具有 any 标签的实体。这就是为什么我以PredicateBuilder.True谓词开头的原因,因此如果没有搜索标签,该查询将返回结果,类似于您的原始查询。

答案 1 :(得分:0)

您知道EF Core Any正在生成什么SQL吗? EF Core具有不幸的设计属性,即如果无法将查询转换为SQL,则会在客户端以静默方式执行查询。

如果您合并KeyValue测试,该怎么办?

(me.Tags.Any(met => tagSearchValues.Any(st => st.Tag == met.Tag && st.Value == met.Value)))

或者如果您改用Contains怎么办?

(me.Tags.Select(t => t.Key).Any(tk => tagSearchValues.Select(s => s.Key).Contains(tk))) &&
(me.Tags.Select(t => t.Value).Any(tv => tagSearchValues.Select(s => s.Value).Contains(tv)))