Избегайте декартова взрыва без разделения всех включений

У меня есть объект Post, который имеет отношения "один ко многим" с Author и Comment. Я хотел бы загрузить все Post и соединить их с первыми Author и всеми Comment. Код с Include будет выглядеть так:

Post[] posts = ctx.Posts.Include(p => p.Authors.Take(1)).Include(p => p.Comments).ToArray();

В этом запросе возникает проблема с декартовым взрывом. Если Post владеет n Comments, Author и Comment будут повторяться n раз в результирующем наборе.

Решение №1

В EF Core 5.0 я мог бы использовать Разделить запрос, но тогда это сгенерирует 3 запроса, когда я хочу сначала загрузить Post с Author, а затем со всеми Comment.

Решение №2

Сначала загрузите Post с помощью Author, а затем перейдите к сообщению на явно загружать свои комментарии, но это приведет к n + 1 запросам.

Post[] posts = ctx.Posts.Include(p => p.Authors.Take(1)).ToArray();
foreach (Post post in posts)
  ctx.Entry(post).Collection(p => p.Comments).Load();

Решение №3

Сначала загрузите Post с Author, затем соберите все идентификаторы сообщений, чтобы сгенерировать один запрос для загрузки комментариев.

Dictionary<int, Post> postsById = ctx.Posts.Include(p => p.Authors.Take(1)).ToDictionnary(p => p.Id);
Comment[] comments = ctx.Comments.Where(c => postsById.ContainsKey(c.PostId)).ToArray();
foreach (Comment comment in comments)
  postsById[comment.PostId].Comments.Add(comment); // How to avoid re-adding comment?

Это решение будет генерировать только 2 запроса без каких-либо дублирующихся данных, но как я могу избежать повторного добавления комментариев к сообщению? Есть ли лучший способ, чем 3 предложенных решения?


person Greg    schedule 11.12.2020    source источник
comment
EF знает, как работать с результирующим набором. Вы всегда получаете уникальные посты. Похоже, вы пытаетесь решить несуществующую проблему.   -  person Gert Arnold    schedule 11.12.2020
comment
У меня была аналогичная проблема - перекрестные соединения из Include в конечном итоге вызовут огромные наборы результатов из запроса SQL по мере увеличения количества связанных записей. В конечном итоге я переключился на ADO для этих типов запросов — это позволило мне разделить запросы вручную, а также запустить их все одновременно (поскольку EF не является потокобезопасным). Прирост производительности был день и ночь.   -  person crgolden    schedule 11.12.2020
comment
@GertArnold Моя проблема здесь не в правильности, а в производительности. Мне нужен один запрос как для сообщения, так и для автора, а второй запрос для комментариев. Я заметил, что мой пример слишком упрощен. Для отношений «один к одному» ядро ​​EF знает, что ему не нужно создавать новый запрос, а можно просто использовать JOIN. Я отредактировал свой пример, чтобы отразить мою проблему. Сообщение теперь имеет отношение «один ко многим» с автором, и мне нужны сообщения с их первым автором и всеми его комментариями.   -  person Greg    schedule 11.12.2020
comment
@crgolden Я бы не хотел писать какой-либо SQL, потому что я использую поставщика в памяти для среды разработки.   -  person Greg    schedule 11.12.2020
comment
Да, я понимаю это сейчас, но это не было очевидно из вашего первого описания, которое, кажется, сосредоточено на репликации данных (что, конечно, влияет на производительность, особенно с повторяющимися длинными строками).   -  person Gert Arnold    schedule 11.12.2020
comment
Включение одного элемента из отношения «один ко многим» не кажется распространенным случаем (я бы скорее сказал, что это должно быть довольно редко), поэтому оптимизация для этого случая, похоже, не стоит усилий. Я думаю, что стандартное разделение на 3 запроса должно быть в порядке.   -  person Ivan Stoev    schedule 11.12.2020
comment
@IvanStoev Правильно, но для моей культуры, не могли бы вы знать, как заставить работать третье решение?   -  person Greg    schedule 11.12.2020


Ответы (2)


Я бы добавил еще один вариант. Поскольку я автор книги «Нетерпеливая загрузка в linq2db. Я почти уверен, что он будет выполнять только два запроса.

