Entity Framework Queryable async


97

Sto lavorando su alcune cose dell'API Web utilizzando Entity Framework 6 e uno dei metodi del mio controller è "Ottieni tutto" che si aspetta di ricevere il contenuto di una tabella dal mio database come file IQueryable<Entity>. Nel mio repository mi chiedo se ci sia qualche ragione vantaggiosa per farlo in modo asincrono dato che sono nuovo nell'uso di EF con async.

Fondamentalmente si riduce a

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

vs

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

La versione asincrona produrrà effettivamente vantaggi in termini di prestazioni qui o sto incorrendo in un sovraccarico non necessario proiettando prima su un elenco (usando async, intendiamoci) e POI andando su IQueryable?


1
context.Urls è di tipo DbSet <URL> che implementa IQueryable <URL>, quindi .AsQueryable () è ridondante. msdn.microsoft.com/en-us/library/gg696460(v=vs.113).aspx Supponendo che tu abbia seguito i modelli forniti da EF o abbia utilizzato gli strumenti che creano il contesto per te.
Sean B

Risposte:


223

Il problema sembra essere che hai frainteso il funzionamento di async / await con Entity Framework.

Informazioni su Entity Framework

Quindi, diamo un'occhiata a questo codice:

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

ed esempio di utilizzo:

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

Cosa succede lì?

  1. Stiamo ottenendo IQueryableoggetto (non ancora accedendo al database) utilizzandorepo.GetAllUrls()
  2. Creiamo un nuovo IQueryableoggetto con la condizione specificata usando.Where(u => <condition>
  3. Creiamo un nuovo IQueryableoggetto con il limite di paging specificato utilizzando.Take(10)
  4. Recuperiamo i risultati dal database utilizzando .ToList(). Il nostro IQueryableoggetto è compilato in sql (like select top 10 * from Urls where <condition>). E il database può utilizzare gli indici, il server sql ti invia solo 10 oggetti dal tuo database (non tutti i miliardi di URL memorizzati nel database)

Ok, diamo un'occhiata al primo codice:

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

Con lo stesso esempio di utilizzo abbiamo ottenuto:

  1. Stiamo caricando in memoria tutti i miliardi di URL memorizzati nel tuo database utilizzando await context.Urls.ToListAsync();.
  2. Abbiamo un overflow di memoria. Il modo giusto per uccidere il tuo server

Informazioni su async / await

Perché è preferibile utilizzare async / await? Diamo un'occhiata a questo codice:

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

Che succede qui?

  1. A partire dalla linea 1 var stuff1 = ...
  2. Inviamo la richiesta al server sql per cui vogliamo ottenere qualcosa1 userId
  3. Aspettiamo (il thread corrente è bloccato)
  4. Aspettiamo (il thread corrente è bloccato)
  5. .....
  6. Il server SQL inviaci la risposta
  7. Passiamo alla riga 2 var stuff2 = ...
  8. Inviamo una richiesta al server sql per cui vogliamo ottenere qualcosa2 userId
  9. Aspettiamo (il thread corrente è bloccato)
  10. E di nuovo
  11. .....
  12. Il server SQL inviaci la risposta
  13. Rendiamo la vista

Quindi diamo un'occhiata a una versione asincrona di esso:

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

Che succede qui?

  1. Inviamo una richiesta al server sql per ottenere stuff1 (riga 1)
  2. Inviamo una richiesta al server sql per ottenere stuff2 (riga 2)
  3. Attendiamo risposte dal server sql, ma il thread corrente non è bloccato, può gestire le query di altri utenti
  4. Rendiamo la vista

Modo giusto per farlo

Quindi buon codice qui:

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

Nota che devi aggiungere using System.Data.Entityper utilizzare il metodo ToListAsync()per IQueryable.

Nota che se non hai bisogno di filtri, paging e cose del genere, non devi lavorare con IQueryable. Puoi semplicemente usare await context.Urls.ToListAsync()e lavorare con materialized List<Url>.


3
@Korijn guardando l'immagine i2.iis.net/media/7188126/… da Introduzione all'architettura IIS posso dire che tutte le richieste in IIS vengono elaborate in modo asincrono
Viktor Lova

7
Poiché non stai agendo sul set di risultati nel GetAllUrlsByUsermetodo, non è necessario renderlo asincrono. Restituisci semplicemente l'attività e salva una macchina a stati non necessaria che viene generata dal compilatore.
Johnathon Sullinger

1
@JohnathonSullinger Anche se ciò funzionerebbe con un flusso felice, non ha l'effetto collaterale che nessuna eccezione emergerà qui e si propagherà al primo posto che ha un'attesa? (Non che sia necessariamente negativo, ma è un cambiamento nel comportamento?)
Henry Been

9
È interessante che nessuno noti che il secondo esempio di codice in "Informazioni su async / await" è totalmente privo di senso, perché genererebbe un'eccezione poiché né EF né EF Core sono thread-safe, quindi provare a eseguire in parallelo genererà un'eccezione
Tseng

1
Sebbene questa risposta sia corretta, ti consiglio di evitare di utilizzare asynce awaitse NON stai facendo nulla con l'elenco. Lascia che il chiamante await. Quando si attende la chiamata in questa fase, return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();si crea un wrapper asincrono aggiuntivo quando si decompila l'assembly e si osserva l'IL.
Ali Khakpouri

10

C'è un'enorme differenza nell'esempio che hai pubblicato, la prima versione:

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

Questo è un male , fondamentalmente lo fa select * from table, restituisce tutti i risultati in memoria e quindi applica il wherecontro quello nella raccolta della memoria invece di farlo select * from table where...contro il database.

Il secondo metodo non raggiungerà effettivamente il database finché non viene applicata una query al IQueryable(probabilmente tramite un'operazione in .Where().Select()stile linq che restituirà solo i valori db che corrispondono alla query.

Se i tuoi esempi fossero comparabili, la asyncversione sarà generalmente leggermente più lenta per richiesta poiché c'è più overhead nella macchina a stati che il compilatore genera per consentire la asyncfunzionalità.

Tuttavia la principale differenza (e vantaggio) è che la asyncversione consente più richieste simultanee in quanto non blocca il thread di elaborazione mentre è in attesa del completamento dell'IO (query db, accesso ai file, richiesta web ecc.).


7
finché una query non viene applicata a IQueryable .... né IQueryable.Where e IQueryable.Select forzano l'esecuzione della query. Il precedente applica un predicato e il secondo applica una proiezione. Non viene eseguito finché non viene utilizzato un operatore di materializzazione, come ToList, ToArray, Single o First.
JJS

0

Per farla breve,
IQueryableè progettato per posticipare il processo RUN e prima costruire l'espressione insieme ad altre IQueryableespressioni, quindi interpreta ed esegue l'espressione nel suo insieme.
Ma il ToList()metodo (o alcuni tipi di metodi come quello), sono destinati a eseguire l'espressione istantaneamente "così com'è".
Il tuo primo metodo ( GetAllUrlsAsync) verrà eseguito immediatamente, perché è IQueryableseguito da ToListAsync()metodo. quindi viene eseguito istantaneamente (asincrono) e restituisce un mucchio di IEnumerables.
Nel frattempo il tuo secondo metodo ( GetAllUrls), non verrà eseguito. Invece, restituisce un'espressione e CALLER di questo metodo è responsabile di eseguire l'espressione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.