[英]SelectMany query with Where produces many SQL queries
我正在使用GetAppRolesForUser函数(并尝试根据此处的答案进行变体):
private AuthContext db = new AuthContext();
...
var userRoles = Mapper.Map<List<RoleApi>>(
db.Users.SingleOrDefault(u => u.InternetId == username)
.Groups.SelectMany(g => g.Roles.Where(r => r.Asset.AssetName == application)));
我最终在SQL Profiler中为每个单个RolesId进行了以下处理:
exec sp_executesql N'SELECT
[Extent2].[GroupId] AS [GroupId],
[Extent2].[GroupName] AS [GroupName]
FROM [Auth].[Permissions] AS [Extent1]
INNER JOIN [Auth].[Groups] AS [Extent2] ON [Extent1].[GroupId] = [Extent2].[GroupId]
WHERE [Extent1].[RolesId] = @EntityKeyValue1',N'@EntityKeyValue1 int',@EntityKeyValue1=6786
如何重构,以便EF对userRoles生成单个查询,而无需花费18秒即可运行?
我认为问题在于您懒于加载组和角色。
一种解决方案是在调用SingleOrDefault
之前急于加载它们
var user = db.Users.Include(x => x.Groups.Select(y => y.Roles))
.SingleOrDefault(u => u.InternetId == username);
var groups = user.Groups.SelectMany(
g => g.Roles.Where(r => r.Asset.AssetName == application));
var userRoles = Mapper.Map<List<RoleApi>>(groups);
另请注意 :此处没有健全性检查是否为null。
TheGeneral的答案涵盖了为什么您会因懒惰加载而陷入困境。 您可能还需要包括Asset才能获得AssetName。
使用AutoMapper,可以通过在IQueryable
使用.ProjectTo<T>()
来避免急于加载实体的需要,前提是Group中有一个可访问的用户。
例如:
var roles = db.Groups.Where(g => g.User.Internetid == username)
.SelectMany(g => g.Roles.Where(r => r.Asset.AssetName == application))
.ProjectTo<RoleApi>()
.ToList();
这应该利用延迟执行,其中AutoMapper将根据您的映射/检查有效地投射所需的.Select()
来填充RoleApi实例。
这是避免延迟加载的另一种方法。 您还可以查看投影,仅包含所需的字段,而不是加载整个列。
var userRoles = Mapper.Map<List<RoleApi>>(
db.Users.Where(u => u.InternetId == username).Select(../* projection */ )
.Groups.SelectMany(g => g.Roles.Where(r => r.Asset.AssetName == application)));
EF还附带:
var userRoles = Mapper.Map<List<RoleApi>>(
db.Users.Where(u => u.InternetId == username).Select(../* projection */ )
.Include(g => g.Roles.Where(r => r.Asset.AssetName == application)));
然后可以使用多个for循环迭代集合。
您必须注意两个区别:
IQueryable<TResult>
函数(延迟)与返回TResult
函数(执行)之间的区别 Enumerable
和Queryable
。 可以在本地进程中处理AsEnumerable
LINQ语句。 它包含所有代码和执行该语句的所有调用。 MoveNext
GetEnumerator
和MoveNext
,将显式地或隐式地使用不返回IEnumerable<...>
foreach
或LINQ语句(例如ToList
, FirstOrDefault
和Any
来执行该语句。
相比之下, IQueryable
并不是要在您的流程中进行处理(但是,可以根据需要进行处理)。 它通常意味着要由不同的过程(通常是数据库管理系统)来处理。
为此, IQueryable
包含一个Expression
和一个Provider
。 Expression
表示必须执行的查询。 Provider
知道谁必须执行查询(DBMS),以及该执行者使用哪种语言(通常是SQL)。 MoveNext
GetEnumerator
和MoveNext
, Provider
将Expression
转换为Executor
的语言。 该查询不发送给执行者。 返回的数据显示为AsEnumerable
,其中MoveNext
GetEnumerator
和MoveNext
。
由于这种转换为SQL,所以IQueryable不能完成IEnumerable可以做的所有事情。 最主要的是它不能调用您的本地函数。 它甚至不能执行所有LINQ函数。 Provider
的质量越好,它可以做的越多。 查看受支持和不受支持的LINQ方法
有两组LINQ方法。 那些返回`IQueryable <...> / IEnumerable <...>的不返回。
第一组使用延迟加载。 这意味着在LINQ语句的末尾已创建了查询,但尚未执行。 只有“ GetEnumerator and
MoveNext” will make that the
提供程序will translate the
Expression并命令DBMS执行查询。
串联IQueryables
只会更改Expression
。 这是一个相当快的过程。 因此,如果执行一个大型LINQ表达式而不是将它们串联起来,则不会提高性能。
通常,与您的流程相比,DBMS更加智能,并且为进行选择做好了更好的准备。 将所选数据传输到本地过程是查询中较慢的部分之一。
建议:尝试创建LINQ语句,以使执行语句是DBMS可以执行的最后一条语句。 确保仅选择您实际计划使用的属性。
因此,例如,如果您不使用外键,请不要转移它们。
让映射器脱离您开始的问题:
db.Users.SingleOrDefault(...)
SingleOrDefault是非延迟函数。 它不返回IQueryable<...>
。 它将执行查询。 它将一个完整的User
(包括其Roles
传输到您的本地流程。
建议将SingleOrDefault推迟到最后一个语句:
var result = myDbcontext.Users
.Where(user => user.InternetId == username)
.SelectMany(user => user.Groups.Roles.Where(role => role.Asset.AssetName == application))
// until here, the query is not executed yet, execute it now:
.SingleOrDefault();
在的话:从序列Users
,只保留那些Users
与InternetId
,等于userName
。 从所有剩余的Users
(您希望只有一个)中,选择每个User
Groups
的Roles
序列。 但是,我们不希望选择所有Roles
,我们只保留AssetName
等于application
的Roles
。 现在,将所有剩余的Roles
放入一个集合中( SelectMany
的many
部分),然后选择期望的零或一个剩余Role
。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.