Поэтому просто установите это расширение linq2db.EntityFrameworkCore (версия 3.x для EF Core 3.1.x и версия 5.x для EF Core 5.x)

И попробуйте этот запрос:

Post[] posts = ctx.Posts
   .Include(p => p.Author)
   .Include(p => p.Comments)
   .ToLinqToDB()
   .ToArray();

Также этот подход должен работать с AutoMapper ProjectTo и полностью пользовательской проекцией. Я знаю, что пользовательская проекция не работает с AsSplitQuery (так как я пробовал)

person Svyatoslav Danyliv    schedule 11.12.2020
comment
Похоже, что все ваши ответы на вопросы, связанные с EFC, заключаются в использовании моста Linq2Db :-) Что, я должен признать, может быть правильным. - person Ivan Stoev; 11.12.2020
comment
Почему нет. Люди переходят на Dapper, потому что EF Core не хочет что-то поддерживать или делать это как новичок. - person Svyatoslav Danyliv; 11.12.2020
comment
Вот что я говорю. Для меня ненормально полагаться на сторонние пакеты для обеспечения базовой функциональности, но это реальность с EF Core (к сожалению) даже после нескольких лет разработки. Единственная проблема - необходимость явного вызова ToLinqToDb(), нам (да и им тоже) было бы намного проще, если бы они указали простой способ замены провайдера, но... - person Ivan Stoev; 11.12.2020
comment
Хуже того, иногда я слежу за их проблемами. Ужасно 20 секунд для перевода LINQ-запроса с 5-6 включениями. Хорошая новость в том, что при этом они не наносят вреда серверу базы данных;) - person Svyatoslav Danyliv; 11.12.2020
comment
Действительно :-( Что касается других вопросов, на которые вы ответили сегодня, вместо NeinLinq и LinqKit вы можете найти интересный подход DelegateDecompiler, а также мою попытку подключить препроцессор запросов здесь и мои неудачные попытки убедить команду EF здесь. По крайней мере, вопрос все еще открыт :-) - person Ivan Stoev; 11.12.2020
comment
@IvanStoev, борись с ними ;) Я правда не понимаю, почему это запрещено. Это чертовски просто и спасает жизни (недели). Обычно пользователи наших расширений говорят точные слова: ваше расширение спасает жизнь, и я горжусь этим ;) - person Svyatoslav Danyliv; 11.12.2020
comment
@GertArnold, понял. Я новичок в SO, некоторые нюансы до сих пор не интуитивно понятны. - person Svyatoslav Danyliv; 11.12.2020
comment
Если вы каким-либо образом или в форме связаны с подключаемым модулем/веб-сайтом/блогом/продуктом/проектом и т. д., на который вы ссылались, пожалуйста, четко укажите свою принадлежность в самом ответе. Нераскрытая аффилиация будет считаться спамом и удалена. Прочтите Как не быть спамером и Могу ли я поддерживать свой продукт на этом сайте?. - person Sabito 錆兎; 11.12.2020
comment
Приятно читать эти ссылки ... Также, пожалуйста, отредактируйте все ответы, соответствующие этим критериям. - person Sabito 錆兎; 11.12.2020
comment
@Svyatoslav Для этой конкретной проблемы попробуйте исходный запрос OP (с .Include(p => p.Authors.Take(1))) с вашим подходом. Я вижу запросы к базе данных 3 и получаю исключение в конце выполнения последнего запроса (поднято at LinqToDB.Common.ConvertBuilder.ConvertDefault(Object value, Type conversionType)) - person Ivan Stoev; 11.12.2020
comment
@IvanStoev, для каждой коллекции будет один запрос. В соответствии с вашим исключением, не могли бы вы создать проблему в github? - person Svyatoslav Danyliv; 12.12.2020

Я нашел способ решения № 2 работать только с двумя запросами здесь: https://github.com/dotnet/efcore/issues/7350.

int postIds = new[] { 3, 4 };
Post[] posts = ctx.Posts
    .Include(p => p.Authors.Take(1))
    .Where(p => postIds.Contains(p.Id))
    .ToArray();

// This line automatically populates posts comments in the same DbContext.
ctx.Comments
    .Where(c => postIds.Contains(c.PostId))
    .Load();
person Greg    schedule 15.12.2020