【问题标题】:Null value in group by after a left outer join in EF CoreEF Core 中的左外连接后 group by 中的 Null 值
【发布时间】:2019-07-26 15:52:04
【问题描述】:

我的 linq 查询有问题,我试图从中获取某些信息,但根本找不到问题所在。我的 ef 核心数据库中有工具、任务和一个 M-N ToolTask​​ 实体。此查询包括 2 个左外连接和最后一个 group by。

即使我正在检查分组值是否不为空,我仍然会收到一条错误消息,例如“可空对象必须有一个值”。我在这里想念什么?

此外,这个 linq 查询似乎是在客户端评估的,我能做些什么来使其成为服务器端吗?当我由于某种原因在代码中没有“let maxDateV ...”行时,它一直是服务器端。

var max = (from t in _fabContext.Tools

join tt in _fabContext.ToolTask on t.ToolId equals tt.ToolId into tt1
from tt in tt1.DefaultIfEmpty()

join ts in _fabContext.Tasks on tt.TaskId equals ts.TaskId into ts1
from ts in ts1.DefaultIfEmpty()

group tt by tt.ToolId into g
let maxOrderV = g.Max(c => c != null ? c.Order : 0)
let maxDateV = g.Max(c => c != null ? c.Task.ExportDate : DateTime.MinValue)
select new
{
  ToolId = g.Key,
  MaxOrder = maxOrderV,
  MaxExportDate = maxDateV
}).ToDictionary(d => d.ToolId, d => 
    new OrderExportDate { 
        Order = d.MaxOrder, 
        ExportDate = d.MaxExportDate 
    });

更新 1(实体类):

任务

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;

namespace Main.DataLayer.EfClasses
{
    public class Task
    {
        public int TaskId { get; private set; }
        [Required]
        public string Name { get; private set; }
        [Required]
        public int ProfileId { get; private set; }
        [Required]
        public DateTime ExportDate { get; private set; }

        private HashSet<ToolTask> _toolTask;
        public IEnumerable<ToolTask> ToolTask => _toolTask?.ToList();

        private Task()
        {
        }

        public Task(
            string name,
            int profileId,
            DateTime exportDate)
        {
            Name = name;
            ProfileId = profileId;
            ExportDate = exportDate;
        }
    }
}

工具任务

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;

namespace Main.DataLayer.EfClasses
{
    public class ToolTask
    {
        public int ToolId
        {
            get; private set;
        }

        public Tool Tool
        {
            get; private set;
        }

        public int TaskId
        {
            get; private set;
        }

        public Task Task
        {
            get; private set;
        }

        [Required]
        public int SortOrder
        {
            get; private set;
        }

        private ToolTask() { }

        internal ToolTask(Tool tool, Task task, int sortOrder)
        {
            Tool = tool;
            ToolId = tool.ToolId;
            Task = task;
            TaskId = task.TaskId;
            SortOrder = sortOrder;
        }

        public void ChangeOrder(int order)
        {
            SortOrder = order;
        }
    }
}

工具

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Main.DataLayer.EfCode;

namespace Main.DataLayer.EfClasses
{
    public class Tool
    {
        public int ToolId
        {
            get; private set;
        }

        [Required]
        public string Name
        {
            get; private set;
        }

        [Required]
        public string Model
        {
            get; private set;
        }

        private HashSet<ToolTask> _toolTask;
        public IEnumerable<ToolTask> ToolTask => _toolTask?.ToList();

        internal Tool() { }

        public Tool(string name, string model)
        {
            Name = name;
            Model = model;

            _toolTask = new HashSet<ToolTask>();
        }

        public void AddTask(Task task, int sortOrder)
        {
            _toolTask?.Add(new ToolTask(this, task, sortOrder));
        }
    }
}

每个实体类都有自己的配置类,但除了设置主键和支持字段的访问模式外,没有什么大不了的。

