Entity Framework Core LINQ создает проблемы при создании (выбранный случай существует) запросы

Я пытаюсь перечислить все элементы с дополнительным столбцом, описывающим, принадлежит ли он текущему пользователю.

Итак, я ищу запрос Linq, который генерирует что-то вроде следующего SQL:

SELECT *,
   CASE WHEN
     EXISTS (
       SELECT NULL FROM OwnedItems
       WHERE OwnedItems.UserId = @UserId AND OwnedItems.ItemId = Items.Id
   )
   THEN 'true'
   ELSE 'false'
   END AS Owned
FROM Items;

Согласно Интернету, а также успешному эксперименту с LinqPad, этот код должен работать.

from item in Items
select new
{
   Owned = OwnedItems.Any(own => own.UserId == userId && own.ItemId == item.Id),
   Item = item
}

В LinqPad этот код генерирует тот же SQL, что и я. Но в моем проекте это совсем другое.

Мой код - это проект .Net Core 2.1 с использованием Entity Framework Core 2.1. Поскольку это основной проект, я не могу напрямую протестировать его в LinqPad, поскольку он еще не поддерживается.

В моем проекте этот код приводит к нефильтрованному оператору SELECT, запрашивающему каждый элемент, а затем для каждого из них отдельный запрос, чтобы проверить, существует ли он в таблице OwnedItems. Нравится:

Выполняется 1 экземпляр этого запроса:

Executed DbCommand (68ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
  SELECT *
  FROM [Items] AS [item]

За ними следуют сотни таких запросов, выполнение которых занимает несколько секунд:

 Executed DbCommand (32ms) [Parameters=[@__userId_0='?' (DbType = Int32), @_outer_Id='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
  SELECT CASE
      WHEN EXISTS (
          SELECT 1
          FROM [OwnedItems] AS [ownedItems]
          WHERE ([ownedItems].[UserId] = @__userId_0) AND ([ownedItems].[ItemId] = @_outer_Id))
      THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
  END

Дополнительная информация, может быть, это поможет: Если я использую ту же строку как часть предложения where, она работает отлично.

var q = from item in Items
    where OwnedItems.Any(o => o.UserId == userId && o.ItemId == item.Id)
    select item;

Вышеупомянутый linq приводит к этому красивому sql:

SELECT *
  FROM [Items] AS [item]
  WHERE EXISTS (
      SELECT 1
      FROM [OwnedItems] AS [o]
      WHERE ([o].[UserId] = @__userId_0) AND ([o].[ItemId] = [item].[Id]))

Примечания:

  • Приведенный выше код был изменен вручную, поэтому в нем могут быть опечатки. Пожалуйста, не обращайте на них внимания.

  • Я понимаю, что этот конкретный запрос может быть выполнен с использованием левого соединения и проверки на нули, но мой фактический запрос более сложен, и мне нужны (вложенные) предложения exists.

ОБНОВЛЕНИЕ РЕШЕНИЯ

Как указал @KorsG, если элемент не материализован, создается правильный запрос. Я обнаружил, что отсутствие материализации Item работает, даже если я напишу следующее:

from item in Items
select new
{
   Owned = OwnedItems.Any(own => own.UserId == userId && own.ItemId == item.Id),
   // Item = item  //THIS LINE GENERATES BAD QUERY
   Item = new Item {
       Id = item.Id,
       Name = item.Name,
       ...
       [Literally every single property listed one by one] = item.CorrespondingProperty
       ...
   }
}

Так что я действительно могу материализовать весь элемент, мне просто нужно явно ввести каждое последнее свойство. ВЕСЕЛЬЕ!

In my project this code results in an unfiltered SELECT statement querying every Item, then for each of them a separate query to check if it exists in the OwnedItems table. - Ну, это то, что делает ваш желаемый запрос. Можем ли мы увидеть результаты запроса EF Core 2.1?
Mark C. 12.12.2018 16:53

Не совсем то же самое. LinqPad запускает его примерно за 0,2 секунды за одну поездку на сервер sql. В моем проекте один и тот же запрос выполняется за секунды с использованием сотен запросов. Я добавил еще немного кода из SQL-запросов моих проектов.

Áron Ürögdi 12.12.2018 17:02

Ах, после того, как вы обновили свой вопрос, становится намного понятнее, что вы имеете в виду. Я считаю, что опубликованный ответ поможет вам решить эту проблему.

Mark C. 12.12.2018 17:06
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
3
1 482
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Вероятно, вам нужно включить активную загрузку для свойства навигации OwnedItems в запросе: https://docs.microsoft.com/en-us/ef/core/querying/related-data#eager-loading

Пожалуйста, опубликуйте свой полный запрос linq, если я должен привести пример.

ОБНОВЛЕНИЕ 1

Похоже, что у подзапросов есть проблемы N + 1 в EF Core, и, возможно, это будет исправлено в версии 3.

Ссылка: https://github.com/aspnet/EntityFrameworkCore/issues/10001

ОБНОВЛЕНИЕ 2

Если вам не нужно полностью материализовать «Items», вы сможете сделать что-то вроде этого, вместо этого вы создадите анонимный объект, который должен «обмануть» EF в том, что вы хотите:

from item in Items
select new
{
   Owned = OwnedItems.Any(own => own.UserId == userId && own.ItemId == item.Id),
   Item = new { Id = item.Id, Name = item.Name }
}

Ссылка: https://github.com/aspnet/EntityFrameworkCore/issues/11186

Я не использую свойства навигации здесь, по крайней мере, я не уверен, как это поможет. Но, возможно, вы на правильном пути, я просто не совсем понимаю. Я обновил вопрос, чтобы включить версию с той же строкой в ​​предложение «где». Этот работает. Как вы думаете, проблема может быть в отложенной загрузке? (кстати, опубликованный выше linq - это весь запрос, я просто помещаю ToList после его фактического запуска)

Áron Ürögdi 12.12.2018 17:12

Я очень удивлен, после прочтения ваших ссылок я все еще немного сбит с толку, но не материализация Items на самом деле работает. Они мне действительно понадобятся полностью, но я могу это обойти. Я пока отмечаю это как ответ. Я все еще надеюсь, что просто не загружаю что-то с энтузиазмом. Спасибо!

Áron Ürögdi 12.12.2018 18:02

Вам нужно указать EF загрузить связанные данные, в данном случае таблицу OwnedItems.

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

var dataWithRelatedData = db_context.Items.Include(x => x.OwnedItems).Select ...

Другой способ избежать большого количества обращений к базе данных - загрузить оба набора данных в отдельные запросы, а затем объединить их в памяти. Таким образом, вы сначала сделаете запрос к Предметы, затем с данными, вернув другой запрос к OwnedItems, и, наконец, объедините их в единый список объектов. Это сделает всего 2 обращения к базе данных, что повысит производительность.

В таблице OwnedItems есть столбец ItemId, который является внешним ключом для таблицы Items. Обратной ссылки нет, так как элемент может принадлежать нескольким пользователям. Так что ваш точный код может не работать, но я пытаюсь как-то его загрузить.

Áron Ürögdi 12.12.2018 17:22

Не имеет значения, является ли отношение 1-1, 1- * или -, вы должны иметь возможность включать любые связанные данные с помощью Включают, если переход к связанным таблицам определен в модели EF.

Th0rndike 12.12.2018 17:24

Спасибо за помощь, я все еще борюсь с включением стороны «Многие» в отношение «1-много», или если это вообще имеет смысл.

Áron Ürögdi 12.12.2018 17:59

Другие вопросы по теме