Я пытаюсь перечислить все элементы с дополнительным столбцом, описывающим, принадлежит ли он текущему пользователю.
Итак, я ищу запрос 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
...
}
}
Так что я действительно могу материализовать весь элемент, мне просто нужно явно ввести каждое последнее свойство. ВЕСЕЛЬЕ!
Не совсем то же самое. LinqPad запускает его примерно за 0,2 секунды за одну поездку на сервер sql. В моем проекте один и тот же запрос выполняется за секунды с использованием сотен запросов. Я добавил еще немного кода из SQL-запросов моих проектов.
Ах, после того, как вы обновили свой вопрос, становится намного понятнее, что вы имеете в виду. Я считаю, что опубликованный ответ поможет вам решить эту проблему.
Вероятно, вам нужно включить активную загрузку для свойства навигации 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 после его фактического запуска)
Я очень удивлен, после прочтения ваших ссылок я все еще немного сбит с толку, но не материализация Items на самом деле работает. Они мне действительно понадобятся полностью, но я могу это обойти. Я пока отмечаю это как ответ. Я все еще надеюсь, что просто не загружаю что-то с энтузиазмом. Спасибо!
Вам нужно указать EF загрузить связанные данные, в данном случае таблицу OwnedItems.
Один из способов сделать это - Включают связанной таблицы. Если есть внешний ключ, который связывает таблицы, это можно легко сделать следующим образом:
var dataWithRelatedData = db_context.Items.Include(x => x.OwnedItems).Select ...
Другой способ избежать большого количества обращений к базе данных - загрузить оба набора данных в отдельные запросы, а затем объединить их в памяти. Таким образом, вы сначала сделаете запрос к Предметы, затем с данными, вернув другой запрос к OwnedItems, и, наконец, объедините их в единый список объектов. Это сделает всего 2 обращения к базе данных, что повысит производительность.
В таблице OwnedItems есть столбец ItemId, который является внешним ключом для таблицы Items. Обратной ссылки нет, так как элемент может принадлежать нескольким пользователям. Так что ваш точный код может не работать, но я пытаюсь как-то его загрузить.
Не имеет значения, является ли отношение 1-1, 1- * или -, вы должны иметь возможность включать любые связанные данные с помощью Включают, если переход к связанным таблицам определен в модели EF.
Спасибо за помощь, я все еще борюсь с включением стороны «Многие» в отношение «1-много», или если это вообще имеет смысл.
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?