EF Core вложенные результаты Linq select в N + 1 SQL-запросах
У меня есть модель данных, где объект "Top" имеет от 0 до N "Sub" объектов. В SQL это достигается с помощью внешнего ключа dbo.Sub.TopId
,
var query = context.Top
//.Include(t => t.Sub) Doesn't seem to do anything
.Select(t => new {
prop1 = t.C1,
prop2 = t.Sub.Select(s => new {
prop21 = s.C3 //C3 is a column in the table 'Sub'
})
//.ToArray() results in N + 1 queries
});
var res = query.ToArray();
В Entity Framework 6 (с отложенной загрузкой) этот запрос Linq будет преобразован в один запрос SQL. Результат будет полностью загружен, поэтому res[0].prop2
будет IEnumerable<SomeAnonymousType>
который уже заполнен.
Однако при использовании EntityFrameworkCore (NuGet v1.1.0) вложенная коллекция еще не загружена и имеет тип:
System.Linq.Enumerable.WhereSelectEnumerableIterator<Microsoft.EntityFrameworkCore.Storage.ValueBuffer, <>f__AnonymousType1<string>>.
Данные не будут загружены, пока вы не выполните итерацию, что приведет к N + 1 запросам. Когда я добавлю .ToArray()
к запросу (как показано в комментариях) данные полностью загружаются в var res
использование профилировщика SQL, однако, показывает, что это больше не достигается в 1 запросе SQL. Для каждого объекта "Top" выполняется запрос к таблице "Sub".
Сначала указав .Include(t => t.Sub)
кажется, ничего не меняет. Использование анонимных типов также не является проблемой, заменив new { ... }
блоки с new MyPocoClass { ... }
ничего не меняет
Мой вопрос: есть ли способ получить поведение, подобное EF6, где все данные загружаются немедленно?
Примечание: я понимаю, что в этом примере проблему можно решить, создав анонимные объекты в памяти после выполнения запроса следующим образом:
var query2 = context.Top
.Include(t => t.Sub)
.ToArray()
.Select(t => new //... select what is needed, fill anonymous types
Однако это всего лишь пример, мне действительно нужно, чтобы создание объектов было частью запроса Linq, так как AutoMapper использует это для заполнения DTO в моем проекте.
Обновление: протестировано с новым EF Core 2.0, проблема все еще присутствует. (21-08-2017)
Проблема отслеживается aspnet/EntityFrameworkCore
GitHub репо: выпуск 4007
Обновление: год спустя, эта проблема была исправлена в версии 2.1.0-preview1-final
, (2018-03-01)
Обновление: EF версии 2.1 была выпущена, она включает в себя исправление. см. мой ответ ниже. (2018-05-31)
2 ответа
Выпуск GitHub № 4007 был отмечен как closed-fixed
для вехи 2.1.0-preview1
, И теперь версия 2.1 preview1 сделана доступной на NuGet, как обсуждалось в этом сообщении в блоге.NET.
Также выпущена правильная версия 2.1, установите ее с помощью следующей команды:
Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 2.1.0
Тогда используйте .ToList()
на вложенном .Select(x => ...)
чтобы указать результат должен быть получен немедленно. Для моего оригинального вопроса это выглядит так:
var query = context.Top
.Select(t => new {
prop1 = t.C1,
prop2 = t.Sub.Select(s => new {
prop21 = s.C3
})
.ToList() // <-- Add this
});
var res = query.ToArray(); // Execute the Linq query
В результате в базе данных выполняется 2 SQL-запроса (вместо N + 1); Сначала равнина SELECT
FROM
верхний стол, а затем SELECT
FROM
таблица 'Sub' с INNER JOIN
FROM
таблица Top, основанная на отношении Key-ForeignKey [Sub].[TopId] = [Top].[Id]
, Результаты этих запросов затем объединяются в памяти.
Результат в точности соответствует тому, что вы ожидаете, и очень похож на то, что возвратил бы EF6: массив анонимного типа 'a
который имеет свойства prop1
а также prop2
где prop2
Список анонимного типа 'b
который имеет свойство prop21
, Самое главное, что все это полностью загружено после .ToArray()
вызов!
Я столкнулся с той же проблемой.
Решение, которое вы предложили, не работает для относительно больших таблиц. Если вы посмотрите на сгенерированный запрос, это будет внутреннее соединение без условия where.
var query2 = context.Top.Include (t => t.Sub) .ToArray () .Select (t => new //... выберите то, что нужно, заполните анонимные типы
Я решил это с редизайном базы данных, хотя я был бы рад услышать лучшее решение.
В моем случае у меня есть две таблицы A и B. Таблица A имеет один ко многим с B. Когда я пытался решить ее напрямую с помощью списка, как вы описали, мне не удалось это сделать (время выполнения для.NET LINQ был 0,5 секунды, тогда как.NET Core LINQ не удалось через 30 секунд работы) .
В результате мне пришлось создать внешний ключ для таблицы B и начать со стороны таблицы B без внутреннего списка.
context.A.Where(a => a.B.ID == 1).ToArray();
После этого вы можете просто манипулировать полученными объектами.NET.