Entity Framework Queryable async

98

Pracuję nad niektórymi elementami interfejsu API sieci Web przy użyciu Entity Framework 6 i jedną z moich metod kontrolera jest „Pobierz wszystko”, która oczekuje, że zawartość tabeli z mojej bazy danych zostanie odebrana jako IQueryable<Entity>. W moim repozytorium zastanawiam się, czy istnieje jakiś korzystny powód, aby robić to asynchronicznie, ponieważ jestem nowy w używaniu EF z asynchronicznie.

Zasadniczo sprowadza się to do

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

vs

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

Czy wersja asynchroniczna rzeczywiście przyniesie tutaj korzyści w zakresie wydajności, czy też poniosę niepotrzebne obciążenie, najpierw wykonując projekcję na Listę (używając asynchronicznego myślenia), a NASTĘPNIE przechodząc do IQueryable?

Jesse Carter
źródło
1
context.Urls jest typu DbSet <URL>, który implementuje IQueryable <URL>, więc .AsQueryable () jest redundantny. msdn.microsoft.com/en-us/library/gg696460(v=vs.113).aspx Zakładając, że postępowałeś zgodnie ze wzorcami udostępnianymi przez EF lub użyłeś narzędzi, które tworzą kontekst dla Ciebie.
Sean B

Odpowiedzi:

224

Wydaje się, że problem polega na tym, że źle zrozumiałeś, jak działa async / await z Entity Framework.

Informacje o Entity Framework

Spójrzmy więc na ten kod:

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

i przykład użycia:

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

Co tu się dzieje?

  1. Otrzymujemy IQueryableobiekt (jeszcze nie uzyskujemy dostępu do bazy danych) za pomocąrepo.GetAllUrls()
  2. Tworzymy nowy IQueryableobiekt z określonym warunkiem za pomocą.Where(u => <condition>
  3. Tworzymy nowy IQueryableobiekt z określonym limitem stronicowania za pomocą.Take(10)
  4. Pobieramy wyniki z bazy danych za pomocą .ToList(). Nasz IQueryableobiekt jest kompilowany do sql (jak select top 10 * from Urls where <condition>). Baza danych może korzystać z indeksów, serwer sql wysyła tylko 10 obiektów z bazy danych (nie wszystkie miliardy adresów URL przechowywane w bazie danych)

Dobra, spójrzmy na pierwszy kod:

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

Z tego samego przykładu użycia otrzymaliśmy:

  1. Ładujemy do pamięci wszystkie miliardy adresów URL przechowywanych w Twojej bazie danych za pomocą await context.Urls.ToListAsync();.
  2. Mamy przepełnienie pamięci. Właściwy sposób na zabicie serwera

Informacje o async / await

Dlaczego preferowane jest użycie async / await? Spójrzmy na ten kod:

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

co się tutaj stało?

  1. Zaczynając od linii 1 var stuff1 = ...
  2. Wysyłamy żądanie do serwera sql, dla którego chcemy pobrać trochę rzeczy1 userId
  3. Czekamy (bieżący wątek jest zablokowany)
  4. Czekamy (bieżący wątek jest zablokowany)
  5. .....
  6. Serwer SQL wyśle ​​do nas odpowiedź
  7. Przechodzimy do linii 2 var stuff2 = ...
  8. Wysyłamy żądanie do serwera sql, dla którego chcemy pobrać trochę stuff2 userId
  9. Czekamy (bieżący wątek jest zablokowany)
  10. I znowu
  11. .....
  12. Serwer SQL wyśle ​​do nas odpowiedź
  13. Renderujemy widok

Spójrzmy więc na wersję asynchroniczną:

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

co się tutaj stało?

  1. Wysyłamy żądanie do serwera sql, aby pobrać stuff1 (wiersz 1)
  2. Wysyłamy żądanie do serwera sql, aby pobrać stuff2 (linia 2)
  3. Czekamy na odpowiedzi z serwera sql, ale aktualny wątek nie jest blokowany, może obsłużyć zapytania od innych użytkowników
  4. Renderujemy widok

Właściwy sposób, aby to zrobić

Tak dobry kod tutaj:

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();
}

Uwaga, niż musisz dodać using System.Data.Entity, aby użyć metody ToListAsync()dla IQueryable.

