Entity framework 5.0 First or Group By Issue - 从 2.2 升级到 5.0 之后

Entity framework 5.0 First or Group By Issue- After upgrading from 2.2 to 5.0

我有一个名为 Products 的 table,我需要为特定类别查找具有唯一标题的产品。早些时候我们曾经在 entity framework 核心 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 异常的错误:

The LINQ expression 'GroupByShaperExpression:KeySelector: t.title, ElementSelector:EntityShaperExpression: EntityType: Project ValueBufferExpression: ProjectionBindingExpression: EmptyProjectionMember IsNullable: False .FirstOrDefault()' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'.

我知道有多种客户端投影方式,但我正在寻找最有效的搜索方式。

您应该在实现后使用 .GroupBy()。不幸的是,EF 核心不支持 GROUP BY。在版本 3 中,他们引入了严格的查询,这意味着您无法执行无法转换为 SQL 的 IQeuriables,除非您禁用此配置(不推荐)。另外,我不确定你想用 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();

简短回答您是否处理 EF Core 版本中的重大更改

您应该考虑从 2.2 迁移到 5.0 总 API 和行为变化 ,如下所示:

使用较新版本编写有效表达式时,您可能会遇到其他问题。在我看来,升级到更新版本本身并不重要。了解如何使用特定版本很重要。

您可以使用 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 操作是在数据库中执行的,并且客户端没有获取额外的记录。

很可能 LINQ 查询也无法在 EF Core 2.2 中转换,因为 GroupBy 运算符具有一些限制。

来自docs

Since no database structure can represent an IGrouping, GroupBy operators have no translation in most cases. When an aggregate operator is applied to each group, which returns a scalar, it can be translated to SQL GROUP BY in relational databases. The SQL GROUP BY is restrictive too. It requires you to group only by scalar values. The projection can only contain grouping key columns or any aggregate applied over a column.

EF Core 2.x 中发生的事情是,每当它无法翻译表达式时,它会自动切换到客户端评估并仅给出警告。

这被列为 breaking change with highest impact when migrating 到 EF Core >= 3.x :

Old behavior

Before 3.0, when EF Core couldn't convert an expression that was part of a query to either SQL or a parameter, it automatically evaluated the expression on the client. By default, client evaluation of potentially expensive expressions only triggered a warning.

New behavior

Starting with 3.0, EF Core only allows expressions in the top-level projection (the last Select() call in the query) to be evaluated on the client. When expressions in any other part of the query can't be converted to either SQL or a parameter, an exception is thrown.

因此,如果在使用 EF Core 时该表达式的性能足够好 2.x,如果您决定显式切换到客户端评估,它会像以前一样好 使用 EF Core 时 5.x。那是因为两者都是客户评估的,之前和现在,唯一的区别是你现在必须明确说明。因此,如果之前的性能是 acceptable,那么简单的出路就是让客户端使用 .AsEnumerable().ToList().

评估查询的最后一部分

如果客户端评估性能未被接受table(这意味着它也不是在迁移之前),那么您必须重写查询。 Ivan Stoev 的 couple of answers 可能会激发您的灵感。

我对你想要实现的目标的描述有点困惑: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 table 中得到了不同的标题,并且每个不同的标题我得到了第一个匹配它的 Product (应该等同于 GroupBy(x => x.Title)+ FirstOrDefault 据我所知)。如果需要,您可以在 Take(1) 之前添加一些排序。

正如 Ivan Stoev 所提到的,EFC 2.x 只是默默地加载完整的 table 到客户端,然后应用所需的逻辑来提取所需的结果。这是一种消耗资源的方式,感谢 EFC 团队发现了此类潜在的有害查询。

已知最有效的方法 - 原始 SQL 和 window 函数。 SO 充满了这样的答案。

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 是此类问题的优先考虑,我建议尝试我们为 EF Core 项目改编的 linq2db ORM: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();