Entity framework 5.0 First или Group By Issue - после обновления с 2.2 до 5.0

У меня есть таблица с именем Products, и мне нужно найти продукты с уникальным названием для определенной категории. Раньше мы использовали этот запрос в Entity Framework Core 2.2:

currentContext.Products
              .GroupBy(x => x.Title)
              .Select(x => x.FirstOrDefault()))
              .Select(x => new ProductViewModel
                 {
                     Id = x.Id,
                     Title = x.Title,
                     CategoryId= x.CategoryId
                 }).ToList();

Но после обновления до Entity Framework Core 5.0 мы получаем ошибку исключения Groupby Shaker:

Выражение LINQ «GroupByShaperExpression: KeySelector: t.title, ElementSelector: EntityShaperExpression: EntityType: Project ValueBufferExpression: ProjectionBindingExpression: EmptyProjectionMember IsNullable: False .FirstOrDefault ()» не может быть переведено. Либо перепишите запрос в форме, которая может быть переведена, либо явно переключитесь на оценку клиента, вставив вызов AsEnumerable, AsAsyncEnumerable, ToList или ToListAsync.

Я знаю, что есть несколько способов проекции клиента, но я ищу наиболее эффективный способ поиска.


person Jalpesh Vadgama    schedule 17.03.2021    source источник
comment
Предполагаю обновление с 2.x до 5.0. Поскольку этот запрос недействителен для LINQ to Entities.   -  person Svyatoslav Danyliv    schedule 17.03.2021
comment
Сообщение об исключении сообщает вам, что результат FirstOrDefault больше GroupBy не поддерживается. Так что либо воспользуйтесь советом по переключению оценки клиента (поскольку ваш запрос действительно оценивался клиентом до EFC 3.0), либо обратитесь к SO, чтобы переписать запрос - есть много похожих вопросов, поскольку это обычная проблема.   -  person Ivan Stoev    schedule 17.03.2021
comment
Да, это проблема при переходе с 2.2 на 5.0   -  person Jalpesh Vadgama    schedule 17.03.2021
comment
Возможно, основная проблема заключается в том, что Title не уникален. Это не кажется здоровым условием для Product стола.   -  person Gert Arnold    schedule 17.03.2021
comment
Есть и другие аспекты таблицы, такие как цвет продукта и т. Д. Но это не имеет значения для данного запроса. И он отлично работает в версии 2.2, но в Entity Framework 5.0 я хочу найти лучший способ найти это.   -  person Jalpesh Vadgama    schedule 19.03.2021
comment
(1) Проблема НЕ в обновлении. Ваш запрос не был переведен на SQL в 2.x. Он работал, молча оценивая его на стороне клиента. Эквивалент вставки AsEnumerable() перед GroupBy. Если раньше тебе было все равно, то почему сейчас? (2) Как я писал в предыдущем комментарии, так называемые Top N элементов для каждой группы очень распространены, но не обрабатываются EFC, поэтому их много раз спрашивали и отвечали, что вы бы нашли, если бы искали SO, как было предложено. Я могу легко приспособиться к вашему случаю и дать один из моих собственных ответов, но это не имеет смысла - вопрос просто дурацкий.   -  person Ivan Stoev    schedule 24.03.2021
comment
Вот точная открытая проблема: Переведите GroupBy, а затем FirstOrDefault по группе №12088. Как видите, он запланирован на 6.0, так что подождите до середины ноября, и все будет в порядке.   -  person Ivan Stoev    schedule 24.03.2021
comment
@JalpeshVadgama Я опубликовал два возможных обходных пути, в зависимости от того, что вы хотите сделать. Дайте мне знать, если вы попробуете.   -  person dglozano    schedule 29.03.2021


Ответы (5)


Скорее всего, этот запрос LINQ также не может быть переведен в EF Core 2.2 из-за некоторых ограничений, которые имеет оператор GroupBy.

Из документов:

Поскольку никакая структура базы данных не может представлять IGrouping, операторы GroupBy в большинстве случаев не имеют перевода. Когда к каждой группе применяется агрегатный оператор, который возвращает скаляр, его можно преобразовать в SQL GROUP BY в реляционных базах данных. SQL GROUP BY также является ограничительным. Это требует от вас группировки только по скалярным значениям. Прогноз может содержать только ключевые столбцы группировки или любой агрегат, примененный к столбцу.

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

Это указано как критическое изменение, оказывающее наибольшее влияние при переходе на EF Core› = 3.x:

