Assíncrono consultável do Entity Framework

96

Estou trabalhando em algumas coisas de API da Web usando o Entity Framework 6 e um dos meus métodos de controlador é um "Get All" que espera receber o conteúdo de uma tabela do meu banco de dados como IQueryable<Entity>. No meu repositório, estou me perguntando se há algum motivo vantajoso para fazer isso de forma assíncrona, já que sou novo no uso de EF com async.

Basicamente, tudo se resume 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();
 }

A versão assíncrona realmente renderá benefícios de desempenho aqui ou estou incorrendo em sobrecarga desnecessária ao projetar para uma lista primeiro (usando async, veja bem) e ENTÃO indo para IQueryable?

Jesse Carter
fonte
1
context.Urls é do tipo DbSet <URL> que implementa IQueryable <URL>, portanto .AsQueryable () é redundante. msdn.microsoft.com/en-us/library/gg696460(v=vs.113).aspx Supondo que você seguiu os padrões que a EF fornece ou usou as ferramentas que criam o contexto para você.
Sean B de

Respostas:

222

O problema parece ser que você não entendeu como async / await funcionam com o Entity Framework.

Sobre Entity Framework

Então, vamos dar uma olhada neste código:

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

e exemplo de seu uso:

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

O que acontece lá?

  1. Estamos obtendo IQueryableobjeto (não acessando banco de dados ainda) usandorepo.GetAllUrls()
  2. Nós criamos um novo IQueryable objeto com a condição especificada usando.Where(u => <condition>
  3. Nós criamos um novo IQueryable objeto com limite de paginação especificado usando.Take(10)
  4. Recuperamos os resultados do banco de dados usando .ToList(). Nosso IQueryableobjeto é compilado em sql (comoselect top 10 * from Urls where <condition> ). E o banco de dados pode usar índices, o sql server envia a você apenas 10 objetos do seu banco de dados (nem todos os bilhões de urls armazenados no banco de dados)

Ok, vamos dar uma olhada no primeiro código:

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

Com o mesmo exemplo de uso que obtivemos:

  1. Estamos carregando na memória todos os bilhões de urls armazenados em seu banco de dados usando await context.Urls.ToListAsync(); .
  2. Temos estouro de memória. Maneira certa de matar seu servidor

Sobre async / await

Por que async / await é preferido para usar? Vejamos este código:

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

o que acontece aqui?

  1. Começando na linha 1 var stuff1 = ...
  2. Enviamos um pedido ao servidor sql para o qual queremos obter algumas coisas1 userId
  3. Esperamos (a discussão atual está bloqueada)
  4. Esperamos (a discussão atual está bloqueada)
  5. .....
  6. Servidor Sql enviar para nós resposta
  7. Vamos para a linha 2 var stuff2 = ...
  8. Enviamos uma solicitação ao servidor sql para a qual queremos obter algumas coisas2 userId
  9. Esperamos (a discussão atual está bloqueada)
  10. E de novo
  11. .....
  12. Servidor Sql enviar para nós resposta
  13. Nós renderizamos a vista

Então, vamos olhar para uma versão assíncrona dele:

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

o que acontece aqui?

  1. Enviamos a solicitação ao servidor sql para obter stuff1 (linha 1)
  2. Enviamos a solicitação ao servidor sql para obter stuff2 (linha 2)
  3. Esperamos por respostas do servidor sql, mas o thread atual não está bloqueado, ele pode lidar com consultas de outros usuários
  4. Nós renderizamos a vista

Maneira certa de fazer isso

Bom código aqui:

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

Observe que você deve adicionar using System.Data.Entitypara usar o método ToListAsync()para IQueryable.

Observe que se você não precisa de filtragem, paginação e outras coisas, não precisa trabalhar com IQueryable. Você pode apenas usar await context.Urls.ToListAsync()e trabalhar com materializado List<Url>.

Viktor Lova
fonte
3
@Korijn olhando a imagem i2.iis.net/media/7188126/… de Introdução à arquitetura IIS , posso dizer que todas as solicitações no IIS são processadas de forma assíncrona
Viktor Lova
7
Como você não está agindo de acordo com o conjunto de resultados no GetAllUrlsByUsermétodo, não precisa torná-lo assíncrono. Basta retornar a Tarefa e evitar que uma máquina de estado desnecessária seja gerada pelo compilador.
Johnathon Sullinger
1
@JohnathonSullinger Embora funcione em um fluxo feliz, isso não tem o efeito colateral de que nenhuma exceção aparecerá aqui e se propagará para o primeiro lugar que tem uma espera? (Não que isso seja necessariamente ruim, mas é uma mudança de comportamento?)
Henry Sido
9
É interessante que ninguém perceba que o segundo exemplo de código em "About async / await" é totalmente sem sentido, porque lançaria uma exceção, pois nem EF nem EF Core são thread-safe, então tentar rodar em paralelo apenas lançará uma exceção
Tseng
1
Embora esta resposta esteja correta, recomendo evitar o uso asynce awaitse você NÃO estiver fazendo nada com a lista. Deixe o chamador fazer awaitisso. Ao aguardar a chamada neste estágio, return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();você está criando um wrapper assíncrono extra ao descompilar o assembly e examinar o IL.
Ali Khakpouri de
10

Há uma diferença enorme no exemplo que você postou, a primeira versão:

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

Isso é ruim , basicamente select * from tableretorna, retorna todos os resultados na memória e, em seguida, aplica o wherecontra aquele na coleção de memória em vez de select * from table where...contra o banco de dados.

O segundo método não irá realmente atingir o banco de dados até que uma consulta seja aplicada ao IQueryable(provavelmente através de um linq.Where().Select() operação no estilo que retornará apenas os valores db que correspondem à consulta.

Se seus exemplos forem comparáveis, a asyncversão normalmente será um pouco mais lenta por solicitação, pois há mais sobrecarga na máquina de estado que o compilador gera para permitir a asyncfuncionalidade.

No entanto, a principal diferença (e benefício) é que a asyncversão permite mais solicitações simultâneas, pois não bloqueia o thread de processamento enquanto aguarda a conclusão do IO (consulta de banco de dados, acesso a arquivo, solicitação da web, etc.).

Trevor Pilley
fonte
7
até que uma consulta seja aplicada a IQueryable .... nem IQueryable.Where e IQueryable.Select forçam a execução da consulta. O anterior aplica um predicado e o último aplica uma projeção. Não é executado até que um operador de materialização seja usado, como ToList, ToArray, Single ou First.
JJS de
0

Resumindo,
IQueryableé projetado para adiar o processo RUN e, em primeiro lugar, construir a expressão em conjunto com outras IQueryableexpressões e, em seguida, interpretar e executar a expressão como um todo.
Mas o ToList()método (ou alguns tipos de métodos como esse) são para executar a expressão instantaneamente "como está".
Seu primeiro método ( GetAllUrlsAsync), será executado imediatamente, pois é IQueryableseguido por ToListAsync()método. portanto, ele é executado instantaneamente (assíncrono) e retorna um monte de IEnumerables.
Enquanto isso, seu segundo método ( GetAllUrls) não será executado. Em vez disso, ele retorna uma expressão e CALLER desse método é responsável por executar a expressão.

Rzassar
fonte