EF Core 3.1 / EF Core 5.0中的GroupBy无法工作,即使是最简单的示例也是如此。

40

我正在将一个EF6.x项目升级到EF Core 3.1。决定回归基础,重新按照如何从头开始设置关系的示例进行操作。

根据官方Microsoft文档EF Core Relationship Examples,我将示例翻译成了以下控制台应用程序:

using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace BlogPostsExample
{
    class Program
    {
        async static Task Main(string[] args)
        {
            // SQL Running in a Docker container - update as required
            var conString = "data source=localhost,14330;initial catalog=BlogsDb;persist security info=True;user id=sa;password=<Your super secure SA password>;MultipleActiveResultSets=True;App=EntityFramework;";

            var ctx = new MyContext(conString);

            await ctx.Database.EnsureCreatedAsync();

            var result = await ctx.Posts.GroupBy(p => p.Blog).ToArrayAsync();

        }
    }

    class MyContext : DbContext
    {
        private readonly string _connectionString;

        public MyContext(string connectionString)
        {
            _connectionString = connectionString;
        }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);
            if (!optionsBuilder.IsConfigured)
            {
                optionsBuilder
                .UseSqlServer(_connectionString);
            }
        }
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {

            modelBuilder.Entity<Post>()
            .HasOne(p => p.Blog)
            .WithMany(b => b.Posts)
            .HasForeignKey(p => p.BlogId) //Tried with and without these keys defined.
            .HasPrincipalKey(b => b.BlogId);
        }

    }
    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }

        public List<Post> Posts { get; set; }
    }

    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        public int BlogId { get; set; }
        public Blog Blog { get; set; }
    }
}

数据库中没有数据。EF Core 无法转换。

ctx.Posts.GroupBy(p => p.Blog)  

转换为存储查询。这对我来说似乎是您可以尝试的最简单的GroupBy示例。

运行此代码时,您将获得以下异常:

System.InvalidOperationException: 'The LINQ expression 'DbSet<Post>
    .Join(
        outer: DbSet<Blog>, 
        inner: p => EF.Property<Nullable<int>>(p, "BlogId"), 
        outerKeySelector: b => EF.Property<Nullable<int>>(b, "BlogId"), 
        innerKeySelector: (o, i) => new TransparentIdentifier<Post, Blog>(
            Outer = o, 
            Inner = i
        ))
    .GroupBy(
        source: p => p.Inner, 
        keySelector: p => p.Outer)' 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 either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.'

唯一让它工作的方法是在GroupBy之前添加类似于AsEnumerable()的内容。
从性能角度来看,这显然不是很好,它将分组操作转换为客户端操作,而您真正想做的是在服务器端进行分组。
我错过了什么重要的东西吗? 我很难相信EF Core不能像EF Framework自Day 1以来一样执行最简单的分组。 这似乎是任何数据驱动应用程序的基本要求? (或任何带有数据的应用程序!)
更新: enter image description here 添加属性(例如所讨论的博客的主键)没有任何区别。
更新2:
如果您遵循JetBrains文章中的this,则可以执行此操作:
var ctx = new EntertainmentDbContext(conString);
await ctx.Database.EnsureCreatedAsync();

var dataTask = ctx
                .Ratings
                .GroupBy(x => x.Source)
                .Select(x => new {Source = x.Key, Count = x.Count()})
                .OrderByDescending(x => x.Count)
                .ToListAsync();

var data = await dataTask;

不是这样:

var ctx = new EntertainmentDbContext(conString);
await ctx.Database.EnsureCreatedAsync();

var dataTask = ctx
                .Ratings
                .GroupBy(x => x.Source)
                // .Select(x => new {Source = x.Key, Count = x.Count()})
                // .OrderByDescending(x => x.Count)
                .ToListAsync();

var data = await dataTask;

它只能与聚合函数一起使用,例如像上面的 Count 函数。

在 SQL 中类似的语法也适用。

SELECT COUNT(R.Id), R.Source
FROM 
    [EntertainmentDb].[dbo].[Ratings] R
GROUP BY R.Source

但是,如果移除聚合函数,COUNT就不能用了,你会收到类似以下的消息:

