web-dev-qa-db-ja.com

Entity FrameworkでOR条件を使用した動的クエリ

次のSOの質問のように、データベースを検索し、ユーザーが任意の基準を動的に追加できるようにするアプリケーションを作成できるアプリケーションを作成しています(約50件): エンティティフレームワーク 。現在、各基準をチェックする検索を行っており、空でない場合はクエリに追加します。

C#

var query = Db.Names.AsQueryable();
  if (!string.IsNullOrWhiteSpace(first))
      query = query.Where(q => q.first.Contains(first));
  if (!string.IsNullOrWhiteSpace(last))
      query = query.Where(q => q.last.Contains(last));
  //.. around 50 additional criteria
  return query.ToList();

このコードは、SQLサーバーで次のようなものを生成します(理解を容易にするために簡略化しました)

[〜#〜] sql [〜#〜]

SELECT
    [Id],
    [FirstName],
    [LastName],
    ...etc
FROM [dbo].[Names]
WHERE [FirstName] LIKE '%first%'
  AND [LastName] LIKE '%last%'

私は今、エンティティフレームワークを介してC#で次のSQLを生成する方法を追加しようとしていますが、[〜#〜] or [〜#〜][〜#〜] and [〜#〜]の代わりに、条件を動的に追加する機能を維持します。

[〜#〜] sql [〜#〜]

SELECT
    [Id],
    [FirstName],
    [LastName],
    ...etc
  FROM [dbo].[Names]
WHERE [FirstName] LIKE '%first%'
  OR [LastName] LIKE '%last%' <-- NOTICE THE "OR"

通常、クエリの基準は2つまたは3つの項目よりも大きくなりませんが、それらを1つの巨大なクエリに結合することはオプションではありません。連結、結合、および交差を試みましたが、それらはすべてクエリを複製し、UNIONで結合します。

エンティティフレームワークを使用して動的に生成されたクエリに「OR」条件を追加する簡単でクリーンな方法はありますか?

ソリューションで編集-2015年9月29日

これを投稿してから、これが少し注目されていることに気づいたので、ソリューションを投稿することにしました

// Make sure to add required nuget
// PM> Install-Package LinqKit

var searchCriteria = new 
{
    FirstName = "sha",
    LastName = "hill",
    Address = string.Empty,
    Dob = (DateTime?)new DateTime(1970, 1, 1),
    MaritalStatus = "S",
    HireDate = (DateTime?)null,
    LoginId = string.Empty,
};

var predicate = PredicateBuilder.False<Person>();
if (!string.IsNullOrWhiteSpace(searchCriteria.FirstName))
{
    predicate = predicate.Or(p => p.FirstName.Contains(searchCriteria.FirstName));
}

if (!string.IsNullOrWhiteSpace(searchCriteria.LastName))
{
    predicate = predicate.Or(p => p.LastName.Contains(searchCriteria.LastName));
}

// Quite a few more conditions...

foreach(var person in this.Persons.Where(predicate.Compile()))
{
    Console.WriteLine("First: {0} Last: {1}", person.FirstName, person.LastName);
}
34
Ben Anderson

おそらく、 述語ビルダー のようなものを探しているので、whereステートメントのANDとORを簡単に制御できます。

Dynamic Linq もあります。これにより、SQL文字列のようなWHERE句を送信でき、WHEREの正しい述語に解析されます。

21
Steven V

LINQKitとそのPredicateBuilderは非常に汎用性がありますが、いくつかの簡単なユーティリティ(それぞれが他の式操作操作の基盤として機能することができます)でこれをより直接行うことができます。

まず、汎用のExpression Replacer:

_public class ExpressionReplacer : ExpressionVisitor
{
    private readonly Func<Expression, Expression> replacer;

    public ExpressionReplacer(Func<Expression, Expression> replacer)
    {
        this.replacer = replacer;
    }

    public override Expression Visit(Expression node)
    {
        return base.Visit(replacer(node));
    }
}
_

次に、特定の式で1つのパラメーターの使用を別のパラメーターに置き換える簡単なユーティリティメソッド:

_public static T ReplaceParameter<T>(T expr, ParameterExpression toReplace, ParameterExpression replacement)
    where T : Expression
{
    var replacer = new ExpressionReplacer(e => e == toReplace ? replacement : e);
    return (T)replacer.Visit(expr);
}
_

これは、2つの異なる式のラムダパラメータが実際には異なるパラメータであるため、同じ名前であっても必要です。たとえば、q => q.first.Contains(first) || q.last.Contains(last)で終わる場合、q.last.Contains(last)qexact sameqラムダ式の先頭で提供されます。

次に、_Func<T, TReturn>_スタイルのLambda式を特定のバイナリ式ジェネレーターと結合できる汎用Joinメソッドが必要です。

_public static Expression<Func<T, TReturn>> Join<T, TReturn>(Func<Expression, Expression, BinaryExpression> joiner, IReadOnlyCollection<Expression<Func<T, TReturn>>> expressions)
{
    if (!expressions.Any())
    {
        throw new ArgumentException("No expressions were provided");
    }
    var firstExpression = expressions.First();
    var otherExpressions = expressions.Skip(1);
    var firstParameter = firstExpression.Parameters.Single();
    var otherExpressionsWithParameterReplaced = otherExpressions.Select(e => ReplaceParameter(e.Body, e.Parameters.Single(), firstParameter));
    var bodies = new[] { firstExpression.Body }.Concat(otherExpressionsWithParameterReplaced);
    var joinedBodies = bodies.Aggregate(joiner);
    return Expression.Lambda<Func<T, TReturn>>(joinedBodies, firstParameter);
}
_

_Expression.Or_でこれを使用しますが、数値式を_Expression.Add_と組み合わせるなど、さまざまな目的で同じメソッドを使用できます。

最後に、すべてをまとめると、次のようになります。

_var searchCriteria = new List<Expression<Func<Name, bool>>();

  if (!string.IsNullOrWhiteSpace(first))
      searchCriteria.Add(q => q.first.Contains(first));
  if (!string.IsNullOrWhiteSpace(last))
      searchCriteria.Add(q => q.last.Contains(last));
  //.. around 50 additional criteria
var query = Db.Names.AsQueryable();
if(searchCriteria.Any())
{
    var joinedSearchCriteria = Join(Expression.Or, searchCriteria);
    query = query.Where(joinedSearchCriteria);
}
  return query.ToList();
_
7

エンティティフレームワークを使用して動的に生成されたクエリに「OR」条件を追加する簡単でクリーンな方法はありますか?

はい、これは、実行時にwhere部分が動的に「無効」または「有効」になる単一のブール式を含む単一のOR句に依存するだけで実現できます。したがって、LINQKitまたはカスタム述語ビルダーを作成します。

あなたの例を参照して:

_var isFirstValid = !string.IsNullOrWhiteSpace(first);
var isLastValid = !string.IsNullOrWhiteSpace(last);

var query = db.Names
  .AsQueryable()
  .Where(name =>
    (isFirstValid && name.first.Contains(first)) ||
    (isLastValid && name.last.Contains(last))
  )
  .ToList();
_

上記の例でわかるように、以前に評価された施設(例:where)に基づいて、isFirstValid- filter式のOR部分を動的に「オン」または「オフ」に切り替えています。

たとえば、isFirstValidtrueではない場合、name.first.Contains(first)short-circuited であり、実行も結果セットにも影響しません。さらに、EF CoreのDefaultQuerySqlGeneratorは、実行前にwhere内のブール式をさらに 最適化および削減 します(たとえば_false && x || true && y || false && z_は単純に削減できます) y単純な静的分析を介して)。

注:前提条件のいずれもtrueでない場合、結果セットは空になります。これは、あなたの場合に望ましい動作であると思われます。ただし、何らかの理由でIQueryableソースからすべての要素を選択したい場合は、trueに評価される式に最終変数を追加できます(例:.Where( ... || shouldReturnAll) with var shouldReturnAll = !(isFirstValid || isLastValid)または類似のもの)。

最後の注意:この手法の欠点は、クエリが存在するメソッド本体(より正確にはクエリのwhere部分)にある「中央集中型」ブール式を作成することを余儀なくされることです。何らかの理由で、述語の構築プロセスを分散化し、引数として注入したり、クエリビルダーを介してチェーンしたい場合は、他の回答で提案されているように、述語ビルダーに固執する必要があります。そうでなければ、この簡単なテクニックをお楽しみください:)

1
B12Toaster