エンティティフレームワーククエリ可能な非同期


96

私はEntity Framework 6を​​使用していくつかのWeb APIに取り組んでおり、私のコントローラーメソッドの1つは「Get All」で、データベースからテーブルのコンテンツをとして受け取ることを期待していますIQueryable<Entity>。私のリポジトリでは、非同期でEFを使用するのが初めてなので、これを非同期で実行する利点があるかどうか疑問に思っています。

基本的にそれは煮詰めます

 public async Task<IQueryable<URL>> GetAllUrlsAsync()
 {
    var urls = await context.Urls.ToListAsync();
    return urls.AsQueryable();
 }

 public IQueryable<URL> GetAllUrls()
 {
    return context.Urls.AsQueryable();
 }

非同期バージョンは実際にここでパフォーマンス上の利点をもたらしますか、それとも最初に(非同期を使用して)リストに投影し、次にIQueryableに移動することによって不要なオーバーヘッドを招きますか?


1
context.Urlsは、IQueryable <URL>を実装するDbSet <URL>タイプであるため、.AsQueryable()は冗長です。 msdn.microsoft.com/en-us/library/gg696460(v=vs.113).aspx EFが提供するパターンに従うか、またはコンテキストを作成するツールを使用したと仮定します。
Sean B

回答:


222

問題は、非同期/待機がEntity Frameworkでどのように機能するかを誤解しているようです。

Entity Frameworkについて

だから、このコードを見てみましょう:

public IQueryable<URL> GetAllUrls()
{
    return context.Urls.AsQueryable();
}

そしてその使用例:

repo.GetAllUrls().Where(u => <condition>).Take(10).ToList()

