使用 FromSqlRaw 或 Linq 查询对数据进行分组和求和

Using FromSqlRaw or Linq query to Group and Sum data

我正在尝试使用带有 GroupBy 和 Sum 的查询。首先,我尝试使用 SQL:

          string query = $"SELECT Year(Datum) AS y, Month(Datum) AS m, SUM(Bedrag) AS Total FROM Facturens GROUP BY Year(Datum), Month(Datum) ORDER BY y, m";
          Grafiek = await _db.Facturens.FromSqlRaw(query).ToListAsync();

我收到这个错误: “InvalidOperationException:'FromSql' 操作的结果中不存在所需的列 'FacturenID'。” “FacturenID”是 Facturens table 中的第一列。 SQL 查询在直接使用时工作正常。

然后我尝试了 Linq:

        Grafiek = (IEnumerable<Factuur>)await _db.Facturens
         .GroupBy(a => new { a.Datum.Value.Year, a.Datum.Value.Month }, (key, group) => new
           {
               jaar = key.Year,
               maand = key.Month,
               Total = group.Sum(b => b.Bedrag)
            })
        .Select(c => new { c.jaar, c.maand, c.Total })
        .ToListAsync();

这会导致错误:“InvalidOperationException:可为空的对象必须有一个值。”

事实:

using System.ComponentModel.DataAnnotations;

namespace StallingRazor.Model
{
    public class Factuur
    {
        [Key]
        public int FacturenID { get; set; }
        public int EigenarenID { get; set; }

        [Display(Name = "Factuurdatum")]
        [DataType(DataType.Date)]
        [DisplayFormat(NullDisplayText = "")]
        public DateTime? Datum { get; set; }
        public decimal? Bedrag { get; set; }
        public decimal? BTW { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(NullDisplayText = "")]
        public DateTime? Betaaldatum { get; set; }
        [Display(Name = "Betaald bedrag")]
        public decimal? Betaald_bedrag { get; set; } 
        [Display(Name = "Totaal bedrag")]
        public decimal? Totaal_bedrag { get; set; }   
        public int ObjectenID { get; set; }    
        [DataType(DataType.Date)]
        public DateTime? Verzonden { get; set; }    
        public string? Mededeling { get; set; }
        [Display(Name = "Begindatum")]
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{dd-MM-yyyy}", NullDisplayText = "")]
        public DateTime? Begin_datum { get; set; }
        [Display(Name = "Einddatum")]
        [DataType(DataType.Date)]
        [DisplayFormat(NullDisplayText = "")]
        public DateTime? Eind_datum { get; set; }

    }
}

当使用 SQL 对模型执行聚合查询时,结果不会并且通常不能轻易与原始模型相同的结构形式,您使用的 Set<T>.FromSqlRaw() 方法需要SQL解析ALL的属性为指定类型的T

FromSqlRaw Limitations

  • The SQL query must return data for all properties of the entity type.
  • The column names in the result set must match the column names that properties are mapped to. Note this behavior is different from EF6. EF6 ignored property to column mapping for raw SQL queries and result set column names had to match the property names.
  • The SQL query can't contain related data. However, in many cases you can compose on top of the query using the Include operator to return related data (see Including related data).

对于聚合查询,我们通常会定义一个新类型来保存来自 SQL 聚合的响应。在 C# 中,LINQ GroupBy 的行为与 SQL 非常不同,在 SQL 中,详细信息行被排除在外,只有聚合集被 returned。在 LINQ 中,所有的行都被保留,但是它们被键投影到 groups 中,根本没有特定的聚合,在 LINQ groupby 之后你会执行任何你可能的聚合分析要求。

我们需要做的第一件事是定义响应的结构,如下所示:

public class FactuurSamenvatting
{
    public int? Jaar { get; set; }
    public int? Maand { get; set; } 
    public int? Total { get; set; }
}

然后如果此类型已注册 DBContext 作为新 DbSet:

/// <summary>Summary of Invoice Totals by Month</summary>
public Set<FactuurSamenvatting> FacturenOmmen { get;set; }

然后您可以使用这个原始 SQL 查询:

string query = $"SELECT Year(Datum) AS Jaar, Month(Datum) AS Maand, SUM(Bedrag) AS Total FROM Facturens GROUP BY Year(Datum), Month(Datum) ORDER BY Jaar, Maand";
var grafiek = await _db.FacturenOmmen.FromSqlRaw(query).ToListAsync();


Ad-Hoc 通用解决方案

Though the above solution is encouraged, it is possible to achieve the same thing without formally adding your aggregate type directly to your DbContext. Following and his updated reference on Github we can create a dynamic context that explicitly contains the setup for any generic type

