EF Core активно загружает коллекции при использовании GroupBy после включения

var productsByRecalls = await _context
  .Products
  .Include(x => x.Suppliers)                              // <-----
  .Include(x => x.Recalls)                                // <-----
  .AsSplitQuery()
  .OrderBy(x => x.CreatedAt)
  .Where(x => x.Status == ProductStatus.Approved)
  .GroupBy(x => x.Recalls)                                // <-----
  .Where(x => x.Any()).Select(x => x.SingleOrDefault())
  .ToListAsync();

Без группировки это работает должным образом, и коллекции Include быстро загружаются.

Но при использовании группировки коллекции Suppliers и Recalls пусты.

Почему это происходит и как их включить?

Когда вы вызываете GroupBy, результатом является набор групп, и каждая группа по сути является проекцией исходных результатов запроса. Операторы Include предназначены для непосредственного применения к объектам, а не к сгруппированным проекциям. В результате нетерпеливая загрузка, настроенная Include, не переносится после вызова GroupBy, что приводит к пустым коллекциям для поставщиков и отзывов. Итак, то, чего вы пытаетесь достичь, требует разделения вашего запроса на две части в качестве обходного пути.

Md Farid Uddin Kiron 12.07.2024 09:27

Что этот код пытается сделать в первую очередь? Группировка по всей таблице не имеет особого смысла в SQL. Похоже, вы пытаетесь выбрать самый ранний отозванный продукт для каждого отзыва? В этом случае вам, вероятно, следует начать с context.Recalls, используйте фильтрованное включение, чтобы получить только одобренные продукты.

Panagiotis Kanavos 12.07.2024 09:41
.Where(x => x.Any()).Select(x => x.SingleOrDefault()) становится немного излишним, если вы понимаете, что фактически пытаетесь выполнить TOP 1 в подзапросе. Даже если намерение состоит в том, чтобы ограничить результаты продуктами только с одним отзывом, .Where(x => x.Any()) не требуется, поскольку предложение HAVING COUNT(*)=1 не возвращает ни одной строки NULL. Пожалуйста, объясните, что вы хотите сделать, опубликуйте схему таблицы и классы, чтобы люди могли рассказать вам, как решить реальную проблему.
Panagiotis Kanavos 12.07.2024 09:57

Что означает группировка по коллекции Recalls?

Svyatoslav Danyliv 12.07.2024 10:33

@SvyatlavDanyliv на основе общего запроса Recalls — это отдельная таблица, свойство навигации Products таблицы. Результат запроса необходимо сгруппировать по значению Recalls.

Md Farid Uddin Kiron 12.07.2024 11:07

@MdFaridUddinKiron, может быть, у вас хрустальный шар получше, но из вопроса я вижу, что Recalls - это свойство навигации по коллекции.

Svyatoslav Danyliv 12.07.2024 11:10

@SvyatlavDanyliv Да, ты прав.

Md Farid Uddin Kiron 12.07.2024 11:14

Спасибо всем ответам. Пример надуманный, я старался избегать примера foo/bar/baz. Я не могу опубликовать настоящий код, но то, что я опубликовал, функционально идентично моей реальной проблеме. Спасибо за объяснение, что инструкция включения не соблюдается после включения. Я постараюсь придумать лучший пример или просто реализовать его по-другому.

lonix 12.07.2024 12:37

Если предположить, что это EF Core, то это ошибка, о которой вам следует сообщить на сайте github.com/dotnet/efcore

Charlieface 12.07.2024 13:59
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
9
63
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Когда вы используете GroupBy в своем запросе, он преобразует результат в набор групп, что нарушает нетерпеливую загрузку, заданную операторами Include. В результате коллекции «Поставщики» и «Отзывы» не загружаются. В качестве решения вы можете выполнить операцию группировки в памяти после получения данных из базы данных, например:

// Step 1: Fetch the data with includes
var products = await _context
    .Products
    .Include(x => x.Suppliers)
    .Include(x => x.Recalls)
    .AsSplitQuery()
    .OrderBy(x => x.CreatedAt)
    .Where(x => x.Status == ProductStatus.Approved)
    .ToListAsync();

// Step 2: Perform the grouping and filtering in memory
var productsByRecalls = products
    .GroupBy(x => x.Recalls)
    .Where(g => g.Any())
    .Select(g => g.SingleOrDefault())
    .ToList();

Это только скроет проблему и приведет к ужасной производительности даже при очень небольшом объеме данных. База данных не является деталью реализации и обычно на несколько порядков мощнее любого клиента благодаря индексам, мощным процессорам, быстрым дискам и большому количеству оперативной памяти.

Panagiotis Kanavos 12.07.2024 09:53

но что вы подразумеваете под ужасной производительностью? И почему именно это не решит проблему?

Dima 12.07.2024 09:56

Представьте себе, что вы загружаете в память 1000 товаров и выбираете 10. Или 100 тыс. товаров. Это ужасно, как бы вы это ни измеряли. Общее время, задержка сети, использование памяти как на сервере, так и на клиенте, использование диска. А также будет блокировать таблицы намного дольше, чем необходимо, задерживая любой другой запрос, который пытается изменить строки в этих таблицах.

Panagiotis Kanavos 12.07.2024 09:58

Это справедливо, но что, если мы попытаемся получить только необходимые данные, например, получить только идентификаторы продуктов и их идентификаторы отзыва, затем сгруппировать их по идентификаторам отзыва в памяти и получить идентификаторы продуктов и только затем загрузить продукты с необходимыми включениями на основе сгруппированные идентификаторы продуктов

Dima 12.07.2024 10:01

это должно оптимизировать наши запросы при работе с большими наборами

