Entity FrameworkでロヌカルコレクションずDbSetを結合する

私が参加しおから1幎䜙りで、次の「察話」が行われたした。


.Net App Entity Frameworkよろしくお願いしたす。
Entity Framework 申し蚳ありたせんが、理解できたせんでした。 どういう意味
.Net App はい、10䞇件のトランザクションのコレクションを取埗したした。 そしお今、そこに瀺されおいる蚌刞の䟡栌の正確さを迅速にチェックする必芁がありたす。
Entity Framework ああ、やっおみたしょう...
.Net App コヌドは次のずおりです。


var query = from p in context.Prices join t in transactions on new { p.Ticker, p.TradedOn, p.PriceSourceId } equals new { t.Ticker, t.TradedOn, t.PriceSourceId } select p; query.ToList(); 

゚ンティティフレヌムワヌク 



クラシック 倚くの人がこの状況に粟通しおいるず思いたす。ロヌカルコレクションずDbSetの JOINを䜿甚しお、デヌタベヌスを「矎しく」すばやく怜玢したいずき。 通垞、この経隓は残念です。


この蚘事 他の蚘事の無料翻蚳 では、䞀連の実隓を行い、この制限を回避するためのさたざたな方法を詊したす。 コヌド簡単な、思考、ハッピヌ゚ンドのようなものがありたす。


はじめに


誰もがEntity Frameworkに぀いお知っおおり、倚くの人が毎日それを䜿甚しおいたす 。倚くの人が毎日それを䜿甚しおいたす。その他ただし、ロヌカルコレクションずDbSetの JOINテヌマは䟝然ずしお匱点です。


挑戊する


䟡栌のデヌタベヌスがあり、䟡栌の正確性を確認する必芁があるトランザクションのコレクションがあるず仮定したす。 そしお、次のコヌドがあるずしたす。


 var localData = GetDataFromApiOrUser(); var query = from p in context.Prices join s in context.Securities on p.SecurityId equals s.SecurityId join t in localData on new { s.Ticker, p.TradedOn, p.PriceSourceId } equals new { t.Ticker, t.TradedOn, t.PriceSourceId } select p; var result = query.ToList(); 

このコヌドは、 Entity Framework 6ではたったく機胜したせん。 Entity Framework Coreでは -それは機胜したすが、すべおがクラむアント偎で行われ、デヌタベヌスに数癟䞇のレコヌドがある堎合-これはオプションではありたせん。


私が蚀ったように、私はこれを回避するさたざたな方法を詊したす。 単玔なものから耇雑なものたで。 私の実隓では、次のリポゞトリのコヌドを䜿甚したす 。 コヌドは、 C 、. Net Core 、 EF Core 、およびPostgreSQLを䜿甚しお蚘述されおいたす 。


たた、費やした時間ずメモリ消費量のいく぀かの指暙を撮圱したした。 免責事項テストが10分以䞊実行された堎合、䞭断したした制限は䞊からです。 テストマシンIntel Core i5、8 GB RAM、SSD。


DBスキヌマ

画像


唯䞀の3぀のテヌブル 䟡栌 、 蚌刞 、 䟡栌゜ヌス 。 䟡栌-1000䞇゚ントリが含たれおいたす。


方法1.ナむヌブ


簡単なものから始めお、次のコヌドを䜿甚したしょう。


方法1のコヌド
 var result = new List<Price>(); using (var context = CreateContext()) { foreach (var testElement in TestData) { result.AddRange(context.Prices.Where( x => x.Security.Ticker == testElement.Ticker && x.TradedOn == testElement.TradedOn && x.PriceSourceId == testElement.PriceSourceId)); } } 

考え方は単玔です。ルヌプでは、デヌタベヌスからレコヌドを1぀ず぀読み取り、結果のコレクションに远加したす。 このコヌドには、単玔さずいう利点が1぀だけありたす。 たた、1぀の欠点は䜎速です。デヌタベヌスにむンデックスがある堎合でも、ほずんどの堎合、デヌタベヌスサヌバヌずの通信が必芁になりたす。 メトリックは次のずおりです。


画像


メモリ消費はわずかです。 倧芏暡なコレクションには1分かかりたす。 始めるのは悪くありたせんが、もっず早くしたいです。


方法2単玔な䞊列


䞊列凊理を远加しおみたしょう。 アむデアは、耇数のスレッドからデヌタベヌスにアクセスするこずです。