public static class SqlQueryExtensions
{
    public static IList<T> SqlQuery<T>(this DbContext db, string sql, params object[] parameters) where T : class
    {
        using (var db2 = new ContextForQueryType<T>(db.Database.GetDbConnection()))
        {
            return db2.Set<T>().FromSqlRaw(sql, parameters).ToList();
        }
    }

    private class ContextForQueryType<T> : DbContext where T : class
    {
        private readonly DbConnection connection;

        public ContextForQueryType(DbConnection connection)
        {
            this.connection = connection;
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(connection, options => options.EnableRetryOnFailure());

            base.OnConfiguring(optionsBuilder);
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<T>().HasNoKey();
            base.OnModelCreating(modelBuilder);
        }
    }
}

现在我们根本不需要 pre-register 聚合类型,您可以简单地使用此语法来执行查询:

然后您可以使用这个原始 SQL 查询:

string query = $"SELECT Year(Datum) AS Jaar, Month(Datum) AS Maand, SUM(Bedrag) AS Total FROM Facturens GROUP BY Year(Datum), Month(Datum) ORDER BY Jaar, Maand";
var grafiek = _db.SqlQuery<FactuurSamenvatting>(query).ToList();

原始回复

Updated after Factuur model posted
Below is a general walk through responding to the original post and the specific exceptions that were raised. I had originally assumed that OP was using an aggregate type definition, I had forgotten that to do so is itself an advanced technique, the following response is still helpful if you define your aggregate type correctly but still observe the same exceptions.

通常投射到 已知 类型的 LINQ 表达式会抛出两个常见错误:

InvalidOperationException: The required column 'FacturenID' was not present...

此错误相当明显,您投影到的模型 Factuur 有一个名为 FacturenID 的必填列,您的输出未提供该列。您在第一次尝试中的投影期望 Factuur:

中的这些列
public int y { get;set; }
public int m { get;set; }
public int? Total { get;set; }

如果您将第一个查询更改为使用 Factuur 中现有的匹配 属性 名称,那么您很可能仍会遇到下一个问题...

错误 InvalidOperationException: Nullable object must have a value. 出现在两种情况下:

  1. 当您的 LINQ 表达式在内存中运行并尝试访问空对象上的 属性 时,很可能在第二个查询的情况下,如果任何值都可能发生Datumnull,这会使 Datum.Value.

    无效
    • 如果在 SQL 中计算表达式,即使字段为 null 也允许使用此语法,结果将只是 null.
  2. 当一个SQL结果投影到c#类型时,当结果集中其中一列的值为null但对应的属性 您要投影到的 类型 不允许空值。

在这种情况下,jaarmaandTotal 列之一需要为空,通常它将是 SUM 聚合的结果,但在这种情况只有在 Bedrag 在您的数据集中可以为 null 时才会发生。

通过检查此记录集来测试您的数据,请注意我不是将结果转换为特定类型,我们会将它们保留在匿名类型 形式进行分析,我们也将排除空数据。对于这个测试。

var data = await _db.Facturens
                    .Where (f => f.Datum != null)
                    .GroupBy(a => new { a.Datum.Value.Year, a.Datum.Value.Month }, (key, group) => new
                        {
                            jaar = key.Year,
                            maand = key.Month,
                            Total = group.Sum(b => b.Bedrag)
                        })
                   .Select(c => new { c.jaar, c.maand, c.Total })
                   .ToListAsync();

在您的原始查询中,要考虑空值和 return Total 而不是改变您的模型以接受空值,那么您可以用这个:

string query = $"SELECT Year(Datum) AS jaar, Month(Datum) AS maand, SUM(ISNULL(Bedrag,0)) AS Total FROM Facturens GROUP BY Year(Datum), Month(Datum) ORDER BY jaar, maand";
      Grafiek = await _db.Facturens.FromSqlRaw(query).ToListAsync();

In this SQL we didn't need to exclude the null datums, these will be returned with respctive values of null for both of jaar and maand


鉴于 jaarmaand 可能 为空的唯一情况是列 Datum 具有空值,因此您可以在不修改模型的情况下使用此 SQL 到 return 与预期类型相同的列,只要这些是模型中的所有列。在这种情况下,我建议使用简单的 WHERE 子句

从结果中排除这些记录
SELECT 
     Year(Datum) AS jaar
   , Month(Datum) AS maand
   , SUM(ISNULL(Bedrag,0)) AS Total 
FROM Facturens 
WHERE Datum IS NOT NULL
GROUP BY Year(Datum), Month(Datum) ORDER BY jaar, maand