列 'EntertainmentDb.dbo.Ratings.Id' 在选择列表中无效,因为它既不包含在聚合函数中,也不包含在GROUP BY子句中。

看起来我正在尝试向EF Core提出一个在TSQL中无法提问的问题。


4
你尝试过使用ctx.Posts.GroupBy(p => p.Blog.BlogId)吗?p.Blog引用了一个实体(数据库中的表),而p.Blog.BlogId引用了一个属性(数据库中的列)。 - Pepelui360
4
请参见 https://learn.microsoft.com/en-us/ef/core/querying/complex-query-operators#groupby 了解为什么不支持这样的“GroupBy”查询。 SQL 没有这样的查询等效项,因此必须在客户端执行分组,他们希望您意识到并明确执行它(使用 AsEnumerable() 或类似方法)。如果您想加入/投票,请参见 GitHub 的问题/讨论 https://github.com/dotnet/efcore/issues/17068 。 - Ivan Stoev
2
我认为正确等价的链式 LINQ 应该是 ctx.Posts.GroupBy(p => new { p.Blog.BlogId }).Select(g=>g.Key).ToArrayAsync()。自 EF Core 3 版本以来,必须使用 select 方法。请参见 https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes#linq-queries-are-no-longer-evaluated-on-the-client。 - Pepelui360
3
SQL中的GROUP BY只支持选择关键字和聚合函数。LINQ的GroupBy则允许选择关键字和相关项列表。这在SQL中没有相应的功能,这也是EF Core 3.0/3.1设计人员不愿意支持它的原因。EF6是一个不同的框架,因此它支持什么和不支持什么都是无关紧要的。我们能做的就是加入讨论/投票并努力说服EF Core团队改变他们目前的决定。 - Ivan Stoev
2
我刚刚尝试使用EF Core 5.0运行此示例,结果出现了相同/类似的异常。 - Ian Robertson
显示剩余16条评论
4个回答

4

以前EF/EF core在无法进行服务器端查询时自动转换为客户端查询评估。

按键分组而没有选择是SQL不支持的操作,它总是客户端操作。

从EF 3.0+开始,他们明确了哪些查询应该在服务器上运行,哪些应该在客户端上运行。技术上,明确知道哪些查询将在服务器上运行,哪些将在客户端上运行比框架代表我们做出决策更好。

您可以在此处阅读更多信息: https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.x/breaking-changes#linq-queries-are-no-longer-evaluated-on-the-client


通过强制开发者添加 .AsEnumerable() 以强制客户端求值,并不是“显式”的。"AsEnumerable"并不意味着"客户端"。忽视这一点则会查询SQL,从纯LINQ角度来看,这是不必要的。 - Ian Robertson
LINQ对数据源是不可知的,但这导致并非所有的LINQ特性都能在源上使用。通过失败并强制开发人员添加AsEnumerable,他们让开发人员意识到源的限制。如果没有这个,可能会导致人们认为它会在服务器端发生,而客户端却被数据压垮,影响性能。我认为这个话题是主观的。EFcore团队选择了“快速失败”的方法,而不是旧的“优雅失败”的方法。在我的情况下,这个特性帮助我将查询转换为尽可能多地利用服务器端计算的方式进行开发。 - Abbas Cyclewala
它应该是不可知的。它应该在没有 AsEnumerable 的情况下工作 - 使用内存集合/列表/数组,LINQ 将正常工作。只有当您切换到 SQL 时,它才会失败 - 因此它不是不可知的。 - Ian Robertson

-1

在我看来,问题似乎是,正如你在编辑中所说的那样,你正在将一些 LINQ 组合起来,本应该工作;但却没有得到 DB 提供程序的支持。

不幸的是,EFCore 的工作方式似乎是(我没有参与创建它,所以这纯粹基于观察):它不是在服务器上操作可查询对象(您使用的 LINQ 可以使用),而是将 LINQ 转换为 SQL,并将其发送到 DB(据我所知)。在团队更改 EFCore 功能之前,看起来你有两个选择:

使用 LINQ 或 T-SQL 在数据库提供程序规定的范围内操纵数据(例如使用聚合规范)。