そこで何が起こりますか?

  1. を使用してIQueryableオブジェクトを取得しています(まだデータベースにアクセスしていません)。repo.GetAllUrls()
  2. IQueryable指定した条件で新しいオブジェクトを作成します.Where(u => <condition>
  3. IQueryable指定したページング制限で新しいオブジェクトを作成します.Take(10)
  4. を使用してデータベースから結果を取得します.ToList()。私たちのIQueryableオブジェクトは、SQL(のようにコンパイルされますselect top 10 * from Urls where <condition>)。データベースはインデックスを使用でき、SQLサーバーはデータベースから10個のオブジェクトのみを送信します(データベースに保存されているすべての10億のURLではありません)

さて、最初のコードを見てみましょう:

public async Task<IQueryable<URL>> GetAllUrlsAsync()
{
    var urls = await context.Urls.ToListAsync();
    return urls.AsQueryable();
}

私たちが得たのと同じ使用例で:

  1. を使用して、データベースに保存されているすべての10億のURLをメモリにロードしていますawait context.Urls.ToListAsync();
  2. メモリがオーバーフローしました。サーバーを強制終了する正しい方法

async / awaitについて

なぜasync / awaitの使用が推奨されるのですか?このコードを見てみましょう:

var stuff1 = repo.GetStuff1ForUser(userId);
var stuff2 = repo.GetStuff2ForUser(userId);
return View(new Model(stuff1, stuff2));

そこで何が起こるの?

  1. 1行目から var stuff1 = ...
  2. 何かを取得したいというリクエストをSQLサーバーに送信します1 userId
  3. 待機中(現在のスレッドはブロックされています)
  4. 待機中(現在のスレッドはブロックされています)
  5. .....
  6. SQLサーバーが応答を送信する
  7. 2行目に移動します var stuff2 = ...
  8. 何かを取得したいというリクエストをSQLサーバーに送信します userId
  9. 待機中(現在のスレッドはブロックされています)
  10. そしてまた
  11. .....
  12. SQLサーバーが応答を送信する
  13. ビューをレンダリングします

それでは、非同期バージョンを見てみましょう。

var stuff1Task = repo.GetStuff1ForUserAsync(userId);
var stuff2Task = repo.GetStuff2ForUserAsync(userId);
await Task.WhenAll(stuff1Task, stuff2Task);
return View(new Model(stuff1Task.Result, stuff2Task.Result));

そこで何が起こるの?

  1. SQLサーバーにリクエストを送信してstuff1を取得します(1行目)
  2. SQLサーバーにリクエストを送信してstuff2を取得します(2行目)
  3. SQLサーバーからの応答を待ちますが、現在のスレッドはブロックされず、別のユーザーからのクエリを処理できます
  4. ビューをレンダリングします

それを行う正しい方法

ここでとても良いコード:

using System.Data.Entity;

public IQueryable<URL> GetAllUrls()
{
   return context.Urls.AsQueryable();
}

public async Task<List<URL>> GetAllUrlsByUser(int userId) {
   return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();
}

IQueryableのusing System.Data.Entityメソッドを使用するために追加する必要があることに注意してくださいToListAsync()

フィルタリングやページングなどが必要ない場合は、で作業する必要がないことに注意してくださいIQueryableawait context.Urls.ToListAsync()マテリアライズドを使用して作業できList<Url>ます。


3
絵を見て@Korijn i2.iis.net/media/7188126/...からIISアーキテクチャの概要私はIISのすべての要求が非同期の方法で処理されていると言うことができます
ヴィクトルLOVA

7
GetAllUrlsByUserメソッドの結果セットを操作するわけではないので、非同期にする必要はありません。タスクを返して、コンパイラが不要なステートマシンを生成しないようにしてください。
Johnathon Sullinger、2016

1
@JohnathonSullingerこれはハッピーフローで機能しますが、例外がここに表示されず、待機している最初の場所に伝播しないという副作用はありませんか?(それは必ずしも悪いことではありませんが、それは振る舞いの変化ですか?)
ヘンリーは

9
「About async / await」の2番目のコード例がまったく意味がないことに気付く人はいないだろう。EFもEF Coreもスレッドセーフではないため例外がスローされるため、並列で実行しようとすると例外がスローされる
Tseng 2017

1
この答えは正しいですが、リストを使用asyncawaitて何もしていない場合は使用しないことをお勧めします。発信者にawaitそれを聞かせてください。この段階で呼び出しを待つreturn await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();と、アセンブリを逆コンパイルしてILを確認するときに、追加の非同期ラッパーが作成されます。
Ali Khakpouri

10

あなたが投稿した例、最初のバージョンには大きな違いがあります:

var urls = await context.Urls.ToListAsync();

これは悪いことです。基本的にはselect * from table、すべての結果をメモリに返し、データベースに対してではwhereなく、メモリコレクションの結果に対してそれを適用select * from table where...します。

2番目のメソッドは、クエリがIQueryable(おそらくlinq .Where().Select()スタイルの操作を介して)に適用されるまで実際にはデータベースにヒットしません。このクエリは、クエリに一致するdb値のみを返します。

例が同等の場合async、コンパイラーがasync機能を許可するために生成するステートマシンのオーバーヘッドが増えるため、バージョンは通常リクエストごとに少し遅くなります。

ただし、主な違い(および利点)は、asyncIOが完了するのを待っている間(dbクエリ、ファイルアクセス、Webリクエストなど)、処理スレッドをブロックしないため、より多くの同時リクエストを許可することです。


7
IQueryable ....にクエリが適用されるまで、IQueryable.WhereもIQueryable.Selectも強制的にクエリを実行しません。前者は述語を適用し、後者は射影を適用します。ToList、ToArray、Single、Firstなどのマテリアライズ演算子が使用されるまで実行されません。
JJS 2016年

0

簡単に言えば、
IQueryableRUNプロセスを延期し、最初に他のIQueryable式と組み合わせて式を作成し、次に式全体を解釈して実行するように設計されています。
しかし、ToList()メソッド(またはそのようないくつかのメソッド)は、式を「そのまま」即座に実行するためのものです。
最初のメソッド(GetAllUrlsAsync)は、メソッドがIQueryable後に続くため、すぐに実行されToListAsync()ます。したがって、即座に(非同期で)実行され、IEnumerablesの束を返します。
一方、2番目のメソッド(GetAllUrls)は実行されません。代わりに、式を返し、このメソッドのCALLERが式を実行します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.