Старое поведение

До версии 3.0, когда EF Core не мог преобразовать выражение, являющееся частью запроса, в SQL или в параметр, оно автоматически вычисляло выражение на клиенте. По умолчанию при оценке потенциально дорогостоящих выражений клиентом выдается только предупреждение.

Новое поведение

Начиная с версии 3.0, EF Core позволяет оценивать на клиенте только выражения в проекции верхнего уровня (последний вызов Select () в запросе). Если выражения в любой другой части запроса не могут быть преобразованы ни в SQL, ни в параметр, создается исключение.

Поэтому, если производительность этого выражения была достаточно хорошей при использовании EF Core 2.x, она будет такой же хорошей, как и раньше, если вы решите явно переключиться на оценку клиента при использовании EF Core 5.x. Это потому, что оба оцениваются клиентом, раньше и сейчас, с той лишь разницей, что вы должны прямо сейчас заявить об этом. Таким образом, простой выход, если производительность ранее была приемлемой, - это просто оценить клиентом последнюю часть запроса, используя .AsEnumerable() или .ToList().

Если производительность оценки клиента неприемлема (что будет означать, что этого не было и до миграции), вам необходимо переписать запрос. Есть пара ответов Ивана Стоева, которые могут вас вдохновить.

Меня немного смущает описание того, чего вы хотите достичь: I need to find the products with unique title for a particular category и опубликованный вами код, поскольку я считаю, что он не выполняет то, что вы объяснили. В любом случае я предоставлю возможные решения для обеих интерпретаций.

Это моя попытка написать запрос to find the products with unique title for a particular category.

var uniqueProductTitlesForCategoryQueryable = currentContext.Products
              .Where(x => x.CategoryId == categoryId)
              .GroupBy(x => x.Title)
              .Where(x => x.Count() == 1)
              .Select(x => x.Key); // Key being the title

var productsWithUniqueTitleForCategory = currentContext.Products
              .Where(x => x.CategoryId == categoryId)
              .Where(x => uniqueProductTitlesForCategoryQueryable .Contains(x.Title))
              .Select(x => new ProductViewModel
                 {
                     Id = x.Id,
                     Title = x.Title,
                     CategoryId= x.CategoryId
                 }).ToList();

И это моя попытка переписать отправленный вами запрос:

currentContext.Products
              .Select(product => product.Title)
              .Distinct()
              .SelectMany(uniqueTitle => currentContext.Products.Where(product => product.Title == uniqueTitle ).Take(1))
              .Select(product => new ProductViewModel
                 {
                     Id = product.Id,
                     Title = product.Title,
                     CategoryId= product.CategoryId
                 })
              .ToList();

Я получаю различные заголовки в таблице Product, и для каждого отдельного заголовка я получаю первый Product, который ему соответствует (это должно быть эквивалентно GroupBy(x => x.Title) + FirstOrDefault AFAIK). При необходимости вы можете добавить сортировку перед Take(1).

person dglozano    schedule 28.03.2021
comment
По крайней мере, вы показываете, что очень уродливый обходной путь - это все, что у нас есть на данный момент, но сам обходной путь неверен. Выбор группы с одним элементом - это не то же самое, что выбор первого элемента в группе. - person Gert Arnold; 28.03.2021
comment
@GertArnold, это правда, но меня немного смущает его описание того, чего он хочет достичь, и опубликованный им код. Мое намерение состояло в том, чтобы предоставить код для того, что он объяснил как I need to find products with unique titles for a specific category (но я все равно могу ошибаться ...). Мог бы переписать код, если ОП поясняет - person dglozano; 28.03.2021
comment
@GertArnold обновил мой ответ, чтобы охватить, как мне кажется, обе возможные интерпретации - person dglozano; 28.03.2021

Вы можете использовать Join для этого запроса, как показано ниже:

currentContext.Products
                .GroupBy(x => x.Title)
                .Select(x => new ProductViewModel() 
                { 
                    Title = x.Key,
                    Id = x.Min(b => b.Id) 
                })
                .Join(currentContext.Products, a => a.Id, b => b.Id, 
                     (a, b) => new ProductViewModel()
                {
                    Id = a.Id,
                    Title = a.Title,
                    CategoryId = b.CategoryId
                }).ToList(); 

Если вы смотрите или регистрируете переведенный SQL-запрос, он будет выглядеть следующим образом:

