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
пусты.
Почему это происходит и как их включить?
Что этот код пытается сделать в первую очередь? Группировка по всей таблице не имеет особого смысла в SQL. Похоже, вы пытаетесь выбрать самый ранний отозванный продукт для каждого отзыва? В этом случае вам, вероятно, следует начать с context.Recalls
, используйте фильтрованное включение, чтобы получить только одобренные продукты.
.Where(x => x.Any()).Select(x => x.SingleOrDefault())
становится немного излишним, если вы понимаете, что фактически пытаетесь выполнить TOP 1
в подзапросе. Даже если намерение состоит в том, чтобы ограничить результаты продуктами только с одним отзывом, .Where(x => x.Any())
не требуется, поскольку предложение HAVING COUNT(*)=1
не возвращает ни одной строки NULL
. Пожалуйста, объясните, что вы хотите сделать, опубликуйте схему таблицы и классы, чтобы люди могли рассказать вам, как решить реальную проблему.
Что означает группировка по коллекции Recalls
?
@SvyatlavDanyliv на основе общего запроса Recalls
— это отдельная таблица, свойство навигации Products
таблицы. Результат запроса необходимо сгруппировать по значению Recalls.
@MdFaridUddinKiron, может быть, у вас хрустальный шар получше, но из вопроса я вижу, что Recalls
- это свойство навигации по коллекции.
@SvyatlavDanyliv Да, ты прав.
Спасибо всем ответам. Пример надуманный, я старался избегать примера foo/bar/baz. Я не могу опубликовать настоящий код, но то, что я опубликовал, функционально идентично моей реальной проблеме. Спасибо за объяснение, что инструкция включения не соблюдается после включения. Я постараюсь придумать лучший пример или просто реализовать его по-другому.
Если предположить, что это EF Core, то это ошибка, о которой вам следует сообщить на сайте github.com/dotnet/efcore
Когда вы используете 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();
Это только скроет проблему и приведет к ужасной производительности даже при очень небольшом объеме данных. База данных не является деталью реализации и обычно на несколько порядков мощнее любого клиента благодаря индексам, мощным процессорам, быстрым дискам и большому количеству оперативной памяти.
но что вы подразумеваете под ужасной производительностью? И почему именно это не решит проблему?
Представьте себе, что вы загружаете в память 1000 товаров и выбираете 10. Или 100 тыс. товаров. Это ужасно, как бы вы это ни измеряли. Общее время, задержка сети, использование памяти как на сервере, так и на клиенте, использование диска. А также будет блокировать таблицы намного дольше, чем необходимо, задерживая любой другой запрос, который пытается изменить строки в этих таблицах.
Это справедливо, но что, если мы попытаемся получить только необходимые данные, например, получить только идентификаторы продуктов и их идентификаторы отзыва, затем сгруппировать их по идентификаторам отзыва в памяти и получить идентификаторы продуктов и только затем загрузить продукты с необходимыми включениями на основе сгруппированные идентификаторы продуктов
это должно оптимизировать наши запросы при работе с большими наборами
или, если мы говорим о действительно огромном объеме данных, почему мы вообще говорим о LINQ? Почему бы не выполнить подкачку на стороне SQL?
Это та же ошибка с меньшим количеством данных (и подвержена призракам, поскольку идентификаторы теперь загружаются отдельно). Зачем загружать 10 тысяч строк и пытаться фильтровать без каких-либо индексов, когда вы можете просто загрузить одну строку только с теми данными, которые вам нужны. Это не what if
, это базовая ошибка как в ORM, так и в запросах к базе данных. В самом деле, какой смысл использовать ORM, если вы не собираетесь загружать объекты? С таким же успехом можно написать чистый SQL-запрос, чтобы загрузить только те данные, которые вам нужны.
В конечном итоге я реструктурировал свой запрос, чтобы избежать проблемы, но ценю вашу помощь, спасибо.
Но при использовании группировки коллекции «Поставщики» и «Отзывы» пустой.
Использование 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.
Но если я не ошибаюсь, вопрос задавался по ядру EF, и я тоже моделировал на ядре EF.
Я вижу только списки. Как это связано с EF Core? В любом случае вопрос выглядит неправильным. Группировать товары по коллекциям не имеет смысла.
Давайте подождем, пока ОП вернется и посмотрим наши дополнительные вопросы, чтобы ОП мог дать точное объяснение. Я просто попытался смоделировать сценарий, чтобы он мог помочь ФП, если он подойдет.
В конечном итоге я реструктурировал свой код, чтобы избежать проблемы, но ваш ответ на самом деле объясняет проблему и предлагает решение... так что принято. Спасибо!
Спасибо за ваш ответ, я рад помочь вам в этом.
Вы пытаетесь получить первую запись каждой группы, что можно сделать более естественным для 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();
Спасибо, это интересное решение.
Когда вы вызываете GroupBy, результатом является набор групп, и каждая группа по сути является проекцией исходных результатов запроса. Операторы Include предназначены для непосредственного применения к объектам, а не к сгруппированным проекциям. В результате нетерпеливая загрузка, настроенная Include, не переносится после вызова GroupBy, что приводит к пустым коллекциям для поставщиков и отзывов. Итак, то, чего вы пытаетесь достичь, требует разделения вашего запроса на две части в качестве обходного пути.