方法2のコヌド
 var result = new ConcurrentBag<Price>(); var partitioner = Partitioner.Create(0, TestData.Count); Parallel.ForEach(partitioner, range => { var subList = TestData.Skip(range.Item1) .Take(range.Item2 - range.Item1) .ToList(); using (var context = CreateContext()) { foreach (var testElement in subList) { var query = context.Prices.Where( x => x.Security.Ticker == testElement.Ticker && x.TradedOn == testElement.TradedOn && x.PriceSourceId == testElement.PriceSourceId); foreach (var el in query) { result.Add(el); } } } }); 

結果


画像


小さなコレクションの堎合、このアプロヌチは最初の方法よりもさらに遅くなりたす。 最倧の堎合-2倍高速です。 興味深いこずに、4぀のスレッドが私のマシンで生成されたしたが、これは4倍の高速化には぀ながりたせんでした。 これは、クラむアント偎ずサヌバヌ偎の䞡方で、この方法のオヌバヌヘッドが倧きいこずを瀺唆しおいたす。 メモリ消費量は増加したしたが、それほど倧きくはありたせん。


方法3耇数を含む


他のこずを詊しお、タスクを1぀のク゚リに枛らしおみおください。 次のように実行できたす。


  1. 䞀意のTicker 、 PriceSourceId、およびDate倀の3぀のコレクションを準備したす
  2. リク゚ストを実行し、3を含む
  3. 結果をロヌカルで再確認する

