Entity Framework Core 3.1 中使用带除法的键进行分组时存在错误?
Bug in Entity Framework Core 3.1 when grouping using a key with a division?
我正在尝试使用 linq 和 entity framework 3.1 对一组数据进行分组。这是我要完成的查询:
var query = dbset
.Join(vehicles,
i => i.VehicleId,
v => v.Id,
(i, v) => new{i, v})
.Where(o=>o.v.EnterpriseId == enterpriseId && o.i.ReportDate>=startDate && o.i.ReportDate<=endDate);
dateDataQuery = query.GroupBy(o =>
new
{
Week=(int)(o.i.ReportDate.DayOfYear / 7),
o.i.ReportDate.Year
})
.OrderBy(o=> o.Key.Year).ThenBy(o=>o.Key.Week)
.Select(g => new TransferDateData()
{
Week = g.Key.Week,
Year = g.Key.Year,
Value = g.Sum(o=>o.i.Cost),
});
如您所见,我按周和年分组。问题是 DateTime 没有 Week 属性 来获取一年中可以转换为数据库过程的星期。正如我们可以用 .Month 或 .Day 做的那样。所以,必须计算周。
此查询输出相同周和年的不同结果。例如,我可以有几个结果,如:
{ Week = 20, Year=2020, Value = 20}, {Week=20, Year=2020, Value=30} ...
如您所见,它们本应分组,但实际上没有。如果我在分组中使用一周的硬编码数字,则分组有效并且所有值都被聚合并且只返回一个结果。
所以,我猜除法和浮点数结果有问题,尽管 Week 是一个整数。
以下是我使用的两个模型及其属性:
public class GenericDateData<T>
{
public T Value { get; set; }
public int Month { get; set; }
public int Year { get; set; }
public int Day { get; set; }
public int Week { get; set; }
}
public class TransferDateData : GenericDateData<float>
{
public float TotalKm { get; set; }
}
你能猜到发生了什么吗?对我来说似乎是个错误。
您的查询确实有效。正如@PanagiotisKanavos 在评论中提到的,它 不准确 ,但它 确实将 转换为有效的 SQL:
using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
public class Incident
{
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime ReportDate { get; set; }
public float Cost { get; set; }
}
public class Vehicle
{
public int Id { get; set; }
public int EnterpriseId { get; set; }
}
public class Enterprise
{
public int Id { get; set; }
}
public class GenericDateData<T>
{
public T Value { get; set; }
public int Month { get; set; }
public int Year { get; set; }
public int Day { get; set; }
public int Week { get; set; }
}
public class TransferDateData : GenericDateData<float>
{
public float TotalKm { get; set; }
}
public class Context : DbContext
{
public DbSet<Incident> Incidents { get; set; }
public DbSet<Vehicle> Vehicles { get; set; }
public DbSet<Enterprise> Enterprises { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(
@"Data Source=.\MSSQL14;Integrated Security=SSPI;Initial Catalog=So63118907")
.UseLoggerFactory(
LoggerFactory.Create(
b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Incident>().HasData(
new Incident {Id = 1, VehicleId = 1, ReportDate = new DateTime(2020, 1, 1), Cost = 42.00f},
new Incident {Id = 2, VehicleId = 2, ReportDate = new DateTime(2020, 2, 1), Cost = 21.00f});
modelBuilder.Entity<Vehicle>().HasData(
new Vehicle {Id = 1, EnterpriseId = 1},
new Vehicle {Id = 2, EnterpriseId = 1});
modelBuilder.Entity<Enterprise>().HasData(
new Enterprise {Id = 1});
}
}
internal static class Program
{
private static void Main()
{
using var context = new Context();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var enterpriseId = 1;
var startDate = new DateTime(2020, 1, 1);
var endDate = new DateTime(2021, 12, 31);
var costPerWeek = context.Incidents
.Join(
context.Vehicles,
i => i.VehicleId,
v => v.Id,
(i, v) => new {i, v})
.Where(
o => o.v.EnterpriseId == enterpriseId &&
o.i.ReportDate >= startDate &&
o.i.ReportDate <= endDate)
.GroupBy(
o =>
new
{
Week = (int) (o.i.ReportDate.DayOfYear / 7),
o.i.ReportDate.Year
})
.OrderBy(o => o.Key.Year)
.ThenBy(o => o.Key.Week)
.Select(
g => new TransferDateData()
{
Week = g.Key.Week,
Year = g.Key.Year,
Value = g.Sum(o => o.i.Cost),
})
.ToList();
Debug.Assert(costPerWeek.Count == 2);
Debug.Assert(costPerWeek[0].Week == 0);
Debug.Assert(costPerWeek[0].Value == 42.00f);
Debug.Assert(costPerWeek[1].Week == 4);
Debug.Assert(costPerWeek[1].Value == 21.00f);
}
}
}
查询被翻译成以下 SQL:
SELECT DATEPART(dayofyear, [i].[ReportDate]) / 7 AS [Week], DATEPART(year, [i].[ReportDate]) AS [Year], CAST(SUM([i].[Cost]) AS real) AS [Value]
FROM [Incidents] AS [i]
INNER JOIN [Vehicles] AS [v] ON [i].[VehicleId] = [v].[Id]
WHERE (([v].[EnterpriseId] = @__enterpriseId_0) AND ([i].[ReportDate] >= @__startDate_1)) AND ([i].[ReportDate] <= @__endDate_2)
GROUP BY DATEPART(dayofyear, [i].[ReportDate]) / 7, DATEPART(year, [i].[ReportDate])
ORDER BY DATEPART(year, [i].[ReportDate]), DATEPART(dayofyear, [i].[ReportDate]) / 7
它目前从 0
开始计算周数,尽管如此,日期可能会被一些人关闭(取决于日历周在该应用程序所使用的文化中的计算方式)天。
更简单、更准确的解决方案可能是使用 DATEPART(week, @date) T-SQL 函数。不幸的是,目前没有 .NET Core 方法可以转换为这个 SQL 函数。
但是您可以定义一个 UDF(用户定义的函数),将调用重定向到 DATEPART
。
下面的示例不仅定义并使用了 UDF,还展示了如何使用导航属性而不是显式连接:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
public class Incident
{
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime ReportDate { get; set; }
public float Cost { get; set; }
public Vehicle Vehicle { get; set; }
public Enterprise Enterprise { get; set; }
}
public class Vehicle
{
public int Id { get; set; }
public int EnterpriseId { get; set; }
public virtual ICollection<Incident> Incidents { get; set; } = new HashSet<Incident>();
}
public class Enterprise
{
public int Id { get; set; }
}
public class GenericDateData<T>
{
public T Value { get; set; }
public int Month { get; set; }
public int Year { get; set; }
public int Day { get; set; }
public int Week { get; set; }
}
public class TransferDateData : GenericDateData<float>
{
public float TotalKm { get; set; }
}
public class Context : DbContext
{
public DbSet<Incident> Incidents { get; set; }
public DbSet<Vehicle> Vehicles { get; set; }
public DbSet<Enterprise> Enterprises { get; set; }
// Define your UDF:
[DbFunction("GetCalendarWeek")]
public static int GetCalendarWeek(DateTime date)
=> throw new InvalidOperationException("Should never be called but only translated to SQL.");
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(
@"Data Source=.\MSSQL14;Integrated Security=SSPI;Initial Catalog=So63118907_01")
.UseLoggerFactory(
LoggerFactory.Create(
b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Incident>().HasData(
new Incident {Id = 1, VehicleId = 1, ReportDate = new DateTime(2020, 1, 1), Cost = 42.00f},
new Incident {Id = 2, VehicleId = 2, ReportDate = new DateTime(2020, 2, 1), Cost = 21.00f});
modelBuilder.Entity<Vehicle>().HasData(
new Vehicle {Id = 1, EnterpriseId = 1},
new Vehicle {Id = 2, EnterpriseId = 1});
modelBuilder.Entity<Enterprise>().HasData(
new Enterprise {Id = 1});
}
}
internal static class Program
{
private static void Main()
{
using var context = new Context();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
// Since this example recreates the database on each run,
// lets create our UDF here as well:
context.Database.ExecuteSqlRaw(@"
CREATE FUNCTION [GetCalendarWeek] (@date DATETIME)
RETURNS INT AS
BEGIN
RETURN DATEPART(week, @date);
END");
var enterpriseId = 1;
var startDate = new DateTime(2020, 1, 1);
var endDate = new DateTime(2021, 12, 31);
var costPerWeek = context.Incidents
.Include(i => i.Vehicle)
.Include(i => i.Enterprise)
.Where(
i => i.Vehicle.EnterpriseId == enterpriseId &&
i.ReportDate >= startDate &&
i.ReportDate <= endDate)
.GroupBy(
i =>
new
{
// Let's call our UDF here:
Week = Context.GetCalendarWeek(i.ReportDate),
i.ReportDate.Year
})
.OrderBy(o => o.Key.Year)
.ThenBy(o => o.Key.Week)
.Select(
g => new TransferDateData()
{
Week = g.Key.Week,
Year = g.Key.Year,
Value = g.Sum(i => i.Cost),
})
.ToList();
Debug.Assert(costPerWeek.Count == 2);
Debug.Assert(costPerWeek[0].Week == 1);
Debug.Assert(costPerWeek[0].Value == 42.00f);
Debug.Assert(costPerWeek[1].Week == 5);
Debug.Assert(costPerWeek[1].Value == 21.00f);
}
}
}
周数现在是正确的(取决于您的文化)。
生成的SQL现在比较简单:
CREATE FUNCTION [GetCalendarWeek] (@date DATETIME)
RETURNS INT AS
BEGIN
RETURN DATEPART(week, @date);
END
SELECT [dbo].[GetCalendarWeek]([i].[ReportDate]) AS [Week], DATEPART(year, [i].[ReportDate]) AS [Year], CAST(SUM([i].[Cost]) AS real) AS [Value]
FROM [Incidents] AS [i]
INNER JOIN [Vehicles] AS [v] ON [i].[VehicleId] = [v].[Id]
我正在尝试使用 linq 和 entity framework 3.1 对一组数据进行分组。这是我要完成的查询:
var query = dbset
.Join(vehicles,
i => i.VehicleId,
v => v.Id,
(i, v) => new{i, v})
.Where(o=>o.v.EnterpriseId == enterpriseId && o.i.ReportDate>=startDate && o.i.ReportDate<=endDate);
dateDataQuery = query.GroupBy(o =>
new
{
Week=(int)(o.i.ReportDate.DayOfYear / 7),
o.i.ReportDate.Year
})
.OrderBy(o=> o.Key.Year).ThenBy(o=>o.Key.Week)
.Select(g => new TransferDateData()
{
Week = g.Key.Week,
Year = g.Key.Year,
Value = g.Sum(o=>o.i.Cost),
});
如您所见,我按周和年分组。问题是 DateTime 没有 Week 属性 来获取一年中可以转换为数据库过程的星期。正如我们可以用 .Month 或 .Day 做的那样。所以,必须计算周。
此查询输出相同周和年的不同结果。例如,我可以有几个结果,如:
{ Week = 20, Year=2020, Value = 20}, {Week=20, Year=2020, Value=30} ...
如您所见,它们本应分组,但实际上没有。如果我在分组中使用一周的硬编码数字,则分组有效并且所有值都被聚合并且只返回一个结果。
所以,我猜除法和浮点数结果有问题,尽管 Week 是一个整数。
以下是我使用的两个模型及其属性:
public class GenericDateData<T>
{
public T Value { get; set; }
public int Month { get; set; }
public int Year { get; set; }
public int Day { get; set; }
public int Week { get; set; }
}
public class TransferDateData : GenericDateData<float>
{
public float TotalKm { get; set; }
}
你能猜到发生了什么吗?对我来说似乎是个错误。
您的查询确实有效。正如@PanagiotisKanavos 在评论中提到的,它 不准确 ,但它 确实将 转换为有效的 SQL:
using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
public class Incident
{
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime ReportDate { get; set; }
public float Cost { get; set; }
}
public class Vehicle
{
public int Id { get; set; }
public int EnterpriseId { get; set; }
}
public class Enterprise
{
public int Id { get; set; }
}
public class GenericDateData<T>
{
public T Value { get; set; }
public int Month { get; set; }
public int Year { get; set; }
public int Day { get; set; }
public int Week { get; set; }
}
public class TransferDateData : GenericDateData<float>
{
public float TotalKm { get; set; }
}
public class Context : DbContext
{
public DbSet<Incident> Incidents { get; set; }
public DbSet<Vehicle> Vehicles { get; set; }
public DbSet<Enterprise> Enterprises { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(
@"Data Source=.\MSSQL14;Integrated Security=SSPI;Initial Catalog=So63118907")
.UseLoggerFactory(
LoggerFactory.Create(
b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Incident>().HasData(
new Incident {Id = 1, VehicleId = 1, ReportDate = new DateTime(2020, 1, 1), Cost = 42.00f},
new Incident {Id = 2, VehicleId = 2, ReportDate = new DateTime(2020, 2, 1), Cost = 21.00f});
modelBuilder.Entity<Vehicle>().HasData(
new Vehicle {Id = 1, EnterpriseId = 1},
new Vehicle {Id = 2, EnterpriseId = 1});
modelBuilder.Entity<Enterprise>().HasData(
new Enterprise {Id = 1});
}
}
internal static class Program
{
private static void Main()
{
using var context = new Context();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var enterpriseId = 1;
var startDate = new DateTime(2020, 1, 1);
var endDate = new DateTime(2021, 12, 31);
var costPerWeek = context.Incidents
.Join(
context.Vehicles,
i => i.VehicleId,
v => v.Id,
(i, v) => new {i, v})
.Where(
o => o.v.EnterpriseId == enterpriseId &&
o.i.ReportDate >= startDate &&
o.i.ReportDate <= endDate)
.GroupBy(
o =>
new
{
Week = (int) (o.i.ReportDate.DayOfYear / 7),
o.i.ReportDate.Year
})
.OrderBy(o => o.Key.Year)
.ThenBy(o => o.Key.Week)
.Select(
g => new TransferDateData()
{
Week = g.Key.Week,
Year = g.Key.Year,
Value = g.Sum(o => o.i.Cost),
})
.ToList();
Debug.Assert(costPerWeek.Count == 2);
Debug.Assert(costPerWeek[0].Week == 0);
Debug.Assert(costPerWeek[0].Value == 42.00f);
Debug.Assert(costPerWeek[1].Week == 4);
Debug.Assert(costPerWeek[1].Value == 21.00f);
}
}
}
查询被翻译成以下 SQL:
SELECT DATEPART(dayofyear, [i].[ReportDate]) / 7 AS [Week], DATEPART(year, [i].[ReportDate]) AS [Year], CAST(SUM([i].[Cost]) AS real) AS [Value]
FROM [Incidents] AS [i]
INNER JOIN [Vehicles] AS [v] ON [i].[VehicleId] = [v].[Id]
WHERE (([v].[EnterpriseId] = @__enterpriseId_0) AND ([i].[ReportDate] >= @__startDate_1)) AND ([i].[ReportDate] <= @__endDate_2)
GROUP BY DATEPART(dayofyear, [i].[ReportDate]) / 7, DATEPART(year, [i].[ReportDate])
ORDER BY DATEPART(year, [i].[ReportDate]), DATEPART(dayofyear, [i].[ReportDate]) / 7
它目前从 0
开始计算周数,尽管如此,日期可能会被一些人关闭(取决于日历周在该应用程序所使用的文化中的计算方式)天。
更简单、更准确的解决方案可能是使用 DATEPART(week, @date) T-SQL 函数。不幸的是,目前没有 .NET Core 方法可以转换为这个 SQL 函数。
但是您可以定义一个 UDF(用户定义的函数),将调用重定向到 DATEPART
。
下面的示例不仅定义并使用了 UDF,还展示了如何使用导航属性而不是显式连接:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
public class Incident
{
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime ReportDate { get; set; }
public float Cost { get; set; }
public Vehicle Vehicle { get; set; }
public Enterprise Enterprise { get; set; }
}
public class Vehicle
{
public int Id { get; set; }
public int EnterpriseId { get; set; }
public virtual ICollection<Incident> Incidents { get; set; } = new HashSet<Incident>();
}
public class Enterprise
{
public int Id { get; set; }
}
public class GenericDateData<T>
{
public T Value { get; set; }
public int Month { get; set; }
public int Year { get; set; }
public int Day { get; set; }
public int Week { get; set; }
}
public class TransferDateData : GenericDateData<float>
{
public float TotalKm { get; set; }
}
public class Context : DbContext
{
public DbSet<Incident> Incidents { get; set; }
public DbSet<Vehicle> Vehicles { get; set; }
public DbSet<Enterprise> Enterprises { get; set; }
// Define your UDF:
[DbFunction("GetCalendarWeek")]
public static int GetCalendarWeek(DateTime date)
=> throw new InvalidOperationException("Should never be called but only translated to SQL.");
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(
@"Data Source=.\MSSQL14;Integrated Security=SSPI;Initial Catalog=So63118907_01")
.UseLoggerFactory(
LoggerFactory.Create(
b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Incident>().HasData(
new Incident {Id = 1, VehicleId = 1, ReportDate = new DateTime(2020, 1, 1), Cost = 42.00f},
new Incident {Id = 2, VehicleId = 2, ReportDate = new DateTime(2020, 2, 1), Cost = 21.00f});
modelBuilder.Entity<Vehicle>().HasData(
new Vehicle {Id = 1, EnterpriseId = 1},
new Vehicle {Id = 2, EnterpriseId = 1});
modelBuilder.Entity<Enterprise>().HasData(
new Enterprise {Id = 1});
}
}
internal static class Program
{
private static void Main()
{
using var context = new Context();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
// Since this example recreates the database on each run,
// lets create our UDF here as well:
context.Database.ExecuteSqlRaw(@"
CREATE FUNCTION [GetCalendarWeek] (@date DATETIME)
RETURNS INT AS
BEGIN
RETURN DATEPART(week, @date);
END");
var enterpriseId = 1;
var startDate = new DateTime(2020, 1, 1);
var endDate = new DateTime(2021, 12, 31);
var costPerWeek = context.Incidents
.Include(i => i.Vehicle)
.Include(i => i.Enterprise)
.Where(
i => i.Vehicle.EnterpriseId == enterpriseId &&
i.ReportDate >= startDate &&
i.ReportDate <= endDate)
.GroupBy(
i =>
new
{
// Let's call our UDF here:
Week = Context.GetCalendarWeek(i.ReportDate),
i.ReportDate.Year
})
.OrderBy(o => o.Key.Year)
.ThenBy(o => o.Key.Week)
.Select(
g => new TransferDateData()
{
Week = g.Key.Week,
Year = g.Key.Year,
Value = g.Sum(i => i.Cost),
})
.ToList();
Debug.Assert(costPerWeek.Count == 2);
Debug.Assert(costPerWeek[0].Week == 1);
Debug.Assert(costPerWeek[0].Value == 42.00f);
Debug.Assert(costPerWeek[1].Week == 5);
Debug.Assert(costPerWeek[1].Value == 21.00f);
}
}
}
周数现在是正确的(取决于您的文化)。
生成的SQL现在比较简单:
CREATE FUNCTION [GetCalendarWeek] (@date DATETIME)
RETURNS INT AS
BEGIN
RETURN DATEPART(week, @date);
END
SELECT [dbo].[GetCalendarWeek]([i].[ReportDate]) AS [Week], DATEPART(year, [i].[ReportDate]) AS [Year], CAST(SUM([i].[Cost]) AS real) AS [Value]
FROM [Incidents] AS [i]
INNER JOIN [Vehicles] AS [v] ON [i].[VehicleId] = [v].[Id]