结合表达式而不是在Entity Framework中使用多个查询

时间:2016-08-18 10:33:17

标签: c# asp.net entity-framework expression-trees iqueryable

我有以下通用可查询(可能已经应用了选项):

IQueryable<TEntity> queryable = DBSet<TEntity>.AsQueryable();

然后有Provider类看起来像这样:

public class Provider<TEntity>
{
    public Expression<Func<TEntity, bool>> Condition { get; set; }

    [...]
}

可以按以下方式为每个实例定义Condition

Condition = entity => entity.Id == 3;

现在,我想选择至少由Provider的一个实体满足Condition的所有DBSet个实例:

List<Provider> providers = [...];
var matchingProviders = providers.Where(provider => queryable.Any(provider.Condition))

问题:我正在开始查询列表中的每个Provider实例。我宁愿使用单个查询来获得相同的结果。由于性能可疑,此主题尤其重要。如何使用Linq语句或Expression Trees语句在单个查询中获得相同的结果并提高效果?

5 个答案:

答案 0 :(得分:5)

有趣的挑战。我看到的唯一方法是构建动态SELECT TOP 1 0 FROM Table WHERE Condition[0] UNION ALL SELECT TOP 1 1 FROM Table WHERE Condition[1] ... UNION ALL SELECT TOP 1 N-1 FROM Table WHERE Condition[N-1] 查询,如下所示:

var parameter = Expression.Parameter(typeof(TEntity), "e");
var indexQuery = providers
    .Select((provider, index) => queryable
        .Where(provider.Condition)
        .Take(1)
        .Select(Expression.Lambda<Func<TEntity, int>>(Expression.Constant(index), parameter)))
    .Aggregate(Queryable.Concat);

var indexes = indexQuery.ToList();
var matchingProviders = indexes.Select(index => providers[index]);

然后使用返回的数字作为索引来获取匹配的提供者。

这样的事情:

Expression

请注意,我可以在不使用Select类的情况下构建查询,方法是将上面的.Select(_ => index) 替换为

Active

但这会为每个索引引入不必要的SQL查询参数。

答案 1 :(得分:4)

这是我心中想到的另一个(疯狂的)想法。请注意,与我之前的答案类似,它并不能保证更好的性能(事实上它可能更糟)。它只是提供了一种通过单个SQL查询来执行您所要求的方法。

这里我们将创建一个返回单个string的查询,其长度为N,由'0'和'1'组成,'1'表示匹配(类似于字符串位数组)。该查询将使用我最喜欢的组,通过常量技术动态构建如下:

var matchInfo = queryable
    .GroupBy(e => 1)
    .Select(g =>
        (g.Max(Condition[0] ? "1" : "0")) +
        (g.Max(Condition[1] ? "1" : "0")) +
            ...
        (g.Max(Condition[N-1] ? "1" : "0")))
    .FirstOrDefault() ?? "";

以下是代码:

var group = Expression.Parameter(typeof(IGrouping<int, TEntity>), "g");

var concatArgs = providers.Select(provider => Expression.Call(
        typeof(Enumerable), "Max", new[] { typeof(TEntity), typeof(string) },
        group, Expression.Lambda(
            Expression.Condition(
                provider.Condition.Body, Expression.Constant("1"), Expression.Constant("0")),
            provider.Condition.Parameters)));

var concatCall = Expression.Call(
    typeof(string).GetMethod("Concat", new[] { typeof(string[]) }),
    Expression.NewArrayInit(typeof(string), concatArgs));

var selector = Expression.Lambda<Func<IGrouping<int, TEntity>, string>>(concatCall, group);

var matchInfo = queryable
    .GroupBy(e => 1)
    .Select(selector)
    .FirstOrDefault() ?? "";

var matchingProviders = matchInfo.Zip(providers,
    (match, provider) => match == '1' ? provider : null)
    .Where(provider => provider != null)
    .ToList();

享受:)

PS 在我看来,此查询将以恒定速度运行(关于条件的数量和类型,即可以被认为是最好的 O(N),最差和平均情况,其中 N 是表中记录的数量),因为数据库必须始终执行全表扫描。知道什么是实际表现仍然很有趣,但最有可能做这样的事情并不值得努力。

更新:关于赏金和更新后的要求:

  

查找快速查询仅读取表格的记录如果已满足所有条件则结束查询

没有满足这两个条件的标准SQL构造(甚至不谈论LINQ查询转换)。允许像EXISTS这样的早期结束的构造可以用于单个条件,因此当针对多个条件执行时,将违反仅读取一次表记录的第一个规则。虽然在这个答案中使用聚合的构造满足第一个规则,但是为了产生聚合值,它们必须读取所有记录,因此不能提前退出。

很快,没有任何查询可以满足这两个要求。那么 fast 部分,它实际上取决于数据的大小以及条件的数量和类型,表索引等,所以对于所有情况,根本没有“最佳”的通用解决方案。

