EF Core вложенные Linq выбирают результаты в 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 будет преобразован в запрос single 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 repo: Проблема 4007 С >

Обновление: Год спустя эта проблема была исправлена ​​в версии 2.1.0-preview1-final, см. мой ответ ниже. (01-03-2018)

Ответ 1

Проблема GitHub # 4007 была отмечена как closed-fixed для этапа 2.1.0-preview1. И теперь 2.1 preview1 был доступен на NuGet, как обсуждалось в этом . Запись в блоге NET.

Сначала установите новую версию:

Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 2.1.0-preview1-final

Затем используйте .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 таблица "Top", а затем SELECT FROM таблица "Sub" с таблицей INNER JOIN FROM "Top" на основе отношения Key-ForeignKey [Sub].[TopId] = [Top].[Id]. Результаты этих запросов затем объединяются в память.

Результат - именно то, что вы ожидали бы и очень похоже на то, что EF6 вернет: массив анонимного типа 'a, который имеет свойства prop1 и prop2, где prop2 - это список анонимных типов 'b, обладающее свойством prop21. Самое главное все это полностью загружается после вызова .ToArray()!

Ответ 2

У меня была та же проблема.

Решение, которое вы предложили, не работает для относительно больших таблиц. Если вы посмотрите на сгенерированный запрос, это будет внутреннее соединение без условия.

var query2 = context.Top     . Включить (t = > t.Sub)     .ToArray()     .Выберите (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.