【问题标题】:EF Core Grouping Multiple Joined TablesEF Core 对多个连接表进行分组
【发布时间】:2020-07-23 02:13:59
【问题描述】:

尝试对已连接在一起的 3 个表进行分组时,我在运行时遇到错误。我知道我可以在 sql 中做到这一点,但如果可能的话,我会尽量使用 EF Core。 EF Core 抱怨无法翻译 linq 表达式,或者我需要切换到客户端评估。目标是让这个简单的连接和分组在 sql server 中运行。

from line in _context.OrderLines
    join item in _context.Items on line.ItemId equals item.ItemId
    join supplier in _context.Suppliers on item.SupplierId equals supplier.SupplierId
group line by supplier.Name

上面的分组在编译时是一个有效的查询表达式,如果没有随后的选择就会失败,但要详细说明我的最终用途:我希望能够按供应商汇总我的行数量。这应该是分组后的简单聚合。

from line in _context.OrderLines
    join item in _context.Items on line.ItemId equals item.ItemId
    join supplier in _context.Suppliers on item.SupplierId equals supplier.SupplierId
group line by supplier.Name into lineGrouping
select new { Name = lineGrouping.Key, Qty = lineGrouping.Sum(x => x.Qty) }

执行导致如下错误:

System.InvalidOperationException:不支持客户端 GroupBy。 在 Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.VisitExtension(Expression extensionExpression)

【问题讨论】:

  • Supplier.Name 是什么?不应该是supplier.Name吗?
  • 语法更正@GuruStron
  • 你能添加完整的查询吗?
  • 使用应该足以显示错误,但我已经用我正在拍摄的完整用例更新了我的问题。 @GuruStron
  • 我认为这是 EF Core 3.1?

标签: c# linq entity-framework-core


【解决方案1】:

查看官方 EF Core 文档,了解有关 Complex Query Operators: GroupBy 的一般信息。

由于没有数据库结构可以表示 IGrouping,因此 GroupBy 运算符在大多数情况下没有翻译。当聚合运算符应用于每个返回标量的组时,可以将其转换为关系数据库中的 SQL GROUP BY。 SQL GROUP BY 也有限制。它要求您仅按标量值进行分组。投影只能包含分组键列或应用于列的任何聚合。 EF Core 识别此模式并将其转换为服务器 [...]

现在,对于您的具体示例,它可以正常工作。下面是一个示例控制台项目来证明:

using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace IssueConsoleTemplate
{
    public class OrderLine
    {
        [Key]
        public int OrderLineId { get; set; }
        public int ItemId { get; set; }
        public int Qty { get; set; }
    }

    public class Supplier
    {
        [Key]
        public int SupplierId { get; set; }
        public string Name { get; set; }
    }

    public class Item
    {
        [Key]
        public int ItemId { get; set; }
        public int SupplierId { get; set; }
    }
    
    public class Context : DbContext
    {
        public DbSet<OrderLine> OrderLines { get; set; }
        public DbSet<Supplier> Suppliers { get; set; }
        public DbSet<Item> Items { get; set; }
        
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder
                .UseSqlServer(@"Data Source=.\MSSQL14;Integrated Security=SSPI;Initial Catalog=So63040380")
                .UseLoggerFactory(
                    LoggerFactory.Create(
                        b => b
                            .AddConsole()
                            .AddFilter(level => level >= LogLevel.Information)))
                .EnableSensitiveDataLogging()
                .EnableDetailedErrors();
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.Entity<OrderLine>()
                .HasData(
                    new OrderLine {OrderLineId = 1, ItemId = 1, Qty = 100},
                    new OrderLine {OrderLineId = 2, ItemId = 2, Qty = 42},
                    new OrderLine {OrderLineId = 3, ItemId = 3, Qty = 21});

            builder.Entity<Supplier>()
                .HasData(
                    new Supplier {SupplierId = 1, Name = "Supplier A"},
                    new Supplier {SupplierId = 2, Name = "Supplier B"});
            
            builder.Entity<Item>()
                .HasData(
                    new Item {ItemId = 1, SupplierId = 1},
                    new Item {ItemId = 2, SupplierId = 1},
                    new Item {ItemId = 3, SupplierId = 2});
        }
    }

    internal static class Program
    {
        private static void Main()
        {
            using var context = new Context();

            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();

            var supplierNameAndQuantity = (from line in context.OrderLines
                join item in context.Items on line.ItemId equals item.ItemId
                join supplier in context.Suppliers on item.SupplierId equals supplier.SupplierId
                group line by supplier.Name into lineGrouping
                select new {Name = lineGrouping.Key, Qty = lineGrouping.Sum(x => x.Qty)})
                .ToList();

            Debug.Assert(supplierNameAndQuantity.Count == 2);
            Debug.Assert(supplierNameAndQuantity[0].Name == "Supplier A");
            Debug.Assert(supplierNameAndQuantity[0].Qty == 142);
            Debug.Assert(supplierNameAndQuantity[1].Name == "Supplier B");
            Debug.Assert(supplierNameAndQuantity[1].Qty == 21);
        }
    }
}

查询被翻译成以下 SQL,这是正确的:

SELECT [s].[Name], SUM([o].[Qty]) AS [Qty]
FROM [OrderLines] AS [o]
INNER JOIN [Items] AS [i] ON [o].[ItemId] = [i].[ItemId]
INNER JOIN [Suppliers] AS [s] ON [i].[SupplierId] = [s].[SupplierId]
GROUP BY [s].[Name]

【讨论】:

  • 这一行准确地描述了查询,并表示应该翻译:“投影只能包含分组键列或应用于列的任何聚合”
  • 没错,它可以正常工作。正如 OP 所提供的,查询翻译得很好,应该是这样。我更新了答案以包含生成的 SQL。
  • 动态创建数据库的好用处。
  • @NetMage 是的,我是 Pomelo.EntityFrameworkCore.MySql(EF Core 的 MySQL 提供者)的主要开发者。所以我不得不发布很多这样的小型示例控制台项目,向不同经验水平的用户展示行为。
  • 这很奇怪。从我的应用程序中运行时,我不知道是什么导致了无效操作。你已经完美地复制了它,因为我理解它不起作用。在 db 上下文的设置中必须有其他内容。感谢您的完整性检查:证明我最初的想法很疯狂。
猜你喜欢
  • 2019-10-13
  • 2020-04-18
  • 1970-01-01
  • 2017-11-14
  • 2021-01-14
  • 2018-06-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多