EntityFramework: Linq на SQL: содержит или IndexOf? - PullRequest
1 голос
/ 30 января 2020

У меня странная ситуация с EntityFramework 6 с. NET 4.5 (C#).

У меня (почти) один и тот же запрос в двух разных местах. Но один раз он снова запрашивает базу данных, а второй - запросы к объектам в памяти. И так как я фильтрую по подстроке, это принципиальное отличие:

Структура базы данных - это таблицы Role, Right и кросс-таблица 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, потому что он использует Contains метод string вместо преобразования его в SQL LIKE, а string.Contains чувствителен к регистру.

Моя проблема в том, что я не могу - исходя из кода - предсказать, когда запрос выполняется к базе данных и когда он выполняется в памяти, и поскольку я не могу использовать IndexOf в первом запросе и Contains во втором это кажется немного непредсказуемым для меня. Что происходит, когда однажды второй запрос выполняется первым, а данные еще не находятся в памяти?

Изменить 10 февраля 2020

ОК, поэтому я выяснил, что Основное отличие заключается в. context.Right имеет тип DbSet, который является IQueryable, как и последующий метод расширения Where. Однако userRole.Right возвращает ICollection, который является IEnumerable, как и последующие Where. Есть ли способ сделать свойство отношения объекта сущности к IQueryable? AsQueryable не работает. Это означает, что все связанные Right сущности всегда извлекаются из базы данных перед выполнением в памяти Where. Мы не говорим об огромных объемах данных, и, по крайней мере, сейчас это поведение предсказуемо, но, тем не менее, я считаю его неудачным.

Ответы [ 2 ]

1 голос
/ 30 января 2020

Моя проблема в том, что я не могу - исходя из кода - предсказать, когда запрос выполняется к базе данных и когда он выполняется в памяти, и так как я не могу использовать IndexOf в первом запросе и содержится в во-вторых, это кажется мне немного непредсказуемым.

Вы можете использовать IndexOf и Contains в обоих запросах, если вы не используете перегрузку с StringComparison. Как указывает @BrettCaswell, сопоставление регистра фиксируется с помощью сортировки вашей базы данных / таблицы / столбца. Запрос будет преобразован в SQL, если его root является значением DbSet контекста, и все вызовы метода переводятся в SQL.

Как только метод не может быть переведен, запрос текущего состояния выполняется на уровне SQL, а остальная часть запроса выполняется в памяти приложения. Net.

Также я думаю, что значение p__linq__0 должно быть '%~_a%', поскольку _ специальный символ в LIKE предложениях.

0 голосов
/ 10 февраля 2020

ОК, поэтому я нашел два разных решения, чтобы всегда выполнять запросы к базе данных в случае, если отношение содержит огромный набор результатов. Оба решения не являются интуитивно понятными - ИМХО - и вам понадобится переменная DbContext, которая ранее вам не нужна.

В первом решении используется таблица Role в качестве отправной точки и простая фильтрация для юридическое лицо с правильным 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)
...