web-dev-qa-db-ja.com

新しいラムダ式を動的に追加して、フィルターを作成します

これを行うことで必要なエンティティを取得するには、ObjectSetでフィルタリングを行う必要があります。

_query = this.ObjectSet.Where(x => x.TypeId == 3); // this is just an example;
_

コードの後半(および遅延実行を開始する前)で、次のようにクエリを再度フィルタリングします。

_query = query.Where(<another lambda here ...>);
_

これまでのところ、これは非常にうまく機能します。

これが私の問題です:

エンティティには、DateFromプロパティとDateToプロパティが含まれ、どちらもDataTimeタイプです。それらは期間を表します。

エンティティをフィルタリングして、期間コレクションの一部であるエンティティのみを取得する必要があります。コレクション内の期間必ずしも連続している必要はありませんしたがって、エンティティを取得するロジックは次のようになります。

_entities.Where(x => x.DateFrom >= Period1.DateFrom and x.DateTo <= Period1.DateTo)
||
entities.Where(x => x.DateFrom >= Period2.DateFrom and x.DateTo <= Period2.DateTo)
||
_

...そしてコレクション内のすべての期間にわたって何度も繰り返します。

私はそれをやってみました:

_foreach (var ratePeriod in ratePeriods)
{
    var period = ratePeriod;

    query = query.Where(de =>
        de.Date >= period.DateFrom && de.Date <= period.DateTo);
}
_

しかし、遅延実行を開始すると、これを必要に応じてSQLに変換します(コレクション内にある期間ごとに1つのフィルター)が、OR比較。これは、エンティティが複数の期間の一部になることはできないため、エンティティをまったく返しません。

期間フィルターを集約するために、ここである種の動的なlinqを構築する必要があります。


更新

ハッテンの答えに基づいて、私は次のメンバーを追加しました:

_private Expression<Func<T, bool>> CombineWithOr<T>(Expression<Func<T, bool>> firstExpression, Expression<Func<T, bool>> secondExpression)
{
    // Create a parameter to use for both of the expression bodies.
    var parameter = Expression.Parameter(typeof(T), "x");
    // Invoke each expression with the new parameter, and combine the expression bodies with OR.
    var resultBody = Expression.Or(Expression.Invoke(firstExpression, parameter), Expression.Invoke(secondExpression, parameter));
    // Combine the parameter with the resulting expression body to create a new lambda expression.
    return Expression.Lambda<Func<T, bool>>(resultBody, parameter);
}
_

新しいCombineWithOr式を宣言しました:

_Expression<Func<DocumentEntry, bool>> resultExpression = n => false;
_

そして、このように私の期間コレクションの反復でそれを使用しました:

_foreach (var ratePeriod in ratePeriods)
{
    var period = ratePeriod;
    Expression<Func<DocumentEntry, bool>> expression = de => de.Date >= period.DateFrom && de.Date <= period.DateTo;
    resultExpression = this.CombineWithOr(resultExpression, expression);
}

var documentEntries = query.Where(resultExpression.Compile()).ToList();
_

結果のSQLを調べたところ、式はまったく効果がないようです。結果のSQLは、以前にプログラムされたフィルターを返しますが、結合されたフィルターは返しません。どうして ?


アップデート2

FeO2xの提案を試してみたかったので、フィルタークエリを次のように書き直しました。

_query = query.AsEnumerable()
    .Where(de => ratePeriods
        .Any(rp => rp.DateFrom <= de.Date && rp.DateTo >= de.Date))
_

ご覧のとおり、AsEnumerable()を追加しましたが、コンパイラーがIEnumerableをIQueryableに変換できないというエラーを表示したため、クエリの最後にToQueryable()を追加しました。

_query = query.AsEnumerable()
    .Where(de => ratePeriods
        .Any(rp => rp.DateFrom <= de.Date && rp.DateTo >= de.Date))
            .ToQueryable();
_

すべてが正常に動作します。コードをコンパイルして、このクエリを起動できます。しかし、それは私のニーズに合いません。

結果のSQLのプロファイリング中に、処理中にメモリ内の日付をフィルタリングするため、フィルタリングSQLクエリの一部ではないであることがわかります。あなたはすでにそれについて知っていると思います、そしてそれはあなたが提案しようとしたものです。

あなたの提案は機能しますが、メモリ内でフィルタリングする前にデータベースからすべてのエンティティをフェッチするため(そしてそれらは数千と数千あります)、その膨大な量をメモリ内に戻すのは本当に遅いですデータベース。

私が本当に望んでいるのは、期間フィルタリングを送信することです結果のSQLクエリの一部としてなので、フィルタリングプロセスを完了する前に大量のエンティティを返すことはありません。

12
user2324540