答案 2 :(得分:3)

基于@Ivan的这个Post我创建了一个在某些情况下稍微快一点的表达式。

它使用Any代替Max来获得所需的结果。

var group = Expression.Parameter(typeof(IGrouping<int, TEntity>), "g");

var anyMethod = typeof(Enumerable)
    .GetMethods()
    .First(m => m.Name == "Any" && m.GetParameters()
    .Count() == 2)
    .MakeGenericMethod(typeof(TEntity));

var concatArgs = Providers.Select(provider => 
    Expression.Call(anyMethod, group, 
    Expression.Lambda(provider.Condition.Body, provider.Condition.Parameters)));

var convertExpression = concatArgs.Select(concat =>
    Expression.Condition(concat, Expression.Constant("1"), Expression.Constant("0")));

var concatCall = Expression.Call(
    typeof(string).GetMethod("Concat", new[] { typeof(string[]) }),
    Expression.NewArrayInit(typeof(string), convertExpression));

var selector = Expression.Lambda<Func<IGrouping<int, TEntity>, string>>(concatCall, group);

var matchInfo = queryable
    .GroupBy(e => 1)
    .Select(selector)
    .First();

var MatchingProviders = matchInfo.Zip(Providers,
    (match, provider) => match == '1' ? provider : null)
    .Where(provider => provider != null)
    .ToList();

答案 3 :(得分:1)

我在这里尝试的方法是创建Conditions并将它们嵌套到一个Expression中。如果符合Conditions之一,我们会获得Provider的索引。

private static Expression NestedExpression(
    IEnumerable<Expression<Func<TEntity, bool>>> expressions, 
    int startIndex = 0)
{
    var range = expressions.ToList();
    range.RemoveRange(0, startIndex);

    if (range.Count == 0)
        return Expression.Constant(-1);

    return Expression.Condition(
        range[0].Body, 
        Expression.Constant(startIndex), 
        NestedExpression(expressions, ++startIndex));
}

由于Expressions仍然可以使用不同的ParameterExpressions,我们需要ExpressionVisitor来重写这些内容:

private class PredicateRewriterVisitor : ExpressionVisitor
{
    private readonly ParameterExpression _parameterExpression;

    public PredicateRewriterVisitor(ParameterExpression parameterExpression)
    {
        _parameterExpression = parameterExpression;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        return _parameterExpression;
    }
}

对于重写,我们只需要调用此方法:

private static Expression<Func<T, bool>> Rewrite<T>(
    Expression<Func<T, bool>> exp, 
    ParameterExpression parameterExpression)
{
    var newExpression = new PredicateRewriterVisitor(parameterExpression).Visit(exp);
    return (Expression<Func<T, bool>>)newExpression;
}

查询本身和Provider实例的选择如下:

var parameterExpression = Expression.Parameter(typeof(TEntity), "src");
var conditions = Providers.Select(provider => 
    Rewrite(provider.Condition, parameterExpression)
);

var nestedExpression = NestedExpression(conditions);
var lambda = Expression.Lambda<Func<TEntity, int>>(nestedExpression, parameterExpression);

var matchInfo = queryable.Select(lambda).Distinct();
var MatchingProviders = Providers.Where((provider, index) => matchInfo.Contains(index));

注意:另一个选项并不是很快

答案 4 :(得分:1)

以下是与表达式无关的问题的另一种观点。

由于主要目标是提高性能,如果尝试使用单个查询生成结果没有帮助,我们可以尝试通过并行执行原始多查询解决方案来提高速度。

由于它确实是LINQ to Objects查询(内部执行多个EF查询),理论上应该通过插入PLINQ来将其转换为AsParallel查询这(不工作):

var matchingProviders = providers
    .AsParallel()
    .Where(provider => queryable.Any(provider.Condition))
    .ToList();

然而,事实证明EF DbContext不适合多线程访问,而上述只是生成运行时错误。所以我不得不使用允许我们提供本地状态的TPL重载之一来Parallel.ForEach,我曾经在执行期间分配了几个DbContext实例。

最终的工作代码如下所示:

var matchingProviders = new List<Provider<TEntity>>();
Parallel.ForEach(providers,
    () => new
    {
        context = new MyDbContext(),
        matchingProviders = new List<Provider<TEntity>>()
    },
    (provider, state, data) =>
    {
        if (data.context.Set<TEntity>().Any(provider.Condition))
            data.matchingProviders.Add(provider);
        return data;
    },
    data =>
    {
        data.context.Dispose();
        if (data.matchingProviders.Count > 0)
        {
            lock (matchingProviders)
                matchingProviders.AddRange(data.matchingProviders);
        }
    }
);

如果你有一个多核CPU(现在很正常)和一个好的数据库服务器,这应该可以为你提供所需的改进。