2017-01-10 2 views
10

У меня есть модель данных, в которой объект 'Top' имеет объекты от 0 до N 'Sub'. В SQL это достигается с помощью внешнего ключа dbo.Sub.TopId.EF Core вложен Linq выбрать результаты в N + 1 SQL-запросы

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 репо: Issue 4007

Update: Через год эта проблема была исправлена ​​в версии 2.1.0-preview1-final см мой ответ ниже. (01-03-2018)

+0

Можете ли вы показать нам, где/как вы поворачиваете ленивый груз? –

+0

Лёгкая нагрузка применяется к EF6. Мой вопрос касается EF Core. Насколько я знаю, Core не имеет ленивой загрузки. – GWigWam

+1

А, okidoke ... включение, которое вы используете для попытки выполнения. Нежелательная загрузка игнорируется, потому что вы не возвращаете экземпляр объекта типа, с которого начинается запрос. –

ответ

1

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

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

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); Сначала a SELECTFROM таблица «Top», а затем SELECTFROM таблица «Sub» с INNER JOINFROM таблица «Top», основанная на ключевом отношенииKeyKey [Sub].[TopId] = [Top].[Id]. Результаты этих запросов затем объединяются в память.

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

1

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

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

вар Query2 = context.Top .INCLUDE (т => t.Sub) .ToArray() .Select (т => новый // ...выберите, что нужно, заполните анонимные типы

Я решил это с редизайном базы данных, хотя я был бы рад услышать лучшее решение.

В моем случае у меня есть две таблицы A и B. Таблица A имеет один-ко-многим с B. Когда я попытался решить ее непосредственно со списком, как вы описали, я не смог это сделать (работает время для .NET LINQ составляло 0,5 секунды, тогда как .NET Core LINQ провалился через 30 секунд времени работы).

В результате мне пришлось создать внешний ключ для таблицы B и начать со стороны таблицы B без внутреннего списка.

context.A.Where(a => a.B.ID == 1).ToArray(); 

Впоследствии вы можете просто манипулировать приведенными объектами .NET.

+0

В некоторых случаях это может быть достаточно хорошим обходным путем, однако этого недостаточно в моем случае при использовании AutoMapper или когда вы хотите выбрать только несколько свойств в анонимном объекте Он также изменяет набор результатов, вместо того, чтобы возвращать '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '0'' Спасибо для вашего ответа, но я не буду принимать это как решение. – GWigWam

Смежные вопросы