良い提案にもかかわらず、私はLinqKit 1を使わなければなりませんでした。その理由の1つは、コード内の他の多くの場所で同じ種類の述語集約を繰り返さなければならないことです。 LinqKitを使用するのが最も簡単な方法です。言うまでもなく、数行のコードを書くだけでそれを実行できます。

LinqKitを使用して問題を解決した方法は次のとおりです。

_var predicate = PredicateBuilder.False<Document>();
foreach (var submittedPeriod in submittedPeriods)
{
    var period = period;
    predicate = predicate.Or(d =>
        d.Date >= period.DateFrom && d.Date <= period.DateTo);
}
_

そして、遅延実行を開始します(直前にAsExpandable()を呼び出すことに注意してください):

_var documents = this.ObjectSet.AsExpandable().Where(predicate).ToList();
_

結果のSQLを調べたところ、述語をSQLに変換するのに適しています。

8
user2324540

次のような方法を使用できます。

Expression<Func<T, bool>> CombineWithOr<T>(Expression<Func<T, bool>> firstExpression, Expression<Func<T, bool>> secondExpression)
{
    // Create a parameter to use for both of the expression bodies.
    var parameter = Expression.Parameter(typeof(T), "x");
    // Invoke each expression with the new parameter, and combine the expression bodies with OR.
    var resultBody = Expression.Or(Expression.Invoke(firstExpression, parameter), Expression.Invoke(secondExpression, parameter));
    // Combine the parameter with the resulting expression body to create a new lambda expression.
    return Expression.Lambda<Func<T, bool>>(resultBody, parameter);
}

その後:

Expression<Func<T, bool>> resultExpression = n => false; // Always false, so that it won't affect the OR.
foreach (var ratePeriod in ratePeriods)
{
    var period = ratePeriod;
    Expression<Func<T, bool>> expression = (de => de.Date >= period.DateFrom && de.Date <= period.DateTo);
    resultExpression = CombineWithOr(resultExpression, expression);
}

// Don't forget to compile the expression in the end.
query = query.Where(resultExpression.Compile());

詳細については、以下を確認してください。

2つの式を組み合わせる(Expression <Func <T、bool >>)

http://www.albahari.com/nutshell/predicatebuilder.aspx

