EF Core中的左外部联接后,group by中的空值

时间:2019-03-05 08:30:59

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

我在使用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;
        }
    }
}

ToolTask​​

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));
        }
    }
}

每个实体类都有自己的配置类,但是除了为备份字段设置主键和访问模式外,没有什么其他重要的东西。

1 个答案:

答案 0 :(得分:2)

该实现的问题是您按tt.ToolId分组,其中tt来自左外部联接的“右侧”,因此tt可以是{{1 }}。

您可以通过使用左侧的相应字段轻松地解决此问题,例如null

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

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

将它们应用于您的查询:

DateTime.MinValue

可以很好地转换为单个SQL查询

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背景的人所期望的。

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

  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]