Dima 12.07.2024 10:04

или, если мы говорим о действительно огромном объеме данных, почему мы вообще говорим о LINQ? Почему бы не выполнить подкачку на стороне SQL?

Dima 12.07.2024 10:06

Это та же ошибка с меньшим количеством данных (и подвержена призракам, поскольку идентификаторы теперь загружаются отдельно). Зачем загружать 10 тысяч строк и пытаться фильтровать без каких-либо индексов, когда вы можете просто загрузить одну строку только с теми данными, которые вам нужны. Это не what if, это базовая ошибка как в ORM, так и в запросах к базе данных. В самом деле, какой смысл использовать ORM, если вы не собираетесь загружать объекты? С таким же успехом можно написать чистый SQL-запрос, чтобы загрузить только те данные, которые вам нужны.

Panagiotis Kanavos 12.07.2024 10:08

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

lonix 13.07.2024 06:00
Ответ принят как подходящий

Но при использовании группировки коллекции «Поставщики» и «Отзывы» пустой.

Использование GroupBy создает группы данных на основе вашего запроса. Каждая группа действует как снимок исходных результатов. Однако операторы Include предназначены для добавления связанных данных к отдельным элементам, а не к группам, созданным с помощью GroupBy. По этой причине дополнительные данные, которые вы хотели загрузить с помощью функции «Включить» (например, «Поставщики» и «Отзывы»), не включаются в окончательные результаты после группировки, оставляя их пустыми.

Почему это происходит и как их включить?

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

Я имею в виду, что сначала загрузите данные с помощью операторов Include, чтобы обеспечить быструю загрузку связанных данных.

После этого, как только данные будут загружены, вы можете выполнять GroupBy и другие операции в памяти. Это гарантирует, что инструкции Include уже вступили в силу до завершения группировки.

Давайте посмотрим на реализацию.

Представьте себе, у меня есть следующие примеры данных:

 var product1 = new Product
    {
        Id = 1,
        Name = "Product1",
        CreatedAt = DateTime.Now.AddDays(-10),
        Status = ProductStatus.Approved,
        Suppliers = new List<Supplier>
        {
            new Supplier { Id = 1, Name = "Supplier1", ProductId = 1 },
            new Supplier { Id = 2, Name = "Supplier2", ProductId = 1 }
        },
            Recalls = new List<Recall>
        {
            new Recall { Id = 1, Reason = "Reason1", ProductId = 1 },
            new Recall { Id = 2, Reason = "Reason2", ProductId = 1 }
        }
    };
    
    var product2 = new Product
    {
        Id = 2,
        Name = "Product2",
        CreatedAt = DateTime.Now.AddDays(-5),
        Status = ProductStatus.Approved,
        Suppliers = new List<Supplier>
        {
            new Supplier { Id = 3, Name = "Supplier3", ProductId = 2 }
        },
            Recalls = new List<Recall>
        {
            new Recall { Id = 3, Reason = "Reason3", ProductId = 2 }
        }
    };
    
    var product3 = new Product
    {
        Id = 3,
        Name = "Product3",
        CreatedAt = DateTime.Now.AddDays(-2),
        Status = ProductStatus.Pending
    };

Итак, сначала приобретите одобренные продукты:

var products = new List<Product> { product1, product2, product3 };

var approvedProducts = products.Where(x => x.Status == ProductStatus.Approved)
.OrderBy(x => x.CreatedAt)
.ToList();

Теперь на основе отфильтрованного результата будем выполнять группировку и выбор:

var productsByRecalls = approvedProducts
    .GroupBy(x => x.Recalls)
    .Where(g => g.Any())
    .Select(g => g.SingleOrDefault())
    .ToList();

Наконец, верните продуктыByRecalls.

Выход:

Танслятор EF Core LINQ и LINQ to Objects — это разные вещи. ИМХО, этот ответ бесполезен без EF Core.

Svyatoslav Danyliv 12.07.2024 10:20

Но если я не ошибаюсь, вопрос задавался по ядру EF, и я тоже моделировал на ядре EF.

Md Farid Uddin Kiron 12.07.2024 11:04

Я вижу только списки. Как это связано с EF Core? В любом случае вопрос выглядит неправильным. Группировать товары по коллекциям не имеет смысла.

Svyatoslav Danyliv 12.07.2024 11:08

Давайте подождем, пока ОП вернется и посмотрим наши дополнительные вопросы, чтобы ОП мог дать точное объяснение. Я просто попытался смоделировать сценарий, чтобы он мог помочь ФП, если он подойдет.

Md Farid Uddin Kiron 12.07.2024 11:13

В конечном итоге я реструктурировал свой код, чтобы избежать проблемы, но ваш ответ на самом деле объясняет проблему и предлагает решение... так что принято. Спасибо!

lonix 12.07.2024 13:01

Спасибо за ваш ответ, я рад помочь вам в этом.

Md Farid Uddin Kiron 12.07.2024 14:55

Вы пытаетесь получить первую запись каждой группы, что можно сделать более естественным для SQL способом:

Обратите внимание: я заменил Recalls на SomeKey. Он должен показать, как выполнять такие запросы в EF Core.

var products = _context.Products
    .Include(x => x.Suppliers)
    .Include(x => x.Recalls)
    .Where(x => x.Status == ProductStatus.Approved);

var query = 
  from d in products.Select(d => new { d.SomeKey }).Distinct()
  from p in products.Where(p => p.SomeKey == d.SomeKey)
    .OrderBy(p => p.CreatedAt)
    .Take(1)
  select p;

var productsByRecalls = await query
    .AsSplitQuery()
    .ToListAsync();

Спасибо, это интересное решение.

lonix 12.07.2024 13:06

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