編集:Expression<Func<DocumentEntry, bool>> resultExpression = n => false;は単なるプレースホルダーです。 Expression<Func<DocumentEntry, bool>> resultExpression;', you can't use it in the call toCombineWithOrfor the first time in yourforeach`ループを作成する場合、CombineWithOrメソッドを組み合わせるには2つのメソッドが必要です。次のコードのようになります。

int resultOfMultiplications = 1;
for (int i = 0; i < 10; i++)
    resultOfMultiplications = resultOfMultiplications * i;

そもそもresultOfMultiplicationsに何もない場合、ループで使用することはできません。

ラムダがn => falseである理由について。 ORステートメントでは効果がないためです。たとえば、false OR someExpression OR someExpressionsomeExpression OR someExpressionと同じです。そのfalseは何の効果もありません。

4
hattenn

このコードはどうですか:

_var targets = query.Where(de => 
    ratePeriods.Any(period => 
        de.Date >= period.DateFrom && de.Date <= period.DateTo));
_

LINQ Any演算子を使用して、_de.Date_に準拠するレート期間があるかどうかを判別します。これがエンティティごとに効率的なSQLステートメントにどのように変換されるかはよくわかりませんが。結果のSQLを投稿できれば、それは私にとって非常に興味深いことです。

お役に立てれば。

ハッテンの答えの後の更新:

Entity FrameworkはLINQ式を使用して、データベースに対して実行されるSQLまたはDMLを生成するため、hattennのソリューションが機能するとは思いません。したがって、Entity Frameworkは、_IQueryable<T>_ではなく_IEnumerable<T>_インターフェイスに依存しています。現在、デフォルトのLINQ演算子(Where、Any、OrderBy、FirstOrDefaultなど)が両方のインターフェイスに実装されているため、違いがわかりにくい場合があります。これらのインターフェイスの主な違いは、_IEnumerable<T>_拡張メソッドの場合、返される列挙可能オブジェクトが副作用なしに継続的に更新されるのに対し、_IQueryable<T>_の場合、実際の式が再構成されることです。これは無料ではありません。副作用の(つまり、SQLクエリの作成に最終的に使用される式ツリーを変更している)。

現在、EntityFrameworkはcaをサポートしています。 LINQの50の標準クエリ演算子ですが、_IQueryable<T>_を操作する独自のメソッド(hatennのメソッドなど)を作成すると、EntityFrameworkが解析できない式ツリーが作成されます。新しい拡張方法を知っています。これが、作成後に結合されたフィルターが表示されない原因である可能性があります(ただし、例外が予想されます)。

Any演算子を使用したソリューションはいつ機能しますか:

コメントで、_System.NotSupportedException_:タイプ 'RatePeriod'の定数値を作成できませんでした。このコンテキストでは、プリミティブ型または列挙型のみがサポートされます。これは、RatePeriodオブジェクトがメモリ内オブジェクトであり、Entity FrameworkObjectContextまたはDbContextによって追跡されない場合です。ここからダウンロードできる小さなテストソリューションを作成しました: https://dl.dropboxusercontent.com/u/14810011/LinqToEntitiesOrOperator.Zip

Visual Studio2012をLocalDBとEntityFramework 5で使用しました。結果を確認するには、クラスLinqToEntitiesOrOperatorTestを開き、テストエクスプローラーを開いて、ソリューションをビルドし、すべてのテストを実行します。 ComplexOrOperatorTestWithInMemoryObjectsが失敗し、他のすべてが合格する必要があることがわかります。

私が使用したコンテキストは次のようになります。

_public class DatabaseContext : DbContext
{
    public DbSet<Post> Posts { get; set; }
    public DbSet<RatePeriod> RatePeriods { get; set; }
}
public class Post
{
    public int ID { get; set; }
    public DateTime PostDate { get; set; }
}
public class RatePeriod
{
    public int ID { get; set; }
    public DateTime From { get; set; }
    public DateTime To { get; set; }
}
_

まあ、それはそれが得るのと同じくらい簡単です:-)。テストプロジェクトでは、2つの重要な単体テスト方法があります。

_    [TestMethod]
    public void ComplexOrOperatorDBTest()
    {
        var allAffectedPosts =
            DatabaseContext.Posts.Where(
                post =>
                DatabaseContext.RatePeriods.Any(period => period.From < post.PostDate && period.To > post.PostDate));

        Assert.AreEqual(3, allAffectedPosts.Count());
    }

    [TestMethod]
    public void ComplexOrOperatorTestWithInMemoryObjects()
    {
        var inMemoryRatePeriods = new List<RatePeriod>
            {
                new RatePeriod {ID = 1000, From = new DateTime(2002, 01, 01), To = new DateTime(2006, 01, 01)},
                new RatePeriod {ID = 1001, From = new DateTime(1963, 01, 01), To = new DateTime(1967, 01, 01)}
            };

        var allAffectedPosts =
            DatabaseContext.Posts.Where(
                post => inMemoryRatePeriods.Any(period => period.From < post.PostDate && period.To > post.PostDate));
        Assert.AreEqual(3, allAffectedPosts.Count());
    }
_

最初のメソッドは成功し、2番目のメソッドは失敗しますが、両方のメソッドはまったく同じことを行いますが、2番目のケースでは、DatabaseContextが認識しないレート期間オブジェクトをメモリに作成した点が異なります。

この問題を解決するために何ができますか?

  1. RatePeriodオブジェクトはそれぞれ同じObjectContextまたはDbContextにありますか?次に、上記の最初の単体テストで行ったように、それらをそのまま使用します。

  2. そうでない場合は、すべての投稿を一度に読み込むことができますか、それともOutOfMemoryExceptionになりますか?そうでない場合は、次のコードを使用できます。 AsEnumerable()呼び出しにより、_IEnumerable<T>_ではなく_IQueryable<T>_インターフェースに対してWhere演算子が使用されることに注意してください。事実上、これにより、すべての投稿がメモリに読み込まれ、フィルタリングされます。

    _[TestMethod]
    public void CorrectComplexOrOperatorTestWithInMemoryObjects()
    {
        var inMemoryRatePeriods = new List<RatePeriod>
            {
                new RatePeriod {ID = 1000, From = new DateTime(2002, 01, 01), To = new DateTime(2006, 01, 01)},
                new RatePeriod {ID = 1001, From = new DateTime(1963, 01, 01), To = new DateTime(1967, 01, 01)}
            };
    
        var allAffectedPosts =
            DatabaseContext.Posts.AsEnumerable()
                           .Where(
                               post =>
                               inMemoryRatePeriods.Any(
                                   period => period.From < post.PostDate && period.To > post.PostDate));
        Assert.AreEqual(3, allAffectedPosts.Count());
    }
    _
  3. 2番目の解決策が不可能な場合は、レート期間を渡して正しいSQLステートメントを形成するTSQLストアドプロシージャを作成することをお勧めします。このソリューションは、最もパフォーマンスの高いソリューションでもあります。

1
feO2x

とにかく、動的LINQクエリの作成は思ったほど簡単ではなかったと思います。以下のように、エンティティSQLを使用してみてください。

var filters = new List<string>();
foreach (var ratePeriod in ratePeriods)
{
    filters.Add(string.Format("(it.Date >= {0} AND it.Date <= {1})", ratePeriod.DateFrom, ratePeriod.DateTo));
}

var filter = string.Join(" OR ", filters);
var result = query.Where(filter);

これは正確には正しくないかもしれませんが(私は試していません)、これに似たものになるはずです。

0
hattenn