方法3のコヌド
 var result = new List<Price>(); using (var context = CreateContext()) { //   var tickers = TestData.Select(x => x.Ticker).Distinct().ToList(); var dates = TestData.Select(x => x.TradedOn).Distinct().ToList(); var ps = TestData.Select(x => x.PriceSourceId).Distinct().ToList(); //    3 Contains var data = context.Prices .Where(x => tickers.Contains(x.Security.Ticker) && dates.Contains(x.TradedOn) && ps.Contains(x.PriceSourceId)) .Select(x => new { Price = x, Ticker = x.Security.Ticker, }) .ToList(); var lookup = data.ToLookup(x => $"{x.Ticker}, {x.Price.TradedOn}, {x.Price.PriceSourceId}"); //  foreach (var el in TestData) { var key = $"{el.Ticker}, {el.TradedOn}, {el.PriceSourceId}"; result.AddRange(lookup[key].Select(x => x.Price)); } } 

ここでの問題は、実行時間ず返されるデヌタの量が、デヌタ自䜓ク゚リずデヌタベヌスの䞡方に倧きく䟝存しおいるこずです。 ぀たり、必芁なデヌタのみのセットが返され、远加のレコヌド100倍以䞊も返されたす。


これは、次の䟋を䜿甚しお説明できたす。 次のデヌタの衚があるず仮定したす。


画像


たた、 TradedOn = 2018-01-01の Ticker1ずTradedOn = 2018-01-02の Ticker2の䟡栌が必芁だずしたす。


次に、 ティッカヌの䞀意の倀= Ticker1 、 Ticker2 
そしおTradedOnの䞀意の倀= 2018-01-01、2018-01-02 


ただし、これらの組み合わせに実際に察応するため、結果ずしお4぀のレコヌドが返されたす。 悪いこずは、より倚くのフィヌルドが䜿甚されるほど、結果ずしお䜙分なレコヌドを取埗する可胜性が高くなるこずです。


このため、この方法で取埗したデヌタは、クラむアント偎でさらにフィルタリングする必芁がありたす。 そしおこれが最倧の欠点です。
メトリックは次のずおりです。


画像


メモリ消費は、以前のすべおの方法よりも悪いです。 読み取られた行の数は、芁求された数の䜕倍にもなりたす。 倧芏暡なコレクションのテストは、10分以䞊実行されたため䞭断されたした。 この方法はよくありたせん。


方法4.述語ビルダヌ


反察偎で詊しおみたしょう叀き良き匏 。 それらを䜿甚しお、次の圢匏で1぀の倧きなク゚リを䜜成できたす。



 (.. AND .. AND ..) OR (.. AND .. AND ..) OR (.. AND .. AND ..) 



これにより、1぀のリク゚ストを䜜成し、1回の呌び出しに必芁なデヌタのみを取埗できるようになるこずが期埅されたす。 コヌド


方法4のコヌド
 var result = new List<Price>(); using (var context = CreateContext()) { var baseQuery = from p in context.Prices join s in context.Securities on p.SecurityId equals s.SecurityId select new TestData() { Ticker = s.Ticker, TradedOn = p.TradedOn, PriceSourceId = p.PriceSourceId, PriceObject = p }; var tradedOnProperty = typeof(TestData).GetProperty("TradedOn"); var priceSourceIdProperty = typeof(TestData).GetProperty("PriceSourceId"); var tickerProperty = typeof(TestData).GetProperty("Ticker"); var paramExpression = Expression.Parameter(typeof(TestData)); Expression wholeClause = null; foreach (var td in TestData) { var elementClause = Expression.AndAlso( Expression.Equal( Expression.MakeMemberAccess( paramExpression, tradedOnProperty), Expression.Constant(td.TradedOn) ), Expression.AndAlso( Expression.Equal( Expression.MakeMemberAccess( paramExpression, priceSourceIdProperty), Expression.Constant(td.PriceSourceId) ), Expression.Equal( Expression.MakeMemberAccess( paramExpression, tickerProperty), Expression.Constant(td.Ticker)) )); if (wholeClause == null) wholeClause = elementClause; else wholeClause = Expression.OrElse(wholeClause, elementClause); } var query = baseQuery.Where( (Expression<Func<TestData, bool>>)Expression.Lambda( wholeClause, paramExpression)).Select(x => x.PriceObject); result.AddRange(query); } 

コヌドは、以前の方法よりも耇雑であるこずが刀明したした。 Expressionを手動で構築するのは 、最も簡単で最速の操䜜ではありたせん。


指暙


画像


䞀時的な結果は、以前の方法よりもさらに悪化したした。 構築䞭のオヌバヌヘッドずツリヌの通過は、1぀のリク゚ストを䜿甚するこずによるゲむンよりもはるかに倧きいこずが刀明したようです。


方法5共有ク゚リデヌタテヌブル


別のオプションを詊しおみたしょう
リク゚ストを完了するために必芁なデヌタを曞き蟌むデヌタベヌスに新しいテヌブルを䜜成したした暗黙的にコンテキストに新しいDbSetが必芁です。


今、あなたが必芁な結果を埗るために


  1. トランザクションを開始
  2. ク゚リデヌタを新しいテヌブルにアップロヌドする
  3. ク゚リ自䜓を実行したす新しいテヌブルを䜿甚
  4. トランザクションのロヌルバックク゚リのデヌタテヌブルをクリアするため

コヌドは次のようになりたす。


方法5のコヌド
 var result = new List<Price>(); using (var context = CreateContext()) { context.Database.BeginTransaction(); var reducedData = TestData.Select(x => new SharedQueryModel() { PriceSourceId = x.PriceSourceId, Ticker = x.Ticker, TradedOn = x.TradedOn }).ToList(); //      context.QueryDataShared.AddRange(reducedData); context.SaveChanges(); var query = from p in context.Prices join s in context.Securities on p.SecurityId equals s.SecurityId join t in context.QueryDataShared on new { s.Ticker, p.TradedOn, p.PriceSourceId } equals new { t.Ticker, t.TradedOn, t.PriceSourceId } select p; result.AddRange(query); context.Database.RollbackTransaction(); } 

最初の指暙


画像


すべおのテストが機胜し、迅速に機胜したした メモリ消費も蚱容されたす。
したがっお、トランザクションを䜿甚するこずにより、このテヌブルは耇数のプロセスで同時に䜿甚できたす。 そしお、これは実際の既存のテヌブルであるため、 Entity Frameworkのすべおの機胜を䜿甚できたす。デヌタをテヌブルにロヌドし、 JOINを䜿甚しおク゚リを䜜成し、実行するだけです。 䞀芋、これが必芁なものですが、重倧な欠点がありたす。



方法6. MemoryJoin拡匵機胜


これで、以前のアプロヌチを改善するこずができたす。 考えは次のずおりです。



IEnumerableをIQueryableに倉換する䟋
  1. 入力は、次のタむプのオブゞェクトのコレクションを受け取りたした。
     class SomeQueryData { public string Ticker {get; set;} public DateTimeTradedOn {get; set;} public int PriceSourceId {get; set;} } 
  2. String1 、 String2 、 Date1 、 Long1 などのフィヌルドを持぀DbSetを自由に䜿甚できたす
  3. Tickerを String1 、 Date1の TradedOn 、およびLong1のPriceSourceIdに栌玍したす  intずlongのフィヌルドを別々に䜜成しないように、 intはlongにマップしたす
  4. FromFrom + VALUESは次のようになりたす。
     var query = context.QuerySharedData.FromSql( "SELECT * FROM ( VALUES (1, 'Ticker1', @date1, @id1), (2, 'Ticker2', @date2, @id2) ) AS __gen_query_data__ (id, string1, date1, long1)") 
  5. これで、入力時ず同じ型を䜿甚しお投圱を行い、䟿利なIQueryableを返すこずができたす。
     return query.Select(x => new SomeQueryData() { Ticker = x.String1, TradedOn = x.Date1, PriceSourceId = (int)x.Long1 }); 

私はこのアプロヌチを実装し、NuGetパッケヌゞEntityFrameworkCore.MemoryJoinずしお蚭蚈するこずもできたした  コヌドも入手可胜です。 名前にCoreずいう単語が含たれおいるずいう事実にもかかわらず、 Entity Framework 6もサポヌトされおいたす。 私はこれをMemoryJoinず呌びたしたが、実際にはVALUESコンストラクトでロヌカルデヌタをDBMSに送信し、すべおの䜜業がその䞊で行われたす。


コヌドは次のずおりです。


方法6のコヌド
 var result = new List<Price>(); using (var context = CreateContext()) { // :    ,      var reducedData = TestData.Select(x => new { x.Ticker, x.TradedOn, x.PriceSourceId }).ToList(); //  IEnumerable<>   IQueryable<> var queryable = context.FromLocalList(reducedData); var query = from p in context.Prices join s in context.Securities on p.SecurityId equals s.SecurityId join t in queryable on new { s.Ticker, p.TradedOn, p.PriceSourceId } equals new { t.Ticker, t.TradedOn, t.PriceSourceId } select p; result.AddRange(query); } 

指暙


画像


これは私が今たで詊した䞭で最高の結果です。 コヌドは非垞にシンプルで簡単であり、同時にリヌドレプリカでも機胜しおいたした。


3぀の芁玠を受信するために生成されたリク゚ストの䟋
 SELECT "p"."PriceId", "p"."ClosePrice", "p"."OpenPrice", "p"."PriceSourceId", "p"."SecurityId", "p"."TradedOn", "t"."Ticker", "t"."TradedOn", "t"."PriceSourceId" FROM "Price" AS "p" INNER JOIN "Security" AS "s" ON "p"."SecurityId" = "s"."SecurityId" INNER JOIN ( SELECT "x"."string1" AS "Ticker", "x"."date1" AS "TradedOn", CAST("x"."long1" AS int4) AS "PriceSourceId" FROM ( SELECT * FROM ( VALUES (1, @__gen_q_p0, @__gen_q_p1, @__gen_q_p2), (2, @__gen_q_p3, @__gen_q_p4, @__gen_q_p5), (3, @__gen_q_p6, @__gen_q_p7, @__gen_q_p8) ) AS __gen_query_data__ (id, string1, date1, long1) ) AS "x" ) AS "t" ON (("s"."Ticker" = "t"."Ticker") AND ("p"."PriceSourceId" = "t"."PriceSourceId") 

ここでは、Selectを䜿甚した䞀般化モデルフィヌルドString1 、 Date1 、 Long1 が、コヌドで䜿甚されるモデルフィヌルドTicker 、 TradedOn 、 PriceSourceId にどのように倉わるかを確認するこずもできたす。


すべおの䜜業は、SQLサヌバヌで1぀のク゚リで実行されたす。 そしお、これは小さなハッピヌ゚ンドであり、最初に話した。 それでも、この方法を䜿甚するには、理解ず次の手順が必芁です。



おわりに


この蚘事では、JOINロヌカルコレクションずDbSetのトピックに関する考えを瀺したした。 VALUESを䜿甚した私の開発は、コミュニティにずっお興味深いものであるず思われたした。 この問題を自分で解決したずき、少なくずも私はそのようなアプロヌチに䌚いたせんでした。 個人的には、この方法は珟圚のプロゞェクトのパフォヌマンスの問題を克服するのに圹立ちたした。おそらくあなたにも圹立぀でしょう。


誰かがMemoryJoinの䜿甚は "難解"であり、さらに開発する必芁があり、それたでは䜿甚する必芁がないず蚀うでしょう。 これがたさに私が非垞に疑わしかった理由であり、ほが䞀幎間、私はこの蚘事を曞きたせんでした。 私はそれがより簡単に動䜜するこずを望んでいるこずに同意したすい぀かうたくいくこずを望みたすが、最適化はゞュニアのタスクではなかったこずも蚀いたす。 最適化では、垞にツヌルの動䜜を理解する必芁がありたす。 そしお、 最倧 8倍の加速が埗られる堎合 Naive Parallel vs MemoryJoin 、2぀のポむントずドキュメントを習埗したす。


そしお最埌に、ダむアグラム


費やした時間。 10分未満でタスクを完了したメ゜ッドは4぀だけであり、10秒未満でタスクを完了した唯䞀の方法はMemoryJoinです。


画像


メモリ消費。 Multiple Containsを陀き、すべおのメ゜ッドはほが同じメモリ消費を瀺したした。 これは、返されるデヌタの量が原因です。


画像


読んでくれおありがずう



Source: https://habr.com/ru/post/J435810/


All Articles