web-dev-qa-db-ja.com

Entity Frameworkのクエリ可能な非同期

私はEntity Framework 6を​​使用していくつかのWeb APIに取り組んでおり、コントローラーメソッドの1つは、データベースのテーブルの内容をIQueryable<Entity>として受け取ることを期待する「Get All」です。私のリポジトリでは、非同期で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に行くことで不要なオーバーヘッドが発生しますか?

79
Jesse Carter

問題は、Entity Frameworkでasync/awaitがどのように機能するかを誤解しているように思われます。

Entity Frameworkについて

それでは、このコードを見てみましょう。

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

そして、その使用例:

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

そこで何が起きますか?

  1. repo.GetAllUrls()を使用してIQueryableオブジェクト(まだデータベースにアクセスしていない)を取得しています
  2. .Where(u => <condition>を使用して、指定された条件で新しいIQueryableオブジェクトを作成します
  3. .Take(10)を使用して、指定されたページング制限を持つ新しいIQueryableオブジェクトを作成します
  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. await context.Urls.ToListAsync();を使用して、データベースに保存されている10億のURLすべてをメモリにロードしています。
  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. userIdのstuff1を取得するリクエストをSQLサーバーに送信します
  3. 待機しています(現在のスレッドはブロックされています)
  4. 待機しています(現在のスレッドはブロックされています)
  5. .....
  6. SQLサーバーが応答を送信します
  7. 2行目に移動しますvar stuff2 = ...
  8. userIdのstuff2を取得するリクエストをSQLサーバーに送信します
  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. Stuff1を取得するためにSQLサーバーにリクエストを送信します(1行目)
  2. Stuff2を取得するためにSQLサーバーにリクエストを送信します(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のメソッドToListAsync()を使用するには、using System.Data.Entityを追加する必要があることに注意してください。

フィルタリングとページングなどが必要ない場合は、IQueryableを使用する必要がないことに注意してください。 await context.Urls.ToListAsync()を使用して、実体化されたList<Url>を操作するだけです。

194
Viktor Lova

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

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

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

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

あなたの例が比較可能である場合、コンパイラがasync機能を許可するために生成するステートマシンにより多くのオーバーヘッドがあるため、asyncバージョンは通常リクエストごとにわずかに遅くなります。

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

9
Trevor Pilley