实体框架可查询异步

我正在使用 Entity Framework 6处理一些 Web API 的东西,我的控制器方法之一是“ 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();
}

异步版本实际上会在这里产生性能优势吗? 还是我先投射到一个 List (使用异步注意) ,然后再投射到 IQueryable 会产生不必要的开销?

157460 次浏览

你发布的第一个例子有很大的不同:

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

这是 很糟糕,它基本上执行 select * from table,将所有结果返回到内存中,然后对内存集合中的结果应用 where,而不是对数据库执行 select * from table where...

第二个方法直到对 IQueryable应用了一个查询(可能是通过 linq .Where().Select()样式操作,该操作只返回与查询匹配的 db 值)才会真正触及数据库。

如果您的示例具有可比性,那么每个请求的 async版本通常会稍微慢一些,因为在状态机中存在更多的开销,编译器会生成这些开销以允许使用 async功能。

然而,主要的区别(和好处)是,async版本允许更多的并发请求,因为它不会阻塞处理线程,而它正在等待 IO 完成(数据库查询,文件访问,Web 请求等)。

问题似乎是您误解了实体框架的异步/等待工作方式。

关于实体框架

让我们看看这段代码:

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个对象(并非所有存储在数据库中的十亿个 URL)

好,我们来看第一条代码:

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

用同样的例子我们得到:

  1. 我们使用 await context.Urls.ToListAsync();在内存中加载存储在数据库中的所有十亿个 URL。
  2. 我们内存溢出了,正好可以干掉你的服务器

关于异步/等待

为什么首选使用异步/等待? 让我们看看下面的代码:

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

What happens here?

  1. 从第一行 var stuff1 = ...开始
  2. 我们向 sql 服务器发送请求,希望为 userId获得一些东西
  3. 我们等待(当前线程被阻塞)
  4. 我们等待(当前线程被阻塞)
  5. .....
  6. Sql server send to us response
  7. 我们到2号线 var stuff2 = ...
  8. 我们向 sql 服务器发送请求,希望为 userId获得一些东西
  9. 我们等待(当前线程被阻塞)
  10. And again
  11. .....
  12. 服务器向我们发送响应
  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. We render view

正确的做法

这里的代码很好:

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>

长话短说,
IQueryable的设计目标是延迟 RUN 过程,首先与其他 IQueryable表达式一起构建表达式,然后对表达式进行整体解释和运行。
但是 ToList()方法(或者一些类似的方法)可以立即“按原样”运行表达式。
您的第一个方法(GetAllUrlsAsync)将立即运行,因为它是 IQueryable后跟 ToListAsync()方法。因此它立即运行(异步) ,并返回大量的 IEnumerable
Meanwhile your second method (GetAllUrls), won't get run. Instead, it returns an expression and CALLER of this method is responsible to run the expression.