EntityFramework:SQL 上的 Linq:包含或 IndexOf?

EntityFramework: Linq on SQL: Contains or IndexOf?

关于使用 .NET 4.5 (C#) 的 EntityFramework 6,我有一个奇怪的情况。

我在两个不同的地方有(几乎)相同的查询。但是一次它查询数据库,第二次查询内存中的对象。因为我过滤的是子字符串,所以这是一个关键的区别:

数据库结构都是tables Role, right and a crosstable Role_Right

我第一次想找到所有尚未分配给该角色的可用权限加上(这就是它变得复杂的地方)一个手动过滤器来减少结果列表:

Role role = ...;
string filter = ...;
var roleRightNames = role.Right.Select(roleRight => roleRight.RightName);
var filteredRights = context.Right.Where(right => !roleRightNames.Contains(right.RightName));
if (!string.IsNullOrWhiteSpace(filter))
{
    filteredRights = filteredRights.Where(e => e.RightName.Contains(filter));
}
var result = filteredRights.ToList();

我无法使用 IndexOf(filter, StringComparison.InvariantCultureIgnoreCase) >= 0),因为这无法转换为 SQL。但我对 Contains 没问题,因为它会产生所需的结果(见下文)。

启用 SQL 输出时,我得到:

SELECT [Extent1].[RightName] AS [RightName]
FROM [dbo].[Right] AS [Extent1]
WHERE ( NOT ([Extent1].[RightName] IN ('Right_A1', 'Right_A2', 'Right_B1'))) AND ([Extent1].[RightName] LIKE @p__linq__0 ESCAPE '~'
-- p__linq__0: '%~_a%' (Type = AnsiString, Size = 8000)

这正是我想要的,在过滤器“_a”上进行不区分大小写的搜索以查找例如 'Right_A3'

我第二次要为同一个过滤器过滤现有的关联权限:

Role role = ...;
string filter = ...;
var filteredRights = string.IsNullOrWhiteSpace(filter)
    ? role.Right
    : role.Right.Where(e => e.RightName.IndexOf(filter, StringComparison.InvariantCultureIgnoreCase) >= 0);            
var result = filteredRights.ToList();

这次它迫使我使用 IndexOf 因为它使用 stringContains 方法而不是将其转换为 SQL LIKE string.Contains 区分大小写。

我的问题是我无法 - 通过查看代码 - 预测何时对数据库执行查询以及何时在内存中完成查询,因为我不能在第一个查询中使用 IndexOfContains 第二个对我来说似乎有点不可思议table。如果有一天第二个查询先执行并且数据不在内存中,会发生什么情况?

编辑 2020 年 2 月 10 日

好的,所以我弄清楚了主要区别是什么。 context.Right 属于 DbSet 类型,它是 IQueryable 类型,后续扩展方法 Where 也是如此。但是 userRole.Right returns 一个 ICollection 是一个 IEnumerable ,随后的 Where 也是如此。有没有办法使实体对象的关系 属性 成为 IQueryableAsQueryable 无效。这意味着所有关联的 Right 实体总是在执行内存中 Where 之前从数据库中获取。 我们不是在谈论大量数据,至少现在这种行为是预测的table,但我仍然觉得它很不幸。

My problem is that I cannot - from looking at the code - predict when a query is executed against the database and when it is done in-memory and since I cannot use IndexOf in the first query and Contains in the second this seems to be a bit unpredictable to me.

您可以在两个查询中使用 IndexOfContains,只要您不使用具有 StringComparison 的重载。正如@BrettCaswell 所指出的,大小写匹配由 Database/Table/Column 的排序规则确定。 如果查询的根是上下文的 DbSet 并且所有方法调用都可转换为 SQL.

,则该查询将被转换为 SQL

一旦无法转换方法,就会在 SQL 级别执行当前状态请求,并且在 .Net 应用程序的内存中执行查询的其余部分。

另外我认为 p__linq__0 值应该是 '%~_a%' 因为 _LIKE 子句中的特殊字符。

好的,所以我找到了两种不同的解决方案,以在关系包含巨大结果集的情况下始终查询数据库。这两种解决方案都不是直接直观的 - 恕我直言 - 你将需要你以前不需要的 DbContext 变量。

解决方案一是使用 Role table 作为起点并简单地过滤具有正确 ID 的实体。 注意您不能使用Single,因为那样您处理的是单个实体对象,您又回到了开始的地方。您需要使用 Where,然后使用 SelectMany,即使它违反直觉:

Role role = ...;
string filter = ...;
var filteredRights = context.Role.Where(e => e.RoleId == userRole.RoleId).SelectMany(e => e.Right);
if (!string.IsNullOrWhiteSpace(filter))
{
    filteredRights = filteredRights.Where(e => e.RightName.Contains(filter));
}
var rights = filteredRights.ToList();

这导致对数据库的 SQL 查询:

SELECT 
    [Extent1].[RightName] AS [RightName]
    FROM [dbo].[Role_Right] AS [Extent1]
    WHERE ([Extent1].[RoleId] = @p__linq__0) AND ([Extent1].[RightName] LIKE @p__linq__1 ESCAPE '~')
-- p__linq__0: '42' (Type = Int32, IsNullable = false)
-- p__linq__1: '%~_a%' (Type = AnsiString, Size = 8000)

我在这里找到的第二个解决方案:

在我的例子中,结果是:

Role role = ...;
string filter = ...;
var filteredRights = context.Entry(userRole).Collection(e => e.Right).Query();
if (!string.IsNullOrWhiteSpace(filter))
{
    filteredRights = filteredRights.Where(e => e.RightName.Contains(filter));
}
var rights = filteredRights.ToList();

和SQL

SELECT 
    [Extent1].[RightName] AS [RightName]
    FROM [dbo].[Role_Right] AS [Extent1]
    WHERE ([Extent1].[RoleId] = @EntityKeyValue1) AND ([Extent1].[RightName] LIKE @p__linq__0 ESCAPE '~')
-- EntityKeyValue1: '42' (Type = Int32, IsNullable = false)
-- p__linq__0: '%~_a%' (Type = AnsiString, Size = 8000)