SELECT [t].[Title], [t].[c] AS [Id], [p0].[CategoryId] AS [CategoryId]
FROM (
    SELECT [p].[Title], MIN([p].[Id]) AS [c]
    FROM [Product].[Products] AS [p]
    GROUP BY [p].[Title]
) AS [t]
INNER JOIN [Product].[Products] AS [p0] ON [t].[c] = [p0].[Id]

Как видите, весь запрос преобразуется в один запрос SQL, и это очень эффективно, поскольку операция GroupBy выполняется в базе данных, и клиент не извлекает дополнительную запись.

person Navid Rsh    schedule 28.03.2021
comment
Я думал об аналогичном решении, но я думаю, что это вряд ли обходной путь. OP пытается сделать x.FirstOrDefault(), но это имеет смысл только тогда, когда есть некоторый порядок. Использование Min (или Max) Id не является разумным порядком IMO. Очень часто требуется получить самый последний элемент в группе (на определенную дату). Нельзя сейчас обойтись без прыжков через обручи, да и вообще, это безумие. - person Gert Arnold; 28.03.2021
comment
В большинстве случаев Id - это приращение, поэтому первое вхождение имеет наименьший идентификатор. Но в других сценариях вы правы, вторая часть запроса требует некоторых изменений, чтобы получить первое вхождение. может быть, этого можно добиться другими GroupBy и Take(1). - person Navid Rsh; 28.03.2021

Как упоминал Иван Стоев, EFC 2.x просто незаметно загружает полную таблицу на клиентскую сторону, а затем применяет необходимую логику для извлечения необходимого результата. Это ресурсоемкий способ, и спасибо команде EFC за обнаружение таких потенциально опасных запросов.

Самый эффективный способ уже известен - необработанный SQL и оконные функции. ТАК полно таких ответов.

SELECT 
   s.Id,
   s.Title,
   s.CategoryId
FROM 
  (SELECT 
     ROW_NUMBER() OVER (PARTITION BY p.Title ORDER BY p.Id) AS RN,
     p.*
  FROM Products p) s
WHERE s.RN = 1

Не уверен, что команда EFC в ближайшем будущем изобретет универсальный алгоритм для генерации такого SQL, но для особых случаев это выполнимо, и, возможно, это их план сделать это для EFC 6.0.

В любом случае, если производительность и LINQ являются приоритетом для такого вопроса, я предлагаю попробовать нашу адаптацию linq2db ORM для проектов EF Core: linq2db.EntityFrameworkCore

И получить желаемый результат можно не выходя из LINQ:

urrentContext.Products
    .Select(x =>  new 
    { 
        Product = x,
        RN = Sql.Ext.RowNumber().Over()
            .PartitionBy(x.Title)
            .OrderBy(x.Id)
            .ToValue()
    })
    .Where(x => x.RN == 1)
    .Select(x => x.Product)
    .Select(x => new ProductViewModel
        {
            Id = x.Id,
            Title = x.Title,
            CategoryId = x.CategoryId
        })
    .ToLinqToDB()
    .ToList();
person Svyatoslav Danyliv    schedule 29.03.2021

Краткий ответ: вы имеете дело с критическими изменениями в версиях EF Core.

При переходе с 2.2 на 5.0 следует учитывать общие изменения API и поведения, как я указал ниже:

Вы можете столкнуться с другими проблемами при написании правильных выражений в более новой версии. На мой взгляд, обновление до более новой версии само по себе не важно. Это важно знать, как работать с конкретной версией.

person Amirhossein Mehrvarzi    schedule 25.03.2021

Вы должны использовать .GroupBy () ПОСЛЕ материализации. К сожалению, ядро ​​EF не поддерживает GROUP BY. В версии 3 были введены строгие запросы, что означает, что вы не можете выполнять IQeuriables, которые нельзя преобразовать в SQL, если вы не отключите эту конфигурацию (что не рекомендуется). Кроме того, я не уверен, что вы пытаетесь получить с помощью GroupBy () и как это повлияет на ваш конечный результат. В любом случае, я предлагаю вам обновить свой запрос следующим образом:

currentContext.Products
          .Select(x=> new {
             x.Id,
             x.Title,
             x.Category
          })
          .ToList()
          .GroupBy(x=> x.Title)
          .Select(x => new Wrapper
             { 
                 ProductsTitle = x.Key,
                 Products = x.Select(p=> new ProductViewModel{
                       Id = p.Id,
                       Title = p.Title,
                       CategoryId= p.CategoryId
                 }).ToList()
             }).ToList();
person dantey89    schedule 24.03.2021