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); Сначала равнина SELECTFROM верхний стол, а затем SELECTFROM таблица 'Sub' с INNER JOINFROM таблица 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.

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