实体框架可查询异步

本文关键字:异步 查询 框架 实体 | 更新日期: 2023-09-27 18:28:41

我正在使用Entity Framework 6处理一些Web API内容,我的控制器方法之一是"GetAll",它希望以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,从而产生不必要的开销?

实体框架可查询异步

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

关于实体框架

所以,让我们看看这个代码:

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

好的,让我们看看第一个代码:

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

使用相同的用法示例:

  1. 我们正在使用await context.Urls.ToListAsync();将存储在您数据库中的所有十亿个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. 我们向sql server发送请求,希望获得userId的一些stuff1
  3. 我们等待(当前线程被阻止)
  4. 我们等待(当前线程被阻止)
  5. Sql服务器发送给我们的响应
  6. 我们转到2号线var stuff2 = ...
  7. 我们向sql server发送请求,希望获得userId的一些stuff2
  8. 我们等待(当前线程被阻止)
  9. 再说一遍
  10. Sql服务器发送给我们的响应
  11. 我们渲染视图

因此,让我们看看它的异步版本:

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 server发送请求以获取stuff1(第1行)
  2. 我们向sql server发送请求以获取stuff2(第2行)
  3. 我们等待来自sql server的响应,但当前线程没有被阻止,他可以处理来自其他用户的查询
  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();
}

请注意,必须添加using System.Data.Entity才能将方法ToListAsync()用于IQueryable。

注意,如果您不需要过滤和分页之类的东西,那么就不需要使用IQueryable。您只需使用await context.Urls.ToListAsync()并使用物化的List<Url>即可。

您发布的示例中有一个巨大的差异,第一个版本:

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

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

第二种方法在对IQueryable应用查询之前不会实际命中数据库(可能是通过linq .Where().Select()风格的操作,该操作只返回与查询匹配的db值)

如果您的示例具有可比性,则async版本通常会稍微慢一点,因为编译器为允许async功能而生成的状态机中有更多的开销。

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

长话短说,
IQueryable被设计为推迟RUN过程,并首先将该表达式与其他IQueryable表达式一起构建,然后将该表达式作为一个整体进行解释和运行
但是ToList()方法(或类似的一些方法),需要立即"按原样"运行表达式
您的第一个方法(GetAllUrlsAsync)将立即运行,因为它是IQueryable,后面是ToListAsync()方法。因此它立即运行(异步),并返回一堆IEnumerable s。
同时,您的第二个方法(GetAllUrls)不会运行。相反,它返回一个表达式,该方法的CALLER负责运行该表达式。