或者,在 .NET 代码中创建数据的本地副本,方法是使用 AsEnumerable 或等效块进行操作。无论您是在客户端还是服务器端进行此操作都可以(我建议通过 API 调用或类似方法在服务器端进行此操作)。


-2

我认为这可能是 GroupBy 的不当使用,因为您实际上并没有按照新的分组或聚合数据,而是使用现有的关系并选择博客并包括帖子。

注意:未经测试的代码

var blogs = ctx.Blogs.Include(x => x.Posts);
// Optional filters.
var blogsWithPosts = ctx.Blogs
    .Include(x => x.Posts)
    .Where(x => x.Posts.Any())  // Only blogs with posts
    .Where(x => x.Posts.Any(y => y.Title == "A Specific Title")) // Only blogs with posts that have the title "A Specific Title"

如果你只需要包含博客文章的子集,也可以这样做。

var blogsAndMathingPosts = ctx.Blogs
    .Where(x => x.Posts.Any(y => y.Title == "A Specific Title")) // Only blogs that have at least one post with "A Specific Title"
    .Select(x => new Blog() {
        BlogId = x.BlogId,
        Url = x.Url,
        Posts = ctx.Posts.Where(y => y.BlogId == x.BlogId && y.Title == "A Specific Title").ToList()
    );

我认为你并没有完全理解这个问题。如果你忽略了这与EF有关,只是把它看作LINQ查询,那么我尝试的方法应该没有任何问题。真正的问题来自于将其转换为存储查询。EF在将LINQ与T-SQL更紧密地联系起来时打破了LINQ的惯例。 - Ian Robertson

-4

按照异常信息所说的做就可以了!你还需要将“var”更改为显式。

我也遇到了这个问题,收到了与你相同的异常信息:

var GroupByM2S =
            dbContext.CatL1s
           .GroupBy(x => x.UserId);   

我改成了这个。测试过了,运行良好。

IEnumerable<IGrouping<int, CatL1>> MsGrpByAsEnumerExplicit =              
            (dbContext.CatL1s).AsEnumerable()
           .GroupBy(x => x.UserId);

所以基本上按照我的方式更改'var'。 这里的int,IGrouping<int,...>是您的分组键属性/列的数据类型。 然后用括号包围dbContext.EntityName,接着加上.AsEnumerable().GroupBy(...)。
IEnumerable<IGrouping<dataTypeOfGpByKey, EntityName>> GrpByIEnumAsEnumExplicit =
        ( //<--Open Par dbCtx.EntityName
    .Join(
            outer: DbSet<Blog>,
            inner: p => EF.Property<Nullable<int>>(p, "BlogId"),
            outerKeySelector: b => EF.Property<Nullable<int>>(b, "BlogId"),
            innerKeySelector: (o, i) => new TransparentIdentifier<Post, Blog>(
                Outer = o,
                Inner = i
            )).AsEnumerable() //<-- Put here
                    .GroupBy(
                    source: p => p.Inner,
                    keySelector: p => p.Outer)
                    ...

对于遇到相同异常信息的人,可以尝试一下这个。


你仍然可以在这里使用 var。唯一的区别是使用 AsEnumerable(),换句话说:切换到客户端评估,就像异常消息所建议的那样。但问题更多地涉及 GroupBy 支持变化的原因,因此无法回答。 - Gert Arnold
它在我的代码中使用“var”无法编译。此外,当您知道类型时,为什么不明确指定呢?我同意这不是更多“为什么”的答案,但它是对异常消息的有效工作答案。 - FlazzG
我想要解决的问题是使用LINQ并(非常重要的“并且”)执行服务器端分组。无论是否使用AsEnumberable,表面上的LINQ应该具有相同的行为。对我来说,“AsEnumerable”并不意味着“让我们在客户端进行这个分组”。出于性能原因,开发人员希望在服务器端运行分组操作。但我希望使用富有表现力的LINQ来实现它。 - Ian Robertson
当然,您可以在这些孤立的语句中使用 var。至于“为什么不明确”,您似乎错过了引入 var 关键字的原因。 - Gert Arnold

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接