Запрос SelectMany с Where производит много запросов SQL - PullRequest
0 голосов
/ 30 октября 2018

Я использую для функции 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 выдавал один запрос для пользовательских ролей и не занимал 18 секунд для запуска?

Ответы [ 4 ]

0 голосов
/ 30 октября 2018

Вы должны знать о двух различиях:

  • Разница между IEnumerable и IQueryable
  • Разница между функциями, которые возвращают IQueryable<TResult> (ленивый), и функциями, которые возвращают TResult (выполнение)

Разница между Enumerable и Queryable

. Оператор LINQ AsEnumerable предназначен для обработки в локальном процессе. Он содержит весь код и все вызовы для выполнения оператора. Этот оператор выполняется, как только вызываются GetEnumerator и MoveNext, явно или неявно, с использованием операторов foreach или LINQ, которые не возвращают IEnumerable<...>, например ToList, FirstOrDefault и Any.

Напротив, IQueryable не предназначен для обработки в вашем процессе (однако это может быть сделано, если вы хотите). Обычно он обрабатывается другим процессом, обычно системой управления базами данных.

Для этого IQueryable содержит Expression и Provider. Expression представляет запрос, который должен быть выполнен. Provider знает, кто должен выполнить запрос (СУБД) и на каком языке этот исполнитель использует (обычно SQL). Когда вызываются GetEnumerator и MoveNext, Provider берет Expression и переводит его на язык Executor. Запрос не отправлен исполнителю. Возвращенные данные представлены AsEnumerable, где вызываются GetEnumerator и MoveNext.

Из-за этого перевода в SQL IQueryable не может делать все то, что может делать IEnumerable. Главное, что он не может вызывать ваши локальные функции. Он не может даже выполнить все функции LINQ. Чем лучше качество Provider, тем больше он может сделать. См. поддерживаемые и неподдерживаемые методы LINQ

Ленивые методы LINQ и выполнение методов LINQ

Существует две группы методов LINQ. Те, которые возвращают `IQueryable <...> / IEnumerable <...> и те, которые не возвращают.

Первая группа использует ленивую загрузку. Это означает, что в конце оператора LINQ запрос был создан, но он еще не выполнен. Только 'GetEnumerator and MoveNext will make that the Provider will translate the Expression` и приказать СУБД выполнить запрос.

Объединение IQueryables изменит только Expression. Это довольно быстрая процедура. Следовательно, производительность не улучшится, если вы создадите одно большое выражение LINQ вместо того, чтобы объединить их перед выполнением запроса.

Обычно СУБД умнее и лучше подготовлена ​​для выбора, чем ваш процесс. Перенос выбранных данных в локальный процесс является одной из медленных частей вашего запроса.

Совет: попробуйте создать ваши операторы LINQ такими, чтобы выполняющиеся оператор является последним, который может быть выполнен СУБД. Удостовериться что вы выбираете только те свойства, которые фактически планируете использовать.

Так, например, не передавайте внешние ключи, если вы ими не пользуетесь.

Вернуться к вашему вопросу

Оставляя маппер вне вопроса, с которого вы начинаете:

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 (которые, как вы надеетесь, будет только один), выберите последовательность Roles из Groups каждого User. Однако мы не хотим выбирать все Roles, мы сохраняем только Roles с AssetName равным application. Теперь поместите все оставшиеся Roles в одну коллекцию (часть many в SelectMany) и выберите ноль или один оставшийся Role, который вы ожидаете.

0 голосов
/ 30 октября 2018

Ответ TheGeneral описывает, почему вас застает ленивая загрузка. Вам также может понадобиться включить Asset для получения AssetName.

С AutoMapper вы можете избежать необходимости загружать объекты, используя .ProjectTo<T>() в IQueryable, при условии, что пользователь доступен в группе.

Например:

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 на основе вашего сопоставления / проверки.

0 голосов
/ 30 октября 2018

Вот еще один способ избежать отложенной загрузки. Вы также можете смотреть на проекцию и иметь только те поля, которые вам нужны, а не загружать все столбцы.

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.

0 голосов
/ 30 октября 2018

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

Одно из решений - загрузить их, прежде чем позвонить 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);

Также обратите внимание : здесь нет проверки работоспособности на ноль.

...