实体框架可查询异步
本文关键字:异步 查询 框架 实体 | 更新日期: 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()
那里发生了什么?
- 我们正在使用
repo.GetAllUrls()
获取IQueryable
对象(尚未访问数据库) - 我们使用
.Where(u => <condition>
创建一个具有指定条件的新IQueryable
对象 - 我们使用
.Take(10)
创建一个具有指定分页限制的新IQueryable
对象 - 我们使用
.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();
}
使用相同的用法示例:
- 我们正在使用
await context.Urls.ToListAsync();
将存储在您数据库中的所有十亿个URL加载到内存中 - 内存溢出。杀死服务器的正确方法
关于async/await
为什么首选async/await?让我们看看这个代码:
var stuff1 = repo.GetStuff1ForUser(userId);
var stuff2 = repo.GetStuff2ForUser(userId);
return View(new Model(stuff1, stuff2));
这里发生了什么?
- 在线路1
var stuff1 = ...
上启动 - 我们向sql server发送请求,希望获得
userId
的一些stuff1 - 我们等待(当前线程被阻止)
- 我们等待(当前线程被阻止)
- Sql服务器发送给我们的响应
- 我们转到2号线
var stuff2 = ...
- 我们向sql server发送请求,希望获得
userId
的一些stuff2 - 我们等待(当前线程被阻止)
- 再说一遍
- Sql服务器发送给我们的响应
- 我们渲染视图
因此,让我们看看它的异步版本:
var stuff1Task = repo.GetStuff1ForUserAsync(userId);
var stuff2Task = repo.GetStuff2ForUserAsync(userId);
await Task.WhenAll(stuff1Task, stuff2Task);
return View(new Model(stuff1Task.Result, stuff2Task.Result));
这里发生了什么?
- 我们向sql server发送请求以获取stuff1(第1行)
- 我们向sql server发送请求以获取stuff2(第2行)
- 我们等待来自sql server的响应,但当前线程没有被阻止,他可以处理来自其他用户的查询
- 我们渲染视图
正确的做法
这里有这么好的代码:
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负责运行该表达式。