これを行うことで必要なエンティティを取得するには、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クエリの一部としてなので、フィルタリングプロセスを完了する前に大量のエンティティを返すことはありません。
良い提案にもかかわらず、私は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に変換するのに適しています。
次のような方法を使用できます。
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 to
CombineWithOrfor the first time in your
foreach`ループを作成する場合、CombineWithOr
メソッドを組み合わせるには2つのメソッドが必要です。次のコードのようになります。
int resultOfMultiplications = 1;
for (int i = 0; i < 10; i++)
resultOfMultiplications = resultOfMultiplications * i;
そもそもresultOfMultiplications
に何もない場合、ループで使用することはできません。
ラムダがn => false
である理由について。 OR
ステートメントでは効果がないためです。たとえば、false OR someExpression OR someExpression
はsomeExpression OR someExpression
と同じです。そのfalse
は何の効果もありません。
このコードはどうですか:
_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
が認識しないレート期間オブジェクトをメモリに作成した点が異なります。
この問題を解決するために何ができますか?
RatePeriod
オブジェクトはそれぞれ同じObjectContext
またはDbContext
にありますか?次に、上記の最初の単体テストで行ったように、それらをそのまま使用します。
そうでない場合は、すべての投稿を一度に読み込むことができますか、それとも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());
}
_
2番目の解決策が不可能な場合は、レート期間を渡して正しいSQLステートメントを形成するTSQLストアドプロシージャを作成することをお勧めします。このソリューションは、最もパフォーマンスの高いソリューションでもあります。
とにかく、動的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);
これは正確には正しくないかもしれませんが(私は試していません)、これに似たものになるはずです。