用于查找与 LINQ 查询中的动态字段列表匹配的部分字符串的通用表达式

问题描述 投票:0回答:1

据我了解,尤其是 SQL Server:

  • LINQ 查询将查询代码转换为 T-SQL 以针对 SQL Server 执行。
  • LINQ 代码逻辑并不总是无缝转换为 T-SQL,这通常需要额外的步骤才能使代码逻辑正常工作。示例:将
    .ToList()
    添加到上下文表调用会将所有记录拉入内存,生成 T-SQL 的其余代码可以对其进行查询。

最后一点是我需要帮助的地方。让我从我想要达到的目标开始。我希望能够创建一个可以从 LINQ 查询调用的通用方法,传入表字段列表和字符串值列表,如果在 中部分找到任何字符串值,则该方法将返回 true 或 false任何表字段。我使用的以下方法目前有效:

    public static bool SearchTermMatch(List<string> t, List<string> f)
    {
        return
        (
            (t.Count() == 0) ||
            (t.Count(_t => f.Any(_f => _f != null && _f.ToLower().Contains(_t.ToLower())) || _t == "") == t.Count())
        );
    }

我可以这样称呼它,例如:

var terms = "Tom Jones";    
var termlist = (string.IsNullOrEmpty(terms)) ? new List<string>() : terms.Split(' ').ToList();
var results = (from tbl in context.MyTable.ToList() where SearchTermMatch(termlist, new List<string>() { tbl.Field1, tbl.Field2, tbl.Field3 }) select new { tbl.Field1 }).Take(10).ToList();

正如我上面提到的,这是可行的,但只是因为我必须将

.ToList()
应用于上下文表。然而,我不想承担这样的工作方式所带来的成本。基本上,MyTable 中的所有记录都被拉入内存,而 SearchTermMatch 方法的其余部分则应用于内存中的结果。当 MyTable 具有数十万条记录时,这会变得非常昂贵,因为对于 MyTable 中的每条记录,SearchTermMatch 都会将 Field1、Field2 和 Field3 的值与我要从中查找部分匹配的字符串列表进行比较。因此,本质上,T-SQL 通过将 MyTable 中的所有记录传回服务器内存来快速执行,其中 LINQ 的其余部分根据服务器内存中的字符串列表处理每个记录的结果字段值。这是低效的。

我相信答案是将 SearchTermMatch 方法转换为表达式,该表达式将创建与字符串列表进行比较的表字段到可以使用 MyTable 查询执行的 T-SQL。基本上,目标是让 SearchTermMatch 方法将 T-SQL 传回 LINQ 查询,这样我就可以摆脱内存消耗的

.ToList()
附录。我只是希望我的 LINQ 在 SQL Server 上完全执行。

有人对我应该如何处理这个问题有任何建议吗?我一直在尝试让我的大脑理解表达式的概念,并觉得这是我需要进入的方向,但我很难弄清楚如何将替代的 SearchTermMatch 方法组合在一起。

感谢您提供的任何帮助。

c# sql-server linq t-sql expression
1个回答
0
投票

因此,首先,我们需要构建一些构建块,以便在进入业务逻辑之前为自己提供一些更好的工具来操作表达式。

首先,我们需要一个方法来用另一个表达式替换一个表达式的参数:

public static Expression ReplaceParameter(this Expression expression,
    ParameterExpression toReplace,
    Expression newExpression)
{
    return new ParameterReplaceVisitor(toReplace, newExpression)
        .Visit(expression);
}

这将需要表达式访问者将特定参数替换为另一个参数:

public class ParameterReplaceVisitor : ExpressionVisitor
{
    private ParameterExpression from;
    private Expression to;
    public ParameterReplaceVisitor(ParameterExpression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    protected override Expression VisitParameter(ParameterExpression node)
    {
        return node == from ? to : base.Visit(node);
    }
}

接下来我们需要一个组合表达式的方法,以便我们可以采用一个表达式,然后使用另一个表达式的结果,并将它们组合成一个同时执行这两个操作的表达式:

public static Expression<Func<TSource, TResult>> Compose<TSource, TIntermediate, TResult>(
    this Expression<Func<TSource, TIntermediate>> first,
    Expression<Func<TIntermediate, TResult>> second)
{
    var param = Expression.Parameter(typeof(TSource));
    var intermediateValue = first.Body.ReplaceParameter(first.Parameters[0], param);
    var body = second.Body.ReplaceParameter(second.Parameters[0], intermediateValue);
    return Expression.Lambda<Func<TSource, TResult>>(body, param);
}

接下来,我们需要一种方法来获取一堆表达式并生成一个表达式来告诉我们它们是否都为真:

public static Expression<Func<T, bool>> All<T>(this IEnumerable<Expression<Func<T, bool>>> predicates)
{
    var param = Expression.Parameter(typeof(T));
    var body = predicates.Select(prediate => prediate.Body.ReplaceParameter(prediate.Parameters.First(), param))
        .Aggregate(Expression.AndAlso);
    return Expression.Lambda<Func<T, bool>>(body, param);
}

我们可以使用

Compose
来实现这一点,但这会导致比仅替换每个参数一次并仅构建一个新的 lambda 要做更多的工作。

我们还需要一种方法来告诉我们谓词序列中的任何一个是否为真,这就是

All
,但使用 OR 而不是 AND 来组合它们:

public static Expression<Func<T, bool>> Any<T>(this IEnumerable<Expression<Func<T, bool>>> predicates)
{
    var param = Expression.Parameter(typeof(T));
    var body = predicates.Select(prediate => prediate.Body.ReplaceParameter(prediate.Parameters.First(), param))
        .Aggregate(Expression.OrElse);
    return Expression.Lambda<Func<T, bool>>(body, param);
}

好的,通用工具就是这样。接下来,我们需要调整您的实际方法。

IQueryable
无法操作您的自定义 C# 方法,您需要接受您的术语作为表达式并返回代表您自己的逻辑的新表达式,因此签名需要显着不同。

接下来,最好在开始时清理数据并进行参数验证,并完全避免尝试在表达式操作代码中执行任何操作。

之后,为了确定任何字段是否包含单个术语,我们可以将每个字段投影到一个表达式中,告诉我们该字段是否包含该术语,然后将它们与之前的

Any
函数结合起来。

然后我们只需要查看所有术语是否都包含在任何字段中即可。

public static Expression<Func<T, bool>> SearchTermMatch<T>(IEnumerable<string> terms, params Expression<Func<T, string>>[] fields)
{
    terms = terms.Where(term => term != "").ToList();
    if (!terms.Any()) return _ => true;
    if (!fields.Any()) return _ => false;

    Expression<Func<T, bool>> AnyFieldContainsTerm(string term) =>
        fields.Select(field => field.Compose(value => value.Contains(term)))
        .Any();

    return terms.Select(AnyFieldContainsTerm)
                .All();
}

为了将它们整合在一起,我们只需要调整方法的调用方式,以便您调用方法本身,而不是在另一个表达式中使用它:

var terms = "Tom Jones";
var termlist = (string.IsNullOrEmpty(terms)) ? new List<string>() : terms.Split(' ').ToList();

var query = context.MyTable.Where(SearchTermMatch<Foo>(termlist, f => f.Field1, f => f.Field2, f => f.Field3))
    .Select(table => table.Field1)
    .Take(10);
© www.soinside.com 2019 - 2024. All rights reserved.