Zwróć uwagę, że jeśli nie potrzebujesz filtrowania, stronicowania i innych rzeczy, nie musisz pracować z IQueryable. Możesz po prostu używać await context.Urls.ToListAsync()i pracować ze zmaterializowanymi List<Url>.

Viktor Lova
źródło
3
@Korijn patrząc na obrazek i2.iis.net/media/7188126/… z Wprowadzenie do architektury IIS Mogę powiedzieć, że wszystkie żądania w IIS są przetwarzane w sposób asynchroniczny
Viktor Lova
7
Ponieważ nie działasz na zestawie wyników w GetAllUrlsByUsermetodzie, nie musisz ustawiać go jako asynchronicznego. Po prostu zwróć zadanie i zapisz sobie niepotrzebną maszynę stanu przed wygenerowaniem przez kompilator.
Johnathon Sullinger,
1
@JohnathonSullinger Chociaż działałoby to w szczęśliwym przepływie, czy nie ma to efektu ubocznego, że żaden wyjątek nie pojawi się tutaj i nie rozprzestrzeni się do pierwszego miejsca, które czeka? (Nie to koniecznie złe, ale to zmiana w zachowaniu?)
Henry był
9
Interesujący nikt nie zauważył, że drugi przykład kodu w „About async / await” jest całkowicie bezsensowny, ponieważ zgłosiłby wyjątek, ponieważ ani EF, ani EF Core nie są bezpieczne wątkowo, więc próba uruchomienia równoległego spowoduje po prostu zgłoszenie wyjątku
Tseng
1
Chociaż ta odpowiedź jest prawidłowa, radziłbym unikać używania asynci awaitjeśli NIE robisz nic z listą. Niech dzwoni do awaittego. Kiedy czekasz na wywołanie na tym etapie return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();, tworzysz dodatkową otokę asynchroniczną podczas dekompilacji zestawu i patrzenia na IL.
Ali Khakpouri
10

W opublikowanym przykładzie jest ogromna różnica, pierwsza wersja:

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

To jest złe , w zasadzie tak select * from table, zwraca wszystkie wyniki do pamięci, a następnie stosuje whereprzeciw temu w kolekcji pamięci zamiast robić select * from table where...w bazie danych.

Druga metoda nie trafi w rzeczywistości do bazy danych, dopóki zapytanie nie zostanie zastosowane do IQueryable(prawdopodobnie za pośrednictwem .Where().Select()operacji w stylu linq , która zwróci tylko wartości db, które pasują do zapytania.

Jeśli twoje przykłady były porównywalne, asyncwersja będzie zwykle nieco wolniejsza na żądanie, ponieważ na maszynie stanów jest więcej narzutów, które generuje kompilator, aby umożliwić asyncfunkcjonalność.

Jednak główną różnicą (i korzyścią) jest to, że asyncwersja umożliwia więcej jednoczesnych żądań, ponieważ nie blokuje wątku przetwarzania podczas oczekiwania na zakończenie operacji we / wy (zapytanie bazy danych, dostęp do pliku, żądanie sieciowe itp.).

Trevor Pilley
źródło
7
dopóki zapytanie nie zostanie zastosowane do IQueryable .... ani IQueryable.Where, ani IQueryable.Select wymuszają wykonanie zapytania. Poprzednik stosuje predykat, a drugi rzutowanie. Nie jest wykonywany, dopóki nie zostanie użyty operator materializujący, taki jak ToList, ToArray, Single lub First.
JJS
0

Krótko mówiąc,
IQueryablema na celu opóźnienie procesu RUN i najpierw zbudowanie wyrażenia w połączeniu z innymi IQueryablewyrażeniami, a następnie interpretuje i uruchamia wyrażenie jako całość.
Jednak ToList()metoda method (lub kilka takich metod) ma na celu natychmiastowe uruchamianie wyrażenia „tak jak jest”.
Twoja pierwsza metoda ( GetAllUrlsAsync) zostanie uruchomiona natychmiast, ponieważ IQueryablenastępuje po niej ToListAsync()metoda method. dlatego działa natychmiast (asynchronicznie) i zwraca kilka IEnumerables.
W międzyczasie Twoja druga metoda ( GetAllUrls) nie zostanie uruchomiona. Zamiast tego zwraca wyrażenie, a CALLER tej metody jest odpowiedzialny za uruchomienie wyrażenia.

Rzassar
źródło