【问题讨论】:

  • tt 可以是null 这里是tt.ToolId
  • 这很明显,谢谢。我只是不确定如何解决它?由于我需要结果字典中的工具 ID,因此按它进行分组会消除在“let”行中的工具任务实体上使用 max 的可能性?
  • let 行使用空合并运算符“?”因此无法远程执行。
  • 您可能需要改用t.ToolId,因为已知 left 边不为空。当前的 EF Core 查询翻译中也存在一些缺陷,需要以特定方式编写 LINQ 查询,以使其完全翻译并在服务器端执行 - 例如,使用导航属性而不是手动连接,即编写方式聚合表达式等。为了给你一个具体的答案,我们需要看看你的实体模型。
  • 如果我使用 t.ToolId,我将如何访问分组中的字段?我为模型添加了实体类,但从 cmets 和不必要的字段中删除了它。你还需要什么吗?

标签: c# entity-framework entity-framework-core ef-core-2.2


【解决方案1】:

该实现的问题在于您按tt.ToolId 分组,其中tt 来自左外连接的“右侧”,因此tt 可以是null。

您可以通过使用“左侧”的相应字段轻松解决此问题,例如group tt by t.ToolId into g。

但这不会使查询完全可翻译。为此,您需要(至少使用当前的 EF Core 查询翻译器)在构建 LINQ 查询时遵循一些规则:

  1. 使用导航属性而不是手动连接。
  2. 特别是对于带有聚合的GroupBy 查询,始终使用group {element} by {key} 构造的{element} 部分来预选聚合方法中需要的所有表达式。
  3. 在 SQL(服务器端)查询中,自然支持 nulls,即使数据类型不可为空并且来自连接的可选端。但不是在 C# 中,因此您必须使用强制转换来明确指定。
  4. 始终使用Min 和Max 的可空重载。如果需要,将结果(不是操作数)转换为标记值。但最好保留它null 和可为空的类型。避免使用 DateTime.MinValue,因为它在 SqlServer 中没有表示形式。

将它们应用于您的查询:

var max =
    (from t in _fabContext.Tools
     from tt in t.ToolTask.DefaultIfEmpty()
     let ts = tt.Task
     group new
     {
         SortOrder = (int?)tt.SortOrder,
         ExportDate = (DateTime?)ts.ExportDate
     }
     by new { t.ToolId }
     into g
     select new
     {
         ToolId = g.Key.ToolId,
         MaxOrder = g.Max(e => e.SortOrder) ?? 0,
         MaxExportDate = g.Max(e => e.ExportDate)
     })
    .ToDictionary(d => d.ToolId, d => new OrderExportDate
    {
        Order = d.MaxOrder,
        ExportDate = d.MaxExportDate ?? DateTime.MinValue
    });

很好地转换为单个 SQL 查询

  SELECT [t].[ToolId], MAX([t.ToolTask].[SortOrder]) AS [MaxOrder], MAX([t.ToolTask.Task].[ExportDate]) AS [MaxExportDate]
  FROM [Tool] AS [t]
  LEFT JOIN [ToolTask] AS [t.ToolTask] ON [t].[ToolId] = [t.ToolTask].[ToolId]
  LEFT JOIN [Task] AS [t.ToolTask.Task] ON [t.ToolTask].[TaskId] = [t.ToolTask.Task].[TaskId]
  GROUP BY [t].[ToolId]

这正是具有 SQL 背景的人所期望的。

如果您不关心翻译后的 SQL 查询的形状,您可以避免 group by,而只需以简单的逻辑方式编写 LINQ 查询:

var max2 = _fabContext.Tools
    .Select(t => new
    {
        t.ToolId,
        MaxOrder = t.ToolTask.Max(tt => (int?)tt.SortOrder),
        MaxExportDate = t.ToolTask.Max(tt => (DateTime?)tt.Task.ExportDate)
    })
    .ToDictionary(d => d.ToolId, d => new OrderExportDate
    {
        Order = d.MaxOrder ?? 0,
        ExportDate = d.MaxExportDate ?? DateTime.MinValue
    });

【讨论】:

  • 这真的让我更好地理解了如何使用 ef core 和 linq 以及如何创建我的查询。谢谢!
猜你喜欢
  • 2011-02-07
  • 2019-01-15
  • 2017-12-21
  • 2022-01-16
  • 1970-01-01
  • 1970-01-01
  • 2020-07-07
  • 2018-07-09
  • 1970-01-01
相关资源
最近更新 更多